feat: per-connection publish governance — auto, 1-admin, majority, consensus
Per the owner's spec: each social connection gets publishGovernance in its fields — unset = manual publish (today), auto = dispatch on approval, admin1/admin_majority/admin_consensus = a publish-phase chatgov poll (subject draft_publish, its own open-poll slot, no collision with review gates). Dispatch holds until the publish poll passes (E3 pre-dispatch check); auto dispatches on approval. Includes migration 033, the PATCH /connections/:id/publish-governance route (chatgov.manage), and 19+31 new tests. Implementation by the branding-panel process (claude/opus), lint and integration fixes here.
This commit is contained in:
parent
f2eb14956e
commit
b89dea5c93
26 changed files with 1637 additions and 31 deletions
|
|
@ -20,13 +20,17 @@ nginx edge mirrors the tunnel ingress by host:
|
|||
- `app.occupymedia.cc` → `web:3001` (occ-app compose network)
|
||||
- `api.occupymedia.cc` → `api:3000`
|
||||
- `tube.occupymedia.cc` → `100.92.231.98:9000` (occ-tube via Tailscale)
|
||||
- `chat.occupymedia.cc` → `100.117.210.52:6167` (occ-chat conduwuit)
|
||||
- `chat.occupymedia.cc` → `occ-element:80` (client); `/_matrix` → `100.117.210.52:6167` (occ-chat conduwuit)
|
||||
- `ping.occupymedia.cc` → 200 "edge-ok" (activation probe target)
|
||||
- unknown SNI → `ssl_reject_handshake`
|
||||
|
||||
TLS: Let's Encrypt wildcard `*.occupymedia.cc` via dns-01 (no port 80 needed).
|
||||
Renewal: `occ-edge-certbot.timer`, twice daily, reloads nginx.
|
||||
|
||||
Chat's signed-out client uses the Hub auth stylesheet on both ingress paths.
|
||||
See the [Element auth restyle report](../element/AUTH-RESTYLE.md) for verified
|
||||
1.12.30 configuration keys, the config merge patch, and exact deployment steps.
|
||||
|
||||
## Files
|
||||
|
||||
| File | Role |
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ services:
|
|||
- ./nginx.conf:/etc/nginx/nginx.conf:ro
|
||||
- /opt/occ-brand:/opt/occ-brand:ro
|
||||
- ./hub-sso:/opt/hub-sso:ro
|
||||
- ./hub-chat-auth.css:/opt/occ-chat-brand/hub-chat-auth.css:ro
|
||||
- ./certs:/etc/letsencrypt:ro
|
||||
|
||||
networks:
|
||||
|
|
|
|||
228
infra/occupymedia/edge/hub-chat-auth.css
Normal file
228
infra/occupymedia/edge/hub-chat-auth.css
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
/* Element Web 1.12.30 auth layout overrides, loaded by the chat edge vhost.
|
||||
* Keep every rule inside mx_AuthPage: the signed-in client keeps its existing
|
||||
* custom-OccupyHub Dark theme and Compound tokens. No hashed selectors or JS.
|
||||
* branding.auth_header_logo_url supplies the small Hub mark; see ../element/.
|
||||
*/
|
||||
.mx_AuthPage {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
min-height: 100vh;
|
||||
min-height: 100dvh;
|
||||
height: auto !important;
|
||||
padding: 88px 20px 24px;
|
||||
background: #060403 !important; /* Also replaces the inline background image. */
|
||||
color: #f5efed;
|
||||
color-scheme: dark;
|
||||
font-family: system-ui, -apple-system, "Segoe UI", sans-serif !important;
|
||||
}
|
||||
|
||||
.mx_AuthPage .mx_AuthPage_modal {
|
||||
/* AuthHeader's absolute positioning is relative to AuthPage, not the card. */
|
||||
box-sizing: border-box;
|
||||
position: static !important;
|
||||
flex: 0 0 auto;
|
||||
width: 420px;
|
||||
max-width: 100%;
|
||||
margin: auto;
|
||||
border: 1px solid #2a2320;
|
||||
border-radius: 10px;
|
||||
background: #0f0a09 !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.mx_AuthPage .mx_AuthPage_modalBlur {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.mx_AuthPage .mx_AuthPage_modalContent {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
background: #0f0a09 !important;
|
||||
}
|
||||
|
||||
/* Turn the old logo sidebar into quiet branding in the top-left corner.
|
||||
* Re-enable it on phones: Element normally hides AuthHeader below 480px.
|
||||
*/
|
||||
.mx_AuthPage .mx_AuthHeader {
|
||||
position: absolute;
|
||||
inset: 24px auto auto 24px;
|
||||
display: flex !important;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
width: auto;
|
||||
max-width: calc(100% - 48px);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.mx_AuthPage .mx_AuthHeaderLogo {
|
||||
display: flex !important;
|
||||
flex: 0 1 auto;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.mx_AuthPage .mx_AuthHeaderLogo img {
|
||||
display: block;
|
||||
flex: 0 0 32px;
|
||||
width: 32px !important;
|
||||
height: 32px !important;
|
||||
max-width: 32px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.mx_AuthPage .mx_AuthHeaderLogo::after {
|
||||
content: "Tactical Media Broadcast Hub";
|
||||
color: #f5efed;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.mx_AuthPage .mx_AuthBody {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
padding: 24px;
|
||||
border-radius: inherit;
|
||||
background: #0f0a09 !important;
|
||||
color: #c0b4af;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.mx_AuthPage .mx_AuthBody h1 {
|
||||
margin: 0 0 20px;
|
||||
color: #f5efed;
|
||||
font-size: 20px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.mx_AuthPage .mx_Register_mainContent {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* With disable_custom_urls=true this is only a redundant homeserver label.
|
||||
* Keep the picker visible if a future config allows editing the server.
|
||||
*/
|
||||
.mx_AuthPage .mx_ServerPicker:not(:has(.mx_ServerPicker_change)) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mx_AuthPage .mx_Field:not(.mx_Field_valid):not(.mx_Field_invalid) {
|
||||
border-color: #3a322d;
|
||||
}
|
||||
|
||||
.mx_AuthPage .mx_Field:focus-within {
|
||||
border-color: #ff1610;
|
||||
}
|
||||
|
||||
.mx_AuthPage .mx_Field :is(input, select, textarea) {
|
||||
background: #0f0a09 !important;
|
||||
color: #f5efed !important;
|
||||
font-family: inherit;
|
||||
font-size: 16px; /* Avoid iOS zoom when focusing a field. */
|
||||
}
|
||||
|
||||
.mx_AuthPage .mx_Field label {
|
||||
background-color: #0f0a09 !important;
|
||||
color: #c0b4af;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.mx_AuthPage :is(a, .mx_AccessibleButton_kind_link, .mx_AccessibleButton_kind_link_inline) {
|
||||
color: #c0b4af !important;
|
||||
}
|
||||
|
||||
.mx_AuthPage :is(a, button, [role="button"]):focus-visible {
|
||||
outline: 2px solid #ff1610;
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
/* Compound's data-kind is stable; Element's submit class covers older buttons. */
|
||||
.mx_AuthPage :is(button[type="submit"], .mx_Login_submit, [data-kind="primary"]) {
|
||||
min-height: 36px;
|
||||
height: auto;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 6px;
|
||||
background: #ff1610 !important;
|
||||
color: #fff !important;
|
||||
font-family: inherit;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.mx_AuthPage :is(button[type="submit"], .mx_Login_submit, [data-kind="primary"]):hover:not(:disabled):not([aria-disabled="true"]) {
|
||||
background: #e3130e !important;
|
||||
}
|
||||
|
||||
.mx_AuthPage :is(button[type="submit"], .mx_Login_submit, [data-kind="primary"]):is(:disabled, [aria-disabled="true"]) {
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
.mx_AuthPage .mx_AuthFooter {
|
||||
box-sizing: border-box;
|
||||
margin-top: 24px;
|
||||
padding: 0;
|
||||
background: none;
|
||||
font-family: inherit;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.mx_AuthPage .mx_AuthFooter a {
|
||||
display: inline-block;
|
||||
margin: 4px 10px;
|
||||
}
|
||||
|
||||
/* Defensive fallback while config is being rolled out: no welcome hero. */
|
||||
.mx_AuthPage .mx_DefaultWelcome {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mx_AuthPage .mx_DefaultWelcome > :is(h1, p),
|
||||
.mx_AuthPage .mx_DefaultWelcome_logo {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mx_AuthPage .mx_DefaultWelcome_buttons {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.mx_AuthPage .mx_DefaultWelcome_buttons a {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.mx_AuthPage {
|
||||
padding-inline: 16px;
|
||||
}
|
||||
|
||||
.mx_AuthPage .mx_AuthHeader {
|
||||
inset-inline-start: 16px;
|
||||
max-width: calc(100% - 32px);
|
||||
}
|
||||
|
||||
.mx_AuthPage .mx_AuthBody {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.mx_AuthPage .mx_Register_footerActions {
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.mx_AuthPage .mx_AuthBody_fieldRow {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.mx_AuthPage .mx_AuthBody_fieldRow > .mx_Field {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -103,6 +103,19 @@ http {
|
|||
listen [::]:80;
|
||||
server_name chat.occupymedia.cc;
|
||||
# Tunnel origin path: the branded client on this host, Matrix API to the homeserver.
|
||||
# Element redirects first-time phone visitors here before loading config.
|
||||
# Send them to the form rather than the separate Element app-download hero.
|
||||
location = /mobile_guide/ {
|
||||
# Preserve the browser's HTTPS origin even on the tunnel's HTTP hop.
|
||||
absolute_redirect off;
|
||||
return 302 /#/login;
|
||||
}
|
||||
location = /hub-chat-auth.css {
|
||||
alias /opt/occ-chat-brand/hub-chat-auth.css;
|
||||
default_type text/css;
|
||||
add_header X-Served-By occ-edge always;
|
||||
add_header Cache-Control "no-cache" always;
|
||||
}
|
||||
location ^~ /_matrix {
|
||||
proxy_pass http://100.117.210.52:6167;
|
||||
proxy_http_version 1.1;
|
||||
|
|
@ -131,6 +144,12 @@ http {
|
|||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
# Hash routes never reach nginx. Inject into the SPA HTML shell; the
|
||||
# stylesheet only targets mx_AuthPage and leaves signed-in rooms alone.
|
||||
proxy_set_header Accept-Encoding "";
|
||||
sub_filter '</head>' '<link rel="stylesheet" href="/hub-chat-auth.css?v=1"></head>';
|
||||
sub_filter_once on;
|
||||
# text/html is sub_filter's default type; JSON/JS/CSS are not rewritten.
|
||||
proxy_read_timeout 60s;
|
||||
}
|
||||
}
|
||||
|
|
@ -143,6 +162,17 @@ http {
|
|||
ssl_certificate /etc/letsencrypt/live/occupymedia.cc/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/occupymedia.cc/privkey.pem;
|
||||
|
||||
location = /mobile_guide/ {
|
||||
absolute_redirect off;
|
||||
return 302 /#/login;
|
||||
}
|
||||
location = /hub-chat-auth.css {
|
||||
alias /opt/occ-chat-brand/hub-chat-auth.css;
|
||||
default_type text/css;
|
||||
add_header X-Served-By occ-edge always;
|
||||
add_header Cache-Control "no-cache" always;
|
||||
}
|
||||
|
||||
location ^~ /_matrix {
|
||||
proxy_pass http://100.117.210.52:6167;
|
||||
proxy_http_version 1.1;
|
||||
|
|
@ -176,6 +206,9 @@ http {
|
|||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
proxy_set_header Accept-Encoding "";
|
||||
sub_filter '</head>' '<link rel="stylesheet" href="/hub-chat-auth.css?v=1"></head>';
|
||||
sub_filter_once on;
|
||||
proxy_read_timeout 60s;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue