AIS fixes + daily

This commit is contained in:
2026-06-27 13:38:52 -04:00
parent bea55ac448
commit c5477d05ad
13 changed files with 251 additions and 259 deletions

View File

@@ -1,7 +1,7 @@
import express from 'express';
import {resolve, dirname} from 'path';
import {fileURLToPath} from 'url';
import {cfg, localDateStr} from './config.mjs';
import {cfg} from './config.mjs';
import {queryCurrent, queryHourly, queryDaily, getCoords} from './sensors.mjs';
import {getCelestialCurrent, getCelestialForecast} from './celestial.mjs';
import {apiReference} from '@scalar/express-api-reference';
@@ -10,11 +10,13 @@ import {existsSync} from 'fs';
import {getAIS} from './ais.mjs';
import {getADSB, getADSBHistory, getADSBRange, initAircraftDb} from './adsb.mjs';
import {fetchIcon} from './openweather.mjs';
import {lastForecast, getForecast, getWeatherCondition, forecastTTL} from './forecast.mjs';
import {getForecast, getWeatherCondition, forecastTTL} from './forecast.mjs';
import {dailyWeather, hourlyWeather} from './openmeteo.mjs';
// ── Uncaught error handlers ───────────────────────────────────────────────────
const asyncHandler = fn => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);
process.on('unhandledRejection', (reason, promise) => {
console.error('[unhandledRejection]', promise, '\nReason:', reason)
})
@@ -47,7 +49,7 @@ function filterArr(arr, fields) {
// ── Current ───────────────────────────────────────────────────────────────────
app.get('/api/current', async (req, res) => {
app.get('/api/current', asyncHandler(async (req, res) => {
const { fields } = req.query
const [sensors, coords] = await Promise.all([queryCurrent(), getCoords()])
const space = getCelestialCurrent(coords.latitude, coords.longitude)
@@ -67,21 +69,19 @@ app.get('/api/current', async (req, res) => {
}
res.json(filterFields(merged, fields))
})
}));
// ── Hourly ────────────────────────────────────────────────────────────────────
app.get('/api/hourly', async (req, res) => {
app.get('/api/hourly', asyncHandler(async (req, res) => {
const { fields } = req.query
const coords = await getCoords()
const now = new Date()
now.setMinutes(0, 0, 0)
const forecastLimit = new Date(now.getTime() + 24 * 60 * 60_000)
const start = req.query.start ? new Date(req.query.start) : now
const end = req.query.end ? new Date(req.query.end) : forecastLimit
const coords = await getCoords()
const [history, forecast24, forecast3rdParty] = await Promise.all([
start.getTime() < now.getTime() ? queryHourly(start, now) : Promise.resolve([]),
end.getTime() > now.getTime() ? getForecast().then(f => f.slots.filter(h => new Date(h.time) <= end)) : Promise.resolve([]),
@@ -90,55 +90,46 @@ app.get('/api/hourly', async (req, res) => {
const hourly = getCelestialForecast(coords.latitude, coords.longitude, [...history, ...forecast24, ...forecast3rdParty])
res.json(filterArr(hourly, fields))
})
}));
// ── Daily ─────────────────────────────────────────────────────────────────────
app.get('/api/daily', async (req, res) => {
app.get('/api/daily', asyncHandler(async (req, res) => {
const { fields } = req.query
const coords = await getCoords();
const now = new Date()
const todayStart = new Date(now)
todayStart.setHours(0, 0, 0, 0)
const next = new Date(todayStart)
next.setDate(todayStart.getDate() + 1)
const todayEnd = new Date(todayStart)
todayEnd.setDate(todayStart.getDate() + 1)
const start = req.query.start ? new Date(req.query.start) : todayEnd
const end = req.query.end ? new Date(req.query.end) : new Date(todayEnd.getTime() + 5 * 24 * 60 * 60_000)
const start = req.query.start ? new Date(req.query.start) : todayStart
const end = req.query.end ? new Date(req.query.end) : next
const coords = await getCoords()
const todayStr = localDateStr(now)
const usePhysics = lastForecast.summary && start <= now && end > now
const [sensorResult] = await Promise.allSettled([start < now ? queryDaily(start, now) : Promise.resolve([])])
const history = sensorResult.status === 'fulfilled' ? sensorResult.value : []
const meteoFrom = new Date(Math.max(next.getTime(), start.getTime()))
const [meteoResult] = await Promise.allSettled([end > next ? dailyWeather(coords.latitude, coords.longitude, meteoFrom, end) : Promise.resolve([])])
const meteo = meteoResult.status === 'fulfilled' ? meteoResult.value : []
const todaySlot = usePhysics ? [lastForecast.summary] : []
const meteoClean = meteo.filter(r => localDateStr(new Date(r.time)) !== todayStr)
const daily = getCelestialForecast(coords.latitude, coords.longitude, [...history, ...todaySlot, ...meteoClean])
const [history, current, future] = Promise.all([
start.getTime() < todayStart.getTime() ? queryDaily(start, todayStart) : Promise.resolve([]), // Historic
start.getTime() <= todayStart.getTime() ? queryCurrent().then(resp => [resp]) : Promise.resolve([]), // Current
start.getTime() >= todayEnd.getTime() ? dailyWeather(coords.latitude, coords.longitude, todayStart, end) : Promise.resolve([]), // Future
]);
const daily = [...history, ...current, ...getCelestialForecast(coords.latitude, coords.longitude, future)];
res.json(filterArr(daily, fields))
})
}));
// ── Position ──────────────────────────────────────────────────────────────────
app.get('/api/position', async (req, res) => {
app.get('/api/position', asyncHandler(async (req, res) => {
const {fields} = req.query;
const data = await getCoords();
res.json(filterFields(data, fields));
});
}));
// ── Icon ──────────────────────────────────────────────────────────────────────
app.get('/api/icon/:icon', async (req, resp, next) => {
app.get('/api/icon/:icon', asyncHandler(async (req, res) => {
const { icon } = req.params;
if(!icon) return next(); // 404 fallback can handle missing icons
resp.contentType('image/png');
resp.sendFile(await fetchIcon(icon));
});
}));
// ── Space ─────────────────────────────────────────────────────────────────────
@@ -150,20 +141,20 @@ app.get('/api/icon/:icon', async (req, resp, next) => {
// ── ADSB Proxy ────────────────────────────────────────────────────────────────
app.get('/api/adsb', async (req, res) => res.json(await getADSB()));
app.get('/api/adsb/:icao', async (req, res) => res.json(await getADSBHistory(req.params.icao)));
app.get('/api/ais', async (req, res) => res.json(await getAIS()));
app.get('/api/range', async (req, res) => res.json(await getADSBRange()));
app.get('/api/adsb', asyncHandler(async (req, res) => res.json(await getADSB())));
app.get('/api/adsb/:icao', asyncHandler(async (req, res) => res.json(await getADSBHistory(req.params.icao))));
app.get('/api/ais', asyncHandler(async (req, res) => res.json(await getAIS())));
app.get('/api/range', asyncHandler(async (req, res) => res.json(await getADSBRange())));
// ── 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) => {
app.get('/openapi.json',asyncHandler(async (req, res) => res.json(spec)));
app.get('/openapi.yaml',asyncHandler(async (req, res) => {
const {stringify} = await import('yaml');
res.setHeader('Content-Type', 'text/yaml');
res.send(stringify(spec));
});
}));
// ── Website ───────────────────────────────────────────────────────────────────