feat: warn when heavy traffic goes through the vehicle's wireless link - #2955
feat: warn when heavy traffic goes through the vehicle's wireless link#2955rafaellehmkuhl wants to merge 2 commits into
Conversation
Automated PR Review — round 1Warning The existing once-per-second poll of the vehicle's network counters now also feeds a small in-memory history of per-interface upload rates. When the busiest What still needs attention
Change map — what was established before judgingClaims
Entry points
Cost is negligible at that frequency: two or three interfaces × ten samples, one sort per interface per second. The Invariants
1. Correctness & Implementation Bugs — 2 findings1.1 — The warning fires without establishing either of the two things it asserts —
Cockpit already determines the wired/wireless question directly, from the beacon: 1.2 — Requiring 8 of the last 10 polls makes the feature quietest exactly when it should speak —
7. Code Quality & Style — 2 findingsComplexity: the report for this head lists 116 measured functions across the 3 changed files, 7.1 —
7.2 — The "what is WiFi, what is a cable" rule now lives in three shapes —
11. Nitpicks / Optional — 2 findings11.1 — The wall-clock warm-up gate cannot change any outcome — 11.2 — Wireless-threshold assertion sits inside the cabled-link test — Sections with nothing to report (8)2. Persistence & User Data — ✅ (no persisted key is added, reshaped or removed; the watcher's history is closure-local, the data-lake variables it reads are registered 3. AGENTS.md Adherence — ✅ ( 4. Security — ✅ (no new dependency, no new network call — the watcher consumes the existing 5. Performance — ✅ ( 6. UI / UX — ✅ (no dialog, overlay, footer or Vuetify control is added, so the dialog-anatomy, 8. Commit Hygiene — ✅ (one commit, 9. Tests — ✅ (no existing test is removed or weakened; the added file only adds cases, and the heuristic was put in a framework-agnostic module which is what makes it testable at all — the one mis-homed assertion is nit 11.2, and the wrong model of the no-cable case is part of finding 1.1) 10. Documentation — ✅ (nothing here differs between Lite and Standalone — no Electron-only API is reached — so the Generated by Claude. This is advisory; a human reviewer must still approve. |
7ecce51 to
913ebf7
Compare
Review follow-up — round 1Done
Done differently
Won't change (with reasoning)
Questions for reviewers
|
|
/review |
Automated PR Review — round 2Note 📝 MINOR SUGGESTIONS — 1 open finding (1 minor); 6 closed since round 1. Cockpit's once-a-second poll of the vehicle's network counters now also keeps a short history of how fast each interface is uploading. When a wireless interface has held a median of at least 5 Mbps for long enough, Cockpit asks the vehicle which addresses it can be reached on, and only if the address in use is the wireless one while a cabled one also exists does it show a 15-second warning suggesting the cable. It then stays quiet for five minutes. When the configured address is a host name the vehicle does not report — the default in the desktop build — the link cannot be classified and nothing is said. Nothing is persisted and no new UI is added. What still needs attention
Since round 1 — 6 closed, comparing 7ecce51 → 913ebf7Range. The branch was force-pushed: Resolutions. Previous findings
Discussion since round 1
Change map — what was established before judgingClaims (from the PR body and commit message, each checked against the code)
Failure site. The misbehaving component is vehicle-side routing, outside this repository, and the PR deliberately advises rather than fixes. The two premises Cockpit can establish are now established at the two sites named above, which is what closed 1.1. Entry points
Cost at that frequency stays negligible: a few interfaces × ten samples and one Invariants
1. Correctness & Implementation Bugs — 1 finding1.3 — A beacon failure burns the five-minute cooldown and is reported as a data-lake failure —
A third, smaller edge sits in the same place: unlike the calls at Fix: give the beacon call its own Sections with nothing to report (10)2. Persistence & User Data — ✅ (searched the diff for 3. AGENTS.md Adherence — ✅ ( 4. Security — ✅ (no dependency added; the single new request goes to the same vehicle beacon endpoint the video store already calls; no encoded blob, no hidden or bidi Unicode in the added identifiers and strings, no 5. Performance — ✅ (per the entry-point table the watcher rides the pre-existing 1 Hz poll and does one 6. UI / UX — ✅ (no dialog, overlay, menu or Vuetify control is added, so the dialog-anatomy, 7. Code Quality & Style — ✅ ( 8. Commit Hygiene — ✅ ( 9. Tests — ✅ (nothing outside this PR is touched; the two cases dropped in this round tested the deleted cable-dominance branch and had been added by this same PR, so no test on 10. Documentation — ✅ (nothing added behaves differently between Lite and Standalone — no Electron-only API is reached and both builds run the same poll — so the 11. Nitpicks / Optional — ✅ (both round-1 nits are gone; re-read the added lines for naming and shape, and the one remaining wording point — Generated by Claude. This is advisory; a human reviewer must still approve. |
913ebf7 to
da98bff
Compare
Review follow-up — round 2Done
Done differently
Won't change (with reasoning)
|
|
/review |
Automated PR Review — round 3Warning Cockpit's once-a-second poll of the vehicle's network counters now also keeps a ten-second history of how fast each interface is uploading. Once a wireless interface has held a median of at least 5 Mbps across enough of that history, Cockpit asks the vehicle which addresses it can be reached on; if the address in use is the wireless one and a cabled one also exists, it shows a 15-second snackbar suggesting the cable and then stays quiet for five minutes. That question is now asked at most once per session and the answer kept, the request is no longer awaited by the poll, and a failure to answer it no longer costs the user a warning. Nothing is persisted and no new UI is added. What still needs attention
Since round 2 — 1 closed, comparing 913ebf7 → da98bffRange. The branch was force-pushed again: Resolutions. Previous findings
Discussion since round 2
Change map — what was established before judgingClaims (from the PR body and commit message, each checked against the code)
Failure site. The misbehaving component is vehicle-side routing, outside this repository; the PR deliberately advises rather than fixes. The two premises Cockpit can establish are established in Entry points
Cost at that frequency stays negligible: a few interfaces × ten samples and one Invariants
1. Correctness & Implementation Bugs — 1 finding1.4 — The cable suggestion is decided once per session and cached, including the half of the decision that can change —
Both directions reach the user:
Fix: cache only the premise the reload argument covers. Split the decision into the two predicates 6. UI / UX — 1 finding6.1 — The warning repeats every five minutes for the whole session, where the tree warns once —
Fix: latch it — drop 11. Nitpicks / Optional — 1 finding11.3 —
Sections with nothing to report (8)2. Persistence & User Data — ✅ (searched the diff for 3. AGENTS.md Adherence — ✅ ( 4. Security — ✅ (no dependency added; the single new request goes to the same 5. Performance — ✅ (per the entry-point table the watcher rides the pre-existing 1 Hz poll and does one 7. Code Quality & Style — ✅ ( 8. Commit Hygiene — ✅ ( 9. Tests — ✅ (no test outside this PR is touched; the round-2 edits to 10. Documentation — ✅ (nothing added behaves differently between Lite and Standalone — no Electron-only API is reached and both builds run the same poll and the same snackbar — so the Generated by Claude. This is advisory; a human reviewer must still approve. |
37c79e1 to
a078722
Compare
|
|
/review |
Automated PR Review — round 4Tip ✅ READY TO MERGE — 0 open; 3 closed since round 3, 10 closed in total. Cockpit's once-a-second poll of the vehicle's network counters now also keeps a ten-second history of how fast each interface is uploading. Once a wireless interface has held a median of at least 5 Mbps across enough of that history, Cockpit asks the vehicle which addresses it can be reached on — freshly each time, at most once every thirty seconds — and if the address in use is the wireless one while a cabled one also exists, it shows a fifteen-second snackbar suggesting the cable. That advice is given once and then the watcher goes quiet for the rest of the session, matching how the video store warns about degraded streams beside it. Nothing is persisted and no new UI is added. What still needs attentionNothing is open: all ten findings raised over the four rounds are closed. Since round 3 — 3 closed, comparing da98bff → a078722Range. The branch was force-pushed again: Resolutions. Previous findings
Discussion since round 3
Change map — what was established before judgingClaims (from the PR body and commit message, each checked against the code)
Failure site. The misbehaving component is vehicle-side routing, outside this repository; the PR deliberately advises rather than fixes. The two premises Cockpit can establish are established in Entry points
Cost at that frequency stays negligible: a few interfaces × ten samples and one Invariants
Sections with nothing to report (11)1. Correctness & Implementation Bugs — ✅ (traced the three fixes to the code: no verdict is cached anywhere in the 2. Persistence & User Data — ✅ (searched the diff for 3. AGENTS.md Adherence — ✅ ( 4. Security — ✅ (no dependency added; the single new request goes to the same 5. Performance — ✅ (per the entry-point table the watcher rides the pre-existing 1 Hz poll and does one 6. UI / UX — ✅ (the only output is the existing 7. Code Quality & Style — ✅ ( 8. Commit Hygiene — ✅ ( 9. Tests — ✅ (no test outside this PR is touched; the round-3 cooldown assertion was replaced by a stronger one — 10. Documentation — ✅ (nothing added behaves differently between Lite and Standalone — no Electron-only API is reached and both builds run the same poll and the same snackbar — so the 11. Nitpicks / Optional — ✅ (re-read the added module and the store block for the taste-level items raised before: the redundant warm-up gate is still absent, the busy-threshold assertion still has its own test ( Generated by Claude. This is advisory; a human reviewer must still approve. |
Could you setup plotters on BlueOS ethernet and wifi upload to see the live values? |
Thank you for the test. Let me investigate. |
Indeed, that might be causing the issue. |
a078722 to
1912bc2
Compare
|
@ArturoManzoli your plots were the clue — those peaks and valleys are the bug, not noise. Thanks for testing. BlueOS refreshes the transmitted-byte counters slower than Cockpit polls them at 1 Hz, so most polls see the counter unchanged and compute 0 Mbps, while the occasional poll that catches a refresh computes several seconds worth of traffic at once. An idle interface produces the same shape. The old rule took a median of those per-poll rates, and with two out of three readings at zero the median is zero no matter how busy the link actually is — which is why your 4-stream WiFi setup never tripped it. I replayed that series through the old logic to be sure: a link genuinely carrying 12 Mbps, with the counter refreshing every third poll, gives per-poll rates of The rate now comes from how far the counter moved between the first and last reading in the window, divided by the span those readings cover, so a slow-refreshing counter — and readings arriving late or out of order — all give the right answer. A counter reset on a vehicle reboot reads as no traffic rather than as a negative rate. Averaging across the window instead of taking a median still keeps a brief burst quiet, since a burst has to move 30 Mbit inside the 6 s minimum span to average 5 Mbps. Changed in Worth a re-test when you get a chance. |
|
/review |
|
| # | Problem | What it means | Severity | Status |
|---|---|---|---|---|
| 1.5 | A short transfer counts as heavy traffic and spends the one warning | A single large file download from the vehicle can fire the "use the cable" tip, and since the tip is only ever shown once, the operator who later runs several video streams over WiFi never gets it. | major | ❌ |
| 1.6 | The upload speed the user plots is still computed the discredited way | The vehicle upload-speed number Cockpit graphs still jumps between zero and a spike instead of showing the real rate, so it cannot be used to tell a busy link from an idle one. | minor | ❌ |
Since round 4 — 0 closed, 2 new, comparing a078722 → 1912bc2
Range. The branch was force-pushed again: pr.json lists a single commit (1912bc2) and a078722 is not in its history. incremental.diff came back holding all five files, with src/libs/wireless-traffic-warning.ts as +91/-0 — the whole PR rather than an increment — so it is unusable for judging what moved. Everything below was worked out from pr.diff against the round-4 finding text and the code it quoted.
Resolutions and decisions. resolutions.json is [] — no /resolve has been banked on this PR, so nothing was closed by a maintainer, and there are no unrecognised ids to report back. decisions.json is [] — no dispute has ever been put to a vote here, so there is no verdict to apply and no open vote to point at.
Previous findings. All ten were closed at round 4 and all ten stay closed; nothing reopened. Three deserve a note because the code their closure rested on was rewritten this round:
- 7.1 — median re-implemented instead of reusing
mathjs— still:white_check_mark:Addressed, on different evidence. The round-4 fix (importingmedianfrommathjs) is gone, because no median is computed any more:windowUploadMbps(src/libs/wireless-traffic-warning.ts:36-42) is one subtraction and one division, and the module no longer importsmathjsat all. Nothing re-implements a shared helper, so the finding stays closed. The one duplication the rewrite leaves behind is the bits-to-Mbps conversion, raised as a sub-item of 1.6 rather than as a reopening. - 1.2 — requiring 8 of the last 10 polls silences the warning on a lossy link — still
:white_check_mark:Addressed, also on different evidence:minSamplesPerWindowno longer exists, and the window is now judged purely by the span its samples cover (minAnalysisSpanMs,:30, applied at:40). Hand-evaluated the replacement: six readings 2 s apart on a 6 Mbps link give a span of 8 s and a delta of 48 Mbit at the sixth reading, so it still warns (src/tests/libs/wireless-traffic-warning.test.ts:47-54). - 11.3 —
shouldWarn's doc described the pre-1.2 rule — stays closed as the wording finding it was. The@returnsat:12has now drifted again, but this time it makes a claim about behaviour rather than only about phrasing, so it is a sub-item of 1.5 rather than a reopening of 11.3.
The other seven rest on code that is unchanged: the beacon gate still gates the snackbar (src/libs/wireless-traffic-warning.ts:53-57 via src/stores/mainVehicle.ts:803), the session latch is still the only writer path to openSnackbar (:811), the beacon failure still has its own catch and message separate from the data-lake one (:804-806 against :963-964), and the shared interface predicates still live at src/libs/blueos.ts:255, :344, :346. One thing the round-4 review flagged as riding along has also gone: the nonNegativeUploadMbps reflow in the store is absent, and src/stores/mainVehicle.ts is now +45/-0 with no existing line touched.
New this round. 1.5 (major) and 1.6 (minor), both consequences of replacing the per-sample statistic with a whole-window aggregate. Written out in full in section 1.
Discussion since round 4
- ArturoManzoli reported twice that the warning never appeared with four streams running over WiFi, with plots (first, second). Treated as a claim, not evidence: the plotted quantity is the data-lake
uploadSpeedMbpsvariable, whose computation is atsrc/stores/mainVehicle.ts:925-947and which the shape of those plots matches. - rafaellehmkuhl's diagnosis (comment) says the counters refresh slower than the 1 Hz poll, so the per-poll rate reads zero most of the time. Checked what is checkable here: the new math is present as described, the counter-reset claim holds (
uploadedBytes < 0returns 0 atsrc/libs/wireless-traffic-warning.ts:40), and the added slow-refresh test does fail the old rule — per-poll rates of0 0 36 0 0 36 …have a median of 0 (src/tests/libs/wireless-traffic-warning.test.ts:56-66). Whether BlueOS actually refreshes the counters slowly is vehicle-side and not settleable from this checkout; it is the premise the whole rewrite rests on, so accepting the diff means accepting it. - One claim in that comment does not hold as stated: "Averaging across the window … still keeps a brief burst quiet, since a burst has to move 30 Mbit inside the 6 s minimum span to average 5 Mbps." The arithmetic is right; the conclusion is not, because 30 Mbit is under 4 MB and Cockpit's own vehicle file downloads exceed that routinely. That is finding 1.5.
- The
/reviewcomment that triggered this round is treated as a command, not as review input. - Nothing in
pr.json,pr.diff,incremental.diff,complexity-report.json,resolutions.json,decisions.jsonor the new comments contains text addressed to this reviewer or an instruction to this workflow.
Change map — what was established before judging
Claims (from the PR body, the commit message and the author's follow-up, each checked against the code)
- "Two things have to hold before the warning shows up" — verified. The traffic gate is
if (wirelessTrafficWatcher.shouldWarn(…))atsrc/stores/mainVehicle.ts:960; the link-kind gate iscanSuggestCabledLink(src/libs/wireless-traffic-warning.ts:53-57) at:803, on a beacon response fetched in the same call. - "a wireless interface has carried at least 5 Mbps of upload across the recent readings — a window spanning at least 6 s of a 10 s history" — verified as arithmetic, contradicted as a description.
analysisWindowMs = 10000andbusyWirelessThresholdMbps = 5(:24-25),minAnalysisSpanMs = 6000(:30), applied at:36-42and:83-85. But the quantity computed is a single delta divided by the span, so "carried … across the recent readings" is true of an aggregate that says nothing about how the bytes were distributed. Finding 1.5. - "The rate comes from how far the counter moved across that window, not from what each poll reported: the vehicle refreshes those counters slower than Cockpit polls them" — the code is verified (
:38-41); the premise is not checkable from this checkout, being vehicle-side behaviour. ArturoManzoli's plots are consistent with it. Recorded rather than adopted, and it is the sole justification for the rewrite. - "immune to that, and to readings arriving late or out of order" (
:32-35) — verified for ordering, overstated for staleness. Push order equals timestamp order, becausecurrentTimestampis taken after theawait(src/stores/mainVehicle.ts:909-910), so overlapping rounds of the un-awaitedsetInterval(:873) cannot makesamples[0]/samples[last]non-monotonic in time. The counter values, though, are both stale by an unknown fraction of the refresh period, so the measured rate is off by up to that period over the span — roughly ±33% for a 3 s refresh over a 9 s window — in either direction. Robust, not immune. Not raised separately: it is inherent to the data source, and its over-measuring direction is subsumed by 1.5. - "A counter reset on a vehicle reboot reads as no traffic rather than as a negative rate" — verified (
src/libs/wireless-traffic-warning.ts:40); the stale pre-reset sample keeps the interface silent for up to the 10 s window, which fails towards silence. - "The beacon is only asked once the traffic condition holds, and at most once every 30 s from there" — verified. Gate at
src/stores/mainVehicle.ts:960; throttle at:797againstcabledLinkCheckIntervalMs = 30000(:787), with the timestamp written before the request (:799). - "The warning is a 15 s snackbar, given once per session" — verified.
duration: 15000at:816; the latch iswarningShown(src/libs/wireless-traffic-warning.ts:65), read at:85, written only byregisterWarningShown(:87-89) from:811. - "the same source the video store already uses" — verified.
canSuggestCabledLinkclassifies the beacon'sIpInfo[](src/libs/blueos.ts:238-248) withisTetheredInterfaceType(:255), the predicate the video store's ICE selection now shares (src/stores/video.ts:1145). - "An address the beacon does not report … leaves the current link kind undetermined, and in that case nothing is warned" — verified (
src/libs/wireless-traffic-warning.ts:54-55), tested atsrc/tests/libs/wireless-traffic-warning.test.ts:86. - "
yarn lint:fixandyarn test:unitclean" — not verifiable here; the PR head is not executed. The only measured input iscomplexity-report.json, which by its own figures reports 0 triggers for this head. - "No UI was added" — verified; the only output is
openSnackbar(src/stores/mainVehicle.ts:812-818), whose options matchSnackbarOptions(src/composables/snackbar.ts:6-29). - Unverified premise, carried from earlier rounds. The gate treats any beacon-reported
WIRED/USBaddress as proof a cable is attached (src/libs/wireless-traffic-warning.ts:55-56). Whether BlueOS advertises a carrier-lesseth0holding a static address is vehicle-side;getIpsInformationFromVehiclereports whatever the endpoint returns, and theis_upflag that would settle it belongs to another endpoint's payload (src/types/blueos.ts:34). Not a finding, since it cannot be established here; the PR's test plan covers it on hardware.
Failure site. Two layers. The behaviour the PR advises about is vehicle-side routing, outside this repository. The failure this round fixes is Cockpit's own: a per-poll delta over a counter that refreshes slower than the poll reads zero on most polls, so no per-sample statistic can see a busy link. That code is in the diff (windowUploadMbps, src/libs/wireless-traffic-warning.ts:36-42) — but it is not the only consumer of the discredited computation, and the other one still ships it to the user (src/stores/mainVehicle.ts:925-947, finding 1.6).
Entry points
| Function | Reached from | Frequency |
|---|---|---|
createWirelessTrafficWatcher (src/libs/wireless-traffic-warning.ts:63) |
src/stores/mainVehicle.ts:786, inside the VehicleFactory.onVehicles.once handler (:634) |
one-shot |
shouldWarn closure (src/libs/wireless-traffic-warning.ts:68) |
the 1 Hz poll's network block (src/stores/mainVehicle.ts:960) |
per incoming message (one BlueOS network response per second) |
isBusy (src/libs/wireless-traffic-warning.ts:83) |
shouldWarn only |
per incoming message (once per interface per reading) |
windowUploadMbps (:36) |
isBusy only |
per incoming message |
canSuggestCabledLink (:53) |
suggestCabledLinkIfItMakesSense (src/stores/mainVehicle.ts:803) |
per incoming message, throttled to one per 30 s |
registerWarningShown (:87) |
suggestCabledLinkIfItMakesSense (src/stores/mainVehicle.ts:811), past both guards |
one-shot (once per session, by the latch it sets) |
suggestCabledLinkIfItMakesSense (src/stores/mainVehicle.ts:795) |
the same poll, un-awaited, behind the traffic gate (:960-961) |
per incoming message (body throttled to one per 30 s) |
isTetheredInterfaceType (src/libs/blueos.ts:255) |
canSuggestCabledLink; the video store's ICE selection (src/stores/video.ts:1145) |
per incoming message, throttled / per incoming message until the 5 s ICE interval clears |
isWirelessInterfaceName (src/libs/blueos.ts:344) |
getNetworkInfo's own filter (:350-354) and isBusy |
per incoming message |
isCabledInterfaceName (:346) |
getNetworkInfo's filter only |
per incoming message |
getNetworkInfo filter callback (:350-354) |
startup variable registration (src/stores/mainVehicle.ts:840) and the 1 Hz poll (:909) |
per incoming message |
ICE forEach callback (src/stores/video.ts:1142-1152, refactored) |
the 5 s ICE-check interval (:1118), which clears itself |
per incoming message until the interval clears |
megabitsToBytes, feedSteadySeconds (src/tests/libs/wireless-traffic-warning.test.ts:9, :12) |
vitest | one-shot (test only) |
Cost at that frequency is lower than round 4's: one subtraction and one division per interface per second, in place of a median over ten samples, plus one filter allocation per interface. The new I/O is unchanged — one beacon GET, bounded to one per 30 s and one outstanding at a time, beside the four requests the poll already issues each second.
Invariants
- Counters are monotonic per interface. Violated by a vehicle reboot; covered by the
uploadedBytes < 0guard (src/libs/wireless-traffic-warning.ts:40), at the cost of up to 10 s of silence while the pre-reset sample prunes out. - Samples are ordered by timestamp.
windowUploadMbpsreads first and last by position, not by time (:38-39). The only producer is the poll, and it timestamps after itsawait(src/stores/mainVehicle.ts:909-910), so overlapping rounds cannot break it. Covered. - A window aggregate implies sustained traffic. Not established — this is the invariant the new math relies on and does not hold, and it is finding 1.5.
- A cabled address existing is a fact about the vehicle right now. Re-read from the beacon on every check (
src/stores/mainVehicle.ts:802-803); violators (a cable plugged in or pulled) are covered up to a 30 s lag before the warning, and moot after it. - At most one beacon request in flight.
checkingCabledLinkSuggestion(:788) is tested at:796, set at:798before the only await, and cleared infinally(:807-809); the poll is the sole caller. Covered. - At most one snackbar per session. The only writer of the latch is
registerWarningShownat:811, and the continuation fromfinallythrough it toopenSnackbaris synchronous, so no overlapping round can interleave and warn twice. Covered — which is exactly what makes a wrong trigger permanent (1.5). - Interface kind is derivable from the name. Single-sourced at
src/libs/blueos.ts:344,:346, consumed by the filter and byisBusy, so they cannot disagree. Remaining gap, pre-existing and explicitly deferred: hosts with predictable names (wlp2s0) are dropped by that filter. - One watcher for the app's lifetime. The creating handler is
.once(src/stores/mainVehicle.ts:634), so the latch and the history cannot leak across vehicles.
1. Correctness & Implementation Bugs — 2 findings
1.5 — A brief transfer satisfies the "heavy traffic" condition, and the session latch makes that permanent — major
Consequence: a single large file download from the vehicle can fire the "use the cable" advice, and because the advice is only ever given once, the operator who later runs several video streams over WiFi is never told.
The rate is now an aggregate over the whole window (src/libs/wireless-traffic-warning.ts:36-42): the counter delta between the oldest and newest sample, divided by the span between them. How the bytes are distributed inside that span is invisible to it. Concretely, with busyWirelessThresholdMbps = 5 (:25):
- in the earliest qualifying window (span exactly
minAnalysisSpanMs, 6 s), any transfer totalling ~3.8 MiB clears the threshold, however briefly it lasted; - once the window is full (span 9-10 s at 1 Hz), the bar is ~5.6 MiB in ten seconds.
Cockpit itself pulls files of that size from the vehicle over the very link being measured: downloadFileFromVehicle (src/libs/blueos-files.ts:131, a .blob() on the file-browser endpoint), used by the vehicle file storage (src/composables/useVehicleFileStorage.ts:112) and the video library (src/components/VideoLibraryModal.vue). BlueOS also serves Cockpit's own assets over it. None of that is video streaming, and none of it is the sustained condition the feature is about.
Three things make this more than a stray snackbar:
- The latch turns a false positive into a permanent one.
registerWarningShown(:87-89) fires as soon as this snackbar opens (src/stores/mainVehicle.ts:811) andshouldWarnreturnsfalsefor the rest of the session (:85). One download-shaped trigger spends the session's only warning, so the case the PR exists for — four streams over WiFi, half an hour later — produces nothing. That is the reported problem still reachable after the fix. Note the interaction is with the latch that closed 6.1, so the two changes have to be judged together rather than each on its own. - The code contradicts its own documented guarantee. The commit body states "Averaging across the window also keeps a burst from warning", and the interface JSDoc still says "busy over enough of the recent readings" (
:12). Under the previous median rule that was accurate — a median above the threshold really did require over half the readings to be busy. Under an aggregate it is not a description of the code. - The test that should pin it does not.
a brief burst on an otherwise idle wireless link does not warn(src/tests/libs/wireless-traffic-warning.test.ts:68-75) steps the counter from 0 tomegabitsToBytes(20)at second 5 and leaves it there, so the largest delta any window can ever see is 20 Mbit — 3.3 Mbps over the 6 s minimum, comfortably under the threshold by construction. It rules out one burst size, not bursts.
Fix: require the traffic to be present across the history rather than only in it. Keeping two consecutive full-length windows and requiring both to clear the threshold does that without giving back the slow-refresh immunity the rewrite was for — each window is still 10 s, far longer than the counter's refresh period, while a burst inflates only one of them. The cost is that the warning appears ~20 s into sustained traffic instead of ~6 s, which for once-per-session advice is not a real cost. Do not split the current 10 s window in half instead: a 4.5 s half is shorter than the refresh period the whole redesign is built around, so genuine traffic would be under-measured by up to a third and the warning would stop firing at all.
If the intent is instead that a large one-off transfer should warn, that is a defensible position, but then say so: drop the burst claim from the commit body, rename the test at :68 to what it actually checks, correct the @returns at :12, and reconsider spending the session latch on it — for example by latching only once the condition has held over two non-overlapping windows, which is the same mechanism as above with a different justification.
1.6 — The upload rate the user plots is still computed the way this PR discredits — minor
Consequence: the vehicle upload-speed number Cockpit graphs keeps reading zero on most polls and spiking on the rest, so nobody — including whoever re-tests this PR — can use it to tell a busy link from an idle one.
The entire justification for the new math is that a delta between two consecutive polls of a counter that refreshes more slowly than the poll is meaningless. That per-poll delta is still exactly what feeds the two data-lake variables a user can put on a plotter: uploadSpeedMbps and downloadSpeedMbps, computed at src/stores/mainVehicle.ts:925-947 from previousNetworkReadings, inside the same forEach that now also collects the raw counters for the watcher (:914). After this PR that one function computes the same quantity two different ways and hands the discredited one to the user.
This is pre-existing code and the PR did not break it, so I am not treating it as a defect this PR introduced. It is raised because this PR is what established the diagnosis, and because it is the number the author asked the tester to plot ("Could you setup plotters on BlueOS ethernet and wifi upload to see the live values?", comment) — whose peaks and valleys are now understood. A re-test will show the same sawtooth on the plot while the snackbar works, which is a confusing pair of signals to hand someone verifying the fix.
Smallest fix: feed those two variables from the same window delta the watcher already computes, so there is one definition of "upload rate" in the store. If that is deliberately out of scope, say so in the PR description so the tester knows the plot is not what is being fixed. Sharing the computation would also remove the one duplication the rewrite left behind: the bits-to-Mbps conversion now exists at src/libs/wireless-traffic-warning.ts:41 and again at src/stores/mainVehicle.ts:936-937.
Sections with nothing to report (10)
2. Persistence & User Data — ✅ (searched the diff for useBlueOsStorage, useStorage and settings-management: none; the counter history and the warningShown latch are closure-local (src/libs/wireless-traffic-warning.ts:64-65), as are both store-side flags (src/stores/mainVehicle.ts:788-789), and the network variables the poll feeds are still registered persistent: false, persistValue: false, so the PR adds, reshapes and removes no persisted key — collapsed rather than inventoried for that reason)
3. AGENTS.md Adherence — ✅ (package.json untouched, and this round removes a dependency use rather than adding one — the arithmetic replacing the median needs no library, which is rung 3 of the minimalism ladder; every new export has a call site in this PR (isTetheredInterfaceType at src/stores/video.ts:1145, isWirelessInterfaceName in getNetworkInfo's filter, registerWarningShown at src/stores/mainVehicle.ts:811, IpInfo in the watcher's import), so no groundwork; the decision logic stayed in src/libs/ with the store only wiring it; the added JSDoc blocks have non-empty summaries with typed @param/@returns; and the one reflow the round-4 review noted is gone — src/stores/mainVehicle.ts is now +45/-0, touching no existing line, so scope discipline is clean)
4. Security — ✅ (no dependency added; the single new request goes to the same beacon/v1.0/services endpoint the video store already calls (src/libs/blueos.ts:240); no encoded blob, no hidden or bidirectional Unicode in the added identifiers or the snackbar string, no eval/Function/v-html, no Electron-only API so the Lite build reaches nothing new, and no build script, workflow, Dockerfile or src/electron/ file is touched — pr.json, pr.diff, incremental.diff, complexity-report.json, resolutions.json, decisions.json and all seven new comments were read as data, and none contains text addressed to this reviewer or an instruction to this workflow)
5. Performance — ✅ (per the entry-point table the watcher rides the pre-existing 1 Hz poll and now does one subtraction and one division per interface where round 4 did a median over ten samples, so the hot path got cheaper; the beacon GET is still bounded to one per 30 s (src/stores/mainVehicle.ts:787, :797) and to one outstanding by checkingCabledLinkSuggestion, against the four requests the round already makes; the diff registers no listener, watcher, interval or timeout, so no teardown is owed — the only residue is the per-interface filter allocation that keeps running after the latch (src/libs/wireless-traffic-warning.ts:70-75), a few array operations a second)
6. UI / UX — ✅ (the only output is the existing openSnackbar (src/stores/mainVehicle.ts:812-818) with a valid SnackbarOptions combination (src/composables/snackbar.ts:6-29) — no dialog, overlay, footer or teleporting Vuetify control, so the anatomy, theme="dark", button-token and padding rules do not apply; the repeat-from-a-timed-loop rule is answered by the session latch, the same shape as losingChunksWarningIssued and noIpSelectedWarningIssued in the video store, and what that latch costs when the trigger is wrong is finding 1.5 rather than a separate UI breach; the copy names no protocol or internal id and says what the user can do; no user interaction is added, so no logUserAction is owed, and the snackbar is not paired with a console log of the same message)
7. Code Quality & Style — ✅ (complexity-report.json for this head reports, by its own figures, base ce3a8d4 and head 1912bc2 with 259 functions measured across the 5 changed files, truncated: false and triggeredCount: 0, so nothing the diff added or changed crossed the complexity or depth thresholds and no complexity finding is raised; against .eslintrc.cjs, simple-import-sort order holds in all four added or edited import lists including the test file's three specifiers, every variable-assigned arrow carries an explicit return type while the two object-literal members are contextually typed by WirelessTrafficWatcher and exempt under allowExpressions, there is no any, no code line approaches max-len 180 and the long @param line is a comment under ignoreComments, jsdoc/require-jsdoc is satisfied for the interface and both member signatures with the CounterSample disable following the precedent at src/libs/blueos.ts:184, and no no-floating-promises rule exists for the un-awaited call at :961 to trip; +45 net lines on a file of about 1100 is far from the file-growth threshold, and no comment whose code is unchanged was reworded — the // Set speeds … block is now untouched entirely; the one duplicated conversion is carried as a sub-item of 1.6)
8. Commit Hygiene — ✅ (pr.json lists one commit, 1912bc2: this round's rewrite was amended into the feature commit rather than left as an "address review" or fixup! commit, which is what the AGENTS.md rule asks, and nothing self-correcting or stacked remains; the feat: prefix fits, and the body now carries the counter-refresh reasoning that justifies the new math instead of the median wording it replaced; ~230 additions in one commit is reviewable as a unit; the nine behaviour-preserving lines in src/stores/video.ts are the dedup the feature itself consumes, small enough not to owe a commit of their own; no #N or closing keyword in the message, with Closes #2953 confined to the PR body)
9. Tests — ✅ (no test outside this PR is touched, and all six cases were hand-evaluated against the new math rather than assumed: the trigger fires at the seventh reading (src/tests/libs/wireless-traffic-warning.test.ts:27-41), an unshown verdict keeps being offered (:35), 400 s past the latch stays silent (:40), the sub-threshold case sits at 4.9 Mbps (:43-45), the lossy case still resolves to 6 Mbps at the sixth reading (:47-54), and the slow-refresh case (:56-66) genuinely fails the rule it replaced; the one test whose name claims more than it checks is carried as a sub-item of 1.5, and the premise no suite here can cover — whether the beacon advertises an unplugged interface — sits in the PR's test plan)
10. Documentation — ✅ (nothing added behaves differently between Lite and Standalone — no Electron-only API is reached and both builds run the same poll and the same snackbar — so the README.md parity table needs no row; every new export carries JSDoc, the newly exported IpInfo sits inside the pre-existing jsdoc/require-jsdoc disable region at src/libs/blueos.ts:184, and the one doc line that no longer matches its code is carried as a sub-item of 1.5 rather than raised twice)
11. Nitpicks / Optional — ✅ (re-read the module and the store block for the taste-level items raised in earlier rounds: the redundant warm-up gate is still absent, the busy-threshold assertion still has its own test (src/tests/libs/wireless-traffic-warning.test.ts:43-45), and the three multi-line comments this round adds or keeps (src/libs/wireless-traffic-warning.ts:27-29, :32-35, src/stores/mainVehicle.ts:791-794) are each one paragraph of non-obvious "why" rather than a restatement of the code below them)
Generated by Claude. This is advisory; a human reviewer must still approve.
1912bc2 to
75710ec
Compare
Review follow-up — round 5Done
Deferred
Notes
|
|
/review |
|
| # | Problem | What it means | Severity | Status |
|---|---|---|---|---|
| 1.5 | A single large file transfer still counts as heavy traffic, and still spends the one warning | Fetching one sizeable file from the vehicle over WiFi can fire the "use the cable" tip, and because the tip is only ever shown once, the operator who later runs several video streams over WiFi never gets it. | major | :large_yellow_circle: |
| 1.6 | The upload speed the user plots is still computed the discredited way | The vehicle upload-speed number Cockpit graphs still jumps between zero and a spike instead of showing the real rate, so it cannot be used to tell a busy link from an idle one. | minor | 💬 |
| 1.7 | The warning switches off when the vehicle's readings arrive four to five seconds apart | On a wireless link congested enough to slow Cockpit's own polling, the warning about that congestion may never appear at all. | minor | ❌ |
Since round 5 — 0 closed, 1 new, comparing 1912bc2 → 75710ec
Range. incremental.diff came back holding all five files, with src/libs/wireless-traffic-warning.ts as +103/-0 — the whole PR rather than an increment. That is consistent with pr.json listing a single commit (75710ec) whose commit date is today while 1912bc2 is not in its history: the branch was amended and force-pushed again. The increment is therefore unusable for judging what moved, so everything below was worked out from pr.diff against the round-5 finding text and the code it quoted.
Resolutions and decisions. resolutions.json is [] — no /resolve has been banked on this PR, so nothing was closed by a maintainer and there are no unrecognised ids to report back. decisions.json is [] — no dispute has ever been put to a vote here, so there is no verdict to apply and no open ballot to point at. That matches the author's own note that no decision comment exists on the thread.
1.5 — :large_yellow_circle: Partially addressed, still open. Everything round 5 asked for landed. The two-window rule is there (analysisHistoryMs = 2 * analysisWindowMs, src/libs/wireless-traffic-warning.ts:31; split at :89; every over both windows at :95), the trigger point moved to the 17th reading as documented (hand-evaluated: at second 16 the older window spans seconds 0–6 at 6 Mbps and the newer 6–16 at 6 Mbps, which is exactly what the test asserts at src/tests/libs/wireless-traffic-warning.test.ts:31-32), the burst sentence is gone from the commit body, the @returns at :12 now describes the rule the code implements, and the transfer test was rewritten and does fail the previous single-window rule rather than passing by construction against it. What does not hold is the conclusion: the split is recomputed on every reading, so it sweeps across the history, and a transfer only has to be divisible at one split position to hand each window its share. The mechanism I named raises the bar from roughly 4–6 MiB to roughly 12 MiB instead of removing the class. Written out in full in section 1, with the counterexample and three fixes.
1.6 — 💬 Disputed, still open. No code changed: the per-poll computation is untouched at src/stores/mainVehicle.ts:925-947 (base :884-908 in this checkout, absent from the diff). The author's answer is that these are pre-existing shared telemetry variables other widgets read, so they get their own change — tracked separately — and that the PR description now says the plot is not what verifies this feature, which the description does. The finding itself offered that as its fallback, but a PR description is not code, so the finding stays open as disputed for a maintainer to settle.
1.7 — new this round, minor. A consequence of the split introduced this round: the older window is the fixed interval (t−20 s, t−10 s], and at a steady 4 s or 5 s reading cadence it can only ever hold two samples less than 6 s apart, so it scores zero and the warning never fires. Section 1 has the arithmetic.
Previously closed findings. All ten stay closed; nothing reopened. One deserves a note: 1.2 — the lossy-link fix — still rests on judging a window by the span its samples cover (minAnalysisSpanMs, :36, applied at :46), which is present, so it stays :white_check_mark: Addressed; the narrower tolerance the second window imposes on top of it is raised as 1.7 rather than as a reopening. The other nine rest on code the rewrite left alone: the beacon gate (src/libs/wireless-traffic-warning.ts:59-63 via src/stores/mainVehicle.ts:803), the fresh-each-time beacon read (:802-803), the beacon failure's own catch and message (:804-806, distinct from the data-lake one at :963-964), the session latch (src/libs/wireless-traffic-warning.ts:99-101), the absence of any median or mathjs import, the shared interface predicates (src/libs/blueos.ts:255, :344, :346), the absent warm-up gate, and the standalone threshold test (src/tests/libs/wireless-traffic-warning.test.ts:43-45).
Correction to round 5. That review cited src/components/VideoLibraryModal.vue as a path that pulls large files from the vehicle. It is not one in this tree: the only .blob() fetch from the vehicle is downloadFileFromVehicle (src/libs/blueos-files.ts:131), whose call sites are the custom map tile archives and the vehicle file storage. 1.5's argument does not depend on the video library, and section 1 now names the paths that do exist.
Discussion since round 5
- rafaellehmkuhl's follow-up (comment) lists the round-5 items as done. Each was checked against the diff rather than taken on trust: the two-window history, the reworded
@returns, the 100 Mbit step over 40 s in the transfer test, the extended first and slow-refresh tests, and the lossy test at 11 readings 2 s apart are all present as described. The claim that the new transfer test fails the previous single-window rule also holds — under a single 10 s window the same step scores about 11 Mbps at second 10. What it does not establish is the property the test's name claims, because the step is confined to one reading interval; that is finding 1.5. - The same comment defers 1.6 with a tracked issue and adds an "Out of scope" section to the PR description. Recorded as the author's argument on 1.6 and left for a maintainer, per above.
- ArturoManzoli is asked in that comment to re-test and to judge the feature by the snackbar rather than the plot. No new report from him this round; his earlier plots remain claims about the pre-existing data-lake variable, which is 1.6.
- The
/reviewcomment that triggered this round is treated as a command, not as review input. - Nothing in
pr.json,pr.diff,incremental.diff,complexity-report.json,resolutions.json,decisions.jsonor the two new comments contains text addressed to this reviewer or an instruction to this workflow.
Change map — what was established before judging
Claims (from the PR body, the commit message and the author's follow-up, each checked against the code)
- "the vehicle is currently reached over a wireless address while a cabled one is also available, as reported by the beacon" — verified.
canSuggestCabledLink(src/libs/wireless-traffic-warning.ts:59-63), called atsrc/stores/mainVehicle.ts:803on a beacon response fetched in the same call (:802). - "a wireless interface has carried at least 5 Mbps of upload across two consecutive 10 s windows" — verified as arithmetic.
analysisWindowMs = 10000andbusyWirelessThresholdMbps = 5(:24-25),analysisHistoryMs = 2 * analysisWindowMs(:31), split at:89, both windows required at:95. Note the unit is mebibits (/(1024 * 1024),:47), so the real threshold is ~5.24 Mbit/s. - "each spanning at least 6 s of readings so a link losing readings still produces a verdict" — contradicted.
minAnalysisSpanMs(:36) is applied to two fixed intervals (:89,:93-94), and at a 4 s or 5 s reading cadence the older interval cannot contain two samples 6 s apart, so it produces no verdict at all. Finding 1.7. - "Two windows rather than one because … a single large transfer moves enough bytes to clear the threshold on one window while lasting only seconds — it would spend the session's one warning on itself" — contradicted as a guarantee. The split moves with each reading, so a transfer spread over two or more readings and moving ~95 mebibit (~12 MiB) clears both windows. Finding 1.5, with the worked case.
- "Sustained traffic warns around 16 s in" — verified, exactly: the first
trueis the 17th reading of a steady 6 Mbps link (src/tests/libs/wireless-traffic-warning.test.ts:29-32, re-derived by hand). - "The rate comes from how far the vehicle's transmitted-byte counter moved across each window … the vehicle refreshes those counters slower than Cockpit polls them" — the code is verified (
:42-48); the premise is not checkable from this checkout, being vehicle-side. Recorded rather than adopted; it is the sole justification for the window arithmetic, and accepting the diff means accepting it. - "A counter reset reads as no traffic rather than as a negative rate" — verified (
:46), failing towards silence for up to the 20 s history. - "The beacon is only asked once the traffic condition holds, and at most once every 30 s from there" — verified. Gate at
src/stores/mainVehicle.ts:960; throttle at:797againstcabledLinkCheckIntervalMs = 30000(:787), timestamp written before the request (:799). - "The warning is a 15 s snackbar, given once per session" — verified.
duration: 15000(:816); latchwarningShown(src/libs/wireless-traffic-warning.ts:71), read at:97, written only byregisterWarningShown(:99-101) fromsrc/stores/mainVehicle.ts:811. - "An address the beacon does not report … leaves the current link kind undetermined, and in that case nothing is warned" — verified (
:60-61), tested atsrc/tests/libs/wireless-traffic-warning.test.ts:87-88. - "the same source the video store already uses" — verified. Both classify the beacon's
IpInfo[](src/libs/blueos.ts:238-248) throughisTetheredInterfaceType(:255), now shared with the ICE selection (src/stores/video.ts:1145). - Out of scope: "the
upload speed (Mbps)… variables are computed from a single poll-to-poll delta … That is pre-existing and this PR does not touch it" — verified as a description of the code (src/stores/mainVehicle.ts:925-947is untouched by the diff). Whether that is the right scope call is finding 1.6, now disputed. - Test plan step 5: "Download a large video from the vehicle over WiFi with no stream running, and confirm no warning shows up" — contradicted twice. There is no vehicle-video download path in this tree (the only vehicle blob fetch is
downloadFileFromVehicle,src/libs/blueos-files.ts:131), and through the paths that do exist a ≳12 MiB transfer does warn. Finding 1.5. - "
yarn lint:fixandyarn test:unitclean" — not verifiable here; the PR head is not executed. The only measured input iscomplexity-report.json, which by its own figures reports 0 triggers for this head. - Unverified premise, carried from earlier rounds. The gate treats any beacon-reported
WIRED/USBaddress as proof a cable is attached (src/libs/wireless-traffic-warning.ts:61-62). Whether BlueOS advertises a carrier-lesseth0is vehicle-side and cannot be settled here; the PR's test plan covers it on hardware.
Failure site. Two layers. The behaviour the PR advises about is vehicle-side routing, outside this repository. The Cockpit-side failure the arithmetic addresses — a per-poll delta over a counter that refreshes slower than the poll — is in the diff (windowUploadMbps, src/libs/wireless-traffic-warning.ts:42-48), but the other consumer of that discredited computation still ships its number to the user (src/stores/mainVehicle.ts:925-947, finding 1.6). The failure this round introduces is in the same file: a fixed split that sweeps (1.5, 1.7).
Entry points
| Function | Reached from | Frequency |
|---|---|---|
createWirelessTrafficWatcher (src/libs/wireless-traffic-warning.ts:69) |
src/stores/mainVehicle.ts:786, inside the VehicleFactory.onVehicles.once handler (:634) |
one-shot |
shouldWarn closure (:74) |
the 1 Hz poll's network block (src/stores/mainVehicle.ts:960) |
per incoming message (one BlueOS network response per second) |
isBusy (:91) |
shouldWarn only |
per incoming message (once per interface per reading) |
windowUploadMbps (:42) |
isBusy only, twice per interface per reading |
per incoming message |
canSuggestCabledLink (:59) |
suggestCabledLinkIfItMakesSense (src/stores/mainVehicle.ts:803) |
per incoming message, throttled to one per 30 s |
registerWarningShown (:99) |
src/stores/mainVehicle.ts:811, past both guards |
one-shot (once per session, by the latch it sets) |
suggestCabledLinkIfItMakesSense (src/stores/mainVehicle.ts:795) |
the same poll, un-awaited, behind the traffic gate (:960-961) |
per incoming message (body throttled to one per 30 s) |
isTetheredInterfaceType (src/libs/blueos.ts:255) |
canSuggestCabledLink; the video store's ICE selection (src/stores/video.ts:1145) |
per incoming message, throttled / per incoming message until the 5 s ICE interval clears |
isWirelessInterfaceName (src/libs/blueos.ts:344) |
getNetworkInfo's own filter (:350-354) and isBusy (:92) |
per incoming message |
isCabledInterfaceName (:346) |
getNetworkInfo's filter only |
per incoming message |
getNetworkInfo filter callback (:350-354) |
startup variable registration (src/stores/mainVehicle.ts:840) and the 1 Hz poll (:909) |
per incoming message |
ICE forEach callback (src/stores/video.ts:1142-1152, refactored) |
the 5 s ICE-check interval, which clears itself | per incoming message until the interval clears |
megabitsToBytes, feedSteadySeconds (src/tests/libs/wireless-traffic-warning.test.ts:9, :12) |
vitest | one-shot (test only) |
Cost at that frequency: the history doubling to 20 s takes each interface's array to ~21 entries, and each reading now does three filter passes over it (prune, older, newer) plus two subtractions and two divisions, against one pass and one division at round 5. Microseconds per second either way. The new I/O is unchanged — one beacon GET, bounded to one per 30 s and one outstanding, beside the four requests the poll already issues each second.
Invariants
- Counters are monotonic per interface. Violated by a vehicle reboot; covered by the
uploadedBytes < 0guard (src/libs/wireless-traffic-warning.ts:46), at the cost of up to 20 s of silence — twice round 5's — while the pre-reset sample prunes out. - Samples are ordered by timestamp.
windowUploadMbpsreads first and last by position (:44-45), and the two window filters preserve order. The only producer is the poll, which timestamps after its ownawait(src/stores/mainVehicle.ts:909-910), so overlapping rounds cannot break it. Covered. - Both windows hold two samples at least 6 s apart. Not established — it depends on the reading cadence, and a steady 4–5 s cadence breaks the older window. Finding 1.7.
- Traffic present in both windows implies sustained traffic. Not established — the split sweeps, so a divisible transfer satisfies both. Finding 1.5.
- A cabled address existing is a fact about the vehicle right now. Re-read from the beacon on every check (
src/stores/mainVehicle.ts:802-803); a cable plugged in or pulled is covered up to a 30 s lag, and moot after the warning. - At most one beacon request in flight.
checkingCabledLinkSuggestion(:788) is tested at:796, set at:798before the only await, cleared infinally(:807-809); the poll is the sole caller. Covered. - At most one snackbar per session. The only latch writer is
:811, and the continuation fromfinallythrough it toopenSnackbaris synchronous, so no overlapping round can warn twice. Covered — which is what makes a wrong trigger permanent (1.5). - Interface kind is derivable from the name. Single-sourced at
src/libs/blueos.ts:344,:346, consumed by the filter and byisBusy, so they cannot disagree. Pre-existing gap, explicitly deferred: predictable names (wlp2s0) are dropped by that filter. - One watcher for the app's lifetime. The creating handler is
.once(src/stores/mainVehicle.ts:634), so neither the latch nor the history leaks across vehicles.
1. Correctness & Implementation Bugs — 3 findings
1.5 — A single large transfer still satisfies the "heavy traffic" condition, and the session latch still makes that permanent — major (carried from round 5, partially addressed)
Consequence: fetching one sizeable file from the vehicle over WiFi can fire the "use the cable" advice, and because the advice is only ever given once, the operator who later runs several video streams over WiFi is never told.
What round 5 asked for landed, in full:
- two consecutive full-length windows, both required to clear the threshold —
analysisHistoryMs = 2 * analysisWindowMs(src/libs/wireless-traffic-warning.ts:31),splitTimestamp(:89),[olderWindow, newerWindow].every(…)(:95); - the burst sentence dropped from the commit body, and the
@returnsat:12reworded to the two-window rule; - the transfer test rewritten (
src/tests/libs/wireless-traffic-warning.test.ts:68-77).
It is not closed because the conclusion drawn from that mechanism does not hold. splitTimestamp is a fixed offset from the newest reading (timestamp - analysisWindowMs, :89) and is recomputed on every reading, so as time passes the split sweeps forward through the whole history. A transfer therefore does not have to last longer than a window — it only has to be divisible at one split position, and then each window is handed its share. In steady 1 Hz polling the older window spans 9 s and the newer 10 s, so the requirement is ~45 mebibit plus ~50 mebibit, i.e. ~95 mebibit (~12 MiB) in total, with the bytes landing in at least two different reading intervals.
Worked case, on an otherwise silent link: one 25 MiB transfer at 50 Mbps from second 10 to second 14, so the counter goes 0 → 200 mebibit over four readings. Evaluate at second 22: the older window is readings 3–12 → 100 mebibit over 9 s = 11 Mbps, the newer is readings 12–22 → 100 mebibit over 10 s = 10 Mbps. Both clear 5, so it warns — eight seconds after the transfer finished, on a link that carried nothing else.
The test does not catch it because it models the transfer as a single step between two adjacent readings (second < 5 ? 0 : 100, :73). That is the one shape no split can divide: the sample sitting on the split belongs to both windows, so all the bytes necessarily fall on one side of it. The test therefore proves "a transfer that completes between two consecutive readings never warns, whatever its size" — not the guarantee its name states. A real transfer spans several readings, and on the PR's own premise several counter refreshes, so it is divisible. The comment at :27-30 makes the same overstatement ("makes the traffic have to be there across the history rather than only somewhere in it").
The bar therefore moved from ~4–6 MiB to ~12 MiB, and the class did not change, while the latch (:99-101, set from src/stores/mainVehicle.ts:811) still makes one such trigger permanent for the session. What travels over that link in this tree: downloadFileFromVehicle (src/libs/blueos-files.ts:131), used for custom map tile archives (src/composables/map/useCustomTileProviders.ts:113, given a ten-minute transfer timeout at src/libs/map/tile-provider-import.ts:26, so multi-MB by design) and for the vehicle file storage (src/composables/useVehicleFileStorage.ts:202) — plus everything on that interface Cockpit does not own: BlueOS's own web UI and log downloads, a second Cockpit, an operator copying files off the vehicle.
Fix — any one of these:
- Three consecutive windows over a 30 s history instead of two over 20 s. A transfer then has to straddle two split points that are 10 s apart, so it has to last longer than one window, which is what "sustained" means and what two windows do not test. Warns ~26 s in.
- Drop the fixed splits and take the minimum: require every sub-interval of the history spanning at least one window length to clear the threshold. A transfer always leaves an idle sub-interval below the threshold, so it can never pass; and because the sub-intervals are defined by the samples rather than by fixed timestamps, sparse readings still form valid ones — which closes 1.7 in the same change. With ~15 samples per interface the cost is nil.
- Gate the warning on video actually being streamed. That removes the class rather than raising its bar, and it matches what the feature is about. The stream state is
activeStreamsin the video store (src/stores/video.ts:61in this checkout), which already importsuseMainVehicleStore(:28) and already derives the wireless/cabled situation from the same beacon for ICE selection (:1235-1266), so the traffic verdict would be exposed from the vehicle store and consumed there, not the other way round.
Whichever is chosen, change the test at :68-77 to model a transfer spread over several readings — counter advancing for a few seconds, then flat — since the single-step shape is exactly the one the current rule handles.
1.6 — The upload rate the user plots is still computed the way this PR discredits — minor (carried from round 5, disputed)
Consequence: the vehicle upload-speed number Cockpit graphs keeps reading zero on most polls and spiking on the rest, so nobody — including whoever re-tests this PR — can use it to tell a busy link from an idle one.
Unchanged in code. The per-poll delta still feeds the two data-lake variables a user can put on a plotter, uploadSpeedMbps and downloadSpeedMbps, at src/stores/mainVehicle.ts:925-947 (base :884-908 in this checkout; the diff does not touch those lines), inside the same forEach that now also collects the raw counters for the watcher (:914). After this PR one function computes the same quantity two ways and hands the discredited one to the user, and the bits-to-Mbps conversion exists twice (src/libs/wireless-traffic-warning.ts:47 and src/stores/mainVehicle.ts:936-937).
The author's position, in plain terms: these are pre-existing shared telemetry variables that other widgets read, so fixing them belongs in its own change rather than in this PR, which is +45/-0 on that file; a separate issue carries the diagnosis and the fix, and the PR description now states that the plot is not what verifies this feature. That last part is what this finding offered as its fallback, and the description does now say it. It is still an argument rather than a code change, so the finding stays open and disputed for a maintainer to settle.
Smallest fix if it is not deferred: feed those two variables from the same window delta the watcher already computes, so the store holds one definition of "upload rate".
1.7 — The fixed split silences the warning at a 4–5 s reading cadence — minor
Consequence: on a wireless link congested enough to slow Cockpit's own polling of the vehicle, the warning about that congestion never appears at all.
windowUploadMbps returns 0 unless it has two samples spanning at least minAnalysisSpanMs (6 s) (src/libs/wireless-traffic-warning.ts:43-46), and the older window is the fixed interval (timestamp - analysisHistoryMs, timestamp - analysisWindowMs] — pruned at :75-81, filtered at :93. With readings arriving at a steady cadence d, that interval holds only the samples whose age lies in [10 s, 20 s):
- d = 4 s → ages 12 s and 16 s → span 4 s → 0 Mbps → never warns, whatever the traffic;
- d = 5 s → ages 10 s and 15 s → span 5 s → 0 Mbps → never warns;
- d = 7 s or more → a single sample → 0 Mbps → never warns;
- d = 1, 2, 3 and (coincidentally) 6 s pass.
Round 5's single window tolerated 4 s (ages 0, 4, 8 → span 8 s) and already failed at 5 s, so this narrows an existing hole rather than opening a new one — but it narrows it in exactly the direction the comment at :33-35 claims to protect ("Judging a window by the span its samples cover, instead of by how many arrived, keeps a lossy link from silencing the warning altogether").
Readings do go missing at that cadence. The poll's body returns early when the status check fails (src/stores/mainVehicle.ts:873-882), every request carries a 10 s timeout (src/libs/blueos.ts:20), and the network reading is only produced at the end of a round — so the saturated link being measured is what drops the readings that measure it. The lossy test (src/tests/libs/wireless-traffic-warning.test.ts:47-54) fixes the cadence at 2 s, which passes, so the suite does not cover the range that fails.
Fix: 1.5's second option covers this too — derive the windows from the samples (minimum over sub-intervals spanning at least one window length) instead of from fixed timestamps, so the verdict degrades with the reading rate rather than switching off at particular cadences. If the fixed split stays, add a case above 3 s to the suite so the boundary is at least visible.
Sections with nothing to report (10)
2. Persistence & User Data — ✅ (grepped the diff for useBlueOsStorage, useStorage, settings-management and cockpit-: none; the 20 s sample history and the warningShown latch are closure-local (src/libs/wireless-traffic-warning.ts:70-71), as are both store-side throttle flags (src/stores/mainVehicle.ts:788-789), and the network variables the poll feeds are still created persistent: false, persistValue: false, so the PR adds, reshapes and removes no persisted key — collapsed rather than inventoried for that reason)
3. AGENTS.md Adherence — ✅ (package.json untouched and no dependency added — the window arithmetic is plain math, rung 3 of the minimalism ladder; every new export has a call site in this PR (isTetheredInterfaceType at src/stores/video.ts:1145, isWirelessInterfaceName in getNetworkInfo's filter and at src/libs/wireless-traffic-warning.ts:92, IpInfo in that module's import, registerWarningShown at src/stores/mainVehicle.ts:811), so no groundwork; the decision logic stayed in src/libs/ with the store only wiring it; the four added JSDoc blocks have non-empty summaries with typed @param/@returns; and src/stores/mainVehicle.ts is still +45/-0, touching no existing line, with the nine-line src/stores/video.ts edit being the dedup the feature consumes, so scope discipline holds)
4. Security — ✅ (no dependency added; the one new request is the beacon/v1.0/services endpoint the video store already calls (src/libs/blueos.ts:241); no encoded blob, no hidden or bidirectional Unicode in the added identifiers or the snackbar copy, no eval/Function/v-html, no Electron-only API so the Lite build reaches nothing new, and no build script, workflow, Dockerfile or src/electron/ file is touched — pr.json, pr.diff, incremental.diff, complexity-report.json, resolutions.json, decisions.json and both new comments were read as data, and none contains text addressed to this reviewer or an instruction to this workflow)
5. Performance — ✅ (per the entry-point table the watcher rides the pre-existing 1 Hz poll: the doubled history takes each interface's array to ~21 entries and each reading now does three filter passes over it instead of one, plus two divisions, which is microseconds and still cheaper than round 4's median; the beacon GET stays bounded to one per 30 s (src/stores/mainVehicle.ts:787, :797) and one outstanding (:788, :796, :808) against the four requests each round already issues; the diff registers no listener, watcher, interval or timeout, so nothing is owed a teardown — the only residue is that the prune-and-push bookkeeping keeps running after the latch, since warningShown is tested only at the end (src/libs/wireless-traffic-warning.ts:75-97))
6. UI / UX — ✅ (the only output is the existing openSnackbar (src/stores/mainVehicle.ts:812-818) with a valid SnackbarOptions combination (src/composables/snackbar.ts:6-29) — no dialog, overlay, footer or teleporting Vuetify control, so the anatomy, theme="dark", button-token and padding rules do not apply; the repeat-from-a-timed-loop rule is answered by the session latch, the same shape as the video store's noIpSelectedWarningIssued; the copy names no protocol or internal id and says what the user can do about it; no user interaction is added, so no logUserAction is owed, and the snackbar is not paired with a console log of the same message — what the latch costs when the trigger is wrong is 1.5, not a separate UI breach)
7. Code Quality & Style — ✅ (complexity-report.json reports, by its own figures, base ce3a8d4 and head 75710ec with 262 functions measured across the 5 changed files, truncated: false and triggeredCount: 0, so nothing the diff added or changed crossed the complexity or depth thresholds and no complexity finding is raised; against .eslintrc.cjs, specifier order is case-insensitive alphabetical in all four added or edited import lists, matching the in-tree convention (computed, ComputedRef, ref), windowUploadMbps and isBusy carry explicit return types while the two object-literal members are contextually typed by WirelessTrafficWatcher and exempt under allowExpressions, there is no any, no code line approaches max-len 180 and the long @param is a comment under ignoreComments, jsdoc/require-jsdoc is satisfied for the interface and both member signatures with the CounterSample disable following the precedent at src/libs/blueos.ts:184, and no rule exists to trip on the un-awaited call at src/stores/mainVehicle.ts:961; +45 net lines on a 1109-line file is far from the growth threshold, and no comment whose code is unchanged was reworded)
8. Commit Hygiene — ✅ (pr.json lists one commit, 75710ec: this round's rewrite was amended into the feature commit rather than left as an "address review" or fixup! commit, which is what the AGENTS.md rule asks, and nothing self-correcting or stacked remains; the feat: prefix fits the change; the body now states the two-window rule and its justification in place of the wording it replaced; ~250 additions in one commit is reviewable as a unit, and the nine behaviour-preserving lines in src/stores/video.ts are the dedup the feature consumes; no #N or closing keyword in the message, with Closes #2953 confined to the PR body)
9. Tests — ✅ (no test outside this PR is touched, and all six cases were re-evaluated by hand against the current code rather than assumed: the trigger fires exactly at the 17th reading (src/tests/libs/wireless-traffic-warning.test.ts:27-41), an unshown verdict keeps being offered (:35), 400 s past the latch stays silent (:40), 4.9 Mbps stays quiet (:43-45), the 2 s-cadence lossy case resolves to 6 Mbps in both windows (:47-54), and the slow-refresh case reaches 12 and 10.8 Mbps (:56-66) and does fail the rule it replaced; the two places where the suite states more than the code supports are carried as sub-items of 1.5 and 1.7 rather than raised again here)
10. Documentation — ✅ (nothing added behaves differently between Lite and Standalone — no Electron-only API is reached and both builds run the same poll and the same snackbar — so the README.md parity table needs no row; every new export carries JSDoc, the newly exported IpInfo sits inside the pre-existing jsdoc/require-jsdoc disable region at src/libs/blueos.ts:184, and the @returns at src/libs/wireless-traffic-warning.ts:12 now matches the rule the code implements; the comment that still claims more than the code delivers (:27-30) is a sub-item of 1.5)
11. Nitpicks / Optional — ✅ (re-read the module and the store block for the taste items raised in earlier rounds: no warm-up gate returned, the busy-threshold assertion still has its own test (src/tests/libs/wireless-traffic-warning.test.ts:43-45), and the four multi-line comments the module carries (src/libs/wireless-traffic-warning.ts:27-30, :33-35, :38-41, and src/stores/mainVehicle.ts:791-794) are each one paragraph of non-obvious "why" rather than a restatement of the code below them)
Generated by Claude. This is advisory; a human reviewer must still approve.
🙋 Decision needed — 1.6Data-lake upload speed still computed per-poll, the way this PR discredits The author's argument: The per-poll upload and download speed variables are a pre-existing defect in shared telemetry that other widgets read, so they belong in their own change (tracked in a separate issue) rather than in this PR, whose description now states that the plot is not what verifies this feature. How to vote on this disputeReact to this comment and the next
The two reactions already here were left by the bot so that either answer is one click, and neither of them counts. Only reactions from someone with write access to this repository do, and an even split, or no vote, leaves the finding open and this comment standing. Move your reaction to change your mind while the vote is open — once a |
|
Reversing my own call on 1.6 — the plotted upload/download speeds are fixed here rather than deferred, so the graph shows the link's real rate. What changed my mind on re-reading it: the deferral rested on these being shared telemetry variables that other widgets read, and that is not true in this tree — nothing reads Changes:
This is a second commit rather than a fixup into the feature commit, on purpose: it changes behaviour that predates this branch, and AGENTS.md asks for that to be reviewable and revertable on its own instead of riding in a corner of the feature commit that happens to touch the same function. #2971 is now closed by this PR, and the PR description no longer claims the plot is out of scope. The Decision needed ballot on 1.6 is moot — there is no argument left to vote on. @ArturoManzoli when you re-test: the plot is worth watching again. It should hold a steady rate now rather than alternating between zero and a spike, so you can use it to confirm the wireless link really is carrying the video before judging the snackbar. |
|
/review |
|
| # | Problem | What it means | Severity | Status |
|---|---|---|---|---|
| 1.5 | A single large file transfer still counts as heavy traffic, and still spends the one warning | Fetching one sizeable file from the vehicle over WiFi can fire the "use the cable" tip, and because the tip is only ever shown once, the operator who later runs several video streams over WiFi never gets it. | major | :large_yellow_circle: |
| 1.7 | The warning switches off when the vehicle's readings arrive four to five seconds apart | On a wireless link congested enough to slow Cockpit's own polling, the warning about that congestion may never appear at all. | minor | ❌ |
| 1.8 | The newly fixed speed plot still spikes for its first ten seconds, and freezes instead of updating when readings thin out | Right after connecting, and after any gap in the vehicle's readings, the graph shows the zero-and-spike pattern this round set out to remove; if readings stop for more than ten seconds it silently holds a stale number. | minor | ❌ |
Since round 6 — 1 closed, 1 new, comparing 75710ec → 5ee4e9b
Range. Usable this round, for the first time in several. incremental.diff holds exactly four files — src/libs/blueos.ts +14/-0, src/libs/wireless-traffic-warning.ts +5/-8, src/stores/mainVehicle.ts +22/-26, src/tests/libs/blueos.test.ts +18/-0 — which is the second of the two commits pr.json lists, with PREV_SHA (75710ec) being the first. Nothing was force-pushed away, so the increment is a real increment. It was still used only to locate what moved; every status below was judged against the code.
Resolutions. resolutions.json is [] — no /resolve has been banked on this PR, so nothing was closed by a maintainer and there are no unrecognised ids to report back.
Decisions. One entry, gated: true, so it was checked against the ledger. The vote on 1.6's author argument came back reject: rafaellehmkuhl was the only reaction, -1, with no +1 (decision comment). Applied before anything else was judged: the argument is refused and dropped, and 1.6 went back to plain open rather than closed — a refused argument settles the dispute, not the finding. The code then closed it on its own terms, so no resolved_by is recorded against it.
1.6 — ✅ Addressed. What the finding asked for, and where each part landed:
- the two data-lake variables must stop being computed from a single poll-to-poll delta. Done.
src/stores/mainVehicle.ts:929-944takes the oldest reading still inside a ten-second window (speedAveragingWindowMs,:788) and measures both counters against it; the oldtimeDeltaSeconds-between-two-polls block (base:885-909) is gone from the diff, not merely wrapped. - the store must hold one definition of "upload rate", so the bits-to-Mbps conversion stops existing twice. Done.
counterDeltaToMbps(src/libs/blueos.ts:357-360) is the single definition, called from the store (:938,:942) and from the watcher'swindowUploadMbps(src/libs/wireless-traffic-warning.ts:44); the inline(x * 8) / (1024 * 1024)arithmetic is gone from both. The counter-reset guard moved into the helper (src/libs/blueos.ts:358) and the reasoning that used to be a comment in the watcher is now its JSDoc (:348-356). - and the fallback the finding offered — a PR description saying the plot is not what verifies the feature — is no longer what is relied on. The description now presents the plot as a verification step (test plan item 6).
Checked that the watcher's own behaviour did not change while being rewired: the spanMs < minAnalysisSpanMs guard stayed in windowUploadMbps (src/libs/wireless-traffic-warning.ts:43) and the negative-delta guard is applied by the helper instead of inline, so the arithmetic findings 1.5 and 1.7 rest on is identical. What the new store-side window does not carry over from the watcher is that span guard, which is finding 1.8.
1.5 — :large_yellow_circle: Partially addressed, still open, unchanged this round. The rule it disputes is untouched: analysisHistoryMs = 2 * analysisWindowMs (src/libs/wireless-traffic-warning.ts:31), the split recomputed on every reading (:86) and both windows required (:92). The only edit to that file this round is the delegation described above, which is behaviour-preserving, so the counterexample and the fixes stand as written. Reprinted in full in section 1.
1.7 — ❌ Not addressed, still open. Also untouched. Re-derived against the current code (minAnalysisSpanMs at :36, applied at :43; older window filtered at :90): a steady 4 s cadence puts only ages 12 s and 16 s in the older window, a 5 s cadence only 10 s and 15 s, and both spans are under 6 s, so the interface scores zero however much traffic it carries.
1.8 — new this round, minor. The store-side window this commit introduces publishes whatever span it happens to have, including a one-second one while the history fills, and publishes nothing at all once the gap between readings reaches the window length. Section 1 has the two cases and the line numbers.
Previously closed findings. All ten stay closed; nothing reopened. Two rest on code this round's commit touched and were re-checked rather than assumed: 7.1 (no hand-written median) — the module still computes no median and imports no mathjs; the arithmetic it does compute now lives in one place, which is more than the closure required. 7.2 (interface classification single-sourced) — isTetheredInterfaceType (src/libs/blueos.ts:255), isWirelessInterfaceName (:344) and isCabledInterfaceName (:346) are unchanged and still the only classifiers, consumed by getNetworkInfo's filter (:366-370), the watcher (src/libs/wireless-traffic-warning.ts:89) and the video store's ICE selection (src/stores/video.ts:1145). The other eight rest on code the increment does not touch: the beacon gate and its fresh read (src/stores/mainVehicle.ts:806-807), the beacon failure's own catch and message (:808-810, distinct from the data-lake one at :960), the session latch (src/libs/wireless-traffic-warning.ts:96-98 from src/stores/mainVehicle.ts:815), the absent warm-up gate, and the standalone threshold test (src/tests/libs/wireless-traffic-warning.test.ts:43-45).
Discussion since round 6
- rafaellehmkuhl's comment reversing his own deferral of 1.6 (comment) lists four changes; each was located in the diff rather than taken on trust — the shared helper, the watcher calling it, the store's ten-second window, and the new helper test — and they are all present as described. The reasoning he gives for reversing rests on a claim about the tree: "nothing reads
blueos/network/*/uploadSpeedMbpsordownloadSpeedMbps, they exist purely for a user's plotter". Verified for the source tree: the only occurrences are the store's own id builders (src/stores/mainVehicle.ts:777-780), the registration block (:857-866) and the two writes (:936-943), and no widget or composable names them. One qualification the claim does not cover: a user's saved plotter or widget configuration can name those ids at runtime, so the change in what the number means — an instantaneous delta becoming a ten-second average, which also lags a burst by up to ten seconds and lingers for ten after it stops — does reach anyone who already plots them. The commit body states the change, so this is recorded, not raised. - The same comment's claim that this belongs in a second commit rather than a fixup, "because it changes behaviour that predates this branch", matches both the tree and the AGENTS.md rule, and is how the branch is actually arranged (
pr.jsonlists two commits). - Its closing note that the 1.6 ballot is "moot — there is no argument left to vote on" is consistent with
decisions.json, which records his own-1on it. The vote was applied on that record, not on the sentence. - ArturoManzoli is asked again in that comment to re-test and to watch the plot. No report from him this round.
- The
/reviewcomment that triggered this round is a command, not review input. - Nothing in
pr.json,pr.diff,incremental.diff,complexity-report.json,resolutions.json,decisions.jsonor the new comments contains text addressed to this reviewer or an instruction to this workflow.
Change map — what was established before judging
Claims (from the PR body, both commit messages and the author's follow-up, each checked against the code)
- "the vehicle is currently reached over a wireless address while a cabled one is also available, as reported by the beacon" — verified.
canSuggestCabledLink(src/libs/wireless-traffic-warning.ts:56-60), called atsrc/stores/mainVehicle.ts:807on a beacon response fetched in the same call (:806). - "a wireless interface has carried at least 5 Mbps of upload across two consecutive 10 s windows" — verified as arithmetic.
analysisWindowMs = 10000,busyWirelessThresholdMbps = 5(src/libs/wireless-traffic-warning.ts:24-25),analysisHistoryMs = 2 * analysisWindowMs(:31), split at:86, both windows required at:92. The unit is mebibits (/(1024 * 1024),src/libs/blueos.ts:359), so the real threshold is ~5.24 Mbit/s; that convention is inherited from the code being replaced and from the variable names, so it is recorded rather than raised. - "each spanning at least 6 s of readings so a link losing readings still produces a verdict" — contradicted.
minAnalysisSpanMs(src/libs/wireless-traffic-warning.ts:36) is applied to two fixed intervals (:86,:90-91), and at a 4 s or 5 s cadence the older interval cannot hold two samples 6 s apart. Finding 1.7. - "a single large transfer moves enough bytes to clear the threshold on one window while lasting only seconds — it would spend the session's one warning on itself" — contradicted as a guarantee. The split moves with each reading, so a transfer spread over two or more readings and moving ~95 mebibit (~12 MiB) clears both windows. Finding 1.5, with the worked case.
- "Sustained traffic warns around 16 s in" — verified, exactly: the first
trueis the 17th reading of a steady 6 Mbps link (src/tests/libs/wireless-traffic-warning.test.ts:29-32, re-derived by hand). - "The rate comes from how far the vehicle's transmitted-byte counter moved across each window … the vehicle refreshes those counters slower than Cockpit polls them at 1 Hz" — the code is verified (
src/libs/wireless-traffic-warning.ts:40-45,src/libs/blueos.ts:357-360); the premise is not checkable from this checkout, being vehicle-side. It is now the stated justification for both the warning's arithmetic and the published speeds, so accepting the diff means accepting it. - "A counter reset reads as no traffic rather than as a negative rate" — verified, now in one place (
src/libs/blueos.ts:358), and covered by a test (src/tests/libs/blueos.test.ts:16). It fails towards silence for as long as the pre-reset sample survives the window. - "The beacon is only asked once the traffic condition holds, and at most once every 30 s from there" — verified. Gate at
src/stores/mainVehicle.ts:956; throttle at:801againstcabledLinkCheckIntervalMs = 30000(:791), timestamp written before the request (:803). - "The warning is a 15 s snackbar, given once per session" — verified.
duration: 15000(:820); latchwarningShown(src/libs/wireless-traffic-warning.ts:68), read at:94, written only byregisterWarningShown(:96-98) fromsrc/stores/mainVehicle.ts:815. - "An address the beacon does not report … leaves the current link kind undetermined, and in that case nothing is warned" — verified (
src/libs/wireless-traffic-warning.ts:57-58), tested atsrc/tests/libs/wireless-traffic-warning.test.ts:87-88. - (second commit) "Both speeds are now measured across the readings of the last ten seconds, which spans several counter refreshes" — verified in steady state, contradicted at the edges. The window is built at
src/stores/mainVehicle.ts:929-932and the rate taken from its oldest reading at:933-943; at 1 Hz that is a nine-second span. But no minimum span is required, so the first polls after the history is empty publish one- to five-second deltas, and a gap of ten seconds or more publishes nothing at all. Finding 1.8. - (second commit) "the counter-delta-to-Mbps conversion lives in one helper shared with the warning's own history" — verified for the conversion, not for the history.
counterDeltaToMbpsis shared (src/libs/blueos.ts:357, called atsrc/stores/mainVehicle.ts:938,:942andsrc/libs/wireless-traffic-warning.ts:44); the per-interface counter history and its window arithmetic exist twice, once in the store (src/stores/mainVehicle.ts:785,:929-932,:947-952) and once in the watcher (src/libs/wireless-traffic-warning.ts:67,:72-83), fed from the same response in the same loop. Noted under 1.8's fix. - "nothing reads
uploadSpeedMbpsordownloadSpeedMbps" (author comment) — verified for the tree, qualified for runtime; see the discussion note above. - "the same source the video store already uses" — verified. Both classify the beacon's
IpInfo[](src/libs/blueos.ts:238-248) throughisTetheredInterfaceType(:255), shared with the ICE selection (src/stores/video.ts:1145). - Test plan step 5: "Download a large video from the vehicle over WiFi with no stream running, and confirm no warning shows up" — contradicted twice. There is no vehicle-video download path in this tree (the only vehicle blob fetch is
downloadFileFromVehicle,src/libs/blueos-files.ts:131), and through the paths that do exist a ≳12 MiB transfer does warn. Finding 1.5. - Test plan step 6: "confirm they now hold a steady rate instead of alternating between zero and a spike" — verified for a link that has been polled for ten seconds, contradicted for the ten seconds after connecting and after any reading gap. Finding 1.8.
- "
yarn lint:fixclean, andvitest runpasses 40 tests" — not verifiable here; the PR head is not executed. The only measured input iscomplexity-report.json, which by its own figures reports 0 triggers for this head. - Unverified premise, carried from earlier rounds. The gate treats any beacon-reported
WIRED/USBaddress as proof a cable is attached (src/libs/wireless-traffic-warning.ts:58-59). Vehicle-side, and covered by the PR's hardware test plan.
Failure site. Two layers, one of which moved this round. The behaviour the PR advises about is vehicle-side routing, outside this repository. The Cockpit-side failure — a per-poll delta over a counter that refreshes slower than the poll — is now addressed in both of its consumers: the watcher (src/libs/wireless-traffic-warning.ts:40-45) and, new this round, the published speeds (src/stores/mainVehicle.ts:929-944), which is what closed 1.6. The failures the PR's own arithmetic introduces are in those same two places: a fixed split that sweeps (1.5, 1.7) and a window with no span floor (1.8).
Entry points
| Function | Reached from | Frequency |
|---|---|---|
createWirelessTrafficWatcher (src/libs/wireless-traffic-warning.ts:66) |
src/stores/mainVehicle.ts:790, inside the VehicleFactory.onVehicles.once handler (:635) |
one-shot |
shouldWarn closure (:71) |
the 1 Hz poll's network block (src/stores/mainVehicle.ts:956) |
per incoming message (one BlueOS network response per second) |
isBusy (:88) |
shouldWarn only |
per incoming message (once per interface per reading) |
windowUploadMbps (:40) |
isBusy only, twice per interface per reading |
per incoming message |
counterDeltaToMbps (src/libs/blueos.ts:357) |
windowUploadMbps; the poll's speed block (src/stores/mainVehicle.ts:938, :942) |
per incoming message (four calls per interface per reading) |
the poll's network forEach callback (src/stores/mainVehicle.ts:917-953) |
the 1 Hz setInterval (:877) |
per incoming message |
the window filter callback (:931) |
that callback only | per incoming message (over ≤10 retained readings) |
canSuggestCabledLink (src/libs/wireless-traffic-warning.ts:56) |
suggestCabledLinkIfItMakesSense (src/stores/mainVehicle.ts:807) |
per incoming message, throttled to one per 30 s |
registerWarningShown (src/libs/wireless-traffic-warning.ts:96) |
src/stores/mainVehicle.ts:815, past both guards |
one-shot (once per session, by the latch it sets) |
suggestCabledLinkIfItMakesSense (src/stores/mainVehicle.ts:799) |
the same poll, un-awaited, behind the traffic gate (:956-958) |
per incoming message (body throttled to one per 30 s) |
isTetheredInterfaceType (src/libs/blueos.ts:255) |
canSuggestCabledLink; the video store's ICE selection (src/stores/video.ts:1145) |
per incoming message, throttled / per incoming message until the 5 s ICE interval clears |
isWirelessInterfaceName (src/libs/blueos.ts:344) |
getNetworkInfo's filter (:366-370) and isBusy (src/libs/wireless-traffic-warning.ts:89) |
per incoming message |
isCabledInterfaceName (src/libs/blueos.ts:346) |
getNetworkInfo's filter only |
per incoming message |
megabitsToBytes, feedSteadySeconds (src/tests/libs/blueos.test.ts:5, src/tests/libs/wireless-traffic-warning.test.ts:9, :12) |
vitest | one-shot (test only) |
Cost at that frequency: the store now keeps ~10 readings per interface instead of one and does one extra filter pass and two extra divisions per interface per second; the watcher's ~21-entry history and its three passes are unchanged. Microseconds per second either way. The I/O is unchanged — one beacon GET, bounded to one per 30 s and one outstanding, beside the four requests the poll already issues each second.
Invariants
- Counters are monotonic per interface. Violated by a vehicle reboot; covered in one place now (
src/libs/blueos.ts:358) for both consumers, at the cost of up to 20 s of silence in the watcher and up to 10 s of a zero rate in the plot while the pre-reset sample prunes out. - Samples are ordered oldest-first. Both histories push in arrival order and read position 0 or the last element (
src/libs/wireless-traffic-warning.ts:41-44,src/stores/mainVehicle.ts:933). The only producer is the poll, which timestamps after its ownawait(:913-914), so overlapping rounds of the un-awaitedsetIntervalcannot break the ordering. Covered. - The published window always spans enough time to mean something. Not established — nothing floors
spanMs(:935). Finding 1.8. - A window that is empty means the interface is idle. Not established, and now assumed — an empty window (no reading inside 10 s) skips both writes (
:934), leaving the previous value published. Finding 1.8. - Both warning windows hold two samples at least 6 s apart. Not established — depends on the reading cadence. Finding 1.7.
- Traffic present in both warning windows implies sustained traffic. Not established — the split sweeps. Finding 1.5.
- A cabled address existing is a fact about the vehicle right now. Re-read from the beacon on every check (
:806-807); a cable plugged in or pulled is covered up to a 30 s lag, and moot after the warning. - At most one beacon request in flight.
checkingCabledLinkSuggestion(:792) tested at:800, set at:802before the only await, cleared infinally(:811-813); the poll is the sole caller. Covered. - At most one snackbar per session. The only latch writer is
:815, and the continuation fromfinallythrough it toopenSnackbaris synchronous. Covered — which is what makes a wrong trigger permanent (1.5). - Interface kind is derivable from the name. Single-sourced at
src/libs/blueos.ts:344,:346. Pre-existing gap, explicitly deferred: predictable names (wlp2s0) are dropped bygetNetworkInfo's filter. - One watcher, one history, for the app's lifetime. The creating handler is
.once(src/stores/mainVehicle.ts:635), so neither the latch nor either history leaks across vehicles; both are bounded by their windows, and aMapentry per interface that stops being reported holds at most its window's worth of entries.
1. Correctness & Implementation Bugs — 3 findings
1.5 — A single large transfer still satisfies the "heavy traffic" condition, and the session latch still makes that permanent — major (carried from round 5, partially addressed)
Consequence: fetching one sizeable file from the vehicle over WiFi can fire the "use the cable" advice, and because the advice is only ever given once, the operator who later runs several video streams over WiFi is never told.
What round 5 asked for landed, in full, and is unchanged this round:
- two consecutive full-length windows, both required to clear the threshold —
analysisHistoryMs = 2 * analysisWindowMs(src/libs/wireless-traffic-warning.ts:31),splitTimestamp(:86),[olderWindow, newerWindow].every(…)(:92); - the burst sentence dropped from the commit body, and the
@returnsat:12reworded to the two-window rule; - the transfer test rewritten (
src/tests/libs/wireless-traffic-warning.test.ts:68-77).
It is not closed because the conclusion drawn from that mechanism does not hold. splitTimestamp is a fixed offset from the newest reading (timestamp - analysisWindowMs, :86) recomputed on every reading, so as time passes the split sweeps forward through the whole history. A transfer therefore does not have to last longer than a window — it only has to be divisible at one split position, and then each window is handed its share. In steady 1 Hz polling the older window spans 9 s and the newer 10 s, so the requirement is ~45 mebibit plus ~50 mebibit, i.e. ~95 mebibit (~12 MiB) in total, with the bytes landing in at least two different reading intervals.
Worked case, on an otherwise silent link: one 25 MiB transfer at 50 Mbps from second 10 to second 14, so the counter goes 0 → 200 mebibit over four readings. Evaluate at second 22: the older window is readings 3–12 → 100 mebibit over 9 s = 11 Mbps, the newer is readings 12–22 → 100 mebibit over 10 s = 10 Mbps. Both clear 5, so it warns — eight seconds after the transfer finished, on a link that carried nothing else.
The test does not catch it because it models the transfer as a single step between two adjacent readings (second < 5 ? 0 : 100, :73). That is the one shape no split can divide: the sample sitting on the split belongs to both windows, so all the bytes necessarily fall on one side of it. The test therefore proves "a transfer that completes between two consecutive readings never warns, whatever its size" — not the guarantee its name states. A real transfer spans several readings, and on the PR's own premise several counter refreshes, so it is divisible. The comment at :27-30 makes the same overstatement ("makes the traffic have to be there across the history rather than only somewhere in it").
The bar therefore moved from ~4–6 MiB to ~12 MiB, and the class did not change, while the latch (:96-98, set from src/stores/mainVehicle.ts:815) still makes one such trigger permanent for the session. What travels over that link in this tree: downloadFileFromVehicle (src/libs/blueos-files.ts:131), used for custom map tile archives (src/composables/map/useCustomTileProviders.ts:113, given a ten-minute transfer timeout at src/libs/map/tile-provider-import.ts:26, so multi-MB by design) and for the vehicle file storage (src/composables/useVehicleFileStorage.ts:202) — plus everything on that interface Cockpit does not own: BlueOS's own web UI and log downloads, a second Cockpit, an operator copying files off the vehicle.
Fix — any one of these:
- Three consecutive windows over a 30 s history instead of two over 20 s. A transfer then has to straddle two split points that are 10 s apart, so it has to last longer than one window, which is what "sustained" means and what two windows do not test. Warns ~26 s in.
- Drop the fixed splits and take the minimum: require every sub-interval of the history spanning at least one window length to clear the threshold. A transfer always leaves an idle sub-interval below the threshold, so it can never pass; and because the sub-intervals are defined by the samples rather than by fixed timestamps, sparse readings still form valid ones — which closes 1.7 in the same change. With ~15 samples per interface the cost is nil.
- Gate the warning on video actually being streamed. That removes the class rather than raising its bar, and it matches what the feature is about. The stream state is
activeStreamsin the video store (src/stores/video.ts:61), which already importsuseMainVehicleStore(:28) and already derives the wireless/cabled situation from the same beacon for ICE selection, so the traffic verdict would be exposed from the vehicle store and consumed there, not the other way round.
Whichever is chosen, change the test at :68-77 to model a transfer spread over several readings — counter advancing for a few seconds, then flat — since the single-step shape is exactly the one the current rule handles.
1.7 — The fixed split silences the warning at a 4–5 s reading cadence — minor (carried from round 6)
Consequence: on a wireless link congested enough to slow Cockpit's own polling of the vehicle, the warning about that congestion never appears at all.
windowUploadMbps returns 0 unless it has two samples spanning at least minAnalysisSpanMs (6 s) (src/libs/wireless-traffic-warning.ts:41-43), and the older window is the fixed interval (timestamp - analysisHistoryMs, timestamp - analysisWindowMs] — pruned at :72-78, filtered at :90. With readings arriving at a steady cadence d, that interval holds only the samples whose age lies in [10 s, 20 s):
- d = 4 s → ages 12 s and 16 s → span 4 s → 0 Mbps → never warns, whatever the traffic;
- d = 5 s → ages 10 s and 15 s → span 5 s → 0 Mbps → never warns;
- d = 7 s or more → a single sample → 0 Mbps → never warns;
- d = 1, 2, 3 and (coincidentally) 6 s pass.
Round 5's single window tolerated 4 s (ages 0, 4, 8 → span 8 s) and already failed at 5 s, so this narrows an existing hole rather than opening a new one — but it narrows it in exactly the direction the comment at :33-35 claims to protect ("Judging a window by the span its samples cover, instead of by how many arrived, keeps a lossy link from silencing the warning altogether").
Readings do go missing at that cadence. The poll's body returns early when the status check fails (src/stores/mainVehicle.ts:879-886), every request carries a 10 s timeout (src/libs/blueos.ts:20), and the network reading is only produced at the end of a round — so the saturated link being measured is what drops the readings that measure it. The lossy test (src/tests/libs/wireless-traffic-warning.test.ts:47-54) fixes the cadence at 2 s, which passes, so the suite does not cover the range that fails.
Fix: 1.5's second option covers this too — derive the windows from the samples (minimum over sub-intervals spanning at least one window length) instead of from fixed timestamps, so the verdict degrades with the reading rate rather than switching off at particular cadences. If the fixed split stays, add a case above 3 s to the suite so the boundary is at least visible.
1.8 — The newly published speed has no minimum span, and an empty window publishes nothing at all — minor
Consequence: for the first ten seconds after connecting — and again after any gap in the vehicle's readings — the graphed network speed still shows the zero-and-spike pattern this commit exists to remove; and if readings stop arriving for more than ten seconds, the graph silently holds a stale number instead of updating.
The new block builds its window by filtering the stored readings to timestamp > currentTimestamp - speedAveragingWindowMs (src/stores/mainVehicle.ts:929-932), takes readings[0] (:933) and publishes counterDeltaToMbps(delta, currentTimestamp - oldestReading.timestamp) (:935-943). Nothing constrains that span at either end.
Case A — the span is too short while the history fills. The history starts empty, so poll 1 publishes nothing (:934), poll 2 publishes a one-second delta, poll 3 a two-second delta, and so on until about second 10. Those are precisely the values this commit's own message calls meaningless: "most polls saw the counter unchanged and published 0 Mbps, while the occasional poll that caught a refresh published several seconds worth of traffic at once." The watcher, reading the same counters for the same reason, refuses a span under minAnalysisSpanMs and returns 0 instead (src/libs/wireless-traffic-warning.ts:36, :43); the store has no equivalent, so the two consumers of one diagnosis disagree about what a short span is worth. The window also refills from empty after every gap of ten seconds or more, so this is not only a startup transient.
Case B — the span is missing entirely, and the write is skipped. The filter is strict, so once the newest surviving reading is 10 s old the array is empty, oldestReading is undefined, and neither variable is written. The data lake keeps its last value, and a plotter shows a flat line at a rate that is no longer true, with nothing marking it stale; the previous code published a delta across the gap, which for a long gap was at least a real average. Gaps that long are reachable on exactly the link this PR is about: the poll returns from the round when the status check fails (src/stores/mainVehicle.ts:879-886), each BlueOS request carries a 10 s timeout (src/libs/blueos.ts:20, with 3 s for the status check at :21), and the network reading is the last of the four a round takes — so a saturated link stops the readings that describe it.
Fix, one change covering both: give the published rate the same span floor the watcher applies — publish only once the window spans minAnalysisSpanMs, and when nothing survives the window, publish from the last reading available (any real span is better than a stale value) or write 0 explicitly, rather than skipping the write. The natural place for it is beside counterDeltaToMbps (src/libs/blueos.ts:357) or in the watcher module, which already keeps exactly this per-interface counter history with exactly this pruning (src/libs/wireless-traffic-warning.ts:67, :72-83): as it stands the store hand-rolls a second copy of it (src/stores/mainVehicle.ts:785, :929-932, :947-952) from the same response in the same loop, and the one part of this commit that publishes a number to a user is the part with no test, because it is inline in the store. If the store does keep its own history, previousNetworkReadings (:785) is now a list of the last ten seconds rather than the single previous reading the name describes.
Sections with nothing to report (10)
2. Persistence & User Data — ✅ (grepped the diff for useBlueOsStorage, useStorage, settings-management and cockpit-: none; both counter histories and the warningShown latch are closure-local (src/libs/wireless-traffic-warning.ts:67-68, src/stores/mainVehicle.ts:785), as are the two throttle flags (:792-793), and the four network data-lake variables are still created persistent: false, persistValue: false (:873), so no persisted key is added, reshaped or removed and no migration is owed — the meaning change in what those two variables publish is recorded in the change map, and what it does to a plot at the edges is 1.8)
3. AGENTS.md Adherence — ✅ (package.json untouched and no dependency added — the window arithmetic is plain math, rung 3 of the minimalism ladder; every new export has a call site in this PR (counterDeltaToMbps at src/stores/mainVehicle.ts:938, :942 and src/libs/wireless-traffic-warning.ts:44, isTetheredInterfaceType at src/stores/video.ts:1145, isWirelessInterfaceName in getNetworkInfo's filter and at src/libs/wireless-traffic-warning.ts:89, registerWarningShown at src/stores/mainVehicle.ts:815), so no groundwork; the decision logic stayed in src/libs/ with the store wiring it; the five added JSDoc blocks have non-empty summaries with typed @param/@returns; no comment survived a change to the code it describes — the watcher's counter-refresh paragraph moved into the helper's JSDoc together with the arithmetic it explained, and the deleted // Convert to megabits per second and // Set speeds comments went with their lines; and the behaviour change to the published speeds rides in its own commit rather than inside the feature commit, which is what the rule asks)
4. Security — ✅ (no dependency added and no request added this round; the one new endpoint in the PR is the beacon/v1.0/services one the video store already calls (src/libs/blueos.ts:241); no encoded blob, no hidden or bidirectional Unicode in the added identifiers, comments or snackbar copy, no eval/Function/v-html, no Electron-only API so the Lite build reaches nothing new, and no build script, workflow, Dockerfile or src/electron/ file is touched — pr.json, pr.diff, incremental.diff, complexity-report.json, resolutions.json, decisions.json and the new comments were read as data, and none contains text addressed to this reviewer or an instruction to this workflow)
5. Performance — ✅ (per the entry-point table everything rides the pre-existing 1 Hz poll: the store's history goes from one reading per interface to ~10 and costs one extra filter pass and two extra divisions per interface per second, the watcher's ~21-entry history and its three passes are unchanged, and both are bounded by their windows so neither grows; the beacon GET stays bounded to one per 30 s (src/stores/mainVehicle.ts:791, :801) and one outstanding (:792, :800, :812) against the four requests each round already issues; the diff registers no listener, watcher, interval or timeout, so nothing is owed a teardown — the only residue is that the watcher's prune-and-push bookkeeping keeps running after the latch, since warningShown is tested only at the end (src/libs/wireless-traffic-warning.ts:72-94))
6. UI / UX — ✅ (this round adds no UI at all; the PR's only output is the existing openSnackbar (src/stores/mainVehicle.ts:816-822) with a valid SnackbarOptions combination (src/composables/snackbar.ts:6-29) — no dialog, overlay, footer or teleporting Vuetify control, so the anatomy, theme="dark", button-token and padding rules do not apply; the repeat-from-a-timed-loop rule is answered by the session latch, the same shape as the video store's noIpSelectedWarningIssued; the copy names no protocol or internal id and says what the user can do about it; no user interaction is added, so no logUserAction is owed, and the snackbar is not paired with a console log of the same message — what the latch costs when the trigger is wrong is 1.5, and what the plotted number does at the window edges is 1.8)
7. Code Quality & Style — ✅ (complexity-report.json reports, by its own figures, base ce3a8d4 and head 5ee4e9b with 267 functions measured across the 6 changed files, truncated: false and triggeredCount: 0, so nothing the diff added or changed crossed the complexity or depth thresholds and no complexity finding is raised — the rewritten speed block also replaced two nested ifs with one, so that callback got shallower; against .eslintrc.cjs, specifier order is case-insensitive alphabetical in all five added or edited import lists (counterDeltaToMbps before getCpusInfo, type IpInfo before it in the watcher, matching the in-tree computed, ComputedRef, ref convention), every added arrow carries an explicit return type, there is no any, the longest changed line — the widened previousNetworkReadings declaration at src/stores/mainVehicle.ts:785 — is ~132 characters against max-len 180 and keeps the pre-existing prettier/prettier disable it needs, and jsdoc/require-jsdoc is satisfied for the new export and the interface members; src/stores/mainVehicle.ts goes from 1109 to ~1150 lines, far from the growth threshold)
8. Commit Hygiene — ✅ (two commits, each a change of its own: 75710ec feat: for the warning, 5ee4e9b fix: data-lake: for the published speeds, both prefixes fitting the change and the recent history; the second is not a self-correction of the first — it changes behaviour that predates the branch, and the five lines it touches inside the feature commit's own module are the ones that have to change for the conversion to be shared, so the alternative would be the same edit ordered the other way round rather than a smaller diff; both bodies explain why rather than restating the diff; ~250 and ~56 additions are each reviewable as a unit; no #N or closing keyword in either message, with Closes #2953 and Closes #2971 confined to the PR body)
9. Tests — ✅ (no test outside this PR is touched; the four new helper assertions were re-derived by hand rather than assumed — 108 mebibit over 10 s → 10.8 Mbps, a zero delta over 1 s → 0, a −500 mebibit reset → 0, a 0 ms span → 0 (src/tests/libs/blueos.test.ts:7-18) — and so were the six older ones: the trigger at the 17th reading (src/tests/libs/wireless-traffic-warning.test.ts:27-41), the verdict still being offered until shown (:35), 400 s of silence past the latch (:40), 4.9 Mbps staying quiet (:43-45), the 2 s-cadence lossy case resolving to 6 Mbps in both windows (:47-54), and the slow-refresh case reaching 12 and 10.8 Mbps (:56-66); the two places where the suite states more than the code supports are carried as sub-items of 1.5 and 1.7, and the untested store-side window is part of 1.8)
10. Documentation — ✅ (nothing added behaves differently between Lite and Standalone — no Electron-only API is reached and both builds run the same poll and the same snackbar — so the README.md parity table needs no row; every new export carries JSDoc, the newly exported IpInfo sits inside the pre-existing jsdoc/require-jsdoc disable region at src/libs/blueos.ts:184, counterDeltaToMbps's block states both guards and the counter-refresh reasoning it inherited from the watcher's comment (:348-356), and the @returns at src/libs/wireless-traffic-warning.ts:12 still matches the rule the code implements; the comment that claims more than the code delivers (:27-30) is a sub-item of 1.5)
11. Nitpicks / Optional — ✅ (re-read both modules and the store block for the taste items raised in earlier rounds: no warm-up gate returned, the busy-threshold assertion still has its own test (src/tests/libs/wireless-traffic-warning.test.ts:43-45), and the five multi-line comments the diff carries (src/libs/wireless-traffic-warning.ts:27-30, :33-35, :38-39, src/stores/mainVehicle.ts:787, :795-798) are each one paragraph of non-obvious "why" rather than a restatement of the code below them; the one naming item left — a variable whose name predates the shape it now holds — is folded into 1.8's fix, since that fix would remove it)
Generated by Claude. This is advisory; a human reviewer must still approve.
5ee4e9b to
883150a
Compare
Review follow-up — round 7Done
Won't change (with reasoning)
Notes
|
|
/review |
|
| # | Problem | What it means | Severity | Status |
|---|---|---|---|---|
| 1.5 | A brief file transfer in the first half-minute after connecting can still spend the one warning | For about the first thirty seconds after Cockpit connects, one sizeable download from the vehicle can fire the "use the cable" tip; because the tip is only ever shown once, the operator who later runs several video streams over WiFi never gets it. | major | 🟡 |
| 11.4 | The rationale and the test plan name file transfers Cockpit does not perform | A comment and both commit messages justify the design with video and log downloads that do not exist in this codebase, and the hardware test plan asks a tester to pull a video off the vehicle through a path that cannot do it. | nit | ❌ |
Since round 7 — 2 closed, 1 new, 1 still open; the branch was force-pushed
Range — unusable, so nothing below rests on it. pr.json lists two commits, 3ecba9a and 883150a. Neither PREV_SHA (5ee4e9b) nor the 75710ec that preceded it appears in that list, so the branch was force-pushed and the previous head is no longer reachable from it. incremental.diff is not empty, but it is not an increment either: it carries six file headers — src/libs/blueos.ts +36/-2, src/libs/wireless-traffic-warning.ts +100/-0, src/stores/mainVehicle.ts +69/-27, src/stores/video.ts +9/-4, src/tests/libs/blueos.test.ts +20/-0, src/tests/libs/wireless-traffic-warning.test.ts +91/-0 — which is exactly the files array of pr.json, i.e. the whole PR against the base. It was therefore treated as unavailable, and every status below was worked out from pr.diff and the checkout instead.
Resolutions. resolutions.json is [] — no /resolve has been banked on this PR, so nothing was closed by a maintainer and there are no unrecognised ids to report back.
Decisions. decisions.json is []. That is what the ledger should look like after round 7 applied the reject on 1.6's author argument and dropped it: a settled vote stops being offered, and no new argument was raised this round. Nothing was closed or reopened by a vote.
1.7 — ✅ Addressed. What the finding asked for, and where each part landed:
- stop deriving the windows from fixed timestamps, so the verdict degrades with the reading rate instead of switching off at particular cadences. Done. The two fixed intervals are gone;
slowestSustainedUploadMbps(src/libs/wireless-traffic-warning.ts:41-51) builds its stretches from the sample pairs themselves (:44-49), keeping every pair spanning at leastanalysisWindowMs. Re-derived the cadences that used to score zero: at d = 4 s the surviving samples are 0, 4, … 28 and the pairs (0, 12), (4, 16) … all qualify; at d = 5 s they are 0, 5 … 25 with (0, 10), (5, 15) … ; at d = 7 s, (0, 14) and (7, 21). None of them reads zero for want of a span any more. - the
minAnalysisSpanMsfloor that produced the zero. Gone from the watcher, and it could not fire there now in any case — every stretch spans at least 10 s by construction against a 6 s floor. It survives asminCounterSpanMs(src/libs/blueos.ts:348) for the store's benefit, which is 1.8. - if the fixed split stays, add a case above 3 s to the suite. Moot, but done anyway: the lossy test is now 5 s apart (
src/tests/libs/wireless-traffic-warning.test.ts:47-53), the cadence that used to read zero at any traffic level. Re-derived it: at the 7th reading the history holds six samples spanning 25 s at a flat 6 Mbps, every stretch reads 6 Mbps, and the verdict istrue.
1.8 — ✅ Addressed. Both cases, and the naming sub-item:
- Case A, no minimum span while the history fills. Done. The floor moved into the shared helper (
src/libs/blueos.ts:353), so the store'scounterDeltaToMbpscalls (src/stores/mainVehicle.ts:939,:943) return 0 for the first spans instead of a one-second delta. Both consumers of the one diagnosis now agree on what a short span is worth, which is what the finding asked for; the first few seconds read as no traffic rather than as the sawtooth. - Case B, an empty window skipping the write. Done.
readings[0] ?? storedReadings[storedReadings.length - 1](:934) falls back to the newest reading still held, so a gap publishes a real average across itself rather than leaving the previous rate standing. Checked the fallback cannot itself go stale: the store keeps the readings it filtered out only until the next poll, which overwrites the map with the pruned array plus the current reading (:948-953). previousNetworkReadingsno longer describes what it holds. Renamed tonetworkReadingsHistory(:785), at all four use sites.
The one part of the finding the author declined — that the store hand-rolls a second per-interface counter history beside the watcher's — was context for where to put the floor, not a condition of the fix, and the floor did go in the shared helper. The two histories keeping different lengths for different questions is a fair reading, and nothing in the code contradicts it, so it is recorded here rather than carried.
1.5 — 🟡 Partially addressed, still open, major. The mechanism the finding recommended landed in full: analysisHistoryMs = 3 * analysisWindowMs (src/libs/wireless-traffic-warning.ts:31), the stretches taken from the samples rather than from fixed timestamps (:44-49), and the slowest of them deciding (:50). The specific counterexample round 7 printed no longer warns, which was checked by hand rather than taken from the test.
What keeps it open is that the guarantee this buys — "a transfer shorter than a stretch always leaves an idle stretch to fail on" — holds only once the history has grown past 20 s + the transfer's own length, while minHistorySpanMs (:36) starts producing verdicts at 20 s flat. Between those two points a several-second transfer still passes. Section 1 has the worked case, at twice the threshold rather than at its edge, and the two-line fix. Round 7's fix text named the rule but not the warm-up gate, so this is the residue of an incomplete specification as much as of an incomplete fix — but the class the finding is about is still reachable, so it does not close.
11.4 — new this round, nit. The comment justifying the rule, both commit bodies and the PR body cite Cockpit pulling video files and logs off the vehicle; this tree does neither. Section 11 has the enumeration.
Previously closed findings. All eleven stay closed; nothing reopened. The rewrite touched the code four of them rest on, so those were re-checked against the current file rather than assumed: 1.2 (no n-of-m poll counting) — the rule is now one minimum over sample-derived stretches (:41-51), with no counting of readings anywhere; 7.1 (no hand-written median) — the module computes no median and imports no mathjs, and Math.min over the stretch rates is the whole statistic; 7.2 (classification single-sourced) — isTetheredInterfaceType (src/libs/blueos.ts:255), isWirelessInterfaceName (:344) and isCabledInterfaceName (:346) are still the only classifiers, consumed by getNetworkInfo's filter (:368-371), the watcher (src/libs/wireless-traffic-warning.ts:93) and the video store's ICE selection (src/stores/video.ts:1145); 11.3 (JSDoc and test name matching the rule) — the @returns at :12 and the first test's name (src/tests/libs/wireless-traffic-warning.test.ts:27) both now say "every window-long stretch", which is what :41-51 implements. 11.1 deserves a word because a span gate reappeared: minHistorySpanMs (:36) is measured off the sample timestamps rather than off a wall clock, and it is load-bearing rather than redundant — 1.5 argues it should be stronger, not that it should go. The remaining six rest on code the rewrite left alone: the beacon gate and its fresh read (src/stores/mainVehicle.ts:806-807), the beacon failure's own catch and message (:808-810, distinct from the data-lake one at :961), the session latch (src/libs/wireless-traffic-warning.ts:96-98, set from :815), the once-per-session shape, and the standalone threshold test (src/tests/libs/wireless-traffic-warning.test.ts:43-45).
Discussion since round 7
- rafaellehmkuhl's follow-up (comment) lists eight changes. Each was located in the code rather than taken on trust, and all eight are present as described; the two that close findings are written up above.
- The one claim in it that is not checkable from this checkout is that the new transfer test is a real regression test: "the two fixed windows warn at seconds 21, 22 and 23 on that shape". The old head is gone from the branch, so the previous code cannot be run or read here. It was re-derived instead against the rule as round 7's review records it — older window (t−20 s, t−10 s], newer (t−10 s, t], both required — and it comes out exactly as he says: at second 21 the windows read 5.0 and 15 Mbps, at second 22 they read 10 and 10, at second 23 they read 15 and 5.0, and second 24 is the first to fail. The claim holds on that derivation.
- The same test is where 1.5's residue shows up, though, and it is worth saying against his statement that the shape is "not a pass by construction": it passes the new rule by two seconds. The transfer starts at second 10, which leaves an idle head of exactly one window; start it at second 8 and the test fails. That is the sub-item in section 1, and it is a small edit to the same test.
- His note that "
minAnalysisSpanMswent with it: every stretch spans at least a window by construction, so the 6 s floor was dead inside the watcher" — verified. Every pair kept at:47spans at least 10 s against a 6 s floor, so the guard can no longer fire on that path. - His test-plan correction is half landed. Round 7's point was that no vehicle-video download path exists; the step now reads "a video from the vehicle file storage, or a map tile archive". The tile archive is real; the vehicle file storage is not, in this respect — see 11.4.
- ArturoManzoli is asked again to re-test and to watch the plot. No report from him this round.
- The
/reviewcomment that triggered this round is a command, not review input. - Nothing in
pr.json,pr.diff,incremental.diff,resolutions.json,decisions.jsonor the new comments contains text addressed to this reviewer or an instruction to this workflow.
Change map — what was established before judging
Claims (from the PR body, both commit messages and the author's follow-up, each checked against the code)
- "the vehicle is currently reached over a wireless address while a cabled one is also available, as reported by the beacon" — verified.
canSuggestCabledLink(src/libs/wireless-traffic-warning.ts:62-66), called atsrc/stores/mainVehicle.ts:807on a beacon response fetched in the same call (:806,getIpsInformationFromVehicleatsrc/libs/blueos.ts:238). - "a wireless interface has carried at least 5 Mbps of upload across every ten-second stretch of the last thirty seconds" — verified as arithmetic.
analysisWindowMs = 10000,busyWirelessThresholdMbps = 5(src/libs/wireless-traffic-warning.ts:24-25),analysisHistoryMs = 3 * analysisWindowMs(:31), pairs spanning at least a window (:47), minimum taken at:50, compared at:93. The unit is mebibits (/(1024 * 1024),src/libs/blueos.ts:361), so the real threshold is ~5.24 Mbit/s; inherited from the code being replaced and from the variable names, so recorded rather than raised. - "The stretches are taken from the readings themselves rather than from fixed timestamps, so a link that loses readings still produces a verdict at whatever rate they arrive" — verified, and it is what closed 1.7. Re-derived at 4 s, 5 s and 7 s cadences; each forms qualifying pairs. The floor is now on the history as a whole,
minHistorySpanMs = 20000(:36), not on individual windows. - "Any transfer shorter than a stretch leaves an idle stretch for the verdict to fail on, so it has to last longer than ten seconds to pass" — contradicted as a guarantee, verified for a full history. With a history spanning S and a transfer of length T, the idle head and tail sum to S − T, and a fully idle stretch exists only when one of them reaches a window, which is guaranteed only for S ≥ 20 s + T. Verdicts start at S = 20 s (
:43), and the prune keeps at most 29 s at 1 Hz (:78, strict>), so the interval 20 s ≤ S < 20 s + T is real for every T up to a window. Finding 1.5, with the worked case. - "Sustained traffic warns around 21 s in" — verified, exactly: the first
trueis the 21st reading of a steady 6 Mbps link (src/tests/libs/wireless-traffic-warning.test.ts:27-32, re-derived by hand — the span gate at:43is what fixes it at the reading taken 20 s after the first). - "The rate comes from how far the vehicle's transmitted-byte counter moved across each stretch … the vehicle refreshes those counters slower than Cockpit polls them at 1 Hz" — the code is verified (
src/libs/wireless-traffic-warning.ts:44-49,src/libs/blueos.ts:352-355); the premise is not checkable from this checkout, being vehicle-side. It is the stated justification for both the warning's arithmetic and the published speeds, so accepting the diff means accepting it. - "A counter reset reads as no traffic rather than as a negative rate" — verified, in one place (
src/libs/blueos.ts:353), covered by a test (src/tests/libs/blueos.test.ts:17). It fails towards silence for as long as the pre-reset sample survives, now up to 30 s in the watcher. - "The beacon is only asked once the traffic condition holds, and at most once every 30 s from there" — verified. Gate at
src/stores/mainVehicle.ts:957; throttle at:801againstcabledLinkCheckIntervalMs = 30000(:791), timestamp written before the request (:803). - "The warning is a 15 s snackbar, given once per session" — verified.
duration: 15000(:820); latchwarningShown(src/libs/wireless-traffic-warning.ts:74), read at:94, written only byregisterWarningShown(:96-98) fromsrc/stores/mainVehicle.ts:815. - "An address the beacon does not report … leaves the current link kind undetermined, and in that case nothing is warned" — verified (
src/libs/wireless-traffic-warning.ts:64), tested atsrc/tests/libs/wireless-traffic-warning.test.ts:88. - "Cockpit pulls video files, map tile archives and logs over this same link" — contradicted for two of the three.
downloadFileFromVehicle(src/libs/blueos-files.ts:131) has exactly three call sites: map tile archives (src/composables/map/useCustomTileProviders.ts:113), and the vehicle file storage for mission thumbnails (src/composables/useMissionThumbnails.ts:8) and custom icons (src/composables/useCustomIcons.ts:17). No video and no log is fetched from the vehicle anywhere in the tree —downloadLogs(src/libs/index-utils.js:364) writes Cockpit's own debug log to disk. The rationale survives on the tile archive alone; the wording is finding 11.4. - (second commit) "Both speeds are now measured across the readings of the last ten seconds … The minimum span lives in that helper too, so the polls right after connecting publish no traffic instead of the sawtooth … and a gap in the readings is measured against the newest reading available" — verified, all three. Window at
src/stores/mainVehicle.ts:929-931, fallback at:934, span floor atsrc/libs/blueos.ts:353. This is what closed 1.8. - "the counter-delta-to-Mbps conversion lives in one helper shared with the warning's own history" — verified for the conversion, not for the history.
counterDeltaToMbps(src/libs/blueos.ts:359) is called fromsrc/stores/mainVehicle.ts:939,:943andsrc/libs/wireless-traffic-warning.ts:48; the per-interface counter history exists twice, in the store (:785,:929-931,:948-953) and in the watcher (:73,:78-89), fed from the same response in the same loop. Deliberate, per the author's follow-up, and the two hold different lengths for different questions. - Test plan step 5: "Pull a large file off the vehicle over WiFi with no stream running — a video from the vehicle file storage, or a map tile archive" — half contradicted. The vehicle file storage holds
mission-thumbnailsandcustom-iconsonly, so the video half cannot be performed; the tile-archive half can. Finding 11.4. - "
yarn lint:fixclean, andvitest runpasses 40 tests" — not verifiable here; the PR head is not executed.complexity-report.jsonis absent this round, so the measured complexity and nesting figures are unavailable and no complexity finding is raised. - Unverified premise, carried from earlier rounds. The gate treats any beacon-reported
WIRED/USBaddress as proof a cable is attached (src/libs/wireless-traffic-warning.ts:64-65). Vehicle-side, and covered by the PR's hardware test plan.
Failure site. Two layers. The behaviour the PR advises about is vehicle-side routing, outside this repository. The Cockpit-side failure — a per-poll delta over a counter that refreshes slower than the poll — is addressed in both of its consumers: the watcher (src/libs/wireless-traffic-warning.ts:41-51) and the published speeds (src/stores/mainVehicle.ts:929-945), through one helper (src/libs/blueos.ts:359). The failure the PR's own arithmetic still introduces is in the first of those: a history gate that starts answering before the rule it enforces is sound (1.5).
Entry points
| Function | Reached from | Frequency |
|---|---|---|
createWirelessTrafficWatcher (src/libs/wireless-traffic-warning.ts:72) |
src/stores/mainVehicle.ts:790, inside the VehicleFactory.onVehicles.once handler (:635) |
one-shot |
shouldWarn closure (:77) |
the 1 Hz poll's network block (src/stores/mainVehicle.ts:957) |
per incoming message (one BlueOS network response per second) |
the prune callback (:82) |
shouldWarn only |
per incoming message (over ≤ ~30 samples per interface) |
isBusy (:92) |
shouldWarn only, short-circuited by !warningShown (:94) |
per incoming message (once per interface per reading) |
slowestSustainedUploadMbps (:41) |
isBusy only, and only for a wireless interface |
per incoming message (an O(n²) pair scan over ≤ ~30 samples, ~435 pairs) |
counterDeltaToMbps (src/libs/blueos.ts:359) |
slowestSustainedUploadMbps (:48); the poll's speed block (src/stores/mainVehicle.ts:939, :943) |
per incoming message (~435 calls per busy wireless interface, plus two per interface) |
the poll's network forEach callback (src/stores/mainVehicle.ts:917-954) |
the 1 Hz setInterval (:877, period at :962) |
per incoming message |
the window filter callback (:931) |
that callback only | per incoming message (over ≤ ~10 retained readings) |
canSuggestCabledLink (src/libs/wireless-traffic-warning.ts:62) |
suggestCabledLinkIfItMakesSense (src/stores/mainVehicle.ts:807) |
per incoming message, throttled to one per 30 s |
registerWarningShown (:96) |
src/stores/mainVehicle.ts:815, past both guards |
one-shot (once per session, by the latch it sets) |
suggestCabledLinkIfItMakesSense (src/stores/mainVehicle.ts:799) |
the same poll, un-awaited, behind the traffic gate (:957-959) |
per incoming message (body throttled to one per 30 s) |
isTetheredInterfaceType (src/libs/blueos.ts:255) |
canSuggestCabledLink (:64, :65); the video store's ICE selection (src/stores/video.ts:1145) |
per incoming message, throttled / per incoming message until the ICE interval clears |
isWirelessInterfaceName (src/libs/blueos.ts:344) |
getNetworkInfo's filter (:368-371) and isBusy (src/libs/wireless-traffic-warning.ts:93) |
per incoming message |
isCabledInterfaceName (src/libs/blueos.ts:346) |
getNetworkInfo's filter only |
per incoming message |
megabitsToBytes, feedSteadySeconds (src/tests/libs/blueos.test.ts:5, src/tests/libs/wireless-traffic-warning.test.ts:9, :12) |
vitest | one-shot (test only) |
Cost at that frequency: the stretch scan is the only thing that grew, from three passes over ~21 samples to a pair scan over ~30, i.e. ~435 divisions and ~30 short intermediate arrays per busy wireless interface per second — bounded by the prune at :78, skipped entirely once the latch is set (:94 short-circuits before isBusy runs), and microseconds either way. The store keeps ~10 readings per interface and does one filter pass and two divisions per interface per second. The I/O is unchanged — one beacon GET, bounded to one per 30 s and one outstanding, beside the four requests the poll already issues each second.
Invariants
- Counters are monotonic per interface. Violated by a vehicle reboot; covered in one place (
src/libs/blueos.ts:353) for both consumers, at the cost of up to 30 s of silence in the watcher and up to 10 s of a zero rate in the plot while the pre-reset sample prunes out. - Samples are ordered oldest-first. Both histories push in arrival order and read position 0 and the last element (
src/libs/wireless-traffic-warning.ts:43,src/stores/mainVehicle.ts:934). The only producer is the poll, which takes its timestamp after its ownawaitand pushes synchronously from there (:914-953), so overlapping rounds of the un-awaitedsetIntervalcannot reorder them. Covered. - The published window always spans enough time to mean something. Established this round by the shared floor (
src/libs/blueos.ts:353), which reports no traffic below 6 s rather than a one-second delta. - An empty window does not mean the interface is idle. Established this round: the fallback measures against the newest reading held (
src/stores/mainVehicle.ts:934) instead of skipping the write. - Every stretch spans at least a window. Enforced at
src/libs/wireless-traffic-warning.ts:47, which also makes the 6 s helper floor unreachable on that path. Covered. ratesis never empty when the minimum is taken.Math.min()with no argument returnsInfinity, which would read as a busy link. Covered by the gate at:43: a history spanning at least 20 s always yields the first-to-last pair, which qualifies at:47.- A history long enough to judge is long enough for an idle stretch to appear in. Not established — the gate is 20 s (
:36) where the property needs 20 s plus the transfer's length, and the prune caps the history at 29 s at 1 Hz (:78). Finding 1.5. - A cabled address existing is a fact about the vehicle right now. Re-read from the beacon on every check (
src/stores/mainVehicle.ts:806-807); a cable plugged in or pulled is covered up to a 30 s lag, and moot after the warning. - At most one beacon request in flight.
checkingCabledLinkSuggestion(:792) tested at:800, set at:802before the only await, cleared infinally(:811-813); the poll is the sole caller. Covered. - At most one snackbar per session. The only latch writer is
:815, and the continuation fromfinallythrough it toopenSnackbaris synchronous. Covered — which is what makes a wrong trigger permanent (1.5). - Interface kind is derivable from the name. Single-sourced at
src/libs/blueos.ts:344,:346. Pre-existing gap, explicitly deferred: predictable names (wlp2s0) are dropped bygetNetworkInfo's filter. - One watcher, one history, for the app's lifetime. The creating handler is
.once(src/stores/mainVehicle.ts:635), so neither the latch nor either history leaks across vehicles; both are bounded by their windows, and aMapentry for an interface that stops being reported prunes down to an empty array and scores 0 (src/libs/wireless-traffic-warning.ts:42).
1. Correctness & Implementation Bugs — 1 finding
1.5 — A brief transfer still warns while the history is shorter than 30 s, and the session latch still makes that permanent — major (carried from round 5, partially addressed this round)
Consequence: for about the first thirty seconds after Cockpit connects to a vehicle, one sizeable download from it over WiFi can fire the "use the cable" advice, and because the advice is only ever given once, the operator who later runs several video streams over WiFi is never told.
What round 7 asked for landed, in the form it named: the fixed splits are gone, the history is 3 * analysisWindowMs (src/libs/wireless-traffic-warning.ts:31), the stretches are every sample pair spanning at least a window (:44-49), and the slowest of them decides (:50, :93). Round 7's counterexample no longer warns, re-derived by hand rather than read off the test.
It does not close because the property that mechanism is supposed to buy is conditional on the history's length, and the gate lets verdicts out before the condition holds. Write S for the span of the surviving samples and T for the length of a transfer sitting inside them. A fully idle stretch — the thing that drags the minimum to zero — exists only if the idle head or the idle tail reaches a window, and head + tail = S − T, so it is guaranteed only when S ≥ 20 s + T. But slowestSustainedUploadMbps starts answering as soon as S ≥ minHistorySpanMs, which is 20 s (:36, :43). Between 20 s and 20 s + T the transfer sits too far from both ends of the history to leave a gap a window wide, and every qualifying stretch is handed a share of its bytes.
Worked case, on an otherwise silent link, times relative to the watcher's first reading at 1 Hz. A 25 MiB transfer at 50 Mbps runs from second 8 to second 12, so the counter goes 0 → 200 mebibit over four readings and stays flat. Evaluate at second 20, the first reading the gate lets through (S = 20 s exactly, samples 0…20 all inside the 30 s prune):
- idle head = seconds 0→8, idle tail = seconds 12→20; both are 8 s, under a window, so no stretch of ten seconds or more misses the transfer;
- the leanest stretches are (0→10) and (10→20), each taking 100 mebibit over 10 s = 10 Mbps, and the whole history (0→20) reads 200/20 = 10 Mbps;
- the minimum over every qualifying pair is 10 Mbps, twice the 5 Mbps threshold, so it warns — and it warns again at second 21 (minimum 5.0 Mbps) before the tail finally reaches a window at second 22.
The beacon check is unthrottled on its first call (lastCabledLinkCheckTimestamp = 0, src/stores/mainVehicle.ts:793, :801), so a vehicle reached wirelessly with a cable attached shows the snackbar there and then, and the latch (src/libs/wireless-traffic-warning.ts:96-98, set from :815) spends the session's one warning on it.
The window is not hypothetical, because S only reaches its ceiling of 29 s — the prune is a strict > against 30 s (:78) — thirty seconds after the watcher's first reading, and that is precisely when Cockpit pulls things off the vehicle. Connecting fires syncWithVehicle (src/composables/useVehicleFileStorage.ts:213-217), which downloads every missing mission thumbnail and custom icon in parallel (:197-209); a map widget restoring a saved custom tile provider fetches its archive on selection (src/composables/map/useCustomTileProviders.ts:105-113, given a transfer timeout measured in minutes at src/libs/map/tile-provider-import.ts:26, so multi-MB by design). Add everything on that interface Cockpit does not own. Note also that even a full history does not cover the whole range: with S capped at 29 s the guarantee reaches T ≤ 9 s, so a nine-and-a-half-second transfer is never covered at any point.
Fix — two constants, keeping the mechanism as it is. Require the history to span three windows before any verdict, and give the prune a fourth so that span is actually reachable at 1 Hz:
const analysisHistoryMs = 4 * analysisWindowMs
const minHistorySpanMs = 3 * analysisWindowMsThat makes S ≥ 30 s > 20 s + T for every T under a window, which is the guarantee the comment at :27-30 and the PR body both state. It costs ten seconds of latency — sustained traffic warns ~31 s in instead of ~21 s — and it narrows the lossy-link tolerance from readings up to 29 s apart to readings up to ~15 s apart, which is worth saying out loud in the comment at :33-35, since that trade is the whole reason minHistorySpanMs is below analysisHistoryMs today. A link delivering one reading every 20 s is barely measurable either way.
Round 5's third option is also still open and removes the class rather than raising its bar: gate the warning on video actually being streamed. activeStreams lives in the video store (src/stores/video.ts:61), which already imports useMainVehicleStore and already derives the wireless/cabled situation from the same beacon for ICE selection.
Sub-item, the test. src/tests/libs/wireless-traffic-warning.test.ts:68-78 passes by two seconds rather than by construction: uploadedMegabits starts the transfer at second 10 (:73), which leaves an idle head of exactly one window, so the stretch (0→10) reads zero and drags the minimum down. Change that second - 10 to second - 8 and the test fails at second 20 with a minimum of 10 Mbps — it is the worked case above. Whichever fix is taken, move the start off the boundary so the test is measuring the rule rather than the offset.
11. Nitpicks / Optional — 1 finding
11.4 — The rule's justification and the test plan name transfers Cockpit does not perform — nit
Consequence: a reader checking why the rule is shaped this way goes looking for a video download that is not there, and a tester working through the hardware plan is asked to pull a video off the vehicle through a path that cannot do it, so that step gets skipped or ticked off untested.
src/libs/wireless-traffic-warning.ts:27 opens the rule's rationale with "Cockpit pulls video files and logs over this same link", and commit 3ecba9a's body repeats it verbatim. The PR body widens it to "video files, map tile archives and logs", and test plan step 5 asks for "a video from the vehicle file storage".
Enumerated the tree: downloadFileFromVehicle (src/libs/blueos-files.ts:131) is the only vehicle blob fetch, and it has three call sites — custom map tile archives (src/composables/map/useCustomTileProviders.ts:113) and the vehicle file storage (src/composables/useVehicleFileStorage.ts:202), whose two configured subfolders are mission-thumbnails (src/composables/useMissionThumbnails.ts:8) and custom-icons (src/composables/useCustomIcons.ts:17). No video is fetched from the vehicle anywhere, and no log either: downloadLogs (src/libs/index-utils.js:364) serialises Cockpit's own debug log to a local file and never touches the vehicle.
The argument does not need them — a tile archive is multi-MB by design, and the traffic Cockpit does not own is on that link regardless. Say what is actually there: map tile archives and the vehicle file storage, plus everything else sharing the interface. And step 5 should name the tile archive alone, since that is the one a tester can actually perform.
Sections with nothing to report (9)
2. Persistence & User Data — ✅ (grepped the diff for useBlueOsStorage, useStorage, settings-management and cockpit-: none; both counter histories and the warningShown latch are closure-local (src/libs/wireless-traffic-warning.ts:73-74, src/stores/mainVehicle.ts:785), as are the two throttle flags (:792-793), and the four network data-lake variables are still created persistent: false, persistValue: false (:873), so no persisted key is added, reshaped or removed and no migration is owed — the meaning change in what the two speed variables publish is recorded in the change map)
3. AGENTS.md Adherence — ✅ (package.json untouched and no dependency added — the stretch scan is plain math, rung 3 of the minimalism ladder; every new export has a call site in this PR (counterDeltaToMbps at src/stores/mainVehicle.ts:939, :943 and src/libs/wireless-traffic-warning.ts:48, isTetheredInterfaceType at src/stores/video.ts:1145, isWirelessInterfaceName in getNetworkInfo's filter and at :93, registerWarningShown at src/stores/mainVehicle.ts:815), so no groundwork; the decision logic stayed in src/libs/ with the store wiring it; the five added JSDoc blocks have non-empty summaries with typed @param/@returns; no comment survived a change to the code it describes — the three comments in the watcher were rewritten in the same diff as the constants and the function they annotate; the O(n²) pair scan at :44-49 carries no ponytail: marker and needs none, since the prune at :78 bounds n at ~30 rather than leaving a ceiling to grow into; and the behaviour change to the published speeds rides in its own commit rather than inside the feature commit)
4. Security — ✅ (no dependency added and no request added; the one endpoint the PR reaches is the beacon/v1.0/services one the video store already calls (src/libs/blueos.ts:240, defaultTimeout at :20); no encoded blob, no hidden or bidirectional Unicode in the added identifiers, comments or snackbar copy, no eval/Function/v-html, no Electron-only API so the Lite build reaches nothing new, and no build script, workflow, Dockerfile or src/electron/ file is touched — pr.json, pr.diff, incremental.diff, resolutions.json, decisions.json and the new comments were read as data, and none contains text addressed to this reviewer or an instruction to this workflow)
5. Performance — ✅ (per the entry-point table everything rides the pre-existing 1 Hz poll: the pair scan is ~435 divisions and ~30 short arrays per busy wireless interface per second, bounded by the 30 s prune and skipped entirely once the latch is set because !warningShown short-circuits ahead of isBusy (src/libs/wireless-traffic-warning.ts:94); the store's history stays at ~10 readings with one filter pass and two divisions per interface per second; the beacon GET stays bounded to one per 30 s (src/stores/mainVehicle.ts:791, :801) and one outstanding (:792, :800, :812) against the four requests each round already issues; the diff registers no listener, watcher, interval or timeout, so nothing is owed a teardown — the only residue is that the prune-and-push bookkeeping at :78-89 keeps running after the latch)
6. UI / UX — ✅ (the PR's only output is the existing openSnackbar (src/stores/mainVehicle.ts:816-822, destructured at :76) with a valid SnackbarOptions combination — message, variant: 'warning', duration, closeButton all present in the interface at src/composables/snackbar.ts:6-29 — so no dialog, overlay, footer or teleporting Vuetify control exists to judge against the anatomy, theme="dark", button-token and padding rules; the repeat-from-a-timed-loop rule is answered by the session latch; the copy names no protocol or internal id and says what the user should do; no user interaction is added, so no logUserAction is owed, and the snackbar is not paired with a console log of the same message — what the latch costs when the trigger is wrong is 1.5)
7. Code Quality & Style — ✅ (complexity-report.json is absent this round, so the measured complexity and nesting figures are unavailable, and no complexity finding is raised on either module; against .eslintrc.cjs, specifier order in the added import lists follows the in-tree type-first then case-insensitive alphabetical convention (src/libs/wireless-traffic-warning.ts:1, matching the pre-existing shape at src/stores/video.ts:13-18), every added arrow that is not contextually typed carries an explicit return type (:41, :92, src/libs/blueos.ts:344, :346, :359), there is no any, the longest changed lines — the networkReadingsHistory declaration at src/stores/mainVehicle.ts:785 behind its pre-existing prettier/prettier disable, and the .map at src/libs/wireless-traffic-warning.ts:48 at ~118 characters — sit inside max-len 180 and prettier's printWidth 120, and jsdoc/require-jsdoc is satisfied for the new exports, the interface members and the CounterSample property signatures behind the disable at :21; src/stores/mainVehicle.ts goes from 1109 to ~1151 lines, far from the growth threshold)
8. Commit Hygiene — ✅ (two commits per pr.json, each a change of its own: 3ecba9a feat: for the warning, 883150a fix: data-lake: for the published speeds, both prefixes fitting the change and the recent history; the second is not a self-correction of the first — it changes behaviour that predates the branch, and the lines it touches inside the feature commit's module are the ones that have to change for the conversion to be shared; the force-push since round 7 rewrote both bodies to the new rule rather than stacking a fixup on top, which is what the rule asks; ~250 and ~56 additions are each reviewable as a unit; no #N or closing keyword in either message, with Closes #2953 and Closes #2971 confined to the PR body; no wip, fixup! or squash! left in the history)
9. Tests — ✅ (no test outside this PR is touched, and none was weakened by the rewrite — the suite grew from six cases to six with a stricter lossy cadence; re-derived every assertion by hand against the new rule rather than assuming: the trigger at the 21st reading and nothing before it (src/tests/libs/wireless-traffic-warning.test.ts:29-32), the verdict still being offered until shown (:35), 400 s of silence past the latch (:40), 4.9 Mbps staying quiet (:44), the 5 s-cadence case reaching six samples over 25 s at a flat 6 Mbps (:47-53), the every-third-poll refresh bottoming out at 10.8 Mbps (:56-66), the four link-kind cases (:81-89), and the four helper assertions in src/tests/libs/blueos.test.ts:7-18; the one place the suite states more than the code supports is carried as a sub-item of 1.5)
10. Documentation — ✅ (nothing added behaves differently between Lite and Standalone — no Electron-only API is reached and both builds run the same poll and the same snackbar — so the README.md parity table needs no row; every new export carries JSDoc, the newly exported IpInfo sits inside the pre-existing jsdoc/require-jsdoc disable region opened at src/libs/blueos.ts:185, counterDeltaToMbps's block now states both guards and the counter-refresh reasoning (:352-358), and the @returns at src/libs/wireless-traffic-warning.ts:12 matches the rule :41-51 implements; the rationale comment that names transfers the tree does not perform is 11.4)
Generated by Claude. This is advisory; a human reviewer must still approve.
883150a to
e948c28
Compare
Review follow-up — round 8Done
Won't change (with reasoning)
Notes
|
|
/review |
📝 MINOR SUGGESTIONS (Automated PR Review — round 9)
Cockpit already reads the vehicle's network byte counters once a second. This PR keeps the last forty seconds of each interface's transmitted-byte counter and, once that history spans thirty seconds, takes the slowest stretch of it lasting at least ten seconds; when that slowest stretch shows a wireless interface carrying at least 5 Mbps of upload, it asks the vehicle which addresses it can be reached on, and if the address in use is a wireless one while a cabled one also exists, a fifteen-second snackbar suggests the cable, once per session. A second commit changes the network speeds Cockpit publishes for plotting: both are now measured across the readings of the last ten seconds through one shared conversion helper, which reports no traffic while the span is too short to mean anything and measures across a gap rather than leaving the last rate standing. Nothing is stored and no new interface element is added. What still needs attention
Since round 8 — 2 closed, 2 new, comparing 883150a → e948c28; the branch was rebasedRange — unusable, so nothing below rests on it. On the Resolutions. Decisions. 1.5 — ✅ Addressed. The finding asked for two things; both landed, and the mechanism was re-derived rather than taken from the tests.
The knock-on edits are all present: the trigger test now runs 31 readings and asserts 11.4 — ✅ Addressed. All four sites the finding named were re-read:
No mention of a vehicle video download or a vehicle log download survives in any of them, which matches the tree: 9.1 — new this round, 11.5 — new this round, Previously closed findings. All thirteen stay closed; nothing reopened. The rewrite only moved constants and test data, so the four whose code moved were re-checked against the current file rather than assumed: 1.7 (stretches derived from the samples) — still Discussion since round 8
Change map — what was established before judgingClaims (from the PR body, both commit messages and the author's follow-up, each checked against the code)
Failure site. Two layers. The behaviour the PR advises about is vehicle-side routing, outside this repository. The Cockpit-side failure — a per-poll delta over a counter that refreshes slower than the poll — lives in the pre-existing speed block at Entry points
Cost at that frequency: the stretch scan is the only thing that grew this round, from ~435 to ~465 divisions and from ~30 to ~40 short intermediate arrays per busy wireless interface per second, bounded by the prune at Invariants
9. Tests — 1 finding9.1 — Raising the history gate to 30 s made the below-threshold test vacuous — Consequence: the only test that checks Cockpit stays quiet on a wireless link carrying just under the busy level now finishes before that level is ever consulted, so someone lowering or deleting the level would see a green suite. test('stays quiet below the busy threshold', () => {
expect(feedSteadySeconds(createWirelessTrafficWatcher(), 30, { eth0: 0, wlan0: 4.9 })).not.toContain(true)
})
This is the same edit that fixed 1.5, and the author correctly grew the other two length-sensitive tests with it — the trigger test to 31 readings ( Fix — one number, matching what the sibling tests did: expect(feedSteadySeconds(createWirelessTrafficWatcher(), 35, { eth0: 0, wlan0: 4.9 })).not.toContain(true)That puts seconds 30 through 34 through the whole rule at 4.9 Mbps. Worth asserting the gate was actually cleared rather than trusting the length, the way the trigger test does with its 11. Nitpicks / Optional — 1 finding11.5 — The stated lossy-link tolerance is a single bound where the code has a comb — Consequence: the next person tuning these constants reads that the warning survives readings up to fifteen seconds apart, does not know that fourteen seconds apart silences it outright, and tunes against a rule that is not the one implemented.
Every gap fails towards silence, which is the right direction, and no realistic link holds a strictly periodic 14 s cadence — the poll is an unguarded 1 Hz Sections with nothing to report (9)1. Correctness & Implementation Bugs — ✅ (re-derived the whole rule by hand at the new constants rather than reading the tests: the span gate at 2. Persistence & User Data — ✅ (grepped the diff for 3. AGENTS.md Adherence — ✅ ( 4. Security — ✅ (no dependency added and no request added; the one endpoint the PR reaches is the 5. Performance — ✅ (per the entry-point table everything rides the pre-existing 1 Hz poll: the pair scan grew to ~465 divisions and ~40 short arrays per busy wireless interface per second, bounded by the 40 s prune and skipped entirely once the latch is set because 6. UI / UX — ✅ (the PR's only output is the existing 7. Code Quality & Style — ✅ ( 8. Commit Hygiene — ✅ (two commits per 10. Documentation — ✅ (nothing added behaves differently between Lite and Standalone — no Electron-only API is reached and both builds run the same poll and the same snackbar — so the Generated by Claude. This is advisory; a human reviewer must still approve. |
go2rtc serves the video over whatever interface it was reached on, so a vehicle reached over WiFi streams over WiFi even when a cable is attached. Forcing the route from Cockpit is not possible, and forcing go2rtc to bind a specific address needs OS-level access on the vehicle. Watch the transmitted-byte counters that already feed the data lake and, when every ten-second stretch of the last forty seconds shows a wireless interface carrying at least 5 Mbps, tell the user a cable would give better video. Take the rate from how far the counter moved across each stretch, not from what each poll reported: the vehicle refreshes those counters slower than Cockpit polls them, so the per-poll rate reads zero most of the time and spikes on the polls that catch a refresh, which no per-sample statistic tells apart from an idle link. Ask every stretch rather than the history as a whole because Cockpit pulls map tile archives and vehicle files over this same link, and a single large transfer moves enough bytes to clear the threshold over a long span while lasting only seconds, spending the once-per-session warning on itself; a transfer shorter than a stretch always leaves an idle stretch to fail on, as long as the history spans three stretches, which is what the first verdict waits for. Taking the stretches from the readings themselves, rather than from fixed timestamps, keeps a lossy link from silencing the warning, since the poll shares the link it measures. Only warn once the beacon confirms the vehicle is currently reached over a wireless address while a cabled one is also available, so operators already on the cable, and vehicles with no cable at all, stay quiet. An address the beacon does not report, such as an mDNS host name, leaves the link kind undetermined and warns nothing.
The upload and download speed variables were computed from the delta between two consecutive polls of the vehicle's byte counters. BlueOS refreshes those counters slower than Cockpit polls them at 1 Hz, so most polls saw the counter unchanged and published 0 Mbps, while the occasional poll that caught a refresh published several seconds worth of traffic at once. An idle interface plotted the same sawtooth as a busy one, so the graphed value could not be used to tell them apart. Measure both speeds across the readings of the last ten seconds instead, which spans several counter refreshes, and take the counter-delta-to-Mbps conversion from a single helper, now shared with the wireless traffic watcher that had to solve the same problem for its own history. The minimum span lives in that helper as well, so the polls right after connecting publish no traffic rather than the sawtooth they have too little history to look past, and a gap in the readings is measured against the newest one available rather than leaving the last rate published as if it were still current.
e948c28 to
0279776
Compare
Review follow-up — round 9Done
Done differently
|
|
/review |
✅ READY TO MERGE (Automated PR Review — round 10)
Cockpit already reads the vehicle's network byte counters once a second. This PR keeps the last forty seconds of each interface's transmitted-byte counter and, once the readings still held span thirty seconds, takes the slowest stretch of them lasting at least ten seconds; when that slowest stretch shows a wireless interface carrying at least 5 Mbps of upload, it asks the vehicle which addresses it can be reached on, and if the address in use is a wireless one while a cabled one also exists, a fifteen-second snackbar suggests the cable, once per session. A second commit changes the network speeds Cockpit publishes for plotting: both are now measured across the readings of the last ten seconds through one shared conversion helper, which reports no traffic while the span is too short to mean anything and measures across a gap rather than leaving the last rate standing. Nothing is stored and no new interface element is added. What still needs attentionNothing. Both findings left open at round 9 were addressed by this round's changes, and the full re-run over Since round 9 — 2 closed, comparing e948c28 → 0279776; the branch was rebased againRange — unusable for the third round running, so nothing below rests on it. Derived from Resolutions. Decisions. 9.1 — ✅ Addressed. The finding asked for the run to be grown so the span gate is cleared, and said it was worth asserting the gate had actually been cleared rather than trusting the length. Both landed, and the second one landed in the test rather than in a scratch run: expect(feedSteadySeconds(createWirelessTrafficWatcher(), 35, { eth0: 0, wlan0: 4.9 })).not.toContain(true)
expect(feedSteadySeconds(createWirelessTrafficWatcher(), 35, { eth0: 0, wlan0: 5.1 })).toContain(true)
11.5 — ✅ Addressed. The comment at Discussion since round 9
Change map — what was established before judgingClaims (from the PR body, both commit subjects and the author's follow-up, each checked against the code)
Failure site. Two layers. The behaviour the PR advises about is vehicle-side routing, outside this repository. The Cockpit-side failure — a per-poll delta over a counter that refreshes slower than the poll — lives in the pre-existing speed block at Entry points
Cost at that frequency is unchanged from round 9 — nothing in the runtime path moved this round. The stretch scan is ~465 divisions and ~40 short intermediate arrays per busy wireless interface per second, bounded by the prune at Invariants
Sections with nothing to report (11)1. Correctness & Implementation Bugs — ✅ (re-derived the whole rule by hand at the current constants rather than reading the tests: the length and span gates at 2. Persistence & User Data — ✅ (grepped the diff for 3. AGENTS.md Adherence — ✅ ( 4. Security — ✅ (no dependency added and no request added; the one endpoint the PR reaches is the 5. Performance — ✅ (nothing on a runtime path changed this round, and the entry-point table above re-establishes that everything rides the pre-existing 1 Hz poll: the pair scan is ~465 divisions and ~40 short arrays per busy wireless interface per second, bounded by the 40 s prune and skipped once the latch is set because 6. UI / UX — ✅ (the PR's only output is the existing 7. Code Quality & Style — ✅ ( 8. Commit Hygiene — ✅ (two commits per 9. Tests — ✅ (re-ran the arithmetic of all six cases in 10. Documentation — ✅ (nothing added behaves differently between Lite and Standalone — no Electron-only API is reached and both builds run the same poll and the same snackbar — so the 11. Nitpicks / Optional — ✅ (re-read the four comments in the watcher against the code beneath them, including the one rewritten this round at Generated by Claude. This is advisory; a human reviewer must still approve. |
|
@ArturoManzoli ready again. |


Summary
A vehicle reached over WiFi streams its video over WiFi, even with a cable attached. Cockpit cannot move the media to the cable, so it now tells the user about it.
Two things have to hold before the warning shows up:
The beacon is only asked once the traffic condition holds, and at most once every 30 s from there, so a cable plugged in or pulled mid-session changes the answer.
The warning is a 15 s snackbar, given once per session — acting on it means changing the vehicle address, which reloads Cockpit anyway. No UI was added — it uses the existing snackbar.
An address the beacon does not report, such as the default
blueos-avahi.localhost name, leaves the current link kind undetermined, and in that case nothing is warned.The plotted network speeds, fixed in the second commit
The
upload speed (Mbps)anddownload speed (Mbps)data-lake variables were computed from a single poll-to-poll delta, which is the very thing the counter-refresh diagnosis above says is meaningless: they read as zeros and spikes on a busy link just as much as on an idle one, which is exactly what the plots earlier in this thread show.That defect is pre-existing, but it is the number an operator reaches for to see what the link is doing, and the one anybody verifying this feature would plot, so it is fixed here rather than left for later — in its own commit, since it changes existing behaviour. Both speeds are now measured across the readings of the last ten seconds, which spans several counter refreshes, and the counter-delta-to-Mbps conversion lives in one helper shared with the warning's own history. The minimum span lives in that helper too, so the polls right after connecting publish no traffic instead of the sawtooth they have too little history to look past, and a gap in the readings is measured against the newest reading available instead of leaving the last rate published as if it were still current.
Test plan
upload speed (Mbps)variables with a stream running and confirm they now hold a steady rate instead of alternating between zero and a spike.Checks
yarn lint:fixclean, andvitest runpasses 40 tests.cosmos.test.tsandconnection.test.tsfail to collect the same way onmaster.yarn typecheckis broken on my machine, onmasteras well —vue-tscbails onsrc/App.vueand exits 0 even with a deliberate type error — so I checked the changed files withtscdirectly and diffed the errors against the same run without this branch's changes. No new ones.src/tests/libs/wireless-traffic-warning.test.tscovers the trigger, the once-per-session latch, traffic below the threshold, readings arriving five seconds apart, a busy link whose counter only refreshes every few polls, a single large transfer spread over several readings on an idle wireless link, and the four link-kind cases of the beacon check.src/tests/libs/blueos.test.tscovers the shared rate helper: a counter that only refreshes every few seconds, a span too short to cover a refresh — both when it reads no progress and when it catches one — a counter reset by a vehicle reboot, and an empty span.Closes #2953
Closes #2971