SPB Git forge

spb/ai-atlas

Public
41commits 1branches 0releases
4.6 MBsize
maindefault branch
12 days agolast push
HTML 77.2% TypeScript 10.5% Python 9.6% JavaScript 2.5%
5.4 KB · 102 lines tsx
Raw Blame History
1import type { Metadata } from 'next';2import Link from 'next/link';3import { ActionButton, AdminFilters, AdminTitle, JsonPre, Mono, Notice, StatusChip } from '@/components/admin/ui';4import { DataTable, EmptyRow, Td, Th } from '@/components/ui/data-table';5import { LiveAgo } from '@/components/ui/live';6import { Pagination, withParams } from '@/components/ui/pagination';7import { Note } from '@/components/ui/section';8import { EmptyState, Unavailable } from '@/components/ui/unavailable';9import { quarantineAction } from '@/lib/admin/actions';10import { adminApi, load, requireAdmin } from '@/lib/admin/admin-api';11import { fmtDateTime, fmtInt } from '@/lib/format';1213export const metadata: Metadata = { title: 'Quarantine', robots: { index: false, follow: false } };14export const dynamic = 'force-dynamic';15const LIMIT = 50;1617export default async function AdminQuarantinePage({ searchParams }: { searchParams: Promise<Record<string, string | undefined>> }) {18  await requireAdmin();19  const sp = await searchParams;20  const current: Record<string, string | undefined> = { status: sp.status ?? 'pending' };21  if (sp.offset) current.offset = sp.offset;22  const offset = Math.max(0, Number(current.offset) || 0);23  const href = (patch: Record<string, string | number | undefined | null>) => withParams('/admin/quarantine', current, patch);24  const ret = href({});25  const res = await load(adminApi.quarantine({ status: current.status, limit: LIMIT, offset }));26  return (27    <>28      <AdminTitle title="Quarantine" count={res.ok ? fmtInt(res.data.total) : undefined} lede="Connector runs held back by the writer (breakage suspicion, implausible volumes). Release writes the held facts; discard drops them. Both are audited.">29        <Link href="/admin/runs" className="text-xs text-ink-3 hover:text-ink">30          Runs →31        </Link>32      </AdminTitle>33      <Notice notice={sp.notice} level={sp.level} />34      <AdminFilters action="/admin/quarantine" className="mb-4" fields={[{ kind: 'select', name: 'status', label: 'Status', value: current.status, any: 'pending', options: ['pending', 'released', 'discarded', 'all'].map((s) => ({ value: s, label: s })) }]} />35      {!res.ok ? (36        <Unavailable what="Quarantine" reason={res.error} />37      ) : res.data.items.length === 0 ? (38        <EmptyState title="Nothing in quarantine">No held run for this status.</EmptyState>39      ) : (40        <>41          <DataTable compact scroll caption="Quarantined runs">42            <thead>43              <tr>44                <Th>Id</Th>45                <Th>Connector</Th>46                <Th>Run</Th>47                <Th>Reason</Th>48                <Th>Counts</Th>49                <Th>Status</Th>50                <Th>Created</Th>51                <Th>Actions</Th>52              </tr>53            </thead>54            <tbody>55              {res.data.items.length === 0 && <EmptyRow cols={8}>No rows.</EmptyRow>}56              {res.data.items.map((q) => (57                <tr key={q.id}>58                  <Td primary>59                    <Mono>{q.id}</Mono>60                  </Td>61                  <Td>62                    <Mono>{q.connector_name ?? '—'}</Mono>63                  </Td>64                  <Td>{q.run_id ? <Link href={`/admin/runs?connector=${encodeURIComponent(q.connector_name ?? '')}`} className="mono text-[11px] text-accent hover:underline">{q.run_id}</Link> : '—'}</Td>65                  <Td className="max-w-[20rem] text-xs text-ink-2">{q.reason ?? '—'}</Td>66                  <Td className="text-xs">{q.counts ? <JsonPre value={q.counts} maxHeight="6rem" /> : '—'}</Td>67                  <Td>68                    <StatusChip value={q.status} />69                  </Td>70                  <Td className="text-xs text-ink-3" title={q.created_at ? fmtDateTime(q.created_at) : undefined}>{q.created_at ? <LiveAgo at={q.created_at} /> : '—'}</Td>71                  <Td>72                    {q.status === 'pending' ? (73                      <div className="flex items-center gap-1">74                        <form action={quarantineAction}>75                          <input type="hidden" name="id" value={q.id} />76                          <input type="hidden" name="action" value="release" />77                          <input type="hidden" name="return" value={ret} />78                          <ActionButton tone="positive" title="Write the held facts">Release</ActionButton>79                        </form>80                        <form action={quarantineAction}>81                          <input type="hidden" name="id" value={q.id} />82                          <input type="hidden" name="action" value="discard" />83                          <input type="hidden" name="return" value={ret} />84                          <ActionButton tone="danger" title="Drop the held facts (audited)">Discard</ActionButton>85                        </form>86                      </div>87                    ) : (88                      <span className="text-xs text-ink-3">—</span>89                    )}90                  </Td>91                </tr>92              ))}93            </tbody>94          </DataTable>95          <Pagination total={res.data.total} limit={LIMIT} offset={offset} makeHref={(o) => href({ offset: o || undefined })} className="mt-4" />96        </>97      )}98      <Note className="mt-4">The release / discard endpoints answer 501 while the canonical service has not shipped them; the notice above reports the API's answer verbatim.</Note>99    </>100  );101}102