server/src/swpc.ts
40 lines · 1735 bytes
1// NOAA SWPC feeds. Responses carry cache-control max-age=60; poll no faster.
2
3export const KP_NOW_URL = "https://services.swpc.noaa.gov/json/planetary_k_index_1m.json";
4export const KP_FORECAST_URL = "https://services.swpc.noaa.gov/products/noaa-planetary-k-index-forecast.json";
5
6export interface SpaceWeather {
7 kpNow: number;
8 observedAt: string;
9 forecast: { start: Date; kp: number; predicted: boolean }[];
10}
11
12/** SWPC time tags are UTC without a zone designator. */
13function parse(tag: string): Date {
14 return new Date(tag.endsWith("Z") ? tag : `${tag}Z`);
15}
16
17export function decode(nowJson: unknown, forecastJson: unknown): SpaceWeather {
18 const minutes = nowJson as { time_tag: string; estimated_kp: number }[];
19 const last = minutes.at(-1);
20 if (!last) throw new Error("empty Kp feed");
21 const rows = forecastJson as { time_tag: string; kp: number; observed: string }[];
22 return {
23 kpNow: last.estimated_kp,
24 observedAt: parse(last.time_tag).toISOString(),
25 forecast: rows.map((r) => ({ start: parse(r.time_tag), kp: r.kp, predicted: r.observed !== "observed" })),
26 };
27}
28
29export async function fetchSpaceWeather(): Promise<SpaceWeather> {
30 const [a, b] = await Promise.all([fetch(KP_NOW_URL), fetch(KP_FORECAST_URL)]);
31 if (!a.ok || !b.ok) throw new Error(`SWPC ${a.status}/${b.status}`);
32 return decode(await a.json(), await b.json());
33}
34
35/** Highest predicted Kp in the next 24 hours. */
36export function next24h(w: SpaceWeather, now: Date): number | null {
37 const end = now.getTime() + 24 * 3600_000;
38 const kps = w.forecast.filter((f) => f.predicted && f.start.getTime() + 3 * 3600_000 > now.getTime() && f.start.getTime() <= end).map((f) => f.kp);
39 return kps.length ? Math.max(...kps) : null;
40}