Skip to content

feat(v0.14.2): Live Data panel UX polish — poll-rate, peaks, range bar, snapshot, NRC - #177

Merged
ohgeeceee merged 1 commit into
mainfrom
feat/v0.14.2-slice2-live-panel
Jul 29, 2026
Merged

feat(v0.14.2): Live Data panel UX polish — poll-rate, peaks, range bar, snapshot, NRC#177
ohgeeceee merged 1 commit into
mainfrom
feat/v0.14.2-slice2-live-panel

Conversation

@ohgeeceee

Copy link
Copy Markdown
Owner

Summary

Slice 2 of v0.14.2 "Live Data on the Bench": the Live Data panel UX polish
that was specified in docs/v0.14.2_plan.md lines 56-73. Five user-facing
additions, all on the existing read_live_data Tauri command, no
transport/** or protocol/** changes, no new crate, no new Tauri
command.

What's in this PR

Feature What it does Where
Polling-rate selector <select id="live-poll-rate"> with 100 / 250 / 500 / 1000 ms. startPolling() now reads the dropdown via beeemuuLiveDataPanel.resolvePollRateMs, which falls back to 250 ms for unknown input. Changing the rate mid-session restarts the interval so the new rate takes effect immediately. src/index.html (DOM), src/js/main.js (currentPollRateMs, listener on #live-poll-rate)
Per-gauge peak tracking A .gauge-peak label under every gauge cell showing the highest numeric value seen since startPolling() began (or Reset peaks was clicked). Numeric values only — enum / text-mode gauges are skipped. A consolidated "Current peaks" table below the gauge grid shows all active peaks in one place. src/index.html (#live-peaks panel), src/js/main.js (applyValuesToPeaks reducer in pollOnce, renderPeakTable, formatPeakForLabel), src/css/app.css (.gauge-peak, .live-peaks, .live-peaks-table)
Range bar under each gauge A 4px-wide .gauge-range bar under each gauge label whose fill width tracks (value-min)/(max-min). Skipped for enum / text-mode gauges. src/index.html (DOM built in ensureGauge), src/js/main.js (updateRangeBar), src/css/app.css (.gauge-range + .gauge-range-fill)
Save snapshot button #btn-live-snapshot writes the current values to <HOME>/beeemuu/beeemuu-live-snapshot-<iso>.csv via the same export_text Tauri command the DTC + log exporters use. CSV shape mirrors the v0.11.0 log-export header (metadata line + column header + one data row per value) so it's loadable by src/js/csv_log_export.js. Button is disabled until the first successful poll populates a gauge. src/index.html (button), src/js/main.js (listener), src/js/live_data_panel.js (buildSnapshotCsv + snapshotCsvFilename)
NRC error surface pollOnce's catch block parses the protocol-layer error string via beeemuuLiveDataPanel.parseNrcError and, if the NRC is one of the four canonical "unsupported" codes (0x11 / 0x12 / 0x14 / 0x31), logs a friendly line like Live data: unsupported PID — NRC 0x31 (sid 0x22). Transient / condition NRCs (0x22, 0x78) fall through to the existing log() behavior. src/js/main.js (catch block), src/js/live_data_panel.js (parseNrcError, isUnsupportedNrc, UNSUPPORTED_NRCS)

Why a separate live_data_panel.js helper module

The five features above all share pure helpers (polling-rate resolve,
peak reducer, snapshot-CSV serialiser, NRC parser). Following the
v0.14.0 live_gauges.js / live_can_source.js pattern, those helpers
live in a CommonJS + window.beeemuuLiveDataPanel dual-export module
that's unit-testable under node --test without a webview. The DOM
wiring stays in main.js. Module surface:

const {
  POLL_RATE_MS_OPTIONS,    // [100, 250, 500, 1000]
  DEFAULT_POLL_RATE_MS,    // 250
  resolvePollRateMs,       // (value) -> number in options, fallback 250
  applyValuesToPeaks,      // (state, values[]) -> new state
  formatPeakForLabel,      // (value, unit) -> "—" / "1500" / "91.2"
  buildSnapshotCsv,        // (values[], profileLabel) -> CSV string
  snapshotCsvFilename,     // (Date?) -> "beeemuu-live-snapshot-…csv"
  parseNrcError,           // (msg) -> { sid, nrc, raw } | null
  isUnsupportedNrc,        // (parsed) -> bool
  UNSUPPORTED_NRCS,        // Set([0x11, 0x12, 0x14, 0x31])
} = require("./live_data_panel.js");

Honest scope cut (deferred to v0.14.3)

The cycle plan listed "one-click removal of the offending PID from
the profile"
as part of the NRC error surface. This PR delivers the
error log line but defers the per-PID dim + remove UI: the current
read_live_data Tauri error string surfaces the (sid, nrc) pair but
not the DID that was rejected. A clean per-PID UX needs the
protocol layer to surface the DID in the error, which is a Tier B
change in the protocol module. The plan and the UNSUPPORTED_NRCS set
are already in place — the v0.14.3 follow-up just needs to thread the
DID through and bind it to the addUnsupportedPid /
removeUnsupportedPid helpers. This is a small, well-scoped v0.14.3
slice.

Verification

  • node --check src/js/main.js src/js/live_data_panel.js src/js/live_data_panel.test.js — clean
  • node --test "src/js/**/*.test.js" "src/js/**/*.test.cjs"221/221 green (baseline 206 + 15 new tests in live_data_panel.test.js)
  • node scripts/lint-toml.js — clean (no profile changes; 37 files / 0 problems)
  • No Rust changes → no async_commands allowlist concern
  • No new Tauri command → no new IPC contract

