Commit Graph
24 Commits
Author SHA1 Message Date
JMR-devandCopilot 69012a0711 Extract WindowChromeManager + WindowPlacementService
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>
2026-06-02 12:08:24 -05:00
JMR-devandCopilot 888c2cd852 Extract PlotView control from MainWindow
Move ~460 lines of imperative Canvas plot rendering (draw helpers, value/time axes, zoom state, context flyout, wheel handler, color theming, and the PlotBounds/PlotAxisLayout/PlotSeriesSample records) out of MainWindow into a new Controls/PlotView : Grid that owns its own Canvas. PlotWindow now hosts a PlotView instead of a raw Canvas; the shared rendering path collapses to PlotView.Redraw().

MainWindow shrinks from 2014 to 1445 lines; the inline plot pane and the windowed plot now share a single code path. Public API on PlotView: Redraw(), Clear(), ResetZoom(), ApplyTheme(AppThemeMode).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-02 11:51:46 -05:00
JMR-devandClaude Opus 4.8 ad16c9f3a4 Extract DialogService from MainWindow
Moves the seven ContentDialog flows (rename, pen color, sensor parameters,
web-server settings, web-server authentication, save report, about) plus the
save-report file picker out of MainWindow into a DialogService. The service
reads the live XamlRoot through a provider and writes results back through the
view model; MainWindow's menu/context-menu handlers now delegate to it. The
parameter-editor row builder, the IPv4-address helper, and the
ParameterEditorRow record (all used only by these dialogs) moved with them, and
four now-unused usings were dropped.

First code-only step of the XAML migration; pure relocation, behavior
unchanged. Verified by clean build, 199 tests, and an app smoke-run.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 11:26:35 -05:00
JMR-devandClaude Opus 4.8 6523b570c2 Split TrayIconService into interop, renderer, and orchestration
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>
2026-06-02 11:14:11 -05:00
JMR-devandClaude Opus 4.8 268f15a19e Extract TreeRebuildCoalescer from HardwareMonitorService
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>
2026-06-02 11:06:52 -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-devandClaude Opus 4.8 463e80709f Add characterization tests; consolidate SensorFormatter switches
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>
2026-06-01 19:58:44 -05:00
JMR-dev 54a88e1f31 plot window refactor 2026-06-01 18:45:00 -05:00
JMR-dev 667cb4117f update .gitignore 2026-06-01 16:24:09 -05:00
JMR-devandClaude Opus 4.8 c8378e9ab7 Reuse a single TextBlock for sensor-column text measurement
MeasureText runs for every sensor's Value/Min/Max on each update tick, and the
frequently-changing value strings miss the width cache, so it created and threw
away a WinUI TextBlock (with a native peer) on nearly every call. Reuse one
cached instance to avoid that per-tick allocation churn.

Not a leak fix — the GC reclaimed those elements fine; this just removes
needless allocation and CPU work from the update loop.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 16:22:34 -05:00
JMR-devandClaude Opus 4.8 93784cc630 Fix tiny window on high-DPI displays and tray restore un-maximizing
AppWindow.Resize/Move take physical pixels and the app is PerMonitorV2-aware,
but RestoreWindowBounds passed the logical default/minimum sizes unscaled, so on
a high-DPI display (e.g. a 200% laptop panel) the window came out at half size.
That showed up when restoring from the tray, since RestoreMainWindow's SW_RESTORE
un-maximizes to that size. Scale the default/minimum sizes by the window DPI via
GetDpiForWindow.

Also stop RestoreMainWindow from un-maximizing: it called SW_RESTORE
unconditionally, collapsing a window that was hidden to the tray while maximized.
Only SW_RESTORE when the window is minimized; otherwise SW_SHOW preserves state.

Verified on a 200% display: the restored window is 1520x1360 (= 760x680 logical)
and stays maximized across tray hide/show.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 16:16:49 -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 041e21f9a6 added support for self contained executible 2026-05-30 23:18:32 -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 5f88f71e61 added unit tests 2026-05-30 22:07:19 -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 f8b9949187 performance optimizations - TSC estimation moved off blocking path 2026-05-30 18:55:12 -05:00
JMR-dev c50757868f WIP - optimizing start times 2026-05-30 18:37:39 -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