@hadoku/watchparty-ui 0.45.25 → 0.45.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,15 +1,15 @@
1
1
  import { jsxs as n, jsx as t } from "react/jsx-runtime";
2
- import { u as R, ay as M, X as E } from "./index-CtTxF93n.js";
3
- import { useState as f, useRef as A, useCallback as F, useEffect as I } from "react";
4
- import { l as $, a as w, m as P, n as B, r as T, o as D, p as C } from "./submissionsApi-Dq9WRhp3.js";
5
- import { c as O, B as U } from "./Room-I3TzcEzY.js";
2
+ import { u as R, aC as M, $ as E } from "./index-DOW8Etk-.js";
3
+ import { useState as f, useRef as $, useCallback as A, useEffect as F } from "react";
4
+ import { l as I, a as w, m as P, n as B, r as T, o as D, p as C } from "./submissionsApi-D2UlQjUa.js";
5
+ import { c as O, B as U } from "./Room-DgBCVk-f.js";
6
6
  const H = 5e3, q = 8e3;
7
7
  function K() {
8
- const [s, r] = f(null), [d, m] = f(null), [c, a] = f({}), [i, p] = f(null), [x, b] = f(null), [v, h] = f(""), [y, L] = f(() => Date.now()), g = A(!0), _ = F(async () => {
9
- const e = await $();
8
+ const [s, r] = f(null), [d, m] = f(null), [c, a] = f({}), [i, p] = f(null), [x, b] = f(null), [v, h] = f(""), [y, L] = f(() => Date.now()), g = $(!0), _ = A(async () => {
9
+ const e = await I();
10
10
  g.current && (L(Date.now()), e.ok ? (r(e.value), m(null)) : m(e.failure));
11
11
  }, []);
12
- I(() => {
12
+ F(() => {
13
13
  g.current = !0;
14
14
  const e = setTimeout(() => {
15
15
  _();
@@ -146,7 +146,7 @@ function Z() {
146
146
  ] }) }),
147
147
  c
148
148
  ] }) : /* @__PURE__ */ n("div", { className: "flex-1 overflow-y-auto p-2 space-y-2", children: [
149
- d.map((a) => /* @__PURE__ */ t(X, { client: a, initial: m[a.client_id] }, a.client_id)),
149
+ d.map((a) => /* @__PURE__ */ t(z, { client: a, initial: m[a.client_id] }, a.client_id)),
150
150
  c
151
151
  ] });
152
152
  }
@@ -171,7 +171,7 @@ function Q(s) {
171
171
  }
172
172
  return r;
173
173
  }
