Commit Graph
20 Commits
Author SHA1 Message Date
JMR-dev 8a17a5e799 icons show correctly now 2026-06-10 23:17:25 -05:00
JMR-dev 193de6059e wip 2026-06-08 10:37:00 -05:00
JMR-dev 739b2be4c2 clean up and AMD parity check 2026-06-03 18:59:45 -05:00
JMR-dev 2ffd237ddd cleanup after DI graph rewrite 2026-06-03 17:27:58 -05:00
JMR-devandCopilot 5aa7c30a6a Finalize DI: drop transitional VM ctor; harden disposal
Remove the transitional MainWindowViewModel(AppSettings, IStartupTracer) constructor and the _ownsServices flag now that every caller resolves the view model from the container; the container owns and disposes the hardware monitor (driver unload), so the view model no longer disposes it. Make HardwareMonitorService.Dispose idempotent with a _disposed guard, and run provider disposal in a finally block in MainWindow_Closed so the ring0 driver is always unloaded on real exit even if an earlier teardown step throws.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-02 15:25:39 -05:00
JMR-devandCopilot dcb6a5b803 Add DI container and view-model tests
Add a composition-root test that builds the provider with ValidateOnBuild to catch missing registrations (without touching the real hardware monitor), plus per-service registration assertions. Add a MainWindowViewModel test that constructs the view model entirely from faked IHardwareMonitorService/ILogger/IRemoteWebServer, proving the testability win. To make that possible, inject the UI DispatcherQueue (captured on the UI thread by the composition factory) instead of calling the static DispatcherQueue.GetForCurrentThread() in the view-model constructor, which threw in the headless test host; the tree-rebuild marshalling now treats a null dispatcher as direct invocation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-02 15:20:40 -05:00
JMR-devandCopilot 594429bb7d Add fully-injected MainWindowViewModel constructor
Introduce an injected constructor taking IHardwareMonitorService, ILogger,
IRemoteWebServer (+ the concrete selection/plot/startup services) and
IStartupTracer, with shared settings initialization. A transitional 2-arg
constructor still builds the legacy graph for MainWindow until it is
DI-resolved; an _ownsServices flag keeps disposal correct across both paths.

Break the VM<->RemoteWebServer construction cycle by moving the root provider
to RemoteWebServer.SetRootProvider (removing it from the constructor), and make
RemoteWebServer.Quit idempotent. Update RemoteWebServer tests for the new ctor.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-02 15:09:12 -05:00
JMR-devandCopilot 9b576ea633 Extract startup tracing behind IStartupTracer abstraction
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>
2026-06-02 14:57:48 -05:00
JMR-devandCopilot 73a81138a7 Add plot legend with floating top-right overlay
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>
2026-06-02 14:26:11 -05:00
JMR-devandClaude Opus 4.8 6cd0694cd7 Extract SensorSelectionService from MainWindowViewModel
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>
2026-06-02 10:47:40 -05:00
JMR-devandClaude Opus 4.8 4e5e0961d8 Extract PlotTrackingService from MainWindowViewModel
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>
2026-06-02 10:42:50 -05:00
JMR-devandClaude Opus 4.8 15737ad639 Harden remote web server security
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>
2026-06-02 10:35:13 -05:00
JMR-dev 54a88e1f31 plot window refactor 2026-06-01 18:45:00 -05:00
JMR-devandClaude Opus 4.8 1bde5d531d Fix unbounded memory growth during monitoring
While the app was running, working set grew ~20 MB/min (reaching tens of GB
over a day). dotnet-gcdump traced it to COM-callable wrappers accumulating from
the per-tick binding interop: SensorTreeItemViewModel.RefreshValues raised
PropertyChanged for Value/Min/Max/ToolTip on every sensor every update tick, and
ViewModelBase allocated a new PropertyChangedEventArgs per raise. WinUI's binding
engine is native, so each raised event marshals its args across the boundary and
retains a wrapper.

- ViewModelBase: reuse one cached PropertyChangedEventArgs per property name.
- SensorTreeItemViewModel.RefreshValues: raise PropertyChanged only when the
  formatted text actually changed.

Also close a leaked D3DKMT adapter handle: D3DDisplayDevice.GetDeviceInfoByIdentifier
opened the adapter but skipped CloseAdapter on every early-return failure path,
and it runs on each GPU's Update() tick. Close it in a finally block.

Verified with dotnet-gcdump over an 18-minute soak: managed heap and live object
count stay flat (~50 MB) instead of climbing 47 -> 470 MB.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 16:16:40 -05:00
JMR-dev f9edc1dc64 Fixes applied (14 of 15 findings — see note on R5)
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
2026-05-30 22:56:25 -05:00
JMR-dev c54b5dc4f4 MVP port complete 2026-05-30 20:17:36 -05:00
JMR-dev a1cffce9fd MVP port complete 2026-05-30 19:47:04 -05:00
JMR-dev 2c176e5dd7 WIP - optimizing start times 2026-05-30 17:45:37 -05:00
JMR-dev b744f70b0b WIP - optimizing start times 2026-05-30 15:11:32 -05:00
JMR-dev 45bf81ebfa WIP - have working UI for base functions 2026-05-30 14:01:22 -05:00