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>
Build the Microsoft.Extensions.DependencyInjection provider in App and resolve the view model and runtime factory from it, replacing the manual new MainWindow(trace) path. MainWindow now takes an injected ctor (view model, tracer, runtime factory, provider); window-tied services are produced by MainWindowRuntimeFactory once the window exists. On real shutdown MainWindow_Closed disposes the provider (which disposes the view model first, then domain services and tracer), while the minimize-to-tray branch returns before any teardown.
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>
Replaces WinUI 2 grey brushes with WinUI 3 equivalents and enables the Mica system backdrop on Windows 11. RootGrid is transparent when Mica is active so the material shows through; falls back to ApplicationPageBackgroundThemeBrush on Win10. Sensor header uses LayerFillColorDefaultBrush; plot pane border uses DividerStrokeColorDefaultBrush. Black theme keeps a solid background and disables Mica.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
XamlReader-based template generation was removed when sensor tree moved to XAML, so the Markup namespace is no longer referenced.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
PlotView now has a parameterless ctor + AttachViewModel(vm) so it can be instantiated by the XAML loader. The context flyout is built lazily in AttachViewModel since it captures live ViewModel state. Class is now public sealed (XAML codegen requirement). MainWindow's BuildPlotPane is gone; the plot pane (toolbar + <controls:PlotView x:Name=PlotControl/>) lives in XAML and the ctor just calls PlotControl.AttachViewModel(ViewModel).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Declare the sensor pane (SensorPane), header host (SensorHeaderHost), and TreeView (SensorTree, with its ContentPresenter DataTemplate and SelectionChanged handler) in XAML. The measurer-managed header grid and the per-row CreateSensorRow content remain imperative since they involve dynamic column-width pushing and per-item context flyouts/double-tap handlers; this is the hybrid fallback called out in the Phase 5b plan.
Drop _sensorTree/_sensorPane fields; replace BuildSensorPane with a small PopulateSensorHeader and delete the old XamlReader-loaded DataTemplate.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Declare File/View/Options/Help menus and their toggle items in XAML with x:Bind (TwoWay) to ViewModel properties. Radio/indexed submenus (Temperature Unit, Plot Location, Theme, Stroke Thickness, File Rotation, Update Interval, Logging Interval, Sensor Values Time Window) are declared as named empty MenuFlyoutSubItems and populated by PopulateRadioSubMenu in code-behind so dynamic check-state and Tag-based selection are preserved.
Click handlers for non-toggle items (save report, reset, expand/collapse, web server dialogs, about, exit) live on the code-behind. Delete BuildMenuBar, CreateToggleItem, CreateToggleSettingItem, BuildRadioSubMenu, BuildIndexedSubMenu. CreateMenuItem is retained for the still-imperative sensor row context menu.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Express the status TextBlock and full loading overlay (ProgressRing + label) declaratively with x:Bind to ViewModel.StatusText and ViewModel.HardwareLoadingVisibility. Delete the imperative StatusHost population and BuildLoadingOverlay helper.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Make MainWindow partial and add a XAML skeleton with named hosts (MenuHost, ContentGrid, StatusHost, OverlayHost). Replace BuildRoot/AssignContent/ResolveControls with InitializeComponent plus imperative population of the named hosts; existing menu/sensor/plot/overlay builders are unchanged so later sub-steps can replace each region declaratively.
Bind OverlayHost.Visibility to HardwareLoadingVisibility so the empty host does not intercept hit-testing once loading completes. Builders that previously assigned via the brittle Children[] casting now use out params.
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>
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