SPB Git

spb/rareindex

Public
5.5 KB · 87 lines tsx
1import Link from 'next/link';2import { requireAdmin } from '@/lib/admin/auth';3import { connectorsOverview } from '@/lib/admin/queries';4import { connectorAction } from '@/lib/admin/actions';5import { AdminShell, ActionButton, StatusPill, fmtTs, n } from '@/components/admin/shell';6import { Table, th, td, tdNum } from '@/components/ui/primitives';78function pct(v: unknown): string {9  const x = Number(v);10  return Number.isFinite(x) ? `${Math.round(x * 100)}%` : '—';11}1213export default async function ConnectorsPage() {14  await requireAdmin();15  const rows = await connectorsOverview();16  return (17    <AdminShell current="/admin/connectors" title="Connector control center" subtitle={`${rows.length} connectors registered · actions enqueue jobs on the crawl.run queue consumed by the worker`}>18      <div className="card">19        <Table>20          <thead>21            <tr>22              <th className={th}>Connector</th>23              <th className={th}>Health</th>24              <th className={th}>Status</th>25              <th className={`${th} text-right`}>Pages/min</th>26              <th className={`${th} text-right`}>Errors</th>27              <th className={th}>Last crawl</th>28              <th className={`${th} text-right`}>Records 24h</th>29              <th className={`${th} text-right`}>Dup.</th>30              <th className={`${th} text-right`}>FC rate</th>31              <th className={`${th} text-right`}>SF rate</th>32              <th className={`${th} text-right`}>Parser conf.</th>33              <th className={th}>Schema</th>34              <th className={th}>Actions</th>35            </tr>36          </thead>37          <tbody>38            {rows.length === 0 ? <tr><td className={td} colSpan={13}><span className="text-subtle">No connectors in the registry yet. Add meta.json files under connectors/ and run pnpm db:seed.</span></td></tr> : null}39            {rows.map((r) => {40              const health = (r.health ?? {}) as Record<string, unknown>;41              const anomalies = Array.isArray(r.last_anomalies) ? (r.last_anomalies as string[]) : [];42              const drift = anomalies.some((a) => /schema|selector|missing_field|redesign/.test(a));43              return (44                <tr key={String(r.id)} className="align-top">45                  <td className={td}>46                    <Link href={`/admin/connectors/${String(r.id)}`} className="font-medium hover:underline">{String(r.display_name)}</Link>47                    <div className="text-[11px] text-subtle">{String(r.id)} · {(r.engine_priority as string[]).join('→')} · {String(r.priority)} · every {n(r.refresh_frequency_minutes)} min</div>48                  </td>49                  <td className={td}><StatusPill status={(r.health_status as string) ?? 'unknown'} />{r.health_at ? <div className="text-[10px] text-subtle">{fmtTs(r.health_at)}</div> : null}</td>50                  <td className={td}><StatusPill status={String(r.status)} />{r.last_run_status ? <div className="text-[10px] text-subtle">run: {String(r.last_run_status)}</div> : null}</td>51                  <td className={tdNum}>{n(r.pages_per_min, 1)}</td>52                  <td className={`${tdNum} ${r.last_error ? 'text-loss' : ''}`} title={String(r.last_error ?? '')}>{r.last_error ? '1' : '0'}{anomalies.length ? <div className="text-[10px] text-alert">{anomalies.length} anomal.</div> : null}</td>53                  <td className={td}>{fmtTs(r.last_started_at)}<div className="text-[10px] text-subtle">ok: {fmtTs(r.last_success_at)}</div></td>54                  <td className={tdNum}>{n(r.records_24h)}<div className="text-[10px] text-subtle">{n(r.normalized_24h)} norm.</div></td>55                  <td className={tdNum}>{n(r.duplicates_24h)}</td>56                  <td className={tdNum}>{pct(health.firecrawl_success_rate)}</td>57                  <td className={tdNum}>{pct(health.scrapfly_fallback_rate)}</td>58                  <td className={tdNum}>{pct(r.parser_confidence)}</td>59                  <td className={td}>v{String(r.schema_version)}{drift ? <div className="text-[10px] text-alert">drift?</div> : null}</td>60                  <td className={td}>61                    <div className="flex flex-wrap gap-1">62                      {(['run', 'probe', 'recrawl', 'retry'] as const).map((a) => (63                        <form key={a} action={connectorAction}>64                          <input type="hidden" name="id" value={String(r.id)} />65                          <input type="hidden" name="action" value={a} />66                          <ActionButton label={a} />67                        </form>68                      ))}69                      <form action={connectorAction}>70                        <input type="hidden" name="id" value={String(r.id)} />71                        <input type="hidden" name="action" value={r.status === 'paused' ? 'resume' : 'pause'} />72                        <ActionButton label={r.status === 'paused' ? 'resume' : 'pause'} tone={r.status === 'paused' ? 'primary' : 'danger'} />73                      </form>74                      <Link href={`/admin/connectors/${String(r.id)}`} className="rounded-md border border-border px-2 py-0.5 text-[11px] font-medium hover:bg-inset">inspect</Link>75                    </div>76                  </td>77                </tr>78              );79            })}80          </tbody>81        </Table>82      </div>83      <p className="mt-3 text-[11px] text-subtle">run = incremental crawl · probe = 25-record smoke test · recrawl = backfill mode · retry = re-run from the last cursor. FC = Firecrawl success rate, SF = share of pages that needed Scrapfly (from the 24 h health snapshot).</p>84    </AdminShell>85  );86}87
Files · ad20030daddfa217412dc2fa1b9beb342d281233
Menu

Theme