Add 'All' history range to the dashboard and pressure tendency chart
Publish Container / publish (push) Successful in 2m45s
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:
1 parent
fc390b0bb1
commit
00336c6ce1
2 files changed
+40
-9
No files matched your search
@@ -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="1y">1y</button>
|
||||||
<button class="range-btn" data-range="2y">2y</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="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="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>
|
<button class="range-btn" id="yoyBtn" style="display:none" title="Overlay same period from 1 year ago">YoY</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -612,7 +613,10 @@ let charts = {};
|
|||||||
function fmtTime(ts, range) {
|
function fmtTime(ts, range) {
|
||||||
const d = new Date(ts);
|
const d = new Date(ts);
|
||||||
if (range === '2d') return d.toLocaleDateString('en-US', { weekday: 'short' }) + ' ' + d.toLocaleTimeString('en-US', { hour: 'numeric', hour12: true });
|
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.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
||||||
}
|
}
|
||||||
return d.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false });
|
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;
|
if (hit && Date.now() - hit.at < 120000) pts = hit.data;
|
||||||
else {
|
else {
|
||||||
try {
|
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}`);
|
const r = await fetch(`/api/station/history?range=${range}${bucket}`);
|
||||||
pts = r.ok ? await r.json() : [];
|
pts = r.ok ? await r.json() : [];
|
||||||
} catch { pts = []; }
|
} catch { pts = []; }
|
||||||
@@ -805,7 +809,10 @@ async function drawPresTend(pagePts, pageRange) {
|
|||||||
if (document.getElementById('cPresTend') !== canvas) return; // charts were re-rendered meanwhile
|
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); }
|
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 c = chartColors();
|
||||||
const tend = pts.map(r => {
|
const tend = pts.map(r => {
|
||||||
const t = r.dateutc - winMs;
|
const t = r.dateutc - winMs;
|
||||||
@@ -815,12 +822,12 @@ async function drawPresTend(pagePts, pageRange) {
|
|||||||
const base = pts[lo].baromrelin;
|
const base = pts[lo].baromrelin;
|
||||||
return r.baromrelin != null && base != null ? +(r.baromrelin - base).toFixed(3) : null;
|
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.backgroundColor = tend.map(v => v == null ? 'transparent' : (v >= 0 ? c.cool + 'bb' : c.accent + 'bb'));
|
||||||
ds.borderWidth = 0;
|
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');
|
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';
|
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">
|
<span class="chart-ctl">
|
||||||
<label>Range <select id="ptRange" aria-label="Pressure tendency range">
|
<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="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">
|
<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="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>
|
<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');
|
const area = document.getElementById('chartsArea');
|
||||||
area.innerHTML = '<div class="status" id="loadingMsg">Loading…</div>';
|
area.innerHTML = '<div class="status" id="loadingMsg">Loading…</div>';
|
||||||
try {
|
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}`);
|
const r = await fetch(`/api/station/history?range=${range}${pts}`);
|
||||||
if (r.status === 401) { location.href = '/dashboard/login'; return; }
|
if (r.status === 401) { location.href = '/dashboard/login'; return; }
|
||||||
if (!r.ok) throw new Error('Failed');
|
if (!r.ok) throw new Error('Failed');
|
||||||
|
|||||||
+25
-1
@@ -1475,13 +1475,37 @@ function bucketReadings(readings, points) {
|
|||||||
app.get('/api/station/history', requireDashboardAuth, (req, res) => {
|
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 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 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 rangeMs = rangeMap[range] || rangeMap['24h'];
|
||||||
const readings = loadStationReadings(rangeMs);
|
const readings = loadStationReadings(rangeMs);
|
||||||
const points = parseInt(req.query.points);
|
|
||||||
if (points >= 50 && readings.length > points) return res.json(bucketReadings(readings, points));
|
if (points >= 50 && readings.length > points) return res.json(bucketReadings(readings, points));
|
||||||
res.json(readings);
|
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
|
// Latest station reading
|
||||||
app.get('/api/station/latest', requireDashboardAuth, async (req, res) => {
|
app.get('/api/station/latest', requireDashboardAuth, async (req, res) => {
|
||||||
if (!ambientServerConfig.apiKey || !ambientServerConfig.appKey) {
|
if (!ambientServerConfig.apiKey || !ambientServerConfig.appKey) {
|
||||||
|
|||||||
Reference in new issue
Block a user