import express from 'express'; import {resolve, dirname} from 'path'; import {fileURLToPath} from 'url'; import {cfg} from './config.mjs'; import {queryCurrent, queryHourly, queryDaily, getCoords} from './influx.mjs'; import {getCelestialCurrent, getCelestialForecast} from './celestial.mjs'; import {getSpaceWeather} from './space.mjs'; import {apiReference} from '@scalar/express-api-reference'; import {spec} from './spec.mjs'; import {existsSync} from 'fs'; import {getAirTraffic, getAirTrafficHistory, getShapes} from './airtraffic.mjs'; import {getWeatherCondition} from './openweather.mjs'; import {lastForecast, getForecast, forecastTTL} from './forecast.mjs'; import {dailyWeather, hourlyWeather} from './openmeteo.mjs'; const app = express(); const DIR = dirname(fileURLToPath(import.meta.url)); const CLIENT_DIST = resolve(DIR, '../public'); app.use(express.json()); app.use('/icons', express.static(resolve(DIR, '../public', 'icons'))); app.use((req, res, next) => { res.setHeader('Access-Control-Allow-Origin', '*'); next(); }); app.use(express.static(CLIENT_DIST)); function filterFields(obj, fields) { if(!fields) return obj; const keys = new Set(fields.split(',').map(f => f.trim())); return Object.fromEntries(Object.entries(obj).filter(([k]) => keys.has(k))); } function filterArr(arr, fields) { if(!fields) return arr; return arr.map(row => filterFields(row, fields)); } // ── Current ─────────────────────────────────────────────────────────────────── app.get('/api/current', async (req, res) => { const { fields } = req.query const [sensors, coords] = await Promise.all([queryCurrent(), getCoords()]) const space = getCelestialCurrent(coords.latitude, coords.longitude) const condition = getWeatherCondition(Object.assign(sensors, space)) // Merge forecast summary fields — sensor readings take priority const forecast = await getForecast(); console.log(forecast?.summary); const merged = { ...(forecast?.summary ? { precipitation_chance: forecast.summary.precipitation_chance, temperature_max: forecast.summary.temperature_max, temperature_min: forecast.summary.temperature_min, uv_index_max: forecast.summary.uv_index, } : {}), ...condition, ...sensors, ...space, } res.json(filterFields(merged, fields)) }) // ── Hourly ──────────────────────────────────────────────────────────────────── app.get('/api/hourly', async (req, res) => { const { fields } = req.query const now = new Date() const plus24 = new Date(now.getTime() + 24 * 3600 * 1000) const start = req.query.start ? new Date(req.query.start) : new Date(now.setHours(0, 0, 0, 0)) const end = req.query.end ? new Date(req.query.end) : plus24 const coords = await getCoords() // Past → now: InfluxDB const historyEnd = new Date(Math.min(now, end)) historyEnd.setMinutes(0, 0, 0) const [historyResult] = await Promise.allSettled([ start < now ? queryHourly(start, historyEnd) : Promise.resolve([]) ]) const history = historyResult.status === 'fulfilled' ? historyResult.value : [] // Now → +24h: cached physics forecast const physics = end > now ? lastForecast.slots.filter(s => { const t = new Date(s.time); return t >= now && t <= new Date(Math.min(end, plus24)) }) : [] // +24h → beyond: Open-Meteo let meteo = [] if (end > plus24) { const [meteoResult] = await Promise.allSettled([hourlyWeather(coords.latitude, coords.longitude, plus24, end)]) meteo = meteoResult.status === 'fulfilled' ? meteoResult.value : [] } const hourly = getCelestialForecast(coords.latitude, coords.longitude, [...history, ...physics, ...meteo]) res.json(filterArr(hourly, fields)) }) // ── Daily ───────────────────────────────────────────────────────────────────── app.get('/api/daily', async (req, res) => { const { fields } = req.query const now = new Date() now.setHours(0, 0, 0, 0) const next = new Date(now); next.setDate(now.getDate() + 1) const start = req.query.start ? new Date(req.query.start) : now const end = req.query.end ? new Date(req.query.end) : next const coords = await getCoords() // Does the requested window overlap with today (the 24h forecast window)? const todayStr = now const usePhysics = lastForecast.summary && start <= next && end >= now // Past influx history (days before today) const [sensorResult] = await Promise.allSettled([ start < now ? queryDaily(start, now) : Promise.resolve([]) ]) const history = sensorResult.status === 'fulfilled' ? sensorResult.value : [] // Future meteo (days after today) const meteoStart = end > next ? next : null const [meteoResult] = await Promise.allSettled([ meteoStart && end > next ? dailyWeather(coords.latitude, coords.longitude, next, end) : Promise.resolve([]) ]) const meteo = meteoResult.status === 'fulfilled' ? meteoResult.value : [] // Splice in physics summary for today, skip any meteo row that duplicates it const todaySlot = usePhysics ? [lastForecast.summary] : [] const meteoClean = meteo.filter(r => r.time !== todayStr) const daily = getCelestialForecast( coords.latitude, coords.longitude, [...history, ...todaySlot, ...meteoClean] ) res.json(filterArr(daily, fields)) }) // ── Position ────────────────────────────────────────────────────────────────── app.get('/api/position', async (req, res) => { const {fields} = req.query; const data = await getCoords(); res.json(filterFields(data, fields)); }); // ── Space ───────────────────────────────────────────────────────────────────── app.get('/api/space', async (req, res) => { const {fields} = req.query; res.json(filterFields(await getSpaceWeather(), fields)); }); // ── ADSB Proxy ──────────────────────────────────────────────────────────────── app.get('/api/air-traffic', async (req, res) => res.json(await getAirTraffic())); app.get('/api/air-traffic/:icao', async (req, res) => res.json(await getAirTrafficHistory(req.params.icao))); app.get('/api/air-traffic-shapes', async (req, res) => res.json(await getShapes())); // ── DOCS ────────────────────────────────────────────────────────────────────── app.use('/docs', apiReference({spec: {url: '/openapi.json'}, theme: 'default'})); app.get('/openapi.json', (req, res) => res.json(spec)); app.get('/openapi.yaml', async (req, res) => { const {stringify} = await import('yaml'); res.setHeader('Content-Type', 'text/yaml'); res.send(stringify(spec)); }); // ── Website ─────────────────────────────────────────────────────────────────── app.get('*', (req, res) => { const index = resolve(CLIENT_DIST, 'index.html'); if(existsSync(index)) res.sendFile(index); else res.status(404).send('Client not built yet — run npm run build in /client'); }); // ── Start ───────────────────────────────────────────────────────────────────── const c = cfg(); setTimeout(getForecast, 1) setInterval(getForecast, forecastTTL) app.listen(c.PORT, () => { console.log(`🌦 Weather API — http://localhost:${c.PORT}`) });