Repository Public
1'use client';2import { useState } from 'react';3import { RefreshCw } from 'lucide-react';4import { useAudit, useSystem } from '@/lib/queries';5import { fmtDateTime, fmtTime } from '@/lib/format';6import { Badge, StatusDot } from '@/components/ui/badge';7import { Button } from '@/components/ui/button';8import { EmptyState, ErrorState, PageHeader, Section, Skeleton } from '@/components/ui/misc';9import { HostMetrics } from '@/components/system/host-metrics';10import { useBrand } from '@/stores/branding';11import { loginHint } from '@/lib/branding';1213export default function SystemPage() {14 const brand = useBrand();15 const hint = loginHint(brand);16 const q = useSystem();17 const s = q.data;18 return (19 <div className="flex h-full flex-col overflow-hidden">20 <PageHeader21 title="System"22 description={`Health of ${brand.product} itself and of the host it runs on.`}23 meta={s ? <span className="tnum" data-testid="system-collected">Collected {fmtDateTime(s.collectedAt)} · version {s.version} · refreshes every 15 s while this tab is visible</span> : undefined}24 actions={<Button onClick={() => q.refetch()} loading={q.isFetching}><RefreshCw className="size-3.5" /> Refresh</Button>}25 />26 <div className="min-h-0 flex-1 overflow-y-auto">27 {q.isPending ? <div className="grid gap-4 p-4 md:grid-cols-2 md:p-6">{[0, 1, 2, 3].map((i) => <Skeleton key={i} className="h-40" />)}</div>28 : q.isError ? <ErrorState error={q.error} onRetry={() => q.refetch()} />29 : !s ? null30 : (31 <div className="grid grid-cols-1 gap-4 p-4 md:grid-cols-2 md:p-6">32 <Section title="Components">33 <ul className="divide-y divide-line" data-testid="components">34 {s.components.map((c) => (35 <li key={c.component} className="flex items-center gap-3 px-4 py-2.5 text-xs">36 <span className="w-24 shrink-0"><StatusDot health={c.health} withLabel /></span>37 <span className="min-w-0 flex-1">38 <span className="block truncate text-[13px] text-text">{c.component}</span>39 {c.detail && <span className="block truncate text-[11px] text-muted">{c.detail}</span>}40 </span>41 <span className="tnum shrink-0 text-[11px] text-muted" title={fmtDateTime(c.checkedAt)}>checked {fmtTime(c.checkedAt)}</span>42 </li>43 ))}44 </ul>45 </Section>46 <Section title={`Host · ${s.host.name}`} aside={<span className="tnum">as of {fmtTime(s.collectedAt)}</span>}>47 <div className="p-4"><HostMetrics host={s.host} /></div>48 </Section>49 <Section title="Activity">50 <dl className="grid grid-cols-2 gap-px bg-line sm:grid-cols-4" data-testid="counts">51 {([['Live sessions', s.counts.liveSessions], ['WebSocket clients', s.counts.wsClients], ['Running tasks', s.counts.runningTasks], ['Queued tasks', s.counts.queuedTasks]] as const).map(([k, v]) => (52 <div key={k} className="bg-surface px-4 py-3"><dt className="text-[11px] text-muted">{k}</dt><dd className="tnum mt-0.5 text-base font-semibold">{v}</dd></div>53 ))}54 </dl>55 </Section>56 <Section title={brand.agentName}>57 <dl className="grid grid-cols-[auto_1fr] gap-x-6 gap-y-2 p-4 text-xs" data-testid="claude-info">58 <dt className="text-muted">Version</dt>59 <dd className="tnum">{s.claude.version ?? 'UNKNOWN — the CLI did not report a version'}</dd>60 <dt className="text-muted">Authentication</dt>61 <dd>62 {s.claude.authenticated === true ? <Badge tone="success">Authenticated</Badge> : s.claude.authenticated === false ? <Badge tone="warning">Not authenticated</Badge> : <Badge tone="muted">UNKNOWN</Badge>}63 {s.claude.authenticated === false && <p className="mt-1.5 text-text-2" data-testid="agent-login-hint">{hint.before}<code className="rounded-[4px] bg-surface-2 px-1 font-mono">{hint.code}</code>{hint.after}</p>}64 {s.claude.authenticated == null && <p className="mt-1.5 text-muted">The authentication state could not be determined.</p>}65 </dd>66 </dl>67 </Section>68 <AuditLog />69 </div>70 )}71 </div>72 </div>73 );74}7576const PAGE = 25;77function AuditLog() {78 // Cursor pagination: `before` = id of the last event of the previous page.79 const [cursors, setCursors] = useState<(string | null)[]>([null]);80 const before = cursors[cursors.length - 1] ?? null;81 const q = useAudit(before, PAGE);82 const rows = q.data ?? [];83 return (84 <Section title="Audit log" aside={`page ${cursors.length}`} className="md:col-span-2">85 {q.isPending ? <div className="p-4"><Skeleton className="h-24" /></div>86 : q.isError ? <ErrorState error={q.error} onRetry={() => q.refetch()} />87 : rows.length === 0 ? <EmptyState title={cursors.length > 1 ? 'No older events' : 'No audit events yet'} className="py-8" />88 : (89 <div className="overflow-x-auto">90 <table className="w-full min-w-[640px] border-collapse text-xs" data-testid="audit-table">91 <thead>92 <tr className="text-left text-muted">93 {['Time', 'Actor', 'Action', 'Target', 'IP', 'Result'].map((h) => <th key={h} scope="col" className="border-b border-line-2 px-4 py-2 font-medium">{h}</th>)}94 </tr>95 </thead>96 <tbody>97 {rows.map((e) => (98 <tr key={e.id} className="border-b border-line last:border-0">99 <td className="tnum whitespace-nowrap px-4 py-1.5 text-text-2">{fmtDateTime(e.at)}</td>100 <td className="px-4 py-1.5 text-text-2">{e.actor}</td>101 <td className="px-4 py-1.5 font-mono text-text">{e.action}</td>102 <td className="max-w-56 truncate px-4 py-1.5 font-mono text-muted" title={e.target ?? undefined}>{e.target ?? '—'}</td>103 <td className="tnum px-4 py-1.5 text-muted">{e.ip ?? '—'}</td>104 <td className="px-4 py-1.5">{e.ok ? <Badge tone="success">ok</Badge> : <Badge tone="error">denied</Badge>}</td>105 </tr>106 ))}107 </tbody>108 </table>109 </div>110 )}111 <div className="flex items-center justify-end gap-2 border-t border-line px-4 py-2">112 <Button size="sm" disabled={cursors.length <= 1} onClick={() => setCursors((c) => c.slice(0, -1))}>Newer</Button>113 <Button size="sm" disabled={rows.length < PAGE} onClick={() => { const last = rows[rows.length - 1]; if (last) setCursors((c) => [...c, last.id]); }}>Older</Button>114 </div>115 </Section>116 );117}118