diff --git a/STATUS.md b/STATUS.md
index afe7d52..ccb7feb 100644
--- a/STATUS.md
+++ b/STATUS.md
@@ -31,7 +31,7 @@ Source of truth: this repo, `techy/Media-Server-Setup` on the stack's own Gitea
| 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://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) |
-| Viewer | Live portal lays streams out as an even grid automatically when one joins or leaves ("Arrange" button redoes it after manual moves); audio follows the focused window | `https://live.…/` | Discord |
+| Viewer | Live portal lays streams out as an even grid automatically when one joins or leaves ("Arrange" button redoes it after manual moves); audio follows the focused window; double-click a tab to rename a stream (kept in that browser), double-click the slot at the left of a tab bar to name the group (session) | `https://live.…/` | Discord |
| 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; files are already at game resolution (band removed by the sweep) | Resolve / Premiere | — |
| 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` |
diff --git a/live/src/App.jsx b/live/src/App.jsx
index f5bbcb5..c8e2d9f 100644
--- a/live/src/App.jsx
+++ b/live/src/App.jsx
@@ -4,8 +4,12 @@ import Player from './Player.jsx'
import { fetchStreams } from './api.js'
import { audio } from './useAudio.js'
import GoLive from './GoLive.jsx'
+import Tab from './Tab.jsx'
+import GroupLabel from './GroupLabel.jsx'
+import { streamLabel } from './names.js'
const components = { player: Player }
+const tabComponents = { player: Tab }
const POLL_MS = 3000
// Lay every panel out as a grid (cols = ceil(sqrt(n)), row-major in stream-start order) and equalize sizes.
@@ -47,7 +51,7 @@ export default function App() {
let changed = false
for (const s of list) {
if (!have.has(s.id)) {
- api.addPanel({ id: s.id, component: 'player', title: `${s.user} · ${s.title}`, params: s,
+ api.addPanel({ id: s.id, component: 'player', tabComponent: 'player', title: streamLabel(s.id, s), params: s,
position: api.panels.length ? { direction: 'right' } : undefined }) // relative to the whole grid; arrange() places it
changed = true
} else {
@@ -87,7 +91,8 @@ export default function App() {
Nobody is live right now
This page updates itself when a stream starts.
)}
-
+
>)}
diff --git a/live/src/GroupLabel.jsx b/live/src/GroupLabel.jsx
new file mode 100644
index 0000000..2c18d83
--- /dev/null
+++ b/live/src/GroupLabel.jsx
@@ -0,0 +1,22 @@
+import React, { useEffect, useRef, useState } from 'react'
+import { names } from './names.js'
+
+// Left end of every tab bar: an editable group name (double-click). Session-only.
+export default function GroupLabel({ group }) {
+ const id = group.id
+ const name = names.use(s => s.groups[id])
+ const [editing, setEditing] = useState(false)
+ const [draft, setDraft] = useState('')
+ const ref = useRef(null)
+ useEffect(() => { if (editing) { ref.current?.focus(); ref.current?.select() } }, [editing])
+ const start = e => { e.stopPropagation(); setDraft(name || ''); setEditing(true) }
+ const commit = () => { if (!editing) return; names.renameGroup(id, draft.trim()); setEditing(false) }
+ return (
+