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:
glass beadian 2026-10-14 08:04:56 +00:00
parent f2eb14956e
commit b89dea5c93
26 changed files with 1637 additions and 31 deletions

View file

@ -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 |

View file

@ -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:

View 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;
}
}

View file

@ -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;
}
}

View 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.

View 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()

View 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
}