diff --git a/app/public/dashboard.html b/app/public/dashboard.html
index 2ffe0fd..73e8410 100644
--- a/app/public/dashboard.html
+++ b/app/public/dashboard.html
@@ -551,6 +551,7 @@ input, select, textarea { font-size: 16px; } /* stops iOS zooming in on focus */
+
@@ -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) {
+
@@ -1126,7 +1133,7 @@ async function loadHistory(range) {
const area = document.getElementById('chartsArea');
area.innerHTML = 'Loading…
';
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');
diff --git a/app/server.js b/app/server.js
index 35e4a6d..662578c 100644
--- a/app/server.js
+++ b/app/server.js
@@ -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) {