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