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="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
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user