From 4143b62606268ca611c5c2999cce11beaca9ceb5 Mon Sep 17 00:00:00 2001 From: nicolasestrem <103498114+nicolasestrem@users.noreply.github.com> Date: Sun, 9 Aug 2026 21:25:40 +0200 Subject: [PATCH 1/4] docs: exclude publish-* dirs, document WPF Timeline shell and crash fixes --- .gitignore | 2 ++ CHANGELOG.md | 47 +++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index 50124dc..a51007f 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,8 @@ src/packages/ # Release artifact directory used by the /release skill (a ~69 MB self-contained exe). publish/ +# WPF and UI test publish directories (self-contained executables, ~70 MB each). +publish-*/ # dotnet test output: .trx logs, and the crash/hang dumps --blame-hang writes, which run to # hundreds of megabytes each. diff --git a/CHANGELOG.md b/CHANGELOG.md index cb351f9..93e2b42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,47 @@ version numbers, because that is what those releases were called. ## [Unreleased] +### Added — WPF Timeline + Compare shell (side-by-side with WinForms) + +A new WPF application (`WinRestoreKit.Wpf`) ships alongside the existing WinForms app, implementing the +approved Timeline + Comparison Workspace design direction. It does not replace WinForms yet; both run +from the same solution and share the same Core engine and Application layer. + +**Timeline is the home experience.** Every snapshot and failed attempt on this PC appears as a card on a +point-in-time rail, newest first. Selecting a verified snapshot opens comparison; failed and unreadable +entries open their diagnostic reason. Arrow keys navigate the list and every custom visual exposes UI +Automation names, roles, and states. + +**Compare shows evidence before restore.** Selecting a snapshot compares each module against the current +PC and reports honest state: captured, not captured, or changed. You build a restore set by selecting +whole modules, then move to a Confirm stage that reviews impact, warnings, and application-close +consent before any restore starts. + +**Backup, Progress, and Results are migrated.** The backup workspace offers scope presets, destination, +compression, and validation. Progress shows stage, percent, throughput, byte counts, errors, warnings, +and a live log. Results present a severity summary with per-module outcomes. + +**Visual system.** Neutral Windows surfaces, one mineral-blue action colour, one restrained coral +warning, Light/Dark/Follow-system themes, Segoe UI Variable typography, IBM Plex Mono for technical +identifiers, 6–10 px corner radii, flat panes, and visible keyboard focus throughout. Raw icon-enum +text (e.g. `ErrorCircle`) was eliminated at its source: status glyphs are vector paths keyed by tone, +never text. + +**Application layer extraction.** Orchestration, backup presets, scope groups, run UI, snapshot +services, and update/theme services moved from the WinForms project into a new +`WinRestoreKit.Application` library shared by both shells. Core remains unchanged in its isolation +guarantee: no WinForms or WPF dependency can compile against it. + +### Fixed — WPF crashes on snapshot creation and About navigation + +Two read-only property bindings in the redesigned WPF views caused `InvalidOperationException` crashes: + +- **About page** bound `Run.Text` to the read-only `CurrentVersion` property (TwoWay by default). +- **Progress view** bound `ProgressBar.Value` to the read-only `Percent` property (TwoWay by default). + +Both are fixed with `Mode=OneWay`. Regression tests render each view through the layout dispatcher to +prove the crash path no longer fires. + ### Design - Started a three-direction visual identity and WinUI 3 Home-screen exploration around the @@ -17,12 +58,6 @@ version numbers, because that is what those releases were called. calmer spacing. Logo exploration remains unselected and no runtime or backup-format behavior has changed. -### Changed - -- Rebuilt the app shell and all primary views with the Industry design system: bundled Barlow, Barlow Condensed, and IBM Plex Mono typography; Voltage, Flux, and Follow system palettes; blueprint frames; icon rail navigation; and a dedicated progress view. -- Added snapshot display names, selectable destination folders, Fast and Max archive compression, archive-backed restore discovery, live registry drift detection, rich backup progress metrics, and safe pause or cancel controls. -- Reworked backup, restore, History, Home, and About around real manifest and module data. Existing backup folders and frozen manifest keys remain compatible. - ## [0.0.1] - 2026-08-02 ### Changed From 34a9bab938400065c389a786bda053092e322813 Mon Sep 17 00:00:00 2001 From: nicolasestrem <103498114+nicolasestrem@users.noreply.github.com> Date: Sun, 9 Aug 2026 21:26:16 +0200 Subject: [PATCH 2/4] feat: WPF Timeline + Compare shell, Application layer extraction, crash fixes Add a new WPF application (WinRestoreKit.Wpf) implementing the approved Timeline + Comparison Workspace design, running side-by-side with the existing WinForms shell. Both share the same Core engine and a new WinRestoreKit.Application library. Architecture: - WinRestoreKit.Application: orchestration, backup presets, scope groups, snapshot event catalog, comparison service, update/theme services extracted from the WinForms project and shared by both shells - WinRestoreKit.Wpf: Timeline home, Compare/Confirm restore workflow, Backup workspace with presets, Progress with live metrics, Results, Advanced History, Settings, About, App Restore dialog - WinRestoreKit.Core: unchanged isolation guarantee (no UI dependency) - WinRestoreKit (WinForms): updated to reference Application layer Visual system: - Neutral Windows surfaces, mineral-blue actions, restrained coral warnings - Light/Dark/Follow-system themes with shared token set (~35 colours each) - Segoe UI Variable + IBM Plex Mono typography - Vector status glyphs (eliminates raw ErrorCircle enum text at source) - Full implicit control templates, visible keyboard focus, accessible names - 6-10px radii, flat panes, no gradients/glow/blur Fixed: - AboutView: Run.Text bound CurrentVersion read-only (TwoWay default) -> Mode=OneWay - ProgressWorkspaceView: ProgressBar.Value bound Percent read-only -> Mode=OneWay - Both crash with InvalidOperationException on view render; regression tests added Tests: 980 passed, 0 failed, 0 skipped (Release) Build: 0 warnings, 0 errors --- assets/WinRestoreKit-one-pager.html | 567 ++++++ .../2026-08-09-backup-progress-results.md | 1321 ++++++++++++ .../2026-08-09-compare-confirm-restore.md | 1812 +++++++++++++++++ ...-timeline-compare-wpf-migration-roadmap.md | 77 + .../plans/2026-08-09-timeline-event-model.md | 906 +++++++++ ...-08-09-wpf-cutover-release-verification.md | 1407 +++++++++++++ ...-08-09-wpf-foundation-application-shell.md | 1454 +++++++++++++ ...08-09-timeline-compare-wpf-shell-design.md | 353 ++++ .../AppRestore/AppRestoreService.cs | 270 +++ .../Backup/BackupCompletionPublisher.cs | 44 + .../Backup/BackupPresets.cs | 13 + .../Backup}/ScopeGroups.cs | 72 +- .../Comparison/ComparisonState.cs | 10 + .../Comparison/ModuleComparison.cs | 27 + .../Comparison/SnapshotComparisonService.cs | 261 +++ .../Modules/BackupModuleCatalog.cs | 14 + .../Modules/BackupModuleRegistration.cs | 20 + .../BackupRestoreOrchestrator.cs | 22 +- .../Orchestration/IRunUi.cs | 16 +- .../Orchestration/RunControl.cs | 0 .../Orchestration/RunCoordinator.cs | 0 .../Properties/AssemblyInfo.cs | 5 + .../Results/RunSummary.cs | 17 +- .../Settings}/BackupRootRegistry.cs | 0 .../Settings/IThemeSettings.cs | 8 + .../Settings/RegistryThemeSettings.cs | 46 + .../Settings/ThemeMode.cs | 9 + .../Snapshots}/BackupFolders.cs | 199 +- .../Snapshots/SnapshotEvent.cs | 42 + .../Snapshots/SnapshotEventCatalog.cs | 199 ++ .../Snapshots/SnapshotEventKind.cs | 10 + .../Snapshots/SnapshotPayloadPreparation.cs | 42 + .../SnapshotPayloadPreparationService.cs | 45 + .../Updates/IUpdateCheckService.cs | 10 + .../Updates/UpdateCheckResult.cs | 22 + .../Updates/UpdateCheckService.cs | 112 + .../Updates/UpdateVerdict.cs | 10 + .../Updates/VersionInfo.cs | 27 + .../WinRestoreKit.Application.csproj | 14 + src/WinRestoreKit.Core/Conf/AppStoreApps.cs | 49 +- src/WinRestoreKit.Core/Conf/FileModule.cs | 34 +- src/WinRestoreKit.Core/Conf/FolderModule.cs | 35 +- src/WinRestoreKit.Core/Conf/WPowerPlans.cs | 14 +- src/WinRestoreKit.Core/Results/RegFile.cs | 43 +- .../Results/RestoreContents.cs | 28 +- src/WinRestoreKit.Core/Results/RestoreLog.cs | 33 +- .../Results/SnapshotGate.cs | 17 +- .../WinRestoreKit.Core.csproj | 2 + .../AdvancedHistoryViewModelTests.cs | 88 + .../AppRestoreDialogTests.cs | 690 +------ .../AppRestoreServiceTests.cs | 104 + .../ApplicationBoundaryTests.cs | 34 + .../ArchiveProgressTests.cs | 6 +- .../BackupCompletionPublisherTests.cs | 65 + .../BackupDestinationContainmentTests.cs | 6 +- .../BackupDestinationLifecycleTests.cs | 26 +- .../BackupFoldersReadTests.cs | 23 +- .../BackupModuleCatalogTests.cs | 24 + .../BackupOutputPathTests.cs | 94 + .../BackupPageViewTests.cs | 2 +- src/WinRestoreKit.Tests/BackupPresetsTests.cs | 10 +- .../BackupResultTimelinePublicationTests.cs | 94 + .../BackupWorkspaceViewModelTests.cs | 162 ++ .../BackupWorkspaceViewTests.cs | 35 + .../ComparisonWorkspaceViewModelTests.cs | 222 ++ .../ConfirmViewModelTests.cs | 106 + .../FileFolderStaleArtifactTests.cs | 104 + .../LockedPayloadBackupTests.cs | 5 +- src/WinRestoreKit.Tests/ModuleShapeTests.cs | 14 +- .../ProgressPageViewTests.cs | 16 +- .../ProgressWorkspaceViewModelTests.cs | 181 ++ .../RegFileValidateContentTests.cs | 56 + .../RestoreConsentCancellationTests.cs | 6 +- .../RestoreConsentDialogTests.cs | 42 + .../RestoreContentsPayloadErrorTests.cs | 48 + .../RestoreDialogOwnerTests.cs | 64 + .../RestoreSetViewModelTests.cs | 36 + .../ResultWorkspaceViewModelTests.cs | 93 + src/WinRestoreKit.Tests/RunSummaryTests.cs | 24 +- .../ScopeGroupsPrivacyTests.cs | 3 +- .../ShellBackupFlowTests.cs | 96 + .../SnapshotComparisonServiceTests.cs | 448 ++++ .../SnapshotEventCatalogTests.cs | 257 +++ .../SnapshotFolderPathTests.cs | 8 +- .../SnapshotGateConsentTests.cs | 48 + .../SnapshotPayloadPreparationServiceTests.cs | 132 ++ src/WinRestoreKit.Tests/SnapshotTests.cs | 16 +- src/WinRestoreKit.Tests/ThemeServiceTests.cs | 49 + src/WinRestoreKit.Tests/ThemeSettingsTests.cs | 31 + .../TimelineAccessibilityTests.cs | 166 ++ .../TimelineViewModelTests.cs | 119 ++ .../TimelineWpfSmokeTests.cs | 99 + .../UpdateCheckVerdictTests.cs | 51 + .../VersionParsingTests.cs | 82 +- .../ViewDataHelperTests.cs | 18 +- .../WPowerPlansManifestPreClearTests.cs | 50 + .../WinRestoreKit.Tests.csproj | 5 + .../WpfAppRestoreDialogTests.cs | 46 + src/WinRestoreKit.Tests/WpfLogSinkTests.cs | 42 + src/WinRestoreKit.Tests/WpfRunUiTests.cs | 82 + src/WinRestoreKit.Tests/WpfShellTests.cs | 124 ++ src/WinRestoreKit.Tests/WpfTestHost.cs | 90 + src/WinRestoreKit.Wpf/App.xaml | 11 + src/WinRestoreKit.Wpf/App.xaml.cs | 39 + .../Infrastructure/AsyncDelegateCommand.cs | 45 + .../Infrastructure/DelegateCommand.cs | 26 + .../Infrastructure/ObservableObject.cs | 29 + src/WinRestoreKit.Wpf/MainWindow.xaml | 86 + src/WinRestoreKit.Wpf/MainWindow.xaml.cs | 24 + .../Navigation/CompareWorkflowNavigator.cs | 82 + .../Navigation/ITimelineNavigator.cs | 11 + .../Properties/AssemblyInfo.cs | 3 + .../Resources/SnapshotEventTemplates.xaml | 282 +++ .../Services/CompareDialogService.cs | 19 + .../Services/ExternalLinkService.cs | 9 + .../Services/ICompareDialogService.cs | 11 + .../Services/IExternalLinkService.cs | 7 + .../Services/IRunDialogService.cs | 12 + .../Services/IRunPresentation.cs | 15 + .../Services/ISystemThemeDetector.cs | 7 + .../Services/IThemeService.cs | 13 + .../Services/IWpfDialogService.cs | 10 + .../Services/RestoreRunDialogService.cs | 31 + .../Services/WindowsThemeDetector.cs | 27 + .../Services/WpfAppRestoreDialog.cs | 47 + .../Services/WpfDialogService.cs | 60 + .../Services/WpfDispatcher.cs | 41 + src/WinRestoreKit.Wpf/Services/WpfLogSink.cs | 49 + src/WinRestoreKit.Wpf/Services/WpfRunUi.cs | 138 ++ .../Services/WpfThemeService.cs | 81 + .../Services/WpfUpdatePresenter.cs | 62 + src/WinRestoreKit.Wpf/Themes/Controls.xaml | 870 ++++++++ src/WinRestoreKit.Wpf/Themes/Dark.xaml | 84 + src/WinRestoreKit.Wpf/Themes/Light.xaml | 84 + .../ViewModels/AboutViewModel.cs | 34 + .../ViewModels/AppRestoreDialogViewModel.cs | 182 ++ .../ViewModels/BackupRunCompletion.cs | 21 + .../ViewModels/BackupRunRequest.cs | 24 + .../ViewModels/BackupScopeItemViewModel.cs | 38 + .../ViewModels/BackupWorkspaceViewModel.cs | 125 ++ .../ViewModels/ComparisonFilter.cs | 8 + .../ComparisonWorkspaceViewModel.cs | 179 ++ .../ViewModels/ConfirmViewModel.cs | 173 ++ .../History/AdvancedHistoryViewModel.cs | 96 + .../ModuleComparisonRowViewModel.cs | 65 + .../ViewModels/ModuleImpactViewModel.cs | 27 + .../ViewModels/ProgressWorkspaceViewModel.cs | 372 ++++ .../ViewModels/RestoreSetViewModel.cs | 47 + .../ViewModels/ResultWorkspaceViewModel.cs | 55 + .../ViewModels/SettingsViewModel.cs | 41 + .../ViewModels/ShellViewModel.cs | 198 ++ .../Snapshots/SnapshotEventStatus.cs | 72 + .../Snapshots/SnapshotEventViewModel.cs | 92 + .../ViewModels/Timeline/TimelineViewModel.cs | 116 ++ src/WinRestoreKit.Wpf/Views/AboutView.xaml | 57 + src/WinRestoreKit.Wpf/Views/AboutView.xaml.cs | 12 + .../Views/AdvancedHistoryView.xaml | 158 ++ .../Views/AdvancedHistoryView.xaml.cs | 20 + .../Views/AppRestoreDialog.xaml | 172 ++ .../Views/AppRestoreDialog.xaml.cs | 53 + .../Views/BackupWorkspaceView.xaml | 343 ++++ .../Views/BackupWorkspaceView.xaml.cs | 25 + .../Views/ComparisonWorkspaceView.xaml | 564 +++++ .../Views/ComparisonWorkspaceView.xaml.cs | 12 + src/WinRestoreKit.Wpf/Views/ConfirmView.xaml | 342 ++++ .../Views/ConfirmView.xaml.cs | 26 + .../Views/Controls/SnapshotEventList.xaml | 75 + .../Views/Controls/SnapshotEventList.xaml.cs | 71 + .../Views/Dialogs/RestoreConsentDialog.xaml | 86 + .../Dialogs/RestoreConsentDialog.xaml.cs | 65 + .../Views/ProgressWorkspaceView.xaml | 360 ++++ .../Views/ProgressWorkspaceView.xaml.cs | 12 + .../Views/ResultWorkspaceView.xaml | 230 +++ .../Views/ResultWorkspaceView.xaml.cs | 12 + src/WinRestoreKit.Wpf/Views/SettingsView.xaml | 69 + .../Views/SettingsView.xaml.cs | 12 + src/WinRestoreKit.Wpf/Views/TimelineView.xaml | 98 + .../Views/TimelineView.xaml.cs | 22 + .../WinRestoreKit.Wpf.csproj | 26 + src/WinRestoreKit.sln | 44 + src/WinRestoreKit/Forms/RestAppsForm.cs | 717 +------ src/WinRestoreKit/Helpers/UpdateCheck.cs | 138 -- .../Helpers/WinFormsUpdatePresenter.cs | 76 + src/WinRestoreKit/MainForm.cs | 19 +- src/WinRestoreKit/Program.cs | 71 +- src/WinRestoreKit/Views/AboutPageView.cs | 20 +- src/WinRestoreKit/Views/BackupPageView.cs | 117 +- src/WinRestoreKit/Views/BackupPresets.cs | 39 - src/WinRestoreKit/Views/ProgressPageView.cs | 4 +- .../Views/RestoreWizardStep2View.cs | 12 +- src/WinRestoreKit/WinRestoreKit.csproj | 1 + 191 files changed, 20727 insertions(+), 2013 deletions(-) create mode 100644 assets/WinRestoreKit-one-pager.html create mode 100644 docs/superpowers/plans/2026-08-09-backup-progress-results.md create mode 100644 docs/superpowers/plans/2026-08-09-compare-confirm-restore.md create mode 100644 docs/superpowers/plans/2026-08-09-timeline-compare-wpf-migration-roadmap.md create mode 100644 docs/superpowers/plans/2026-08-09-timeline-event-model.md create mode 100644 docs/superpowers/plans/2026-08-09-wpf-cutover-release-verification.md create mode 100644 docs/superpowers/plans/2026-08-09-wpf-foundation-application-shell.md create mode 100644 docs/superpowers/specs/2026-08-09-timeline-compare-wpf-shell-design.md create mode 100644 src/WinRestoreKit.Application/AppRestore/AppRestoreService.cs create mode 100644 src/WinRestoreKit.Application/Backup/BackupCompletionPublisher.cs create mode 100644 src/WinRestoreKit.Application/Backup/BackupPresets.cs rename src/{WinRestoreKit/Views => WinRestoreKit.Application/Backup}/ScopeGroups.cs (75%) create mode 100644 src/WinRestoreKit.Application/Comparison/ComparisonState.cs create mode 100644 src/WinRestoreKit.Application/Comparison/ModuleComparison.cs create mode 100644 src/WinRestoreKit.Application/Comparison/SnapshotComparisonService.cs create mode 100644 src/WinRestoreKit.Application/Modules/BackupModuleCatalog.cs create mode 100644 src/WinRestoreKit.Application/Modules/BackupModuleRegistration.cs rename src/{WinRestoreKit => WinRestoreKit.Application}/Orchestration/BackupRestoreOrchestrator.cs (98%) rename src/{WinRestoreKit => WinRestoreKit.Application}/Orchestration/IRunUi.cs (74%) rename src/{WinRestoreKit => WinRestoreKit.Application}/Orchestration/RunControl.cs (100%) rename src/{WinRestoreKit => WinRestoreKit.Application}/Orchestration/RunCoordinator.cs (100%) create mode 100644 src/WinRestoreKit.Application/Properties/AssemblyInfo.cs rename src/{WinRestoreKit => WinRestoreKit.Application}/Results/RunSummary.cs (94%) rename src/{WinRestoreKit/Helpers => WinRestoreKit.Application/Settings}/BackupRootRegistry.cs (100%) create mode 100644 src/WinRestoreKit.Application/Settings/IThemeSettings.cs create mode 100644 src/WinRestoreKit.Application/Settings/RegistryThemeSettings.cs create mode 100644 src/WinRestoreKit.Application/Settings/ThemeMode.cs rename src/{WinRestoreKit/Views => WinRestoreKit.Application/Snapshots}/BackupFolders.cs (55%) create mode 100644 src/WinRestoreKit.Application/Snapshots/SnapshotEvent.cs create mode 100644 src/WinRestoreKit.Application/Snapshots/SnapshotEventCatalog.cs create mode 100644 src/WinRestoreKit.Application/Snapshots/SnapshotEventKind.cs create mode 100644 src/WinRestoreKit.Application/Snapshots/SnapshotPayloadPreparation.cs create mode 100644 src/WinRestoreKit.Application/Snapshots/SnapshotPayloadPreparationService.cs create mode 100644 src/WinRestoreKit.Application/Updates/IUpdateCheckService.cs create mode 100644 src/WinRestoreKit.Application/Updates/UpdateCheckResult.cs create mode 100644 src/WinRestoreKit.Application/Updates/UpdateCheckService.cs create mode 100644 src/WinRestoreKit.Application/Updates/UpdateVerdict.cs create mode 100644 src/WinRestoreKit.Application/Updates/VersionInfo.cs create mode 100644 src/WinRestoreKit.Application/WinRestoreKit.Application.csproj create mode 100644 src/WinRestoreKit.Tests/AdvancedHistoryViewModelTests.cs create mode 100644 src/WinRestoreKit.Tests/AppRestoreServiceTests.cs create mode 100644 src/WinRestoreKit.Tests/ApplicationBoundaryTests.cs create mode 100644 src/WinRestoreKit.Tests/BackupCompletionPublisherTests.cs create mode 100644 src/WinRestoreKit.Tests/BackupModuleCatalogTests.cs create mode 100644 src/WinRestoreKit.Tests/BackupOutputPathTests.cs create mode 100644 src/WinRestoreKit.Tests/BackupResultTimelinePublicationTests.cs create mode 100644 src/WinRestoreKit.Tests/BackupWorkspaceViewModelTests.cs create mode 100644 src/WinRestoreKit.Tests/BackupWorkspaceViewTests.cs create mode 100644 src/WinRestoreKit.Tests/ComparisonWorkspaceViewModelTests.cs create mode 100644 src/WinRestoreKit.Tests/ConfirmViewModelTests.cs create mode 100644 src/WinRestoreKit.Tests/FileFolderStaleArtifactTests.cs create mode 100644 src/WinRestoreKit.Tests/ProgressWorkspaceViewModelTests.cs create mode 100644 src/WinRestoreKit.Tests/RegFileValidateContentTests.cs create mode 100644 src/WinRestoreKit.Tests/RestoreConsentDialogTests.cs create mode 100644 src/WinRestoreKit.Tests/RestoreContentsPayloadErrorTests.cs create mode 100644 src/WinRestoreKit.Tests/RestoreDialogOwnerTests.cs create mode 100644 src/WinRestoreKit.Tests/RestoreSetViewModelTests.cs create mode 100644 src/WinRestoreKit.Tests/ResultWorkspaceViewModelTests.cs create mode 100644 src/WinRestoreKit.Tests/ShellBackupFlowTests.cs create mode 100644 src/WinRestoreKit.Tests/SnapshotComparisonServiceTests.cs create mode 100644 src/WinRestoreKit.Tests/SnapshotEventCatalogTests.cs create mode 100644 src/WinRestoreKit.Tests/SnapshotGateConsentTests.cs create mode 100644 src/WinRestoreKit.Tests/SnapshotPayloadPreparationServiceTests.cs create mode 100644 src/WinRestoreKit.Tests/ThemeServiceTests.cs create mode 100644 src/WinRestoreKit.Tests/ThemeSettingsTests.cs create mode 100644 src/WinRestoreKit.Tests/TimelineAccessibilityTests.cs create mode 100644 src/WinRestoreKit.Tests/TimelineViewModelTests.cs create mode 100644 src/WinRestoreKit.Tests/TimelineWpfSmokeTests.cs create mode 100644 src/WinRestoreKit.Tests/UpdateCheckVerdictTests.cs create mode 100644 src/WinRestoreKit.Tests/WPowerPlansManifestPreClearTests.cs create mode 100644 src/WinRestoreKit.Tests/WpfAppRestoreDialogTests.cs create mode 100644 src/WinRestoreKit.Tests/WpfLogSinkTests.cs create mode 100644 src/WinRestoreKit.Tests/WpfRunUiTests.cs create mode 100644 src/WinRestoreKit.Tests/WpfShellTests.cs create mode 100644 src/WinRestoreKit.Tests/WpfTestHost.cs create mode 100644 src/WinRestoreKit.Wpf/App.xaml create mode 100644 src/WinRestoreKit.Wpf/App.xaml.cs create mode 100644 src/WinRestoreKit.Wpf/Infrastructure/AsyncDelegateCommand.cs create mode 100644 src/WinRestoreKit.Wpf/Infrastructure/DelegateCommand.cs create mode 100644 src/WinRestoreKit.Wpf/Infrastructure/ObservableObject.cs create mode 100644 src/WinRestoreKit.Wpf/MainWindow.xaml create mode 100644 src/WinRestoreKit.Wpf/MainWindow.xaml.cs create mode 100644 src/WinRestoreKit.Wpf/Navigation/CompareWorkflowNavigator.cs create mode 100644 src/WinRestoreKit.Wpf/Navigation/ITimelineNavigator.cs create mode 100644 src/WinRestoreKit.Wpf/Properties/AssemblyInfo.cs create mode 100644 src/WinRestoreKit.Wpf/Resources/SnapshotEventTemplates.xaml create mode 100644 src/WinRestoreKit.Wpf/Services/CompareDialogService.cs create mode 100644 src/WinRestoreKit.Wpf/Services/ExternalLinkService.cs create mode 100644 src/WinRestoreKit.Wpf/Services/ICompareDialogService.cs create mode 100644 src/WinRestoreKit.Wpf/Services/IExternalLinkService.cs create mode 100644 src/WinRestoreKit.Wpf/Services/IRunDialogService.cs create mode 100644 src/WinRestoreKit.Wpf/Services/IRunPresentation.cs create mode 100644 src/WinRestoreKit.Wpf/Services/ISystemThemeDetector.cs create mode 100644 src/WinRestoreKit.Wpf/Services/IThemeService.cs create mode 100644 src/WinRestoreKit.Wpf/Services/IWpfDialogService.cs create mode 100644 src/WinRestoreKit.Wpf/Services/RestoreRunDialogService.cs create mode 100644 src/WinRestoreKit.Wpf/Services/WindowsThemeDetector.cs create mode 100644 src/WinRestoreKit.Wpf/Services/WpfAppRestoreDialog.cs create mode 100644 src/WinRestoreKit.Wpf/Services/WpfDialogService.cs create mode 100644 src/WinRestoreKit.Wpf/Services/WpfDispatcher.cs create mode 100644 src/WinRestoreKit.Wpf/Services/WpfLogSink.cs create mode 100644 src/WinRestoreKit.Wpf/Services/WpfRunUi.cs create mode 100644 src/WinRestoreKit.Wpf/Services/WpfThemeService.cs create mode 100644 src/WinRestoreKit.Wpf/Services/WpfUpdatePresenter.cs create mode 100644 src/WinRestoreKit.Wpf/Themes/Controls.xaml create mode 100644 src/WinRestoreKit.Wpf/Themes/Dark.xaml create mode 100644 src/WinRestoreKit.Wpf/Themes/Light.xaml create mode 100644 src/WinRestoreKit.Wpf/ViewModels/AboutViewModel.cs create mode 100644 src/WinRestoreKit.Wpf/ViewModels/AppRestoreDialogViewModel.cs create mode 100644 src/WinRestoreKit.Wpf/ViewModels/BackupRunCompletion.cs create mode 100644 src/WinRestoreKit.Wpf/ViewModels/BackupRunRequest.cs create mode 100644 src/WinRestoreKit.Wpf/ViewModels/BackupScopeItemViewModel.cs create mode 100644 src/WinRestoreKit.Wpf/ViewModels/BackupWorkspaceViewModel.cs create mode 100644 src/WinRestoreKit.Wpf/ViewModels/ComparisonFilter.cs create mode 100644 src/WinRestoreKit.Wpf/ViewModels/ComparisonWorkspaceViewModel.cs create mode 100644 src/WinRestoreKit.Wpf/ViewModels/ConfirmViewModel.cs create mode 100644 src/WinRestoreKit.Wpf/ViewModels/History/AdvancedHistoryViewModel.cs create mode 100644 src/WinRestoreKit.Wpf/ViewModels/ModuleComparisonRowViewModel.cs create mode 100644 src/WinRestoreKit.Wpf/ViewModels/ModuleImpactViewModel.cs create mode 100644 src/WinRestoreKit.Wpf/ViewModels/ProgressWorkspaceViewModel.cs create mode 100644 src/WinRestoreKit.Wpf/ViewModels/RestoreSetViewModel.cs create mode 100644 src/WinRestoreKit.Wpf/ViewModels/ResultWorkspaceViewModel.cs create mode 100644 src/WinRestoreKit.Wpf/ViewModels/SettingsViewModel.cs create mode 100644 src/WinRestoreKit.Wpf/ViewModels/ShellViewModel.cs create mode 100644 src/WinRestoreKit.Wpf/ViewModels/Snapshots/SnapshotEventStatus.cs create mode 100644 src/WinRestoreKit.Wpf/ViewModels/Snapshots/SnapshotEventViewModel.cs create mode 100644 src/WinRestoreKit.Wpf/ViewModels/Timeline/TimelineViewModel.cs create mode 100644 src/WinRestoreKit.Wpf/Views/AboutView.xaml create mode 100644 src/WinRestoreKit.Wpf/Views/AboutView.xaml.cs create mode 100644 src/WinRestoreKit.Wpf/Views/AdvancedHistoryView.xaml create mode 100644 src/WinRestoreKit.Wpf/Views/AdvancedHistoryView.xaml.cs create mode 100644 src/WinRestoreKit.Wpf/Views/AppRestoreDialog.xaml create mode 100644 src/WinRestoreKit.Wpf/Views/AppRestoreDialog.xaml.cs create mode 100644 src/WinRestoreKit.Wpf/Views/BackupWorkspaceView.xaml create mode 100644 src/WinRestoreKit.Wpf/Views/BackupWorkspaceView.xaml.cs create mode 100644 src/WinRestoreKit.Wpf/Views/ComparisonWorkspaceView.xaml create mode 100644 src/WinRestoreKit.Wpf/Views/ComparisonWorkspaceView.xaml.cs create mode 100644 src/WinRestoreKit.Wpf/Views/ConfirmView.xaml create mode 100644 src/WinRestoreKit.Wpf/Views/ConfirmView.xaml.cs create mode 100644 src/WinRestoreKit.Wpf/Views/Controls/SnapshotEventList.xaml create mode 100644 src/WinRestoreKit.Wpf/Views/Controls/SnapshotEventList.xaml.cs create mode 100644 src/WinRestoreKit.Wpf/Views/Dialogs/RestoreConsentDialog.xaml create mode 100644 src/WinRestoreKit.Wpf/Views/Dialogs/RestoreConsentDialog.xaml.cs create mode 100644 src/WinRestoreKit.Wpf/Views/ProgressWorkspaceView.xaml create mode 100644 src/WinRestoreKit.Wpf/Views/ProgressWorkspaceView.xaml.cs create mode 100644 src/WinRestoreKit.Wpf/Views/ResultWorkspaceView.xaml create mode 100644 src/WinRestoreKit.Wpf/Views/ResultWorkspaceView.xaml.cs create mode 100644 src/WinRestoreKit.Wpf/Views/SettingsView.xaml create mode 100644 src/WinRestoreKit.Wpf/Views/SettingsView.xaml.cs create mode 100644 src/WinRestoreKit.Wpf/Views/TimelineView.xaml create mode 100644 src/WinRestoreKit.Wpf/Views/TimelineView.xaml.cs create mode 100644 src/WinRestoreKit.Wpf/WinRestoreKit.Wpf.csproj delete mode 100644 src/WinRestoreKit/Helpers/UpdateCheck.cs create mode 100644 src/WinRestoreKit/Helpers/WinFormsUpdatePresenter.cs delete mode 100644 src/WinRestoreKit/Views/BackupPresets.cs diff --git a/assets/WinRestoreKit-one-pager.html b/assets/WinRestoreKit-one-pager.html new file mode 100644 index 0000000..3e5ddcf --- /dev/null +++ b/assets/WinRestoreKit-one-pager.html @@ -0,0 +1,567 @@ + + + + + + WinRestoreKit — Your Windows setup, saved locally + + + +
+
+
+ + WinRestoreKit +
+
+ Windows 11 x64 + Open source + MIT licence +
+
+ +
+
+

