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.1 KB · 97 lines tsx
Raw Blame History
1import type { Metadata } from 'next';2import Link from 'next/link';3import { AdminFilters, AdminTitle, Mono, 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 { Unavailable } from '@/components/ui/unavailable';8import { adminApi, load, requireAdmin } from '@/lib/admin/admin-api';9import { fmtDateTime, fmtInt, num } from '@/lib/format';10import { routes } from '@/lib/site';1112export const metadata: Metadata = { title: 'Documents', robots: { index: false, follow: false } };13export const dynamic = 'force-dynamic';1415const LIMIT = 50;16const STATUSES = ['active', 'gone', 'blocked', 'error', 'paused'];1718export default async function AdminDocumentsPage({ searchParams }: { searchParams: Promise<Record<string, string | undefined>> }) {19  await requireAdmin();20  const sp = await searchParams;21  const current: Record<string, string | undefined> = {};22  for (const k of ['connector', 'status', 'q', 'offset']) if (sp[k]) current[k] = sp[k];23  const offset = Math.max(0, Number(current.offset) || 0);24  const [docs, connectors] = await Promise.all([load(adminApi.documents({ connector: current.connector, status: current.status, q: current.q, limit: LIMIT, offset })), load(adminApi.connectors())]);25  const names = connectors.ok ? connectors.data.items.map((c) => c.name).sort() : [];26  const href = (patch: Record<string, string | number | undefined | null>) => withParams('/admin/documents', current, patch);27  return (28    <>29      <AdminTitle title="Documents" count={docs.ok ? fmtInt(docs.data.total) : undefined} />30      <AdminFilters31        action="/admin/documents"32        className="mb-4"33        fields={[34          { kind: 'select', name: 'connector', label: 'Connector', value: current.connector, options: names.map((n) => ({ value: n, label: n })) },35          { kind: 'select', name: 'status', label: 'Status', value: current.status, options: STATUSES.map((s) => ({ value: s, label: s })) },36          { kind: 'text', name: 'q', label: 'URL / title', value: current.q, placeholder: 'contains…' },37        ]}38      />39      {!docs.ok ? (40        <Unavailable what="Documents" reason={docs.error} />41      ) : (42        <>43          <DataTable compact scroll caption="Documents">44            <thead>45              <tr>46                <Th>Document</Th>47                <Th>Connector</Th>48                <Th>Type</Th>49                <Th>Entity</Th>50                <Th>Fetched</Th>51                <Th num>HTTP</Th>52                <Th num>Snaps</Th>53                <Th num>Changes</Th>54                <Th num>Fails</Th>55                <Th>Status</Th>56                <Th>Processing</Th>57              </tr>58            </thead>59            <tbody>60              {docs.data.items.length === 0 && <EmptyRow cols={11}>No documents match.</EmptyRow>}61              {docs.data.items.map((d) => (62                <tr key={d.id}>63                  <Td>64                    <Link href={`/admin/documents/${encodeURIComponent(d.id)}`} className="block max-w-[22rem] truncate text-xs font-medium text-ink hover:text-accent" title={d.title ?? d.url}>65                      {d.title ?? d.url.replace(/^https?:\/\/(www\.)?/, '')}66                    </Link>67                    <span className="block max-w-[22rem] truncate text-[11px] text-ink-3" title={d.url}>{d.url.replace(/^https?:\/\/(www\.)?/, '')}</span>68                  </Td>69                  <Td><Mono>{d.connector_name ?? '—'}</Mono></Td>70                  <Td><Mono>{d.doc_type ?? '—'}</Mono></Td>71                  <Td className="text-xs">72                    {d.entity_slug && d.entity_type ? (73                      <Link href={routes.entity({ entity_type: d.entity_type, slug: d.entity_slug })} className="text-ink-2 hover:text-accent" title={d.entity_id ?? undefined}>74                        {d.entity_name ?? d.entity_slug}75                      </Link>76                    ) : (77                      <span className="text-ink-3">—</span>78                    )}79                  </Td>80                  <Td className="text-xs" title={fmtDateTime(d.last_fetched_at)}><LiveAgo at={d.last_fetched_at} /></Td>81                  <Td num className={`tnum text-xs ${(num(d.last_status) ?? 0) >= 400 ? 'text-danger' : 'text-ink-2'}`}>{fmtInt(d.last_status)}</Td>82                  <Td num className="tnum text-xs">{typeof d.snapshots === 'number' ? fmtInt(d.snapshots) : Array.isArray(d.snapshots) ? fmtInt(d.snapshots.length) : '—'}</Td>83                  <Td num className="tnum text-xs">{fmtInt(d.change_count)}</Td>84                  <Td num className={`tnum text-xs ${num(d.fail_count) ? 'text-danger' : 'text-ink-2'}`}>{fmtInt(d.fail_count)}</Td>85                  <Td><StatusChip value={d.status} /></Td>86                  <Td><StatusChip value={d.last_processing_status ?? null} /></Td>87                </tr>88              ))}89            </tbody>90          </DataTable>91          <Pagination total={docs.data.total} limit={LIMIT} offset={offset} makeHref={(o) => href({ offset: o || undefined })} className="mt-4" />92        </>93      )}94    </>95  );96}97