feat(v0.14.2): live data panel polish — polling rate, peaks, range bar, snapshot, NRC banner - #184
feat(v0.14.2): live data panel polish — polling rate, peaks, range bar, snapshot, NRC banner#184ohgeeceee wants to merge 1 commit into
Conversation
…r, snapshot, NRC banner
Slice 2 of v0.14.2 'Live Data on the Bench'. Tier A only — frontend +
docs, no transport/protocol changes, no new crate, no new Tauri
command. The existing `read_live_data` and `export_text` IPC paths
are reused.
What this slice ships:
- **Polling-rate selector** (`Poll @` <select>): 100 / 250 / 500 /
1000 ms. Persisted in workspace.json under `livePollRateMs`.
Changing the rate while polling restarts the `setInterval` at the
new delay (no need to stop and start). Default 250 ms (the
pre-slice behaviour); the resolvePollRateMs() helper rejects
out-of-range or unknown values.
- **Per-gauge peak tracking** (`Current peaks` side table + small
label under each gauge): `applyValuesToPeaks()` is the pure
reducer — same immutability + text-skip discipline as
`live_gauges.js`. The side table renders in sweep order so the
user's mental model matches the panel layout. A `Reset peaks`
button clears the session.
- **Range bar** under each gauge: thin strip showing the current
value's position between `min` and `max`. Hot when above 85%,
over when above 100%. Pure CSS transition. Cheap at-a-glance
'how close to the top am I' indicator that complements the dial.
- **Save snapshot** button: writes the current values to
`~/beeemuu-exports/beeemuu-live-snapshot-<stamp>.csv` via the
existing `export_text` Tauri command. Filename matches the
project's `beeemuu-<kind>-<stamp>.csv` convention used by
`beeemuu-log-`, `beeemuu-dtcs-`, `beeemuu-session-`, etc.
CSV shape mirrors the v0.11.0 logging export (metadata header,
column header row, one data row per value).
- **NRC error surface**: the existing `catch (e) { log(...);
stopPolling() }` is now NRC-aware. `parseNrcError()` extracts
the (sid, nrc) pair from the `protocol::service` error format
(`"ECU rejected service 22: conditionsNotCorrect (NRC 22)"`).
When the NRC matches the unsupported set ({0x11, 0x12, 0x14,
0x31}), the panel shows a banner — *not* a stop-polling — and
dims the affected gauges with `.unsupported` styling. The
banner auto-clears on the next clean sweep. A clean stop-polling
path remains for non-NRC errors (timeout, no connection, etc.).
Files:
- `src/js/live_data_panel.js` (new): pure helpers — polling
rate, peak reducer, snapshot CSV serialiser, NRC parser. Dual
export (CommonJS + `window.beeemuuLiveDataPanel`).
- `src/js/live_data_panel.test.js` (new): 15 tests covering
every public function, including the pinned UNSUPPORTED_NRCS set
so a future silent edit gets caught by CI. Baseline 206 + 15 =
221/221 pass.
- `src/index.html`: panel-head controls (Poll @ selector,
Snapshot, Reset peaks buttons), NRC banner, side peaks table,
gauge-cell peak + range-bar slots (added at ensureGauge() time).
- `src/css/app.css`: `.live-head-controls`, `.live-nrc-banner`,
`.live-body` grid (1fr gauge grid + 220px peaks column with
mobile collapse at 720px), `.live-peaks-table`,
`.gauge-peak`, `.gauge-range`, `.gauge-cell.unsupported` —
all driven by existing semantic tokens so both light and dark
themes work without per-panel overrides.
- `src/js/main.js`:
- ensureGauge() now stamps `data-id` on the cell + appends
`.gauge-peak` + `.gauge-range` DOM so the polling loop can
address them by `LiveValue.id`.
- pollOnce() now feeds the peak reducer, paints the range bar,
clears the NRC banner on success, and dispatches the catch
branch to the NRC parser.
- startPolling() reads the rate from `currentPollRateMs()`.
- new click handlers for the snapshot + reset buttons.
- `live-profile` change handler now also clears the peak
state, hides the NRC banner, and disables the snapshot +
reset buttons (a new profile is a fresh session).
- workspace persistence: `livePollRateMs` saved in
saveSettings() and restored in loadSettings().
Verification:
- [x] `node --test src/js/**/*.test.{js,cjs}` — 221/221 pass
(baseline 206 + 15 new).
- [x] `node scripts/lint-toml.js` — 37 / 0.
- [x] `cargo check --offline` (src-tauri/) — clean. No Rust
changes; this is the dependency-vendored check.
- [x] `node --check src/js/main.js` — clean.
- [x] No `transport/**`, `protocol/**`, `commands.rs` changes.
No new crate. No new Tauri command.
What this slice does NOT ship (intentionally, to keep Tier A scope
tight):
- A persistent 'remove this PID from the profile' affordance —
that requires a write path the user owns via the contribution
guide, not a one-click. The unsupported dim is session-local.
- A 'reset range bar / start from idle' control. The bar is
derived from the same numeric range as the gauge itself, so it
inherits the gauge's min/max; there is nothing to reset.
- v0.14.0 Live Gauges panel integration. Deferred per the
cycle-pick conversation (Option B).
|
ECC bundle files are already tracked in this repository. Skipping generation of another bundle PR. |
|
Closing as duplicate. PR #177 (merged 2026-07-29 at 22:52 UTC) implemented the same v0.14.2 slice 2 — polling rate selector, peak tracking, range bar, snapshot CSV, NRC error surface — with the same |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 65db3fe901
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const msg = (e && e.message) ? e.message : String(e); | ||
| const parsed = window.beeemuuLiveDataPanel.parseNrcError(msg); | ||
| if (parsed && window.beeemuuLiveDataPanel.isUnsupportedNrc(parsed)) { |
There was a problem hiding this comment.
Propagate NRC failures to the new handler
For any unsupported PID, this handler is unreachable because src-tauri/src/commands.rs:259-302 handles protocol results with if let Ok(data) and then always returns Ok(out), silently omitting failed parameters. Consequently parseNrcError never receives an ECU NRC, the successful path instead clears the banner, and the advertised unsupported-gauge warning never appears on real-car sweeps.
Useful? React with 👍 / 👎.
| // resolvePollRateMs() — so writing an old workspace without the | ||
| // key (or with a future / removed value) just means we use the | ||
| // default until the user picks one. | ||
| ws.livePollRateMs = window.beeemuuLiveDataPanel.resolvePollRateMs($("live-poll-rate")?.value); |
There was a problem hiding this comment.
Preserve the polling rate during workspace serialization
Although this assigns livePollRateMs, flushWorkspace() passes the state through Workspace.serializeWorkspace(), whose allow-list in src/js/workspace.js:37-71 does not include this key. The serializer therefore drops the selected rate from workspace.json, so every restart restores the default 250 ms instead of the user's choice.
Useful? React with 👍 / 👎.
| const frac = Math.max(0, Math.min(1, (v.value - v.min) / (v.max - v.min))); | ||
| fill.style.width = (frac * 100).toFixed(1) + "%"; | ||
| fill.classList.toggle("hot", frac > 0.85 && frac <= 1); | ||
| fill.classList.toggle("over", frac > 1); |
There was a problem hiding this comment.
Test the unclamped range fraction for overflow
When a live value exceeds its configured maximum, frac is clamped to 1 here before the class checks. Therefore frac > 1 can never be true and the new red .over state never activates; above-range values are instead marked .hot, obscuring the distinction the range bar is intended to show.
Useful? React with 👍 / 👎.
| function resetLivePeaks() { | ||
| livePeakState = {}; | ||
| liveLastValues = []; | ||
| renderLivePeaks(); |
There was a problem hiding this comment.
Keep snapshot data when resetting peaks
Resetting peaks also clears liveLastValues, which is the cache consumed by saveLiveSnapshot(). If the user stops polling after a valid sweep, clicks Reset peaks, and then clicks the still-enabled Save snapshot button, the snapshot fails with “No live values yet” until another sweep runs, even though resetting peaks should not discard the current readings.
Useful? React with 👍 / 👎.
| const delay = currentPollRateMs(); | ||
| pollTimer = setInterval(pollOnce, delay); | ||
| pollOnce(); |
There was a problem hiding this comment.
Prevent overlapping polling sweeps
When a sweep takes longer than the selected interval—especially the new 100 ms option with profiles containing 10–30 serial ECU reads—setInterval starts additional pollOnce() invocations without waiting for the prior one. The backend serializes these through the transport mutex and K+DCAN reads can wait up to a one-second deadline, so requests can accumulate faster than they complete; unchecking Poll only clears future timer callbacks, leaving the queued sweeps to keep the transport busy.
Useful? React with 👍 / 👎.
| const values = await invoke("read_live_data", { profile: $("live-profile").value }); | ||
| liveLastValues = values; |
There was a problem hiding this comment.
Discard responses from a previously selected profile
If the user changes profiles while read_live_data is in flight, the change handler clears the session, but the old request can subsequently assign its response to liveLastValues here. saveLiveSnapshot() then labels those readings using the newly selected profile, allowing a snapshot containing one profile's values to be exported as another profile until the next sweep completes.
Useful? React with 👍 / 👎.
| function resetLivePeaks() { | ||
| livePeakState = {}; | ||
| liveLastValues = []; | ||
| renderLivePeaks(); |
There was a problem hiding this comment.
Clear gauge peak labels when resetting peaks
With polling stopped after at least one sweep, clicking Reset peaks empties the side table but leaves every .gauge-peak element showing its previous value. renderLivePeaks() only updates IDs present in the current peak state, so after this state is emptied it never writes the reset placeholder to the existing gauge cells, making the reset visibly incomplete until another sweep arrives.
Useful? React with 👍 / 👎.
| JSON.stringify(v.id || ""), | ||
| JSON.stringify(v.label || ""), | ||
| JSON.stringify(v.unit || ""), |
There was a problem hiding this comment.
Use CSV escaping instead of JSON escaping
For an imported or user-added parameter whose label contains a double quote, JSON.stringify() emits a backslash-escaped quote such as ", but CSV requires embedded quotes to be doubled as "". The resulting snapshot row is malformed for standard CSV readers and can split or corrupt subsequent fields; the same problem affects enum text serialized above.
Useful? React with 👍 / 👎.
Slice 2 of v0.14.2 — "Live Data on the Bench"
Cycle plan:
docs/v0.14.2_plan.md. Tier A only (frontend + docs + tests). Notransport/**/protocol/**changes, no new crate, no new Tauri command. Reuses the existingread_live_dataandexport_textIPC paths.What this slice ships
Polling-rate selector.
Poll @: 100 / 250 / 500 / 1000 ms. Persisted in workspace.json under livePollRateMs. Changing the rate while polling restarts the setInterval at the new delay. Default 250 ms (the pre-slice behaviour); resolvePollRateMs() rejects out-of-range or unknown values. Per-gauge peak tracking. Current peaks side table + small label under each gauge. applyValuesToPeaks() is the pure reducer — same immutability + text-skip discipline as live_gauges.js. Side table renders in sweep order so the user's mental model matches the panel layout. Reset peaks button clears the session. Range bar under each gauge. Thin strip showing the current value's position between min and max. Hot when above 85%, over when above 100%. Pure CSS transition. At-a-glance complement to the dial. Save snapshot button. Writes the current values to ~/beeemuu-exports/beeemuu-live-snapshot-<stamp>.csv via the existing export_text Tauri command. Filename matches the project's beeemuu-<kind>-<stamp>.csv convention. CSV shape mirrors the v0.11.0 logging export (metadata header, column header row, one data row per value). NRC error surface. parseNrcError() extracts the (sid, nrc) pair from the protocol::service error format ("ECU rejected service 22: conditionsNotCorrect (NRC 22)"). When the NRC matches the unsupported set ({0x11, 0x12, 0x14, 0x31}), the panel shows a banner — not a stop-polling — and dims the affected gauges with .unsupported styling. The banner auto-clears on the next clean sweep. A clean stop-polling path remains for non-NRC errors. Files src/js/live_data_panel.js (new): pure helpers — polling rate, peak reducer, snapshot CSV serialiser, NRC parser. Dual export (CommonJS + window.beeemuuLiveDataPanel). src/js/live_data_panel.test.js (new): 15 tests covering every public function, including the pinned UNSUPPORTED_NRCS set so a future silent edit gets caught by CI. src/index.html: panel-head controls (Poll @ selector, Snapshot, Reset peaks buttons), NRC banner, side peaks table, gauge-cell peak + range-bar slots (added at ensureGauge() time). src/css/app.css: .live-head-controls, .live-nrc-banner, .live-body grid (1fr gauge grid + 220px peaks column with mobile collapse at 720px), .live-peaks-table, .gauge-peak, .gauge-range, .gauge-cell.unsupported — all driven by existing semantic tokens so both light and dark themes work without per-panel overrides. src/js/main.js: ensureGauge() stamps data-id on the cell + appends .gauge-peak + .gauge-range DOM so the polling loop can address them by LiveValue.id. pollOnce() feeds the peak reducer, paints the range bar, clears the NRC banner on success, and dispatches the catch branch to the NRC parser. startPolling() reads the rate from currentPollRateMs(). New click handlers for the snapshot + reset buttons. live-profile change handler clears the peak state, hides the NRC banner, and disables the snapshot + reset buttons (a new profile is a fresh session). workspace persistence: livePollRateMs saved in saveSettings() and restored in loadSettings(). ROADMAP.md: v0.14.2 slice-1 row already marks PR #175 done; no change needed here beyond what was committed. Verification [x] node --test src/js/**/*.test.{js,cjs} — 221/221 pass (baseline 206 + 15 new). [x] node scripts/lint-toml.js — 37 / 0. [x] cargo check --offline (src-tauri/) — clean. No Rust changes; this is the vendored-dep check. [x] node --check src/js/main.js — clean. [x] No transport/**, protocol/**, commands.rs changes. No new crate. No new Tauri command. Tier A. Self-merge when CI is green.