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 <noreply@anthropic.com>
This commit is contained in:
2026-11-01 01:38:35 -05:00
parent c1a46d66dc
commit 18f4e07de8
9 changed files with 418 additions and 181 deletions

View File

@@ -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
}
# 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}"
}
@me path /me /me/*
handle @me {
reverse_proxy 127.0.0.1:8080 # dumpd: identity for the gateway page
}
redir /about* / 308 # old bookmarks: the gateway page used to live at /about/
handle {
root * /srv/about
try_files {path} /index.html
file_server # the gateway page is the root of the main host
}
}
# 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/* {
# 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}
}
}
}
# OBS browser dock (static)
handle_path /dock* {
root * /srv/dock
# 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
}
}
# everything else is copyparty (web UI + WebDAV)
handle {
reverse_proxy 127.0.0.1:3923 {
flush_interval -1
# 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
}

View File

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

View File

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

View File

@@ -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 (`<you>/<Title>`) 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 |

View File

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

View File

@@ -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 &amp; 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 &amp; 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 &amp; 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 &amp; 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 &amp; 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);

View File

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

View File

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

View File

@@ -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/*'