SPB Git

Repository Public

page.tsx 6.7 KB 118 lines tsx

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
Files main
Menu

Theme