Skip to content

fix(analytics): Packets / Hour readable on long histories (#2193) - #2194

Open
A13xB0 wants to merge 6 commits into
Kpa-clawbot:masterfrom
A13xB0:fix/packets-per-hour-long-span
Open

A13xB0 wants to merge 6 commits into
Kpa-clawbot:masterfrom
A13xB0:fix/packets-per-hour-long-span

Conversation

@A13xB0

@A13xB0 A13xB0 commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

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"):

before

After, same data:

after

Phone:

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):

short store

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 Neighbor-graph filter E2E fails under CI load: skip-message transition is undone by a later re-render (#1758 lifecycle) #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.ai/code/session_01KaR4xu8gVykQBkH8yG8m6H

A13xB0 and others added 3 commits October 12, 2026 21:02
…-clawbot#2193)

Months of hours should roll up to daily bars with a handful of labels,
missing hours count as zero, and the chart should start where the data
does rather than at the few old packets a short store keeps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KaR4xu8gVykQBkH8yG8m6H
…t#2193)

A long history now rolls up to coarser bars (hourly, 6-hourly, daily or
weekly; at most 180) with about 10 labels on round UTC times, missing hours
count as zero, and the title follows the bucket (Packets / Day). The chart
starts where the data does: a short store also keeps a few old packets
that were heard again, weeks before the rest.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KaR4xu8gVykQBkH8yG8m6H
Every load fires a theme-refresh about 300ms after the theme resolves, and
the page re-rendered the current tab on it. On a slow or just-restarted
server the analytics answers land later, so the Overview was drawn from
the empty data object and threw (reading 'totalTransmissions').
loadAnalytics() renders the tab, with the current theme, once the answers
land, so the early refresh has nothing to do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KaR4xu8gVykQBkH8yG8m6H

@efiten efiten left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rollup solves #2193. Two things before merge.

What I checked

  • Bar counts: 2 years of hourly buckets give 105 weekly bars with 7 labels, 122 days give 123 daily bars, 7 days give 168 hourly bars. 24 h, 48 h and 72 h keep hourly bars with 6 to 8 labels.
  • Time handling: missing hours count as zero, and the bucket math is in UTC epoch ms, so DST does not affect it. The hour keys match what the server sends (tx.FirstSeen[:13], cmd/server/store.go:6572, field packetsPerHour), with either separator.
  • Edge cases that work: a single bucket, all-zero counts, fewer than 7 days of data, and an outage at the end.
  • Cost: about 10 ms per call on 2 years of hourly data, and at most 180 bars.
  • Merge: git merge-tree against current master is clean, including #2208, which also touched analytics.js.
  • Tests:
    • The new test passes 7/7 and drives the real analytics.js.
    • test-all.sh passes, apart from the XSS-gate fixtures that need Python, which is not installed on my machine.

Requested changes

  1. The start crop also hides data inside a window the user picked (public/analytics.js:86-88). The floor is relative to the recent days, so a genuinely quieter early period is cut, and nothing on the card says so. Measured with synthetic data:

    • 90 days, the first 30 at 1 packet/hour and then 100/hour: the chart starts 30 days late (61 bars instead of 91).
    • 60 days, 20 quiet ones first: 20 days hidden.
    • A steady ramp from 1 to 200 per hour over 120 days: about 11 days hidden.

    The server already filters by window on tx.FirstSeen (store.go:6640), so with 7d or 30d selected there are no stragglers to remove. The crop only has something to fix under "All data". Suggest:

    • apply it only when no window is set;
    • show it on the card when it applies, for example "since MM-DD", so a cut is never invisible.
  2. The theme-refresh guard has no test (public/analytics.js:293-295). AGENTS.md asks for a regression test with every bug fix, and a vm test is small here: capture the theme-refresh handler, fire it with _analyticsData = {}, and assert that nothing throws and renderTab is not called.

    The guard also skips theme refreshes on tabs that fetch their own data while the main load is pending. That is fine, because loadAnalytics() re-renders when the data lands. One gap: if loadAnalytics() fails (the catch at :303), theme changes no longer re-render any tab until a reload. The page shows "Failed to load" in that state, so the impact is small.

Notes, not blocking

  1. The edge bars are partial periods (:91). Data that starts at 17h gives a first daily bar of 7 hours, and the last bar is the current, unfinished period. Both read as dips in traffic.
  2. Weekly bars and 168 h label steps align on epoch weeks, which start on Thursday. Correct, but Monday would read more naturally.
  3. The comment at :84 says "the last seven whole days", but slice(-8, -1) takes the seven days with data before the last day with data. The behaviour is fine; the comment is not accurate.
  4. The 2%, 1/10 and 180-bar thresholds are hardcoded. AGENTS.md rule 8 asks to note a later customizer step for those.
  5. The theme-refresh crash also happens on master without this chart. It is a separate commit (98789131), which is good; mentioning it in the PR description would help whoever searches for that error later.

A13xB0 and others added 3 commits October 15, 2026 10:19
… alone

Regression test for 9878913. Drives init() with answers that never land,
fires the captured theme-refresh handler, and checks that nothing throws
and the tab is not re-rendered. Without the guard it fails with the
production error (reading 'totalTransmissions').

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KaR4xu8gVykQBkH8yG8m6H
…d says so

Review on Kpa-clawbot#2194: the start crop also cut a genuinely quieter early stretch
inside a window the user picked (90 days, the first 30 quiet: 30 days
hidden), and nothing on the card said so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KaR4xu8gVykQBkH8yG8m6H
In a picked window the server has already cut the data to it, so a quieter
early stretch is real: the chart now starts at the window's first hour.
Under "All data" the straggler crop stays, and the card title says
"since MM-DD" when it applies. The renderer gets the window the data was
fetched with from _analyticsData. Also corrects the comment on which seven
days set the floor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KaR4xu8gVykQBkH8yG8m6H
@A13xB0

A13xB0 commented Oct 15, 2026

Copy link
Copy Markdown
Contributor Author

Review feedback addressed (commits 3fa23c3, 35df4dd, 3dd90ed):

  1. Crop only under "All data", and visible when it applies. packetsPerHourCard(pph, { window }) crops only when no window is set. The renderer passes the window the data was fetched with, which is now kept on _analyticsData. When the crop cuts anything, the title says "since MM-DD". The red commit (35df4dd) adds your first case, 90 days with the first 30 at 1/hour: in a 30d window it now draws 91 bars, where it drew 61 before. The test also checks that a cropped "All data" chart shows "since 10-04" and an uncropped one shows nothing.
  2. Theme-refresh guard test. tests/unit/test-analytics-theme-refresh-before-data.js runs init() in a vm with api() never resolving, captures the theme-refresh handler, and fires it. It asserts that nothing throws (sync or async) and that analyticsContent is not touched, which is the first thing renderTab does. With the guard removed it fails with reading 'totalTransmissions'. The failed-load gap you describe is unchanged: the page shows "Failed to load" there.

Notes:

  • Comment at :84 now reads "the seven days before the last one".
  • Thresholds (2%, a tenth of an hour, 180 bars): listed in the description as a later customizer step (rule 8).
  • Partial edge bars and Thursday-aligned weeks: noted in the description and left as they are in this PR.
  • Theme crash: the description now says it also happens on master and that it has its own commit.

sh test-all.sh passes.

@A13xB0
A13xB0 requested a review from efiten October 15, 2026 14:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Analytics: Packets / Hour chart unreadable on long-retention instances (labels overlap, bars stack)

2 participants