Define interfaces over the three domain services that the view model hard-news
today (the testability blocker). Concrete types implement the new interfaces;
no wiring or behavior change yet.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add Microsoft.Extensions.DependencyInjection package and introduce an
IStartupTracer abstraction with NoOpStartupTracer (zero-overhead default)
and FileStartupTracer (renamed from WinUiStartupTrace). A StartupTracer
factory selects the implementation from environment configuration.
Consumers (App, MainWindow, MainWindowViewModel, SensorColumnMeasurer) now
depend on the non-nullable interface, removing null-conditional trace calls
and decoupling startup instrumentation from construction logic. No behavior
change: tracing still writes the same log when enabled.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Move the text-measurement cache, shared column-width state, 5-second settle timer, and persisted device-column width into a dedicated SensorColumnMeasurer service. MainWindow keeps the traced UpdateSensorColumnWidths wrapper and subscribes to a SettleTriggered event so tracing semantics are preserved.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Move the PlotWindow + SensorGadgetWindow create/show/sync/close lifecycle out of MainWindow into Services/SecondaryWindowCoordinator. The coordinator owns the _plotWindow and _gadgetWindow fields and exposes UpdatePlotWindowVisibility / UpdateGadgetVisibility / SyncGadgetSensors / ApplyTheme / RedrawPlot / CloseAll.
MainWindow's matching methods become one-line delegators; the gadget's HideShowMainWindowRequested event is wired back via a callback passed into the coordinator's constructor.
MainWindow shrinks from 1362 to 1300 lines; no behaviour change.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Move Win32 show/hide/minimize/restore tray-chrome logic (the four user32 P/Invokes, the SW_* constants, and the _isMainWindowHidden flag) into Services/WindowChromeManager, and the DPI-aware bounds save/restore + Maximize logic into Services/WindowPlacementService. MainWindow's MainWindow_Closed now delegates to _placementService.Save(); HideShowMainWindow / MinimizeOrHideMainWindow / HideMainWindowToTray delegate to _chromeManager.
Also removes stale PlotBounds/PlotAxisLayout/PlotSeriesSample records that were left behind from the PlotView extraction (dead code; the records now live as private nested types in PlotView).
MainWindow shrinks from 1445 to 1362 lines; no behaviour change.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The 660-line tray service mixed three concerns. Split into:
- TrayIconInterop: all P/Invoke declarations, interop structs, and the
subclass delegate (consumers use 'using static'), so the Win32 surface is
isolated and reviewable.
- SensorIconRenderer: the GDI drawing of the 16x16 per-sensor value icon.
- TrayIconService: slimmed to tray orchestration (icon lifecycle, context
menus, callback routing) at ~340 lines.
Also dedupes the per-sensor settings-key helper onto
SensorSelectionService.GetSensorSettingName. Pure relocation; behavior is
unchanged. Verified by clean build, 199 tests, and an app smoke-run.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Moves the hardware-change debounce/coalescing worker (the dirty/queued flags
and re-queue logic) into a dedicated class with an injectable delay, so the
concurrency behavior can be unit-tested deterministically instead of living as
an untestable fire-and-forget block in the service. Behavior is unchanged.
Adds 3 tests (single rebuild after delay, burst coalesced to one, no rebuild
when closed) driven by a controllable delay gate.
199 tests pass (was 196).
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>
Establishes a safety net before refactoring, and lands the first
behavior-preserving cleanup.
Tests (no production behavior change):
- SensorFormatter: full per-type coverage of GetFormatString, FormatValue,
and GetPlotUnit across every SensorType.
- RemoteWebServer: extract testable seams (ResolveRoute, VerifyCredentials,
internal pure helpers) and pin routing (incl. the no-hijack rule), query
and Prometheus parsing, JSON/metric shape, credential semantics, and the
legacy SHA-256 vector.
- HardwareMonitorService: pin the enable-flag -> settings-key mapping.
- Logger: add a TimeProvider/base-dir test seam; deterministic rotation tests.
- Add [InternalsVisibleTo] for the test project.
Cleanup:
- Collapse SensorFormatter's three parallel SensorType switches into one
GetFormat source of truth; dedupe CelsiusToFahrenheit. Verified identical
by the new characterization tests.
167 tests pass (was 55).
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