Skip to content

My Mesh: accurate 24h node activity and compact repeater observer cards - #351

Merged
dborup merged 9 commits into
masterfrom
codex/my-mesh-activity-304
Oct 8, 2026
Merged

dborup merged 9 commits into
masterfrom
codex/my-mesh-activity-304

Conversation

@dborup

@dborup dborup commented Oct 7, 2026 •

Copy link
Copy Markdown
Owner

Summary

First, deliberately limited slice of #304 for the My Mesh repeater cards:

  • Replace the 20-recent-packet pseudo-activity chart with 24 rolling hourly buckets of unique node-associated transmissions from the existing in-memory node index.
  • Expose coverage metadata and show an explicit unavailable state when a full 24-hour history cannot be established. A node association is not proof of repeater forwarding.
  • Limit the repeater-card observer preview to three names, with an accessible View all dialog. The full observer list remains available without expanding card height.

The broader #304 status, SNR, and traffic-semantics work remains open. Observer/room role-card classification is separate (#262).

Validation

  • cmd/server: go test ./... — passed.
  • cmd/ingestor: go test ./... — passed (unchanged production code).
  • sh test-all.sh — 227/227 test files passed.
  • CHROMIUM_REQUIRE=1 node test-issue-304-my-mesh-e2e.js — desktop and mobile passed; checked 0/1/51 observers, long/malicious names, full/partial/missing activity, dialog keyboard/focus and touch.
  • CHROMIUM_REQUIRE=1 node test-issue-2027-my-mesh-node-page-e2e.js — 25/25 passed.
  • bash scripts/check-xss-sinks.sh --file public/home.js and git diff --check — passed.

Performance

The 24 counters are updated inside the existing GetNodeHealth loop over byNode[pubkey]; there is no extra per-card API call or second scan. Complexity stays O(node-associated transmissions) per health request, with constant-size 24-bucket output. A 30K-transmission accumulator benchmark measured ~1.05 ms/op on an Apple M5 (-benchtime=3x).

@dborup dborup left a comment •

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Independent review - one actionable finding:

[P2] Unlimited-retention stores can never show the 24-hour chart (cmd/server/node_activity_24h.go, line 75). loadBackgroundChunks returns immediately when retentionHours <= 0 (cmd/server/store.go, line 1679) after setting backgroundLoadDone, but it never computes loadCoverageRatio; that field remains zero. The new coverage gate therefore always returns unknown, and home.js renders Activity unavailable indefinitely, even after LoadChunked loaded the complete database. This is a documented configuration: retentionHours = 0 means unlimited. Please derive proof of coverage for that load mode separately, or explicitly handle the unlimited-retention path, and add a regression test.

The observer preview/dialog, HTML escaping of observer names, rolling bucket boundaries, and fixture coverage otherwise look sound in this diff.

@dborup

dborup commented Oct 7, 2026

Copy link
Copy Markdown
Owner Author

Review feedback addressed (commit c8c14f46):

  1. The 24h coverage gate now recognizes the documented retentionHours: 0 mode when an independently loaded hot-start window spans at least 24 hours. A shorter hot window remains incomplete; the existing exact-ratio gate still applies to bounded retention.
  2. Added regression assertions for 25h versus 12h hot windows in unlimited-retention mode.
  3. Re-ran targeted activity tests and the full cmd/server Go suite; both pass.

@dborup dborup left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Follow-up review of c8c14f4: the unlimited-retention finding is resolved. For hotStartupHours >= 24, the revised gate accepts the proven hot window and still checks oldestLoaded; for shorter hot windows it correctly leaves coverage incomplete. The hotStartupHours = 0 path computes loadCoverageRatio in RunStartupLoad after the full synchronous load, so it remains covered by the ordinary gate. Focused node-activity Go tests passed. No remaining findings from my review.

@dborup

dborup commented Oct 7, 2026 •

Copy link
Copy Markdown
Owner Author

Taking this PR over now (the author has finished). Next: an independent review.

@dborup

dborup commented Oct 7, 2026

Copy link
Copy Markdown
Owner Author

Review — CS-Minimax PR#351 — head b039258

Dom: REQUEST CHANGES

Independent read-only review. Evidence: [T] = I executed it, [A] = analysis, [K] = read in code/docs. Everything was run on the merged tree (git merge-tree --write-tree origin/master b0392585 with master at 461803d9, no conflicts) in a throwaway export, never in a working copy. git ls-remote showed b0392585 before and after.

Findings

# Sev Where Finding
1 blocking cmd/server/node_activity_24h.go:78 The complete gate reads loadCoverageRatio, but in the hotStartupHours == 0 path that ratio is len(s.packets) / COUNT(*) over the whole transmissions table — no retention filter — unlike loadBackgroundChunks, which counts only rows above the retention floor. With the shipped config.example.json (packetStore.retentionHours: 168, hotStartupHours: 0) and retention.packetDays: 30, the ratio is structurally below 1, so activity24h.complete is false forever and the new chart is permanently replaced by "Activity unavailable: 24h history incomplete". The PR's primary deliverable never renders on the documented default configuration.
2 blocking public/home.css:475, public/home.js:349 .mnc-observer-name clips every observer name at max-width: 14ch with overflow: hidden, but the View all disclosure is only emitted when isRepeater && obs.length > 3, and the span carries no title. On a repeater card with 1–3 observers, and on every non-repeater card, a name longer than ~14 characters is clipped with no tooltip and no disclosure — unrecoverable. Master showed the names in full. #304 asks for the full list "without losing names".
3 major cmd/server/node_activity_304_test.go My mutant M3 (a.Complete = true in place of !coverageStart.After(start)) survives the entire cmd/server suite. The rule "coverage is known but shorter than 24 h ⇒ incomplete" — a PR claim and #304's "distinguish known zero activity from missing/incomplete coverage" — has no test.
4 nit cmd/server/node_activity_24h.go:65-80 First call site in the codebase that nests s.mu → bgErrMu. No reverse order exists today, so there is no deadlock [A], but the bgErrMu contract comment (store.go:532-538) says the lock exists so readers need not synchronise on s.mu. Reading the ratio before taking s.mu would keep that property.
5 nit cmd/server/store.go:10497 One new map[string]interface{} outside tests (727 → 728): the extracted getNodeHealthAt. A mechanical extraction of GetNodeHealth's existing return type to inject now, not a new untyped payload.
6 nit cmd/server/node_activity_24h.go:53 add() does an RFC3339Nano time.Parse per node-associated transmission per health request; the surrounding loop otherwise only compares strings (pkt.FirstSeen > todayStart). New per-request CPU proportional to a node's packet count.
7 nit public/home.js:118-121 The shared <dialog> is injected into the home markup unconditionally, including when the user has no nodes, and its aria-labelledby target <h2> is empty until first open.
8 info branch 61 commits behind origin/master. Still merges cleanly against both 461803d9 and the newer eb8f54bb, and master touched none of the PR's eight files in between. [T]
9 info scope #304 is not resolved, and the PR says so. Its criteria 3–6 are untouched; the card still shows "SNR · Good" and "Avg hops".

Finding 1 in detail

[T] I ran the real RunStartupLoad against DBs whose history is longer than the in-memory window, then asked the gate whether the chart may render:

Case DB rows in memory loadCoverageRatio oldestLoaded complete
retentionHours: 168, hotStartupHours: 0, DB holds 30 days 720 168 0.2333 7 days back false
same DB, hotStartupHours: 24 720 169 1.0000 7 days back true
DB fits inside the window 100 100 1.0000 4 days back true

In the first row the in-memory index demonstrably reaches back seven days — far past windowStart — yet the gate refuses. The only difference between rows 1 and 2 is which COUNT(*) the ratio was measured against.

