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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
215
infra/occupymedia/element/AUTH-RESTYLE.md
Normal file
215
infra/occupymedia/element/AUTH-RESTYLE.md
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
# Chat sign-in restyle and deployment report
|
||||
|
||||
Repo implementation only; no production connection, deployment, or git commit
|
||||
was made. The server-owned `/opt/occ-element/config.json` is not in this repo.
|
||||
The supplied patch merges into that file rather than replacing its configuration.
|
||||
|
||||
Signed-out visitors opening `chat.occupymedia.cc` will see a small Hub mark and
|
||||
wordmark in the top-left, a centered card containing **Sign in**, username and
|
||||
password fields, and a red submit button. Password recovery and account creation
|
||||
remain available as quiet links. Registration uses the same compact card. The
|
||||
background is `#060403`, the card is `#0F0A09`, and the font is system-ui. The
|
||||
mark is capped at 32px; there is no logo sidebar, background artwork, or hero.
|
||||
Long forms and errors scroll on small screens rather than overlapping the brand.
|
||||
Element's footer remains small and muted.
|
||||
|
||||
## Verified Element Web 1.12.30 behavior
|
||||
|
||||
No Element source or config template was found in the repository or its installed
|
||||
dependencies. Research used the official **v1.12.30** source archive and the
|
||||
matching `ghcr.io/element-hq/element-web:v1.12.30` image, in temporary local files.
|
||||
|
||||
| Configuration | Effect in 1.12.30 |
|
||||
| --- | --- |
|
||||
| `branding.auth_header_logo_url` | Image source in login/registration headers and the default welcome component. This is nested under `branding`, not a top-level key. |
|
||||
| `branding.logo_link_url` | Link destination where Element actually renders a linked logo, such as default welcome. |
|
||||
| `auth_header_logo_click_url` | Does not exist in this release. The login/registration header renders an unlinked image; a config option cannot make that header clickable. |
|
||||
| `brand` | App name used in translated text; does not change the header's layout or image. |
|
||||
| `branding.welcome_background_url` | Background artwork across auth pages. The injected CSS replaces the inline background with a solid color. |
|
||||
| `embedded_pages.login_for_welcome` | Uses the login form for the signed-out landing page, bypassing default/custom welcome content. |
|
||||
| `default_server_config` | Selects the homeserver and identity-server connection details; does not control branding or layout. Left intact. |
|
||||
| `disable_custom_urls` | Removes server editing. The CSS hides the redundant server label only when the edit control is absent. |
|
||||
| `disable_3pid_login` | Removes the username/email/phone selector, leaving username/password. |
|
||||
| `disable_login_language_selector` | Removes the language dropdown from the auth header. |
|
||||
|
||||
See the pinned [configuration docs](https://github.com/element-hq/element-web/blob/v1.12.30/docs/config.md),
|
||||
[AuthHeaderLogo source](https://github.com/element-hq/element-web/blob/v1.12.30/apps/web/src/components/views/auth/AuthHeaderLogo.tsx),
|
||||
and [DefaultWelcome source](https://github.com/element-hq/element-web/blob/v1.12.30/apps/web/src/components/views/auth/DefaultWelcome.tsx).
|
||||
|
||||
The structure is `.mx_AuthPage` → `.mx_AuthPage_modal` →
|
||||
`main.mx_AuthPage_modalContent`, with `.mx_AuthHeader` and `.mx_AuthBody` as
|
||||
siblings. The large logo comes from `.mx_AuthHeaderLogo > img` inside a 206px
|
||||
sidebar; the original body is 500px wide. The wrapper also renders a blur layer
|
||||
and `.mx_AuthFooter`. See [AuthPage](https://github.com/element-hq/element-web/blob/v1.12.30/apps/web/src/components/views/auth/AuthPage.tsx)
|
||||
and [upstream auth CSS](https://github.com/element-hq/element-web/tree/v1.12.30/apps/web/res/css/views/auth).
|
||||
There is no `.mx_AuthPage_logo` in this release's auth structure.
|
||||
|
||||
`setting_defaults.custom_themes` accepts theme colors, fonts and Compound token
|
||||
overrides, not an arbitrary CSS stylesheet URL. `source_url` records a theme
|
||||
JSON's origin, not a CSS file. The injected link intentionally has no
|
||||
`data-mx-theme`, so Element's theme switching will not disable it. See the
|
||||
[theming docs](https://github.com/element-hq/element-web/blob/v1.12.30/docs/theming.md)
|
||||
and [theme loader/types](https://github.com/element-hq/element-web/blob/v1.12.30/apps/web/src/theme.ts).
|
||||
|
||||
On first visits from iOS/Android, Element redirects a bare `/` to
|
||||
`/mobile_guide/` **before** loading config. Both chat vhosts redirect that
|
||||
app-download page to `/#/login`, which bypasses the mobile redirect. Disabling
|
||||
`mobile_guide_toast` also removes the later mobile-app prompt. See the pinned
|
||||
[startup source](https://github.com/element-hq/element-web/blob/v1.12.30/apps/web/src/vector/index.ts).
|
||||
|
||||
## Files changed
|
||||
|
||||
| File | Purpose |
|
||||
| --- | --- |
|
||||
| [../edge/hub-chat-auth.css](../edge/hub-chat-auth.css) | Scoped auth overrides: corner branding, compact responsive card, warm colors, focus styles and red primary actions. Uses stable `mx_*` classes, structural selectors and Compound `data-kind`, with no hashed class selectors. |
|
||||
| [../edge/nginx.conf](../edge/nginx.conf) | Both HTTP tunnel and HTTPS chat vhosts serve `/hub-chat-auth.css` and inject its link once before `</head>` in HTML. Clears upstream Accept-Encoding so sub_filter can read HTML. Routes the mobile guide to login. Matrix API and SSO helper keep their own locations. |
|
||||
| [../edge/docker-compose.edge.yml](../edge/docker-compose.edge.yml) | Read-only bind mount for the stylesheet. |
|
||||
| [auth-branding.patch.json](auth-branding.patch.json) | Hub logo URL, supported logo-link setting, direct login landing and removal of extra login selectors. |
|
||||
| [apply-auth-branding.py](apply-auth-branding.py) | Merge/preview utility. Preserves unrelated settings and writes in place to retain Docker's single-file bind mount. |
|
||||
| [../edge/README.md](../edge/README.md) | Links this report and corrects the documented chat routing. |
|
||||
| [AUTH-RESTYLE.md](AUTH-RESTYLE.md) | This research and deployment report. |
|
||||
|
||||
The stylesheet is downloaded with the SPA shell; nginx cannot see hash routes.
|
||||
It applies only within `.mx_AuthPage`, including recovery/auth verification
|
||||
screens using that wrapper. Signed-in room UI keeps the existing custom theme.
|
||||
The `element.occupymedia.cc` alias vhost is not changed.
|
||||
|
||||
## Exact deployment steps (operator runs these; not executed here)
|
||||
|
||||
Use the existing CT 150 checkout at `/opt/anarchoposter`. Run server commands
|
||||
with permission to edit `/opt/occ-element/config.json` and manage Docker.
|
||||
|
||||
1. **On the server, before copying files**, back up the config and edge files.
|
||||
Keep this shell open so `occ_auth_backup` remains available for later steps.
|
||||
|
||||
```bash
|
||||
occ_auth_backup="/opt/occ-element/backups/hub-auth-$(date -u +%Y%m%dT%H%M%SZ)"
|
||||
mkdir -p "$occ_auth_backup"
|
||||
cp -p /opt/occ-element/config.json "$occ_auth_backup/config.json"
|
||||
cp -p /opt/anarchoposter/infra/occupymedia/edge/nginx.conf "$occ_auth_backup/nginx.conf"
|
||||
cp -p /opt/anarchoposter/infra/occupymedia/edge/docker-compose.edge.yml "$occ_auth_backup/docker-compose.edge.yml"
|
||||
if test -f /opt/anarchoposter/infra/occupymedia/edge/hub-chat-auth.css; then
|
||||
cp -p /opt/anarchoposter/infra/occupymedia/edge/hub-chat-auth.css "$occ_auth_backup/hub-chat-auth.css"
|
||||
fi
|
||||
printf 'Backup: %s\n' "$occ_auth_backup"
|
||||
```
|
||||
|
||||
2. **From this workstation's repo root**, copy only the reviewed infra files.
|
||||
Set `OCC_EDGE_SSH` to your existing SSH destination for CT 150; no hostname or
|
||||
credentials are assumed here.
|
||||
|
||||
```bash
|
||||
OCC_EDGE_SSH='YOUR_EXISTING_CT_150_SSH_DESTINATION'
|
||||
rsync -aR -- \
|
||||
infra/occupymedia/edge/nginx.conf \
|
||||
infra/occupymedia/edge/docker-compose.edge.yml \
|
||||
infra/occupymedia/edge/hub-chat-auth.css \
|
||||
infra/occupymedia/edge/README.md \
|
||||
infra/occupymedia/element/ \
|
||||
"$OCC_EDGE_SSH:/opt/anarchoposter/"
|
||||
```
|
||||
|
||||
3. **On the server**, validate the candidate edge config with the new mounts,
|
||||
then merge the auth patch and recreate only the edge service:
|
||||
|
||||
```bash
|
||||
cd /opt/anarchoposter/infra/occupymedia/edge
|
||||
docker compose -f docker-compose.edge.yml config --quiet
|
||||
docker compose -f docker-compose.edge.yml run --rm --no-deps edge nginx -t
|
||||
python3 ../element/apply-auth-branding.py /opt/occ-element/config.json > "$occ_auth_backup/config.preview.json"
|
||||
diff -u "$occ_auth_backup/config.json" "$occ_auth_backup/config.preview.json"
|
||||
python3 ../element/apply-auth-branding.py /opt/occ-element/config.json --write
|
||||
docker compose -f docker-compose.edge.yml up -d --no-deps --force-recreate edge
|
||||
docker exec occ-edge-1 nginx -t
|
||||
```
|
||||
|
||||
Run each command only if the preceding validation succeeded; `diff` exits 1
|
||||
for the expected changes. Recreating the edge is required for the new CSS
|
||||
mount and ensures it sees copied files even if their inodes changed. Element
|
||||
needs no restart: the merge utility retains the config inode. Its existing
|
||||
`setting_defaults.custom_themes`, Compound tokens, `default_theme`, and
|
||||
`default_server_config` remain intact. Do not replace config.json with the
|
||||
patch JSON or try to register this CSS as a custom theme.
|
||||
|
||||
4. **On the server**, verify the public HTML, stylesheet and served config:
|
||||
|
||||
```bash
|
||||
curl -fsS --compressed https://chat.occupymedia.cc/ -o "$occ_auth_backup/index.served.html"
|
||||
curl -fsS 'https://chat.occupymedia.cc/hub-chat-auth.css?v=1' -o "$occ_auth_backup/auth.served.css"
|
||||
curl -fsS https://chat.occupymedia.cc/config.json -o "$occ_auth_backup/config.served.json"
|
||||
cmp hub-chat-auth.css "$occ_auth_backup/auth.served.css"
|
||||
python3 - "$occ_auth_backup" <<'PY'
|
||||
import json, sys
|
||||
from pathlib import Path
|
||||
backup = Path(sys.argv[1])
|
||||
html = (backup / "index.served.html").read_text()
|
||||
assert html.count('href="/hub-chat-auth.css?v=1"') == 1
|
||||
old = json.loads((backup / "config.json").read_text())
|
||||
served = json.loads((backup / "config.served.json").read_text())
|
||||
for key in ("setting_defaults", "default_theme", "default_server_config", "default_server_name"):
|
||||
assert served.get(key) == old.get(key), key
|
||||
assert served["branding"]["auth_header_logo_url"] == "https://app.occupymedia.cc/brand/logo.svg"
|
||||
assert served["branding"]["logo_link_url"] == "https://app.occupymedia.cc"
|
||||
assert served["embedded_pages"]["login_for_welcome"] is True
|
||||
assert served["disable_3pid_login"] is True
|
||||
assert served["disable_custom_urls"] is True
|
||||
print("Public HTML injection, stylesheet and preserved theme verified")
|
||||
PY
|
||||
```
|
||||
|
||||
If a CDN serves an older HTML/config response, purge those URLs and retry.
|
||||
In a fresh signed-out browser, open `/`, `/#/login` and `/#/register`, and
|
||||
check the page on a phone as well. Check password errors, keyboard focus,
|
||||
recovery and an existing signed-in room. Browser reloads load the new config.
|
||||
Element also supports domain-specific config files; if this installation
|
||||
starts serving a successful `/config.chat.occupymedia.cc.json`, apply the
|
||||
same merge to its mounted source because it takes precedence over config.json.
|
||||
|
||||
Rollback using the printed backup directory: restore the old nginx and compose
|
||||
files, restore the old config **in place**, then recreate the edge service with
|
||||
the old compose file:
|
||||
|
||||
```bash
|
||||
cp -p "$occ_auth_backup/nginx.conf" /opt/anarchoposter/infra/occupymedia/edge/nginx.conf
|
||||
cp -p "$occ_auth_backup/docker-compose.edge.yml" /opt/anarchoposter/infra/occupymedia/edge/docker-compose.edge.yml
|
||||
if test -f "$occ_auth_backup/hub-chat-auth.css"; then
|
||||
cp -p "$occ_auth_backup/hub-chat-auth.css" /opt/anarchoposter/infra/occupymedia/edge/hub-chat-auth.css
|
||||
fi
|
||||
python3 - "$occ_auth_backup/config.json" /opt/occ-element/config.json <<'PY'
|
||||
from pathlib import Path
|
||||
import sys
|
||||
data = Path(sys.argv[1]).read_bytes()
|
||||
with Path(sys.argv[2]).open("r+b") as target:
|
||||
target.write(data)
|
||||
target.truncate()
|
||||
PY
|
||||
cd /opt/anarchoposter/infra/occupymedia/edge
|
||||
docker compose -f docker-compose.edge.yml run --rm --no-deps edge nginx -t
|
||||
docker compose -f docker-compose.edge.yml up -d --no-deps --force-recreate edge
|
||||
```
|
||||
|
||||
## Local validation
|
||||
|
||||
- Real Element v1.12.30 and nginx:1.27-alpine ran on an isolated local Docker
|
||||
network with temporary certificates and loopback ports; Matrix responses and
|
||||
the Hub asset request were mocked. No live accounts or production services
|
||||
were accessed.
|
||||
- Full repo nginx config passed `nginx -t`; compose configuration parsed.
|
||||
Nginx emits an existing duplicate `text/html` MIME warning in the unrelated
|
||||
files sign-in location; it does not prevent validation.
|
||||
- Both chat HTTP/HTTPS vhosts injected exactly one stylesheet link despite the
|
||||
client advertising gzip. JSON and the SSO helper were unchanged; CSS content
|
||||
and content type matched the repo file.
|
||||
- Chromium checked bare `/` → login, 420px desktop card, 32px corner logo,
|
||||
system-ui, warm background, red submit, two login inputs, mock failed login,
|
||||
visible errors, and registration. Checked widths 320/390/844px including
|
||||
landscape: no horizontal overflow or brand/form overlap. A fresh iPhone user
|
||||
agent reached the form through the mobile-guide redirect.
|
||||
- The injected link remained enabled after custom-theme loading. A room DOM
|
||||
without the auth wrapper retained its original button color.
|
||||
- Config merge checks confirmed preservation of theme, Compound tokens,
|
||||
homeserver and sibling keys, idempotence, and the original file inode.
|
||||
|
||||
Real successful authentication and production cache/routing behavior still need
|
||||
the operator's post-deployment checks. Existing unrelated workspace changes were
|
||||
left alone; no files in `apps/api` were edited by this task.
|
||||
46
infra/occupymedia/element/apply-auth-branding.py
Normal file
46
infra/occupymedia/element/apply-auth-branding.py
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Merge the auth-only patch into an existing, file-bind-mounted Element config."""
|
||||
|
||||
import argparse
|
||||
import copy
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def merge(current, patch):
|
||||
for key, value in patch.items():
|
||||
if isinstance(value, dict):
|
||||
if not isinstance(current.get(key), dict):
|
||||
current[key] = {}
|
||||
merge(current[key], value)
|
||||
else:
|
||||
current[key] = value
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("config", type=Path)
|
||||
parser.add_argument("--write", action="store_true", help="write in place; otherwise print a preview")
|
||||
args = parser.parse_args()
|
||||
patch = json.loads(Path(__file__).with_name("auth-branding.patch.json").read_text())
|
||||
# r+ intentionally retains the inode: replacing this file would leave an
|
||||
# existing Docker single-file bind mount serving the old config.
|
||||
with args.config.open("r+" if args.write else "r", encoding="utf-8") as config_file:
|
||||
current = json.load(config_file)
|
||||
if not isinstance(current, dict):
|
||||
raise ValueError("Element config must be a JSON object")
|
||||
original = copy.deepcopy(current)
|
||||
merge(current, patch)
|
||||
for key in ("setting_defaults", "default_theme", "default_server_config", "default_server_name"):
|
||||
assert current.get(key) == original.get(key), f"Auth patch must preserve {key}"
|
||||
output = json.dumps(current, indent=2, ensure_ascii=False) + "\n"
|
||||
if args.write:
|
||||
config_file.seek(0)
|
||||
config_file.write(output)
|
||||
config_file.truncate()
|
||||
else:
|
||||
print(output, end="")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
14
infra/occupymedia/element/auth-branding.patch.json
Normal file
14
infra/occupymedia/element/auth-branding.patch.json
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"brand": "Tactical Media Broadcast Hub",
|
||||
"branding": {
|
||||
"auth_header_logo_url": "https://app.occupymedia.cc/brand/logo.svg",
|
||||
"logo_link_url": "https://app.occupymedia.cc"
|
||||
},
|
||||
"embedded_pages": {
|
||||
"login_for_welcome": true
|
||||
},
|
||||
"disable_custom_urls": true,
|
||||
"disable_3pid_login": true,
|
||||
"disable_login_language_selector": true,
|
||||
"mobile_guide_toast": false
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue