/** * dashboard-client/src/components/ActiveSessionsTable.tsx — Active sessions table (S39). * * Per-session rows: PID, Repo, Model, Context %, Tokens/Window, * Heartbeat age, State dir, Sparkline. * * Data from /api/sessions (active sessions list) and /api/sessions/timeseries * (per-session data points used to render inline sparklines). */ import type React from "react"; import type { ActiveSession, SessionSeries } from "@contracts"; import { Card, CardContent } from "../components/ui/card"; export interface ActiveSessionsTableProps { /** Active sessions, sorted by lastSeen descending (from /api/sessions). */ sessions: ActiveSession[]; /** Per-session timeseries used to render the inline sparkline column. */ series: SessionSeries[]; } const HEADERS = [ "PID", "Repo", "Model", "Context %", "Tokens / Window", "Last Heartbeat", "State Dir", "Trend", ] as const; /** Format a duration in milliseconds → human-readable age string. */ function fmtAge(ms: number): string { const s = Math.max(0, Math.round(ms / 1000)); if (s < 60) return `${s}s ago`; const m = Math.floor(s / 60); if (m < 60) return `${m}m ${s % 60}s ago`; const h = Math.floor(m / 60); return `${h}h ${m % 60}m ago`; } function fmtTokens(n: number | null): string { if (n == null) return "\u2014"; if (n >= 100_000) return `${(n / 1000).toFixed(0)}k`; if (n >= 1000) return `${(n / 1000).toFixed(1)}k`; return n.toLocaleString(); } function pctClass(pct: number | null): string { if (pct == null) return "sessions-pct-ok"; if (pct >= 85) return "sessions-pct-err"; if (pct >= 60) return "sessions-pct-warn"; return "sessions-pct-ok"; } function fmtPct(pct: number | null): string { if (pct == null) return "\u2014"; return `${Math.round(pct)}%`; } function fmtWindow(tokens: number | null, window: number): string { if (tokens == null) return "\u2014"; return `${fmtTokens(tokens)} / ${fmtTokens(window)}`; } interface SparklineProps { /** Token counts over time (oldest → newest). */ points: number[]; /** Stable hex color for the stroke. */ color: string; } function Sparkline({ points, color }: SparklineProps): React.ReactElement { if (points.length < 2) { return {"\u2014"}; } const min = Math.min(...points); const max = Math.max(...points); const range = max - min || 1; const w = 64; const h = 18; const stepX = w / (points.length - 1); const segments = points.map((p, i) => { const x = i * stepX; const y = h - ((p - min) / range) * h; return `${i === 0 ? "M" : "L"} ${x.toFixed(1)} ${y.toFixed(1)}`; }); return ( ); } /** Map sessionId → series for the sparkline column lookup. */ function indexSeries( series: SessionSeries[], ): Map { const m = new Map(); for (const s of series) m.set(s.sessionId, s); return m; } export function ActiveSessionsTable({ sessions, series, }: ActiveSessionsTableProps): React.ReactElement { const seriesById = indexSeries(series); const now = Date.now(); return (
{HEADERS.map((h, i) => ( ))} {sessions.length === 0 && ( )} {sessions.map((s) => { const srs = seriesById.get(s.sessionId); const sparkPts = srs?.data.map((d) => d.tokens) ?? []; const color = srs?.color ?? "#58a6ff"; return ( ); })}
= 3 && i !== 6 ? " text-right" : "" }`} > {h}
No active sessions.
{s.pid} {s.displayName || s.repoRoot || "\u2014"} {s.model ?? "\u2014"} {fmtPct(s.percent)} {fmtWindow(s.tokens, s.ctxWindow)} {fmtAge(now - s.lastSeen)} {s.stateDir ? s.stateDir.split("/").pop() || s.stateDir : "\u2014"}
); }