[T] Reproduced end to end in a browser, too. A local Go server on the CI-prepared e2e-fixture.db (freshen, seed SQL, corescope-migrate, seeds 2073 and 199) logged coverage=100.0% only after I removed the fixture's negative-id seed rows; as prepared it logged coverage=98.2%, loaded=500, totalInDB=509, because LoadChunked's id cursor starts at -1 and never loads the id = -1000000 and id = 0 fixture rows. /api/perf then showed loadCoverageRatio = 0.9823, retentionHours = 0, backgroundLoadComplete = true, oldestLoaded = 2026-05-15T00:00:00Z, and /api/nodes/<pk>/health returned complete: false, coverageStart: null with a non-zero bucket present. Both My Mesh repeater cards rendered "Activity unavailable: 24h history incomplete" and zero bars.

[K] docs/api-spec.md states the intent correctly — "complete is true only when successful loading reports 100% of the retained transmissions in memory" — so this is the implementation reading the wrong number, not a documented design choice. [A] Two directions would fix it: apply the retention filter to the COUNT(*) in RunStartupLoad's hotStartupHours == 0 branch so the ratio means what the doc says, or drop the ratio from this gate and compare the in-memory count for the 24-hour window against a DB count for the same window, which is what the field actually needs to assert.

[A] Blast radius: broken whenever retentionHours > 0 && hotStartupHours == 0 (the shipped example config) and whenever any row is skipped by the loader; correct when hotStartupHours > 0; correctly false when retentionHours < 24.

Finding 2 in detail

[T] Browser, local server, light and dark, 1280×900 and 375×812: a repeater card with one observer named Jackrabbit Mountain Relay North renders clipped: true, title: null, viewAll: false in all four combinations. On the seven-observer card, BB-8 0EA3 Rak 1W and Mutual Aid logger 🌑 are also clipped — there the dialog recovers them. [A] No test covers the gap: all three mocked cards in test-issue-304-my-mesh-e2e.js are role: 'repeater', and only the 51-observer card carries long names, so neither the "≤3 observers with a long name" nor the non-repeater path is exercised. [A] A title on the span, or extending the disclosure to any card whose preview is clipped, would close it.

The PR description's claims

Claim Verdict
"Replace the 20-recent-packet pseudo-activity chart with 24 rolling hourly buckets of unique node-associated transmissions from the existing in-memory node index" [K] true in code, [T] tested (mutants M1, M2) — but [T] it does not render on the documented default config, finding 1
"Expose coverage metadata and show an explicit unavailable state when a full 24-hour history cannot be established" [K] true; [T] the unavailable state is tested (M4), but [T] the "known-but-too-short coverage" rule is untested, finding 3
"A node association is not proof of repeater forwarding" [K] stated in docs/api-spec.md and carried into the chart label "Node-associated transmissions · last 24h"; [T] asserted by the E2E
"Limit the repeater-card observer preview to three names, with an accessible View all dialog" [T] true and tested (M5, M7, M8); keyboard (Enter/Escape, focus return), touch and backdrop click all verified
"The full observer list remains available without expanding card height" [T] true for repeater cards with >3 observers (heights 241–286 px across themes and viewports); [T] false for the clipped-name case, finding 2
"24 counters updated inside the existing GetNodeHealth loop; no extra per-card API call or second scan" [K] true — one activity.add(pkt) in the existing for _, pkt := range packets loop, constant-size output
"~1.05 ms/op on a 30K accumulator benchmark" [T] 1.355 ms/op at -benchtime=10x on an M2 Pro — same order, claim plausible
"cmd/server/cmd/ingestor go test ./..., sh test-all.sh, the two E2Es, xss gate, git diff --check" [T] all reproduced, see below

#304 acceptance criteria

Criterion Status
>20 transmissions across a day give correct hourly counts, including hours absent from the preview [T] met and tested
Window boundaries, missing coverage, empty data, duplicate observations tested [T] partly — boundaries, empty and duplicate observations are tested; "coverage known but shorter than the window" is not (finding 3)
Cards with 0/1/50+ observers stay compact; full list reachable by keyboard and touch without losing names [T] compact and reachable; names are lost in the clipped case (finding 2)
Advert vs relay activity distinguishable not in this slice
Signal and hop labels do not imply unsupported direct-RF quality not in this slice — "SNR · Good" and "Avg hops" are unchanged
Like-for-like previous-period comparison not in this slice
Desktop and mobile tests cover long names, mixed repeater/observer cards, navigation, synthetic identities [T] desktop+mobile, long names and synthetic identities yes; mixed card roles no — every mocked card is a repeater
Zero-hop and flood advert intervals computed separately not in this slice
Aggregation cost documented, no per-card full-history scan [K][T] documented in the PR and benchmarked; no second scan

Always-checks

Check Result
cmd/server read-only [T] no write path added; the new file is pure computation
No new map[string]interface{} outside tests [T] one added (727 → 728) — finding 5
No hardcoded colours [T] no added line matches a hex/rgb/hsl colour; node scripts/check-css-vars.js OK (2189 var() refs, 0 undefined)
scripts/check-xss-sinks.sh --diff origin/master [T] clean (rc=0) on a branch built off origin/master; --file public/home.js also rc=0; git diff --check clean
Fork-guards 9 and 1 [T] deploy.yml 9, release-fast-path.yml 1 — byte-identical counts to master; the only workflow change is one added E2E line
No closing keywords [T] none in the title, the body or the three commit messages. Note: the body links the issue in prose only ("deliberately limited slice of #304"); there is no literal "Relates to" line, which is harmless for auto-close but differs from the convention the other open PRs follow
Commit author [T] all three commits author and committer dborup <kontakt@meshview.dk>

Tests I ran (merged tree, master 461803d9)

Suite Result
cd cmd/server && go test -race -count=1 -timeout 45m ./... [T] ok, 817.9s
cd cmd/ingestor && go test -count=1 -timeout 45m ./... [T] ok, 113.0s
go vet ./... in both modules [T] clean
sh test-all.sh [T] 232 passed, 0 failed (232 files)
node test-frontend-helpers.js [T] 709 passed, 0 failed
CHROMIUM_REQUIRE=1 node test-issue-304-my-mesh-e2e.js [T] pass
CHROMIUM_REQUIRE=1 node test-issue-2027-my-mesh-node-page-e2e.js [T] 25 passed, 0 failed
BenchmarkNodeActivity24h30K -benchtime=10x [T] 1.355 ms/op (M2 Pro)
Browser: local Go server on the CI-prepared e2e-fixture.db, My Mesh home with two repeater cards, light + dark, 1280×900 + 375×812 [T] no console errors, no page errors, no failed requests, no horizontal overflow; dialog theme-aware and inside the viewport (341 px at 375); card heights 241–286 px
Browser, second server with 30 h of history so complete is true [T] 24 bars render with per-bucket title/aria-label, still no console errors

Servers were started on free ports from 13700 up and stopped by looking the listener up by port.

Red before: [T] the new E2E run against master's public/ fails at its first new assertion (.mnc-observer-name count 0 !== 3). The Go tests cannot compile against master (new symbols), so their red-before state is established by the mutants below instead.

One-off: [T] TestHandleNodePaths_PrefixCollision_1352_FallbackBranch failed once in a non--race full run while several suites, two servers and a browser were running concurrently. It then passed 12/12 on the merged tree and 12/12 on master's tree, and the clean -race full run never hit it. Treating it as load-induced, not PR-related. Neither known flake (#256, #267) appeared.

My mutants (8; 7 killed, 1 survived)

# Mutant Result
M1 add() counts observations instead of unique transmissions killed — TestNodeActivity24hRollingBoundariesAndUniqueTransmissions [T]
M2 half-open window end becomes inclusive (when.After(a.end)) killed — same test, via index out of range [24]; the exclusivity check is load-bearing for bounds safety [T]
M3 a.Complete = true whenever coverage is known SURVIVED the whole cmd/server suite — finding 3 [T]
M4 frontend renders bars even when complete === false killed — E2E (24 !== 0) [T]
M5 observer preview not limited to three names killed — E2E (51 !== 3) [T]
M6 drop the retentionHours < 24 guard killed — TestNodeActivity24hCoverageRequiresLoadedHistory [T]
M7 dialog list uses innerHTML killed — E2E (dialog img count 1 !== 0) [T]
M8 inline preview name not escaped killed — E2E (svg count 1 !== 0) [T]

