/** * The one shape the UI understands. * * Every provider returns wildly different JSON. Each adapter in ./providers * translates its provider's response into these types, so the UI never has * to know which provider it is talking to. * * Rules every adapter follows: * - Units are metric: °C, km/h, hPa, km, mm. * - Times are ISO-8601 instants in UTC ("2026-10-21T23:00:00.000Z"). * The UI formats them in the location's time zone. * - A reading the provider doesn't offer is `null`, and the UI hides it. */ export type ProviderId = "openweather" | "weatherapi" | "tomorrow" | "openmeteo"; /** A deliberately small set of conditions — one per pixel icon. */ export type Condition = "clear" | "clouds" | "rain" | "storm" | "snow" | "fog"; export interface Place { name: string; region?: string; country?: string; lat: number; lon: number; } export interface CurrentWeather { time: string; tempC: number; feelsLikeC: number; condition: Condition; description: string; // human words, e.g. "light rain" isDay: boolean; humidity: number; // % windKph: number; gustKph: number | null; windDeg: number | null; // direction the wind blows FROM, 0 = north pressureHpa: number | null; dewPointC: number | null; visibilityKm: number | null; uvIndex: number | null; cloudCover: number | null; // % precipMm: number | null; // in the last hour } export interface HourlyForecast { time: string; // start of the hour (or 3-hour block) tempC: number; feelsLikeC: number | null; condition: Condition; isDay: boolean; precipChance: number | null; // % precipMm: number | null; windKph: number; gustKph: number | null; windDeg: number | null; humidity: number | null; uvIndex: number | null; } export interface DailyForecast { time: string; // any instant inside that local day (midday where possible) highC: number; lowC: number; condition: Condition; summary: string | null; // provider's own one-liner, if it has one precipChance: number | null; precipMm: number | null; windKph: number | null; // daily max gustKph: number | null; windDeg: number | null; uvIndexMax: number | null; humidity: number | null; // daily average sunrise: string | null; sunset: string | null; } export type AlertSeverity = "extreme" | "severe" | "moderate" | "minor" | "unknown"; export interface WeatherAlert { title: string; severity: AlertSeverity; source: string; // who issued it, e.g. "NWS Twin Cities/Chanhassen MN" starts: string | null; ends: string | null; description: string; instruction: string | null; } export interface AirQuality { usAqi: number; // US EPA scale, 0–500 pm25: number | null; // µg/m³ pm10: number | null; ozone: number | null; no2: number | null; } /** What one provider adapter returns. */ export interface ProviderForecast { current: CurrentWeather; hourly: HourlyForecast[]; daily: DailyForecast[]; hourlyStepHours: 1 | 3; timeZone: string | null; // IANA name, if the provider tells us alerts: WeatherAlert[]; } /** What /api/weather returns: the provider's forecast plus extras from other sources. */ export interface WeatherReport extends ProviderForecast { provider: ProviderId; timeZone: string | null; airQuality: AirQuality | null; alertSource: string | null; fetchedAt: string; } /** The contract each provider adapter fulfils. */ export interface WeatherProvider { id: ProviderId; label: string; /** Name of the env var holding its key, or null if no key is needed. */ envKey: string | null; /** Forecast horizon available on the configured tier. */ limits(): { forecastDays: number; hourlyStepHours: 1 | 3 }; getForecast(lat: number, lon: number): Promise; } /** Configured provider metadata exposed to the browser (never includes keys). */ export interface ProviderInfo { id: ProviderId; label: string; forecastDays: number; hourlyStepHours: 1 | 3; } /** Map overlays the radar panel can show. */ export interface RadarFrame { time: string; url: string; // Leaflet tile template with {z}/{x}/{y} }