Phase 2: Docker and OIDC auth

This commit is contained in:
2026-05-04 13:50:53 +02:00
parent 47baee854b
commit 4226628d5a
71 changed files with 9722 additions and 1347 deletions
@@ -0,0 +1,248 @@
import {
Button,
Chip,
Paper,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Typography,
} from "@mui/material";
import type { NowPlayingSession } from "../types";
interface Props {
sessions: NowPlayingSession[];
emptyMessage?: string;
selectedUserLabel?: string;
onSelectSession?: (session: NowPlayingSession) => void;
}
function formatStateLabel(state: string): string {
const normalized = String(state || "")
.trim()
.toLowerCase();
if (normalized === "playing") {
return "Playing";
}
if (normalized === "paused") {
return "Paused";
}
if (normalized === "idle") {
return "Idle";
}
return normalized
? normalized.charAt(0).toUpperCase() + normalized.slice(1)
: "Unknown";
}
function buildStatusSummary(sessions: NowPlayingSession[]) {
const playing = sessions.filter(
(session) =>
String(session.state || "")
.trim()
.toLowerCase() === "playing",
).length;
const paused = sessions.filter(
(session) =>
String(session.state || "")
.trim()
.toLowerCase() === "paused",
).length;
const idle = sessions.filter(
(session) =>
String(session.state || "")
.trim()
.toLowerCase() === "idle",
).length;
return `${sessions.length} session${sessions.length === 1 ? "" : "s"} · ${playing} playing · ${paused} paused · ${idle} idle`;
}
export function SessionActivityPanel({
sessions,
emptyMessage = "No live sessions matched to this user.",
selectedUserLabel,
onSelectSession,
}: Props) {
const userFallback = selectedUserLabel || "Unknown user";
if (!sessions.length) {
return (
<Typography variant="body2" color="text.secondary">
{emptyMessage}
</Typography>
);
}
return (
<TableContainer
component={Paper}
variant="outlined"
sx={{ maxHeight: 280, borderColor: "divider", borderRadius: 1 }}
>
<Table size="small" stickyHeader aria-label="Session activity details">
<TableHead>
<TableRow>
<TableCell
sx={{
fontWeight: 700,
bgcolor: "background.default",
minWidth: 160,
}}
>
User
</TableCell>
<TableCell
sx={{ fontWeight: 700, bgcolor: "background.default", width: 82 }}
>
State
</TableCell>
<TableCell
sx={{
fontWeight: 700,
bgcolor: "background.default",
minWidth: 140,
}}
>
Title / Type
</TableCell>
<TableCell
sx={{
fontWeight: 700,
bgcolor: "background.default",
minWidth: 140,
}}
>
Device
</TableCell>
<TableCell
sx={{
fontWeight: 700,
bgcolor: "background.default",
width: 118,
}}
>
Transcoding
</TableCell>
{onSelectSession ? (
<TableCell
sx={{
fontWeight: 700,
bgcolor: "background.default",
width: 150,
}}
>
Action
</TableCell>
) : null}
</TableRow>
</TableHead>
<TableBody>
<TableRow>
<TableCell
colSpan={onSelectSession ? 6 : 5}
sx={{ py: 0.75, bgcolor: "background.paper" }}
>
<Typography variant="caption" color="text.secondary">
{buildStatusSummary(sessions)}
</Typography>
</TableCell>
</TableRow>
{sessions.map((session) => {
const state = String(session.state || "")
.trim()
.toLowerCase();
const sessionLabel = formatStateLabel(session.state);
return (
<TableRow
key={session.session_id}
hover
sx={{ cursor: onSelectSession ? "pointer" : "default" }}
onClick={
onSelectSession ? () => onSelectSession(session) : undefined
}
>
<TableCell sx={{ py: 0.75, minWidth: 160 }}>
<Typography
variant="body2"
noWrap
title={session.user || userFallback}
>
{session.user || userFallback}
</Typography>
<Typography
variant="caption"
color="text.secondary"
noWrap
title={session.session_id}
>
{session.session_id}
</Typography>
</TableCell>
<TableCell sx={{ py: 0.75, whiteSpace: "nowrap" }}>
<Chip
size="small"
label={sessionLabel}
color={
state === "playing"
? "primary"
: state === "paused"
? "warning"
: "default"
}
variant={
state === "playing" || state === "paused"
? "filled"
: "outlined"
}
/>
</TableCell>
<TableCell sx={{ py: 0.75, minWidth: 140 }}>
<Typography
variant="body2"
noWrap
title={session.title || ""}
>
{session.title || "(idle)"}
</Typography>
<Typography variant="caption" color="text.secondary" noWrap>
{session.type || "—"}
</Typography>
</TableCell>
<TableCell sx={{ py: 0.75, minWidth: 140 }}>
<Typography variant="body2" noWrap>
{session.device || "Unknown device"}
</Typography>
</TableCell>
<TableCell sx={{ py: 0.75, whiteSpace: "nowrap" }}>
<Typography variant="body2" noWrap>
{session.transcoding === "yes"
? session.transcoding_type
? `yes (${session.transcoding_type})`
: "yes"
: "no"}
</Typography>
</TableCell>
{onSelectSession ? (
<TableCell sx={{ py: 0.75, whiteSpace: "nowrap" }}>
<Button
size="small"
variant="outlined"
onClick={(event) => {
event.stopPropagation();
onSelectSession(session);
}}
>
Open in Users
</Button>
</TableCell>
) : null}
</TableRow>
);
})}
</TableBody>
</Table>
</TableContainer>
);
}