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%
5.6 KB · 132 lines tsx
Raw Blame History
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