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%
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