From 18f4e07de8222fcaefdb195a873b01c8cd2bb636 Mon Sep 17 00:00:00 2001 From: Brandyn / Techy Date: Sun, 1 Nov 2026 01:38:35 -0500 Subject: [PATCH] Split hosts: media (gateway at /), files, clips, sync; live gains /dock, /dock/clock, /dump, /me, /time configure.py: three new host roles (files/sync/clips) in --full, --hosts, .env, Caddy generator; main host serves about/ at the root (/about* redirects); /time only where it is fetched same-origin. about/index.html links across hosts; addCommunityMediaServer.sh asks for the new names; Caddyfile.example regenerated; README/FRESH-INSTALL/HOSTNAME-CHANGE (procedure C)/STATUS updated. Co-Authored-By: Claude Fable 5.1 --- Caddyfile.example | 217 +++++++++++++++++++++++++++++++++---- FRESH-INSTALL.md | 30 ++--- HOSTNAME-CHANGE.md | 45 ++++++-- README.md | 45 +++++--- STATUS.md | 22 ++-- about/index.html | 22 ++-- addCommunityMediaServer.sh | 18 ++- configure.py | 198 +++++++++++++++++---------------- deployLive.sh | 2 +- 9 files changed, 418 insertions(+), 181 deletions(-) diff --git a/Caddyfile.example b/Caddyfile.example index f88af66..8098d57 100644 --- a/Caddyfile.example +++ b/Caddyfile.example @@ -1,29 +1,204 @@ +# vanguard:main media.example.com { encode zstd gzip - - # dump endpoint (python service on the host, does its own auth) - handle /dump* { - reverse_proxy 127.0.0.1:8080 + # /logout on every gated host = sign out of the Discord login (domain-wide session) + redir /logout /outpost.goauthentik.io/sign_out 302 + # Authentik forward-auth (embedded outpost). Needs a Proxy Provider in Authentik — domain-level for the + # whole domain (recommended) or single-application for this host — assigned to the embedded outpost. + # Until then: 4xx, never open. Per-host group requirements are enforced below from X-Authentik-Groups. + reverse_proxy /outpost.goauthentik.io/* 127.0.0.1:9000 + forward_auth 127.0.0.1:9000 { + uri /outpost.goauthentik.io/auth/caddy + copy_headers X-Authentik-Username X-Authentik-Groups X-Authentik-Entitlements X-Authentik-Email X-Authentik-Name X-Authentik-Uid X-Authentik-Jwt X-Authentik-Meta-Jwks X-Authentik-Meta-Outpost X-Authentik-Meta-Provider X-Authentik-Meta-App X-Authentik-Meta-Version + trusted_proxies private_ranges } - - # Syncthing admin UI (its own login; strip the prefix, present a local Host to pass its host check) - redir /sync /sync/ 308 - handle_path /sync/* { - reverse_proxy 127.0.0.1:8384 { - header_up Host {upstream_hostport} - } + # who does Authentik say I am? (debug: shows exactly the identity headers the apps receive) + handle /whoami { + header Content-Type text/plain + respond "user: {http.request.header.X-Authentik-Username} +groups: {http.request.header.X-Authentik-Groups} +email: {http.request.header.X-Authentik-Email} +name: {http.request.header.X-Authentik-Name}" } - - # OBS browser dock (static) - handle_path /dock* { - root * /srv/dock - file_server + @me path /me /me/* + handle @me { + reverse_proxy 127.0.0.1:8080 # dumpd: identity for the gateway page } - - # everything else is copyparty (web UI + WebDAV) + redir /about* / 308 # old bookmarks: the gateway page used to live at /about/ handle { - reverse_proxy 127.0.0.1:3923 { - flush_interval -1 - } + root * /srv/about + try_files {path} /index.html + file_server # the gateway page is the root of the main host } } + +# vanguard:apex +example.com { + redir https://media.example.com/ 302 +} + +# vanguard:files +files.example.com { + encode zstd gzip + # /logout on every gated host = sign out of the Discord login (domain-wide session) + redir /logout /outpost.goauthentik.io/sign_out 302 + # Authentik forward-auth (embedded outpost). Needs a Proxy Provider in Authentik — domain-level for the + # whole domain (recommended) or single-application for this host — assigned to the embedded outpost. + # Until then: 4xx, never open. Per-host group requirements are enforced below from X-Authentik-Groups. + reverse_proxy /outpost.goauthentik.io/* 127.0.0.1:9000 + forward_auth 127.0.0.1:9000 { + uri /outpost.goauthentik.io/auth/caddy + copy_headers X-Authentik-Username X-Authentik-Groups X-Authentik-Entitlements X-Authentik-Email X-Authentik-Name X-Authentik-Uid X-Authentik-Jwt X-Authentik-Meta-Jwks X-Authentik-Meta-Outpost X-Authentik-Meta-Provider X-Authentik-Meta-App X-Authentik-Meta-Version + trusted_proxies private_ranges + } + # copyparty at the root. It honours X-Authentik-Username / -Groups only when the request also carries a header + # whose secret NAME only this proxy knows (copyparty idp-h-key). Clients cannot forge identity. + reverse_proxy 127.0.0.1:3923 { + header_up X-Vanguard-EXAMPLEKEY 1 + flush_interval -1 + } +} + +# vanguard:sync +sync.example.com { + encode zstd gzip + # /logout on every gated host = sign out of the Discord login (domain-wide session) + redir /logout /outpost.goauthentik.io/sign_out 302 + # Authentik forward-auth (embedded outpost). Needs a Proxy Provider in Authentik — domain-level for the + # whole domain (recommended) or single-application for this host — assigned to the embedded outpost. + # Until then: 4xx, never open. Per-host group requirements are enforced below from X-Authentik-Groups. + reverse_proxy /outpost.goauthentik.io/* 127.0.0.1:9000 + forward_auth 127.0.0.1:9000 { + uri /outpost.goauthentik.io/auth/caddy + copy_headers X-Authentik-Username X-Authentik-Groups X-Authentik-Entitlements X-Authentik-Email X-Authentik-Name X-Authentik-Uid X-Authentik-Jwt X-Authentik-Meta-Jwks X-Authentik-Meta-Outpost X-Authentik-Meta-Provider X-Authentik-Meta-App X-Authentik-Meta-Version + trusted_proxies private_ranges + } + @lacks_group not header_regexp X-Authentik-Groups (^|\|)media\-admins(\||$) + handle @lacks_group { + respond "This page is for members of the 'media-admins' group." 403 + } + # Syncthing admin UI (its own GUI login is disabled; the gate + group is the login) + reverse_proxy 127.0.0.1:8384 { + header_up Host {upstream_hostport} + } +} + +# vanguard:clips +clips.example.com { + encode zstd gzip + # /logout on every gated host = sign out of the Discord login (domain-wide session) + redir /logout /outpost.goauthentik.io/sign_out 302 + # Authentik forward-auth (embedded outpost). Needs a Proxy Provider in Authentik — domain-level for the + # whole domain (recommended) or single-application for this host — assigned to the embedded outpost. + # Until then: 4xx, never open. Per-host group requirements are enforced below from X-Authentik-Groups. + reverse_proxy /outpost.goauthentik.io/* 127.0.0.1:9000 + forward_auth 127.0.0.1:9000 { + uri /outpost.goauthentik.io/auth/caddy + copy_headers X-Authentik-Username X-Authentik-Groups X-Authentik-Entitlements X-Authentik-Email X-Authentik-Name X-Authentik-Uid X-Authentik-Jwt X-Authentik-Meta-Jwks X-Authentik-Meta-Outpost X-Authentik-Meta-Provider X-Authentik-Meta-App X-Authentik-Meta-Version + trusted_proxies private_ranges + } + @me path /me /me/* + handle @me { + reverse_proxy 127.0.0.1:8080 # dumpd: identity + /me/clips self-service + } + handle { + root * /srv/clips + try_files {path} /index.html + file_server + } +} + +# vanguard:live +live.example.com { + encode zstd gzip + # /logout on every gated host = sign out of the Discord login (domain-wide session) + redir /logout /outpost.goauthentik.io/sign_out 302 + # Authentik forward-auth (embedded outpost). Needs a Proxy Provider in Authentik — domain-level for the + # whole domain (recommended) or single-application for this host — assigned to the embedded outpost. + # Until then: 4xx, never open. Per-host group requirements are enforced below from X-Authentik-Groups. + reverse_proxy /outpost.goauthentik.io/* 127.0.0.1:9000 + forward_auth 127.0.0.1:9000 { + uri /outpost.goauthentik.io/auth/caddy + copy_headers X-Authentik-Username X-Authentik-Groups X-Authentik-Entitlements X-Authentik-Email X-Authentik-Name X-Authentik-Uid X-Authentik-Jwt X-Authentik-Meta-Jwks X-Authentik-Meta-Outpost X-Authentik-Meta-Provider X-Authentik-Meta-App X-Authentik-Meta-Version + trusted_proxies private_ranges + } + handle /whoami { + header Content-Type text/plain + respond "user: {http.request.header.X-Authentik-Username} +groups: {http.request.header.X-Authentik-Groups}" + } + @me path /me /me/* + handle @me { + reverse_proxy 127.0.0.1:8080 # dumpd: identity, stream tokens (Go Live + dock) + } + handle /time { + reverse_proxy 127.0.0.1:8080 # server clock for the timecode strip (clock page + Go Live) + } + handle /dump* { + reverse_proxy 127.0.0.1:8080 # dump tonight (dock) + } + handle_path /dock* { + root * /srv/dock + file_server # OBS browser dock + /dock/clock/ + } + handle_path /api/* { + reverse_proxy 127.0.0.1:9997 + } + handle_path /webrtc/* { + # Go Live sends MediaMTX credentials in X-Stream-Auth so Authentik's forward-auth never sees an + # Authorization header; Caddy turns it into Authorization only on the hop to MediaMTX. + @creds header X-Stream-Auth * + reverse_proxy @creds 127.0.0.1:8889 { + header_up Authorization {http.request.header.X-Stream-Auth} + header_up -X-Stream-Auth + } + reverse_proxy 127.0.0.1:8889 + } + handle_path /hls/* { + reverse_proxy 127.0.0.1:8888 + } + handle { + root * /srv/live + try_files {path} /index.html + file_server + } +} + +# vanguard:bot +bot.example.com { + encode zstd gzip + # /logout on every gated host = sign out of the Discord login (domain-wide session) + redir /logout /outpost.goauthentik.io/sign_out 302 + # Authentik forward-auth (embedded outpost). Needs a Proxy Provider in Authentik — domain-level for the + # whole domain (recommended) or single-application for this host — assigned to the embedded outpost. + # Until then: 4xx, never open. Per-host group requirements are enforced below from X-Authentik-Groups. + reverse_proxy /outpost.goauthentik.io/* 127.0.0.1:9000 + forward_auth 127.0.0.1:9000 { + uri /outpost.goauthentik.io/auth/caddy + copy_headers X-Authentik-Username X-Authentik-Groups X-Authentik-Entitlements X-Authentik-Email X-Authentik-Name X-Authentik-Uid X-Authentik-Jwt X-Authentik-Meta-Jwks X-Authentik-Meta-Outpost X-Authentik-Meta-Provider X-Authentik-Meta-App X-Authentik-Meta-Version + trusted_proxies private_ranges + } + @lacks_group not header_regexp X-Authentik-Groups (^|\|)bot\-users(\||$) + handle @lacks_group { + respond "This page is for members of the 'bot-users' group." 403 + } + reverse_proxy 127.0.0.1:8000 +} + +# vanguard:git +git.example.com { + encode zstd gzip + reverse_proxy 127.0.0.1:3000 +} + +# vanguard:auth +auth.example.com { + encode zstd gzip + reverse_proxy 127.0.0.1:9000 +} + +# vanguard:watch +watch.example.com { + encode zstd gzip + reverse_proxy 127.0.0.1:8096 +} diff --git a/FRESH-INSTALL.md b/FRESH-INSTALL.md index 6aa383e..265eb03 100644 --- a/FRESH-INSTALL.md +++ b/FRESH-INSTALL.md @@ -1,12 +1,12 @@ # Fresh install — from an empty OVH box to a working stack with the deploy pipeline Everything in order. Scripted steps are commands; manual steps are click lists. `HOST` = your main hostname -(ours: `ovh1.dumpstervigilantes.rip`), `BASE` = the parent domain. Detailed background for each part is in +(ours: `media.dumpstervigilantes.rip`), `BASE` = the parent domain. Detailed background for each part is in README.md (section numbers given). ## 0. Before touching the server -- DNS: `A` record for `HOST` → the server IP; `CNAME`s `auth`, `watch`, `git`, `bot`, `live` → `HOST`. - Optional: the bare domain `A` → server IP (it redirects to the watch site or `/about/`). +- DNS: `A` record for `HOST` → the server IP; `CNAME`s `files`, `clips`, `sync`, `live`, `auth`, `watch`, `git`, `bot` → `HOST`. + Optional: the bare domain `A` → server IP (it redirects to `https://HOST/`). - Discord developer portal → New Application (one app serves Authentik login, the Jellyfin plugin's bot and the Discord bot): - OAuth2 → Redirects: `https://auth.BASE/source/oauth/callback/discord/` (the last segment is the Authentik @@ -39,7 +39,7 @@ build, **firewall** (keep this session open; test a new ssh in a second terminal cd ~/vanguard-stream && ./configure.py --full -Asks: main hostname; `auth`/`watch`/`git`/`bot`/`live` hostnames (defaults); bare domain (optional) and where it +Asks: main hostname (the gateway page); `files`/`clips`/`sync`/`auth`/`watch`/`git`/`bot`/`live` hostnames (defaults); bare domain (optional) and where it redirects; Discord bot token + server ID (optional now, needed for the plugin feed); "is live" webhook (optional). Generates `.env` with every secret, the configs, starts all containers, waits, then runs the post-start fixups (Syncthing GUI auth off, Gitea admin user, Jellyfin plugin). `./configure.py --show` prints every credential @@ -111,11 +111,11 @@ Log in at `https://git.BASE/` as `GITEA_ADMIN_USER` / `GITEA_ADMIN_PASSWORD` fro `DEPLOY_AUTO=1` in `.env` → `./configure.py --apply`; CI: `git mv .gitea/draft-workflows/ci.yml .gitea/workflows/ci.yml`. ## 9. Streamers, gamers, editors (README §5, §5a, §6) -- Streamers: OBS → WebSocket server on; Docks → Custom Browser Dock `https://HOST/dock/` → Discord login → +- Streamers: OBS → WebSocket server on; Docks → Custom Browser Dock `https://live.BASE/dock/` → Discord login → Settings: OBS WebSocket password; Timecode band → "Add band + clock source" (not streaming). Then title → Go Live. -- Gamers: `https://HOST/clips/` → paste their Syncthing Device ID → accept device + folder on their PC (Send Only). - Syncthing UI (`https://HOST/sync/`, media-admins): Actions → Settings → Connections → incoming rate limit 12500 KiB/s. -- Everyone: `https://HOST/about/`. +- Gamers: `https://clips.BASE/` → paste their Syncthing Device ID → accept device + folder on their PC (Send Only). + Syncthing UI (`https://sync.BASE/`, media-admins): Actions → Settings → Connections → incoming rate limit 12500 KiB/s. +- Everyone: `https://HOST/` (the gateway page; editors browse at `https://files.BASE/`). ## 10. Afterwards - Pin `AUTHENTIK_TAG` in `.env` to the running version. Rotate any secret that was ever pasted into chat. @@ -128,7 +128,7 @@ Log in at `https://git.BASE/` as `GITEA_ADMIN_USER` / `GITEA_ADMIN_PASSWORD` fro Conventions: commands prefixed `dev$` run on your workstation (it has the repo at `~/claudevs/vanguard-stream` and your SSH key); `srv$` run on the server over ssh as `debian`. Replace `HOST` / `BASE` with your names -(ours: `ovh1.dumpstervigilantes.rip` / `dumpstervigilantes.rip`). Click paths are written `App → Menu → Item`. +(ours: `media.dumpstervigilantes.rip` / `dumpstervigilantes.rip`). Click paths are written `App → Menu → Item`. Every step ends with how you know it worked. ## A. DNS and Discord (no server yet) @@ -180,7 +180,7 @@ D4. Log out/in once for the docker group: dev$ ssh debian@HOST 'docker ps >/dev ## E. Configure and start the stack E1. dev$ ssh -t debian@HOST 'cd ~/vanguard-stream && ./configure.py --full' - Answers: HOST; Enter ×5 for the sub-hostnames; bare domain (Enter = BASE, or blank to skip) and its redirect; + Answers: HOST; Enter ×8 for the sub-hostnames; bare domain (Enter = BASE, or blank to skip) and its redirect; bot token (from A2) ; server ID (A3) ; "is live" webhook (optional); y to apply. (Pulls ~2 GB of images, starts 13 containers, waits, runs post-start fixups.) E2. dev$ ssh debian@HOST 'cd ~/vanguard-stream && docker compose ps --format "{{.Name}} {{.Status}}"' @@ -282,13 +282,13 @@ I6. (later, when trusted) dev$ ssh debian@HOST 'cd ~/vanguard-stream && sed -i " ## J. People J1. Streamer (OBS): Tools → WebSocket Server Settings → Enable, set a password → OK. - Docks → Custom Browser Docks → name Vanguard, URL https://HOST/dock/ → Apply → log in with Discord inside the dock. + Docks → Custom Browser Docks → name Vanguard, URL https://live.BASE/dock/ → Apply → log in with Discord inside the dock. Dock → Settings: OBS WebSocket password → Save. Dock → Timecode band → "Add band + clock source" (while not streaming). Dock: title → Go Live. Check: dev$ ssh debian@HOST 'journalctl -u dumpd --since -5m --no-pager | grep mtx-auth | tail -1' → "allow publish user=…". -J2. Gamer: https://HOST/clips/ → follow the three steps on the page (install Syncthing, paste Device ID, accept device + folder as Send Only). - Admin check: https://HOST/sync/ shows their device connected. Once: Syncthing → Actions → Settings → Connections → Incoming Rate Limit 12500 KiB/s. -J3. Editor: https://HOST/ (copyparty) → browse, download; https://HOST/finals/ to upload. Needs the editors role. -J4. Everyone: https://HOST/about/. +J2. Gamer: https://clips.BASE/ → follow the three steps on the page (install Syncthing, paste Device ID, accept device + folder as Send Only). + Admin check: https://sync.BASE/ shows their device connected. Once: Syncthing → Actions → Settings → Connections → Incoming Rate Limit 12500 KiB/s. +J3. Editor: https://files.BASE/ (copyparty) → browse, download; https://files.BASE/finals/ to upload. Needs the editors role. +J4. Everyone: https://HOST/ (gateway page). ## K. Verify the whole pipeline once diff --git a/HOSTNAME-CHANGE.md b/HOSTNAME-CHANGE.md index 5a6b719..3cb64ff 100644 --- a/HOSTNAME-CHANGE.md +++ b/HOSTNAME-CHANGE.md @@ -5,13 +5,17 @@ domain. Both are mostly `configure.py --full` plus a known list of places outsid Do it between streams: the main host is MediaMTX's WebRTC advertised host, so a main-host change needs a MediaMTX restart. +Roles: `main` (gateway page at `/`), `files` (copyparty), `clips`, `sync`, `live` (portal + `/dock/`, `/dock/clock/`, +`/dump`, `/me`, `/time`), `auth`, `watch`, `git`, `bot`. Ours: `media.`, `files.`, `clips.`, `sync.`, `live.`, … under +`dumpstervigilantes.rip`. + ## Where hostnames live ### Generated on the server by `configure.py` (regenerated by `--full`, nothing to edit by hand) | Place | Holds | |---|---| | `Caddyfile` | every site block — the source of truth `configure.py` reads back (`# vanguard:` markers) | -| `.env` | `MAIN_HOST`, `WATCH_HOST`, `GIT_HOST`, `BOT_HOST`, `LIVE_HOST`, `APEX_HOST`, `APEX_REDIRECT` | +| `.env` | `MAIN_HOST`, `FILES_HOST`, `CLIPS_HOST`, `SYNC_HOST`, `WATCH_HOST`, `GIT_HOST`, `BOT_HOST`, `LIVE_HOST`, `APEX_HOST`, `APEX_REDIRECT` | | `mediamtx.yml` | `webrtcAdditionalHosts: [MAIN_HOST]` (needs a MediaMTX restart to take effect) | | `docker-compose.yml` → container env | Jellyfin `PublishedServerUrl` (WATCH), Gitea `DOMAIN/ROOT_URL/SSH_DOMAIN` (GIT), bot `WEB_PUBLIC_URL` (BOT), MediaMTX `LIVE_HOST` (text of the "is live" post), act_runner `GITEA_INSTANCE_URL` (GIT). `compose up -d` in the apply recreates the affected containers (seconds each) | | Caddy TLS | certificates for new names are fetched automatically; old ones linger harmlessly | @@ -19,9 +23,9 @@ MediaMTX restart. ### Pages in the repo that derive sibling hosts from the browser's location (label names hard-coded) | File | Assumes | |---|---| -| `about/index.html` | `auth.`, `watch.`, `live.`, `git.`, `bot.` + the parent of the current host | -| `dock/index.html` | `auth.` (sign-out), ingest host prefilled from the current host | -| `switchToClone.sh`, `deployLive.sh` | defaults for `git.dumpstervigilantes.rip` / `ovh1.dumpstervigilantes.rip` (override by env/arg, or edit) | +| `about/index.html` | `files.`, `clips.`, `sync.`, `live.`, `auth.`, `watch.`, `git.`, `bot.` + the parent of the current host | +| `dock/index.html` | `auth.` (sign-out), ingest host prefilled from the current host (`live.`; any name of the box works for RTMP/SRT) | +| `switchToClone.sh`, `deployLive.sh` | defaults for `git.dumpstervigilantes.rip` / `media.dumpstervigilantes.rip` (override by env/arg, or edit) | | `README.md`, `FRESH-INSTALL.md` | examples only | Renaming a *label* (e.g. `watch` → `video`) means editing the first two; changing only the domain does not. @@ -41,9 +45,9 @@ Renaming a *label* (e.g. `watch` → `video`) means editing the first two; chang | Gitea runner | registered with the instance URL → `./registerGiteaRunner.sh` again | git | | Server clone | `git remote set-url origin ssh://git@GIT:2222/techy/Media-Server-Setup.git`; `~/.ssh/config.vanguard` Host line | git | | Dev box | `git remote set-url origin …` | git | -| Streamers' OBS | dock URL (Custom Browser Docks), dock Settings → ingest host (re-save), clock browser source URL (dock → Timecode band → "Add band + clock source" again), any manual RTMP/SRT configs (copy from the dock) | main | +| Streamers' OBS | dock URL (Custom Browser Docks), dock Settings → ingest host (re-save), clock browser source URL (dock → Timecode band → "Add band + clock source" again), any manual RTMP/SRT configs (copy from the dock) | live | | Jellyfin apps on phones/TVs | server address | watch | -| Bookmarks, Discord pinned links | `/about/`, `/clips/`, watch, live | all | +| Bookmarks, Discord pinned links | main (gateway), clips, files, watch, live | all | Not affected: Syncthing pairings (device IDs), MariaDB, stream tokens, the Gitea webhook (gateway IP), Authentik users/groups/policies. ## A. Renaming one subdomain (example: `watch.BASE` → `video.BASE`) @@ -77,10 +81,10 @@ Per-host extras when it is a different host: Plan a maintenance window: Caddy, Jellyfin, Gitea, the bot and MediaMTX all restart, and every member logs in again (the Authentik session cookie is bound to the base domain). -1. DNS on the new domain: `A HOST2 → IP`, CNAMEs `auth`, `watch`, `git`, `bot`, `live` → `HOST2` (optional bare domain A). +1. DNS on the new domain: `A HOST2 → IP`, CNAMEs `files`, `clips`, `sync`, `live`, `auth`, `watch`, `git`, `bot` → `HOST2` (optional bare domain A). Verify they resolve before continuing. Leave the old domain's records in place for now. 2. Discord developer portal → OAuth2 → add redirect `https://auth.NEW/source/oauth/callback/discord/` (keep the old one until step 9). -3. Server, between streams: `./configure.py --full` → new main hostname; the five sub-hostnames default to the new +3. Server, between streams: `./configure.py --full` → new main hostname; the eight sub-hostnames default to the new base (Enter); bare domain; `y`. Then `docker compose restart mediamtx` (webrtcAdditionalHosts changed). All sites answer on the new names within a minute (certificates). 4. Authentik (log in at the NEW auth host with akadmin — Discord login is broken until step 5 is done): @@ -95,11 +99,30 @@ Plan a maintenance window: Caddy, Jellyfin, Gitea, the bot and MediaMTX all rest `./registerGiteaRunner.sh`; server: `cd ~/vanguard-stream && git remote set-url origin ssh://git@git.NEW:2222/techy/Media-Server-Setup.git` and edit `~/.ssh/config.vanguard` (Host line); dev box: `git remote set-url origin …`. Test `sudo vanguard-deploy pull`. 8. Repo defaults: `switchToClone.sh` (REPO_SSH), `deployLive.sh` (HOST) — commit, push, pull. -9. Streamers: re-add the dock at `https://HOST2/dock/`, re-save Settings, "Add band + clock source" again. - Members: `https://HOST2/about/`; Jellyfin apps → new server address. Remove the old Discord redirect and the old DNS. +9. Streamers: re-add the dock at `https://live.NEW/dock/`, re-save Settings, "Add band + clock source" again. + Members: `https://HOST2/`; Jellyfin apps → new server address. Remove the old Discord redirect and the old DNS. + +## C. Splitting the single main host into media / files / clips / sync (done 2026-11-01) + +Before: one main host (`ovh1.BASE`) served copyparty at `/`, `/about/`, `/clips/`, `/sync/`, `/dock/`, `/dump`, `/time`. +After: `media.BASE` = gateway at `/`; `files.BASE` = copyparty; `clips.BASE`; `sync.BASE`; `live.BASE` gains `/dock/`, +`/dock/clock/`, `/dump`, `/me`, `/time`, `/whoami`. `/time` is only on `live` because the two pages that fetch it +(the clock source and Go Live) are same-origin there. Authentik needs nothing: the domain-level provider covers every +name under BASE, the outpost host (`auth.`) is unchanged. + +1. DNS: CNAMEs `media`, `files`, `clips`, `sync` → the box (keep `ovh1` for ssh; it is no longer a web host). +2. Dev box: pull/upload the repo (`configure.py`, `about/`, `addCommunityMediaServer.sh`, docs). +3. Server, between streams: `./configure.py --full` → main hostname `media.BASE`; Enter for the sub-hostnames + (defaults `files.`, `clips.`, `sync.`, …); bare domain redirect → `https://media.BASE/`; `y`. + Then `docker compose restart mediamtx` (webrtcAdditionalHosts is now `media.`) and `./deployLive.sh` (portal + build picks up nothing new, but confirms `/srv/live` is served on `live.`). +4. Streamers: OBS → Custom Browser Docks → replace the dock URL with `https://live.BASE/dock/`; dock Settings → + save; Timecode band → "Add band + clock source" again (the clock browser source URL moved). +5. Discord: re-pin `https://media.BASE/` (members), `https://clips.BASE/` (gamers), `https://files.BASE/` (editors). +6. Old `ovh1` bookmarks stop working (no Caddy block, TLS fails) — point people at the gateway. ## Checklist after any change - dev$ for h in HOST auth.BASE watch.BASE git.BASE live.BASE bot.BASE; do printf '%-26s ' $h; curl -s -o /dev/null -m 10 -w '%{http_code}\n' https://$h/; done + dev$ for h in HOST files.BASE clips.BASE sync.BASE live.BASE auth.BASE watch.BASE git.BASE bot.BASE; do printf '%-26s ' $h; curl -s -o /dev/null -m 10 -w '%{http_code}\n' https://$h/; done https://HOST/whoami → your identity (gate works on the new name) https://watch.BASE/sso/OID/start/authentik → Jellyfin SSO round trip https://git.BASE/user/login → "Sign in with authentik" round trip diff --git a/README.md b/README.md index 3aa969a..d4a9d37 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,21 @@ > **FRESH-INSTALL.md** = the ordered walkthrough from an empty box to a working stack and pipeline, manual steps included. > **HOSTNAME-CHANGE.md** = every place a hostname lives and how to rename a subdomain or move to a new base domain safely. + +**Hostnames** (all CNAMEs to the main host; one Discord login covers all of them): + +| Host | What | Who | +|---|---|---| +| `media.BASE` (main) | gateway page at `/` (role-aware links + how-tos), `/whoami`, `/logout` | everyone | +| `files.BASE` | copyparty: browse, scrub, download, upload finals | editors / media-admins | +| `clips.BASE` | self-service clip intake (pair your Syncthing) | any member | +| `sync.BASE` | Syncthing admin UI | media-admins | +| `live.BASE` | live portal, Go Live, OBS dock `/dock/`, clock source `/dock/clock/`, `/dump`, `/me`, `/time` | everyone (`LIVE_REQUIRE_GROUP`) | +| `watch.BASE` | Jellyfin | Discord via OIDC | +| `git.BASE`, `bot.BASE`, `auth.BASE` | Gitea, bot web UI (`bot-users`), Authentik | — | + +`/time` (the server clock the timecode strip syncs to) lives only on `live` because browsers fetch it +same-origin: both pages that use it, the clock source and Go Live, are on that host. > **STATUS.md** = what runs, who does what, what's unfinished (kept current). **CommunityDiscordMedia.md** = design + phases. Ingest from OBS, browse/scrub/download for editors, automatic normalization to one editing @@ -82,7 +97,7 @@ Everything else is dropped and a trickle is logged (`journalctl -k | grep vg-dro Jellyfin (8096), copyparty, the Syncthing UI and dumpd bind to 127.0.0.1 only and are reached via Caddy or an ssh tunnel, so they are deliberately not opened. MariaDB runs on the host network (not a Docker published port) precisely so the firewall can govern it. -Syncthing's admin UI is proxied at `https://HOST/sync/` behind the Authentik gate + group `media-admins`; its +Syncthing's admin UI is proxied at `https://sync.BASE/` behind the Authentik gate + group `media-admins`; its own GUI login is disabled (no user/password, `insecureAdminAccess=true`) so Discord is the only login: docker exec syncthing syncthing cli --home /var/syncthing/config config gui insecure-admin-access set true @@ -103,7 +118,7 @@ There are no stream passwords or keys to hand out: the dock mints a **12-hour pu logged-in Discord user each time Go Live is pressed and sets it as the OBS stream key itself. OBS → Tools → WebSocket Server Settings → enable, set a password. -OBS → Docks → Custom Browser Docks → `https://media.example.com/dock/` → log in with Discord once. +OBS → Docks → Custom Browser Docks → `https://live.BASE/dock/` → log in with Discord once. In the dock's Settings: ingest host (prefilled), OBS WebSocket password/port, optional title prefix/suffix (`{date}` → YYYY-MM-DD, e.g. prefix `HelmetCam-` or suffix `-{date}`). Per stream: type a title, click **Go Live**. The dock shows the final stream name (`/`) as you type @@ -120,7 +135,7 @@ Browser streaming without OBS: the live portal's **Go Live** (Windows Chrome 141 ## 5a. Onboarding a gamer's clips (Syncthing) — self-service -Gamers do it themselves at **https://HOST/clips/** (Discord login): the page shows our Syncthing Device ID and +Gamers do it themselves at **https://clips.BASE/** (Discord login): the page shows our Syncthing Device ID and asks for theirs; on submit, dumpd creates `clips/<username>` with the video-only `.stignore`, adds their device to our Syncthing (REST API, key read from `/srv/media/.syncthing/config.xml`) and creates the receive-only folder `clips-<username>` shared with them (min free 300 GB). They accept the device + folder on their PC, @@ -136,16 +151,16 @@ Manual fallback (admin, same result for the server folder): ./addClipUser.sh <username> # creates /srv/media/clips/<username> with the video-only .stignore Their side (Windows: Syncthing installer or SyncTrayzor; Mac/Linux: Syncthing): open the UI, **Actions → Show ID**, -send you the Device ID. In their UI: **Add Remote Device** with the server's ID (ours: `/sync/` → Actions → Show ID), +send you the Device ID. In their UI: **Add Remote Device** with the server's ID (ours: `sync.BASE` → Actions → Show ID), then **Add Folder** on their clip directory (ShadowPlay/Medal/Steam saves), Sharing tab → tick the server, Folder Type **Send Only**, Save. -Our side at https://HOST/sync/: **Add Remote Device** → paste their ID, name it after them, Save. When their folder +Our side at https://sync.BASE/: **Add Remote Device** → paste their ID, name it after them, Save. When their folder offer pops up: **Add** → Folder Path `/var/syncthing/clips/<username>`, Folder Type **Receive Only**, Advanced → Minimum Free Disk Space **300 GB**, Save. Clips start arriving; the sweep normalizes them; Jellyfin shows the folder as that person's show. Their own folder structure is mirrored; only video files are pulled. -## 5a². Gateway page — https://HOST/about/ +## 5a². Gateway page — https://media.BASE/ (the main host root; `/about/` redirects here) Role-aware landing page (Discord login): everyone sees Watch / Live / Send clips / Sign out with short how-tos; `streamers` get the dock + OBS setup steps; `editors` the browser + multicam/timecode notes; `media-admins` @@ -162,7 +177,7 @@ already `.mp4`), the raw's name is appended to `.stlocal` (an `#include`d ignore drops it without flagging or re-pulling), and the raw is deleted locally — it still exists on the gamer's PC. `configure.py` creates the folder + ignore file for each streamer. -In the UI (https://HOST/sync/): +In the UI (https://sync.BASE/): - each clips folder → path `/var/syncthing/clips/<user>`, **Folder Type: Receive Only**, Edit → Advanced → **Minimum Free Disk Space: 300 GB** (so clips can never fill the mirror and stall the recordings). @@ -171,8 +186,8 @@ In the UI (https://HOST/sync/): ## 5c. Authentik (Discord login) and Jellyfin -DNS: `auth` and `watch` as CNAMEs to the main host. Then run `./addCommunityMediaServer.sh` — it asks -for both hostnames, checks DNS, generates `.env` (Authentik secret + DB password; never commit it), +DNS: `auth`, `watch` (and `files`, `clips`, `sync`, `live`, `git`, `bot`) as CNAMEs to the main host. Then run `./addCommunityMediaServer.sh` — it asks +for every hostname, checks DNS, generates `.env` (Authentik secret + DB password; never commit it), adds the Caddy site blocks, starts the containers, waits for them and prints the steps below with your hostnames filled in. (`./configure.py --full` asks for the same hostnames if you prefer.) Then: @@ -285,7 +300,7 @@ from `/me/stream-token` for the logged-in Discord user (member of `streamers`). Bandwidth: no transcoding, every viewer gets the source bitrate (~15 Mbps) → ~30 concurrent viewers on the 500 Mbps line. For a crowd, restream to Twitch from OBS. -## 5g. Authentik forward-auth for the Caddy-gated hosts (main host, `live.`, `bot.`) +## 5g. Authentik forward-auth for the Caddy-gated hosts (`media.`, `files.`, `clips.`, `sync.`, `live.`, `bot.`) Design: **Authentik authenticates once for the whole domain; Caddy authorizes per host** from the `X-Authentik-Groups` header the outpost returns; **Discord roles fill the Authentik groups** at login. @@ -296,8 +311,8 @@ Design: **Authentik authenticates once for the whole domain; Caddy authorizes pe cookie domain = the parent domain (e.g. `dumpstervigilantes.rip`). Edit the Embedded Outpost → Applications → add it → Update. One login now covers every gated host. 3. Per-host group requirement = `.env`: `BOT_REQUIRE_GROUP=bot-users` (default), `LIVE_REQUIRE_GROUP=` - (empty = any logged-in member), `SYNC_REQUIRE_GROUP=media-admins` (the `/sync` Syncthing UI on the main host). - On the main host copyparty itself authorizes by group (`editors`, `media-admins`); the dump endpoint lets + (empty = any logged-in member), `SYNC_REQUIRE_GROUP=media-admins` (the Syncthing UI on `sync.`). + On `files.` copyparty itself authorizes by group (`editors`, `media-admins`); the dump endpoint lets `admins`/`media-admins` dump anyone's night. Leave "Intercept header authentication" **off**: login is Discord only, no app passwords, no WebDAV. `./configure.py` apply regenerates the Caddy blocks; a user outside the group gets a 403 from Caddy before the app sees the request. @@ -325,7 +340,7 @@ Jellyfin and Gitea use OIDC providers instead, not this. ## 6. Editors -Log in with Discord at https://media.example.com/ (copyparty behind Caddy + Authentik). Access is by Authentik +Log in with Discord at https://files.BASE/ (copyparty behind Caddy + Authentik). Access is by Authentik group: **editors** read streams and clips and write `/finals`; **media-admins** have full rights everywhere including `/trash`. Scrub any `.mp4` in the browser, download what you want. @@ -371,7 +386,7 @@ Where the time comes from, best first: (`bin/vgstrip.py`) at the start, middle and end of each recording → the timecode is frame-accurate at the start and the sidecar records drift over the file. Game image untouched. OBS: dock → *Timecode band* → "Add band + clock source" (canvas grows by W/160 rows, a locked browser source `Vanguard Clock` → - `/dock/clock/` sits in the band; only while not streaming). Go Live: the "Timecode band" checkbox (default on). + `https://live.BASE/dock/clock/` sits in the band; only while not streaming). Go Live: the "Timecode band" checkbox (default on). 2. **Server arrival time** (no band) — microseconds in the recording filename. Within ~0.1–0.3 s per streamer (encoder + network delay); coarse but always available. @@ -535,4 +550,4 @@ evening; after 17:00 it is tonight. The 05:00 trash purge is 05:00 Eastern. |------|------|------| | normalize-sweep.timer | every 5 min | remux/re-encode finished files to the editing standard | | trash-purge.timer | daily 05:00 Eastern | deletes trash older than 3 days | -| dumpd.service | always | the dump endpoint on 127.0.0.1:8080, behind Caddy at /dump | +| dumpd.service | always | the dump endpoint on 127.0.0.1:8080, behind Caddy on `live.` at /dump, /me, /time and on `clips.` at /me/clips | diff --git a/STATUS.md b/STATUS.md index aa2e3ae..aba1f0c 100644 --- a/STATUS.md +++ b/STATUS.md @@ -1,7 +1,7 @@ # Vanguard stack — what runs, what people do, what's unfinished -Living document. Updated whenever something is deployed or changes behaviour. Last update: 2026-10-31 (evening). -Host: `ovh1.dumpstervigilantes.rip` (OVH SYS-1, Debian 13). Bundle: `~/vanguard-stream` on the box. +Living document. Updated whenever something is deployed or changes behaviour. Last update: 2026-11-01. +Box: OVH SYS-1, Debian 13, ssh `ovh1.dumpstervigilantes.rip`. Web hosts (one Discord login): `media.` gateway at `/`, `files.` copyparty, `clips.`, `sync.`, `live.` (portal + `/dock/`, `/dump`, `/time`), `watch.`, `git.`, `bot.`, `auth.` — all under `dumpstervigilantes.rip`. **Restructure committed 2026-11-01, server deploy pending** (see Unfinished). Bundle: `~/vanguard-stream` on the box. Source of truth: this repo, `techy/Media-Server-Setup` on the stack's own Gitea (`ssh://git@git.dumpstervigilantes.rip:2222/techy/Media-Server-Setup.git`). ## Runs on its own @@ -29,18 +29,18 @@ Source of truth: this repo, `techy/Media-Server-Setup` on the stack's own Gitea | Who | Activity | Where | Login | |---|---|---|---| | Streamer | One-time: dock → Timecode band → "Add band + clock source" (OBS) or leave the Go Live checkbox on — gives editors frame-accurate timecode | OBS dock / Go Live | live | -| Streamer | Type a title (live preview + naming rules), Go Live, Stop. "Dump tonight" if it was junk (3-day undo). Manual-setup accordion with endpoints and a hidden token | OBS browser dock `https://ovh1…/dock/` | Discord login in the dock; dock mints a 12 h token per Go Live — **no passwords, verified live** | -| Gamer | Self-serve pairing at `/clips/` (paste Syncthing Device ID → folder + share created); then nothing, clips save where they always did | `https://ovh1…/clips/` + Syncthing | Discord (any server member) | -| Everyone | Landing page with role-aware links and instructions | `https://ovh1…/about/` | Discord | +| Streamer | Type a title (live preview + naming rules), Go Live, Stop. "Dump tonight" if it was junk (3-day undo). Manual-setup accordion with endpoints and a hidden token | OBS browser dock `https://live.…/dock/` | Discord login in the dock; dock mints a 12 h token per Go Live — **no passwords, verified live** | +| Gamer | Self-serve pairing (paste Syncthing Device ID → folder + share created); then nothing, clips save where they always did | `https://clips.…/` + Syncthing | Discord (any server member) | +| Everyone | Landing page with role-aware links and instructions | `https://media.…/` | Discord | | Editor | Multicam: drop clips on a timeline, sync by the embedded timecode track (frame-accurate when the band was on), cross-check with voice audio; export at the game resolution to drop the band | Resolve / Premiere | — | -| Editor | Browse by streamer/title, scrub in browser, download originals (zip/tar for folders), upload finals | copyparty `https://ovh1…/` | Discord (Authentik); rights by group `editors` / `media-admins` | +| Editor | Browse by streamer/title, scrub in browser, download originals (zip/tar for folders), upload finals | copyparty `https://files.…/` | Discord (Authentik); rights by group `editors` / `media-admins` | | Anyone in the Discord server | Watch recordings and clips by streamer and season | Jellyfin `https://watch.…/` | Discord (Authentik) | | Anyone in the Discord server | Watch live: all active streams in dockable windows, audio follows focus, per-window mute/volume | Live portal `https://live.…/` | Discord (Authentik, domain-level) — **live, shows a real stream** | | Streamer (Windows) | **Go Live** from the browser: pick the WarDogs window (+audio on Windows Chrome 141+; Linux/Mac video only), streams as `<user>/HelmetCam-<date>`; no OBS needed | Live portal → Go Live | Discord; key fetched for the logged-in user — **verified end to end** (publish → `.live` → sweep → title folder) | | Devs | Host code, review, CI | Gitea `https://git.…/`, ssh 2222 | Discord (Authentik), group `git-users`; `git-admins` = site admin | | Bot dev | Push code, restart container, read logs; web UI at `https://bot.…/` | Gitea + compose; `bot/AGENT.md` | web UI behind Discord (Authentik), once its provider exists | | Admin | Hostnames, bot token/guild, webhook; streamers/editors are Discord roles → Authentik groups | `./configure.py` | ssh | -| Admin | Watch pairings / folder states (gamers self-serve the setup) | Syncthing UI `https://ovh1…/sync/` | Discord + group `media-admins` (Syncthing's own login disabled) | +| Admin | Watch pairings / folder states (gamers self-serve the setup) | Syncthing UI `https://sync.…/` | Discord + group `media-admins` (Syncthing's own login disabled) | | Admin | Groups, who may log in, app access | Authentik `https://auth.…/` | Discord or akadmin | | Admin | Install/upgrade the Jellyfin plugin | `./installJellyfinPlugin.sh` | ssh | | Admin | Register the CI runner | `./registerGiteaRunner.sh` | ssh | @@ -51,10 +51,10 @@ Source of truth: this repo, `techy/Media-Server-Setup` on the stack's own Gitea |---|---|---| | MediaMTX | RTMP/SRT ingest, records fMP4; publish auth delegated to dumpd (tokens via `/mtx-auth`) | live, token auth verified | | copyparty | browse/scrub/download/finals upload | behind Authentik, group permissions (`editors`, `media-admins`), Discord-only, no WebDAV | live | -| dumpd + dock | dump, stream tokens, MediaMTX auth hook, `/time`, `/me` identity, `/me/clips` Syncthing self-service; OBS dock with timecode-band setup | identity from Authentik headers | live | -| `/about/` gateway, `/clips/` self-service | static pages behind the gate | — | live (first real pairing pending) | +| dumpd + dock | dump, stream tokens, MediaMTX auth hook, `/time`, `/me` identity (on `live.`), `/me/clips` Syncthing self-service (on `clips.`); OBS dock with timecode-band setup | identity from Authentik headers | live | +| `media.` gateway, `clips.` self-service | static pages behind the gate | — | live (first real pairing pending) | | Timecode | `bin/vgstrip.py` decoder, `tmcd` + sidecar in the sweep, `/dock/clock/` strip page, Go Live band composite, portal crop | server clock is the reference | **live, verified** (OBS: frame-accurate, 0 gaps) | -| Syncthing | clip intake, UI at `/sync/` | UI behind Authentik + `media-admins`, own GUI auth off | live | +| Syncthing | clip intake, UI at `sync.` | UI behind Authentik + `media-admins`, own GUI auth off | live | | normalize sweep | editing standard, one copy per clip | live | | nftables | host firewall (22, 80/443, 1935, 2222, 3306, 8890/udp, 22000) | live | | Caddy | TLS + routing for all hostnames | live | @@ -70,6 +70,8 @@ Source of truth: this repo, `techy/Media-Server-Setup` on the stack's own Gitea ## Unfinished, in rough priority +0. **Hostname restructure deploy** (committed 2026-11-01): DNS CNAMEs `media`, `files`, `clips`, `sync`; `./configure.py --full` with `media.` as main; MediaMTX restart between streams; `./deployLive.sh`; streamers re-add the dock at `live./dock/` and redo "Add band + clock source"; re-pin Discord links. Procedure C in HOSTNAME-CHANGE.md. + 1. Roles→groups policy: bound and updated (revokes only its own grants); will be proven by the next natural Discord logins (`/whoami` should show `bot-users`, `git-admins`). `akadmin` remains the recovery login if a policy misfires. 2. Confirm plugin 0.1.3 under Jellyfin → Plugins and the Gitea runner registered. 3. Portal viewer during a real stream: corner chip `webrtc`; Windows Go Live with WarDogs audio (first Windows streamer). diff --git a/about/index.html b/about/index.html index e5227dd..f66d8d9 100644 --- a/about/index.html +++ b/about/index.html @@ -47,7 +47,7 @@ <h2>Contribute: stream</h2> <p class="lead" data-group="streamers" data-yes="You are in <code>streamers</code>: you can publish under your own name. No passwords, no stream keys to keep." data-no="Admin view — you are <b>not</b> in <code>streamers</code>, so Go Live will refuse a token for you until you get the role."></p> <div class="links"> - <a class="card" href="/dock/"><b>OBS dock</b><span>title → Go Live; Dump tonight; manual setup</span></a> + <a class="card" id="l-dock" href="#"><b>OBS dock</b><span>title → Go Live; Dump tonight; manual setup</span></a> <a class="card" id="l-golive" href="#"><b>Go Live from the browser</b><span>Windows Chrome: share the game window, no OBS</span></a> </div> <details><summary>OBS setup (once)</summary> @@ -72,12 +72,12 @@ <h2>Contribute: your game clips</h2> <p class="lead">Anyone in the server. Pair your gaming PC once; after that your clips upload themselves and appear in Jellyfin under your name.</p> <div class="links"> - <a class="card" href="/clips/"><b>Clip setup & status</b><span>pair your PC, see if it is connected and syncing</span></a> + <a class="card" id="l-clips" href="#"><b>Clip setup & status</b><span>pair your PC, see if it is connected and syncing</span></a> </div> <details open><summary>Three steps</summary> <ol> <li>Install <a href="https://syncthing.net/downloads/" target="_blank" rel="noopener">Syncthing</a> on the PC your clips save to (Windows: Syncthing Tray works well). Open it → Actions → Show ID.</li> - <li>Paste that Device ID on the <a href="/clips/">clips page</a> and press Connect.</li> + <li>Paste that Device ID on the <a class="l-clips" href="#">clips page</a> and press Connect.</li> <li>Back in your Syncthing: accept the new device <b>Vanguard</b>, then accept the folder it offers, point it at your clip directory (ShadowPlay / Medal / Steam), Folder Type <b>Send Only</b>.</li> </ol> <ul> @@ -91,8 +91,8 @@ <h2>Edit</h2> <p class="lead" data-group="editors" data-yes="You are in <code>editors</code>: read streams and clips, write finals." data-no="Admin view — you are not in <code>editors</code>; your copyparty rights come from <code>media-admins</code> instead."></p> <div class="links"> - <a class="card" href="/"><b>Browse & download</b><span>copyparty — scrub in the browser, download originals, zip folders</span></a> - <a class="card" href="/finals/"><b>Upload finals</b><span>the only folder editors can write to</span></a> + <a class="card" id="l-files" href="#"><b>Browse & download</b><span>copyparty — scrub in the browser, download originals, zip folders</span></a> + <a class="card" id="l-finals" href="#"><b>Upload finals</b><span>the only folder editors can write to</span></a> </div> <details><summary>Multicam & timecode</summary> <ul> @@ -108,15 +108,15 @@ <h2>Media admin</h2> <p class="lead" data-group="media-admins" data-yes="You are in <code>media-admins</code>: full rights on every folder, trash, Syncthing, dump anyone's night." data-no="Admin view — you are <b>not</b> in <code>media-admins</code>: copyparty will show you nothing and <code>/sync/</code> will 403 until you get the role."></p> <div class="links"> - <a class="card" href="/trash/"><b>Trash</b><span>dumped nights, purged after 3 days</span></a> - <a class="card" href="/sync/"><b>Syncthing</b><span>device pairing, folder states</span></a> + <a class="card" id="l-trash" href="#"><b>Trash</b><span>dumped nights, purged after 3 days</span></a> + <a class="card" id="l-sync" href="#"><b>Syncthing</b><span>device pairing, folder states</span></a> <a class="card" href="/whoami"><b>Who am I</b><span>identity headers as the apps see them</span></a> </div> <details><summary>Admin notes</summary> <ul> <li>Group changes in Authentik apply on the next login: <a href="/logout">sign out</a> and back in.</li> <li>Dump anyone's night: the dock's Dump works on your own; for others use the API with <code>{"user":"name"}</code> or ask the owner.</li> - <li>New gamer: they self-serve on <a href="/clips/">/clips/</a>; you only need to watch Syncthing accept the device.</li> + <li>New gamer: they self-serve on the <a class="l-clips" href="#">clips page</a>; you only need to watch Syncthing accept the device.</li> </ul> </details> </section> @@ -156,7 +156,7 @@ </details> </section> - <footer>Vanguard Media · <a href="/about/">/about</a> · <a href="/logout">sign out</a></footer> + <footer>Vanguard Media · <a href="/">home</a> · <a href="/logout">sign out</a></footer> </main> <script> const $ = id => document.getElementById(id); @@ -165,7 +165,9 @@ const sub = s => `https://${s}.${base}`; $('l-watch').href = sub('watch'); $('l-live').href = sub('live'); $('l-golive').href = sub('live') + '/#golive'; $('l-git').href = sub('git'); $('l-bot').href = sub('bot'); $('l-auth').href = sub('auth') + '/if/admin/'; $('l-jfadmin').href = sub('watch') + '/web/#/dashboard'; $('l-gitadmin').href = sub('git') + '/-/admin'; -$('dockurl').textContent = location.origin + '/dock/'; $('gitssh').textContent = `ssh://git@${'git.' + base}:2222/org/repo.git`; +$('l-dock').href = sub('live') + '/dock/'; $('dockurl').textContent = sub('live') + '/dock/'; +$('l-files').href = sub('files') + '/'; $('l-finals').href = sub('files') + '/finals/'; $('l-trash').href = sub('files') + '/trash/'; $('l-sync').href = sub('sync') + '/'; +$('l-clips').href = sub('clips') + '/'; document.querySelectorAll('a.l-clips').forEach(a => a.href = sub('clips') + '/'); $('gitssh').textContent = `ssh://git@${'git.' + base}:2222/org/repo.git`; (async () => { try { const r = await fetch('/me', {credentials:'same-origin'}); const d = await r.json(); if (!r.ok) throw new Error(d.error || r.status); diff --git a/addCommunityMediaServer.sh b/addCommunityMediaServer.sh index 09f061a..438416a 100755 --- a/addCommunityMediaServer.sh +++ b/addCommunityMediaServer.sh @@ -24,7 +24,7 @@ read -r AUTH_CUR WATCH_CUR < <(python3 -c 'import configure as c; h=c.read_hosts BASE="${MAIN#*.}"; [[ $MAIN == *.*.* ]] || BASE="$MAIN" say "Community media server — Phase 1 (Authentik + Jellyfin) on $MAIN" -echo "Needs two DNS names as CNAMEs to $MAIN. Create them now if you haven't." +echo "Every hostname below is a CNAME to $MAIN. Create them now if you haven't." AUTH=$(ask "Authentik hostname" "${AUTH_CUR:-auth.$BASE}") WATCH=$(ask "Jellyfin hostname" "${WATCH_CUR:-watch.$BASE}") GIT_CUR=$(python3 -c 'import configure as c; print(c.read_hosts().get("git",""))') @@ -33,11 +33,17 @@ BOT_CUR=$(python3 -c 'import configure as c; print(c.read_hosts().get("bot","")) BOT=$(ask "Bot web UI hostname" "${BOT_CUR:-bot.$BASE}") LIVE_CUR=$(python3 -c 'import configure as c; print(c.read_hosts().get("live",""))') LIVE=$(ask "Live portal hostname" "${LIVE_CUR:-live.$BASE}") +FILES_CUR=$(python3 -c 'import configure as c; print(c.read_hosts().get("files",""))') +FILES=$(ask "Files (copyparty) hostname" "${FILES_CUR:-files.$BASE}") +SYNC_CUR=$(python3 -c 'import configure as c; print(c.read_hosts().get("sync",""))') +SYNC=$(ask "Syncthing UI hostname" "${SYNC_CUR:-sync.$BASE}") +CLIPS_CUR=$(python3 -c 'import configure as c; print(c.read_hosts().get("clips",""))') +CLIPS=$(ask "Clips self-service hostname" "${CLIPS_CUR:-clips.$BASE}") # ---- DNS sanity ---------------------------------------------------------------------------------- say "DNS check" MAIN_IP=$(ip_of "$MAIN"); ok=1 -for h in "$AUTH" "$WATCH" "$GIT" "$BOT" "$LIVE"; do +for h in "$AUTH" "$WATCH" "$GIT" "$BOT" "$LIVE" "$FILES" "$SYNC" "$CLIPS"; do ip=$(ip_of "$h") if [[ -z $ip ]]; then echo " $h: does not resolve yet"; ok=0 elif [[ $ip != "$MAIN_IP" ]]; then echo " $h -> $ip (main is $MAIN_IP) — points somewhere else"; ok=0 @@ -50,7 +56,7 @@ fi # ---- write configs, .env, start containers ----------------------------------------------------- say "Applying (configure.py --hosts)" -VG_DRY=$DRY python3 configure.py --hosts "$AUTH" "$WATCH" "$GIT" "$BOT" "$LIVE" | sed -n '/== writing/,$p' | grep -vE '^ (techy|diceofire|[a-z0-9_-]+) password' || true +VG_DRY=$DRY python3 configure.py --hosts "$AUTH" "$WATCH" "$GIT" "$BOT" "$LIVE" "$FILES" "$SYNC" "$CLIPS" | sed -n '/== writing/,$p' | grep -vE '^ (techy|diceofire|[a-z0-9_-]+) password' || true [[ $DRY == 1 ]] && { echo "(dry run: skipping container wait)"; exit 0; } # ---- wait --------------------------------------------------------------------------------------- @@ -150,7 +156,7 @@ PHASE 3 — Jellyfin (direct play only on this CPU) Custom CSS: paste jellyfin/branding.css -> Save; reload https://$WATCH/ -AUTHENTIK FORWARD-AUTH (gates https://$LIVE/ and https://$BOT/; later the main host) — once: +AUTHENTIK FORWARD-AUTH (gates every Vanguard host: $MAIN, $FILES, $CLIPS, $SYNC, $LIVE, $BOT) — once: 1. Applications -> Outposts -> edit "authentik Embedded Outpost" -> config: authentik_host: https://$AUTH 2. Applications -> Create with provider -> Application "Vanguard sites"; Proxy Provider, authorization flow implicit-consent, mode "Forward auth (domain level)", authentication URL https://$AUTH, @@ -163,9 +169,9 @@ AUTHENTIK FORWARD-AUTH (gates https://$LIVE/ and https://$BOT/; later the main h "discord-roles-to-groups" from authentik/discord-roles-to-groups.py (server id + role-id->group table); bind to default-source-enrollment AND default-source-authentication (engine mode all). 5. Test: private window -> https://$LIVE/ -> Discord -> portal; https://$BOT/ -> 403 unless you hold the - bot role; https://$LIVE/me/stream-key shows your username. + bot role; https://$LIVE/me shows your username and groups. -LIVE PORTAL (https://$LIVE/) +LIVE PORTAL (https://$LIVE/) — also hosts the OBS dock (https://$LIVE/dock/) and the timecode clock source. Deploy the page from the dev box: ./deployLive.sh (builds live/ and copies dist/ to /srv/live). WebRTC viewers and Go Live need UDP/TCP 8189 open (firewall rule included). Optional "is live" Discord post: LIVE_WEBHOOK_URL in .env (asked by configure.py --full). diff --git a/configure.py b/configure.py index b2e97fd..d2d28b0 100755 --- a/configure.py +++ b/configure.py @@ -6,8 +6,12 @@ ./configure.py --show print current streamers, editors, domain and the OBS settings ./configure.py --apply regenerate everything from the values already on disk and apply (non-interactive; used by deploy.sh). Fails if the main hostname was never set. - ./configure.py --hosts AUTH WATCH [GIT [BOT [LIVE]]] set the Authentik/Jellyfin/Gitea/bot/live hostnames and apply (non-interactive; - used by addCommunityMediaServer.sh) + ./configure.py --hosts AUTH WATCH [GIT [BOT [LIVE [FILES [SYNC [CLIPS]]]]]] + set the other hostnames and apply (non-interactive; used by addCommunityMediaServer.sh). + Pass "" to leave one unset. + +Hosts: main = the gateway page at /, files = copyparty, clips = clip intake, sync = Syncthing UI, + live = portal + OBS dock (/dock/, /dock/clock/, /dump, /me, /time), auth, watch, git, bot. Reads : Caddyfile (hostnames), mediamtx.yml (streamers), .env (editors/admins are Authentik groups now: editors, media-admins, admins — nothing local) @@ -154,8 +158,9 @@ def gate(required_group=""): """ return block -def write_caddy(domain, auth_host="", watch_host="", git_host="", bot_host="", live_host=""): +def write_caddy(domain, auth_host="", watch_host="", git_host="", bot_host="", live_host="", files_host="", sync_host="", clips_host=""): env = read_env() + gate_any = gate("") extra = "" apex, target = env.get("APEX_HOST", ""), env.get("APEX_REDIRECT", "") if apex and target: @@ -164,18 +169,70 @@ def write_caddy(domain, auth_host="", watch_host="", git_host="", bot_host="", l {apex} {{ redir {target} 302 }} +""" + if files_host: + extra += f""" +# vanguard:files +{files_host} {{ + encode zstd gzip +{gate_any} # copyparty at the root. It honours X-Authentik-Username / -Groups only when the request also carries a header + # whose secret NAME only this proxy knows (copyparty idp-h-key). Clients cannot forge identity. + reverse_proxy 127.0.0.1:3923 {{ + header_up X-Vanguard-{env.get("COPYPARTY_IDP_KEY", "key")} 1 + flush_interval -1 + }} +}} +""" + if sync_host: + extra += f""" +# vanguard:sync +{sync_host} {{ + encode zstd gzip +{gate(env.get("SYNC_REQUIRE_GROUP", "media-admins"))} # Syncthing admin UI (its own GUI login is disabled; the gate + group is the login) + reverse_proxy 127.0.0.1:8384 {{ + header_up Host {{upstream_hostport}} + }} +}} +""" + if clips_host: + extra += f""" +# vanguard:clips +{clips_host} {{ + encode zstd gzip +{gate_any} @me path /me /me/* + handle @me {{ + reverse_proxy 127.0.0.1:8080 # dumpd: identity + /me/clips self-service + }} + handle {{ + root * /srv/clips + try_files {{path}} /index.html + file_server + }} +}} """ if live_host: extra += f""" # vanguard:live {live_host} {{ encode zstd gzip -{gate(env.get("LIVE_REQUIRE_GROUP", ""))} @me path /me /me/* +{gate(env.get("LIVE_REQUIRE_GROUP", ""))} handle /whoami {{ + header Content-Type text/plain + respond "user: {{http.request.header.X-Authentik-Username}} +groups: {{http.request.header.X-Authentik-Groups}}" + }} + @me path /me /me/* handle @me {{ - reverse_proxy 127.0.0.1:8080 # dumpd: identity + /me/stream-token for the logged-in streamer (Go Live) + reverse_proxy 127.0.0.1:8080 # dumpd: identity, stream tokens (Go Live + dock) }} handle /time {{ - reverse_proxy 127.0.0.1:8080 # server clock for the timecode strip + reverse_proxy 127.0.0.1:8080 # server clock for the timecode strip (clock page + Go Live) + }} + handle /dump* {{ + reverse_proxy 127.0.0.1:8080 # dump tonight (dock) + }} + handle_path /dock* {{ + root * /srv/dock + file_server # OBS browser dock + /dock/clock/ }} handle_path /api/* {{ reverse_proxy 127.0.0.1:9997 @@ -231,24 +288,11 @@ def write_caddy(domain, auth_host="", watch_host="", git_host="", bot_host="", l encode zstd gzip reverse_proxy 127.0.0.1:8096 }} -""" - sync_group = env.get("SYNC_REQUIRE_GROUP", "media-admins") - sync_gate = "" - if sync_group: - g = re.escape(sync_group) - sync_gate = f""" @sync_lacks_group {{ - path /sync /sync/* - not header_regexp X-Authentik-Groups (^|\\|){g}(\\||$) - }} - handle @sync_lacks_group {{ - respond "Syncthing admin is for the '{sync_group}' group." 403 - }} """ Path("Caddyfile").write_text(f"""# vanguard:main {domain} {{ encode zstd gzip -{AUTHENTIK_FORWARD_AUTH} - # who does Authentik say I am? (debug: shows exactly the identity headers the apps receive) +{gate_any} # who does Authentik say I am? (debug: shows exactly the identity headers the apps receive) handle /whoami {{ header Content-Type text/plain respond "user: {{http.request.header.X-Authentik-Username}} @@ -256,60 +300,24 @@ groups: {{http.request.header.X-Authentik-Groups}} email: {{http.request.header.X-Authentik-Email}} name: {{http.request.header.X-Authentik-Name}}" }} - - # dump endpoint + stream tokens (python service on the host; identity from the X-Authentik-* headers Caddy just set) - handle /dump* {{ - reverse_proxy 127.0.0.1:8080 - }} @me path /me /me/* handle @me {{ - reverse_proxy 127.0.0.1:8080 # dumpd: identity, stream tokens, clips self-service + reverse_proxy 127.0.0.1:8080 # dumpd: identity for the gateway page }} - handle /time {{ - reverse_proxy 127.0.0.1:8080 # server clock for the timecode strip - }} - - # Syncthing admin UI: Authentik + group {sync_group or 'any'} (Syncthing's own GUI login is disabled) -{sync_gate} redir /sync /sync/ 308 - handle_path /sync/* {{ - reverse_proxy 127.0.0.1:8384 {{ - header_up Host {{upstream_hostport}} - }} - }} - - # OBS browser dock (static) - handle_path /dock* {{ - root * /srv/dock - file_server - }} - - # gateway page: role-aware links + instructions (static; talks to /me) - redir /about /about/ 308 - handle_path /about/* {{ - root * /srv/about - file_server - }} - - # clip intake self-service (static page; talks to /me/clips) - redir /clips /clips/ 308 - handle_path /clips/* {{ - root * /srv/clips - file_server - }} - - # copyparty. It honours X-Authentik-Username / -Groups only when the request also carries a header whose - # secret NAME only this proxy knows (copyparty idp-h-key). Clients cannot forge identity. + redir /about* / 308 # old bookmarks: the gateway page used to live at /about/ handle {{ - reverse_proxy 127.0.0.1:3923 {{ - header_up X-Vanguard-{env.get("COPYPARTY_IDP_KEY", "key")} 1 - flush_interval -1 - }} + root * /srv/about + try_files {{path}} /index.html + file_server # the gateway page is the root of the main host }} }} """ + extra) -def write_env(watch_host, discord_token=None, discord_guild=None, git_host="", bot_host="", live_host="", main_host="", live_webhook=None): +def write_env(watch_host, discord_token=None, discord_guild=None, git_host="", bot_host="", live_host="", main_host="", live_webhook=None, files_host="", sync_host="", clips_host=""): env = read_env() + if files_host: env["FILES_HOST"] = files_host + if sync_host: env["SYNC_HOST"] = sync_host + if clips_host: env["CLIPS_HOST"] = clips_host env["LIVE_HOST"] = live_host or env.get("LIVE_HOST", "live.example.com") if main_host: env["MAIN_HOST"] = main_host if live_webhook is not None: env["LIVE_WEBHOOK_URL"] = live_webhook @@ -474,15 +482,15 @@ def run(cmd, **kw): if DRY: return 0 return subprocess.call(cmd, **kw) -def apply(domain, admin_pw, streamers, editors, auth_host="", watch_host="", discord_token=None, discord_guild=None, git_host="", bot_host="", live_host="", live_webhook=None): +def apply(domain, admin_pw, streamers, editors, auth_host="", watch_host="", discord_token=None, discord_guild=None, git_host="", bot_host="", live_host="", live_webhook=None, files_host="", sync_host="", clips_host=""): # admin_pw / editors are legacy no-ops: access is by Authentik group now if not streamers and Path("mediamtx.yml").exists() and "CHANGEME" in Path("mediamtx.yml").read_text(): print("\n!! mediamtx.yml contains CHANGEME placeholders — it looks like the repo template was copied over the") print(" generated config. Applying now would leave NO streamers (OBS ingest and the dump button stop working).") if not yes(" continue anyway?", False): print("aborted; add streamers first ([s] in the menu)"); sys.exit(1) print("\n== writing configs") - write_env(watch_host, discord_token, discord_guild, git_host, bot_host, live_host, domain, live_webhook) - write_caddy(domain, auth_host, watch_host, git_host, bot_host, live_host); write_mediamtx(domain); write_copyparty() + write_env(watch_host, discord_token, discord_guild, git_host, bot_host, live_host, domain, live_webhook, files_host, sync_host, clips_host) + write_caddy(domain, auth_host, watch_host, git_host, bot_host, live_host, files_host, sync_host, clips_host); write_mediamtx(domain); write_copyparty() Path("stream-token.secret").write_text(read_env()["STREAM_TOKEN_SECRET"] + "\n"); os.chmod("stream-token.secret", 0o600) run(["sudo", "install", "-d", "-o", os.environ.get("SUDO_USER") or os.environ.get("USER") or "debian", "/srv/live"]) print("== app data folders") @@ -554,11 +562,14 @@ def post_start_fixups(dc): if DRY: print(" (dry) installJellyfinPlugin.sh") else: subprocess.run(["bash", "installJellyfinPlugin.sh"], capture_output=True, text=True); print(" jellyfin plugin: ensured") -def show(domain, admin_pw, streamers, editors, auth_host="", watch_host="", git_host="", bot_host="", live_host=""): - print(f"\n== domain : {domain or '(not set)'}") +def show(domain, admin_pw, streamers, editors, auth_host="", watch_host="", git_host="", bot_host="", live_host="", files_host="", sync_host="", clips_host=""): + print(f"\n== gateway : https://{domain or 'HOST'}/ (/whoami, /logout)") + print(f"== files : https://{files_host}/ (copyparty; editors / media-admins)" if files_host else "== files : (not set — run --full)") + print(f"== clips : https://{clips_host}/ (self-service clip intake, any member)" if clips_host else "== clips : (not set — run --full)") + print(f"== sync : https://{sync_host}/ (Syncthing UI, media-admins)" if sync_host else "== sync : (not set — run --full)") _e = read_env() if _e.get("APEX_HOST") and _e.get("APEX_REDIRECT"): print(f"== apex : https://{_e['APEX_HOST']}/ -> {_e['APEX_REDIRECT']}") - print(f"== live : https://{live_host}/ (behind Authentik forward-auth; deploy live/dist -> /srv/live; WebRTC needs 8189 open)" if live_host else "== live : (not set — run --full)") + print(f"== live : https://{live_host}/ (portal; /dock/ for OBS; WebRTC needs 8189 open)" if live_host else "== live : (not set — run --full)") print(f"== bot web : https://{bot_host}/ (Authentik forward-auth; Caddy requires group '{read_env().get('BOT_REQUIRE_GROUP','bot-users') or 'any'}')" if bot_host else "== bot web : (not set — run --full)") print(f"== gitea : https://{git_host}/ ssh port 2222 admin {read_env().get('GITEA_ADMIN_USER','vanguard')} / {read_env().get('GITEA_ADMIN_PASSWORD','?')} (runner: ./registerGiteaRunner.sh)" if git_host else "== gitea : (not set — run --full)") print(f"== authentik: https://{auth_host}/ akadmin password = AUTHENTIK_BOOTSTRAP_PASSWORD in .env (set on first start; change it in the UI)" if auth_host else "== authentik: (not set — run --full)") @@ -572,11 +583,10 @@ def show(domain, admin_pw, streamers, editors, auth_host="", watch_host="", git_ print("== access : by Authentik group — editors (read streams/clips, write finals), media-admins (everything), admins (dump anyone)") print(" Discord login only — no WebDAV; downloads via the web UI") print("== streamers: anyone in the Authentik 'streamers' group (Discord role). No passwords, no keys to hand out.") - print(f" OBS: add the dock https://{domain or 'HOST'}/dock/ in OBS, log in with Discord, Go Live — it mints a 12 h token") + print(f" OBS: add the dock https://{live_host or 'LIVE'}/dock/ in OBS, log in with Discord, Go Live — it mints a 12 h token") print(f" and sets the stream key itself. Manual setups: dock → 'Manual OBS setup' (rtmp://{domain or 'HOST'}:1935/ + key).") - print(f"\n editors log in with Discord at https://{domain or 'HOST'}/") - print(f" verify: curl -sI https://{domain or 'HOST'}/dock/ | head -1 (expect 200)") - print(f" curl -s -o /dev/null -w '%{{http_code}}\\n' https://{domain or 'HOST'}/dump (expect 401)\n") + print(f"\n editors: https://{files_host or 'FILES'}/ everyone: https://{domain or 'HOST'}/") + print(f" verify: curl -s -o /dev/null -w '%{{http_code}}\\n' https://{domain or 'HOST'}/ (expect 302 → Authentik)\n") # ------------------------------------------------------------------ modes def manage_users(kind, users): @@ -596,12 +606,15 @@ def manage_users(kind, users): else: users[n] = ask_pw(n, users[n]) -def full(domain, admin_pw, streamers, editors, auth_host="", watch_host="", git_host="", bot_host="", live_host=""): +def full(domain, admin_pw, streamers, editors, auth_host="", watch_host="", git_host="", bot_host="", live_host="", files_host="", sync_host="", clips_host=""): print("== FULL SETUP (enter keeps the shown default)\n") while True: - domain = ask("public hostname (DNS A record must point here)", domain) + domain = ask("main hostname = the gateway page (DNS A record must point here)", domain) if domain and "." in domain and " " not in domain: break print(" need a hostname like media.example.com") + files_host = ask("Files (copyparty) hostname (CNAME to the above)", files_host or default_sub(domain, "files")) + clips_host = ask("Clips self-service hostname (CNAME to the above)", clips_host or default_sub(domain, "clips")) + sync_host = ask("Syncthing UI hostname (CNAME to the above)", sync_host or default_sub(domain, "sync")) auth_host = ask("Authentik hostname (CNAME to the above)", auth_host or default_sub(domain, "auth")) watch_host = ask("Jellyfin hostname (CNAME to the above)", watch_host or default_sub(domain, "watch")) git_host = ask("Gitea hostname (CNAME to the above)", git_host or default_sub(domain, "git")) @@ -609,7 +622,7 @@ def full(domain, admin_pw, streamers, editors, auth_host="", watch_host="", git_ live_host = ask("Live portal hostname (CNAME to the above)", live_host or default_sub(domain, "live")) env0 = read_env(); parent = default_sub(domain, "x")[2:] if domain else "" apex = ask("Bare domain to serve too (A record must point here; blank = skip)", env0.get("APEX_HOST", parent)) - apex_to = ask(" bare domain redirects to", env0.get("APEX_REDIRECT") or (f"https://{watch_host}/" if watch_host else "")) if apex else "" + apex_to = ask(" bare domain redirects to", env0.get("APEX_REDIRECT") or f"https://{domain}/") if apex else "" print("\n-- Discord bot (optional; the Bot added to the Authentik Discord application). Enter keeps the current value.") discord_token, discord_guild = ask_discord() live_webhook = ask(" Discord webhook URL for 'is live' notices (optional)", read_env().get("LIVE_WEBHOOK_URL", "")) @@ -636,7 +649,7 @@ def full(domain, admin_pw, streamers, editors, auth_host="", watch_host="", git_ Path(".env").write_text("".join(f"{k}={v}\n" for k, v in e.items())) if Path(".env").exists() else None if not Path(".env").exists(): Path(".env").write_text(f"APEX_HOST={apex}\nAPEX_REDIRECT={apex_to}\n"); os.chmod(".env", 0o600) - return domain, admin_pw, streamers, editors, auth_host, watch_host, discord_token, discord_guild, git_host, bot_host, live_host, live_webhook + return domain, admin_pw, streamers, editors, auth_host, watch_host, discord_token, discord_guild, git_host, bot_host, live_host, live_webhook, files_host, sync_host, clips_host def ask_discord(): env = read_env() @@ -663,27 +676,28 @@ def main(): mode = sys.argv[1] if len(sys.argv) > 1 else "" hosts = read_hosts() domain, auth_host, watch_host, git_host, bot_host, live_host = hosts.get("main", ""), hosts.get("auth", ""), hosts.get("watch", ""), hosts.get("git", ""), hosts.get("bot", ""), hosts.get("live", "") + files_host, sync_host, clips_host = hosts.get("files", ""), hosts.get("sync", ""), hosts.get("clips", "") streamers, accounts = read_streamers(), read_accounts() admin_pw = accounts.pop("admin", "") editors = accounts if mode == "--show": - show(domain, admin_pw, streamers, editors, auth_host, watch_host, git_host, bot_host, live_host); return + show(domain, admin_pw, streamers, editors, auth_host, watch_host, git_host, bot_host, live_host, files_host, sync_host, clips_host); return if mode == "--apply": if not domain: print("domain not set yet — run ./configure.py --full first"); sys.exit(1) - apply(domain, admin_pw, streamers, editors, auth_host, watch_host, git_host=git_host, bot_host=bot_host, live_host=live_host) - show(domain, admin_pw, streamers, editors, auth_host, watch_host, git_host, bot_host, live_host); return + apply(domain, admin_pw, streamers, editors, auth_host, watch_host, git_host=git_host, bot_host=bot_host, live_host=live_host, files_host=files_host, sync_host=sync_host, clips_host=clips_host) + show(domain, admin_pw, streamers, editors, auth_host, watch_host, git_host, bot_host, live_host, files_host, sync_host, clips_host); return if mode == "--hosts": - if len(sys.argv) not in (4, 5, 6, 7): print("usage: configure.py --hosts AUTH_HOST WATCH_HOST [GIT_HOST [BOT_HOST [LIVE_HOST]]]"); sys.exit(2) + if not 4 <= len(sys.argv) <= 10: print("usage: configure.py --hosts AUTH WATCH [GIT [BOT [LIVE [FILES [SYNC [CLIPS]]]]]]"); sys.exit(2) if not domain: print("domain not set yet — run ./configure.py --full first"); sys.exit(1) - auth_host, watch_host = sys.argv[2].strip(), sys.argv[3].strip() - if len(sys.argv) >= 5: git_host = sys.argv[4].strip() - if len(sys.argv) >= 6: bot_host = sys.argv[5].strip() - if len(sys.argv) == 7: live_host = sys.argv[6].strip() - apply(domain, admin_pw, streamers, editors, auth_host, watch_host, git_host=git_host, bot_host=bot_host, live_host=live_host) - show(domain, admin_pw, streamers, editors, auth_host, watch_host, git_host, bot_host, live_host); return + a = [x.strip() for x in sys.argv[2:]] + [None] * 8 + auth_host, watch_host = a[0], a[1] + git_host, bot_host, live_host = (a[2] if a[2] is not None else git_host), (a[3] if a[3] is not None else bot_host), (a[4] if a[4] is not None else live_host) + files_host, sync_host, clips_host = (a[5] if a[5] is not None else files_host), (a[6] if a[6] is not None else sync_host), (a[7] if a[7] is not None else clips_host) + apply(domain, admin_pw, streamers, editors, auth_host, watch_host, git_host=git_host, bot_host=bot_host, live_host=live_host, files_host=files_host, sync_host=sync_host, clips_host=clips_host) + show(domain, admin_pw, streamers, editors, auth_host, watch_host, git_host, bot_host, live_host, files_host, sync_host, clips_host); return discord_token = discord_guild = live_webhook = None if mode == "--full": - domain, admin_pw, streamers, editors, auth_host, watch_host, discord_token, discord_guild, git_host, bot_host, live_host, live_webhook = full(domain, admin_pw, streamers, editors, auth_host, watch_host, git_host, bot_host, live_host) + domain, admin_pw, streamers, editors, auth_host, watch_host, discord_token, discord_guild, git_host, bot_host, live_host, live_webhook, files_host, sync_host, clips_host = full(domain, admin_pw, streamers, editors, auth_host, watch_host, git_host, bot_host, live_host, files_host, sync_host, clips_host) if not yes("\napply now (write configs, restart services)?"): print("nothing written"); return elif mode == "": r = normal(domain, admin_pw, streamers, editors) @@ -691,8 +705,8 @@ def main(): discord_token, discord_guild = r else: print(__doc__); sys.exit(2) - apply(domain, admin_pw, streamers, editors, auth_host, watch_host, discord_token, discord_guild, git_host, bot_host, live_host, live_webhook) - show(domain, admin_pw, streamers, editors, auth_host, watch_host, git_host, bot_host, live_host) + apply(domain, admin_pw, streamers, editors, auth_host, watch_host, discord_token, discord_guild, git_host, bot_host, live_host, live_webhook, files_host, sync_host, clips_host) + show(domain, admin_pw, streamers, editors, auth_host, watch_host, git_host, bot_host, live_host, files_host, sync_host, clips_host) if __name__ == "__main__": try: main() diff --git a/deployLive.sh b/deployLive.sh index e4385a4..ef44bd9 100755 --- a/deployLive.sh +++ b/deployLive.sh @@ -2,7 +2,7 @@ # Build the live portal here and push the static files to /srv/live on the server. Caddy serves them as-is. set -euo pipefail cd "$(dirname "$0")/live" -HOST="${1:-debian@ovh1.dumpstervigilantes.rip}" +HOST="${1:-debian@media.dumpstervigilantes.rip}" # any name of the box works for ssh npm install --no-audit --no-fund --loglevel=error npm run build ssh "$HOST" 'sudo install -d -o "$(id -u)" -g "$(id -g)" /srv/live && rm -rf /srv/live/*'