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, getCelestialHourly, getCelestialDaily, getCelestialForecast} from './celestial.mjs'; import {getSpaceWeather} from './space.mjs'; import {getOpenMeteo} from './openmeteo.mjs'; import {apiReference} from '@scalar/express-api-reference'; import {spec} from './spec.mjs'; import {existsSync} from 'fs'; import {getAirTraffic, getAirTrafficHistory, getShapes} from './airtraffic.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)); } // ── Sensor Data ─────────────────────────────────────────────────────────────── app.get('/api/current', async (req, res) => { const {fields} = req.query; const [sensors, space] = await Promise.all([queryCurrent(), getCelestialCurrent()]); res.json(filterFields({...sensors, ...space}, 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)); }); // ── Daily History/Forecast ──────────────────────────────────────────────────── app.get('/api/hourly', async (req, res) => { const {fields} = req.query; const start = req.query.start || new Date(new Date().setHours(0, 0, 0, 0)).toISOString(); let now = new Date(); now.setHours(new Date().getHours() - 1, 0, 0); now = now.toISOString(); const end = req.query.end || new Date().toISOString(); const coords = await getCoords(); const [sensor, meteo] = await Promise.allSettled([ queryHourly(start, now), getOpenMeteo(coords.latitude, coords.longitude, now, end), ]); const history = sensor.status === 'fulfilled' ? sensor.value : []; const forecast = meteo.status === 'fulfilled' ? meteo.value.hourly : []; const hourly = getCelestialForecast(coords.latitude, coords.longitude, [...history, ...forecast]); res.json(filterArr(hourly, fields)); }); // ── Hourly History/Forecast ─────────────────────────────────────────────────── app.get('/api/daily', async (req, res) => { const {fields} = req.query; const start = req.query.start || new Date(new Date().setHours(0, 0, 0, 0)).toISOString(); let now = new Date(); now.setDate(now.getDate() - 1); now.setHours(0, 0, 0, 0); now = now.toISOString(); const end = req.query.end || new Date().toISOString(); const coords = await getCoords(); const [sensor, meteo] = await Promise.allSettled([ queryDaily(start, now), getOpenMeteo(coords.latitude, coords.longitude, now, end), ]); const history = sensor.status === 'fulfilled' ? sensor.value : []; const forecast = meteo.status === 'fulfilled' ? meteo.value.daily : []; const daily = getCelestialForecast(coords.latitude, coords.longitude, [...history, ...forecast]); res.json(filterArr(daily, 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(); app.listen(c.PORT, () => console.log(`🌦 Weather API — http://localhost:${c.PORT}`));