SPB Git forge

spb/rareindex

Public
54commits 1branches 0releases
7.1 MBsize
maindefault branch
10 days agolast push
TypeScript 61.9% HTML 37.2% SQL 0.7%
13.0 KB · 229 lines tsx
Raw Blame History
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 { HealthLabel, SourceLogo, Pct, Progress, Chip } from '@/components/admin/connector-bits';7import { Table, th, td, tdNum } from '@/components/ui/primitives';89type Row = Record<string, unknown>;1011function str(v: unknown): string {12  return v === null || v === undefined ? '' : String(v);13}1415/** Connector explorer (SPEC §29): every connector with source, geography, acquisition, health, usage, backfill and actions. */16export default async function ConnectorsPage({ searchParams }: { searchParams: Promise<Record<string, string | undefined>> }) {17  await requireAdmin();18  const sp = await searchParams;19  const rowsAll = (await connectorsOverview()) as Row[];20  const q = (sp.q ?? '').toLowerCase();21  const status = sp.status ?? '';22  const category = sp.category ?? '';23  const country = sp.country ?? '';24  const rows = rowsAll.filter((r) => {25    const meta = (r.meta ?? {}) as Row;26    if (status && String(r.health_status ?? 'unknown') !== status && String(r.status) !== status) return false;27    if (category && !((r.categories as string[]) ?? []).includes(category)) return false;28    if (country && String(meta.country ?? (r.regions as string[])?.[0] ?? '') !== country) return false;29    if (q && !`${str(r.id)} ${str(r.display_name)} ${str(meta.domain)} ${((r.categories as string[]) ?? []).join(' ')}`.toLowerCase().includes(q)) return false;30    return true;31  });32  const counts = rowsAll.reduce<Record<string, number>>((acc, r) => {33    const k = String(r.health_status ?? 'unknown');34    acc[k] = (acc[k] ?? 0) + 1;35    return acc;36  }, {});37  const categories = [...new Set(rowsAll.flatMap((r) => (r.categories as string[]) ?? []))].sort();38  const countries = [...new Set(rowsAll.map((r) => String(((r.meta ?? {}) as Row).country ?? (r.regions as string[])?.[0] ?? 'global')))].sort();3940  return (41    <AdminShell42      current="/admin/connectors"43      title="Connector explorer"44      subtitle={45        <>46          {rowsAll.length} connectors · {Object.entries(counts).map(([k, v]) => `${v} ${k}`).join(' · ')} · <Link href="/admin/coverage" className="underline">coverage dashboard</Link> · <Link href="/api/admin/connectors" className="underline">JSON</Link>47        </>48      }49      actions={50        <form className="flex flex-wrap items-center gap-1 text-xs" method="get">51          <input name="q" defaultValue={sp.q ?? ''} placeholder="search id, domain, category" className="rounded-md border border-border bg-sunken px-2 py-1 text-xs" />52          <select name="status" defaultValue={status} className="rounded-md border border-border bg-sunken px-2 py-1 text-xs">53            <option value="">any health</option>54            {['healthy', 'degraded', 'failing', 'disabled', 'paused', 'maintenance', 'unknown'].map((s) => (55              <option key={s} value={s}>{s}</option>56            ))}57          </select>58          <select name="category" defaultValue={category} className="rounded-md border border-border bg-sunken px-2 py-1 text-xs">59            <option value="">any category</option>60            {categories.map((c) => (61              <option key={c} value={c}>{c}</option>62            ))}63          </select>64          <select name="country" defaultValue={country} className="rounded-md border border-border bg-sunken px-2 py-1 text-xs">65            <option value="">any country</option>66            {countries.map((c) => (67              <option key={c} value={c}>{c}</option>68            ))}69          </select>70          <button type="submit" className="rounded-md border border-border px-2 py-1 text-xs hover:bg-inset">Filter</button>71        </form>72      }73    >74      <div className="card overflow-x-auto">75        <Table>76          <thead>77            <tr>78              <th className={th}>Source</th>79              <th className={th}>Category · country</th>80              <th className={th}>Health</th>81              <th className={th}>Status</th>82              <th className={th}>Acquisition</th>83              <th className={th}>Last sync</th>84              <th className={`${th} text-right`}>Items 24h</th>85              <th className={`${th} text-right`}>Errors</th>86              <th className={`${th} text-right`}>FC · SF credits</th>87              <th className={th}>Rate limit</th>88              <th className={th}>Priority</th>89              <th className={th}>Backfill</th>90              <th className={th}>Actions</th>91            </tr>92          </thead>93          <tbody>94            {rows.length === 0 ? (95              <tr>96                <td className={td} colSpan={13}>97                  <span className="text-subtle">No connector matches. Add meta.json files under connectors/, run pnpm registry && pnpm db:seed.</span>98                </td>99              </tr>100            ) : null}101            {rows.map((r) => {102              const meta = (r.meta ?? {}) as Row;103              const health = (r.health ?? {}) as Row;104              const anomalies = Array.isArray(r.last_anomalies) ? (r.last_anomalies as string[]) : [];105              const drift = ((health.schema_drift as string[]) ?? []).length > 0 || anomalies.some((a) => /schema_drift|selector|pagination_failure|result_count_collapse/.test(a));106              const httpErrors = Object.values((health.http_errors as Record<string, number>) ?? {}).reduce((a, b) => a + b, 0);107              const cats = (r.categories as string[]) ?? [];108              const id = str(r.id);109              const domain = str(meta.domain);110              const country = str(meta.country ?? (r.regions as string[])?.[0] ?? 'global');111              const refresh = Number(r.refresh_frequency_minutes);112              const refreshClass = str(meta.refreshClass) || (refresh <= 5 ? 'hot' : refresh <= 60 ? 'active' : refresh <= 1440 ? 'normal' : 'archive');113              const missing = (health.missing_requirements as string[]) ?? [];114              return (115                <tr key={id} className="align-top">116                  <td className={td}>117                    <div className="flex items-center gap-2">118                      <SourceLogo domain={domain} name={str(r.display_name)} logoUrl={str(meta.logoUrl) || null} />119                      <div>120                        <Link href={`/admin/connectors/${id}`} className="font-medium hover:underline">{str(r.display_name)}</Link>121                        <div className="text-[11px] text-subtle">122                          {domain ? (123                            <a href={`https://${domain}`} target="_blank" rel="noreferrer noopener" className="hover:underline">{domain}</a>124                          ) : (125                            id126                          )}127                          {' · '}128                          {id}129                        </div>130                      </div>131                    </div>132                  </td>133                  <td className={td}>134                    <div className="flex max-w-[220px] flex-wrap gap-1">135                      {cats.slice(0, 3).map((c) => (136                        <Chip key={c}>{c}</Chip>137                      ))}138                      {cats.length > 3 ? <Chip>+{cats.length - 3}</Chip> : null}139                      <Chip tone="index">{country}</Chip>140                    </div>141                  </td>142                  <td className={td}>143                    <HealthLabel status={str(r.health_status) || 'unknown'} />144                    <div className="text-[10px] text-subtle">145                      {health.success_rate_24h != null ? <>success <Pct value={health.success_rate_24h} /></> : 'no runs 24h'}146                      {r.health_at ? ` · ${fmtTs(r.health_at).slice(5, 16)}` : ''}147                    </div>148                    {drift ? <div className="text-[10px] text-alert">schema drift?</div> : null}149                    {missing.length ? <div className="text-[10px] text-alert">needs {missing.join(', ')}</div> : null}150                  </td>151                  <td className={td}>152                    <StatusPill status={str(r.status)} />153                    {r.last_run_status ? <div className="text-[10px] text-subtle">run: {str(r.last_run_status)}</div> : null}154                  </td>155                  <td className={td}>156                    <div className="text-xs">{str(meta.acquisitionMethod) || (r.engine_priority as string[]).join(' → ')}</div>157                    <div className="text-[10px] text-subtle">{(r.engine_priority as string[]).join('→')} · {refreshClass} · every {n(refresh)} min</div>158                  </td>159                  <td className={td}>160                    {fmtTs(r.last_started_at)}161                    <div className="text-[10px] text-subtle">ok: {fmtTs(r.last_success_at).slice(0, 16)}</div>162                    <div className="text-[10px] text-subtle">fresh: {health.data_freshness ? fmtTs(health.data_freshness).slice(0, 10) : '—'}</div>163                  </td>164                  <td className={tdNum}>165                    {n(r.records_24h)}166                    <div className="text-[10px] text-subtle">{n(r.normalized_24h)} norm. · {n(r.duplicates_24h)} dup.</div>167                    <div className="text-[10px] text-subtle">{n(r.pages_per_min, 1)} pages/min</div>168                  </td>169                  <td className={`${tdNum} ${r.last_error || httpErrors ? 'text-loss' : ''}`} title={str(r.last_error)}>170                    {httpErrors || (r.last_error ? 1 : 0)}171                    {anomalies.length ? <div className="text-[10px] text-alert">{anomalies.length} anomal.</div> : null}172                    {Number(health.challenges_24h) > 0 ? <div className="text-[10px] text-alert">{n(health.challenges_24h)} challenges</div> : null}173                  </td>174                  <td className={tdNum}>175                    {n(r.fc_credits_24h)} · {n(r.sf_credits_24h)}176                    <div className="text-[10px] text-subtle">177                      FC <Pct value={health.firecrawl_success_rate} /> · SF <Pct value={health.scrapfly_fallback_rate} />178                    </div>179                  </td>180                  <td className={td}>181                    <div className="text-xs">{health.latency_ms_avg != null ? `${n(health.latency_ms_avg)} ms` : '—'}</div>182                    <div className="text-[10px] text-subtle">{Number(health.rate_limited_24h) > 0 ? `${n(health.rate_limited_24h)}× 429` : 'no 429'} · {n(health.requests_24h)} req</div>183                  </td>184                  <td className={td}>185                    <Chip tone={str(r.priority) === 'high' ? 'index' : 'neutral'}>{str(r.priority)}</Chip>186                    <div className="text-[10px] text-subtle">trust {source_trust(meta)}</div>187                  </td>188                  <td className={td}>{r.backfill_id ? <Progress percent={r.backfill_percent as number | null} status={str(r.backfill_status)} /> : <span className="text-[10px] text-subtle">{str(meta.historicalDepth) && str(meta.historicalDepth) !== 'none' ? `history: ${str(meta.historicalDepth)}` : '—'}</span>}</td>189                  <td className={td}>190                    <div className="flex max-w-[210px] flex-wrap gap-1">191                      {(192                        [193                          ['probe', 'Test'],194                          ['run', 'Run now'],195                          ['backfill', 'Backfill'],196                        ] as const197                      ).map(([a, label]) => (198                        <form key={a} action={connectorAction}>199                          <input type="hidden" name="id" value={id} />200                          <input type="hidden" name="action" value={a} />201                          <ActionButton label={label} />202                        </form>203                      ))}204                      <form action={connectorAction}>205                        <input type="hidden" name="id" value={id} />206                        <input type="hidden" name="action" value={r.status === 'active' ? 'pause' : 'resume'} />207                        <ActionButton label={r.status === 'active' ? 'Pause' : 'Resume'} tone={r.status === 'active' ? 'danger' : 'primary'} />208                      </form>209                      <Link href={`/admin/connectors/${id}`} className="rounded-md border border-border px-2 py-0.5 text-[11px] font-medium hover:bg-inset">Logs</Link>210                    </div>211                  </td>212                </tr>213              );214            })}215          </tbody>216        </Table>217      </div>218      <p className="mt-3 text-[11px] text-subtle">219        Test = 25-record probe · Run now = incremental crawl · Backfill = resumable historical campaign (progress in the Backfill column) · Pause/Resume flips scheduling; Maintenance is available on the connector page. FC/SF = Firecrawl/Scrapfly credits spent in 24 h and success/fallback rates. Health labels: UP · DEGRADED · BROKEN · DISABLED · MAINTENANCE.220      </p>221    </AdminShell>222  );223}224225function source_trust(meta: Row): string {226  const t = Number(meta.trustScore);227  return Number.isFinite(t) ? `${Math.round(t * 100)}%` : '—';228}229