Add 'All' history range to the dashboard and pressure tendency chart
Publish Container / publish (push) Successful in 2m45s

- /api/station/history?range=all returns everything on disk, bucketed (cached 60s)
- Year-aware axis labels for 1y/2y/3y/All
- Pressure tendency widens its change window to the bucket size on long ranges and says so

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Nate EmmertandClaude Sonnet 5.5 committed 2026-10-15 13:17:33 -04:00
1 parent fc390b0bb1
commit 00336c6ce1
2 files changed
+40 -9

No files matched your search

+15 -8
View File
@@ -551,6 +551,7 @@ input, select, textarea { font-size: 16px; } /* stops iOS zooming in on focus */
<button class="range-btn" data-range="1y">1y</button>
<button class="range-btn" data-range="2y">2y</button>
<button class="range-btn" data-range="3y">3y</button>
<button class="range-btn" data-range="all" title="Everything recorded so far">All</button>
<button class="refresh-btn" id="refreshBtn">↻ Refresh</button>
<button class="range-btn" id="yoyBtn" style="display:none" title="Overlay same period from 1 year ago">YoY</button>
</div>
@@ -612,7 +613,10 @@ let charts = {};
function fmtTime(ts, range) {
const d = new Date(ts);
if (range === '2d') return d.toLocaleDateString('en-US', { weekday: 'short' }) + ' ' + d.toLocaleTimeString('en-US', { hour: 'numeric', hour12: true });
if (['7d','30d','90d','1y','2y','3y'].includes(range)) {
if (['1y','2y','3y','all'].includes(range)) {
return d.toLocaleDateString('en-US', { month: 'short', year: '2-digit' });
}
if (['7d','30d','90d'].includes(range)) {
return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
}
return d.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false });
@@ -796,7 +800,7 @@ async function drawPresTend(pagePts, pageRange) {
if (hit && Date.now() - hit.at < 120000) pts = hit.data;
else {
try {
const bucket = ['7d', '30d'].includes(range) ? '&points=720' : '';
const bucket = ['7d', '30d', 'all'].includes(range) ? '&points=720' : '';
const r = await fetch(`/api/station/history?range=${range}${bucket}`);
pts = r.ok ? await r.json() : [];
} catch { pts = []; }
@@ -805,7 +809,10 @@ async function drawPresTend(pagePts, pageRange) {
if (document.getElementById('cPresTend') !== canvas) return; // charts were re-rendered meanwhile
}
if (pts.length > 900) { const step = Math.ceil(pts.length / 720); pts = pts.filter((_, i) => i % step === 0); }
const winMs = presTendWin * 3600000;
// Bucketed (long) ranges can't resolve changes shorter than one bucket: widen the window to fit
const bucketMs = pts.length > 1 ? (pts[pts.length - 1].dateutc - pts[0].dateutc) / (pts.length - 1) : 0;
const effHours = Math.max(presTendWin, Math.ceil(bucketMs / 3600000));
const winMs = effHours * 3600000;
const c = chartColors();
const tend = pts.map(r => {
const t = r.dateutc - winMs;
@@ -815,12 +822,12 @@ async function drawPresTend(pagePts, pageRange) {
const base = pts[lo].baromrelin;
return r.baromrelin != null && base != null ? +(r.baromrelin - base).toFixed(3) : null;
});
const ds = barDataset(`${presTendWin}h change`, tend, c.cool);
const ds = barDataset(`${effHours}h change`, tend, c.cool);
ds.backgroundColor = tend.map(v => v == null ? 'transparent' : (v >= 0 ? c.cool + 'bb' : c.accent + 'bb'));
ds.borderWidth = 0;
makeChart('cPresTend', 'bar', [ds], pts.map(r => fmtTime(r.dateutc, range)), `inHg / ${presTendWin}h`);
makeChart('cPresTend', 'bar', [ds], pts.map(r => fmtTime(r.dateutc, range)), `inHg / ${effHours}h`);
const title = document.getElementById('ptTitle');
if (title) title.textContent = `Pressure Tendency · ${presTendWin}-hour change (inHg)`;
if (title) title.textContent = `Pressure Tendency · ${effHours}-hour change (inHg)${effHours !== presTendWin ? ' — widened to fit this range' : ''}`;
if (!tend.some(v => v != null) && title) title.textContent += ' — not enough data for this window';
}
@@ -898,7 +905,7 @@ function renderCharts(readings, range, yoy = null) {
<span class="chart-ctl">
<label>Range <select id="ptRange" aria-label="Pressure tendency range">
<option value="follow">Same as page</option><option value="6h">6 hours</option><option value="24h">24 hours</option>
<option value="2d">2 days</option><option value="7d">7 days</option><option value="30d">30 days</option></select></label>
<option value="2d">2 days</option><option value="7d">7 days</option><option value="30d">30 days</option><option value="all">All time</option></select></label>
<label>Change over <select id="ptWin" aria-label="Pressure change window">
<option value="1">1 hour</option><option value="3">3 hours</option><option value="6">6 hours</option>
<option value="12">12 hours</option><option value="24">24 hours</option></select></label>
@@ -1126,7 +1133,7 @@ async function loadHistory(range) {
const area = document.getElementById('chartsArea');
area.innerHTML = '<div class="status" id="loadingMsg">Loading…</div>';
try {
const pts = ['7d','30d','90d','1y','2y','3y'].includes(range) ? '&points=720' : '';
const pts = ['7d','30d','90d','1y','2y','3y','all'].includes(range) ? '&points=720' : '';
const r = await fetch(`/api/station/history?range=${range}${pts}`);
if (r.status === 401) { location.href = '/dashboard/login'; return; }
if (!r.ok) throw new Error('Failed');
+25 -1
View File
@@ -1475,13 +1475,37 @@ function bucketReadings(readings, points) {
app.get('/api/station/history', requireDashboardAuth, (req, res) => {
const rangeMap = { '1h': 3600000, '6h': 21600000, '24h': 86400000, '2d': 172800000, '7d': 604800000, '30d': 2592000000, '90d': 7776000000, '1y': 31536000000, '2y': 63072000000, '3y': 94608000000 };
const range = req.query.range || '24h';
const points = parseInt(req.query.points);
if (range === 'all') {
// Everything on disk, always bucketed (default 720 points) so the payload stays small
const first = firstStationDay();
if (!first) return res.json([]);
const rangeAll = Date.now() - Date.parse(first + 'T00:00:00Z') + 2 * 86400000;
const pts = points >= 50 ? points : 720;
const key = `${first}|${pts}`;
if (allHistoryCache.key === key && Date.now() - allHistoryCache.at < 60000) return res.json(allHistoryCache.data);
const readingsAll = loadStationReadings(rangeAll);
const data = readingsAll.length > pts ? bucketReadings(readingsAll, pts) : readingsAll;
allHistoryCache = { key, at: Date.now(), data };
return res.json(data);
}
const rangeMs = rangeMap[range] || rangeMap['24h'];
const readings = loadStationReadings(rangeMs);
const points = parseInt(req.query.points);
if (points >= 50 && readings.length > points) return res.json(bucketReadings(readings, points));
res.json(readings);
});
// Earliest day with saved readings (YYYY-MM-DD, UTC file name)
let allHistoryCache = { key: null, at: 0, data: null };
function firstStationDay() {
try {
const files = fs.readdirSync(STATION_DIR).filter(f => /^\d{4}-\d{2}-\d{2}\.jsonl$/.test(f)).sort();
return files.length ? files[0].slice(0, 10) : null;
} catch { return null; }
}
// Latest station reading
app.get('/api/station/latest', requireDashboardAuth, async (req, res) => {
if (!ambientServerConfig.apiKey || !ambientServerConfig.appKey) {