CI

[T] Run 37610399888, pull_request, head b0392585, attempt 1: Go Build & Test pass (24m31s), Playwright E2E pass (24m18s), Build & Publish Docker Image pass (59s); Release Artifacts, Deploy Staging and Publish Badges skipped (tag-/push-/fork-gated). The two earlier runs on 4130188f and c8c14f46 were cancelled by the next push. [A] CI ran the branch as-is, 45 commits behind master at the time, not the merged tree.

Not verified

  • No staging or production validation, and no server API key used — out of scope by instruction.
  • Whether a real deployment sets hotStartupHours > 0. The repo only ships config.example.json (168 / 0), which is the configuration finding 1 breaks; I could not confirm what the target instance runs, and that determines whether finding 1 is already live.
  • The chart's visual appearance beyond the DOM/geometry assertions and the screenshots I took — I did not do a pixel diff.
  • Screen-reader behaviour of 24 role="img" bars; I checked only that the labels are present and escaped.
  • GetBulkHealth and the other recentPackets consumers (nodes.js, live.js, observer-detail.js) were read but not re-tested; the API change is additive and they are untouched.
  • The remaining My Mesh: make repeater cards compact and fix misleading activity and health metrics #304 criteria (status evidence, SNR attribution, traffic semantics, advert intervals) — the PR does not claim them.

dborup added 2 commits October 7, 2026 19:45
…lth (#351 review F1,F3,F4,F5,F6)

F1 (blocking): RunStartupLoad's hotStartupHours==0 branch measured
loadCoverageRatio against a whole-table COUNT(*), so on the shipped default
(retentionHours=168, hotStartupHours=0) with a DB holding more than 168h of
history the ratio was structurally below 1 and the 24h activity gate never
proved completeness — the chart showed "Activity unavailable" forever. Divide
by the rows LoadChunked actually targeted (the retention/hot-start-filtered
count it already computes), matching docs/api-spec.md and the background-fill
path. New Test351F1StartupCoverageHonoursRetentionWindow covers BOTH startup
paths (hotStartupHours==0 and >0).

F3 (major): add TestNodeActivity24hKnownButShorterCoverageIsIncomplete —
coverage that is known but shorter than 24h must read as incomplete, not as
confirmed silence. Kills mutant M3 (unconditional Complete=true), which
survived the whole suite before.

F4 (nit): read the bgErrMu-guarded coverage ratio in GetNodeHealth BEFORE
taking s.mu and pass it into nodeActivityCoverageStartLocked, so the store
never nests s.mu -> bgErrMu (bgErrMu exists precisely so readers avoid s.mu).

F5 (nit): drop the getNodeHealthAt extraction; inject now into GetNodeHealth
directly so no second untyped-map-returning signature is added (AGENTS.md).

F6 (nit): skip the per-packet RFC3339Nano parse for transmissions that sort
below the window start (the common case for long in-memory history). Only the
lower bound is string-filtered — the upper bound carries now's sub-second
precision and cannot be compared safely as a string. Guarded by
TestNodeActivity24hAddPreFilterKeepsBoundaryPackets.

Relates to #304
…review F2)

F2 (blocking): .mnc-observer-name clips at 14ch, but the full name was only
recoverable on repeater cards with >3 observers (the View all dialog). On a
repeater with 1-3 observers, and on every non-repeater card, a long name was
clipped with no tooltip and no disclosure — the name was lost. #304 requires
the full list reachable "without losing names".

Each observer span now carries a title with the full name (pointer + assistive
tech), and the accessible dialog is offered whenever the preview hides names
OR any shown name is clipped — on all card roles, not just repeaters with >3.

E2E: mixed card roles (repeater with 51/1/0 observers, a <=3-observer repeater
with a clipped name, and a non-repeater with a clipped name) at 1280x900 and
375x812; assert title present, name clipped, dialog offered, and the full name
recoverable via the dialog, with cards staying compact. Kills the mutant that
restores the old "clipped with no recourse" behavior.

Relates to #304
@dborup-agent

Copy link
Copy Markdown
Collaborator

Rapport — CS-MacBook PR#351 runde-2 — head 43ba651

Review feedback addressed (commit 43ba651e). Evidence: [T] executed, [A] analysis, [K] read in code/docs. Worked in a dedicated worktree off origin/codex/my-mesh-activity-304 (b0392585); two plain commits pushed, no rebase/amend/force. One mutant per finding, each verified red before the fix.

Findings

  1. F1 (blocking) — fixed. RunStartupLoad's hotStartupHours==0 branch measured loadCoverageRatio against a raw SELECT COUNT(*) FROM transmissions (whole table). With the shipped default (retentionHours:168, hotStartupHours:0) and a DB holding more than 168h of history, the ratio is structurally below 1, so the 24h coverage gate (loadCoverage < 1) never proves completeness and the chart is replaced by "Activity unavailable" forever. Fix: divide loadedCount by the rows LoadChunked actually targeted — the retention/hot-start-filtered total it already computes — published via a new loadWindowTotalInDB field (written once by LoadChunked, read once by RunStartupLoad on the same goroutine). Now the ratio means "fraction of the RETAINED rows in memory", matching docs/api-spec.md and the background-fill path. The unlimited-retention path is unchanged (its filter is empty, so the target total is still the whole table). [K][T]
  2. F2 (blocking) — fixed. .mnc-observer-name clips at 14ch, but the full name was recoverable only on repeater cards with >3 observers (the dialog). On a repeater with 1–3 observers and on every non-repeater card, a long name was clipped with no tooltip and no disclosure — lost. Fix: every observer span now carries a title with the full name (pointer + assistive tech), and the accessible dialog is offered whenever the preview hides names OR any shown name is clipped — on all card roles. [T] browser-verified at 1280×900 and 375×812, light path, no console/page errors, cards stay compact (<350px).
  3. F3 (major) — fixed. Added TestNodeActivity24hKnownButShorterCoverageIsIncomplete: coverage that is known but shorter than 24h must read incomplete, not confirmed silence; plus the exact-start boundary (complete) and one-nanosecond-past (incomplete). Kills mutant M3 (a.Complete = true whenever coverage is known), which survived the whole suite before. [T]
  4. F4 (nit) — fixed. GetNodeHealth now snapshots the bgErrMu-guarded coverage ratio BEFORE taking s.mu and passes it into nodeActivityCoverageStartLocked, so the store never nests s.mu → bgErrMu. bgErrMu exists precisely so its readers need not synchronise on s.mu (store.go contract); this keeps that property and removes the first-in-codebase nesting site. [A][T]
  5. F5 (nit) — fixed. Dropped the getNodeHealthAt extraction; now is injected directly into GetNodeHealth(pubkey, now). No second untyped-map signature is added — cmd/server/store.go's map[string]interface{} count goes 213 → 212 vs PR head (monotonic decrease, AGENTS.md). Callers in routes.go and three test files updated. [T]
  6. F6 (nit) — fixed. add() now skips the per-packet RFC3339Nano parse for transmissions that sort below WindowStart (the common case for a node whose in-memory history spans past 24h). Only the LOWER bound is string-filtered: a lower-precision same-instant stamp sorts ≥ WindowStart, so an in-window packet is never dropped, and the exact bounds check still runs for survivors. The UPPER bound is intentionally not string-filtered — WindowEnd carries now's sub-second precision, so a whole-second stamp in the final second would sort after it and be dropped wrongly. Guarded by TestNodeActivity24hAddPreFilterKeepsBoundaryPackets. [A][T]

