import { ssnFromFlux, sunActivityLabel } from './celestial.mjs' const CACHE = {} const CACHE_MS = 10 * 60 * 1000 async function cachedFetch(key, url) { const now = Date.now() if (CACHE[key] && now - CACHE[key].ts < CACHE_MS) return CACHE[key].data const res = await fetch(url) const data = await res.json() CACHE[key] = { ts: now, data } return data } async function getKpAp() { const data = await cachedFetch('kp', 'https://services.swpc.noaa.gov/products/noaa-planetary-k-index.json') // First row is always the header const rows = Array.isArray(data) ? data.slice(1).filter(r => Array.isArray(r)) : [] if (!rows.length) return {} const latest = rows[rows.length - 1] const kp = parseFloat(latest[1]) const ap = parseFloat(latest[2]) return { space_kp_index: isNaN(kp) ? null : kp, space_ap_index: isNaN(ap) ? null : ap, space_geomagnetic_storm: kp >= 7 ? 'Severe' : kp >= 6 ? 'Strong' : kp >= 5 ? 'Moderate' : kp >= 4 ? 'Minor' : 'None', } } async function getSolarWind() { const [plasma, mag] = await Promise.all([ cachedFetch('plasma', 'https://services.swpc.noaa.gov/products/solar-wind/plasma.json'), cachedFetch('mag', 'https://services.swpc.noaa.gov/products/solar-wind/mag.json'), ]) // Skip header row, find last valid row const pRows = Array.isArray(plasma) ? plasma.slice(1).filter(r => Array.isArray(r) && r[2] !== null) : [] const mRows = Array.isArray(mag) ? mag.slice(1).filter(r => Array.isArray(r) && r[3] !== null) : [] const p = pRows[pRows.length - 1] || [] const m = mRows[mRows.length - 1] || [] return { space_solar_wind_speed_kms: p[2] != null ? parseFloat(p[2]) : null, space_solar_wind_density: p[1] != null ? parseFloat(p[1]) : null, space_solar_wind_temp: p[3] != null ? parseFloat(p[3]) : null, space_imf_bz: m[3] != null ? parseFloat(m[3]) : null, space_imf_bt: m[6] != null ? parseFloat(m[6]) : null, } } async function getSolarFlux() { const data = await cachedFetch('flux', 'https://services.swpc.noaa.gov/products/10cm-flux-30-day.json') // Format: [["yyyy-mm-dd", flux, ...], ...] — no header row on this one const rows = Array.isArray(data) ? data.filter(r => Array.isArray(r) && !isNaN(parseFloat(r[1]))) : [] if (!rows.length) return {} const f107 = parseFloat(rows[rows.length - 1][1]) return { space_solar_flux_f107: isNaN(f107) ? null : f107, sun_ssn: ssnFromFlux(f107), sun_activity_label: sunActivityLabel(f107), } } export async function getSpaceWeather() { const [kpAp, wind, flux] = await Promise.allSettled([getKpAp(), getSolarWind(), getSolarFlux()]) return { ...(kpAp.status === 'fulfilled' ? kpAp.value : {}), ...(wind.status === 'fulfilled' ? wind.value : {}), ...(flux.status === 'fulfilled' ? flux.value : {}), } }