SPB Git forge
3commits 1branches 0releases
417.0 KBsize
maindefault branch
10 days agolast push
TypeScript 66.5% Python 30.9% JavaScript 1.4% CSS 0.7%
6.4 KB · 131 lines tsx
Raw Blame History
1import type { Metadata } from 'next';2import Link from 'next/link';3import { EnabledToggle, RunNowButton } from '@/components/admin/actions';4import { AdminError, AdminHeader, JsonInline, RunsTable, SectionTitle, Tile, fmtBytes } from '@/components/admin/ui';5import { RunStatusPill, fmtDuration, fmtInterval, fmtUntil } from '@/components/meta/connectors-table';6import { adminApi, safeAdmin } from '@/lib/admin-api';7import { fmtAgo, fmtDateTime, fmtInt, num } from '@/lib/format';89export const metadata: Metadata = { title: 'Overview' };1011export default async function AdminOverviewPage() {12  const res = await safeAdmin(adminApi.overview());13  if (res.error !== null) {14    return (15      <>16        <AdminHeader title="Overview" />17        <AdminError message={res.error} />18      </>19    );20  }21  const d = res.data.data;22  const now = Date.now();23  const db = d.database;24  const openFlags = d.quality.reduce((acc, q) => acc + (num(q.open) ?? 0), 0);25  return (26    <>27      <AdminHeader title="Overview" lede={`Generated ${fmtDateTime(res.data.meta.generated_at)} · request ${res.data.meta.request_id}`} action={<Link href="/status" className="text-sm text-accent hover:underline">Public status →</Link>} />2829      <div className="grid grid-cols-2 gap-3 md:grid-cols-4 xl:grid-cols-8">30        <Tile label="Satellites" value={fmtInt(db.satellites)} />31        <Tile label="Element sets" value={fmtInt(db.elements)} hint="orbital_state rows" />32        <Tile label="Events" value={fmtInt(db.events)} />33        <Tile label="Raw snapshots" value={fmtInt(db.raw)} hint={`${fmtBytes(db.raw_bytes)} uncompressed`} href="/admin/raw" />34        <Tile label="Database" value={fmtBytes(db.db_bytes)} hint={`${fmtInt(db.db_connections)} connections`} />35        <Tile label="Redis" value={d.redis ? 'ok' : 'down'} tone={d.redis ? 'ok' : 'danger'} hint={`queue depth ${fmtInt(d.queue_depth)}`} />36        <Tile label="Open quality flags" value={fmtInt(openFlags)} tone={openFlags > 0 ? 'warn' : undefined} href="/admin/data-quality" />37        <Tile label="Review queue" value={fmtInt(d.review_open)} tone={d.review_open > 0 ? 'warn' : undefined} href="/admin/entity-resolution" />38      </div>3940      <SectionTitle>Connectors</SectionTitle>41      <div className="overflow-x-auto">42        <table className="data-table stack md:min-w-[1100px]">43          <thead>44            <tr>45              <th>Connector</th>46              <th>Status</th>47              <th>Last success</th>48              <th>Next run</th>49              <th className="num">Duration</th>50              <th className="num">Failures</th>51              <th className="num">Errors 7 d</th>52              <th>Circuit</th>53              <th>Actions</th>54            </tr>55          </thead>56          <tbody>57            {d.connectors.map((c) => {58              const circuitOpen = !!c.circuit_open_until && new Date(c.circuit_open_until).getTime() > now;59              return (60                <tr key={c.name}>61                  <td className="primary">62                    <Link href={`/admin/connectors/${encodeURIComponent(c.name)}`} className="mono text-sm text-ink hover:text-accent">63                      {c.name}64                    </Link>65                    <div className="text-xs text-ink-3">66                      {c.source_name} · every {fmtInterval(c.interval_seconds)} · priority {c.priority}67                      {!c.enabled && <span className="ml-2 text-warn">disabled</span>}68                    </div>69                    {c.description && <div className="mt-0.5 max-w-md text-xs text-ink-3">{c.description}</div>}70                  </td>71                  <td data-label="Status">72                    <RunStatusPill status={c.last_status} />73                  </td>74                  <td data-label="Last success" title={fmtDateTime(c.last_success_at)} className="text-sm">75                    {fmtAgo(c.last_success_at, now)}76                  </td>77                  <td data-label="Next run" title={fmtDateTime(c.next_run_at)} className="text-sm text-ink-2">78                    {c.enabled ? fmtUntil(c.next_run_at, now) : '—'}79                  </td>80                  <td data-label="Duration" className="num tnum text-sm text-ink-2">81                    {fmtDuration(c.last_duration_ms)}82                  </td>83                  <td data-label="Failures" className={`num tnum text-sm ${c.consecutive_failures > 0 ? 'text-warn' : 'text-ink-2'}`}>84                    {fmtInt(c.consecutive_failures)}85                  </td>86                  <td data-label="Errors 7 d" className={`num tnum text-sm ${(num(c.errors_7d) ?? 0) > 0 ? 'text-warn' : 'text-ink-2'}`}>87                    {fmtInt(c.errors_7d)}88                  </td>89                  <td data-label="Circuit">{circuitOpen ? <span className="text-xs text-danger">open · resets {fmtUntil(c.circuit_open_until, now)}</span> : <span className="text-xs text-ink-3">closed</span>}</td>90                  <td data-label="Actions" className="primary md:[grid-column:auto]">91                    <div className="flex flex-wrap items-center gap-2">92                      <RunNowButton name={c.name} />93                      <EnabledToggle name={c.name} enabled={c.enabled} />94                    </div>95                  </td>96                </tr>97              );98            })}99          </tbody>100        </table>101      </div>102      <p className="mt-2 text-xs text-ink-3">103        &ldquo;Run now&rdquo; pushes a job to the Redis queue consumed by the scheduler (<code className="mono">si schedule</code>). If Redis is down the job is not visible to the scheduler and the button says so. Enabling a connector also resets its circuit breaker.104      </p>105106      {d.failed_jobs.length > 0 && (107        <>108          <SectionTitle>Failed jobs</SectionTitle>109          <RunsTable runs={d.failed_jobs} now={now} />110        </>111      )}112113      <SectionTitle>Recent runs</SectionTitle>114      <RunsTable runs={d.recent_runs} now={now} />115      {d.recent_runs.some((r) => r.meta) && (116        <details className="mt-3 text-xs text-ink-3">117          <summary className="cursor-pointer py-1 hover:text-ink">Run metadata (latest 5)</summary>118          <ul className="mt-2 space-y-1">119            {d.recent_runs.slice(0, 5).map((r) => (120              <li key={r.id} className="grid gap-1 sm:grid-cols-[220px_minmax(0,1fr)]">121                <span className="mono">{r.connector_name}</span>122                <JsonInline value={r.meta} />123              </li>124            ))}125          </ul>126        </details>127      )}128    </>129  );130}131