HTML 77.2%
TypeScript 10.5%
Python 9.6%
JavaScript 2.5%
1import type { Metadata } from 'next';2import Link from 'next/link';3import { ActionButton, AdminTitle, Bool, Mono, Notice, StatusChip, Trunc } from '@/components/admin/ui';4import { TierBadge } from '@/components/ui/badges';5import { DataTable, EmptyRow, Td, Th } from '@/components/ui/data-table';6import { LiveAgo } from '@/components/ui/live';7import { Unavailable } from '@/components/ui/unavailable';8import { runConnectorAction, toggleConnectorAction } from '@/lib/admin/actions';9import { adminApi, load, requireAdmin } from '@/lib/admin/admin-api';10import { fmtDateTime, fmtDuration, fmtInt, num } from '@/lib/format';1112export const metadata: Metadata = { title: 'Connectors', robots: { index: false, follow: false } };13export const dynamic = 'force-dynamic';1415const RETURN = '/admin/connectors';1617export default async function AdminConnectorsPage({ searchParams }: { searchParams: Promise<Record<string, string | undefined>> }) {18 await requireAdmin();19 const sp = await searchParams;20 const res = await load(adminApi.connectors());21 if (!res.ok) {22 return (23 <>24 <AdminTitle title="Connectors" />25 <Unavailable what="Connectors" reason={res.error} />26 </>27 );28 }29 const items = [...res.data.items].sort((a, b) => (num(a.priority) ?? 9) - (num(b.priority) ?? 9) || a.name.localeCompare(b.name));30 const counts = items.reduce<Record<string, number>>((acc, c) => ((acc[c.health] = (acc[c.health] ?? 0) + 1), acc), {});31 return (32 <>33 <AdminTitle title="Connectors" count={fmtInt(items.length)} lede={Object.entries(counts).map(([k, n]) => `${k} ${n}`).join(' · ')} />34 <Notice notice={sp.notice} level={sp.level} />35 <DataTable compact scroll caption="Connectors">36 <thead>37 <tr>38 <Th>Connector</Th>39 <Th>Source</Th>40 <Th>Health</Th>41 <Th>Enabled</Th>42 <Th num>Prio</Th>43 <Th num>Interval</Th>44 <Th>Last attempt</Th>45 <Th>Last success</Th>46 <Th>Next run</Th>47 <Th>Last run</Th>48 <Th num>Docs / snaps</Th>49 <Th num>Fails</Th>50 <Th>Circuit</Th>51 <Th>Actions</Th>52 </tr>53 </thead>54 <tbody>55 {items.length === 0 && <EmptyRow cols={14}>No connectors registered.</EmptyRow>}56 {items.map((c) => {57 const lr = c.last_run;58 return (59 <tr key={c.name}>60 <Td>61 <div className="flex items-center gap-1.5">62 <Link href={`/admin/runs?connector=${encodeURIComponent(c.name)}`} className="mono text-xs font-medium text-ink hover:text-accent">{c.name}</Link>63 {c.run_now_pending && <span className="text-[10px] uppercase tracking-wide text-accent" title="Run requested; waiting for the scheduler tick">queued</span>}64 {c.in_code === false && <span className="text-[10px] uppercase tracking-wide text-warning" title="Registered in the database but not present in code">no code</span>}65 </div>66 <span className="block max-w-[16rem] truncate text-[11px] text-ink-3" title={c.label}>{c.label}</span>67 </Td>68 <Td className="text-xs">69 <span className="text-ink-2">{c.source_name ?? c.source_key ?? '—'}</span> {c.source_tier !== undefined && c.source_tier !== null && <TierBadge tier={num(c.source_tier)} />}70 </Td>71 <Td><StatusChip value={c.health} /></Td>72 <Td><Bool v={c.enabled} /></Td>73 <Td num className="tnum text-xs">{fmtInt(c.priority)}</Td>74 <Td num className="tnum text-xs">{fmtDuration(c.interval_seconds)}</Td>75 <Td className="text-xs" title={fmtDateTime(c.last_attempt_at)}><LiveAgo at={c.last_attempt_at} /></Td>76 <Td className="text-xs" title={fmtDateTime(c.last_success_at)}><LiveAgo at={c.last_success_at} /></Td>77 <Td className="text-xs text-ink-2" title={fmtDateTime(c.next_run_at)}>{c.next_run_at ? fmtDateTime(c.next_run_at).replace(/ UTC$/, '') : '—'}</Td>78 <Td className="text-xs">79 {lr ? (80 <span className="inline-flex flex-wrap items-center gap-1.5">81 <StatusChip value={lr.status} />82 <span className="tnum text-ink-3">{num(lr.duration_ms) === null ? '' : fmtDuration(Math.round((num(lr.duration_ms) ?? 0) / 1000))}</span>83 <span className="tnum text-ink-3" title="docs changed / fetched">{fmtInt(lr.docs_changed)}/{fmtInt(lr.docs_fetched)}</span>84 {lr.error && <Trunc text={lr.error} max={40} className="text-danger" />}85 </span>86 ) : (87 <span className="text-ink-3">—</span>88 )}89 </Td>90 <Td num className="tnum text-xs text-ink-2">{c.documents === undefined && c.snapshots === undefined ? '—' : `${fmtInt(c.documents)} / ${fmtInt(c.snapshots)}`}</Td>91 <Td num className={`tnum text-xs ${num(c.consecutive_failures) ? 'text-danger' : 'text-ink-2'}`}>{fmtInt(c.consecutive_failures)}</Td>92 <Td className="text-xs">{c.circuit_open_until ? <span className="text-warning" title={fmtDateTime(c.circuit_open_until)}>open · <LiveAgo at={c.circuit_open_until} /></span> : <span className="text-ink-3">closed</span>}</Td>93 <Td>94 <div className="flex items-center gap-1.5">95 <form action={runConnectorAction}>96 <input type="hidden" name="name" value={c.name} />97 <input type="hidden" name="return" value={RETURN} />98 <ActionButton tone="accent" disabled={!!c.run_now_pending} title="Enqueue an immediate run (force)">Run now</ActionButton>99 </form>100 <form action={toggleConnectorAction}>101 <input type="hidden" name="name" value={c.name} />102 <input type="hidden" name="enabled" value={c.enabled ? 'false' : 'true'} />103 <input type="hidden" name="return" value={RETURN} />104 <ActionButton tone={c.enabled ? 'danger' : 'positive'}>{c.enabled ? 'Disable' : 'Enable'}</ActionButton>105 </form>106 </div>107 </Td>108 </tr>109 );110 })}111 </tbody>112 </DataTable>113 {res.data.unregistered_in_db && res.data.unregistered_in_db.length > 0 && (114 <p className="mt-4 text-xs text-ink-3">115 In code but not in the database ({res.data.unregistered_in_db.length}): {res.data.unregistered_in_db.map((n) => <Mono key={n} className="mr-1.5">{n}</Mono>)} — run <Mono>aia seed</Mono> to register them.116 </p>117 )}118 </>119 );120}121