Files
weather-station/server/server.mjs
2026-06-22 00:03:51 -04:00

152 lines
6.1 KiB
JavaScript

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'
import {getAirTraffic, getAirTrafficHistory, getAirTrafficShapes, 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))
}
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))
})
// -- ADSB ----------------------------------------------------------------------
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.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}`))