mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-11-11 13:58:07 +00:00
Fixes #2193. ## Problem On an instance that keeps months of packets, the Overview tab's Packets / Hour chart becomes unreadable: 2,932 hourly bars in 720 units of plot, a date label every day, and the hours with no packets (which the server leaves out) squeezed off the axis. ## Before and after Both use the real `/api/analytics/rf` answer from the instance in the issue (2,933 hourly buckets over 124 days), served to the page in place of the fixture's own. **Before** (master, page on "All data"):  **After**, same data:  Phone: <img src="https://raw.githubusercontent.com/A13xB0/CoreScope/7862abd8/2026-10/pph-2193/after-phone.png" width="300"> **A short store** (an instance holding 7 days in memory, plus a few old packets a day for weeks before that because they were heard again). The chart starts where the real data does (this shot predates the "since MM-DD" note now in the title when that crop applies):  ## Change All in `public/analytics.js`; no API change. - **`packetsPerHourCard()`** replaces the inline block: - rolls the hours up to hourly, 6-hourly, daily or weekly bars, at most 180, and titles the card to match (Packets / Day); - counts missing hours as zero; - puts about 10 labels on round UTC times; - **under "All data" only**, starts on the first UTC day with at least 2% of a typical recent day (the median of the seven days before the last one), at its first hour with a tenth of a typical hour, and says "since MM-DD" in the title when that cuts anything. Scanning forward, an outage after the start is never taken for it. In a picked window (1h to 30d) the server has already cut the data, so nothing is cropped and a quieter early stretch stays visible. - **Theme refresh before the data** (separate commit; this also happens on master without the new chart): every load fires `theme-refresh` about 300 ms after the theme resolves, and the page re-rendered the current tab on it. When the analytics answers came later (a busy or just-restarted server), the Overview was drawn from an empty object and threw `Cannot read properties of undefined (reading 'totalTransmissions')`. It now skips that refresh until the data has landed; `loadAnalytics()` renders the tab with the current theme when it does. The span is still picked by the page's time window. ## Tests - `tests/unit/test-issue-2193-packets-per-hour.js` (in `test-all.sh`, red then green): 122 days gives 123 daily bars with 4 to 12 labels; 7 days gives 168 hourly bars; missing hours are drawn as zero; the chart starts at the data, not at the stragglers, and not after an outage; in a picked window nothing is cropped (90 days with 30 quiet ones gives 91 bars); a cropped chart says "since MM-DD" and an uncropped one does not; no data says so. - `tests/unit/test-analytics-theme-refresh-before-data.js` (in `test-all.sh`): drives `init()` with answers that never land, fires the `theme-refresh` handler, and checks that nothing throws and the tab is not re-rendered. Without the guard it fails with the production error. - `sh test-all.sh` passes; the #1925 neighbour-graph theme-refresh test and the analytics chart test still pass; the screenshots show no page errors and no sideways scroll at 1280 px or 390 px. ## Later - The 2% floor, the tenth-of-an-hour start and the 180-bar cap are hardcoded. Per AGENTS.md rule 8 they can move to the customizer in a later step. - Edge bars are partial periods (data starting at 17h gives a 7-hour first day; the last bar is still filling), and weekly bars align on epoch weeks (Thursday). Both are left as they are here. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01KaR4xu8gVykQBkH8yG8m6H --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
77 lines
3.0 KiB
JavaScript
77 lines
3.0 KiB
JavaScript
/**
|
|
* Analytics: a theme-refresh that fires before the analytics answers land.
|
|
*
|
|
* Every page load fires a theme-refresh about 300ms after the theme resolves.
|
|
* On a slow server the answers land later, and re-rendering the Overview from
|
|
* the empty data object threw (reading 'totalTransmissions'). The handler
|
|
* must leave the tab alone until loadAnalytics() renders it.
|
|
*
|
|
* Usage: node tests/unit/test-analytics-theme-refresh-before-data.js
|
|
*/
|
|
'use strict';
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const vm = require('vm');
|
|
|
|
let passed = 0, failed = 0;
|
|
function assert(cond, msg) {
|
|
if (cond) { passed++; console.log(' ✓ ' + msg); }
|
|
else { failed++; console.error(' ✗ ' + msg); }
|
|
}
|
|
|
|
function el() {
|
|
return {
|
|
style: {}, dataset: {}, value: '', innerHTML: '',
|
|
classList: { add() {}, remove() {}, toggle() {}, contains: () => false },
|
|
addEventListener() {}, removeEventListener() {},
|
|
querySelector: () => null, querySelectorAll: () => [],
|
|
};
|
|
}
|
|
const els = {};
|
|
let contentLookups = 0;
|
|
const listeners = {};
|
|
const ctx = {
|
|
console, Math, JSON, Date, Number, String, Array, Object, Set, Map, RegExp, URLSearchParams, Promise,
|
|
setTimeout, clearTimeout, requestAnimationFrame: () => 0,
|
|
getComputedStyle: () => ({ getPropertyValue: () => '' }),
|
|
localStorage: { getItem: () => null, setItem() {}, removeItem() {} },
|
|
location: { hash: '#/analytics' },
|
|
addEventListener: (type, fn) => { listeners[type] = fn; },
|
|
removeEventListener() {},
|
|
initTabBar() {}, CLIENT_TTL: {},
|
|
// The answers never land: the refresh below fires while they are pending.
|
|
api: () => new Promise(() => {}),
|
|
RegionFilter: { init() {}, onChange() {}, regionQueryString: () => '' },
|
|
AreaFilter: { init() {}, onChange() {}, areaQueryString: () => '' },
|
|
};
|
|
let page = null;
|
|
ctx.registerPage = (name, p) => { if (name === 'analytics') page = p; };
|
|
ctx.window = ctx;
|
|
ctx.document = {
|
|
documentElement: {}, createElement: el,
|
|
addEventListener() {}, removeEventListener() {}, querySelector: () => null, querySelectorAll: () => [],
|
|
getElementById: (id) => {
|
|
if (id === 'analyticsContent') contentLookups++;
|
|
return els[id] || (els[id] = el());
|
|
},
|
|
};
|
|
vm.createContext(ctx);
|
|
vm.runInContext(fs.readFileSync(path.join(__dirname, '..', '..', 'public/analytics.js'), 'utf8'), ctx);
|
|
|
|
const rejections = [];
|
|
process.on('unhandledRejection', (e) => rejections.push(e));
|
|
|
|
(async () => {
|
|
await page.init(el());
|
|
const refresh = listeners['theme-refresh'];
|
|
assert(typeof refresh === 'function', 'init registers a theme-refresh handler');
|
|
const before = contentLookups;
|
|
let threw = null;
|
|
try { refresh(); } catch (e) { threw = e; }
|
|
await new Promise((r) => setTimeout(r, 20));
|
|
assert(!threw && rejections.length === 0, 'a refresh before the data lands does not throw (' + (threw || rejections[0] || 'ok') + ')');
|
|
assert(contentLookups === before, 'and does not re-render the tab');
|
|
console.log(`\n${passed} passed, ${failed} failed`);
|
|
process.exit(failed > 0 ? 1 : 0);
|
|
})();
|