Files
Media-Server-Setup/about/index.html
2026-10-31 18:25:56 -05:00

189 lines
12 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Vanguard Media</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
:root{--bg:#141414;--fg:#e6e6e6;--mut:#9a9a9a;--acc:#3d8bfd;--ok:#2ecc71;--box:#1f1f1f;--line:#2e2e2e}
html,body{margin:0;background:var(--bg);color:var(--fg);font:15px/1.5 system-ui,sans-serif}
main{max-width:860px;margin:0 auto;padding:28px 16px 48px}
header{display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;margin-bottom:6px}
h1{font-size:26px;margin:0}.who{color:var(--mut)}.who b{color:var(--fg)}
.pills{margin:6px 0 22px}.pill{display:inline-block;background:#2a2a2a;border-radius:999px;padding:2px 10px;font-size:12px;margin:0 6px 6px 0;color:#bbb}
section{background:var(--box);border:1px solid var(--line);border-radius:14px;padding:18px 20px;margin-bottom:16px}
section h2{margin:0 0 4px;font-size:17px}section .lead{color:var(--mut);margin:0 0 12px}
.links{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:10px;margin:10px 0}
a.card{display:block;background:#262626;border:1px solid #333;border-radius:10px;padding:12px 14px;color:var(--fg);text-decoration:none}
a.card:hover{border-color:var(--acc)}a.card b{display:block;margin-bottom:2px}a.card span{color:var(--mut);font-size:13px}
details{margin-top:8px}summary{cursor:pointer;color:var(--mut);font-size:13px}
ol,ul{margin:8px 0 0;padding-left:20px}li{margin:4px 0}code{font-family:ui-monospace,monospace;background:#121212;padding:1px 6px;border-radius:5px}
.hidden{display:none}footer{color:var(--mut);font-size:13px;margin-top:20px}footer a{color:var(--mut)}
</style>
</head>
<body>
<main>
<header><h1>Vanguard Media</h1><div class="who">signed in as <b id="who">…</b></div></header>
<div class="pills" id="pills"></div>
<section id="s-everyone">
<h2>Watch</h2>
<p class="lead">Everything here uses your Discord login. One sign-in covers all of it.</p>
<div class="links">
<a class="card" id="l-watch" href="#"><b>Recordings &amp; clips</b><span>Jellyfin — by streamer, by day/week/month</span></a>
<a class="card" id="l-live" href="#"><b>Live now</b><span>every active stream in one page, audio follows the window you click</span></a>
<a class="card" href="/logout"><b>Sign out</b><span>ends the Discord session for all Vanguard sites</span></a>
</div>
<details><summary>How watching works</summary>
<ul>
<li>Jellyfin: each streamer is a show. This week's recordings sit under weekday seasons, older ones roll into week, month and year. Nothing moves; it is only re-labelled.</li>
<li>Live: windows open as people go live and close when they stop. Click a window to hear it; the speaker button pins audio on; the slider is per window.</li>
</ul>
</details>
</section>
<section id="s-clips">
<h2>Send 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>
</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>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>
<li>Only video files are pulled; your folder layout is mirrored; odd formats are converted on the server and one copy is kept per clip.</li>
<li>Nothing is ever written back to your PC.</li>
</ul>
</details>
</section>
<section id="s-streamers" class="hidden">
<h2>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-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>
<ol>
<li>OBS → Tools → WebSocket Server Settings → enable, set a password.</li>
<li>OBS → Docks → Custom Browser Docks → add <code id="dockurl">…/dock/</code>; log in with Discord inside the dock.</li>
<li>Dock → Settings: OBS WebSocket password (the rest is prefilled). Optional title prefix/suffix (<code>{date}</code> = today).</li>
<li>Dock → Timecode band → <b>Add band + clock source</b> while not streaming (gives editors a frame-accurate timecode).</li>
<li>Per stream: type a title, <b>Go Live</b>. The dock mints a 12-hour token and sets your stream key itself.</li>
</ol>
</details>
<details><summary>Rules worth knowing</summary>
<ul>
<li>Everything streamed is recorded and visible to editors and members. <b>Dump tonight</b> throws a night away (3-day undo by an admin).</li>
<li>Titles: letters, digits, <code>_ . -</code>; no double underscore; the dock shows the final name as you type.</li>
<li>Browser Go Live needs Windows + Chrome/Edge 141+ for window audio and the GPU encoder; on Linux use OBS.</li>
</ul>
</details>
</section>
<section id="s-editors" class="hidden">
<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>
</div>
<details><summary>Multicam &amp; timecode</summary>
<ul>
<li>Every stream file has a timecode track (time of day, Eastern) and a <code>.json</code> sidecar with the exact capture time, source (<code>strip</code> = frame-accurate from the overscan band, <code>arrival</code> = coarse), drift and gaps.</li>
<li>Multicam: sync by timecode, cross-check with voice audio. Export at the game resolution (2560×1440 / 1920×1080) — that drops the thin band at the bottom.</li>
<li>Files are H.264 MP4, constant frame rate, index up front. Streams are published a few minutes after they end.</li>
<li>No WebDAV (Discord-only login). Select files/folders and use copyparty's zip/tar download.</li>
</ul>
</details>
</section>
<section id="s-mediaadmins" class="hidden">
<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" 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>
</ul>
</details>
</section>
<section id="s-git" class="hidden">
<h2>Code</h2>
<p class="lead" data-group="git-users" data-alt="git-admins" data-yes="You are in <code>git-users</code>." data-no="Admin view — you are <b>not</b> in <code>git-users</code>/<code>git-admins</code>: Authentik will refuse the Gitea login until you get the role."></p>
<div class="links">
<a class="card" id="l-git" href="#"><b>Gitea</b><span>repos, issues, CI — sign in with authentik</span></a>
</div>
<details><summary>Notes</summary>
<ul><li>SSH remotes use port 2222: <code id="gitssh">ssh://git@…:2222/org/repo.git</code>. Add your key under Settings → SSH keys.</li>
<li>The stack's own repo is <code>techy/Media-Server-Setup</code>.</li></ul>
</details>
</section>
<section id="s-bot" class="hidden">
<h2>Bot</h2>
<p class="lead" data-group="bot-users" data-yes="You are in <code>bot-users</code>." data-no="Admin view — you are <b>not</b> in <code>bot-users</code>: the bot host will 403 you until you get the role."></p>
<div class="links"><a class="card" id="l-bot" href="#"><b>Bot web UI</b><span>Discord bot control panel</span></a></div>
</section>
<section id="s-admins" class="hidden">
<h2>Administration</h2>
<p class="lead">You are in <code>admins</code> or <code>authentik Admins</code>.</p>
<div class="links">
<a class="card" id="l-auth" href="#"><b>Authentik admin</b><span>users, groups, Discord source, policies</span></a>
<a class="card" id="l-jfadmin" href="#"><b>Jellyfin dashboard</b><span>libraries, plugin, scheduled tasks</span></a>
<a class="card" id="l-gitadmin" href="#"><b>Gitea site admin</b><span>auth sources, runners</span></a>
</div>
<details><summary>Where things live</summary>
<ul>
<li>Server: <code>~/vanguard-stream</code> on the box; <code>STATUS.md</code> = what runs / who does what / what's unfinished; <code>README.md</code> = how.</li>
<li>Change hostnames, bot token: <code>./configure.py</code>. Plugin updates: <code>./installJellyfinPlugin.sh</code>. Portal: <code>./deployLive.sh</code> (dev box).</li>
<li>Streamers = Authentik group <code>streamers</code>; editors = <code>editors</code>; everything follows Discord roles via the roles policy.</li>
</ul>
</details>
</section>
<footer>Vanguard Media · <a href="/about/">/about</a> · <a href="/logout">sign out</a></footer>
</main>
<script>
const $ = id => document.getElementById(id);
const host = location.hostname, base = host.replace(/^[^.]+\./, '');
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`;
(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);
$('who').textContent = d.name && d.name !== d.user ? `${d.name} (${d.user})` : d.user;
$('pills').innerHTML = d.groups.map(g => `<span class="pill">${g}</span>`).join('') || '<span class="pill">no groups yet — ask an admin for a role</span>';
const has = g => d.groups.includes(g);
const show = (id, on) => $(id).classList.toggle('hidden', !on);
// sections appear only for what this account can actually do (the gates check these same groups)
const isAdmin = has('admins') || has('authentik Admins');
show('s-streamers', has('streamers'));
show('s-editors', has('editors') || has('media-admins'));
show('s-mediaadmins', has('media-admins'));
show('s-git', has('git-users') || has('git-admins'));
show('s-bot', has('bot-users'));
show('s-admins', isAdmin);
document.querySelectorAll('p.lead[data-group]').forEach(p => { p.innerHTML = p.dataset.yes; });
} catch (e) { $('who').textContent = 'unknown (' + e.message + ')'; }
})();
</script>
</body>
</html>