Renders a small panel inside the plot showing each visible series as [color swatch] [sensor title] [latest value + unit]. Panel width fits the longest sensor name and redraws when sensors are added or removed from the plot (RefreshPlotSeries already fires PlotInvalidated). Falls back to ellipsizing only if the label column would push the panel past the plot bounds. Capped at 12 rows with a +N more footer; auto-hides when plot pane is narrower than 200px. Theme-aware translucent background + border match the plot frame.
Persisted as showPlotLegend (default true) and toggleable from View menu and plot context menu. The View menu item is hidden when ShowPlot is off, but the setting itself is preserved across plot enable/disable and app restarts.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Moves tray/gadget sensor selection (the per-sensor 'tray'/'gadget' settings,
their key convention, and the GadgetSensorsChanged/TraySensorsChanged events)
into a focused, testable collaborator. The view model keeps thin delegating
methods and forwards the events, so its public surface (used by MainWindow) is
unchanged. Behavior is unchanged.
Adds 3 tests (persistence round-trip, event raising, tray filtering).
196 tests pass (was 193).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Moves the plot series collection, color palette, retention constants, and
the ~80-line TrackPlotPoints reconciliation (history + retained synthetic
points + current value, de-duplicated by timestamp and pruned to the
retention window) out of the view model into a focused, unit-testable
collaborator. The view model keeps a thin PlotSeries pass-through and
delegates Track/Reset/RefreshSeriesColor. Behavior is unchanged.
Adds 8 tests covering selection add/remove, history+current merge, timestamp
de-duplication, Fahrenheit conversion, reset, and pen-color application.
193 tests pass (was 185).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
All changes are localized to RemoteWebServer plus two small helpers; the
Phase 0 characterization tests confirm routing, JSON/Prometheus shape, and
credential pass/fail semantics are otherwise unchanged.
- Password hashing: new PasswordHasher uses PBKDF2-HMAC-SHA256 with a
per-credential random salt (self-describing pbkdf2$iters$salt$hash).
Verify() still accepts the legacy unsalted SHA-256 hex hash and a
successful legacy auth transparently upgrades the stored hash, persisted
by the view model on save/shutdown. Property renamed PasswordSHA256 ->
PasswordHash.
- Constant-time comparison: CredentialComparer.FixedTimeEquals for the user
name and password hash; both are evaluated fully (no && short-circuit).
- No information disclosure: POST failures return a generic message instead
of ex.ToString(); detail is logged server-side only.
- Bind intent respected: ResolveListenerIp no longer mutates ListenerIp or
silently falls back to all-interfaces for a specific configured address
(auto/'?'/wildcards still bind all). A bad address now fails Start().
- CORS: removed the Access-Control-Allow-Origin '*' wildcard; common
response headers centralized in WriteCommonHeaders.
- Prometheus: label values are escaped (EscapePrometheusLabel).
185 tests pass (was 167).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Concurrency crashes
- H1 HardwareMonitorService.RebuildTree now builds the tree under _updateLock, so it can't enumerate a hardware's _active HashSet while the update loop mutates it.
- A1 AppSettings now guards every dictionary read/write (and snapshots in Save) with a lock — safe under concurrent access from the parallel discovery threads.
- R2 Plumbed that same lock (HardwareMonitorService.SensorReadLock) into RemoteWebServer and wrapped the Prometheus sensor.Values enumeration with it.
- L1 Computer — refactored Add into AddCore, which performs the cancellation/enabled re-check and the _groups insertion atomically under _lock. A deferred task can
no longer add (and leak) a group after Close() drained the list; if it loses the race it closes the group instead.
- M1 UpdateTimer_Tick now bails before/after the await when _isShuttingDown is set in MainWindow_Closed, so an in-flight tick won't touch the disposed
view-model/Computer.
Broken behavior
- T1 Tray callback now decodes NOTIFYICON_VERSION_4 correctly (message = LOWORD(lParam), icon id = HIWORD(lParam)) — right-click menu and double-click work again.
- M2 A transient update exception no longer calls _timer.Stop(); the loop keeps running.
- H2 Newly discovered (deferred) storage devices get the current ForceDriveWakeup setting applied in HardwareChanged.
- V2 Sensor items carry a parent reference; toggling IsVisible recomputes the parent group's visibility, so no empty group headers. (Strengthened the existing test
that had skipped this assertion.)
- H3 Tree-rebuild coalescing now uses a dirty flag with a re-check, so a change arriving during a rebuild isn't lost.
- R4 Web routing matches endpoints exactly on the query-stripped path (Url.AbsolutePath), so static assets like metrics.html aren't hijacked.
- L8 IntelCpu.Update skips the bus/core-clock math while TimeStampCounterFrequency is still 0 (deferred-TSC window), so clocks keep their prior value instead of
reporting 0 MHz.
- V1 Existing plot series keep their assigned color; only an explicit user pen color updates them (no per-tick color shifting).
- M3 Runtime errors write to a dedicated runtime.log (once), instead of overwriting the shared startup.log.
- T2 CreateSensorIcon returns IntPtr.Zero on DIB failure instead of the shared main-icon handle (which callers DestroyIcon).
I also set _isOpen = false in HardwareMonitorService.Dispose so the rebuild guard actually holds during shutdown (the latent after-close-rebuild issue adjacent to
H3