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%
8.2 KB · 160 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 { EntityBadge } from '@/components/ui/badges';5import { DataTable, EmptyRow, Td, Th } from '@/components/ui/data-table';6import { LiveAgo } from '@/components/ui/live';7import { Pagination, withParams } from '@/components/ui/pagination';8import { EmptyState, Unavailable } from '@/components/ui/unavailable';9import { anomalyAction } from '@/lib/admin/actions';10import { adminApi, load, requireAdmin } from '@/lib/admin/admin-api';11import { cn } from '@/lib/cn';12import { fmtDateTime, fmtInt, fmtValue } from '@/lib/format';13import { routes } from '@/lib/site';1415export const metadata: Metadata = { title: 'Anomalies', robots: { index: false, follow: false } };16export const dynamic = 'force-dynamic';1718const LIMIT = 50;19const SEV: Record<string, string> = { critical: 'text-danger', warning: 'text-warning', info: 'text-ink-3' };2021export default async function AdminAnomaliesPage({ searchParams }: { searchParams: Promise<Record<string, string | undefined>> }) {22  await requireAdmin();23  const sp = await searchParams;24  const current: Record<string, string | undefined> = { status: sp.status ?? 'open' };25  if (sp.severity) current.severity = sp.severity;26  if (sp.check) current.check = sp.check;27  if (sp.offset) current.offset = sp.offset;28  const offset = Math.max(0, Number(current.offset) || 0);29  const href = (patch: Record<string, string | number | undefined | null>) => withParams('/admin/anomalies', current, patch);30  const ret = href({});31  const res = await load(adminApi.anomalies({ status: current.status, severity: current.severity, check: current.check, limit: LIMIT, offset }));32  const byCheck = res.ok ? (res.data.by_check ?? []) : [];33  const checks = [...new Set(byCheck.map((c) => c.check_name))].sort();34  return (35    <>36      <AdminTitle title="Anomalies" count={res.ok ? fmtInt(res.data.total) : undefined} lede="Impossible or suspicious values flagged by the checks in the methodology. Flags are never deleted: resolve when the data was fixed, ignore when the value is right despite the rule, reopen if it comes back.">37        <Link href={`${routes.methodology()}#anomalies`} className="text-xs text-ink-3 hover:text-ink">38          Check definitions →39        </Link>40      </AdminTitle>41      <Notice notice={sp.notice} level={sp.level} />42      {byCheck.length > 0 && (43        <ul className="mb-4 flex flex-wrap gap-1.5 text-xs">44          {byCheck.map((c) => (45            <li key={`${c.check_name}-${c.status}`}>46              <Link href={href({ check: current.check === c.check_name ? undefined : c.check_name, offset: undefined })} className={cn('inline-flex h-7 items-center gap-1.5 border px-2', current.check === c.check_name ? 'border-accent text-accent' : 'border-rule text-ink-2 hover:border-rule-strong')}>47                <span className={SEV[c.severity] ?? 'text-ink-3'}>{c.severity}</span>48                <span className="mono">{c.check_name}</span>49                <span className="tnum text-ink-3">{fmtInt(c.n)}</span>50              </Link>51            </li>52          ))}53        </ul>54      )}55      <AdminFilters56        action="/admin/anomalies"57        className="mb-4"58        fields={[59          { kind: 'select', name: 'status', label: 'Status', value: current.status, any: 'open', options: ['open', 'resolved', 'ignored', 'all'].map((s) => ({ value: s, label: s })) },60          { kind: 'select', name: 'severity', label: 'Severity', value: current.severity, options: ['critical', 'warning', 'info'].map((s) => ({ value: s, label: s })) },61          { kind: 'select', name: 'check', label: 'Check', value: current.check, options: checks.map((c) => ({ value: c, label: c })) },62        ]}63      />64      {!res.ok ? (65        <Unavailable what="Anomalies" reason={res.error} />66      ) : res.data.items.length === 0 ? (67        <EmptyState title="No anomaly matches" />68      ) : (69        <>70          <DataTable compact scroll caption="Anomalies">71            <thead>72              <tr>73                <Th>Severity</Th>74                <Th>Check</Th>75                <Th>Entity</Th>76                <Th>Message</Th>77                <Th num>Value</Th>78                <Th>Status</Th>79                <Th>Seen</Th>80                <Th>Actions</Th>81              </tr>82            </thead>83            <tbody>84              {res.data.items.length === 0 && <EmptyRow cols={8}>No rows.</EmptyRow>}85              {res.data.items.map((a) => (86                <tr key={a.id}>87                  <Td>88                    <span className={cn('text-xs font-medium', SEV[a.severity] ?? 'text-ink-3')}>{a.severity}</span>89                  </Td>90                  <Td>91                    <Mono>{a.check_name}</Mono>92                  </Td>93                  <Td primary>94                    {a.slug ? (95                      <span className="inline-flex items-center gap-1.5">96                        <EntityBadge type={a.entity_type ?? 'model'} small />97                        <Link href={routes.entity({ entity_type: a.entity_type ?? 'model', slug: a.slug })} className="text-ink hover:text-accent hover:underline">98                          {a.entity_name ?? a.slug}99                        </Link>100                      </span>101                    ) : (102                      <Mono>{a.entity_id ?? '—'}</Mono>103                    )}104                  </Td>105                  <Td className="max-w-[24rem] text-xs text-ink-2">106                    {a.message}107                    {a.detail && (108                      <details className="mt-1">109                        <summary className="cursor-pointer text-[11px] text-ink-3 hover:text-ink">detail</summary>110                        <JsonPre value={a.detail} maxHeight="10rem" />111                      </details>112                    )}113                  </Td>114                  <Td num className="tnum text-xs">{fmtValue(a.value)}</Td>115                  <Td>116                    <StatusChip value={a.status} />117                    {a.resolution && typeof a.resolution.note === 'string' && <span className="block text-[11px] text-ink-3">{a.resolution.note}</span>}118                  </Td>119                  <Td className="text-xs text-ink-3" title={`first ${fmtDateTime(a.first_seen_at)} · last ${fmtDateTime(a.last_seen_at)}`}>120                    <LiveAgo at={a.last_seen_at} />121                  </Td>122                  <Td>123                    <div className="flex items-center gap-1">124                      {a.status !== 'resolved' && (125                        <form action={anomalyAction}>126                          <input type="hidden" name="id" value={a.id} />127                          <input type="hidden" name="status" value="resolved" />128                          <input type="hidden" name="return" value={ret} />129                          <ActionButton tone="positive" title="The data was corrected">Resolve</ActionButton>130                        </form>131                      )}132                      {a.status !== 'ignored' && (133                        <form action={anomalyAction}>134                          <input type="hidden" name="id" value={a.id} />135                          <input type="hidden" name="status" value="ignored" />136                          <input type="hidden" name="return" value={ret} />137                          <ActionButton title="The value is right despite the rule">Ignore</ActionButton>138                        </form>139                      )}140                      {a.status !== 'open' && (141                        <form action={anomalyAction}>142                          <input type="hidden" name="id" value={a.id} />143                          <input type="hidden" name="status" value="open" />144                          <input type="hidden" name="return" value={ret} />145                          <ActionButton tone="accent">Reopen</ActionButton>146                        </form>147                      )}148                    </div>149                  </Td>150                </tr>151              ))}152            </tbody>153          </DataTable>154          <Pagination total={res.data.total} limit={LIMIT} offset={offset} makeHref={(o) => href({ offset: o || undefined })} className="mt-4" />155        </>156      )}157    </>158  );159}160