1import type { Metadata } from 'next';2import Link from 'next/link';3import { AdminFilters, AdminTitle, Mono, StatusChip, Trunc } from '@/components/admin/ui';4import { DataTable, EmptyRow, Td, Th } from '@/components/ui/data-table';5import { LiveAgo } from '@/components/ui/live';6import { Unavailable } from '@/components/ui/unavailable';7import { adminApi, load, requireAdmin } from '@/lib/admin/admin-api';8import { fmtDateTime, fmtDuration, fmtInt, num } from '@/lib/format';910export const metadata: Metadata = { title: 'Runs', robots: { index: false, follow: false } };11export const dynamic = 'force-dynamic';1213export default async function AdminRunsPage({ searchParams }: { searchParams: Promise<Record<string, string | undefined>> }) {14 await requireAdmin();15 const sp = await searchParams;16 const limit = Math.min(500, Math.max(10, Number(sp.limit) || 100));17 const [runs, connectors] = await Promise.all([load(adminApi.runs({ connector: sp.connector, limit })), load(adminApi.connectors())]);18 const names = connectors.ok ? connectors.data.items.map((c) => c.name).sort() : [];19 return (20 <>21 <AdminTitle title="Runs" count={runs.ok ? fmtInt(runs.data.total) : undefined} />22 <AdminFilters23 action="/admin/runs"24 className="mb-4"25 fields={[26 { kind: 'select', name: 'connector', label: 'Connector', value: sp.connector, options: names.map((n) => ({ value: n, label: n })) },27 { kind: 'select', name: 'limit', label: 'Rows', value: String(limit), any: '100', options: ['50', '100', '250', '500'].map((v) => ({ value: v, label: v })) },28 ]}29 />30 {!runs.ok ? (31 <Unavailable what="Runs" reason={runs.error} />32 ) : (33 <DataTable compact scroll caption="Connector runs">34 <thead>35 <tr>36 <Th>Connector</Th>37 <Th>Started</Th>38 <Th>Status</Th>39 <Th num>Duration</Th>40 <Th num>Disc.</Th>41 <Th num>Fetched</Th>42 <Th num>Changed</Th>43 <Th num>Unch.</Th>44 <Th num>Failed</Th>45 <Th num>Ent. +/~</Th>46 <Th num>Claims</Th>47 <Th num>Rel.</Th>48 <Th num>Events</Th>49 <Th>Error</Th>50 <Th>Run id</Th>51 </tr>52 </thead>53 <tbody>54 {runs.data.items.length === 0 && <EmptyRow cols={15}>No runs match.</EmptyRow>}55 {runs.data.items.map((r) => (56 <tr key={r.id}>57 <Td><Link href={`/admin/runs?connector=${encodeURIComponent(r.connector_name)}`} className="mono text-xs font-medium text-ink hover:text-accent">{r.connector_name}</Link></Td>58 <Td className="text-xs" title={fmtDateTime(r.started_at)}><LiveAgo at={r.started_at} /></Td>59 <Td><StatusChip value={r.status} /></Td>60 <Td num className="tnum text-xs">{num(r.duration_ms) === null ? '—' : fmtDuration(Math.round((num(r.duration_ms) ?? 0) / 1000))}</Td>61 <Td num className="tnum text-xs">{fmtInt(r.docs_discovered)}</Td>62 <Td num className="tnum text-xs">{fmtInt(r.docs_fetched)}</Td>63 <Td num className="tnum text-xs">{fmtInt(r.docs_changed)}</Td>64 <Td num className="tnum text-xs text-ink-3">{fmtInt(r.docs_unchanged)}</Td>65 <Td num className={`tnum text-xs ${num(r.docs_failed) ? 'text-danger' : ''}`}>{fmtInt(r.docs_failed)}</Td>66 <Td num className="tnum text-xs">{fmtInt(r.entities_created)} / {fmtInt(r.entities_updated)}</Td>67 <Td num className="tnum text-xs">{fmtInt(r.claims_written)}</Td>68 <Td num className="tnum text-xs">{fmtInt(r.relations_written)}</Td>69 <Td num className="tnum text-xs">{fmtInt(r.events_emitted)}</Td>70 <Td className="text-xs text-danger"><Trunc text={r.error} max={50} /></Td>71 <Td><Mono>{r.id}</Mono></Td>72 </tr>73 ))}74 </tbody>75 </DataTable>76 )}77 </>78 );79}80