HTML 77.2%
TypeScript 10.5%
Python 9.6%
JavaScript 2.5%
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