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 } 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' 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)) } function mergeRows(arrays) { const map = {} for (const arr of arrays) { for (const row of arr) { map[row.time] = { ...map[row.time], ...row } } } return Object.values(map).sort((a, b) => a.time.localeCompare(b.time)) } // ── GET /api/data ───────────────────────────────────────────────────────────── app.get('/api/data', async (req, res) => { const { fields } = req.query const coords = await getCoords() const [sensor, space, meteo] = await Promise.allSettled([ queryCurrent(), getSpaceWeather(), getOpenMeteo(coords.lat, coords.lon), ]) const celestial = getCelestialCurrent(coords.lat, coords.lon) const data = { gps_lat: coords.lat, gps_lon: coords.lon, gps_alt: coords.alt, ...(sensor.status === 'fulfilled' ? sensor.value : {}), ...celestial, ...(space.status === 'fulfilled' ? space.value : {}), ...(meteo.status === 'fulfilled' && meteo.value.current ? meteo.value.current : {}), } res.json(filterFields(data, fields)) }) // ── GET /api/hourly ─────────────────────────────────────────────────────────── 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() const end = req.query.end || new Date().toISOString() const coords = await getCoords() const [sensor, meteo, space] = await Promise.allSettled([ queryHourly(start, end), getOpenMeteo(coords.lat, coords.lon, start, end), getSpaceWeather(), ]) const sensorRows = sensor.status === 'fulfilled' ? sensor.value : [] const meteoHourly = meteo.status === 'fulfilled' ? meteo.value.hourly : [] const spaceData = space.status === 'fulfilled' ? space.value : {} const seedRows = sensorRows.length ? sensorRows : meteoHourly const celestial = getCelestialHourly(coords.lat, coords.lon, seedRows) // Spread space weather into every hourly row const result = mergeRows([sensorRows, meteoHourly, celestial]) .map(row => ({ gps_lat: coords.lat, gps_lon: coords.lon, gps_alt: coords.alt, ...spaceData, ...row })) res.json(filterArr(result, fields)) }) // ── GET /api/daily ------------------------------------------------------------ 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() const end = req.query.end || new Date().toISOString() const coords = await getCoords() const [sensor, meteo, space] = await Promise.allSettled([ queryDaily(start, end), getOpenMeteo(coords.lat, coords.lon, start, end), getSpaceWeather(), ]) const sensorRows = sensor.status === 'fulfilled' ? sensor.value : [] const meteoDaily = meteo.status === 'fulfilled' ? meteo.value.daily : [] const spaceData = space.status === 'fulfilled' ? space.value : {} const seedRows = sensorRows.length ? sensorRows : meteoDaily const celestial = getCelestialDaily(coords.lat, coords.lon, seedRows) const result = mergeRows([sensorRows, meteoDaily, celestial]) .map(row => ({ location: coords,gps_lat: coords.lat, gps_lon: coords.lon, gps_alt: coords.alt, ...spaceData, ...row })) res.json(filterArr(result, fields)) }) // ── DOCS ---------------------------------------------------------------------- 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)) }) // Scalar UI app.use('/docs', apiReference({ spec: { url: '/openapi.json' }, theme: 'default' })) // ── SPA Redirect -------------------------------------------------------------- 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}`))