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>
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>
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>
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>
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>
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