SPB Git

Repository Public

page.tsx 6.8 KB 127 lines tsx

1'use client';2import { RefreshCw } from 'lucide-react';3import { useAudit, useModels, useSystem } from '@/lib/queries';4import { useNow } from '@/hooks/use-viewport';5import { useConn } from '@/stores/conn';6import { DASH, fmtDateTime, fmtNum, fmtTime, fmtTokens } from '@/lib/format';7import { PROVIDER_LABEL, TIER_LABEL } from '@/lib/status';8import { Badge } from '@/components/ui/badge';9import { Button } from '@/components/ui/button';10import { ErrorState, Section, Skeleton } from '@/components/ui/misc';11import { HealthDot, HealthStrip, healthItems } from '@/components/system/health';1213const COUNT_LABELS: Record<string, string> = {14  missionsQueued: 'Missions queued', missionsRunning: 'Missions running', missionsWaiting: 'Missions waiting for you', pendingWaits: 'Pending approvals and questions',15  wsClients: 'Console connections',16};17const humanize = (k: string) => COUNT_LABELS[k] ?? k.replace(/([a-z])([A-Z])/g, '$1 $2').replace(/^./, (c) => c.toUpperCase());1819export default function SystemPage() {20  const q = useSystem();21  const models = useModels();22  const audit = useAudit(40);23  const now = useNow(5000);24  const events = useConn((s) => s.events);25  const s = q.data;26  return (27    <div className="h-full overflow-y-auto" data-testid="system-page">28      <div className="mx-auto flex w-full max-w-4xl flex-col gap-5 px-4 pb-32 pt-1 md:px-8 md:pb-12 md:pt-6">29        <div className="flex items-end gap-3">30          <div className="min-w-0 flex-1">31            <h1 className="hidden text-xl font-bold tracking-tight md:block">System</h1>32            <p className="tnum text-[13px] text-text-2">33              {s?.version ? `Agent OS ${s.version}` : 'Agent OS'} · {s?.collectedAt ? `collected at ${fmtTime(s.collectedAt)}` : q.isPending ? 'collecting…' : 'collection time unknown'}34            </p>35          </div>36          <Button variant="secondary" size="sm" onClick={() => q.refetch()} loading={q.isFetching}><RefreshCw className="size-3.5" /> Refresh</Button>37        </div>38        {q.isError ? (39          <div className="card"><ErrorState error={q.error} onRetry={() => q.refetch()} /></div>40        ) : !s ? (41          <Skeleton className="h-40 rounded-md" />42        ) : (43          <>44            <Section title="Health">45              <div className="p-3">46                <HealthStrip items={healthItems(s, now)} className="sm:grid-cols-4" />47                <p className="mt-2.5 flex items-center gap-2 px-1 text-xs text-muted" data-testid="system-events">48                  <HealthDot ok={events === 'open' ? true : events === 'closed' ? false : null} />49                  Live updates to this browser: {events === 'open' ? 'connected' : events === 'connecting' ? 'connecting' : 'disconnected — reconnecting'}50                </p>51              </div>52            </Section>53            <Section title="Activity">54              {s.counts && Object.keys(s.counts).length > 0 ? (55                <dl className="tnum grid grid-cols-2 gap-px bg-line sm:grid-cols-3" data-testid="system-counts">56                  {Object.entries(s.counts).map(([k, v]) => (57                    <div key={k} className="bg-surface px-4 py-3">58                      <dd className="text-[22px] font-bold leading-tight tracking-tight">{fmtNum(v)}</dd>59                      <dt className="text-xs text-muted">{humanize(k)}</dt>60                    </div>61                  ))}62                </dl>63              ) : (64                <p className="px-4 py-5 text-[13px] text-muted">The server did not report counters.</p>65              )}66            </Section>67          </>68        )}69        <Section title="Models" aside="from the providers' model lists, cached 10 min">70          {models.isError ? (71            <ErrorState error={models.error} onRetry={() => models.refetch()} />72          ) : models.isPending ? (73            <div className="p-4"><Skeleton className="h-24" /></div>74          ) : (75            <div className="overflow-x-auto">76              <table className="w-full min-w-[560px] text-[13px]" data-testid="system-models">77                <thead>78                  <tr className="text-left text-[11px] uppercase tracking-wide text-muted">79                    <th scope="col" className="px-4 py-2 font-semibold">Model</th>80                    <th scope="col" className="px-3 py-2 font-semibold">Tier</th>81                    <th scope="col" className="px-3 py-2 text-right font-semibold">Context</th>82                    <th scope="col" className="px-3 py-2 text-right font-semibold">$ / MTok in · out</th>83                    <th scope="col" className="px-4 py-2 font-semibold">Status</th>84                  </tr>85                </thead>86                <tbody className="tnum divide-y divide-line border-t border-line">87                  {(models.data ?? []).map((m) => (88                    <tr key={`${m.provider}/${m.id}`}>89                      <th scope="row" className="px-4 py-2.5 text-left font-normal">90                        <span className="block font-medium text-text">{m.label}</span>91                        <span className="block text-[11px] text-muted">{PROVIDER_LABEL[m.provider]} · <span className="font-mono">{m.id}</span></span>92                      </th>93                      <td className="px-3 py-2.5 text-text-2">{TIER_LABEL[m.tier]}</td>94                      <td className="px-3 py-2.5 text-right text-text-2">{m.contextTokens == null ? DASH : fmtTokens(m.contextTokens)}</td>95                      <td className="px-3 py-2.5 text-right text-text-2">{m.inputUsd == null ? DASH : m.inputUsd} · {m.outputUsd == null ? DASH : m.outputUsd}</td>96                      <td className="px-4 py-2.5">{m.available ? <Badge tone="success">Available</Badge> : <Badge tone="warning">Not available</Badge>}</td>97                    </tr>98                  ))}99                </tbody>100              </table>101            </div>102          )}103        </Section>104        {!audit.isError && (105          <Section title="Audit log" aside="most recent first">106            {audit.isPending ? (107              <div className="p-4"><Skeleton className="h-20" /></div>108            ) : (audit.data ?? []).length === 0 ? (109              <p className="px-4 py-5 text-[13px] text-muted">No audited action yet.</p>110            ) : (111              <ul className="divide-y divide-line" data-testid="audit-list">112                {(audit.data ?? []).map((e) => (113                  <li key={e.id} className="flex items-baseline gap-3 px-4 py-2 text-xs">114                    <span className="tnum w-36 shrink-0 text-muted">{fmtDateTime(e.at)}</span>115                    <span className={`min-w-0 flex-1 truncate font-mono ${e.ok ? 'text-text-2' : 'text-error'}`}>{e.action}{e.target ? ` ${e.target}` : ''}</span>116                    <span className="shrink-0 text-muted">{e.actor}</span>117                  </li>118                ))}119              </ul>120            )}121          </Section>122        )}123      </div>124    </div>125  );126}127
Files main
Menu

Theme