Not changed: F7 (info — unconditional <dialog> injection; a .destroy()-time close() already exists and the empty-<h2>-until-open pattern is standard for a shared modal), F8 (info — behind master; see note below), F9 (info — #304 remaining criteria out of this slice). No doc change needed: docs/api-spec.md already defines complete against the RETAINED transmissions, which F1 makes the implementation honour.

Tests & mutants

Finding Test Mutant Red-before
F1 Test351F1StartupCoverageHonoursRetentionWindow (both startup paths, real DB via OpenDB+RunStartupLoad) whole-table COUNT(*) in RunStartupLoad [T] FAIL loadCoverageRatio=0.2667 < 1 (only the hotStartupHours=0 subtest; the =24 path already correct)
F2 test-issue-304-my-mesh-e2e.js (mixed roles, clipped names, 1280×900 + 375×812) restore old clip-with-no-recourse [T] FAIL (title assertion actual: null)
F3 TestNodeActivity24hKnownButShorterCoverageIsIncomplete a.Complete = true when coverage known [T] FAIL (Complete:true)
F6 TestNodeActivity24hAddPreFilterKeepsBoundaryPackets add `

F4/F5 are structural (no behavior change); covered by the existing cmd/server suite plus the updated call sites.

Suites [T]

  • cd cmd/server && go test -race -count=1 ./... → ok, 394.3s
  • cd cmd/ingestor && go test -count=1 ./... → ok, 111.7s; go vet ./... clean in both modules
  • sh test-all.sh → 227 passed, 0 failed
  • node test-frontend-helpers.js → 709 passed, 0 failed
  • CHROMIUM_REQUIRE=1 node test-issue-304-my-mesh-e2e.js → pass
  • CHROMIUM_REQUIRE=1 node test-issue-2027-my-mesh-node-page-e2e.js → 25 passed, 0 failed
  • scripts/check-xss-sinks.sh --diff origin/master → rc=0; check-css-vars.js → OK, 0 undefined; git diff --check → clean

CI per job (run 37661809142, head 43ba651, pull_request)

  • Go Build & Test: success
  • Playwright E2E Tests: success
  • Build & Publish Docker Image: success
  • Release Artifacts / Deploy Staging / Publish Badges & Summary: skipped (tag-/push-/gate-conditional)

Attempt 1 failed on the Verify proto syntax step, which timed out after 5 min while apt-get install protobuf-compiler stalled on a slow mirror (::error::protoc not found after install) — a transient infra failure, not #271/#301 and unrelated to this diff (no proto files touched; the Go tests never ran on that attempt). Re-ran the failed jobs once per instruction; attempt 2 is green across all three non-skipped jobs.

Notes

  • Draft state: the PR currently reports not draft (isDraft:false). A push does not change draft status, so this predates this round — I did not mark it ready and left the flag untouched for the owner to decide.
  • No merge with master. The branch is behind origin/master (eb8f54b), but master touches none of this PR's files and the merge is clean [T, per prior review]; My Mesh: show estimated zero-hop and flood advert intervals on repeater cards #352 is stacked on this branch, so I kept the base stable (plain commits, no merge-commit) to avoid moving the stack.
  • Not verified: no staging/production, no API key, no upstream writes, no pixel-diff of the chart.

@dborup-agent

Copy link
Copy Markdown
Collaborator

Review — CS-pve-agent2 PR#351 — head 43ba651

Dom: REQUEST CHANGES

This is an independent, read-only re-review of round 2 (the commits 1d0fa095 and 43ba651e on top of b0392585). Evidence tags: [T] = I executed it, [A] = analysis, [K] = read in code or docs.

I ran everything on a git archive of the merged tree in a scratch directory, never in a working copy. The merged tree is git merge-tree --write-tree origin/master 43ba651e…, which gives 74b50881. Master was 2dc04642 and the merge had no conflicts. git ls-remote showed 43ba651e before and after the review.

F1, F3, F4 and F5 are resolved. F6 is resolved to nit level. F2 is only partly resolved. A name that is visually clipped but is 14 characters or shorter still gets no dialog. On a touch device, the only way to read it would be the title tooltip, which touch devices don't show. That is the one blocking item.

Findings

# Sev Where Finding
R1 blocking (residual of F2) public/home.js (previewClipped, OBS_CLAMP_CH = 14) The decision to show the dialog is a heuristic on JS string length (length > 14). The real clamp is CSS max-width: 14ch, which is the width of fourteen "0" glyphs. Uppercase and wide-glyph names that are 14 characters or shorter overflow that width, yet no button appears. [T] At 1280×900 and 375×812, in light and dark, with the real /api/nodes/:pk/health and only the observer names substituted, these rendered clipped: true with no View all/Full names button: MOUNT WOLFHAWK (1-observer repeater card), WWWWWWWWWWWWWW, MMMMMMMMMMMMM and ØSTERBRO MOLE (3-observer repeater card). In a standalone check, DOKK1 REPEATER and KUNSTHAL AARHU clip as well. The span keeps title, but on the 375×812 touch viewport the screenshot shows Heard by: MOUNT WOL… with no recourse, so keyboard and touch users cannot recover the name. [A] Fix: decide after layout instead of by string length. For example, after the cards are inserted, set the button visible when any .mnc-observer-name has scrollWidth > clientWidth. Alternatively, always offer the dialog when a card has at least one observer. Please add a ≤14-character uppercase name to test-issue-304-my-mesh-e2e.js.
R2 minor (test gap) test-issue-304-my-mesh-e2e.js My mutant O9 removes escapeAttr from the new title="${…}" on the observer span, and it survives both the E2E and scripts/check-xss-sinks.sh --file public/home.js (rc=0). The gate passes because escapeHtml(...) sits on the same line. The fixture's hostile names (<svg onload=…>, <img …>) contain no ", so they cannot break out of an attribute. [A] Add an observer name containing " (for example x" onmouseover="alert(1)) and assert that the span has no extra attributes.
R3 nit cmd/server/node_activity_24h.go add() The F6 string pre-filter is only order-safe when WindowStart carries at least as many fraction digits as the stamp. RFC3339Nano trims trailing zeros. [T] Probe: now = 13:17:00.000000000Z makes WindowStart = …13:17:00Z, and a packet at start+100ms (…13:17:00.100Z, the ingestor's .000Z format) is dropped (bucket 0 = 0). The same happens whenever now's nanoseconds have trailing zeros, e.g. .5Z against a packet at .501Z. In production this needs now.Nanosecond() to be a multiple of 10⁷ or more, so at most one packet in the first second of the window can be undercounted on about 1 request in 10⁷. Either format the lower bound with a fixed-width fraction for the comparison (2006-01-02T15:04:05.000000000Z) or correct the comment.
R4 nit cmd/server/node_activity_24h.go:80 The comment still says that loadCoverage "is read by the caller (getNodeHealthAt)". That helper was removed in this round and the caller is now GetNodeHealth.
R5 info CI attempt 1 The round-2 report says "the Go tests never ran on that attempt". [T] They did run and passed in attempt 1 (ok github.com/corescope/server 1003.718s, ok …/ingestor 138.814s). Only Verify proto syntax failed, on a 5-minute timeout. This doesn't affect the verdict.
R6 info e2e fixture [T] On the CI-prepared e2e-fixture.db, loadCoverageRatio = 0.938 (loaded=500, totalInDB=533). LoadChunked's id cursor starts at -1, so the 33 negative-id seed rows (seed-2073, seed-245, -1000000) are never loaded. Every activity24h.complete is therefore false against the CI server, and the chart's "available" path is only exercised by the mocked E2E. This is not caused by this PR and doesn't occur with production's positive ids, but it means CI cannot catch a regression in the real-server render path.

Answers to the round-2 checklist

F1 — coverage source in both startup paths: resolved. [K] LoadChunked now publishes loadWindowTotalInDB, the same retention- and hot-start-filtered COUNT(*) its chunk loop walks (t2.last_seen >= cutoff on production schemas). RunStartupLoad's hotStartupHours == 0 branch divides by that count. The hotStartupHours > 0 path is unchanged, because loadBackgroundChunks already counted only rows above the retention floor. [K] The field is written and read on the same goroutine (RunStartupLoad → LoadChunked → return → read), with no other readers (grep).

[T] I ran real servers on a fixture copy with 150 rows aged 10 days past a 168 h retention window, 349 retained rows spread over about 6.5 days, last_seen derived from first_seen, and negative-id rows removed:

Config log / /api/perf activity24h (busiest repeater)
retentionHours: 168, hotStartupHours: 0 coverage=100.0%, loaded=349, totalInDB=349, ratio 1 complete: true, coverageStart 6.5 days back, 27 tx
retentionHours: 168, hotStartupHours: 24 bg fill done, ratio 1, oldestLoaded = cutoff 168 h complete: true, 27 tx (identical buckets total)

[T] Red-before: mutant O1 restores the whole-table COUNT(*) and is killed by Test351F1StartupCoverageHonoursRetentionWindow. [K] That test seeds last_seen, so it exercises the production filter axis. [A] One conservative edge remains. With hotStartupHours == 0, oldestLoaded is the oldest loaded first_seen, so a DB with no traffic between 24 h and the retention floor reports incomplete. That is a false "unavailable", never a false "complete", and it is acceptable.

F2 — observer names recoverable on all cards: partly. [T] Every observer span now has an escaped title. Names longer than 14 characters get a Full names → / View all N → button on every card role, including the 1-observer repeater and non-repeater cards. I checked this in a browser on a local Go server at 1280×900 and 375×812, light and dark. Results: no console errors, page errors or failed requests, and no horizontal overflow. Card heights were 253 px on desktop and 280 px on mobile. 24 bars rendered with complete: true. However, the R1 gap above remains on both viewports.

F3 — mutant M3: dies. [T] With a.Complete = true substituted, TestNodeActivity24hKnownButShorterCoverageIsIncomplete fails (Complete:true). It is the only failing test, so the new test is what kills it.

F5 — no new map[string]interface{}: met. [T] Counted with git grep -c, non-test Go only: merge-base 727, head 727, origin/master 745, merged tree 745, so the delta is 0. The only diff line that touches the token is the GetNodeHealth signature, which gains now time.Time (-/+ pair). The round-1 getNodeHealthAt duplicate is gone.

F4 — lock order: avoided. [K] GetNodeHealth snapshots loadCoverageRatio under bgErrMu.RLock and releases it before s.mu.RLock. nodeActivityCoverageStartLocked takes the value as a parameter. grep shows no remaining site that takes bgErrMu while holding s.mu, or the reverse. [A] Reading the ratio before backgroundLoadDone is conservative, because writers set the ratio before flipping done, so a stale snapshot can only produce "incomplete".

F6 — time.Parse per packet: reduced, nit-level. [K] Packets that sort below WindowStart skip the parse. Packets inside the window still parse once each, so the cost is bounded by the node's 24-hour transmission count, not its whole history. [T] Measured under concurrent load on an i7-1260P with 30 K packets: all in window 1.46 ms/op (existing benchmark), spread over 7 days 0.41 ms/op. Acceptable. See R3 for the pre-filter edge.

Always-checks

Check Result
Each #304 criterion this slice claims has a red-before/green-after test [T] E2E red on master's home.js/css (0 !== 3 observer spans) and red on round-1 frontend (title null), green on head. Go: the new tests reference new symbols, so red-before is shown via mutants (M3, O1, O3, O6, O7, O8 all killed). Exception: R1 has no test.
No behaviour change beyond purpose [K] Two observable changes outside the card. /api/perf.loadCoverageRatio on the hotStartupHours == 0 path now uses the retained-window denominator, which matches the bg-loader path and docs/api-spec.md. GetNodeHealth gains activity24h, which is additive, and recentPackets is still capped at 20. Nothing else reads the ratio (grep).
cmd/server read-only [T][K] Only a SELECT COUNT(*) that already existed; no write path
No hardcoded colours [T] No added hex, rgb or hsl colour in CSS/JS. node scripts/check-css-vars.js: OK, 2189 refs, 0 undefined
scripts/check-xss-sinks.sh --diff origin/master [T] rc=0 on a scratch clone at the merged tree (parent origin/master). git diff --check clean
Fork-guards 9 / 1 [T] deploy.yml 9, release-fast-path.yml 1, identical to master; the only workflow change is the added test-issue-304-my-mesh-e2e.js line
No closing keywords [T] None in the title, body or any of the 5 commit messages
Commit author [T] All 5 commits have author and committer dborup <kontakt@meshview.dk>

Tests I ran (merged tree 74b50881, master 2dc04642)

Suite Result
cd cmd/server && go test -count=1 ./... [T] ok (529 s) on a clean run. The first run happened while browsers, mutant runs and benchmarks were loading this 4-core host. It failed once in Test1690_BackgroundLoadHonesty (638 s, 5000/5000 packets loaded). That test then passed 3/3 in isolation (568, 376 and 298 s), and the clean full run is green. See below.
cd cmd/ingestor && go test -count=1 ./... [T] ok (1807 s, under the same concurrent load)
sh test-all.sh [T] 233 passed, 0 failed (233 files)
node test-frontend-helpers.js [T] 709 passed, 0 failed
CHROMIUM_REQUIRE=1 node test-issue-304-my-mesh-e2e.js [T] pass
CHROMIUM_REQUIRE=1 node test-issue-2027-my-mesh-node-page-e2e.js [T] 25 passed, 0 failed
test-home-coverage-e2e.js against a local server [T] 12 passed, 0 failed
test-a11y-axe-1668.js against a local server (includes /home with the new <dialog>) [T] net=0 across 120 cells
Browser, local Go server on CI-prepared e2e-fixture.db (freshen, seed SQL, corescope-migrate, seeds 2073/199/245) [T] served; every complete: false (R6)
Browser, F1 fixture copy (table above), 3 repeater cards, 1280×900 + 375×812, light + dark [T] see F2 answer; R1 repro with substituted names

Servers ran on free ports 13711–13713 and were stopped by looking up the listener by port. Neither known flaky test (#256, #267) appeared. [A] I do not attribute the single Test1690_BackgroundLoadHonesty failure to this PR. This PR changes nothing in LoadChunked's memory cap or in loadBackgroundChunks; the only change in LoadChunked is the added s.loadWindowTotalInDB assignment. Earlier reviews on these hosts (#264, #266, #313, #320) recorded the same test as I/O-bound and very slow under load, and it was green in 4 of 5 runs here. It is not one of the known flaky tests, so I'm listing it rather than hiding it.

Mutants

# Mutant Result
M3 a.Complete = true whenever coverage is known killed — TestNodeActivity24hKnownButShorterCoverageIsIncomplete [T]
O1 RunStartupLoad back to whole-table COUNT(*) killed — Test351F1StartupCoverageHonoursRetentionWindow [T]
O3 ignore memory-cap eviction (packets[0] lower bound) killed — TestNodeActivity24hCoverageRequiresLoadedHistory [T]
O6 pre-filter < → <= (drops the exact-start packet) killed — three tests incl. …AddPreFilterKeepsBoundaryPackets [T]
O7 coverage gate < 1 → < 0.9 killed — TestNodeActivity24hCoverageRequiresLoadedHistory [T]
O8 drop the backgroundLoadFailed guard killed — same test [T]
O4 dialog only when names are hidden (round-1 behaviour) killed — E2E card 3 must offer the observer dialog [T]
O5 drop title from the observer span killed — E2E (title null) [T]
O9 title="${obsFullName(o)}" without escapeAttr survived E2E and XSS gate — R2 [T]

All mutants were applied in a separate scratch copy and restored afterwards; I checked each restore with cmp against the merged tree.

CI

[T] Run 37661809142 (pull_request, head 43ba651e). In attempt 2, Go Build & Test, Playwright E2E and Build & Publish Docker Image succeeded. Release Artifacts, Deploy Staging and Publish Badges & Summary were skipped (gated). In attempt 1, Go Build & Test failed only at Verify proto syntax (5-minute timeout during protoc install); its Go test steps had passed (R5). [A] CI tests the GitHub PR merge ref, not a re-merge onto today's master. My merged-tree runs above cover that.

Not verified

  • No staging or production check, and the server API key was not used, as instructed.
  • I did not determine which packetStore config the deployed instance uses. F1 is verified for both hotStartupHours settings with retentionHours: 168. retentionHours: 0 with hotStartupHours: 0 relies on the existing whole-table ratio, which was correct before this round.
  • Font-dependence of R1 beyond Chromium's default sans on Linux. Other platforms' fonts change exactly which names clip, which is why a measured check is preferable to a character count.
  • Screen-reader behaviour of the 24 role="img" bars and of the <dialog>, beyond axe and the labels being present.
  • My Mesh: show estimated zero-hop and flood advert intervals on repeater cards #352, which is stacked on this branch, was not reviewed. The remaining My Mesh: make repeater cards compact and fix misleading activity and health metrics #304 criteria (status evidence, SNR/hops labels, previous-period comparison, advert intervals) are outside this slice and the PR doesn't claim them.

dborup and others added 4 commits October 8, 2026 05:01
…e escaping (#351 review R1, R2)

R1: the observer clamp is CSS max-width: 14ch, so 14-char uppercase names
(WWWWWWWWWWWWWW, MOUNT WOLFHAWK) clip without the dialog button. Assert on
every card that the button is visible exactly when the preview hides
observers or a span renders clipped (scrollWidth > clientWidth), and that
a touch tap on 375x812 opens the full name.

R2: an observer name containing a double quote must stay inside the title
attribute, which kills the review's mutant O9 (escapeAttr dropped).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The dialog button was gated on a character count (length > 14), but the
clamp is glyph width. Render the button hidden when the preview hides no
observers and reveal it after layout on any card whose name actually
clips (scrollWidth > clientWidth, which is when the ellipsis is drawn).
All reads happen before the writes, so the grid costs one layout pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…351 review R3)

RFC3339Nano trims trailing zeros, so WindowStart ("...:00Z", ".5Z") can
carry fewer fraction digits than a stamp (".100Z", ".501Z") and the
string pre-filter sorts an in-window packet below the bound.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
 review R3, R4)

Compare against the window start truncated to whole seconds in a fixed
width layout. A UTC stamp at or after the start always sorts at or above
it whatever its fraction, and the start's own second falls through to the
exact time check. Also name the caller GetNodeHealth in the lock-order
comment instead of the removed getNodeHealthAt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dborup-agent

Copy link
Copy Markdown
Collaborator

Rapport — CS-pve-agent3 PR#351 runde-3 — head d971bbd

Review feedback addressed (commit d971bbd3). Evidence: [T] executed, [A] analysis, [K] read in code/docs. Worked in a dedicated worktree off origin/codex/my-mesh-activity-304 (43ba651e). Four plain commits (test → fix per finding group), pushed as a fast-forward, with no rebase, amend or force. Author and committer on all four: dborup <kontakt@meshview.dk>.

  1. R1 (blocking): addressed in b8a62635, test in 7e573d21. The dialog button is no longer gated on name.length > 14. When the preview hides no observers, the button is rendered hidden. After grid.innerHTML is set, revealClippedObserverButtons() measures every observer span and un-hides the button on any card where scrollWidth > clientWidth. All reads happen before any write, so the whole grid costs one layout pass. [T] I chose scrollWidth > clientWidth over a Range-width comparison on purpose. A Range check flags aaaaaaaaaaaaaa, which overflows by 1/64 px (90.766 vs 90.75 px). A 4× screenshot shows Chromium draws no ellipsis there, and scrollWidth/clientWidth agree (91/91). For every short-but-wide name in the review (MOUNT WOLFHAWK, WWWWWWWWWWWWWW, MMMMMMMMMMMMM, ØSTERBRO MOLE, DOKK1 REPEATER, KUNSTHAL AARHU), both methods report clipped. [A] Body text uses the system font stack (Aldrich is only used by the logo SVG). A customizer theme or font change fires theme-refresh → renderHome → loadMyNodes, which measures again. So I did not add a ResizeObserver.
  2. R2 (test gap, XSS): addressed in the test; not added to the gate. Reasoning below. test-issue-304-my-mesh-e2e.js now renders an observer named x" onmouseover="alert(1) in the 51-observer card's preview. It asserts that every .mnc-observer-name has exactly the attributes class and title, that title equals the text verbatim, and that no [onmouseover] element exists. [T] Mutant O9 (title="${obsFullName(o)}" without escapeAttr) is now killed: observer span gained attributes: class,onmouseover,title.
    Gate: [T] scripts/check-xss-sinks.sh --file still passes O9 (rc=0). The reason is not the escapeHtml on the same line. There are two structural blind spots: (a) the O9 line contains no sink. The gate audits only lines with innerHTML=, insertAdjacentHTML( and the like, and the sink here is grid.innerHTML = cards.join('') many lines later. Also, obsFullName is not an allowlisted token. (b) Even on a sink line, strip() runs the double-quoted-string pass before the template-literal pass, so any attr="${x}" interpolation is erased before the audit. Repro: el.innerHTML = `<span title="${o.observer_name}">x</span>` → rc=0, while el.innerHTML = `<span>${o.observer_name}</span>` → rc=1 (flagged). [A] Catching O9 would need a sink-independent attribute-context rule plus taint propagation through local helpers. Fixing (b) changes what the shared CI gate flags on every open PR. Both are a gate redesign, outside this PR's "no other changes" scope. I recommend a separate issue for (b) at least, since it is a real false negative on sink lines.
  3. R3 (nit): addressed in d971bbd3, test in 073783e2. The pre-filter now compares tx.FirstSeen against startSecond, the window start truncated to whole seconds in the fixed-width layout 2006-01-02T15:04:05 (no fraction, no zone). Any UTC stamp at or after the start sorts at or above that prefix whatever its fraction width. A stamp in the start's own second falls through to the exact time.Parse bounds check. [A] Cost is unchanged: one string compare per tx. At most the packets in a single extra second reach the parse. Like before, this assumes UTC stamps, which is what the ingestor writes. A non-UTC offset is only ever let through to the exact check, never wrongly dropped, unless the offset is negative.
  4. R4 (nit): addressed in d971bbd3. The lock-order comment now names GetNodeHealth instead of the removed getNodeHealthAt.

Not changed: R5 and R6 (info). R5 corrects the round-2 report: the attempt-1 Go tests did run and pass. R6 is pre-existing and not caused by this PR; it still shows on the local fixture server (activity24h.complete: false).

Tests & mutants

Finding Test Red before fix Mutants (all killed [T])
R1 test-issue-304-my-mesh-e2e.js: measured invariant on all 8 cards (button visible ⇔ preview hides observers OR a span renders clipped), a 14-char WWWWWWWWWWWWWW 1-observer repeater that must clip, MOUNT WOLFHAWK/MMMMMMMMMMMMM/ØSTERBRO MOLE on a repeater and a non-repeater card, tap-to-dialog on 375×812 touch [T] card 5 at 1280px: clipped=["WWWWWWWWWWWWWW"] hides=false but button visible=false R1a drop the measurement call → card 3 must offer the observer dialog; R1b never hide the button → card 1 (One) fails; R1c back to textContent.length > 14 → card 5 … button visible=false
R2 same E2E, quote-breakout payload n/a (head already escaped; the test exists to kill O9) O9 → observer span gained attributes: class,onmouseover,title
R3 TestNodeActivity24hPreFilterIgnoresFractionWidth: 5 cases (whole-second now vs .100Z and 1 ns stamps, trimmed .5Z vs .501Z/.500001Z, exact start as .500Z, plus a 1 ms pre-window drop) [T] all 5 subtests FAIL (WindowStart=…13:17:00Z, stamp=…13:17:00.100Z: in-window packet dropped) R3a compare against WindowStart again; R3b full-precision bound; R3c bound one second late (also kills two existing boundary tests)

I made one E2E tweak alongside: the round-2 F2 loop's .mnc-view-all count is now :visible, because a hidden button exists in the DOM. Without it, mutant R1a was only killed by a click timeout.

Suites [T]

Browser check [T]

Local Go server on the fixture copy, with two observer names substituted in the scratch DB (MOUNT WOLFHAWK, WWWWWWWWWWWWWW). My Mesh had three real repeater cards (1 observer each). Viewports 375×812 (touch, isMobile) and 1280×900, in light and dark:

  • MOUNT WOL… and WWWWWWWW… both render clipped, and each card shows Full names →. The control card (SBC Room-O, not clipped) shows no button.
  • On 375×812, tapping the button opens the dialog listing MOUNT WOLFHAWK in full; Escape closes it.
  • 0 console errors and 0 page errors, no horizontal overflow. Card height is 246–248 px on mobile and 218–221 px on desktop.

CI per job (run 37736044834, head d971bbd, pull_request, attempt 1)

  • Go Build & Test: success (server ok 979 s, ingestor ok)
  • Playwright E2E Tests: success (My Mesh #304 … passed, #2027 … 25 passed)
  • Build & Publish Docker Image: success
  • Release Artifacts / Deploy Staging / Publish Badges & Summary: skipped (gated)

No reruns were needed, and neither #271 nor #301 appeared.

Notes

  • Draft state: the PR still reports isDraft: false, as noted in round 2. I did not change it either way.
  • No merge with master. git merge-tree against origin/master (ff9a501c) is clean. Master touches deploy.yml, routes.go, store.go and api-spec.md, but without conflicts. My Mesh: show estimated zero-hop and flood advert intervals on repeater cards #352 is stacked on this branch, so I kept history linear with plain commits only.
  • Not verified: no staging or production, no API key, no upstream writes. Clipping was checked only with Chromium's default Linux sans font, but the check is now measured, so other fonts change which names clip, not whether the button appears.

@dborup-agent

Copy link
Copy Markdown
Collaborator

Review — CS-pve-agent2 PR#351 — head d971bbd

Dom: APPROVE med nits

This is an independent, read-only re-review of round 3: commits 7e573d21, b8a62635, 073783e2 and d971bbd3 on top of 43ba651e. Evidence tags: [T] = I executed it, [A] = analysis, [K] = read in code or docs. The related issues are #304 (compact repeater cards, activity/health metrics) and #262 (observer identity).

I ran everything on git archive exports in a scratch directory, never in a working copy. I used both the head and the merged tree. The merged tree is git merge-tree --write-tree origin/master d971bbd3…, which gives 9e70dd00. Master was 689a68c5 and the merge had no conflicts. git ls-remote showed d971bbd3 before and after the review.

All four round-2 findings are resolved. R1 was the blocking one. It is now decided by measuring the rendered spans. I verified this in a browser at 1280×900, 375×812 (touch) and 320×640, in light and dark, and across live resizes. What remains are three nits and two info items, none blocking.

Findings

# Sev Where Finding
N1 nit public/home.js (revealClippedObserverButtons comment) The comment says "scrollWidth > clientWidth is exactly when the ellipsis is drawn". That is not exact. The integer rounding of scrollWidth can report a 1 px overflow when the text overflows by only 1/64 px and Chromium draws no ellipsis. [T] At a 908 px viewport, 00000000000000 and aaaaaaaaaaaaaa give scrollWidth/clientWidth = 87/86, with text 86.5 px against a box of 86.484 px. A 4× screenshot shows the full name and no ellipsis, yet the card offers Full names →. At 901 px and at 1280 px, the same names measure 86/86 and 91/91. The error goes the harmless way: an extra button, never a missing one. Please soften the comment, e.g. "is true whenever an ellipsis is drawn (it can also fire on a sub-pixel overflow)". No code change is needed.
N2 nit PR description The description still describes round 1 ("Limit the repeater-card observer preview … View all dialog", Validation "227/227"). It does not mention two behaviours added in rounds 2–3. First, Full names → now appears on any card role when a name is clipped. Second, /api/perf.loadCoverageRatio on the hotStartupHours == 0 path now uses the retained-window denominator. Reviewers of #352 and future readers will look here first, so please update the body.
N3 info public/home.js revealClippedObserverButtons The measurement runs once per render. There is no resize or font listener, and a revealed button is never hidden again. [T][A] This doesn't cause stale state in practice. --fs-md is fluid (clamp(14px, 0.20vw + 13px, 17px)), so the font size does change on resize. But each name's width and the 14ch clamp scale together, so whether a name visibly clips doesn't depend on the viewport. My sweep is described below. Every disagreement it found was the N1 sub-pixel artifact. None was a drawn ellipsis without a button.
N4 info scripts/check-xss-sinks.sh I confirm the author's analysis of R2. The gate does not flag an unescaped interpolation inside a quoted attribute, even on a sink line. [T] el.innerHTML = `<span title="${o.observer_name}">x</span>` gives rc=0, the single-quoted variant gives rc=0, and el.innerHTML = `<span>${o.observer_name}</span>` gives rc=1. This is a real false negative in the shared CI gate. Fixing it changes what the gate flags on every open PR, so it is reasonable to keep it out of this PR [A]. I recommend the maintainer track it separately. I did not open an issue.
N5 info #352 #352's head (1f54e5a4) is based on b0392585, the round-1 head. It contains none of the round-2/3 commits. History on this branch is intact (see below), so that is fine. But when #352 is updated, it needs a merge of this branch, not a rebase, to keep the stack fast-forward.

Answers to the round-3 checklist

R1 — clipping decided by rendering: resolved. [K] The length > 14 heuristic and OBS_CLAMP_CH are gone. If the preview hides observers, the button is rendered visible (View all N →). Otherwise it is rendered with hidden, and revealClippedObserverButtons(grid) runs right after grid.innerHTML = …. It un-hides the button when any .mnc-observer-name on the card has scrollWidth > clientWidth. All reads happen before any write, which costs one forced layout per render [A]. [K] .mnc-btn has no display rule, so the UA [hidden] { display: none } applies.

[T] Browser setup: a local Go server on the CI-prepared e2e-fixture.db and the real /api/nodes/:pk/health, with only observers substituted through Playwright route interception. There were five cards: 4 repeaters and 1 room.

Card Observer names 1280×900 light/dark 375×812 touch light/dark
repeater, 1 obs MOUNT WOLFHAWK (short but wide) clipped 114/91 → Full names → clipped 103/82 → Full names →
repeater, 3 obs ab, cd, WWWWWWWWWWWWW (wide name in 3rd position) clipped → Full names → clipped → Full names →
room, 2 obs iiiiiiiiiiiiiiiiiiii (20 chars), lllllllllllllllll (long but narrow) not clipped (52/52) → no button not clipped (47/47) → no button
repeater, 3 obs iiiiiiiiiiiiiiiiiiii, ab, cd not clipped → no button not clipped → no button
repeater, 5 obs x" onmouseover="alert(1), a, b (+2 hidden) View all 5 → View all 5 →
  • [T] On 375×812, tapping Full names → opens the dialog. It lists MOUNT WOLFHAWK in full and stays inside the viewport, and Escape closes it.
  • [T] No console errors, page errors or horizontal overflow. Every span has exactly the attributes class,title, and [onmouseover] matches 0 elements.
  • [T] Card heights are 218–221 px on desktop and 246–248 px on mobile.

[T] Resize checks, without re-rendering:

  • I rendered at 1280 and resized to 375 → 320 → 1280, and rendered at 375 and resized to 1920 → 320. The invariant "button visible ⇔ preview hides observers OR a span is clipped" held for all five cards at every step.
  • I also swept 320–1920 px in 7 px steps after rendering at 375 and at 1280, with names at the 14ch boundary (aaaaaaaaaaaaaa, 00000000000000, bbbbbbbbbbbbbb, ssssssssssssss, Repeater Nord1). Measured clipped and the button disagreed only at the 1 px rounding artifact from N1 (908, 915, 1335 and 1342 px). There, screenshots show no ellipsis, so nothing is hidden from the user.

R2 — the XSS test gap: resolved in the test. [T] I ran mutant O9 myself (title="${obsFullName(o)}" without escapeAttr) on the merged tree. test-issue-304-my-mesh-e2e.js fails with observer span gained attributes: class,onmouseover,title. check-xss-sinks.sh still passes O9, both with --file public/home.js (rc=0) and with --diff against master on a scratch clone (rc=0). [A] The author's explanation holds. The O9 line contains no sink, because the sink is grid.innerHTML = cards.join(''). And even on a sink line, the attribute interpolation is erased before the audit, as N4 reproduces. A gate redesign is outside "only the requested changes", so the justification for not touching the gate is acceptable.

R3 — fraction-width-safe pre-filter: resolved. [K] The pre-filter now compares tx.FirstSeen < a.startSecond, where startSecond is the window start truncated to 2006-01-02T15:04:05, a fixed width with no fraction and no zone. [A] Any UTC stamp at or after the window start has a 19-character prefix ≥ startSecond. If the prefixes are equal, the stamp is longer and therefore sorts strictly greater. So no in-window stamp is dropped, whatever its fraction width. A stamp in the start's own second falls through to the exact time.Parse check. The cost is unchanged: one string compare per transmission, plus at most one extra second of stamps parsed.

  • [T] Red-before: TestNodeActivity24hPreFilterIgnoresFractionWidth run against 43ba651e's node_activity_24h.go fails all 5 subtests. It is green on head.
  • [T] My mutants N2 and N3 (see Mutants) are both killed.

R4 — stale comment: resolved. [K] The comment now names GetNodeHealth.

History / #352: intact. [T] Head is a plain fast-forward from 43ba651e: four single-parent commits, 7e573d21 → b8a62635 → 073783e2 → d971bbd3. The PR timeline has no head_ref_force_pushed event. b0392585, the base of #352, is still an ancestor of head. Author and committer on all 9 commits are dborup <kontakt@meshview.dk>.

Scope: no creep. [K] git diff 43ba651e d971bbd3 touches exactly four files: node_activity_24h.go, node_activity_304_test.go, public/home.js and test-issue-304-my-mesh-e2e.js (+128/−26). The changes are the R1 measurement, the R2/R1 E2E cases, the R3 pre-filter and test, and the R4 comment, plus the reported E2E :visible tweak. That tweak is needed because a hidden button now exists in the DOM. Nothing else changed: no CSS, no API, no workflow.

Always-checks

Check Result
Each claimed #304 criterion has a red-before / green-after test [T] R1: head's E2E against 43ba651e's home.js fails (card 5 at 1280px: clipped=["WWWWWWWWWWWWWW"] hides=false but button visible=false) and passes on head. R3: 5/5 subtests red on 43ba651e. R2: the test kills O9. Earlier criteria were verified red/green in round 2 and are unchanged.
No behaviour change beyond purpose [K] This round only changes when the observer-dialog button shows (now measured) and the pre-filter bound. Both are covered by the requested findings.
cmd/server read-only [K] This round has no DB access at all; the Go change is pure string/time computation
No new map[string]interface{} outside tests [T] Non-test Go: merge-base 727, 43ba651e 727, head 727. Master and the merged tree both have 745. Delta 0.
No hardcoded colours [T] No added hex, rgb or hsl colour in the round-3 diff. The only #NNN hits are #351 in comments. No CSS changed this round.
scripts/check-xss-sinks.sh --diff origin/master [T] rc=0 on a scratch clone at the merged tree, with master 689a68c5 as base. --file public/home.js also gives rc=0.
Fork guards 9 / 1 [T] Merged tree: deploy.yml 9, release-fast-path.yml 1, identical to master. The only workflow change in the whole PR is the added test-issue-304-my-mesh-e2e.js line.
No closing keywords [T] None in the title, body or any of the 9 commit messages
Commit author [T] All 9: dborup <kontakt@meshview.dk> (author and committer)

Tests I ran (merged tree 9e70dd00, master 689a68c5)

Suite Result
cd cmd/server && go vet ./... && go test -count=1 -timeout 60m ./... [T] ok
cd cmd/server && go test -race -count=1 -timeout 60m ./... (as in CI) [T] ok (682.8 s)
cd cmd/ingestor && go test -count=1 -timeout 60m ./... (no ingestor diff; run because the merged tree includes master changes) [T] ok (203.3 s)
sh test-all.sh [T] 234 passed, 0 failed (234 files)
node test-frontend-helpers.js [T] 712 passed, 0 failed
CHROMIUM_REQUIRE=1 node test-issue-304-my-mesh-e2e.js [T] pass
CHROMIUM_REQUIRE=1 node test-issue-2027-my-mesh-node-page-e2e.js [T] 25 passed, 0 failed
test-home-coverage-e2e.js against a local server [T] 12 passed, 0 failed
test-a11y-axe-1668.js against a local server [T] 0 violations across 120 cells
Browser probes against a local server (R1 table, resize, sweep) [T] see R1

The local server was corescope-server built from the merged tree. It used e2e-fixture.db prepared as in CI: freshen, the Kpa-clawbot#1486/Kpa-clawbot#1791 seed SQL, corescope-migrate, then seeds 2073 and 199 (and 245, as CI does). It logged coverage=93.8%, loaded=500, totalInDB=533, which is R6 from round 2: pre-existing, and the reason every real-server card shows "Activity unavailable". It ran on a free port and was stopped by looking up the listener by port. Neither known flaky test (#256, #267) appeared.

Mutants

# Mutant Result
O9 title="${obsFullName(o)}" without escapeAttr killed by the E2E (observer span gained attributes: class,onmouseover,title); survives the XSS gate (N4) [T]
N1 (mine) measure only the first preview name (querySelector instead of querySelectorAll(...).some) killed — E2E card 7 at 1280px: clipped=[…] hides=false but button visible=false (the Shorty + wide-names client card) [T]
N6 (mine) scrollWidth >= clientWidth (reveals on every card) killed — E2E (1 !== 0, a control card gains a button) [T]
N2 (mine) startSecond layout 2006-01-02T15:04:05.000Z (fixed ms fraction instead of whole seconds) killed — TestNodeActivity24hPreFilterIgnoresFractionWidth (1 ns stamp and .500001Z stamp dropped) [T]
N3 (mine) startSecond from end − 23h (bound one hour late) killed — three tests, including …RollingBoundaries… and …AddPreFilterKeepsBoundaryPackets [T]

All mutants were applied on a scratch clone at the merged tree and reverted with git checkout. Afterwards I checked that git status was clean.

CI

[T] Run 37736044834 (pull_request, head d971bbd3, attempt 1): Go Build & Test success (ok github.com/corescope/server 979.454s, 0 --- FAIL, ingestor ok). Playwright E2E Tests success: the log shows My Mesh #304: desktop/mobile card, sparkline, coverage and observer dialog passed. Build & Publish Docker Image success. Release Artifacts, Deploy Staging and Publish Badges & Summary were skipped (gated). [A] CI tests the PR merge ref, not a re-merge onto today's master; my merged-tree runs above cover that.

Not verified

@dborup
dborup merged commit 1f423e0 into master Oct 8, 2026
6 checks passed
dborup-agent pushed a commit that referenced this pull request Oct 8, 2026
Bring #351 rounds 2-3 (merged as 1f423e0) under #352's advert cadence.

Conflicts:
- public/home.js: keep master's clipped-name disclosure (title +
  revealClippedObserverButtons) and add #352's cadence block after the
  observer row.
- test-issue-304-my-mesh-e2e.js: keep master's eight mixed-role cards,
  wide/clipped names and the escapeAttr quote test; carry #352's
  advertIntervals/advertRouteBackfill fixtures on repeaters 0-2 and the
  opt-in health URL. Also pin that the no-estimate repeater shows no
  provisional badge and a non-repeater shows no cadence.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dborup-agent pushed a commit that referenced this pull request Oct 20, 2026
…retention floor (#509)

With hotStartupHours=0 the load floor is s.packets[0].FirstSeen. A loaded
re-heard row (old first_seen, recent last_seen) pulls it below the retention
floor, so the verification counts DB rows that were never meant to be
loaded and coverage stays unknown although every retained row is in
memory. No retention tick is needed; this is the default-config case
(#351 F1).

The second test adds concurrent ingest during the load and between the two
counts, and rows on both sides of the retention floor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

2 participants