# Architecture SysManager is a tabbed WPF desktop app on .NET 10, written in C# 14. It follows a standard MVVM layout with a thin service layer that wraps Windows APIs, PowerShell, and external CLIs (winget, Ookla `speedtest`). It's gamer-focused: network presets for CS2, PUBG, FACEIT and streaming plus a general one, and safe cleanup for Steam / Epic / Battle.net / Riot / GOG / EA caches. Built by [laurentiu021](https://github.com/laurentiu021) · MIT licensed. ## Solution layout ``` SystemManager/ # the repository ├── SysManager/ # the solution: SysManager.sln, Directory.Build.props, and │ │ # Directory.Packages.props, which pins every package version │ ├── SysManager/ # main WPF app │ │ ├── Data/ # static data files (ProcessDescriptions.json) │ │ ├── Models/ # POCOs (snapshots, samples, reports, cleanup categories) │ │ ├── Services/ # Windows / PowerShell / CLI wrappers │ │ ├── ViewModels/ # one VM per tab, MainWindowViewModel, and the row and chart │ │ │ # view-models the tabs build (NavItem, UndoChangeRow, …) │ │ ├── Views/ # XAML views + code-behind, plus six shared controls: │ │ │ # AdminBanner (the elevation banner, 31 tabs) │ │ │ # EmptyState (icon + title + message for an empty list) │ │ │ # DevelopmentBanner (the PREVIEW notice) │ │ │ # StatusFooter (progress bar + status line, 22 tabs) │ │ │ # ConsoleView (the live output console, five tabs) │ │ │ # ThemePopup (the Appearance panel) │ │ ├── Helpers/ # AdminHelper, converters, collections, parsers, the signature │ │ │ # and path checks │ │ ├── Resources/ # icons and assets │ │ ├── App.xaml(.cs), MainWindow.xaml(.cs) │ │ ├── ServiceRegistration.cs # DI container configuration │ │ ├── AssemblyInfo.cs # InternalsVisibleTo for the two test projects that need it │ │ ├── app.manifest │ │ └── SysManager.csproj │ ├── SysManager.Tests/ # xUnit unit tests (CI-safe, no system deps) │ ├── SysManager.IntegrationTests/# xUnit integration tests (CI, non-blocking) │ └── SysManager.UITests/ # FlaUI UI-automation tests (CI, non-blocking) ├── docs/ # screenshots, GIFs, manual-smoke.ps1 ├── .github/ # workflows, issue and PR templates, Dependabot ├── global.json # SDK pin, and the Microsoft.Testing.Platform runner └── publish.ps1 # the release build ``` ## Tabs (view models) The sidebar organises tabs into 12 groups (11 collapsible + a flat top-level Dashboard) via `NavGroup` → `NavItem` hierarchy built by `BuildNavGroups()` using `Group()` plus one of two item helpers: `Tab()`, which defers resolving the view-model until the tab is first opened, and `EagerItem()` for the few entries whose view-model must exist at startup. Dashboard renders as a flat top-level entry. Each group carries an icon, passed to `Group()` as a Segoe Fluent Icons code point and asserted distinct; leaves carry none, so the icon column belongs to the twelve headings rather than the sixty pages. Collapsed groups show a child count badge, a written two-line subtitle passed to `Group()` and asserted to fit that budget, and a tooltip still generated from the child labels. Exactly one group starts expanded — `InitiallyExpandedGroupId` (`grp-cleanup`) — set in the same loop that adds the groups; `#1519`'s arithmetic is why it is one and not two. `MainWindowViewModelTests.NavGroups_ExactlyTheCleanupGroupStartsExpanded` (integration) builds a real `MainWindowViewModel` and asserts that exactly one group opens and that it is the constant's, which also catches a constant naming a group that no longer exists; `ArchitectureTests.TheStartupExpansion_GoesThroughTheNamedConstant` checks that the source goes through the constant rather than a repeated literal. It costs no view-model: the child rows bind only `NavItem`'s own properties, none of which touches `Content`. `MainWindowViewModel.SelectedNav` mirrors selection into `NavItem.IsSelected`. `NavItem` also mirrors the tab's `IsBusy`, `Progress` and `IsProgressIndeterminate` out of its view-model, and `MainWindowViewModel.MapTaskbarProgress` turns the selected tab's pair into the Windows taskbar button's `ProgressState`/`ProgressValue` (bound in `MainWindow.xaml`). Both progress signals are read, not one: 37 view-models set the indeterminate flag and 7 set a percentage. Deep Cleanup, File Shredder and Speed Test set neither — they report progress through properties of their own — so their work does not reach the taskbar yet. The mapping reads the MIRRORED values and never `NavItem.Content`, because touching Content materialises the view-model and would rebuild every lazy tab the shell asked about. The flat Dashboard row and grouped leaf rows are invokable buttons styled `SidebarNavButton`. Their inner visuals consume selection through the shared `SidebarNavRow`, `SidebarNavText`, and `SidebarActiveMark` styles, while `SelectionStatus` exposes the same state to UI Automation. Group headers are keyboard-focusable toggles; collapsed content is disabled so visually hidden leaves cannot receive focus. Keyboard focus is drawn by ONE shared resource, `FocusRing` in `App.xaml`, applied through `FocusVisualStyle` on the button, toggle and chip styles whose template replaces the default one — a custom template otherwise takes the framework's focus adorner with it. The ComboBox and its items, the DataGrid column headers and the sidebar's group headers do not set it. It is a two-`Rectangle` adorner (white inner stroke, near-black outer) rather than a themed border, because the ring must stay visible on `PrimaryButton`'s accent fill, `DangerButton`'s red, a raised grey and a card surface; any single colour, accent included, falls to 1.00:1 against at least one of those. `ArchitectureTests.NoStyle_SuppressesTheKeyboardFocusIndicator` forbids `FocusVisualStyle="{x:Null}"` anywhere and asserts the ring keeps both strokes. `ButtonBase` carries `MinWidth`/`MinHeight` of 28, and the six styles derived from it in `App.xaml` (Primary, Secondary, Ghost, DangerGhost, Admin and Danger) inherit it, as does `SidebarNavButton` in `MainWindow.xaml`. Without a floor a button was exactly its padding plus its content, and many trim their padding to fit a compact row or toolbar. Every one of the 110 buttons in the views naming an explicit `Padding` was rendered on the STA thread with its own style, padding, font size and label: **30 measured under WCAG 2.5.8 AA's 24 × 24**, across 16 files — 26 failing on height alone, 4 on both axes, none on width alone. The worst two are DNS & Hosts' remove-entry `X` at 20.2 × 19.3, which is the smallest destructive target in the app, and the sidebar's clear-search glyph at 49.1 × 15.3 (#1556). 28 rather than 24 because a floor at the threshold leaves nothing for a fractional DPI scale, and because 28 × 28 is already the size of the sidebar footer chips. The floor also **clamps an explicit size upward** — WPF resolves a size as `Max(MinWidth, Min(MaxWidth, Width))` — so the clear-search button, which then carried `Width="20"`, came out 28 wide, and the reserved right padding on the TextBox it overlays had to grow from 22 to 32 with it. That was the one knock-on the change had, found by measuring rather than on screen; the explicit width has since been removed. `SysManager.IntegrationTests.HitTargetSizeTests` **measures** rather than reading the setters — a test asserting `MinWidth` would pass while a derived style, a template or a call-site `Padding` made the rendered box smaller, and the rendered box is what a user has to hit. Its floor is the WCAG 24, deliberately below the 28 the style sets, so a deliberate rise to 32 does not fail a guard meant to catch a regression. Two companion tests bound the cost: one asserts the ordinary buttons are still sized by their content, since a minimum is only free while it does not reshape what was already large enough; the other pins the `Max(MinWidth, Width)` resolution, because the intuitive belief is the opposite one. The type scale in `App.xaml` runs in two families. **Text**: `Display` (28) → `Heading` (20) → `SectionTitle` (14) → `Body` (13) → `Subtle` (12) → `Caption` (11). **Numbers**: `MetricHero` (30), `MetricLarge` (26), `Metric` (22), `MetricSmall` (20), `MetricCompact` (16). Every rung except `Display` was added for a size views were *already* rendering with raw `FontSize` attributes, so adopting one has never changed how anything looks — only where the number comes from. `Body` is the largest of those: 13 is what the app writes in, 54 TextBlocks set it by hand before the rung existed, and having no name for it was why a contributor reached for the raw value (#1634). `Heading` was restored alongside its first real user — the sidebar wordmark — having been deleted once for having none (#1630). `Heading` and `MetricSmall` share a size and are deliberately separate: one is text, the other a number, and System Logs' severity counts take the metric one for that reason. Two 16/SemiBold headings on DNS & Hosts stay raw for the mirror-image reason — they are headings, and the nearest heading token would shrink them. Every rung except `Display` is `BasedOn` the implicit `TextBlock` style, and that is load-bearing rather than tidy: an explicit style REPLACES the keyless `