Diff stat

 src/css/app.css                |  35 ++++++++
 src/index.html                 |  20 ++++-
 src/js/live_data_panel.js      | 193 +++++++++++++++++++++++++++++++++++++++
 src/js/live_data_panel.test.js | 199 +++++++++++++++++++++++++++++++++++++++++
 src/js/main.js                 | 191 ++++++++++++++++++++++++++++++++++++++-
 5 files changed, 634 insertions(+), 4 deletions(-)

Tier

A — frontend UI + a pure-helper module. No transport/**,
protocol/**, commands.rs, or ops/** touches. Auto-merge on CI
green per CLAUDE.md rule 2.

Cross-references

…r, snapshot, NRC

Slice 2 of the v0.14.2 'Live Data on the Bench' cycle. Five
user-facing additions, all on the existing read_live_data Tauri
command:

- Polling-rate selector (100/250/500/1000 ms) wired via
  resolvePollRateMs, falls back to 250ms for unknown values.
- Per-gauge peak tracking — applyValuesToPeaks reducer in pollOnce,
  formatPeakForLabel formatter, consolidated 'Current peaks' table
  under the gauge grid, 'Reset peaks' button.
- Range bar under each gauge cell — fill width from
  (value-min)/(max-min), skipped for enum/text-mode gauges.
- Save snapshot button — builds a v0.11.0-shape CSV via
  buildSnapshotCsv, writes to <HOME>/beeemuu/ via export_text.
- NRC error surface — parseNrcError + isUnsupportedNrc, friendly
  log line on the four canonical 'unsupported' codes (0x11/0x12/0x14/0x31).

Pure helpers in live_data_panel.js (CommonJS + window dual export),
DOM wiring in main.js. No transport/protocol/commands.rs changes.
No new Tauri command.

Honest scope cut: per-PID dim + 'remove from profile' UI is deferred
to v0.14.3 — the current protocol error string surfaces (sid, nrc)
but not the DID. The UNSUPPORTED_NRCS set + the wire-shape of the
panel are in place; the v0.14.3 slice just needs to thread the DID
through the protocol layer and bind it to the local flag.

Verification: 221/221 JS tests green (baseline 206 + 15 new in
live_data_panel.test.js); node --check clean on all 3 JS files;
TOML lint clean. No Rust changes, no async_commands allowlist risk.

Co-authored-by: ohgeeceee <ohgeeceee@users.noreply.github.com>
@ecc-tools

ecc-tools Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

ECC bundle files are already tracked in this repository. Skipping generation of another bundle PR.

github-actions Bot pushed a commit that referenced this pull request Jul 29, 2026
…closeout (#178)

Slice 3 of the v0.14.2 'Live Data on the Bench' cycle. Two
doc-only changes:

- New docs/validation/n62-real-car.md — chassis-specific
  bench-verification harness for the N62 / E70 X5 4.8L
  profile. Mirrors the shape of can-broadcast.md, dtc-history.md,
  injector-validation.md, etc. Five sections covering wire-up,
  cold readings, running readings, GitHub report template, and
  what we'll do with the report. Critical row is oil temp (0x5C)
  — if it returns -40 °C the slice 1 enrichment needs reverting
  for that PID only.
- ROADMAP.md — v0.14.2 cycle header In Progress -> Shipped
  2026-07-29; all four cycle-table rows marked ✅ Done (PR #171
  / #175 / #177 / #178); last-updated line rewritten with the
  cycle closure summary and the explicit v0.14.3+ carry-over for
  the deferred PIDs (0x5E, 0x5F, 0x62) and the per-PID dim +
  remove-from-profile UI.

Tier A, no transport/protocol/commands.rs touches. No test impact.
EOL parity: new doc matches the existing 6 docs/validation/
files (CRLF); ROADMAP unchanged EOL.

Co-authored-by: ohgeeceee <ohgeeceee@users.noreply.github.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1e23a4e015

ℹ️ 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".

Comment thread src/js/main.js
// startPolling so changing the dropdown restarts the interval.
function currentPollRateMs() {
const sel = $("live-poll-rate");
return window.beeemuuLiveDataPanel.resolvePollRateMs(sel ? sel.value : 250);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Load the live-data helper before using it

src/index.html:642-658 loads the frontend as plain scripts and never includes js/live_data_panel.js, so every attempt to enable Live Data reaches this dereference with window.beeemuuLiveDataPanel undefined. The resulting ReferenceError occurs before setInterval and pollOnce, preventing the existing Live Data panel from polling at all; load the new helper before main.js.

Useful? React with 👍 / 👎.

Comment thread src/js/main.js
Comment on lines +1402 to +1404
const nrc = window.beeemuuLiveDataPanel.parseNrcError(String(e));
if (nrc && window.beeemuuLiveDataPanel.isUnsupportedNrc(nrc)) {
log("Live data: unsupported PID — NRC 0x" + nrc.nrc.toString(16).toUpperCase() + " (sid 0x" + (nrc.sid !== null ? nrc.sid.toString(16).toUpperCase() : "??") + ").");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Propagate per-parameter NRCs to this handler

When a DID/PID returns an NRC, this catch block cannot receive the protocol error: src-tauri/src/commands.rs:259-300 handles each read with if let Ok(data) and always returns Ok(out), silently omitting failed parameters. Consequently unsupported PIDs never produce the new friendly log line even after the helper is loaded; read_live_data must return per-parameter failure information or otherwise propagate the NRC.

Useful? React with 👍 / 👎.

Comment thread src/js/main.js
Comment on lines +1414 to 1416
const ms = currentPollRateMs();
pollTimer = setInterval(pollOnce, ms);
pollOnce();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Prevent overlapping reads at the new 100 ms rate

With the newly exposed 100 ms setting, any real-cable profile sweep taking longer than 100 ms starts another invoke before the previous one finishes. read_live_data holds the shared transport mutex for the entire multi-parameter sweep (src-tauri/src/commands.rs:86-92,257-303), so these calls queue behind one another and can continue returning stale sweeps after polling is stopped; schedule the next poll after the current promise settles or add an in-flight guard.

Useful? React with 👍 / 👎.

Comment thread src/js/main.js
Comment on lines +1438 to +1440
if (pollTimer) {
stopPolling();
startPolling();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep the polling checkbox checked after a rate change

When the user changes rate during an active polling run, stopPolling() unchecks #live-poll, and startPolling() starts the replacement interval without checking it again. The UI therefore says polling is off while bus reads continue; clicking the unchecked control once merely checks it and leaves the existing interval running, requiring a second click to actually stop polling.

Useful? React with 👍 / 👎.

Comment thread src/js/main.js
id,
label: g.label,
unit: g.unit || "",
value: g.value,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Export raw readings instead of clamped gauge values

For a numeric reading outside its configured display range, Gauge.set() clamps g.value to min/max (src/js/gauges.js:73-77), so this snapshot silently records the boundary rather than the actual ECU value. Out-of-range readings are especially important diagnostic evidence; retain the latest raw LiveValue response and serialize that instead of the presentation-layer gauge state.

Useful? React with 👍 / 👎.

Comment thread src/js/live_data_panel.js
Comment on lines +107 to +115
const text = v.text !== undefined && v.text !== null ? JSON.stringify(String(v.text)) : "";
const value = Number.isFinite(Number(v.value)) ? Number(v.value).toFixed(2) : "";
rows.push(
[
JSON.stringify(v.id || ""),
JSON.stringify(v.label || ""),
JSON.stringify(v.unit || ""),
value,
text,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Quote snapshot fields using CSV escaping rules

JSON.stringify escapes embedded quotes as \", but CSV requires a quote inside a quoted field to be doubled as "". When an imported profile label, unit, ID, or enum text contains a quote, standard CSV parsers split the generated row into the wrong number of columns, so the saved snapshot is malformed; use a dedicated CSV field encoder for every string field.

Useful? React with 👍 / 👎.

Comment thread src/js/main.js
Comment on lines 1284 to +1292
gauges.clear();
$("gauge-grid").innerHTML = "";
// v0.14.2 slice 2 — peak state is per-profile. A new profile may
// have a different parameter set, so a stale peak for an id the
// new profile doesn't have would never be cleared by pollOnce
// (no sweep). Reset the peak state here.
livePeakState = {};
renderPeakTable();
updateSnapshotButton();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Discard responses from the previously selected profile

If the profile is changed while a poll is in flight, this handler clears the gauges, but the pending request was already invoked with the old profile and can repopulate them after the clear; later sweeps then add the new profile's gauges to the same map. The new snapshot exporter labels the file with the currently selected profile while serializing the entire mixed map, so it can record values from two profiles; stop or invalidate outstanding polls when the selector changes.

Useful? React with 👍 / 👎.

Comment thread src/js/main.js
stopPolling();
startPolling();
}
saveSettings();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Persist the selected polling rate

The rate-change handler calls saveSettings(), but that function only records liveProfile, logProfile, and the existing workspace fields (src/js/main.js:61-97); neither it nor loadSettings() stores or restores #live-poll-rate. Every application restart therefore silently resets the user's selected bus load to 250 ms despite the explicit save call and the workspace's role as the source of truth for UI preferences.

Useful? React with 👍 / 👎.

Comment thread src/css/app.css

/* ---------- v0.14.2 slice 2: Live Data panel UX polish ---------- */
.poll-rate-select {
background: #1a2332; color: var(--fg);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Use a defined foreground color for the dark controls

--fg is not declared anywhere in app.css, so this color declaration is discarded. In the default light theme the dark #1a2332 selector therefore inherits the nearly black body text, making the selected polling rate effectively unreadable; the same undefined token at line 314 makes the peak values nearly invisible on the dark peaks panel. Use a defined light-on-dark foreground color for both elements.

Useful? React with 👍 / 👎.

Comment thread src/js/main.js
const csv = window.beeemuuLiveDataPanel.buildSnapshotCsv(values, profileLabel);
const filename = window.beeemuuLiveDataPanel.snapshotCsvFilename();
try {
const path = await invoke("export_text", { filename, content: csv });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Write live snapshots to the advertised directory

This passes only a basename to export_text, which always writes under <HOME>/beeemuu-exports and strips any directory components (src-tauri/src/commands.rs:716-731). The slice specification requires live snapshots under <HOME>/beeemuu/live-snapshots/<timestamp>.csv, so the button can never place them where users and downstream tooling are told to find them; use an export path/command that supports the intended live-snapshot directory.

Useful? React with 👍 / 👎.

@ohgeeceee
ohgeeceee merged commit b2bc0e5 into main Jul 29, 2026
23 of 24 checks passed
ohgeeceee added a commit that referenced this pull request Jul 30, 2026
Slice 3a of v0.14.3 'Finish the Bench'. Tier B (touches
src-tauri/src/protocol/** + commands.rs, adds one new Tauri
command). Backend half — slice 3b re-wires the frontend to
consume the new shape and surface the per-PID dim/remove UX.

## What this slice ships

**1. `protocol::nrc_from_error(msg: &str) -> Option<(u8, u8)>`**
(pub, in src-tauri/src/protocol/mod.rs). Pure parser that
extracts (sid, nrc) from the error string produced by
`service()`:
  "ECU rejected service 22: Conditions not correct (NRC 22)"
                                          ^------^         ^-^
                                          nrc parser      sid parser
Co-located with `nrc_text` (the producer) so the format
contract has one source of truth. Case-insensitive on both
hex digits; tolerant of extra whitespace at the boundary.
4 unit tests pin the format and the failure modes (transport
timeouts, non-NRC errors, malformed hex).

**2. `read_live_data` return-type split.** Pre-v0.14.3, a single
per-PID failure short-circuited the whole sweep with
`Err(String)`; the frontend had no way to tell which PID
tripped the error and so couldn't offer a 'remove this
unsupported PID from the profile' affordance. Now returns
`LiveSweepResult { values, errors }`:
- `values`: `Vec<LiveValue>` — only the PIDs whose read AND
  decode succeeded (matches pre-v0.14.3 semantics for the
  happy path; no growth in the values array).
- `errors`: `Vec<LiveError>` — one entry per failed PID with
  `{ id, label, sid, nrc, error }`. The structured `sid` +
  `nrc` fields are populated via `nrc_from_error`; the
  verbatim `error` string is preserved for the frontend's
  fallback parser and for non-NRC errors (transport timeouts,
  'Not connected', 'Unexpected ... response').
The whole sweep still returns `Err(_)` for systemic
problems (no transport, unknown profile, poisoned state lock)
— never for a single per-PID NRC.

**3. `live::remove_param_from_profile(profile_id, param_id)`**
(in src-tauri/src/data/live.rs). Mutates the in-memory profile
registry: removes the matching `LiveParam`, returns `true`
when something was removed / `false` for unknown ids.
Idempotent — calling twice on the same id is a no-op the
second time. 3 unit tests pin the happy path + idempotency +
unknown-profile failure mode.

**4. `remove_profile_pid` Tauri command.** New `async fn`
in src-tauri/src/commands.rs:
- Step 1: in-memory removal via `live::remove_param_from_profile`.
  Returns an error to the frontend if the param id is unknown
  (so the UI can distinguish 'removed' from 'already gone').
- Step 2: serialise the updated profile with `live::profile_to_toml`
  (round-trips `[profile.theme]` blocks).
- Step 3: write to `<community>/profiles/<id>.toml` via
  `tokio::fs::write`. `create_dir_all(parent)` first —
  mirrors `save_freeze_schema`'s pattern.
- Returns the full file path the backend wrote, so the
  frontend can show the user 'Removed from <path>'.
- Async because of the file I/O. NOT in the SYNC_ALLOWLIST
  (per tests/async_commands.rs the allowlist gates the
  *inverse*: sync commands are forbidden).

**5. Cargo.toml.** Adds `fs` to tokio's features so the
file write in `remove_profile_pid` can use `tokio::fs`.
No new crate enters the graph; tokio is already in the tree
via Tauri.

**6. lib.rs.** Registers `commands::remove_profile_pid` in
the `invoke_handler` so the frontend can call it.

## Why the frontend will need its own PR

The pre-v0.14.3 `main.js::pollOnce` does `for (const v of
values)` against the result, where `values` was a
`Vec<LiveValue>`. Slice 3a returns `{ values, errors }`
instead. The frontend has to switch to
`result.values.forEach(...) + result.errors.forEach(...) `,
which is a non-trivial rewire of the same area that landed
the v0.14.2 slice 2 PR (#177). That's slice 3b; this PR
lands the backend it consumes.

## Verification

- `cargo test --offline --lib data::live` — 35/35 pass
  (was 32 + 3 new `remove_param_from_profile` tests).
- `cargo test --offline --lib protocol::` — 15/15 pass
  (was 11 + 4 new `nrc_from_error` tests).
- `cargo test --offline --lib` — 149/149 pass (full
  src-tauri suite; no regressions; +7 tests total).
- `cargo check --offline` — clean.
- `cargo clippy --offline --lib` — no new warnings on
  src/protocol/mod.rs, src/data/live.rs, src/commands.rs.
- Pre-existing `cargo fmt` diff in src/analysis.rs is
  unrelated to this PR.
- `node --test src/js/**/*.test.{js,cjs}` — 221/221 pass
  (no JS changes; baseline unaffected).
- `node scripts/lint-toml.js` — 37/0 clean (no TOML changes).

## Tier

B. Touches `src-tauri/src/protocol/**` + `src-tauri/src/commands.rs`
+ adds a new Tauri command. `src-tauri/src/transport/**`
untouched. Will need a human merge per CLAUDE.md Tier B
rules; auto-merge disabled even if all checks pass.

Wait — actually this is the *cycle plan's* Tier B call.
Looking at the file paths: `protocol/**` + `commands.rs`
are both on the protected-path list. Slice 3b (frontend-only)
will land as Tier A after this. I haven't changed the
release tag or pushed to a production server, so no Tier C.

Co-authored-by: ohgeeceee <ohgeeceee@users.noreply.github.com>
github-actions Bot pushed a commit that referenced this pull request Jul 30, 2026
Closes the CHANGELOG gap that PR #188 (v0.14.3 slice 4) flagged
in the "Notes on the version surface" section. v0.14.1 and
v0.14.2 shipped without CHANGELOG entries because each cycle's
slice-closeout PR either forgot the version-surface sync step
or deferred it as a separate housekeeping follow-up.

This PR does the backfill from PR commit history:

- ## [0.14.1] — 2026-07-27
  - Tauri 2 `window.confirm()` auto-dismiss fix (PR #169, Tier B)
  - Simulator regenerate-on-identify (PR #169, Tier B)
  - Per-ECU freeze-schema split (PR #170, Tier A)
  Note: PR #169 shipped two slices in one PR (the dialog.js
  helper + the sim regenerate-on-identify). PR #170 is grouped
  under v0.14.1 because the ROADMAP treats the freeze-schema
  split as part of the v0.14.1 housekeeping arc (it was
  originally targeted at v0.14.0 but its tests-only refactor
  landed late and folded into the v0.14.1 PR #171 cycle-table
  retroactive close).

- ## [0.14.2] — 2026-07-29
  - Cycle plan + ROADMAP v0.14.2 header (PR #171, Tier A)
  - `community/profiles/n62.toml` enrichment — `0x5C` oil temp
    (PR #175, Tier A)
  - Live Data panel UX polish — poll-rate, peaks, range bar,
    snapshot-CSV, NRC error surface (PR #177, Tier A)
  - `docs/validation/n62-real-car.md` harness doc (PR #178,
    Tier A)
  - Claude review workflow repair — remove unsupported
    `Bash(gh pr review:*)` tool from `--allowedTools` (PR #176,
    Tier B)
  Note: the original v0.14.3 "Notes on the version surface"
  paragraph omitted PR #176 from the v0.14.2 PR list — fixed in
  this backfill.

Also updates the v0.14.3 "Notes on the version surface" section
to point at this backfill PR instead of flagging it as a
backlog item, and includes PR #176 in the v0.14.2 PR list.

Tier A — docs only. No code changes, no transport/** changes,
no protocol/** changes. PR auto-merge eligible per CLAUDE.md
once CI is green.

Verification:
- [x] CHANGELOG section order preserved: [0.14.0] (line 8) →
      [0.14.1] (line 71) → [0.14.2] (line 105) → [0.14.3]
      (line 168) → [0.13.0] (line 273) — chronological order
      matches merge order (verified via `gh pr list --state
      merged --json number,title,mergedAt`)
- [x] Every PR number cited in a backfill entry exists and
      was actually merged to main (verified via `gh pr view
      N --json mergedAt` for PRs #169, #170, #171, #175, #176,
      #177, #178)
- [x] All slice claims verified against each PR's actual body
      — no fabricated content per the data-over-invention rule
- [x] `node --test src/js/*.test.js` — 163/163 pass (no code
      changes; 5 slice 3b tests absent because this branch is
      from origin/main pre-PR-190)
- [x] `pytest backend/tests/` — 166/166 pass

Cross-references:
- PR #188 — v0.14.3 slice 4 (the cycle-closeout PR that
  flagged this backfill as the appropriate scope)
- CLAUDE.md golden rule #5 — version-surface sync (the rule
  this PR enforces retroactively for v0.14.1 + v0.14.2)
- docs/v0.14.3_plan.md — the cycle plan that calls out the
  forward-roadmap maintenance pattern this PR continues

Co-authored-by: ohgeeceee <ohgeeceee@users.noreply.github.com>
ohgeeceee added a commit that referenced this pull request Jul 31, 2026
…#190)

Cycle plan: docs/v0.14.3_plan.md (slice 3 lines 103-124). Tier B
(frontend + CSS; no transport/** changes, no protocol/** changes,
no Rust changes — the backend contract was already shipped by
PR #187). Per CLAUDE.md Tier B rules: PR is the review; no self-merge
even if all checks pass. Awaiting human merge.

## What this slice ships

### 1. live_data_panel.js — new `classifyNrc(err)` helper

Categorises each `LiveError` from `read_live_data`'s new
`LiveSweepResult { values, errors }` shape into one of three buckets
the UI acts on:

  - "unsupported" — the PID is structurally absent on this ECU
    (NRC 0x11 / 0x12 / 0x31 / 0x14); user should be offered the
    "remove from profile" affordance.
  - "transient"  — ECU is unhappy right now (NRC 0x22 conditionsNotCorrect,
    NRC 0x33 securityAccessRequired, NRC 0x78 responsePending, etc.);
    no UI action — the next sweep will retry.
  - "unknown"    — no structured sid/nrc to act on (transport timeout,
    "Not connected", "Unexpected DID response: ..."); same UI treatment
    as transient.

Uses the structured (sid, nrc) fields populated by
`protocol::nrc_from_error` (PR #187) as the fast path. Falls back to
parsing `err.error` (the verbatim protocol error string) via
`parseNrcError` when the structured fields are null, so legacy
callers and any race where the backend populated `error` but not
the structured fields still get a sensible classification.

Exported from the public surface so main.js can call it as
`window.beeemuuLiveDataPanel.classifyNrc(err)`.

### 2. live_data_panel.test.js — 5 new tests

Pinned tests for classifyNrc covering:
- structured (sid, nrc) fast path: unsupported for the four canonical NRCs
- structured (sid, nrc) fast path: transient for non-unsupported NRCs
- structured nrc=null falls back to parsing err.error
- structured nrc=null + unparsable error string returns 'unknown'
- defensive: null / non-object / missing fields never throw

Total: 20 tests in this file (was 15).

### 3. main.js — three call sites rewired for `LiveSweepResult`

The new return shape of `read_live_data` (PR #187) breaks the three
call sites that did `const values = await invoke("read_live_data", ...)`.
All three are updated:

- pollOnce (Live Data tab, line 1365) — main consumer; iterates
  `result.values` for the gauge loop, then a NEW loop over
  `result.errors` that classifies each PID and either (a) logs the
  canonical "unsupported" line + dims the gauge cell + appends a
  one-click "Remove from profile" button that calls the async
  `remove_profile_pid` Tauri command, or (b) logs the transient
  / unknown case and skips UI affordance. Clears any prior dim
  state on a successful read (PID may recover after a re-key cycle).

- buildLogParams (Logging tab channel list, line 2109) — iterates
  `result.values` only; the Logging tab doesn't render error UI.

- logTick (Logging tab chart tick, line 2203) — iterates
  `result.values` only.

New handler `removePidFromProfile(pidId, pidLabel)` — gated behind
`window.beeemuuDialog.ask()` (the same `tauri-plugin-dialog`
confirmation pattern the v0.14.1 issue-#161 fix uses). On success,
removes the gauge cell from the DOM (the PID is gone from the
in-memory profile + the on-disk TOML, so the next sweep won't
return it).

New panel-head count badge `#live-unsupported-count` — hidden when
zero, shows "N unsupported on this ECU" otherwise.

### 4. index.html — `#live-unsupported-count` slot

One-line addition in the Live Data panel head, next to the profile
selector. `hidden` by default; populated by pollOnce.

### 5. app.css — dimmed state + remove button + count badge

Three new rule blocks:
- `.gauge-cell.dimmed` — opacity 0.45 + " (unsupported)" appended
  to the label via ::after pseudo-element. Cleared on next sweep.
- `.pid-remove` — minimal inline button that sits inside the gauge
  cell below the peak label. Hover colour is the project's --err.
- `.live-unsupported-count` — small muted text in the panel head.

## What this PR does NOT do

- No Rust changes. PR #187 already shipped `protocol::nrc_from_error`,
  `LiveSweepResult { values, errors }`, `LiveError`, and the async
  `remove_profile_pid` Tauri command. This PR is the frontend consumer.
- No transport/** changes (K+DCAN / ENET transports untouched).
- No protocol/** changes.
- No new crate / dependency.
- No change to the v0.14.0 Live Gauges panel (still sim-only, per the
  v0.14.2 cycle-pick conversation).
- No version bumps. The release cut (Cargo.toml + tauri.conf.json +
  git tag) is a separate Tier C step that follows slice 3b.

## Tier

B per CLAUDE.md — `src/js/main.js` + `src/js/live_data_panel.js` +
`src/index.html` + `src/css/app.css`. Per CLAUDE.md Tier B rules:
"PR is the review; auto-merge disabled even if all checks pass.
Awaiting human merge."

## Verification

- [x] `node --check src/js/main.js` — rc=0
- [x] `node --check src/js/live_data_panel.js` — rc=0
- [x] `node --test src/js/*.test.js` — 168/168 pass (was 163, +5
  new for classifyNrc)
- [x] `cargo test --lib --offline protocol::` — 15/15 pass (no
  regression on the backend surface this slice consumes)
- [x] `pytest backend/tests/` — 166/166 pass (one test,
  test_app_live_endpoint::test_endpoint_returns_ok, was order-
  dependent flaky on first run; passed on re-run — not introduced
  by this slice)

## Cross-references

- v0.14.3 plan: docs/v0.14.3_plan.md (slice 3 lines 103-124)
- Backend contract this PR consumes: PR #187 — feat(v0.14.3):
  per-PID NRC errors + remove_profile_pid command
- Prior slice shipping the NRC-aware helpers + friendlier log line:
  PR #177 — feat(v0.14.2): Live Data panel UX polish
  (parseNrcError + isUnsupportedNrc were drafted there as
  dead-but-not-yet-usable helpers; this slice activates them via
  the per-PID error loop + classifyNrc)
- Re-enabled orphan from v0.14.2 slice 2 WIP: docs/v0.14.3_plan.md
  §"Open question #2" — the `#live-unsupported-count` badge + the
  dim/remove UI together fulfil the deferred WIP

Co-authored-by: ohgeeceee <ohgeeceee@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant