spb/satelliteindex
Public
TypeScript 66.5%
Python 30.9%
JavaScript 1.4%
CSS 0.7%
1import type { Metadata } from 'next';2import Link from 'next/link';3import { AdminError, AdminHeader, SectionTitle, Tile } from '@/components/admin/ui';4import { Pagination } from '@/components/ui/pagination';5import { adminApi, safeAdmin } from '@/lib/admin-api';6import { fmtAgo, fmtDateTime, fmtInt, num } from '@/lib/format';78export const metadata: Metadata = { title: 'Data quality' };910const CHECKS: { key: keyof Awaited<ReturnType<typeof adminApi.dataQuality>>['checks']; label: string; hint: string }[] = [11 { key: 'missing_norad', label: 'Missing NORAD', hint: 'satellites without a catalog number' },12 { key: 'missing_cospar', label: 'Missing COSPAR', hint: 'no international designator (analyst / unassigned objects)' },13 { key: 'active_without_operator', label: 'Active without operator', hint: 'active payloads/stations with no resolved operator' },14 { key: 'active_without_country', label: 'Active without country', hint: 'active objects whose owner code has no country' },15 { key: 'stale_active', label: 'Stale active', hint: 'active with GP but latest epoch > 30 d' },16 { key: 'broken_launch_links', label: 'Broken launch links', hint: 'COSPAR present but no launch row' },17 { key: 'active_without_elements', label: 'Active without elements', hint: 'active but never seen in the GP feed' },18];1920export default async function AdminDataQualityPage({ searchParams }: { searchParams: Promise<{ page?: string; flag?: string }> }) {21 const sp = await searchParams;22 const page = Math.max(1, Number(sp.page) || 1);23 const flag = sp.flag || null;24 const res = await safeAdmin(adminApi.dataQuality(page, flag, 50));25 if (res.error !== null) {26 return (27 <>28 <AdminHeader title="Data quality" />29 <AdminError message={res.error} />30 </>31 );32 }33 const d = res.data;34 const now = Date.now();35 const href = (p: number, f: string | null = flag) => `/admin/data-quality?${new URLSearchParams({ ...(f ? { flag: f } : {}), ...(p > 1 ? { page: String(p) } : {}) }).toString()}`;36 return (37 <>38 <AdminHeader title="Data quality" lede="Live checks computed on the canonical satellites table, plus the open flags raised by the connectors during normalization. Flags are never auto-resolved by deleting data." />3940 <SectionTitle>Checks (live counts)</SectionTitle>41 <div className="grid grid-cols-2 gap-3 md:grid-cols-4 xl:grid-cols-7">42 {CHECKS.map((c) => {43 const v = num(d.checks[c.key]) ?? 0;44 return <Tile key={c.key} label={c.label} value={fmtInt(v)} hint={c.hint} tone={v > 0 && (c.key === 'stale_active' || c.key === 'broken_launch_links' || c.key === 'missing_norad') ? 'warn' : undefined} />;45 })}46 </div>4748 <SectionTitle>Open flags by type</SectionTitle>49 {d.summary.length === 0 ? (50 <p className="rounded-md border border-dashed border-rule-strong px-4 py-6 text-center text-sm text-ink-3">No open quality flags</p>51 ) : (52 <div className="no-scrollbar -mx-4 flex gap-1.5 overflow-x-auto px-4 md:mx-0 md:flex-wrap md:px-0">53 <Link href={href(1, null)} className={`inline-flex min-h-9 shrink-0 items-center rounded-md border px-3 text-xs ${!flag ? 'border-accent/40 bg-accent-soft text-accent' : 'border-rule text-ink-2 hover:bg-plane-2'}`}>54 All ({fmtInt(d.summary.reduce((a, s) => a + (num(s.open) ?? 0), 0))})55 </Link>56 {d.summary.map((s) => (57 <Link key={s.flag} href={href(1, s.flag)} className={`mono inline-flex min-h-9 shrink-0 items-center rounded-md border px-3 text-xs ${flag === s.flag ? 'border-accent/40 bg-accent-soft text-accent' : 'border-rule text-ink-2 hover:bg-plane-2'}`}>58 {s.flag} · {fmtInt(s.open)}59 </Link>60 ))}61 </div>62 )}6364 <SectionTitle>Flagged satellites</SectionTitle>65 {d.data.length === 0 ? (66 <p className="rounded-md border border-dashed border-rule-strong px-4 py-6 text-center text-sm text-ink-3">No flagged rows{flag ? ` for ${flag}` : ''}</p>67 ) : (68 <>69 <div className="overflow-x-auto">70 <table className="data-table stack md:min-w-[800px]">71 <thead>72 <tr>73 <th>Satellite</th>74 <th>Flag</th>75 <th>Detail</th>76 <th>Raised</th>77 </tr>78 </thead>79 <tbody>80 {d.data.map((f) => (81 <tr key={f.id}>82 <td className="primary">83 {f.slug ? (84 <Link href={`/satellite/${f.slug}`} className="text-sm text-ink hover:text-accent">85 {f.name ?? f.slug}86 </Link>87 ) : (88 <span className="mono text-xs text-ink-2">89 {f.entity_type} {f.entity_id}90 </span>91 )}92 {f.norad_id !== null && <span className="mono ml-2 text-xs text-ink-3">NORAD {f.norad_id}</span>}93 </td>94 <td data-label="Flag" className="mono text-xs text-warn">95 {f.flag}96 </td>97 <td data-label="Detail" className="text-sm text-ink-2">98 {f.detail ?? '—'}99 </td>100 <td data-label="Raised" title={fmtDateTime(f.created_at)} className="text-sm text-ink-2">101 {fmtAgo(f.created_at, now)}102 </td>103 </tr>104 ))}105 </tbody>106 </table>107 </div>108 <Pagination className="mt-4" page={d.pagination.page} pages={d.pagination.pages} total={d.pagination.total} pageSize={d.pagination.page_size} makeHref={(p) => href(p)} />109 </>110 )}111 </>112 );113}114