krz/moonrise

Night-sky companion for iPhone, Apple Watch and Vision Pro: what's up tonight, the Moon, planets, launches and missions, and a Library of the solar system. SwiftUI app with a Cloudflare Workers backend. astronomy cloudflare-workers ios night-sky space swiftui visionos watchos

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}