Files
weather-station/server/server.mjs
2026-06-23 22:17:01 -04:00

147 lines
6.6 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, 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));
}
// ── Sensor Data ───────────────────────────────────────────────────────────────
app.get('/api/current', async (req, res) => {
const {fields} = req.query;
const data = await queryCurrent();
res.json(filterFields(data, 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, mode} = 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();
if(mode === 'hourly') {
const space = await getSpaceWeather();
Object.assign(space, getCelestialHourly(coords.lat, coords.lon, start, end));
res.json(filterFields(space, fields));
} else if(mode === 'daily') {
const space = await getSpaceWeather();
Object.assign(space, getCelestialDaily(coords.lat, coords.lon, start, end));
res.json(filterFields(space, fields));
} else {
const space = await getSpaceWeather();
Object.assign(space, getCelestialCurrent(coords.lat, coords.lon));
res.json(filterFields(space, 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();
const now = new Date().toISOString();
const end = req.query.end || new Date().toISOString();
const coords = await getCoords();
const [sensor, meteo] = await Promise.allSettled([
queryHourly(start, now),
getOpenMeteo(coords.lat, coords.lon, now, end),
]);
const history = sensor.status === 'fulfilled' ? sensor.value : [];
const forecast = meteo.status === 'fulfilled' ? meteo.value.hourly : [];
res.json(filterArr([...history, ...forecast], 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();
const now = new Date().toISOString();
const end = req.query.end || new Date().toISOString();
const coords = await getCoords();
const [sensor, meteo] = await Promise.allSettled([
queryDaily(start, now),
getOpenMeteo(coords.lat, coords.lon, now, end),
]);
const history = sensor.status === 'fulfilled' ? sensor.value : [];
const forecast = meteo.status === 'fulfilled' ? meteo.value.daily : [];
res.json(filterArr([...history, ...forecast], 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}`));