wayfarer-weather/lib/weather/extras/nws-alerts.ts

76 lines
2.8 KiB
TypeScript
Raw Normal View History

2026-10-31 21:20:16 -05:00
import { toSeverity } from "../describe";
import { fetchJson } from "../http";
import type { WeatherAlert } from "../types";
/**
* Active US weather alerts from the National Weather Service (free, no key).
* Docs: https://www.weather.gov/documentation/services-web-api
*
* NWS asks every app to identify itself with a User-Agent containing contact
* info, so set NWS_CONTACT in .env.local (an email or website).
* Outside the US this returns nothing, which is fine.
*/
interface NwsResponse {
features: {
properties: {
event: string;
headline: string | null;
severity: string;
senderName: string;
onset: string | null;
effective: string;
ends: string | null;
expires: string;
description: string;
instruction: string | null;
};
}[];
}
/** Rough boxes around US states and territories — skips pointless calls elsewhere. */
function mightBeInUS(lat: number, lon: number): boolean {
const mainland = lat > 24 && lat < 50 && lon > -125 && lon < -66;
const alaska = lat > 51 && lat < 72 && (lon < -129 || lon > 172);
const hawaii = lat > 18 && lat < 23 && lon > -161 && lon < -154;
const caribbean = lat > 17 && lat < 19 && lon > -68 && lon < -64; // Puerto Rico, USVI
const pacific = lat > 13 && lat < 21 && lon > 144 && lon < 146; // Guam, Northern Marianas
return mainland || alaska || hawaii || caribbean || pacific;
}
let warnedMissingContact = false;
function buildNwsUserAgent(): string {
const explicit = process.env.NWS_CONTACT?.trim();
if (explicit) return `WayfarerWeather/1.0 (${explicit})`;
const deployedHost = process.env.VERCEL_PROJECT_PRODUCTION_URL?.trim() || process.env.VERCEL_URL?.trim();
if (deployedHost) return `WayfarerWeather/1.0 (https://${deployedHost})`;
if (process.env.NODE_ENV === "production" && !warnedMissingContact) {
warnedMissingContact = true;
console.warn("[Wayfarer] Set NWS_CONTACT (email or site URL) so api.weather.gov requests include contact details.");
}
return "WayfarerWeather/1.0 (local-dev)";
}
export async function getNwsAlerts(lat: number, lon: number): Promise<WeatherAlert[]> {
if (!mightBeInUS(lat, lon)) return [];
const point = `${lat.toFixed(4)},${lon.toFixed(4)}`;
const data = await fetchJson<NwsResponse>(`https://api.weather.gov/alerts/active?point=${point}`, "National Weather Service", {
cacheSeconds: 300, // alerts matter; refresh more often
headers: { "User-Agent": buildNwsUserAgent(), Accept: "application/geo+json" },
});
return data.features.map(({ properties: p }) => ({
title: p.headline ?? p.event,
severity: toSeverity(p.severity),
source: p.senderName,
starts: p.onset ?? p.effective,
ends: p.ends ?? p.expires,
description: p.description,
instruction: p.instruction,
}));
}