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, fmtBytes } from '@/components/admin/ui';4import { fmtDuration } from '@/components/meta/connectors-table';5import { adminApi, safeAdmin } from '@/lib/admin-api';6import { fmtDate, fmtInt, num } from '@/lib/format';78export const metadata: Metadata = { title: 'Costs' };910export default async function AdminCostsPage() {11 const res = await safeAdmin(adminApi.costs());12 if (res.error !== null) {13 return (14 <>15 <AdminHeader title="Costs" />16 <AdminError message={res.error} />17 </>18 );19 }20 const d = res.data.data;21 const totalRuns = d.per_connector.reduce((a, c) => a + (num(c.runs_30d) ?? 0), 0);22 const totalMs = d.per_connector.reduce((a, c) => a + (num(c.total_ms) ?? 0), 0);23 const totalFailed = d.per_connector.reduce((a, c) => a + (num(c.failed) ?? 0), 0);24 return (25 <>26 <AdminHeader title="Costs" lede="Compute (connector run time), storage and third-party provider usage over the last 30 days. All current sources are free; no paid provider is enabled." />2728 <div className="grid grid-cols-2 gap-3 md:grid-cols-4">29 <Tile label="Runs · 30 d" value={fmtInt(totalRuns)} />30 <Tile label="Run time · 30 d" value={fmtDuration(totalMs)} />31 <Tile label="Failed runs" value={fmtInt(totalFailed)} tone={totalFailed > 0 ? 'warn' : undefined} />32 <Tile label="Database" value={d.storage.database} />33 </div>3435 <SectionTitle>Per connector · last 30 days</SectionTitle>36 {d.per_connector.length === 0 ? (37 <p className="rounded-md border border-dashed border-rule-strong px-4 py-6 text-center text-sm text-ink-3">No runs in the last 30 days</p>38 ) : (39 <div className="overflow-x-auto">40 <table className="data-table stack md:min-w-[720px]">41 <thead>42 <tr>43 <th>Connector</th>44 <th className="num">Runs</th>45 <th className="num">Records</th>46 <th className="num">Total time</th>47 <th className="num">Avg / run</th>48 <th className="num">Failed</th>49 </tr>50 </thead>51 <tbody>52 {d.per_connector.map((c) => {53 const runs = num(c.runs_30d) ?? 0;54 const ms = num(c.total_ms) ?? 0;55 const failed = num(c.failed) ?? 0;56 return (57 <tr key={c.connector_name}>58 <td className="primary">59 <Link href={`/admin/connectors/${encodeURIComponent(c.connector_name)}`} className="mono text-sm text-ink hover:text-accent">60 {c.connector_name}61 </Link>62 </td>63 <td data-label="Runs" className="num tnum text-sm">64 {fmtInt(runs)}65 </td>66 <td data-label="Records" className="num tnum text-sm text-ink-2">67 {fmtInt(c.records)}68 </td>69 <td data-label="Total time" className="num tnum text-sm text-ink-2">70 {fmtDuration(ms)}71 </td>72 <td data-label="Avg / run" className="num tnum text-sm text-ink-2">73 {runs > 0 ? fmtDuration(ms / runs) : '—'}74 </td>75 <td data-label="Failed" className={`num tnum text-sm ${failed > 0 ? 'text-warn' : 'text-ink-2'}`}>76 {fmtInt(failed)}77 </td>78 </tr>79 );80 })}81 </tbody>82 </table>83 </div>84 )}8586 <SectionTitle>Storage</SectionTitle>87 <div className="grid grid-cols-2 gap-3 md:grid-cols-4">88 <Tile label="Database" value={d.storage.database} hint="pg_database_size" />89 <Tile label="orbital_elements" value={d.storage.orbital_elements} hint="append-only history" />90 <Tile label="satellites" value={d.storage.satellites} hint="canonical table + indexes" />91 <Tile label="Raw payloads" value={d.storage.raw_uncompressed} hint="uncompressed bytes (stored gzip)" />92 </div>9394 <SectionTitle>Raw snapshot growth · 30 days</SectionTitle>95 {d.raw_growth.length === 0 ? (96 <p className="rounded-md border border-dashed border-rule-strong px-4 py-6 text-center text-sm text-ink-3">No snapshots in the last 30 days</p>97 ) : (98 <div className="overflow-x-auto">99 <table className="data-table md:max-w-md">100 <thead>101 <tr>102 <th>Day</th>103 <th className="num">Snapshots</th>104 <th className="num">Bytes</th>105 </tr>106 </thead>107 <tbody>108 {d.raw_growth.map((g) => (109 <tr key={g.day}>110 <td className="text-sm">{fmtDate(g.day)}</td>111 <td className="num tnum text-sm">{fmtInt(g.snapshots)}</td>112 <td className="num tnum text-sm text-ink-2">{fmtBytes(g.bytes)}</td>113 </tr>114 ))}115 </tbody>116 </table>117 </div>118 )}119120 <SectionTitle>Paid providers</SectionTitle>121 <ul className="divide-y divide-rule border-y border-rule">122 {Object.entries(d.paid_providers).map(([k, v]) => (123 <li key={k} className="flex items-center justify-between gap-3 py-2.5 text-sm">124 <span className="mono text-ink">{k}</span>125 <span className={v === 'not enabled' ? 'text-ink-3' : 'text-warn'}>{v}</span>126 </li>127 ))}128 </ul>129 </>130 );131}132