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>
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>
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>
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>
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>
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>
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>
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
* Add support for Intel Panther Lake microarchitecture
* Reorder RaptorLake in CPU microarchitecture list
* Handle unknown microarchitecture case in IntelCpu
* Refactor Intel CPU microarchitecture detection