174
- function X({ client: s, initial: r }) {
174
+ function z({ client: s, initial: r }) {
175
175
  const m = (Math.round(s.avg_download_rate_bps / 1024) / 1024).toFixed(1), c = (s.buffered_ahead_ms / 1e3).toFixed(1), a = Math.round(s.rtt_ms), i = O(s), p = s.stale_ms !== void 0, x = p ? `Silent ${Math.round(s.stale_ms / 1e3)}s` : U[i], b = p || i === "critical" || i === "buffering", v = b ? "text-danger bg-danger/10" : i === "idle" ? "text-text-muted bg-bg-alt/40" : i === "warning" ? "text-on-neutral-bg bg-neutral/10" : "text-success bg-success/10", h = b ? "bg-danger" : i === "warning" ? "bg-neutral-bg" : "bg-success", y = Math.min(100, s.buffered_ahead_ms / E * 100);
176
176
  return /* @__PURE__ */ n("div", { className: "rounded-lg border border-border/50 bg-bg-alt/20 p-2.5", "data-silent": p || void 0, children: [
177
177
  /* @__PURE__ */ n("div", { className: "flex items-center justify-between mb-2", children: [
@@ -224,4 +224,4 @@ function X({ client: s, initial: r }) {
224
224
  export {
225
225
  Z as default
226
226
  };
227
- //# sourceMappingURL=AdminPanel-Ce1ncAv2.js.map
227
+ //# sourceMappingURL=AdminPanel-DwCWmVfW.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"AdminPanel-Ce1ncAv2.js","sources":["../../src/components/SubmissionQueue.tsx","../../src/components/AdminPanel.tsx"],"sourcesContent":["/**\n * The operator's moderation queue — every `pending` submission, with approve\n * and reject per row. Service tier only (the routes are `serviceAuth`; the\n * caller gates rendering on `isPrivileged()`, and a refusal is shown rather\n * than hidden if the gate and the server ever disagree).\n *\n * A row the server says has already moved (409, or 404 once it is gone) is not\n * an error in the click — a stale queue page is the ordinary way to meet it —\n * so it stays in place carrying the outcome, then drops off after\n * `NOTE_LINGER_MS`. Dropping it at once would make the click look like it did\n * nothing.\n *\n * Buttons carry real classes (`.drawer-btn`): the unlayered `button` reset\n * discards every Tailwind colour/font utility on a `<button>`.\n */\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport type { TSubmission } from '@watchparty/shared';\n/** `TSubmission` plus the `series` field the shared schema gains for magnets\n * (packages/shared/src/submissions.ts) — a cast rather than a schema edit,\n * since this file does not own that schema; harmless once the real field\n * lands, since the shapes agree. */\ntype SubmissionWithSeries = TSubmission & { series?: string };\nimport {\n approveSubmission,\n decisionFailureLabel,\n decisionMeansRowMoved,\n fetchPendingSubmissions,\n formatSubmissionAge,\n rejectSubmission,\n submissionFailureLabel,\n type SubmissionFailure,\n} from '../lib/submissionsApi';\n\nconst POLL_MS = 5000;\nconst NOTE_LINGER_MS = 8000;\n\ninterface Note { row: TSubmission; message: string; tone: 'info' | 'error' }\n\nexport default function SubmissionQueue() {\n const [rows, setRows] = useState<TSubmission[] | null>(null);\n const [listFailure, setListFailure] = useState<SubmissionFailure | null>(null);\n const [notes, setNotes] = useState<Record<string, Note>>({});\n const [busyId, setBusyId] = useState<string | null>(null);\n const [rejectingId, setRejectingId] = useState<string | null>(null);\n const [reason, setReason] = useState('');\n const [now, setNow] = useState(() => Date.now());\n const liveRef = useRef(true);\n\n const refresh = useCallback(async () => {\n const res = await fetchPendingSubmissions();\n if (!liveRef.current) return;\n setNow(Date.now());\n if (res.ok) { setRows(res.value); setListFailure(null); }\n else { setListFailure(res.failure); }\n }, []);\n\n useEffect(() => {\n liveRef.current = true;\n // Deferred a tick: the first read is a subscription callback, not a\n // synchronous setState in the effect body.\n const first = setTimeout(() => { void refresh(); }, 0);\n const timer = setInterval(() => { void refresh(); }, POLL_MS);\n return () => { liveRef.current = false; clearTimeout(first); clearInterval(timer); };\n }, [refresh]);\n\n function leaveNote(row: TSubmission, message: string, tone: Note['tone']) {\n setNotes(prev => ({ ...prev, [row.id]: { row, message, tone } }));\n setTimeout(() => {\n if (!liveRef.current) return;\n setNotes(prev => Object.fromEntries(Object.entries(prev).filter(([id]) => id !== row.id)));\n }, NOTE_LINGER_MS);\n }\n\n async function decide(row: TSubmission, verb: 'approve' | 'reject') {\n if (busyId) return;\n setBusyId(row.id);\n const res = verb === 'approve'\n ? await approveSubmission(row.id)\n : await rejectSubmission(row.id, reason);\n setBusyId(null);\n if (!liveRef.current) return;\n if (res.ok) {\n setRejectingId(null);\n setReason('');\n setRows(prev => (prev ?? []).filter(r => r.id !== row.id));\n leaveNote(row, res.value.state, 'info');\n } else if (decisionMeansRowMoved(res.failure)) {\n setRejectingId(null);\n setRows(prev => (prev ?? []).filter(r => r.id !== row.id));\n leaveNote(row, decisionFailureLabel(res.failure), 'error');\n } else {\n leaveNote(row, decisionFailureLabel(res.failure), 'error');\n }\n void refresh();\n }\n\n const shown = [\n ...(rows ?? []).map(r => ({ row: r, note: notes[r.id] })),\n ...Object.values(notes)\n .filter(n => !(rows ?? []).some(r => r.id === n.row.id))\n .map(n => ({ row: n.row, note: n })),\n ];\n\n // Nothing to moderate is shown as nothing — no header, no \"(none)\". A list\n // failure still renders, since that IS state the leader needs to see.\n if (!listFailure && shown.length === 0) return null;\n\n return (\n <section className=\"border-t border-border pt-2\" data-testid=\"submission-queue\">\n <div className=\"flex items-center px-1 pb-1\">\n <span className=\"text-[10px] uppercase tracking-wider text-text-muted\">Submissions</span>\n <span className=\"ml-1 text-[10px] text-text-muted\" data-testid=\"submission-queue-count\">\n {rows?.length ?? 0}\n </span>\n </div>\n {listFailure && (\n <div className=\"px-2 py-1 text-[11px] bg-danger-bg text-on-danger-bg rounded\">{submissionFailureLabel(listFailure)}</div>\n )}\n <ul className=\"list-none m-0 p-0\">\n {shown.map(({ row, note }) => {\n const decided = note && !(rows ?? []).some(r => r.id === row.id);\n return (\n <li\n key={row.id}\n className=\"px-1 py-1.5 border-b border-border/50 flex flex-col gap-1\"\n data-submission-id={row.id}\n data-submission-state={decided ? 'decided' : row.state}\n data-submission-kind={row.kind}\n data-submission-series={(row as SubmissionWithSeries).series ?? ''}\n >\n <div className=\"flex items-center gap-2 text-[11px] min-w-0\">\n <span className=\"text-[10px] uppercase tracking-wider text-text-muted shrink-0\">{row.kind}</span>\n <span className=\"text-text truncate flex-1\" title={row.source}>\n {(row as SubmissionWithSeries).series\n ? `${(row as SubmissionWithSeries).series} — ${row.title || row.source}`\n : (row.title || row.source)}\n </span>\n <span className=\"text-text-muted shrink-0\">{formatSubmissionAge(row.createdAt, now)}</span>\n </div>\n <div className=\"text-[10px] text-text-muted truncate\" title={row.ownerUserId}>{row.ownerUserId}</div>\n {note && (\n <div\n data-testid=\"submission-decision-note\"\n className={`text-[11px] px-1.5 py-0.5 rounded ${note.tone === 'error' ? 'bg-danger-bg text-on-danger-bg' : 'bg-success-bg text-on-success-bg'}`}\n >\n {note.message}\n </div>\n )}\n {!decided && rejectingId === row.id ? (\n <div className=\"flex items-center gap-1\">\n <input\n value={reason}\n onChange={e => setReason(e.target.value)}\n onKeyDown={e => { if (e.key === 'Enter') void decide(row, 'reject'); }}\n aria-label=\"Reject reason\"\n placeholder=\"reason\"\n className=\"flex-1 min-w-0 bg-bg-alt border border-border outline-none px-2 py-0.5 rounded text-[11px]\"\n />\n <button\n className=\"drawer-btn drawer-btn-danger\"\n aria-label=\"Confirm reject\"\n disabled={busyId === row.id}\n onClick={() => { void decide(row, 'reject'); }}\n >\n Reject\n </button>\n <button\n className=\"drawer-btn\"\n aria-label=\"Cancel reject\"\n onClick={() => { setRejectingId(null); setReason(''); }}\n >\n Cancel\n </button>\n </div>\n ) : !decided && (\n <div className=\"flex items-center gap-1\">\n <button\n className=\"drawer-btn drawer-btn-primary\"\n aria-label=\"Approve submission\"\n disabled={busyId === row.id}\n onClick={() => { void decide(row, 'approve'); }}\n >\n Approve\n </button>\n <button\n className=\"drawer-btn\"\n aria-label=\"Reject submission\"\n disabled={busyId === row.id}\n onClick={() => { setRejectingId(row.id); setReason(''); }}\n >\n Reject\n </button>\n </div>\n )}\n </li>\n );\n })}\n </ul>\n </section>\n );\n}\n","import { useAppStore } from '../lib/store';\nimport { BUFFER_AHEAD_MS, type TClientHealth } from '@watchparty/shared';\nimport { isPrivileged } from '../lib/auth';\nimport SubmissionQueue from './SubmissionQueue';\nimport { bufferHealthLevel, BUFFER_HEALTH_LABEL } from './bufferHealth';\n\n/**\n * Health monitor panel — renders as sidebar content (replaces chat when active).\n * Shows a card per connected client with live health metrics.\n */\nexport default function AdminPanel() {\n const healthSnapshot = useAppStore(s => s.healthSnapshot);\n\n const clients = healthSnapshot?.clients ?? [];\n // Slowest download first — easiest to spot struggling clients\n const sorted = [...clients].sort((a, b) => a.avg_download_rate_bps - b.avg_download_rate_bps);\n\n const initials = computeInitials(sorted);\n // Moderation is service tier, like the routes behind it.\n const queue = isPrivileged() ? <SubmissionQueue /> : null;\n\n if (clients.length === 0) {\n return (\n <div className=\"flex-1 overflow-y-auto p-2 flex flex-col\">\n <div className=\"flex-1 flex items-center justify-center p-4\">\n <p className=\"text-text-muted text-xs text-center\">\n No health data yet.<br />Clients report once media is loaded.\n </p>\n </div>\n {queue}\n </div>\n );\n }\n\n return (\n <div className=\"flex-1 overflow-y-auto p-2 space-y-2\">\n {sorted.map(c => <ClientCard key={c.client_id} client={c} initial={initials[c.client_id]} />)}\n {queue}\n </div>\n );\n}\n\n/* ------------------------------------------------------------------ */\n\nfunction formatTime(ms: number): string {\n const totalSecs = Math.floor(ms / 1000);\n const mins = Math.floor(totalSecs / 60);\n const secs = totalSecs % 60;\n return `${mins}:${secs.toString().padStart(2, '0')}`;\n}\n\n// Initials are first alphanumeric of display name; on conflict, advance to next\n// unused alphanumeric letter in the same name. Stable across renders via client_id sort.\nfunction computeInitials(clients: TClientHealth[]): Record<string, string> {\n const out: Record<string, string> = {};\n const taken = new Set<string>();\n const ordered = [...clients].sort((a, b) => a.client_id.localeCompare(b.client_id));\n for (const c of ordered) {\n const name = c.name ?? '';\n let chosen = '?';\n for (const ch of name) {\n if (!/[\\p{L}\\p{N}]/u.test(ch)) continue;\n const up = ch.toUpperCase();\n if (!taken.has(up)) { chosen = up; taken.add(up); break; }\n }\n out[c.client_id] = chosen;\n }\n return out;\n}\n\nfunction ClientCard({ client, initial }: { client: TClientHealth; initial: string }) {\n const dlKbps = Math.round(client.avg_download_rate_bps / 1024);\n const dlMbps = (dlKbps / 1024).toFixed(1);\n const bufferS = (client.buffered_ahead_ms / 1000).toFixed(1);\n const rtt = Math.round(client.rtt_ms);\n\n // Idle: not playing, no meaningful position — don't alarm about \"Critical\".\n // Near end-of-stream is exempt from the buffered-ahead thresholds (see\n // bufferHealth.ts) so the final seconds of every episode don't false-Critical.\n const level = bufferHealthLevel(client);\n // A connected client whose reports stopped arriving: the figures below are\n // its last known state, and the silence itself is the status.\n const silent = client.stale_ms !== undefined;\n const statusLabel = silent ? `Silent ${Math.round(client.stale_ms! / 1000)}s` : BUFFER_HEALTH_LABEL[level];\n const isRed = silent || level === 'critical' || level === 'buffering';\n const statusClasses = isRed\n ? 'text-danger bg-danger/10'\n : level === 'idle'\n ? 'text-text-muted bg-bg-alt/40'\n : level === 'warning'\n ? 'text-on-neutral-bg bg-neutral/10'\n : 'text-success bg-success/10';\n\n const barColor = isRed ? 'bg-danger' : level === 'warning' ? 'bg-neutral-bg' : 'bg-success';\n const barPct = Math.min(100, (client.buffered_ahead_ms / BUFFER_AHEAD_MS) * 100);\n\n return (\n <div className=\"rounded-lg border border-border/50 bg-bg-alt/20 p-2.5\" data-silent={silent || undefined}>\n {/* Row 1: Name + Status */}\n <div className=\"flex items-center justify-between mb-2\">\n <div className=\"flex items-center gap-2 min-w-0\">\n <div className=\"w-5 h-5 rounded-full bg-bg-alt flex items-center justify-center text-[9px] font-semibold shrink-0\">\n {initial}\n </div>\n <span className=\"text-sm font-medium text-text truncate\">{client.name}</span>\n </div>\n <span className={`inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium shrink-0 ${statusClasses}`}>\n {statusLabel}\n </span>\n </div>\n\n {/* Row 2: Stats grid */}\n <div className={`grid grid-cols-3 gap-x-2 gap-y-1 text-[11px] ${silent ? 'opacity-50' : ''}`}>\n <div>\n <span className=\"text-text-muted\">DL</span>\n <span className=\"ml-1 font-mono text-text\">{dlMbps}</span>\n <span className=\"text-text-muted ml-0.5\">Mbps</span>\n </div>\n <div>\n <span className=\"text-text-muted\">Ping</span>\n <span className=\"ml-1 font-mono text-text\">{rtt}</span>\n <span className=\"text-text-muted ml-0.5\">ms</span>\n </div>\n <div>\n <span className=\"text-text-muted\">Pos</span>\n <span className=\"ml-1 font-mono text-text-secondary\">{formatTime(client.playhead_ms)}</span>\n </div>\n\n {/* Buffer row spans full width */}\n <div className=\"col-span-2 flex items-center gap-1.5\">\n <span className=\"text-text-muted\">Buf</span>\n <span className=\"font-mono text-text\">{bufferS}s</span>\n <div className=\"flex-1 h-1 bg-bg-alt rounded-full overflow-hidden\">\n <div className={`h-full rounded-full transition-all ${barColor}`} style={{ width: `${barPct}%` }} />\n </div>\n </div>\n <div className=\"flex gap-1 font-mono justify-end\">\n <span className=\"bg-success-bg text-on-success-bg px-1 rounded\">{client.fragments_loaded}L</span>\n <span className=\"text-text-muted\">{client.fragments_pending}P</span>\n {client.fragments_error > 0 && <span className=\"bg-danger-bg text-on-danger-bg px-1 rounded\">{client.fragments_error}E</span>}\n </div>\n </div>\n </div>\n );\n}\n"],"names":["POLL_MS","NOTE_LINGER_MS","SubmissionQueue","rows","setRows","useState","listFailure","setListFailure","notes","setNotes","busyId","setBusyId","rejectingId","setRejectingId","reason","setReason","now","setNow","liveRef","useRef","refresh","useCallback","res","fetchPendingSubmissions","useEffect","first","timer","leaveNote","row","message","tone","prev","id","decide","verb","approveSubmission","rejectSubmission","r","decisionMeansRowMoved","decisionFailureLabel","shown","n","jsxs","jsx","submissionFailureLabel","note","decided","e","AdminPanel","clients","useAppStore","s","sorted","b","initials","computeInitials","queue","isPrivileged","c","ClientCard","formatTime","ms","totalSecs","mins","secs","out","taken","ordered","a","name","chosen","ch","up","client","initial","dlMbps","bufferS","rtt","level","bufferHealthLevel","silent","statusLabel","BUFFER_HEALTH_LABEL","isRed","statusClasses","barColor","barPct","BUFFER_AHEAD_MS"],"mappings":"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"}
1
+ {"version":3,"file":"AdminPanel-DwCWmVfW.js","sources":["../../src/components/SubmissionQueue.tsx","../../src/components/AdminPanel.tsx"],"sourcesContent":["/**\n * The operator's moderation queue — every `pending` submission, with approve\n * and reject per row. Service tier only (the routes are `serviceAuth`; the\n * caller gates rendering on `isPrivileged()`, and a refusal is shown rather\n * than hidden if the gate and the server ever disagree).\n *\n * A row the server says has already moved (409, or 404 once it is gone) is not\n * an error in the click — a stale queue page is the ordinary way to meet it —\n * so it stays in place carrying the outcome, then drops off after\n * `NOTE_LINGER_MS`. Dropping it at once would make the click look like it did\n * nothing.\n *\n * Buttons carry real classes (`.drawer-btn`): the unlayered `button` reset\n * discards every Tailwind colour/font utility on a `<button>`.\n */\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport type { TSubmission } from '@watchparty/shared';\n/** `TSubmission` plus the `series` field the shared schema gains for magnets\n * (packages/shared/src/submissions.ts) — a cast rather than a schema edit,\n * since this file does not own that schema; harmless once the real field\n * lands, since the shapes agree. */\ntype SubmissionWithSeries = TSubmission & { series?: string };\nimport {\n approveSubmission,\n decisionFailureLabel,\n decisionMeansRowMoved,\n fetchPendingSubmissions,\n formatSubmissionAge,\n rejectSubmission,\n submissionFailureLabel,\n type SubmissionFailure,\n} from '../lib/submissionsApi';\n\nconst POLL_MS = 5000;\nconst NOTE_LINGER_MS = 8000;\n\ninterface Note { row: TSubmission; message: string; tone: 'info' | 'error' }\n\nexport default function SubmissionQueue() {\n const [rows, setRows] = useState<TSubmission[] | null>(null);\n const [listFailure, setListFailure] = useState<SubmissionFailure | null>(null);\n const [notes, setNotes] = useState<Record<string, Note>>({});\n const [busyId, setBusyId] = useState<string | null>(null);\n const [rejectingId, setRejectingId] = useState<string | null>(null);\n const [reason, setReason] = useState('');\n const [now, setNow] = useState(() => Date.now());\n const liveRef = useRef(true);\n\n const refresh = useCallback(async () => {\n const res = await fetchPendingSubmissions();\n if (!liveRef.current) return;\n setNow(Date.now());\n if (res.ok) { setRows(res.value); setListFailure(null); }\n else { setListFailure(res.failure); }\n }, []);\n\n useEffect(() => {\n liveRef.current = true;\n // Deferred a tick: the first read is a subscription callback, not a\n // synchronous setState in the effect body.\n const first = setTimeout(() => { void refresh(); }, 0);\n const timer = setInterval(() => { void refresh(); }, POLL_MS);\n return () => { liveRef.current = false; clearTimeout(first); clearInterval(timer); };\n }, [refresh]);\n\n function leaveNote(row: TSubmission, message: string, tone: Note['tone']) {\n setNotes(prev => ({ ...prev, [row.id]: { row, message, tone } }));\n setTimeout(() => {\n if (!liveRef.current) return;\n setNotes(prev => Object.fromEntries(Object.entries(prev).filter(([id]) => id !== row.id)));\n }, NOTE_LINGER_MS);\n }\n\n async function decide(row: TSubmission, verb: 'approve' | 'reject') {\n if (busyId) return;\n setBusyId(row.id);\n const res = verb === 'approve'\n ? await approveSubmission(row.id)\n : await rejectSubmission(row.id, reason);\n setBusyId(null);\n if (!liveRef.current) return;\n if (res.ok) {\n setRejectingId(null);\n setReason('');\n setRows(prev => (prev ?? []).filter(r => r.id !== row.id));\n leaveNote(row, res.value.state, 'info');\n } else if (decisionMeansRowMoved(res.failure)) {\n setRejectingId(null);\n setRows(prev => (prev ?? []).filter(r => r.id !== row.id));\n leaveNote(row, decisionFailureLabel(res.failure), 'error');\n } else {\n leaveNote(row, decisionFailureLabel(res.failure), 'error');\n }\n void refresh();\n }\n\n const shown = [\n ...(rows ?? []).map(r => ({ row: r, note: notes[r.id] })),\n ...Object.values(notes)\n .filter(n => !(rows ?? []).some(r => r.id === n.row.id))\n .map(n => ({ row: n.row, note: n })),\n ];\n\n // Nothing to moderate is shown as nothing — no header, no \"(none)\". A list\n // failure still renders, since that IS state the leader needs to see.\n if (!listFailure && shown.length === 0) return null;\n\n return (\n <section className=\"border-t border-border pt-2\" data-testid=\"submission-queue\">\n <div className=\"flex items-center px-1 pb-1\">\n <span className=\"text-[10px] uppercase tracking-wider text-text-muted\">Submissions</span>\n <span className=\"ml-1 text-[10px] text-text-muted\" data-testid=\"submission-queue-count\">\n {rows?.length ?? 0}\n </span>\n </div>\n {listFailure && (\n <div className=\"px-2 py-1 text-[11px] bg-danger-bg text-on-danger-bg rounded\">{submissionFailureLabel(listFailure)}</div>\n )}\n <ul className=\"list-none m-0 p-0\">\n {shown.map(({ row, note }) => {\n const decided = note && !(rows ?? []).some(r => r.id === row.id);\n return (\n <li\n key={row.id}\n className=\"px-1 py-1.5 border-b border-border/50 flex flex-col gap-1\"\n data-submission-id={row.id}\n data-submission-state={decided ? 'decided' : row.state}\n data-submission-kind={row.kind}\n data-submission-series={(row as SubmissionWithSeries).series ?? ''}\n >\n <div className=\"flex items-center gap-2 text-[11px] min-w-0\">\n <span className=\"text-[10px] uppercase tracking-wider text-text-muted shrink-0\">{row.kind}</span>\n <span className=\"text-text truncate flex-1\" title={row.source}>\n {(row as SubmissionWithSeries).series\n ? `${(row as SubmissionWithSeries).series} — ${row.title || row.source}`\n : (row.title || row.source)}\n </span>\n <span className=\"text-text-muted shrink-0\">{formatSubmissionAge(row.createdAt, now)}</span>\n </div>\n <div className=\"text-[10px] text-text-muted truncate\" title={row.ownerUserId}>{row.ownerUserId}</div>\n {note && (\n <div\n data-testid=\"submission-decision-note\"\n className={`text-[11px] px-1.5 py-0.5 rounded ${note.tone === 'error' ? 'bg-danger-bg text-on-danger-bg' : 'bg-success-bg text-on-success-bg'}`}\n >\n {note.message}\n </div>\n )}\n {!decided && rejectingId === row.id ? (\n <div className=\"flex items-center gap-1\">\n <input\n value={reason}\n onChange={e => setReason(e.target.value)}\n onKeyDown={e => { if (e.key === 'Enter') void decide(row, 'reject'); }}\n aria-label=\"Reject reason\"\n placeholder=\"reason\"\n className=\"flex-1 min-w-0 bg-bg-alt border border-border outline-none px-2 py-0.5 rounded text-[11px]\"\n />\n <button\n className=\"drawer-btn drawer-btn-danger\"\n aria-label=\"Confirm reject\"\n disabled={busyId === row.id}\n onClick={() => { void decide(row, 'reject'); }}\n >\n Reject\n </button>\n <button\n className=\"drawer-btn\"\n aria-label=\"Cancel reject\"\n onClick={() => { setRejectingId(null); setReason(''); }}\n >\n Cancel\n </button>\n </div>\n ) : !decided && (\n <div className=\"flex items-center gap-1\">\n <button\n className=\"drawer-btn drawer-btn-primary\"\n aria-label=\"Approve submission\"\n disabled={busyId === row.id}\n onClick={() => { void decide(row, 'approve'); }}\n >\n Approve\n </button>\n <button\n className=\"drawer-btn\"\n aria-label=\"Reject submission\"\n disabled={busyId === row.id}\n onClick={() => { setRejectingId(row.id); setReason(''); }}\n >\n Reject\n </button>\n </div>\n )}\n </li>\n );\n })}\n </ul>\n </section>\n );\n}\n","import { useAppStore } from '../lib/store';\nimport { BUFFER_AHEAD_MS, type TClientHealth } from '@watchparty/shared';\nimport { isPrivileged } from '../lib/auth';\nimport SubmissionQueue from './SubmissionQueue';\nimport { bufferHealthLevel, BUFFER_HEALTH_LABEL } from './bufferHealth';\n\n/**\n * Health monitor panel — renders as sidebar content (replaces chat when active).\n * Shows a card per connected client with live health metrics.\n */\nexport default function AdminPanel() {\n const healthSnapshot = useAppStore(s => s.healthSnapshot);\n\n const clients = healthSnapshot?.clients ?? [];\n // Slowest download first — easiest to spot struggling clients\n const sorted = [...clients].sort((a, b) => a.avg_download_rate_bps - b.avg_download_rate_bps);\n\n const initials = computeInitials(sorted);\n // Moderation is service tier, like the routes behind it.\n const queue = isPrivileged() ? <SubmissionQueue /> : null;\n\n if (clients.length === 0) {\n return (\n <div className=\"flex-1 overflow-y-auto p-2 flex flex-col\">\n <div className=\"flex-1 flex items-center justify-center p-4\">\n <p className=\"text-text-muted text-xs text-center\">\n No health data yet.<br />Clients report once media is loaded.\n </p>\n </div>\n {queue}\n </div>\n );\n }\n\n return (\n <div className=\"flex-1 overflow-y-auto p-2 space-y-2\">\n {sorted.map(c => <ClientCard key={c.client_id} client={c} initial={initials[c.client_id]} />)}\n {queue}\n </div>\n );\n}\n\n/* ------------------------------------------------------------------ */\n\nfunction formatTime(ms: number): string {\n const totalSecs = Math.floor(ms / 1000);\n const mins = Math.floor(totalSecs / 60);\n const secs = totalSecs % 60;\n return `${mins}:${secs.toString().padStart(2, '0')}`;\n}\n\n// Initials are first alphanumeric of display name; on conflict, advance to next\n// unused alphanumeric letter in the same name. Stable across renders via client_id sort.\nfunction computeInitials(clients: TClientHealth[]): Record<string, string> {\n const out: Record<string, string> = {};\n const taken = new Set<string>();\n const ordered = [...clients].sort((a, b) => a.client_id.localeCompare(b.client_id));\n for (const c of ordered) {\n const name = c.name ?? '';\n let chosen = '?';\n for (const ch of name) {\n if (!/[\\p{L}\\p{N}]/u.test(ch)) continue;\n const up = ch.toUpperCase();\n if (!taken.has(up)) { chosen = up; taken.add(up); break; }\n }\n out[c.client_id] = chosen;\n }\n return out;\n}\n\nfunction ClientCard({ client, initial }: { client: TClientHealth; initial: string }) {\n const dlKbps = Math.round(client.avg_download_rate_bps / 1024);\n const dlMbps = (dlKbps / 1024).toFixed(1);\n const bufferS = (client.buffered_ahead_ms / 1000).toFixed(1);\n const rtt = Math.round(client.rtt_ms);\n\n // Idle: not playing, no meaningful position — don't alarm about \"Critical\".\n // Near end-of-stream is exempt from the buffered-ahead thresholds (see\n // bufferHealth.ts) so the final seconds of every episode don't false-Critical.\n const level = bufferHealthLevel(client);\n // A connected client whose reports stopped arriving: the figures below are\n // its last known state, and the silence itself is the status.\n const silent = client.stale_ms !== undefined;\n const statusLabel = silent ? `Silent ${Math.round(client.stale_ms! / 1000)}s` : BUFFER_HEALTH_LABEL[level];\n const isRed = silent || level === 'critical' || level === 'buffering';\n const statusClasses = isRed\n ? 'text-danger bg-danger/10'\n : level === 'idle'\n ? 'text-text-muted bg-bg-alt/40'\n : level === 'warning'\n ? 'text-on-neutral-bg bg-neutral/10'\n : 'text-success bg-success/10';\n\n const barColor = isRed ? 'bg-danger' : level === 'warning' ? 'bg-neutral-bg' : 'bg-success';\n const barPct = Math.min(100, (client.buffered_ahead_ms / BUFFER_AHEAD_MS) * 100);\n\n return (\n <div className=\"rounded-lg border border-border/50 bg-bg-alt/20 p-2.5\" data-silent={silent || undefined}>\n {/* Row 1: Name + Status */}\n <div className=\"flex items-center justify-between mb-2\">\n <div className=\"flex items-center gap-2 min-w-0\">\n <div className=\"w-5 h-5 rounded-full bg-bg-alt flex items-center justify-center text-[9px] font-semibold shrink-0\">\n {initial}\n </div>\n <span className=\"text-sm font-medium text-text truncate\">{client.name}</span>\n </div>\n <span className={`inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium shrink-0 ${statusClasses}`}>\n {statusLabel}\n </span>\n </div>\n\n {/* Row 2: Stats grid */}\n <div className={`grid grid-cols-3 gap-x-2 gap-y-1 text-[11px] ${silent ? 'opacity-50' : ''}`}>\n <div>\n <span className=\"text-text-muted\">DL</span>\n <span className=\"ml-1 font-mono text-text\">{dlMbps}</span>\n <span className=\"text-text-muted ml-0.5\">Mbps</span>\n </div>\n <div>\n <span className=\"text-text-muted\">Ping</span>\n <span className=\"ml-1 font-mono text-text\">{rtt}</span>\n <span className=\"text-text-muted ml-0.5\">ms</span>\n </div>\n <div>\n <span className=\"text-text-muted\">Pos</span>\n <span className=\"ml-1 font-mono text-text-secondary\">{formatTime(client.playhead_ms)}</span>\n </div>\n\n {/* Buffer row spans full width */}\n <div className=\"col-span-2 flex items-center gap-1.5\">\n <span className=\"text-text-muted\">Buf</span>\n <span className=\"font-mono text-text\">{bufferS}s</span>\n <div className=\"flex-1 h-1 bg-bg-alt rounded-full overflow-hidden\">\n <div className={`h-full rounded-full transition-all ${barColor}`} style={{ width: `${barPct}%` }} />\n </div>\n </div>\n <div className=\"flex gap-1 font-mono justify-end\">\n <span className=\"bg-success-bg text-on-success-bg px-1 rounded\">{client.fragments_loaded}L</span>\n <span className=\"text-text-muted\">{client.fragments_pending}P</span>\n {client.fragments_error > 0 && <span className=\"bg-danger-bg text-on-danger-bg px-1 rounded\">{client.fragments_error}E</span>}\n </div>\n </div>\n </div>\n );\n}\n"],"names":["POLL_MS","NOTE_LINGER_MS","SubmissionQueue","rows","setRows","useState","listFailure","setListFailure","notes","setNotes","busyId","setBusyId","rejectingId","setRejectingId","reason","setReason","now","setNow","liveRef","useRef","refresh","useCallback","res","fetchPendingSubmissions","useEffect","first","timer","leaveNote","row","message","tone","prev","id","decide","verb","approveSubmission","rejectSubmission","r","decisionMeansRowMoved","decisionFailureLabel","shown","n","jsxs","jsx","submissionFailureLabel","note","decided","e","AdminPanel","clients","useAppStore","s","sorted","b","initials","computeInitials","queue","isPrivileged","c","ClientCard","formatTime","ms","totalSecs","mins","secs","out","taken","ordered","a","name","chosen","ch","up","client","initial","dlMbps","bufferS","rtt","level","bufferHealthLevel","silent","statusLabel","BUFFER_HEALTH_LABEL","isRed","statusClasses","barColor","barPct","BUFFER_AHEAD_MS"],"mappings":"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"}