Repository navigation
feat(compare): reference-to-many observer comparison over the full window (#122) - #450
Conversation
…ndow (#122) Relates to #122 Server: GET /api/analytics/observer-comparison computes exact packet overlap between one reference observer and up to 40 compared observers, from a single indexed scan over observations.timestamp (not a browser sample, not transmissions.first_seen). Direction is matched case-insensitively to RX only; TX and unknown/NULL never count. Bounded, cached (30s, #430-style ceiling + reject-before-work) and singleflighted. Frontend: compare.js gains a "One vs. many" mode alongside the existing two-observer workflow (unchanged), with a sortable comparison table, drill-down into the pairwise view, deep-linkable state, and a remembered reference observer. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…uery plan, document endpoint (#122) - server: reject an over-long ref / observer id with 400 before any lookup - compare.js: a reference remembered in localStorage is used only while that observer is still eligible (no error page for a vanished observer) - test: EXPLAIN QUERY PLAN on the real scan SQL asserts the (observer_idx, timestamp) index is used - docs: api-spec.md section for /api/analytics/observer-comparison with limits Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Rapport — CS-MacBook PR#450 afslutning — head 8c7970fReview feedback addressed (commit There were no review comments or findings posted on this PR (checked issue comments, inline comments and reviews), so I audited the #122 requirements myself. Findings, most important first:
Requirement verification
Disagreements / accepted limits (not changed)
Tests (red, then green)
Mutants (one per finding, each caught)
CI per job (head
PR remains draft. No merge, ready-for-review or issue closing. |
Rapport — CS-Minimax PR#450 #122 — head 8c7970fDom: APPROVE with nits Independent, read-only review. Note on the head SHA: this review is of Evidence tags: [T] test I ran myself · [A] code/diff analysis · [K] manual check against a locally built server. Findings
Nothing here blocks the PR. Findings 2–5 are missing tests and a stale comment around code that behaves correctly at head. Answers to the review pointsWas the a11y test weakening legitimate? Exact and explicit overlap semantics. Beyond the 10,000-packet cap, independent of pagination. There is no pagination and no packet-list cap anywhere in the path; the only bound is the 1 M-pair backstop, which fails the request closed instead of truncating [A]. Measured at scale: 11,250 unique transmissions for the reference through the real HTTP handler [T] — see finding 3 for the missing assertion. Validation before work (#430). Visibility. Compared observers that are unknown, inactive ( Perf and query plan. Cache. Keyed by Read-only and typed. The endpoint only Frontend. All new colours use existing tokens ( Invariants. Fork guards at head: The prior review. There is no review on this PR on GitHub — no reviews, no inline comments, no issue comments [A] — so I could not check the nits against it. The follow-up commit Tests and mutantsHead = merged tree (the branch already contains master), Go 1.26.0, darwin/arm64:
E2E against a locally built server on a CI-prepared
Live endpoint checks against the seeded fixture [K]: a valid 24 h request returns the full typed body with both asymmetric percentages; CI. Run My own mutants. 12 applied to the head tree, each reverted from a pristine copy afterwards. 10 killed, 2 survived.
For the meaningful survivor I wrote the missing test to prove it is a real gap and not an equivalent mutant: it is green at head and red with Left for the owner
What I did not verify
|
Review — CS-Macmini PR#450 — head 8c7970fDom: APPROVE med nits This is a follow-up commit ( Fund-tabel
Svar på punkterne (#122 krav, gennemgået enkeltvis)
Scope creepNone found. The follow-up commit touches exactly five files: the server validation/SQL-sharing, one Go test file, one doc file, Tests og mutanter kørt af mig
Repo-invarianter
Hvad jeg ikke har verificeret
|
Rapport — CS-MacBook PR#450 master-sync — head 1051237Review feedback addressed (commit
Tests (local, merged tree)
Mutants
CI per job (head
Rester
PR remains draft. No merge, ready-for-review or issue closing. |
Relates to #122
Summary
Extends the Compare page with a reference-to-many observer comparison, built on top of the existing two-observer workflow rather than a new page. It is descriptive only: no SNR/RSSI comparison and no health score/classification, matching the issue's explicit MVP scope.
GET /api/analytics/observer-comparison?ref=&hours=&observers=computes exact packet overlap between one reference observer and up to 40 compared observers, aggregated server-side from a single indexed scan overobservations.timestamp— not a 10,000-packet browser sample, and nottransmissions.first_seen(so a transmission re-observed inside the window counts even if first seen earlier).localStorage.Plan / what changed
Server endpoint (
cmd/server/observer_comparison.go)ObserverComparisonResponse,..Reference,..Window,..Row) — no newmap[string]interface{}.refrequired,hours∈ {1,6,24},observersrequired/bounded (1–40, must not includeref, raw param length capped before parsing).GetObservers/IsObserverBlacklistedalready apply elsewhere); an unknown/blacklisted reference is a 404.UPPER(direction) = 'RX'— matched case-insensitively, but NULL/TX/anything else is excluded. This matters in practice: the committede2e-fixture.dbhasdirection = NULLon every row (it predates that field), so without a seed the endpoint correctly reports "no data" rather than guessing.SELECT DISTINCT observer_idx, transmission_id, so a transmission observed via multiple paths by one observer counts once.(observer_idx, timestamp)index (migrationobs_observer_ts_idx_v1, already present), then two linear passes in Go to build per-observer shared/total counts — O(matched rows), not O(observers × packets). A hardobserverComparisonScanRowLimit(var, lowerable in tests) fails the request closed with an explicit error rather than silently truncating the aggregate population.observerComparisonCache, mirrorschannelListCache/nodeAdvertRouteCache), singleflighted per(ref, hours, sorted other ids).cmd/server/openapi.go(TestOpenAPICompletenesswas red until this was added — see CI below).Frontend (
public/compare.js,public/style.css,public/app.js)[data-mode-btn]), bound once on the stable.compare-pageroot ininit()— not re-bound per render, so no listener stacking (fix(ui): stop observers.js leaking delegated handlers on #app (#405) #419/fix(ui): stop compare/channels/live leaking handlers on #app (#428) #437 precedent followed, not re-litigated).tr[data-action="navigate"]delegated handlers.pairHours: the existing two-observer view's hardcoded 24h packet-fetch window is now parameterized from?hours=, so a drill-down from a 1h/6h reference comparison opens the pairwise detail at the same window. Legacy#/compare?a=&b=links carry nohours=and are unaffected.--accent,--text-muted,--space-*,--fs-*, …) — no hardcoded hex.scripts/check-xss-sinks.sh --diff origin/masteris clean.Tests
cmd/server/observer_comparison_test.go— pure-function table tests (empty/zero/perfect/partial/asymmetric overlap, the issue's own 1000/800/600 → 60%/75% example scaled down and matched exactly), cache TTL/ceiling-eviction/singleflight, request validation, RX/TX/unknown-direction exclusion, dedup across paths, window-uses-observation-timestamp-not-first_seen, exact[start,end)boundaries (at the DB-scan level, so it doesn't depend on wall-clock timing), visibility filtering, v3-schema guard.observer_comparison_perf_test.go, AGENTS.md rule 0 proof): 30,000 transmissions / 8 observers / ~90K RX observation rows, full comparison via the real HTTP handler, budgeted well above the measured-racetime (CI runs-raceforcmd/server) — see "Perf proof" below.test-issue-122-compare-reference-many.js(sort/format/other-ids helpers, including the "null percentage sorts last in both directions, never as 0" case).test-issue-122-compare-reference-many-e2e.js, registered inscripts/e2e-shards.json(shard 3) — mode selection, window change + stale-response race, sorting, drill-down, reload/deep-link restore, empty state, narrow viewport. A new fixture seed (test-fixtures/seed-122-observer-comparison.sql, wired intodeploy.yml) gives the E2E fixture real (non-null-direction) overlap data, since the committed fixture has none.Perf proof (AGENTS.md rule 0)
Dataset: 30,000 transmissions, 8 observers (1 ref + 7 compared), ~90,000 RX observation rows (
TestObserverComparisonPerf_RealisticFixtureEndToEnd).-race-race(CI'scmd/serverconfig)The committed budget (12s) sits well above the real implementation's measured
-racetime and below the mutant's, so CI noise can't make a correct change flaky while a regression toward per-observer queries would still fail it. A second test (TestComputeObserverComparison_ScalesLinearlyInObservers) asserts the pure Go aggregation scales linearly in input size as the compared-observer count grows from 4 to 40 at a fixed per-observer workload.Known limitation
The server endpoint caps compared observers at 40 per request (
observerComparisonMaxObservers) and returns a 400 past that, rather than silently truncating. On an install with more eligible observers than that, the page currently can't show all of them in one reference-to-many table — the per-pair detail drill-down still works for any individual pair. Flagging as a known gap, not addressed in this PR.Not in scope (per issue)
SNR/RSSI delta analysis, automatic health classification/alerting, multi-reference consensus, map overlays, 7-day windows, historical radio-configuration tracking.