SPB Git

spb/earth-now Public License

earth-now.co — real-time planetary dashboard: live world metrics modeled, not streamed.

TypeScript 93% Shell 2.3% SQL 1.4% JavaScript 1.3% Dockerfile 1.2% CSS 0.8%
3.0 KB · 130 lines typescript
Raw Blame History
1/**2 * earth-now.co3 * Author:  Simon-Pierre Boucher4 * Contact: contact@spboucher.ai5 * File:    apps/web/lib/api.ts6 * Purpose: Typed fetchers + MetricSummary type mirroring the apps/api REST contract (/v1/metrics, /v1/models, /v1/rt/quakes)7 */89import type { CounterModel } from "@earth-now/counter";1011export type Locale = "fr" | "en";1213export interface LocalizedText {14  fr: string;15  en: string;16}1718export type MetricDomain =19  | "population"20  | "climate"21  | "emissions"22  | "forest"23  | "ocean"24  | "energy"25  | "society"26  | "health"27  | "economy"28  | "tech"29  | "realtime"30  | "space";3132export type MetricKind = "stock" | "cumulative" | "event" | "derived";3334export type ModelLevel = 0 | 1 | 2 | "rt" | "derived";3536export type DerivedOp =37  | "linear-combination"38  | "window"39  | "rate-of"40  | "depletion-countdown";4142export interface MetricSource {43  id: string;44  name: string;45  url: string;46  license: string;47  cadence: string;48}4950export interface MetricDisplay {51  decimals: number;52  sigFigs?: number;53  scale?: number;54  unit: LocalizedText;55}5657export interface DerivedSpec {58  op: DerivedOp;59  inputs: Array<{ id: string; weight: number }>;60  constant?: number;61  window?: "today" | "ytd" | "session";62}6364export interface MetricSummary {65  id: string;66  name: LocalizedText;67  domain: MetricDomain;68  priority: string;69  kind: MetricKind;70  level: ModelLevel;71  model: string;72  unit: string;73  sources: MetricSource[];74  display: MetricDisplay;75  windows: string[];76  derived?: DerivedSpec;77  uncertaintyFraction?: number;78  editorialNote?: LocalizedText;79  stale: boolean;80  modelVersion?: string;81  observedAt?: string;82}8384export interface ModelsResponse {85  models: Record<string, CounterModel>;86  generatedAt: string;87}8889export interface QuakesResponse {90  count24h: number;91  lastMajor: { mag: number; place: string; timeIso: string } | null;92  fetchedAt: string;93}9495/**96 * API base URL. Client-side: NEXT_PUBLIC_API_URL (inlined at build time).97 * Server-side: API_URL takes precedence, then NEXT_PUBLIC_API_URL.98 */99export function apiBase(): string {100  if (typeof window === "undefined") {101    return (102      process.env.API_URL ?? process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:4000"103    );104  }105  return process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:4000";106}107108async function getJson<T>(path: string): Promise<T> {109  const res = await fetch(`${apiBase()}${path}`, { cache: "no-store" });110  if (!res.ok) throw new Error(`API ${path} responded ${res.status}`);111  return (await res.json()) as T;112}113114export async function fetchMetrics(): Promise<MetricSummary[]> {115  return getJson<MetricSummary[]>("/v1/metrics");116}117118export async function fetchModels(): Promise<ModelsResponse> {119  return getJson<ModelsResponse>("/v1/models");120}121122export async function fetchQuakes(): Promise<QuakesResponse> {123  return getJson<QuakesResponse>("/v1/rt/quakes");124}125126/** SSE stream URL — subscribed with EventSource in the client dashboard. */127export function sseStreamUrl(): string {128  return `${apiBase()}/sse/stream`;129}130