/** * earth-now.co * Author: Simon-Pierre Boucher * Contact: contact@spboucher.ai * File: apps/web/lib/api.ts * Purpose: Typed fetchers + MetricSummary type mirroring the apps/api REST contract (/v1/metrics, /v1/models, /v1/rt/quakes) */ import type { CounterModel } from "@earth-now/counter"; export type Locale = "fr" | "en"; export interface LocalizedText { fr: string; en: string; } export type MetricDomain = | "population" | "climate" | "emissions" | "forest" | "ocean" | "energy" | "society" | "health" | "economy" | "tech" | "realtime" | "space"; export type MetricKind = "stock" | "cumulative" | "event" | "derived"; export type ModelLevel = 0 | 1 | 2 | "rt" | "derived"; export type DerivedOp = | "linear-combination" | "window" | "rate-of" | "depletion-countdown"; export interface MetricSource { id: string; name: string; url: string; license: string; cadence: string; } export interface MetricDisplay { decimals: number; sigFigs?: number; scale?: number; unit: LocalizedText; } export interface DerivedSpec { op: DerivedOp; inputs: Array<{ id: string; weight: number }>; constant?: number; window?: "today" | "ytd" | "session"; } export interface MetricSummary { id: string; name: LocalizedText; domain: MetricDomain; priority: string; kind: MetricKind; level: ModelLevel; model: string; unit: string; sources: MetricSource[]; display: MetricDisplay; windows: string[]; derived?: DerivedSpec; uncertaintyFraction?: number; editorialNote?: LocalizedText; stale: boolean; modelVersion?: string; observedAt?: string; } export interface ModelsResponse { models: Record; generatedAt: string; } export interface QuakesResponse { count24h: number; lastMajor: { mag: number; place: string; timeIso: string } | null; fetchedAt: string; } /** * API base URL. Client-side: NEXT_PUBLIC_API_URL (inlined at build time). * Server-side: API_URL takes precedence, then NEXT_PUBLIC_API_URL. */ export function apiBase(): string { if (typeof window === "undefined") { return ( process.env.API_URL ?? process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:4000" ); } return process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:4000"; } async function getJson(path: string): Promise { const res = await fetch(`${apiBase()}${path}`, { cache: "no-store" }); if (!res.ok) throw new Error(`API ${path} responded ${res.status}`); return (await res.json()) as T; } export async function fetchMetrics(): Promise { return getJson("/v1/metrics"); } export async function fetchModels(): Promise { return getJson("/v1/models"); } export async function fetchQuakes(): Promise { return getJson("/v1/rt/quakes"); } /** SSE stream URL — subscribed with EventSource in the client dashboard. */ export function sseStreamUrl(): string { return `${apiBase()}/sse/stream`; }