Back up the details that make Windows yours

+

Your Windows setup, saved locally.

+

+ WinRestoreKit backs up the settings that make a PC feel familiar—then helps restore + only the modules you choose. No cloud account. No telemetry. No .NET install. +

+
+ + +
+ +
+
+

A restore path you can understand

+ backup_2026-08-09_1430 +
+
+
ChoosePick a preset or select individual modules.
+
CaptureCreate a lightweight, timestamped local backup.
+
ReviewSee what restore will overwrite before you consent.
+
RestoreProtect the current state with a pre-restore snapshot.
+
+
+ +
+
+

Back up more than a wallpaper

+
+
+
01
+
Windows personality

Taskbar, themes, visual effects, regional preferences, accessibility settings, and more.

+
+
+
02
+
Devices and connections

Printers, mouse and keyboard settings, Wi-Fi credentials, mapped drives, and network configuration.

+
+
+
03
+
Apps and developer tools

Selected app settings, terminal and VS Code data, SSH configuration, hosts file, and a winget package list.

+
+
+
04
+
Evidence with every run

Human-readable logs plus backup_manifest.json record what succeeded, skipped, or failed.

+
+
+
+ Selective modulesPresetsHistory timeline + Power plansLocal foldersSingle executable +
+
+ +
+
+
Safety without spin
+

