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%
4.3 KB · 92 lines tsx
Raw Blame History
1import type { Metadata } from 'next';2import { AdminFilters, AdminTitle, JsonPre, Mono, Notice } from '@/components/admin/ui';3import { DataTable, EmptyRow, Td, Th } from '@/components/ui/data-table';4import { LiveAgo } from '@/components/ui/live';5import { Pagination, withParams } from '@/components/ui/pagination';6import { Note } from '@/components/ui/section';7import { EmptyState, Unavailable } from '@/components/ui/unavailable';8import { adminApi, load, requireAdmin } from '@/lib/admin/admin-api';9import { cn } from '@/lib/cn';10import { fmtDateTime, fmtInt } from '@/lib/format';1112export const metadata: Metadata = { title: 'Audit log', robots: { index: false, follow: false } };13export const dynamic = 'force-dynamic';14const LIMIT = 50;1516export default async function AdminAuditPage({ searchParams }: { searchParams: Promise<Record<string, string | undefined>> }) {17  await requireAdmin();18  const sp = await searchParams;19  const current: Record<string, string | undefined> = {};20  if (sp.action) current.action = sp.action;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/audit', current, patch);24  const res = await load(adminApi.audit({ action: current.action, limit: LIMIT, offset }));25  return (26    <>27      <AdminTitle title="Audit log" count={res.ok ? fmtInt(res.data.total) : undefined} lede="Every admin call except the polling GETs (overview, infrastructure, llm/health, audit) writes a row: actor, action, target, payload, IP." />28      <Notice notice={sp.notice} level={sp.level} />29      <AdminFilters action="/admin/audit" className="mb-4" fields={[{ kind: 'text', name: 'action', label: 'Action contains', value: current.action, placeholder: 'entity-resolution, rollback, POST…' }]} />30      {!res.ok ? (31        <Unavailable what="Audit log" reason={res.error} />32      ) : res.data.items.length === 0 ? (33        <EmptyState title="No audit rows match" />34      ) : (35        <>36          <DataTable compact scroll caption="Audit log">37            <thead>38              <tr>39                <Th>When</Th>40                <Th>Actor</Th>41                <Th>Action</Th>42                <Th>Target</Th>43                <Th>Payload</Th>44                <Th>IP</Th>45                <Th>Id</Th>46              </tr>47            </thead>48            <tbody>49              {res.data.items.length === 0 && <EmptyRow cols={7}>No rows.</EmptyRow>}50              {res.data.items.map((r) => {51                const mutating = !r.action.startsWith('GET ');52                return (53                  <tr key={String(r.id)}>54                    <Td className="text-xs text-ink-3" title={fmtDateTime(r.created_at)}>55                      <LiveAgo at={r.created_at} />56                    </Td>57                    <Td>58                      <Mono>{r.actor}</Mono>59                    </Td>60                    <Td primary>61                      <span className={cn('mono text-xs', mutating ? 'font-medium text-ink' : 'text-ink-2')}>{r.action}</span>62                    </Td>63                    <Td>{r.target ? <Mono>{r.target}</Mono> : <span className="text-ink-3">—</span>}</Td>64                    <Td className="max-w-[26rem]">65                      {r.payload ? (66                        <details>67                          <summary className="cursor-pointer text-[11px] text-ink-3 hover:text-ink">{r.payload.method ? `${String(r.payload.method)} ${String(r.payload.path ?? '')}` : 'payload'}</summary>68                          <JsonPre value={r.payload} maxHeight="10rem" />69                        </details>70                      ) : (71                        <span className="text-ink-3">—</span>72                      )}73                    </Td>74                    <Td>75                      <Mono>{r.ip ?? '—'}</Mono>76                    </Td>77                    <Td>78                      <Mono>{String(r.id)}</Mono>79                    </Td>80                  </tr>81                );82              })}83            </tbody>84          </DataTable>85          <Pagination total={res.data.total} limit={LIMIT} offset={offset} makeHref={(o) => href({ offset: o || undefined })} className="mt-4" />86        </>87      )}88      <Note className="mt-4">Rows are append-only; this page itself is not logged (polling GET).</Note>89    </>90  );91}92