Know what restore can—and cannot—undo.

+

Before changes begin, WinRestoreKit states what will be overwritten, asks for consent, and creates a pre-restore snapshot. Registry imports and folder copies are additive, so the app says plainly when restoring a snapshot cannot remove newly added values or files.

+
+
+
Backups stay localNo cloud account or telemetry
+
1 executableSelf-contained Windows download
+
Pick the scopePreset or module-by-module
+
Undo pointAutomatic snapshot before restore
+
+
+
+ + +
+ + diff --git a/docs/superpowers/plans/2026-08-09-backup-progress-results.md b/docs/superpowers/plans/2026-08-09-backup-progress-results.md new file mode 100644 index 0000000..c74eadf --- /dev/null +++ b/docs/superpowers/plans/2026-08-09-backup-progress-results.md @@ -0,0 +1,1321 @@ +# Backup, Progress, Results, and App Restore WPF Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Deliver the WPF Create Snapshot workflow from module selection through safe execution, honest results, and return to an updated Timeline, including the owner-bound app-reinstall dialog, while the WinForms application remains runnable. + +**Architecture:** Reuse the Foundation plan's framework-neutral `WinRestoreKit.Application` runner, run-control, and summary contracts plus the Compare/Confirm plan's module-catalog contract without modifying Core backup/restore semantics or snapshot files. WPF view models own selection, navigation, presentation, and commands; Foundation's `WpfRunUi` and `WpfLogSink` are extended and composed with the dispatcher and owner-bound dialogs. A small Application completion publisher asks the Timeline event catalog to discover retained output first and creates an in-memory failure event only when no retained event exists. + +**Tech Stack:** .NET 8 for Windows, WPF/XAML, MVVM (`INotifyPropertyChanged` and `ICommand`), xUnit 2.9.3, existing Core `BackupBase`/payload/manifest/logging APIs, and the existing Windows registry-backed destination registry. + +## Global Constraints + +- Execute after `2026-08-09-wpf-foundation-application-shell.md`, `2026-08-09-timeline-event-model.md`, and `2026-08-09-compare-confirm-restore.md`; this plan does not duplicate their contracts. +- Keep `WinRestoreKit.Core` backup, restore, manifest, payload, cleanup, containment, ownership, archive, rollback, Explorer, and shutdown semantics unchanged. Do not change the snapshot format. +- `src/WinRestoreKit.Application` references Core only; it MUST NOT reference WPF or WinForms. WPF views and view models MUST NOT parse registry exports, JSON payload text, or archive contents. +- Preserve the six ordered scopes, their current module membership, default-selection behavior, environment-variable explicit opt-in, warning text, `BackupPresets` literal membership, destination containment protection, name validation, and `SnapshotCompression.None/Fast/Max` behavior. The default compression remains `Fast`. +- The only admission point is `RunCoordinator.TryStart()`. A rejected second attempt must not construct a runner, replace the active workspace, install a log sink, or mutate snapshot state. Release admission with `RunCoordinator.SetRunning(false)` in the run owner's `finally` path. +- `RunControl.Pause()`, `Resume()`, and `RequestCancellation()` retain their existing boundary-only behavior: an active module finishes; no later module starts; cancellation wakes a paused run. Never claim rollback for work already performed. +- Disable cancellation when the runner reports the exact `BackupRestoreOrchestrator.ArchiveProgressText` value. Preserve real metric values from `ProgressMetrics`; use `N/A` where Core reports no byte measurement, never an invented estimate. +- `RunSummary` is the sole authority for completion wording. Render `RunSeverity { Information, Warning, Error }`; do not inspect `RunControl.IsCancellationRequested` to relabel a summary. In particular, a completed `RunSummary.For(...)` remains completed after a late cancel click, while `RunSummary.Incomplete(...)` remains visibly incomplete. +- The Foundation `IRunUi` has no `IWin32Window Owner` and no Windows Forms type. `DialogOwner` is an opaque `object` supplied by the shell solely for Core's existing app-reinstall dialog seam; Application neither casts it nor passes it into `AppRestoreService`. Its exact surface is: + + ```csharp + internal interface IRunUi + { + object DialogOwner { get; } + void SetProgressText(string text); + void SetProgressPercent(int percent); + void SetProgressDetail(string groupInfo, string elapsed, string remaining, + string throughput, long bytesWritten, int errors, int warnings); + void ShowSummary(RunSummary summary, string caption, + IReadOnlyList outcomes); + IReadOnlyList ShowConsentDialog(RestorePlan plan); + bool ConfirmSnapshotOverride(string text, string caption); + void ShowPlanCompositionError(string text, string caption); + void SetExplorerRestartVisible(bool visible); + } + ``` + +- Use the Timeline plan's app-lifetime `SnapshotEventCatalog`: `IReadOnlyList Read()` and `void RecordSessionFailure(DateTime created, string displayName, string diagnosticReason)`. Its immutable events use `SnapshotEventKind { Verified, Partial, Failed, Unreadable }`; only `Verified` and `Partial` are restorable. +- First re-read the catalog after a backup. Publish a session failure only when no retained recognized event corresponds to the expected backup path (for example, folder creation failed or cancellation removed the new folder). Retained partial, failed, or unreadable folders are discovered from disk and MUST NOT receive a duplicate session event. Never keep an incomplete folder merely to manufacture history. +- The WPF shell registers Core's existing `AppStoreApps.RestoreDialog` with a callback that accepts only a live WPF `Window` owner; `AppRestoreService` has no owner argument. Source-selection/read or winget-install failures appear in the owner-bound WPF dialog or inline dialog state; never use an ownerless `MessageBox`. +- Keep the original `src/WinRestoreKit` WinForms shell and `RestAppsForm` runnable during this phase. Port shared, non-UI app-restore logic out of the form and update the form to consume it; do not delete the form, designer, or legacy construction tests until Cutover. +- Keep tests in `src/WinRestoreKit.Tests`. Preserve pure tests, update all `IRunUi` fakes for the Foundation interface, and use Foundation's `WpfTestHost.Run(...)` for WPF window/control construction. Do not run formatters, linters, builds, or tests while drafting this plan. + +## Prerequisite Interfaces + +Foundation supplies the Application runner/run-control/summary contracts, WPF shell/adapters, and `WpfTestHost`; Compare/Confirm supplies `BackupModuleCatalog`. Do not recreate any of them: + +```csharp +// src/WinRestoreKit.Application/Modules/BackupModuleCatalog.cs (Compare/Confirm) +public static IReadOnlyList BackupModuleCatalog.CreateAll(); +// BackupModuleRegistration exposes public BackupBase Module, string Category, string Title. + +// src/WinRestoreKit.Application/Orchestration/BackupRestoreOrchestrator.cs +internal BackupRestoreOrchestrator(IRunUi ui, RunControl runControl = null); +internal Task RunBackup(IReadOnlyList modules, string destination, + string snapshotName, SnapshotCompression compression); +internal Task RunRestore(IReadOnlyList modules, string backupPath); +internal string BackupOutputPath { get; private set; } + +// src/WinRestoreKit.Application/Results/RunSummary.cs +internal enum RunState { Problems, Done, NothingDone, Canceled, DidNotRun } +internal enum RunSeverity { Information, Warning, Error } +// RunSummary retains For(IReadOnlyList, bool, RunVerb, string), +// Incomplete(IReadOnlyList, RunVerb, string), and Canceled(RunVerb). +// It exposes State, Severity, Headline, and Detail, maps Problems (including Incomplete) +// and DidNotRun to Warning, and all remaining current states to Information; it contains no MessageBoxIcon. + +// src/WinRestoreKit.Wpf/ViewModels/ShellViewModel.cs +public object CurrentWorkspace { get; private set; } +public string WorkflowLabel { get; private set; } +public ICommand ShowTimelineCommand { get; } +internal void ShowTimeline(); +internal void NavigateTo(object workspace, string workflowLabel); + +// src/WinRestoreKit.Wpf/Navigation/ITimelineNavigator.cs +void OpenCompare(SnapshotPayloadPreparation preparation); +void ShowSnapshotDiagnostic(SnapshotEvent snapshot); +``` + +## File Map + +| Path | Responsibility | +| --- | --- | +| `src/WinRestoreKit.Application/Backup/ScopeGroups.cs` | Framework-neutral six-scope catalog and immutable scope rows. | +| `src/WinRestoreKit.Application/Backup/BackupPresets.cs` | Literal Developer machine and Minimal privacy-safe preset memberships. | +| `src/WinRestoreKit.Application/Backup/BackupCompletionPublisher.cs` | Discover retained post-run event first; add session-only failed event only when discovery cannot represent the attempt. | +| `src/WinRestoreKit.Application/AppRestore/AppRestoreService.cs` | Shared app-export source discovery from current payload plus public Timeline events, payload preparation/disposal, parsing, list state, winget outcome wording, and install loop. | +| `src/WinRestoreKit.Wpf/ViewModels/BackupWorkspaceViewModel.cs` | Scope/preset/metadata selection and a validated, admitted backup request. | +| `src/WinRestoreKit.Wpf/Infrastructure/AsyncDelegateCommand.cs` | Reusable non-reentrant `ICommand` that awaits view-model work and re-enables on completion. | +| `src/WinRestoreKit.Wpf/Views/BackupWorkspaceView.xaml` | Accessible Create Snapshot selection UI; no backup policy or payload parsing. | +| `src/WinRestoreKit.Wpf/ViewModels/ProgressWorkspaceViewModel.cs` | One admitted run's live metrics, logs, pause/cancel commands, and terminal summary. | +| `src/WinRestoreKit.Wpf/ViewModels/ResultWorkspaceViewModel.cs` | Neutral summary/outcome rendering and return-to-Timeline command. | +| `src/WinRestoreKit.Wpf/Services/WpfRunUi.cs` | Dispatcher-safe implementation of the neutral Application callback interface and owner-bound dialog bridge. | +| `src/WinRestoreKit.Wpf/Services/WpfLogSink.cs` | Dispatcher-safe `ILogSink` that stops posting after disposal. | +| `src/WinRestoreKit.Wpf/Services/WpfAppRestoreDialog.cs` | Core `AppStoreApps.RestoreDialog` registration and owner-bound WPF package-picker construction. | +| `src/WinRestoreKit.Wpf/Views/AppRestoreDialog.xaml` and `.xaml.cs` | WPF package-picker dialog. | +| `src/WinRestoreKit.Wpf/ViewModels/AppRestoreDialogViewModel.cs` | App-export/list/install/stop state without WPF payload or JSON parsing. | +| `src/WinRestoreKit.Wpf/ViewModels/ShellViewModel.cs`, `src/WinRestoreKit.Wpf/MainWindow.xaml` | Create Snapshot command, workspace data template, run availability, and return navigation. | +| `src/WinRestoreKit/Views/BackupPageView.cs`, `src/WinRestoreKit/Forms/RestAppsForm.cs` | Still-runnable WinForms clients updated to consume moved shared selection/app-restore services; no duplicate logic. | +| `src/WinRestoreKit.Tests/WpfTestHost.cs` | Foundation-owned deterministic STA/dispatcher helper reused by all WPF runtime tests. | +| `src/WinRestoreKit.Tests/*Tests.cs` listed in each task | Focused regression, MVVM, dispatcher, dialog, and completion-publication coverage. | + +--- + +### Task 1: Move shared backup-selection metadata and add completion publication + +**Files:** +- Create: `src/WinRestoreKit.Application/Backup/ScopeGroups.cs` +- Create: `src/WinRestoreKit.Application/Backup/BackupPresets.cs` +- Create: `src/WinRestoreKit.Application/Backup/BackupCompletionPublisher.cs` +- Modify: `src/WinRestoreKit/Views/BackupPageView.cs` +- Modify: `src/WinRestoreKit.Tests/ScopeGroupsPrivacyTests.cs` +- Modify: `src/WinRestoreKit.Tests/ViewDataHelperTests.cs` +- Modify: `src/WinRestoreKit.Tests/BackupPresetsTests.cs` +- Create: `src/WinRestoreKit.Tests/BackupCompletionPublisherTests.cs` + +**Interfaces:** +- Consumes: `BackupModuleCatalog.CreateAll()`, `SnapshotEventCatalog.Read()`, `SnapshotEvent.CanonicalPath`, and `RunSummary.Detail`; neither `BackupFolders` nor a WPF-local root parser is used. +- Produces: `public sealed class ScopeGroupRow`; `public static class ScopeGroups` with `public static IReadOnlyList Build()`; `public static class BackupPresets` with `DeveloperMachine` and `MinimalPrivacySafeExclusions`; and `internal sealed class BackupCompletionPublisher` with `void Publish(string attemptedBackupPath, string displayName, RunSummary summary, DateTime created)`. + +- [ ] **Step 1: Write the failing scope, preset, and no-duplicate-event tests** + + Move the existing pure scope/preset assertions away from `Views` and add completion-publication tests that pass the runner's exact attempted output path: create a recognized retained folder whose name deliberately differs from `Data.NowShort`, then use a fresh nonexistent attempted path for the session-only case. + + ```csharp + [Fact] + public void Publish_RetainedPartialFolder_DoesNotAddSessionFailure() + { + using var isolation = new BackupRunIsolation(); + string path = CreateRecognizedPartialFolder(isolation.DestinationRoot); + BackupRootRegistry.Remember(isolation.DestinationRoot); + var catalog = new SnapshotEventCatalog(); + var publisher = new BackupCompletionPublisher(catalog); + + publisher.Publish(path, "nightly", RunSummary.Incomplete( + Array.Empty(), RunVerb.Backup, + "Cancellation was requested. No further group was started."), + new DateTime(2026, 8, 9, 9, 0, 0)); + + SnapshotEvent discovered = Assert.Single(catalog.Read()); + Assert.Equal(SnapshotEventKind.Partial, discovered.Kind); + Assert.Equal(Path.GetFullPath(path), discovered.CanonicalPath, + StringComparer.OrdinalIgnoreCase); + } + + private static string CreateRecognizedPartialFolder(string root) + { + string path = Directory.CreateDirectory(Path.Combine(root, "snapshot-started-before-rollover")).FullName; + string manifest = BackupManifest.Compose( + new BackupBase[] { new DMouse() }, Array.Empty(), + new DateTime(2026, 8, 9, 9, 0, 0), "test-machine", "test-user", "test-build", "0.0.0"); + File.WriteAllText(Path.Combine(path, BackupManifest.FileName), manifest); + return path; + } + + [Fact] + public void Publish_NoRetainedRecognizedFolder_RecordsSessionFailureOnly() + { + var catalog = new SnapshotEventCatalog(); + var publisher = new BackupCompletionPublisher(catalog); + var summary = RunSummary.For(Array.Empty(), false, RunVerb.Backup, + "the backup folder could not be created: access denied"); + + string attemptedPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"), + "snapshot-attempted-before-rollover"); + publisher.Publish(attemptedPath, "nightly", summary, new DateTime(2026, 8, 9, 9, 0, 0)); + + SnapshotEvent failure = Assert.Single(catalog.Read().Where(e => e.Kind == SnapshotEventKind.Failed)); + Assert.Equal("nightly", failure.DisplayName); + Assert.Contains("could not be created", failure.DiagnosticReason, StringComparison.OrdinalIgnoreCase); + Assert.False(failure.IsRestorable); + } + ``` + + Keep the current literal expected scopes and type lists in `ViewDataHelperTests`; add assertions that `ScopeGroups.Build()` is directly importable from `WinRestoreKit`, not `Views`. + +- [ ] **Step 2: Run the focused tests to verify they fail before the move** + + Run: + + ```powershell + dotnet test src/WinRestoreKit.Tests/WinRestoreKit.Tests.csproj --filter "FullyQualifiedName~ScopeGroupsPrivacyTests|FullyQualifiedName~ViewDataHelperTests|FullyQualifiedName~BackupPresetsTests|FullyQualifiedName~BackupCompletionPublisherTests" + ``` + + Expected: FAIL because Application `ScopeGroups`, `BackupPresets`, and `BackupCompletionPublisher` do not exist yet. Do not accept a failure caused by a stale `Views` import as evidence that the new behavior is covered. + +- [ ] **Step 3: Move the selection data without changing its membership or defaults** + + Move the implementation from `Views/ScopeGroups.cs` into the Application file, change its namespace to `WinRestoreKit`, make the row/catalog callable by WPF, and construct registrations through the Compare/Confirm `BackupModuleCatalog` facade. Preserve exactly the current six definitions, test-all-modules-once guard, detail truncation, and current default/opt-in calculation: + + ```csharp + public sealed class ScopeGroupRow + { + internal ScopeGroupRow(string name, string detail, bool defaultChecked, + bool requiresExplicitOptIn, string cautionNote, IReadOnlyList modules) + { + Name = name; + Detail = detail; + DefaultChecked = defaultChecked; + RequiresExplicitOptIn = requiresExplicitOptIn; + CautionNote = cautionNote ?? string.Empty; + Modules = modules; + } + + public string Name { get; } + public string Detail { get; } + public bool DefaultChecked { get; } + public bool RequiresExplicitOptIn { get; } + public string CautionNote { get; } + public IReadOnlyList Modules { get; } + } + + public static class ScopeGroups + { + private const int DetailLimit = 96; + private static readonly ScopeDefinition[] Definitions = + { + new("Explorer & shell", IsExplorerAndShell), + new("Power & devices", IsPowerAndDevices), + new("Fonts", static module => module is WFonts), + new("Network profiles", IsNetworkProfile), + new("Environment variables (unfiltered)", static module => module is EEnvironment, + true, static modules => modules.Single().WarningMessage), + new("App settings (AppData)", IsAppSetting) + }; + + public static IReadOnlyList Build() + { + IReadOnlyList registrations = BackupModuleCatalog.CreateAll(); + var modulesByScope = Definitions.ToDictionary( + definition => definition, _ => new List()); + foreach (BackupModuleRegistration registration in registrations) + { + ScopeDefinition match = null; + foreach (ScopeDefinition definition in Definitions) + { + if (!definition.Includes(registration.Module)) + continue; + if (match != null) + throw new InvalidOperationException( + $"Module '{registration.Module.GetType().FullName}' belongs to multiple backup scopes."); + match = definition; + } + if (match == null) + throw new InvalidOperationException( + $"Module '{registration.Module.GetType().FullName}' has no backup scope."); + modulesByScope[match].Add(registration.Module); + } + return Definitions.Select(definition => + { + IReadOnlyList modules = modulesByScope[definition]; + return new ScopeGroupRow(definition.Name, + modules.Count == 0 ? "No supported items detected" : Truncate(modules[0].Info), + !definition.RequiresExplicitOptIn && modules.Any(module => module.IsInstalled()), + definition.RequiresExplicitOptIn, + definition.CautionNoteFactory?.Invoke(modules) ?? string.Empty, modules); + }).ToList(); + } + + private static bool IsExplorerAndShell(BackupBase module) => + module is WPersonalization or WVisualEffects or WTaskbar or WThemes or APinnedApps; + private static bool IsPowerAndDevices(BackupBase module) => + module is WPowerPlans or DPrinters or DMouse or DKeyboard or DTouchpad; + private static bool IsNetworkProfile(BackupBase module) => + module is WNetworkConf or WMappedDrives or CWiFiConf or EHosts; + private static bool IsAppSetting(BackupBase module) => + module is WPrivacy or WAPrivacy or WTelemetry or WUpdates or WAccessibility or WRegional + or WOther or AppStoreApps or GGaming or ETerminal or EVSCode or ESsh + or EEnvironmentFiltered; + private static string Truncate(string value) + { + string singleLine = string.Join(" ", (value ?? string.Empty).Split( + (char[])null, StringSplitOptions.RemoveEmptyEntries)); + return singleLine.Length <= DetailLimit ? singleLine : + singleLine.Substring(0, DetailLimit - 3).TrimEnd() + "..."; + } + + private sealed class ScopeDefinition + { + internal ScopeDefinition(string name, Func includes, + bool requiresExplicitOptIn = false, + Func, string> cautionNoteFactory = null) + { + Name = name; Includes = includes; RequiresExplicitOptIn = requiresExplicitOptIn; + CautionNoteFactory = cautionNoteFactory; + } + internal string Name { get; } + internal Func Includes { get; } + internal bool RequiresExplicitOptIn { get; } + internal Func, string> CautionNoteFactory { get; } + } + } + ``` + + `BackupPresets.cs` must keep these exact values and no UI dependency: + + ```csharp + public static readonly IReadOnlyList DeveloperMachine = + new[] { "ETerminal", "EVSCode", "ESsh", "EEnvironment", "EHosts" }; + public static readonly IReadOnlyList MinimalPrivacySafeExclusions = + new[] { "WUpdates", "EEnvironment", "EEnvironmentFiltered", "CWiFiConf" }; + ``` + + Update `BackupPageView` to import these Application types and remove the old `Views` definitions; retain its form behavior so WinForms remains runnable. Do not leave an Application-to-WinForms forwarding shim. + +- [ ] **Step 4: Implement discovery-first completion publication** + + Create a small Application-only publisher. It does not create files, write manifests, prune folders, or decide cleanup; those choices remain in `BackupRestoreOrchestrator`. + + ```csharp + internal sealed class BackupCompletionPublisher + { + private readonly SnapshotEventCatalog catalog; + + internal BackupCompletionPublisher(SnapshotEventCatalog catalog) + => this.catalog = catalog ?? throw new ArgumentNullException(nameof(catalog)); + + internal void Publish(string attemptedBackupPath, string displayName, + RunSummary summary, DateTime created) + { + string canonicalPath = TryCanonicalize(attemptedBackupPath); + bool retained = canonicalPath != null && catalog.Read().Any(snapshot => + string.Equals(snapshot.CanonicalPath, canonicalPath, + StringComparison.OrdinalIgnoreCase)); + + if (retained) + return; + + string diagnostic = summary?.Detail; + if (string.IsNullOrWhiteSpace(diagnostic)) + diagnostic = "The snapshot run ended without a retained recognizable result."; + + catalog.RecordSessionFailure(created, displayName ?? string.Empty, diagnostic); + } + + private static string TryCanonicalize(string path) + { + try { return Path.GetFullPath(path); } + catch (Exception) { return null; } + } + } + ``` + + The publisher intentionally permits a session event even if a completed summary cannot be reconciled to a recognizable folder: the UI reports the observable discrepancy rather than inventing a verified snapshot. It intentionally does *not* publish where `Read()` finds the exact attempted canonical path as `Verified`, `Partial`, `Failed`, or `Unreadable`. It never computes a folder name or calls `Data.NowShort`; cleanup remains solely the runner's existing responsibility. + +- [ ] **Step 5: Run the focused tests to verify the shared contracts pass** + + Run the Step 2 command again. + + Expected: PASS. The six scopes retain their exact order and module assignment; unfiltered environment variables remain unchecked with their source warning; preset names still resolve; retained partial output yields exactly its discovered event; missing output yields one non-restorable current-session failure without filesystem retention. + +- [ ] **Step 6: Commit the shared selection and completion layer** + + ```powershell + git add src/WinRestoreKit.Application/Backup/ScopeGroups.cs src/WinRestoreKit.Application/Backup/BackupPresets.cs src/WinRestoreKit.Application/Backup/BackupCompletionPublisher.cs src/WinRestoreKit/Views/BackupPageView.cs src/WinRestoreKit.Tests/ScopeGroupsPrivacyTests.cs src/WinRestoreKit.Tests/ViewDataHelperTests.cs src/WinRestoreKit.Tests/BackupPresetsTests.cs src/WinRestoreKit.Tests/BackupCompletionPublisherTests.cs + git commit -m "feat: share backup selection and completion events" + ``` + +### Task 2: Build the accessible WPF Create Snapshot selection workspace + +**Files:** +- Create: `src/WinRestoreKit.Wpf/ViewModels/BackupRunRequest.cs` +- Create: `src/WinRestoreKit.Wpf/ViewModels/BackupScopeItemViewModel.cs` +- Create: `src/WinRestoreKit.Wpf/ViewModels/BackupWorkspaceViewModel.cs` +- Create: `src/WinRestoreKit.Wpf/Infrastructure/AsyncDelegateCommand.cs` +- Create: `src/WinRestoreKit.Wpf/Views/BackupWorkspaceView.xaml` +- Create: `src/WinRestoreKit.Wpf/Views/BackupWorkspaceView.xaml.cs` +- Modify: `src/WinRestoreKit.Wpf/WinRestoreKit.Wpf.csproj` +- Modify: `src/WinRestoreKit.Wpf/MainWindow.xaml` +- Create: `src/WinRestoreKit.Tests/BackupWorkspaceViewModelTests.cs` +- Create: `src/WinRestoreKit.Tests/BackupWorkspaceViewTests.cs` + +**Interfaces:** +- Consumes: Task 1's `ScopeGroups.Build()` and `BackupPresets`; Foundation `ObservableObject`, `DelegateCommand`, and `RunCoordinator`; Compare/Confirm `BackupModuleCatalog`; `SnapshotCompression` from Core; and Foundation `WpfTestHost`. +- Produces: `internal sealed class BackupRunRequest` (`IReadOnlyList Modules`, `string SnapshotName`, `SnapshotCompression Compression`, `string Destination`); `internal sealed class BackupWorkspaceViewModel`; and `internal Task StartAsync()` that invokes its supplied `Func` only after local selection/destination validation. + +- [ ] **Step 1: Write failing VM and STA view tests** + + Reuse Foundation's `WpfTestHost.Run(Action)` for each test that constructs a WPF control or window. It supplies the STA thread, pumps its dispatcher, propagates action failures, and shuts the dispatcher down; do not create a second helper. + + Test observable selection behavior, not XAML implementation details: + + ```csharp + [Fact] + public async Task StartAsync_SelectedScope_RequestsItsConcreteModulesAndFastCompression() + { + BackupRunRequest request = null; + var vm = new BackupWorkspaceViewModel(r => { request = r; return Task.CompletedTask; }, @"C:\snapshots"); + foreach (BackupScopeItemViewModel scope in vm.Scopes) scope.IsSelected = false; + vm.Scopes.Single(s => s.Name == "Explorer & shell").IsSelected = true; + + await vm.StartAsync(); + + Assert.Equal(SnapshotCompression.Fast, request.Compression); + Assert.Equal(new[] { typeof(WPersonalization), typeof(WVisualEffects), typeof(WTaskbar), + typeof(WThemes), typeof(APinnedApps) }, request.Modules.Select(m => m.GetType())); + } + + [Fact] + public async Task StartAsync_EmptySelectionOrDestination_ShowsValidationAndDoesNotRequestRun() + { + int starts = 0; + var vm = new BackupWorkspaceViewModel(_ => { starts++; return Task.CompletedTask; }, ""); + foreach (BackupScopeItemViewModel scope in vm.Scopes) scope.IsSelected = false; + + await vm.StartAsync(); + + Assert.Equal(0, starts); + Assert.Equal("Select at least one scope with supported items.", vm.ValidationMessage); + } + + [Fact] + public void View_ExposesScopeWarningsAndLabeledCompressionChoices() + { + WpfTestHost.Run(() => + { + var view = new BackupWorkspaceView { DataContext = new BackupWorkspaceViewModel(_ => Task.CompletedTask, @"C:\snapshots") }; + Assert.NotNull(view.FindName("CreateSnapshotButton")); + Assert.NotNull(view.FindName("CompressionComboBox")); + }); + } + ``` + + Add cases that pin: the unfiltered environment scope begins unselected and exposes `CautionNote`; blank destination produces the exact destination message once there is a scope; presets use the Task 1 literal lists; `None`, `Fast`, and `Max` map directly to their enum values; module flattening is `Distinct()` while preserving catalog order. + +- [ ] **Step 2: Run the new selection tests to verify they fail** + + Run: + + ```powershell + dotnet test src/WinRestoreKit.Tests/WinRestoreKit.Tests.csproj --filter "FullyQualifiedName~BackupWorkspaceViewModelTests|FullyQualifiedName~BackupWorkspaceViewTests" + ``` + + Expected: FAIL at compile time because the WPF workspace and request do not exist. `WpfTestHost` is already supplied by Foundation and must not be recreated. + +- [ ] **Step 3: Implement the selection VM with no backup execution policy** + + Keep request composition in the VM, but leave name safety, containment, destination creation, ownership, archiving, manifest writing, and custom-root persistence exclusively in `BackupRestoreOrchestrator`. + + ```csharp + internal sealed class BackupWorkspaceViewModel : ObservableObject + { + private readonly Func startRunAsync; + + internal BackupWorkspaceViewModel(Func startRunAsync, string defaultDestination) + { + this.startRunAsync = startRunAsync ?? throw new ArgumentNullException(nameof(startRunAsync)); + Destination = defaultDestination ?? string.Empty; + Compression = SnapshotCompression.Fast; + Scopes = new ObservableCollection( + ScopeGroups.Build().Select(scope => new BackupScopeItemViewModel(scope))); + StartCommand = new AsyncDelegateCommand(StartAsync, ex => + { + ValidationMessage = ex.Message; + OnPropertyChanged(nameof(ValidationMessage)); + }); + } + + public ObservableCollection Scopes { get; } + public IReadOnlyList CompressionOptions { get; } = + new[] { SnapshotCompression.None, SnapshotCompression.Fast, SnapshotCompression.Max }; + public string SnapshotName { get; set; } = string.Empty; + public string Destination { get; set; } + public SnapshotCompression Compression { get; set; } + public string ValidationMessage { get; private set; } + public ICommand StartCommand { get; } + + internal async Task StartAsync() + { + IReadOnlyList modules = Scopes.Where(scope => scope.IsSelected) + .SelectMany(scope => scope.Modules).Distinct().ToArray(); + if (modules.Count == 0) + { + ValidationMessage = "Select at least one scope with supported items."; + OnPropertyChanged(nameof(ValidationMessage)); + return; + } + if (string.IsNullOrWhiteSpace(Destination)) + { + ValidationMessage = "Choose a destination folder before capturing."; + OnPropertyChanged(nameof(ValidationMessage)); + return; + } + + ValidationMessage = null; + OnPropertyChanged(nameof(ValidationMessage)); + await startRunAsync(new BackupRunRequest(modules, SnapshotName?.Trim() ?? string.Empty, + Compression, Destination.Trim())); + } + } + ``` + If Foundation's `DelegateCommand` cannot await tasks, implement the following WPF-infrastructure command. Its `async void` method is the required `ICommand.Execute` boundary; it catches every task fault and is not a view click handler: + + ```csharp + internal sealed class AsyncDelegateCommand : ICommand + { + private readonly Func executeAsync; + private readonly Action reportFailure; + private bool executing; + + internal AsyncDelegateCommand(Func executeAsync, Action reportFailure = null) + { + this.executeAsync = executeAsync ?? throw new ArgumentNullException(nameof(executeAsync)); + this.reportFailure = reportFailure; + } + + public event EventHandler CanExecuteChanged; + public bool CanExecute(object parameter) => !executing; + + public async void Execute(object parameter) + { + if (executing) return; + executing = true; + CanExecuteChanged?.Invoke(this, EventArgs.Empty); + try { await executeAsync(); } + catch (Exception ex) { reportFailure?.Invoke(ex); } + finally + { + executing = false; + CanExecuteChanged?.Invoke(this, EventArgs.Empty); + } + } + } + ``` + + Keep this command internal to WPF infrastructure; do not use an `async void` view click handler. + `BackupScopeItemViewModel` carries `Name`, `Detail`, `CautionNote`, computed `HasCaution`, `RequiresExplicitOptIn`, `IReadOnlyList Modules`, and mutable `IsSelected` initialized from `DefaultChecked`. Implement commands for Select all, Clear, Developer machine, and Minimal privacy-safe by transforming the existing type-name lists through one tested `SelectModulesByTypeName`/exclusion routine; do not hard-code module type names in WPF. + + +- [ ] **Step 4: Implement XAML for keyboard and automation parity** + + Create a content view with the Foundation resource dictionaries and a named, labeled destination browser. Use `Microsoft.Win32.OpenFolderDialog` in code-behind only to set `Destination`; the binding and view model retain all selection state. + + ```xml + + + + + + + + + + + + + + + + + + + + + + + + + +