From 97c887baea0b0f403e4f4d19df020388b302f686 Mon Sep 17 00:00:00 2001 From: nicolasestrem <103498114+nicolasestrem@users.noreply.github.com> Date: Tue, 11 Aug 2026 14:24:58 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20WPF=20cutover=20=E2=80=94=20sole=20ship?= =?UTF-8?q?ping=20app=20is=20WinRestoreKit=20(0.1.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Completes Stage 5 of the Timeline+Compare WPF migration: the WPF shell becomes the only shipping WinRestoreKit application, all WinForms is removed, and the release artifact is one self-contained WinRestoreKit.exe. Cutover (no compatibility shims): - src/WinRestoreKit is now the WPF app (AssemblyName WinRestoreKit, UseWPF, GenerateAssemblyInfo=false). Shell keeps its WinRestoreKit.Wpf C# namespace; only the assembly/exe identity is WinRestoreKit. - Moved App/MainWindow/Views/ViewModels/Services/Themes/Resources/ Navigation/Infrastructure from src/WinRestoreKit.Wpf into src/WinRestoreKit. - Deleted the entire WinForms shell: MainForm, Program, Views/, Forms/, Controls/, WinForms Helpers/, Resources.*, and WinForms-only fonts. Preserved the load-bearing shared assets at their paths: app.manifest, WinRestoreKit.ico, Properties/AssemblyInfo.cs, IBMPlexMono-Regular.ttf. - Removed the src/WinRestoreKit.Wpf project; solution now has four projects (WinRestoreKit WPF, Core, Application, Tests). - Repointed the five /WinRestoreKit.Wpf;component/ pack URIs to /WinRestoreKit;component/ (themes, templates). - Dropped redundant InternalsVisibleTo("WinRestoreKit.Wpf") from Core and Application (the renamed app is covered by the existing "WinRestoreKit"). Safety net restored for the new shell: - Relocated DescribeStartupFailure to the neutral Application layer (StartupDiagnostics). App.OnStartup now wraps shell construction in a try/catch that shows the diagnostic and rethrows, preserving the Phase 2c guarantee that a construction-time failure is a visible message, not a silent WER tear-down. Test triage (985 pass, 0 fail): - Deleted WinForms-only tests (MainForm/ProgressPageView/BackupPageView/ HomePageView/HistoryPageView/FontLoader/NavigationService/ShellLayout/ RestoreDialogOwner); their contracts are covered by the WPF STA tests. - Deleted CompressedDriftPayloadTests + HomePageViewDriftTests (reflected the deleted HomePageView.DetectDrift); compressed-payload drift + scope disposal is covered by SnapshotPayloadPreparationServiceTests, SnapshotComparisonServiceTests, and BackupPayloadTests. - Ported LogHelperTests to a neutral ILogSink capture (drops the RichTextBox dependency; keeps the load-bearing format-string-hazard coverage). - Repointed typeof(MainForm/Program).Assembly anchors to typeof(WinRestoreKit.Wpf.App).Assembly; removed one WinForms close-rule fact and two stale `using Views;` imports. Version + docs: - Bumped AssemblyVersion/AssemblyFileVersion to 0.1.0 (three-part) in the canonical AssemblyInfo.cs; updated RebrandIdentity expected version. - CLAUDE.md updated to describe the WPF app, the four-project layout, and the relocated seams/startup guard. Verified: Release build 0 warnings/0 errors; 985/985 tests pass; published artifact is a single 68.8 MB self-contained WinRestoreKit.exe that launches and renders the WPF shell (Timeline, navigation, dark theme) on a real Windows desktop. --- CHANGELOG.md | 20 + CLAUDE.md | 48 +- .../Properties/AssemblyInfo.cs | 1 - .../StartupDiagnostics.cs | 33 + .../WinRestoreKit.Core.csproj | 1 - .../AppRestoreDialogTests.cs | 10 - .../BackupDestinationLifecycleTests.cs | 1 - .../BackupPageViewTests.cs | 61 -- .../CompressedDriftPayloadTests.cs | 92 -- src/WinRestoreKit.Tests/FontLoaderTests.cs | 44 - .../HistoryPageViewTests.cs | 42 - .../HomePageViewBaselineTests.cs | 146 ---- .../HomePageViewDriftTests.cs | 109 --- src/WinRestoreKit.Tests/LogHelperTests.cs | 68 +- .../MainFormRunAdmissionTests.cs | 136 --- .../NavigationServiceTests.cs | 31 - .../ProgressPageViewTests.cs | 134 --- .../RebrandIdentityTests.cs | 4 +- .../RestoreDeclarationTests.cs | 2 +- .../RestoreDialogOwnerTests.cs | 64 -- src/WinRestoreKit.Tests/ShellLayoutTests.cs | 44 - .../SnapshotFolderPathTests.cs | 1 - .../VersionParsingTests.cs | 16 +- .../ViewDataHelperTests.cs | 18 - .../WinRestoreKit.Tests.csproj | 9 +- src/WinRestoreKit.Wpf/App.xaml.cs | 39 - .../Properties/AssemblyInfo.cs | 3 - .../WinRestoreKit.Wpf.csproj | 32 - src/WinRestoreKit.sln | 22 - .../App.xaml | 0 src/WinRestoreKit/App.xaml.cs | 53 ++ src/WinRestoreKit/Controls/BlueprintFrame.cs | 69 -- src/WinRestoreKit/Controls/CustomCheckbox.cs | 139 --- src/WinRestoreKit/Controls/KeyedMark.cs | 63 -- src/WinRestoreKit/Controls/NavButton.cs | 339 -------- .../Controls/SegmentedControl.cs | 141 ---- src/WinRestoreKit/Controls/TagChip.cs | 97 --- src/WinRestoreKit/Fonts/Barlow-Medium.otf | Bin 135264 -> 0 bytes src/WinRestoreKit/Fonts/Barlow-Regular.otf | Bin 136284 -> 0 bytes src/WinRestoreKit/Fonts/Barlow-SemiBold.otf | Bin 135084 -> 0 bytes .../Fonts/BarlowCondensed-Bold.otf | Bin 144380 -> 0 bytes .../Fonts/BarlowCondensed-Regular.otf | Bin 140412 -> 0 bytes .../Fonts/BarlowCondensed-SemiBold.otf | Bin 144004 -> 0 bytes .../Fonts/IBMPlexMono-Medium.ttf | Bin 156996 -> 0 bytes .../Forms/RestAppsForm.Designer.cs | 147 ---- src/WinRestoreKit/Forms/RestAppsForm.cs | 209 ----- src/WinRestoreKit/Forms/RestAppsForm.resx | 120 --- .../Forms/RestoreConfirmForm.Designer.cs | 187 ----- src/WinRestoreKit/Forms/RestoreConfirmForm.cs | 124 --- .../Forms/RestoreConfirmForm.resx | 120 --- src/WinRestoreKit/GitHub.cs | 62 -- src/WinRestoreKit/GitHubIcon.png | Bin 1160 -> 0 bytes src/WinRestoreKit/Helpers/FontLoader.cs | 74 -- .../Helpers/NavigationService.cs | 108 --- src/WinRestoreKit/Helpers/ProgressLogSink.cs | 47 -- .../Helpers/RichTextBoxLogSink.cs | 62 -- src/WinRestoreKit/Helpers/Theme.cs | 465 ---------- src/WinRestoreKit/Helpers/Ui.cs | 83 -- .../Helpers/WinFormsUpdatePresenter.cs | 76 -- .../Infrastructure/AsyncDelegateCommand.cs | 0 .../BooleanToOffscreenBehaviorConverter.cs | 0 .../Infrastructure/DelegateCommand.cs | 0 .../Infrastructure/ObservableObject.cs | 0 src/WinRestoreKit/MainForm.Designer.cs | 346 -------- src/WinRestoreKit/MainForm.cs | 389 --------- src/WinRestoreKit/MainForm.resx | 120 --- .../MainWindow.xaml | 0 .../MainWindow.xaml.cs | 0 .../Navigation/CompareWorkflowNavigator.cs | 0 .../Navigation/ITimelineNavigator.cs | 0 src/WinRestoreKit/Program.cs | 127 --- src/WinRestoreKit/Properties/AssemblyInfo.cs | 4 +- .../Properties/Resources.Designer.cs | 63 -- src/WinRestoreKit/Properties/Resources.resx | 117 --- .../Resources/SnapshotEventTemplates.xaml | 0 .../Services/CompareDialogService.cs | 0 .../Services/ExternalLinkService.cs | 0 .../Services/ICompareDialogService.cs | 0 .../Services/IExternalLinkService.cs | 0 .../Services/IRunDialogService.cs | 0 .../Services/IRunPresentation.cs | 0 .../Services/ISystemThemeDetector.cs | 0 .../Services/IThemeService.cs | 0 .../Services/IWpfDialogService.cs | 0 .../Services/RestoreRunDialogService.cs | 0 .../Services/WindowsThemeDetector.cs | 0 .../Services/WpfAppRestoreDialog.cs | 0 .../Services/WpfDialogService.cs | 0 .../Services/WpfDispatcher.cs | 0 .../Services/WpfLogSink.cs | 0 .../Services/WpfRunUi.cs | 0 .../Services/WpfThemeService.cs | 4 +- .../Services/WpfUpdatePresenter.cs | 0 .../Themes/Controls.xaml | 0 .../Themes/Dark.xaml | 0 .../Themes/Light.xaml | 0 .../ViewModels/AboutViewModel.cs | 0 .../ViewModels/AppRestoreDialogViewModel.cs | 0 .../ViewModels/BackupRunCompletion.cs | 0 .../ViewModels/BackupRunRequest.cs | 0 .../ViewModels/BackupScopeItemViewModel.cs | 0 .../ViewModels/BackupWorkspaceViewModel.cs | 0 .../ViewModels/ComparisonFilter.cs | 0 .../ComparisonWorkspaceViewModel.cs | 0 .../ViewModels/ConfirmViewModel.cs | 0 .../History/AdvancedHistoryViewModel.cs | 0 .../ModuleComparisonRowViewModel.cs | 0 .../ViewModels/ModuleImpactViewModel.cs | 0 .../ViewModels/ProgressWorkspaceViewModel.cs | 0 .../ViewModels/RestoreSetViewModel.cs | 0 .../ViewModels/ResultWorkspaceViewModel.cs | 0 .../ViewModels/SettingsViewModel.cs | 0 .../ViewModels/ShellViewModel.cs | 0 .../Snapshots/SnapshotEventStatus.cs | 0 .../Snapshots/SnapshotEventViewModel.cs | 0 .../ViewModels/Timeline/TimelineViewModel.cs | 0 .../Views/AboutPageView.Designer.cs | 6 - src/WinRestoreKit/Views/AboutPageView.cs | 469 ----------- src/WinRestoreKit/Views/AboutPageView.resx | 141 ---- .../Views/AboutView.xaml | 0 .../Views/AboutView.xaml.cs | 0 .../Views/AdvancedHistoryView.xaml | 2 +- .../Views/AdvancedHistoryView.xaml.cs | 0 .../Views/AppRestoreDialog.xaml | 0 .../Views/AppRestoreDialog.xaml.cs | 0 src/WinRestoreKit/Views/BackupPageView.cs | 496 ----------- src/WinRestoreKit/Views/BackupPageView.resx | 126 --- .../Views/BackupWorkspaceView.xaml | 0 .../Views/BackupWorkspaceView.xaml.cs | 0 .../Views/ComparisonWorkspaceView.xaml | 0 .../Views/ComparisonWorkspaceView.xaml.cs | 0 .../Views/ConfirmView.xaml | 0 .../Views/ConfirmView.xaml.cs | 0 .../Views/Controls/SnapshotEventList.xaml | 2 +- .../Views/Controls/SnapshotEventList.xaml.cs | 0 .../Views/Dialogs/RestoreConsentDialog.xaml | 0 .../Dialogs/RestoreConsentDialog.xaml.cs | 0 src/WinRestoreKit/Views/HistoryPageView.cs | 748 ----------------- src/WinRestoreKit/Views/HomePageView.cs | 599 ------------- src/WinRestoreKit/Views/IRefreshableView.cs | 37 - src/WinRestoreKit/Views/ProgressPageView.cs | 792 ------------------ .../Views/ProgressWorkspaceView.xaml | 0 .../Views/ProgressWorkspaceView.xaml.cs | 0 .../Views/RestoreWizardStep1View.cs | 411 --------- .../Views/RestoreWizardStep2View.cs | 447 ---------- .../Views/ResultWorkspaceView.xaml | 0 .../Views/ResultWorkspaceView.xaml.cs | 0 .../Views/SettingsView.xaml | 0 .../Views/SettingsView.xaml.cs | 0 .../Views/TimelineView.xaml | 2 +- .../Views/TimelineView.xaml.cs | 0 src/WinRestoreKit/Views/WatchedGroups.cs | 32 - src/WinRestoreKit/WinRestoreKit.csproj | 51 +- 153 files changed, 220 insertions(+), 9327 deletions(-) create mode 100644 src/WinRestoreKit.Application/StartupDiagnostics.cs delete mode 100644 src/WinRestoreKit.Tests/BackupPageViewTests.cs delete mode 100644 src/WinRestoreKit.Tests/CompressedDriftPayloadTests.cs delete mode 100644 src/WinRestoreKit.Tests/FontLoaderTests.cs delete mode 100644 src/WinRestoreKit.Tests/HistoryPageViewTests.cs delete mode 100644 src/WinRestoreKit.Tests/HomePageViewBaselineTests.cs delete mode 100644 src/WinRestoreKit.Tests/HomePageViewDriftTests.cs delete mode 100644 src/WinRestoreKit.Tests/MainFormRunAdmissionTests.cs delete mode 100644 src/WinRestoreKit.Tests/NavigationServiceTests.cs delete mode 100644 src/WinRestoreKit.Tests/ProgressPageViewTests.cs delete mode 100644 src/WinRestoreKit.Tests/RestoreDialogOwnerTests.cs delete mode 100644 src/WinRestoreKit.Tests/ShellLayoutTests.cs delete mode 100644 src/WinRestoreKit.Wpf/App.xaml.cs delete mode 100644 src/WinRestoreKit.Wpf/Properties/AssemblyInfo.cs delete mode 100644 src/WinRestoreKit.Wpf/WinRestoreKit.Wpf.csproj rename src/{WinRestoreKit.Wpf => WinRestoreKit}/App.xaml (100%) create mode 100644 src/WinRestoreKit/App.xaml.cs delete mode 100644 src/WinRestoreKit/Controls/BlueprintFrame.cs delete mode 100644 src/WinRestoreKit/Controls/CustomCheckbox.cs delete mode 100644 src/WinRestoreKit/Controls/KeyedMark.cs delete mode 100644 src/WinRestoreKit/Controls/NavButton.cs delete mode 100644 src/WinRestoreKit/Controls/SegmentedControl.cs delete mode 100644 src/WinRestoreKit/Controls/TagChip.cs delete mode 100644 src/WinRestoreKit/Fonts/Barlow-Medium.otf delete mode 100644 src/WinRestoreKit/Fonts/Barlow-Regular.otf delete mode 100644 src/WinRestoreKit/Fonts/Barlow-SemiBold.otf delete mode 100644 src/WinRestoreKit/Fonts/BarlowCondensed-Bold.otf delete mode 100644 src/WinRestoreKit/Fonts/BarlowCondensed-Regular.otf delete mode 100644 src/WinRestoreKit/Fonts/BarlowCondensed-SemiBold.otf delete mode 100644 src/WinRestoreKit/Fonts/IBMPlexMono-Medium.ttf delete mode 100644 src/WinRestoreKit/Forms/RestAppsForm.Designer.cs delete mode 100644 src/WinRestoreKit/Forms/RestAppsForm.cs delete mode 100644 src/WinRestoreKit/Forms/RestAppsForm.resx delete mode 100644 src/WinRestoreKit/Forms/RestoreConfirmForm.Designer.cs delete mode 100644 src/WinRestoreKit/Forms/RestoreConfirmForm.cs delete mode 100644 src/WinRestoreKit/Forms/RestoreConfirmForm.resx delete mode 100644 src/WinRestoreKit/GitHub.cs delete mode 100644 src/WinRestoreKit/GitHubIcon.png delete mode 100644 src/WinRestoreKit/Helpers/FontLoader.cs delete mode 100644 src/WinRestoreKit/Helpers/NavigationService.cs delete mode 100644 src/WinRestoreKit/Helpers/ProgressLogSink.cs delete mode 100644 src/WinRestoreKit/Helpers/RichTextBoxLogSink.cs delete mode 100644 src/WinRestoreKit/Helpers/Theme.cs delete mode 100644 src/WinRestoreKit/Helpers/Ui.cs delete mode 100644 src/WinRestoreKit/Helpers/WinFormsUpdatePresenter.cs rename src/{WinRestoreKit.Wpf => WinRestoreKit}/Infrastructure/AsyncDelegateCommand.cs (100%) rename src/{WinRestoreKit.Wpf => WinRestoreKit}/Infrastructure/BooleanToOffscreenBehaviorConverter.cs (100%) rename src/{WinRestoreKit.Wpf => WinRestoreKit}/Infrastructure/DelegateCommand.cs (100%) rename src/{WinRestoreKit.Wpf => WinRestoreKit}/Infrastructure/ObservableObject.cs (100%) delete mode 100644 src/WinRestoreKit/MainForm.Designer.cs delete mode 100644 src/WinRestoreKit/MainForm.cs delete mode 100644 src/WinRestoreKit/MainForm.resx rename src/{WinRestoreKit.Wpf => WinRestoreKit}/MainWindow.xaml (100%) rename src/{WinRestoreKit.Wpf => WinRestoreKit}/MainWindow.xaml.cs (100%) rename src/{WinRestoreKit.Wpf => WinRestoreKit}/Navigation/CompareWorkflowNavigator.cs (100%) rename src/{WinRestoreKit.Wpf => WinRestoreKit}/Navigation/ITimelineNavigator.cs (100%) delete mode 100644 src/WinRestoreKit/Program.cs delete mode 100644 src/WinRestoreKit/Properties/Resources.Designer.cs delete mode 100644 src/WinRestoreKit/Properties/Resources.resx rename src/{WinRestoreKit.Wpf => WinRestoreKit}/Resources/SnapshotEventTemplates.xaml (100%) rename src/{WinRestoreKit.Wpf => WinRestoreKit}/Services/CompareDialogService.cs (100%) rename src/{WinRestoreKit.Wpf => WinRestoreKit}/Services/ExternalLinkService.cs (100%) rename src/{WinRestoreKit.Wpf => WinRestoreKit}/Services/ICompareDialogService.cs (100%) rename src/{WinRestoreKit.Wpf => WinRestoreKit}/Services/IExternalLinkService.cs (100%) rename src/{WinRestoreKit.Wpf => WinRestoreKit}/Services/IRunDialogService.cs (100%) rename src/{WinRestoreKit.Wpf => WinRestoreKit}/Services/IRunPresentation.cs (100%) rename src/{WinRestoreKit.Wpf => WinRestoreKit}/Services/ISystemThemeDetector.cs (100%) rename src/{WinRestoreKit.Wpf => WinRestoreKit}/Services/IThemeService.cs (100%) rename src/{WinRestoreKit.Wpf => WinRestoreKit}/Services/IWpfDialogService.cs (100%) rename src/{WinRestoreKit.Wpf => WinRestoreKit}/Services/RestoreRunDialogService.cs (100%) rename src/{WinRestoreKit.Wpf => WinRestoreKit}/Services/WindowsThemeDetector.cs (100%) rename src/{WinRestoreKit.Wpf => WinRestoreKit}/Services/WpfAppRestoreDialog.cs (100%) rename src/{WinRestoreKit.Wpf => WinRestoreKit}/Services/WpfDialogService.cs (100%) rename src/{WinRestoreKit.Wpf => WinRestoreKit}/Services/WpfDispatcher.cs (100%) rename src/{WinRestoreKit.Wpf => WinRestoreKit}/Services/WpfLogSink.cs (100%) rename src/{WinRestoreKit.Wpf => WinRestoreKit}/Services/WpfRunUi.cs (100%) rename src/{WinRestoreKit.Wpf => WinRestoreKit}/Services/WpfThemeService.cs (96%) rename src/{WinRestoreKit.Wpf => WinRestoreKit}/Services/WpfUpdatePresenter.cs (100%) rename src/{WinRestoreKit.Wpf => WinRestoreKit}/Themes/Controls.xaml (100%) rename src/{WinRestoreKit.Wpf => WinRestoreKit}/Themes/Dark.xaml (100%) rename src/{WinRestoreKit.Wpf => WinRestoreKit}/Themes/Light.xaml (100%) rename src/{WinRestoreKit.Wpf => WinRestoreKit}/ViewModels/AboutViewModel.cs (100%) rename src/{WinRestoreKit.Wpf => WinRestoreKit}/ViewModels/AppRestoreDialogViewModel.cs (100%) rename src/{WinRestoreKit.Wpf => WinRestoreKit}/ViewModels/BackupRunCompletion.cs (100%) rename src/{WinRestoreKit.Wpf => WinRestoreKit}/ViewModels/BackupRunRequest.cs (100%) rename src/{WinRestoreKit.Wpf => WinRestoreKit}/ViewModels/BackupScopeItemViewModel.cs (100%) rename src/{WinRestoreKit.Wpf => WinRestoreKit}/ViewModels/BackupWorkspaceViewModel.cs (100%) rename src/{WinRestoreKit.Wpf => WinRestoreKit}/ViewModels/ComparisonFilter.cs (100%) rename src/{WinRestoreKit.Wpf => WinRestoreKit}/ViewModels/ComparisonWorkspaceViewModel.cs (100%) rename src/{WinRestoreKit.Wpf => WinRestoreKit}/ViewModels/ConfirmViewModel.cs (100%) rename src/{WinRestoreKit.Wpf => WinRestoreKit}/ViewModels/History/AdvancedHistoryViewModel.cs (100%) rename src/{WinRestoreKit.Wpf => WinRestoreKit}/ViewModels/ModuleComparisonRowViewModel.cs (100%) rename src/{WinRestoreKit.Wpf => WinRestoreKit}/ViewModels/ModuleImpactViewModel.cs (100%) rename src/{WinRestoreKit.Wpf => WinRestoreKit}/ViewModels/ProgressWorkspaceViewModel.cs (100%) rename src/{WinRestoreKit.Wpf => WinRestoreKit}/ViewModels/RestoreSetViewModel.cs (100%) rename src/{WinRestoreKit.Wpf => WinRestoreKit}/ViewModels/ResultWorkspaceViewModel.cs (100%) rename src/{WinRestoreKit.Wpf => WinRestoreKit}/ViewModels/SettingsViewModel.cs (100%) rename src/{WinRestoreKit.Wpf => WinRestoreKit}/ViewModels/ShellViewModel.cs (100%) rename src/{WinRestoreKit.Wpf => WinRestoreKit}/ViewModels/Snapshots/SnapshotEventStatus.cs (100%) rename src/{WinRestoreKit.Wpf => WinRestoreKit}/ViewModels/Snapshots/SnapshotEventViewModel.cs (100%) rename src/{WinRestoreKit.Wpf => WinRestoreKit}/ViewModels/Timeline/TimelineViewModel.cs (100%) delete mode 100644 src/WinRestoreKit/Views/AboutPageView.Designer.cs delete mode 100644 src/WinRestoreKit/Views/AboutPageView.cs delete mode 100644 src/WinRestoreKit/Views/AboutPageView.resx rename src/{WinRestoreKit.Wpf => WinRestoreKit}/Views/AboutView.xaml (100%) rename src/{WinRestoreKit.Wpf => WinRestoreKit}/Views/AboutView.xaml.cs (100%) rename src/{WinRestoreKit.Wpf => WinRestoreKit}/Views/AdvancedHistoryView.xaml (98%) rename src/{WinRestoreKit.Wpf => WinRestoreKit}/Views/AdvancedHistoryView.xaml.cs (100%) rename src/{WinRestoreKit.Wpf => WinRestoreKit}/Views/AppRestoreDialog.xaml (100%) rename src/{WinRestoreKit.Wpf => WinRestoreKit}/Views/AppRestoreDialog.xaml.cs (100%) delete mode 100644 src/WinRestoreKit/Views/BackupPageView.cs delete mode 100644 src/WinRestoreKit/Views/BackupPageView.resx rename src/{WinRestoreKit.Wpf => WinRestoreKit}/Views/BackupWorkspaceView.xaml (100%) rename src/{WinRestoreKit.Wpf => WinRestoreKit}/Views/BackupWorkspaceView.xaml.cs (100%) rename src/{WinRestoreKit.Wpf => WinRestoreKit}/Views/ComparisonWorkspaceView.xaml (100%) rename src/{WinRestoreKit.Wpf => WinRestoreKit}/Views/ComparisonWorkspaceView.xaml.cs (100%) rename src/{WinRestoreKit.Wpf => WinRestoreKit}/Views/ConfirmView.xaml (100%) rename src/{WinRestoreKit.Wpf => WinRestoreKit}/Views/ConfirmView.xaml.cs (100%) rename src/{WinRestoreKit.Wpf => WinRestoreKit}/Views/Controls/SnapshotEventList.xaml (97%) rename src/{WinRestoreKit.Wpf => WinRestoreKit}/Views/Controls/SnapshotEventList.xaml.cs (100%) rename src/{WinRestoreKit.Wpf => WinRestoreKit}/Views/Dialogs/RestoreConsentDialog.xaml (100%) rename src/{WinRestoreKit.Wpf => WinRestoreKit}/Views/Dialogs/RestoreConsentDialog.xaml.cs (100%) delete mode 100644 src/WinRestoreKit/Views/HistoryPageView.cs delete mode 100644 src/WinRestoreKit/Views/HomePageView.cs delete mode 100644 src/WinRestoreKit/Views/IRefreshableView.cs delete mode 100644 src/WinRestoreKit/Views/ProgressPageView.cs rename src/{WinRestoreKit.Wpf => WinRestoreKit}/Views/ProgressWorkspaceView.xaml (100%) rename src/{WinRestoreKit.Wpf => WinRestoreKit}/Views/ProgressWorkspaceView.xaml.cs (100%) delete mode 100644 src/WinRestoreKit/Views/RestoreWizardStep1View.cs delete mode 100644 src/WinRestoreKit/Views/RestoreWizardStep2View.cs rename src/{WinRestoreKit.Wpf => WinRestoreKit}/Views/ResultWorkspaceView.xaml (100%) rename src/{WinRestoreKit.Wpf => WinRestoreKit}/Views/ResultWorkspaceView.xaml.cs (100%) rename src/{WinRestoreKit.Wpf => WinRestoreKit}/Views/SettingsView.xaml (100%) rename src/{WinRestoreKit.Wpf => WinRestoreKit}/Views/SettingsView.xaml.cs (100%) rename src/{WinRestoreKit.Wpf => WinRestoreKit}/Views/TimelineView.xaml (98%) rename src/{WinRestoreKit.Wpf => WinRestoreKit}/Views/TimelineView.xaml.cs (100%) delete mode 100644 src/WinRestoreKit/Views/WatchedGroups.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 970b3af..f8fb2f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,26 @@ version numbers, because that is what those releases were called. ## [Unreleased] +_Nothing yet._ + +## [0.1.0] - 2026-08-11 + +### Changed: WPF is the sole shipping application + +First production release of the WPF Timeline + Compare shell. The WPF app is now the only +shipping WinRestoreKit; the WinForms shell is removed and the release artifact is one +self-contained WinRestoreKit.exe (~69 MB, no .NET install required). + +- Cutover with no compatibility shims: `src/WinRestoreKit` is the WPF app (assembly and executable + identity WinRestoreKit; version sourced from the unchanged `AssemblyInfo.cs`). Removed all + WinForms source, controls, and WinForms-only fonts; preserved the load-bearing `app.manifest`, + `WinRestoreKit.ico`, and `Properties/AssemblyInfo.cs` at their paths. +- Restored the startup-failure guard for the new shell: `App.OnStartup` wraps construction in a + try/catch backed by a neutral `StartupDiagnostics`, preserving the guarantee that a startup + failure is a visible message rather than a silent crash. +- Repointed tests to the WPF assembly identity; deleted WinForms-only tests whose contracts the + WPF STA tests already cover (985 pass, 0 fail). + ### Fixed: WPF restore completion and release truth - Aligned the WPF shell assembly and displayed product version with the canonical 0.0.1 release. diff --git a/CLAUDE.md b/CLAUDE.md index 6cfcbc7..0ed2431 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project Overview -WinRestoreKit is a Windows Forms desktop app (.NET 8, C#) that backs up and restores Windows 11 settings locally - an offline alternative to the built-in Windows Backup app. Backups are lightweight: each module exports registry keys (as `.reg` files) and/or copies folders/files into a timestamped folder. +WinRestoreKit is a WPF desktop app (.NET 8, C#) that backs up and restores Windows 11 settings locally - an offline alternative to the built-in Windows Backup app. Backups are lightweight: each module exports registry keys (as `.reg` files) and/or copies folders/files into a timestamped folder. ## Build @@ -16,17 +16,22 @@ dotnet test src\WinRestoreKit.sln ``` - **`src/WinRestoreKit.Core`** - the engine: `BackupBase`, all of `Conf/`, most of `Results/`, and the - `Utils`/`Data`/`OsHelper`/`LogHelper` helpers. It deliberately does **not** set `UseWindowsForms`, - and that is load-bearing: being unable to compile against WinForms is what keeps a `MessageBox` out + `Utils`/`Data`/`OsHelper`/`LogHelper` helpers. It deliberately sets neither `UseWindowsForms` nor `UseWPF`, + and that is load-bearing: being unable to compile against any UI framework is what keeps a `MessageBox` out of a backup module by construction rather than by review. Extracted in Phase 4 PR 2. -- **`src/WinRestoreKit`** - the WinForms app: `MainForm`, `Views/`, `Forms/`, `Program`, `RunSummary`, - and the three sinks/seams that hand the engine its UI (below). References Core. -- **`src/WinRestoreKit.Tests`** - xUnit. References the app project only; Core arrives transitively. +- **`src/WinRestoreKit.Application`** - the framework-neutral orchestration layer: `Orchestration` + (`BackupRestoreOrchestrator`, `RunCoordinator`), `Settings`, `Updates` (`VersionInfo`, `UpdateCheckService`, + `StartupDiagnostics`), `Snapshots`, `Comparison`, `Backup`, `AppRestore`, `Modules`. References Core only - + neither WinForms nor WPF, by construction. +- **`src/WinRestoreKit`** - the WPF app: `App.xaml`/`MainWindow.xaml`, `Views/`, `ViewModels/`, `Services/`, + `Themes/`, and the seams that hand the engine its UI (below). References Core and Application. +- **`src/WinRestoreKit.Tests`** - xUnit. References the app and Application projects; Core arrives transitively. **The engine reaches the user through three registered seams, never by referencing UI.** All three are -filled in by `Program.RegisterUiSeams()` before the message pump starts: `LogHelper`'s `ILogSink` -(implemented by `RichTextBoxLogSink`), `Utils.UrlFailureUi` (the could-not-open-link dialog), and -`Conf.AppStoreApps.RestoreDialog` (opens `RestAppsForm`). Unregistered, each fails safe on purpose - +filled in during WPF startup: `LogHelper`'s `ILogSink` (registered to `WpfLogSink` in `ShellViewModel`), +`Utils.UrlFailureUi` (the could-not-open-link dialog, wired in `App.OnStartup`), and +`Conf.AppStoreApps.RestoreDialog` (registered via `WpfAppRestoreDialog.Register` in the `ShellViewModel` +constructor; opens the `AppRestoreDialog` window). Unregistered, each fails safe on purpose - logging goes nowhere, the link failure only logs, and the app-restore module reports **`Failed`**, which is deliberately not `Skipped` because `Skipped` is already that module's genuine success reason. @@ -36,7 +41,7 @@ compile, the project reference is wrong, not the modifier. Output lands in `src\WinRestoreKit\bin\\net8.0-windows\`. This dev build is framework-dependent, so running it needs the **.NET Desktop Runtime 8** (`Microsoft.WindowsDesktop.App` 8.0.x) installed. -Releases are different: they ship **self-contained single-file**, so end users install nothing. The `/release` skill has the exact publish command and the flags it depends on - all of them matter, and the artifact must come out as exactly one ~69 MB `WinRestoreKit.exe`. Never ship the framework-dependent `bin\Release\` exe; on its own it cannot start. Do not add `PublishTrimmed` - WinForms resolves types by reflection and is not trim-safe. +Releases are different: they ship **self-contained single-file**, so end users install nothing. The `/release` skill has the exact publish command and the flags it depends on - all of them matter, and the artifact must come out as exactly one ~69 MB `WinRestoreKit.exe`. Never ship the framework-dependent `bin\Release\` exe; on its own it cannot start. Do not add `PublishTrimmed` - WPF resolves types by reflection and is not trim-safe. The only runtime NuGet dependency is Newtonsoft.Json, declared as a `` in the app, Application, and Core projects at the same version (`packages.config` is gone). Tests are xUnit, in `src/WinRestoreKit.Tests`. There is no linter. @@ -130,7 +135,7 @@ that from coming back; each was written after the corresponding mistake was actu nothing. Always check the artifact the command was supposed to produce. - **Log data-bearing text with `LogHelper.LogMessage`, never `LogHelper.Log`.** `Log` treats its first argument as a format string, so a registry path or exception message containing `{` throws inside - the logger and the line is routed to `Console.WriteLine` - invisible in a WinForms app. The message + the logger and the line is routed to `Console.WriteLine` - invisible in a GUI app. The message is not lost loudly; it is lost silently. - **Don't identify files by a name pattern you did not write.** `CWiFiConf` matched `WLAN*.xml` while `netsh` writes `-.xml`, so restore found 0 of 19 profiles. Match on content when @@ -140,20 +145,31 @@ The csproj no longer needs a `` entry - the SDK project globs ` ### UI navigation -`MainForm` is the shell - a left rail (Home · Back up · Restore · History, About in the footer) and a content host - and `NavigationService` (`Helpers/NavigationService.cs`) owns which view is in `pnlForm`, with `Show` for rail navigation, `Push`/`Pop` for going deeper and back, and `IRefreshableView` for views that must re-read disk on every visit. Views live in `Views/`: `HomePageView` (am I okay?), `BackupPageView` (presets + the module tree; renamed from `ConfPageView` in Phase 4 PR 7), `RestoreWizardStep1View`/`RestoreWizardStep2View` (pick a backup, then its contents), `HistoryPageView` (the merged backup/undo-point timeline, which replaced `RestPageView`), `AboutPageView`, and the shared `RunResultsPanel` that renders a run's per-module outcomes in-page. `Forms/RestAppsForm` is a dialog for reinstalling apps from a winget export, and `Forms/RestoreConfirmForm` is the consent dialog. Every view is built with `TableLayoutPanel`/`Dock`/`AutoSize` - absolute positioning is gone, because the process runs `HighDpiMode.PerMonitorV2`. +The shell is `MainWindow.xaml` + `ShellViewModel` (MVVM). `ShellViewModel` owns navigation between the +workspaces - Timeline (am I okay?), Create snapshot (presets + scope tree + compression), History +(advanced timeline search), Settings (theme), About - and the backup/restore run lifecycle. Selecting a +snapshot on the Timeline opens the Compare workspace (`ComparisonWorkspaceView`/`ComparisonWorkspaceViewModel`), +which continues to Confirm (`ConfirmView`/`ConfirmViewModel`) and then the pre-restore consent dialog. +Views live in `Views/` (one `.xaml` + code-behind per workspace), view models in `ViewModels/`, themes in +`Themes/`, and WPF services in `Services/` (`WpfRunUi`, `WpfLogSink`, `WpfThemeService`, `WpfAppRestoreDialog`). +Workspace controls carry stable UI Automation identifiers (`TimelineEventList`, `ComparisonWorkspace`, +`CompareModuleList`, `ConfirmRestoreButton`, `CreateSnapshotButton`, `SettingsThemeFollowSystem`, etc.) - +these are contracts, not layout details. The WPF STA smoke tests construct the real `ShellViewModel` and +`MainWindow` on a dedicated thread (`WpfTestHost`) and assert those identifiers, so a renamed or removed +automation surface fails the suite. ### Data flow and paths - `DataHelper.Data` (`src/WinRestoreKit.Core/Helpers/DataHelper.cs`) centralizes paths and URLs. Backups go to `\app\\` (`Data.DataRootDir`); each backup folder gets a `backup_log.txt` listing what was backed up plus a machine-readable `backup_manifest.json`, both read by Home and the History timeline. `DataRootDir` resolves the exe directory from `Environment.ProcessPath` - **measured** under a real single-file self-contained publish, because the modes need not agree there and nothing in build or test exercises it. The trailing separator is part of the field's contract. Read the comment there before touching the line. -- `LogHelper` (singleton) composes the line and hands the text to an `ILogSink`; the app registers `RichTextBoxLogSink`, which owns the `InvokeRequired`/`Invoke` marshaling. `SetTarget(richTextBox)` still exists as an app-side extension, so call sites read as before. With no sink registered, logging is silent rather than fatal - every test class outside `LogHelperTests` runs that way while product code logs freely. +- `LogHelper` (singleton) composes the line and hands the text to an `ILogSink`; the WPF app registers `WpfLogSink`, which marshals onto the UI `Dispatcher`. With no sink registered, logging is silent rather than fatal - every test class outside `LogHelperTests` runs that way while product code logs freely. - Open web links with `Utils.OpenUrl`, never `Process.Start` directly. The app runs elevated, and `ShellExecute` passes that elevated token to the browser it launches; `OpenUrl` goes through `explorer.exe` so the browser runs as the user, rejects anything that is not an `http`/`https` URL (a shell launch would otherwise execute it), and cannot throw - it is called from a timer thread where .NET 8 turns an escaping exception into process termination. -- Update check (`UpdateCheck.CheckForUpdatesAsync`, app-side since Phase 4 PR 2 - it is almost all MessageBoxes and it calls `Program`) asks the **GitHub Releases API** for the newest release and takes `tag_name` via `Data.ParseLatestReleaseTag`. On ANY failure of that path - non-2xx including the shared-IP rate-limit 403, timeout, malformed JSON, or an empty tag - it falls back to `Data.Uri.URL_ASSEMBLY`, which fetches `src/WinRestoreKit/Properties/AssemblyInfo.cs` from `nicolasestrem/WinRestoreKit`, and string-parses `[assembly: AssemblyFileVersion("x.y.z")]` with `Data.ParseLatestVersion`. That fallback is inherited from Appcopier, but WinRestoreKit starts at 0.0.1 and has no deployed clients of its own, so it is kept on current merit rather than for compatibility: the rate-limit 403 is common, and once the repository is public but before the first Release is published the Releases API has nothing to return while main already carries an AssemblyFileVersion, making the raw path the only one that answers. Note that NEITHER source answers while the repository is private: both requests are unauthenticated, so both 404 and the check reports a failure. The update path only works once the repository is published. `Program.GetCurrentVersionTostring()` reads that same attribute off the running assembly by reflection. Both sides then go through `Version.ToString(3)`, so three-part AssemblyFileVersion values are required. Note the asymmetry: the FALLBACK reads the same attribute this app reads, so a difference there is always a real version difference, but the PRIMARY tag is a separate hand-entered value that never reads the attribute, so a tag that disagrees with the shipped `AssemblyFileVersion` produces a permanent phantom update. Keeping them equal is the release process's job, not the code's. +- Update check (Application's `UpdateCheckService`, presented by the WPF `WpfUpdatePresenter`) asks the **GitHub Releases API** for the newest release and takes `tag_name` via `Data.ParseLatestReleaseTag`. On ANY failure of that path - non-2xx including the shared-IP rate-limit 403, timeout, malformed JSON, or an empty tag - it falls back to `Data.Uri.URL_ASSEMBLY`, which fetches `src/WinRestoreKit/Properties/AssemblyInfo.cs` from `nicolasestrem/WinRestoreKit`, and string-parses `[assembly: AssemblyFileVersion("x.y.z")]` with `Data.ParseLatestVersion`. That fallback is inherited from Appcopier, but WinRestoreKit starts at 0.0.1 and has no deployed clients of its own, so it is kept on current merit rather than for compatibility: the rate-limit 403 is common, and once the repository is public but before the first Release is published the Releases API has nothing to return while main already carries an AssemblyFileVersion, making the raw path the only one that answers. Note that NEITHER source answers while the repository is private: both requests are unauthenticated, so both 404 and the check reports a failure. The update path only works once the repository is published. `VersionInfo.GetCurrentVersion` (Application) reads that same attribute off the running assembly by reflection. Both sides then go through `VersionInfo.Normalize` / `Version.ToString(3)`, so three-part AssemblyFileVersion values are required. Note the asymmetry: the FALLBACK reads the same attribute this app reads, so a difference there is always a real version difference, but the PRIMARY tag is a separate hand-entered value that never reads the attribute, so a tag that disagrees with the shipped `AssemblyFileVersion` produces a permanent phantom update. Keeping them equal is the release process's job, not the code's. ### Namespace quirk -Namespaces do not follow folder structure and are flat: `WinRestoreKit` (core + helpers like `Utils`, `LogHelper`, plus app-side `Ui`/`Theme`/`NavigationService`/`BackupRestoreOrchestrator`), `Conf` (all backup modules and `ModuleCatalog`), `Views`, `DataHelper`. Match the existing namespace of the folder you're working in. +Namespaces do not follow folder structure and are flat: `WinRestoreKit` (Core helpers like `Utils`, `LogHelper`, plus Application orchestration like `BackupRestoreOrchestrator`, `VersionInfo`, `StartupDiagnostics`), `Conf` (all backup modules and `ModuleCatalog`), `DataHelper`, and `WinRestoreKit.Wpf` (the shell: views, view models, services). Match the existing namespace of the folder you're working in. -They also **straddle the two assemblies** since the Core extraction - `WinRestoreKit` and `DataHelper` each have types in both `WinRestoreKit.Core.dll` and `WinRestoreKit.dll` (e.g. `Utils` in Core, `RunSummary` in the app, both in namespace `WinRestoreKit`). That is legal and deliberate: renaming namespaces to match the split would have made a rename-only refactor into a whole-tree edit. It is also why the `InternalsVisibleTo` pair is mandatory rather than a convenience. +They also **straddle assemblies** since the Core extraction - namespace `WinRestoreKit` has types in `WinRestoreKit.Core.dll`, `WinRestoreKit.Application.dll`, and `WinRestoreKit.dll` (e.g. `Utils` in Core, `BackupRestoreOrchestrator` in Application, all in namespace `WinRestoreKit`). That is legal and deliberate: renaming namespaces to match the split would have made a rename-only refactor into a whole-tree edit. It is also why the `InternalsVisibleTo` set is mandatory rather than a convenience: Core grants it to `WinRestoreKit` (the app), `WinRestoreKit.Application`, and `WinRestoreKit.Tests`; Application grants it to `WinRestoreKit` and `WinRestoreKit.Tests`. ## Project automation (`.claude/`) diff --git a/src/WinRestoreKit.Application/Properties/AssemblyInfo.cs b/src/WinRestoreKit.Application/Properties/AssemblyInfo.cs index 503ee65..d8306fb 100644 --- a/src/WinRestoreKit.Application/Properties/AssemblyInfo.cs +++ b/src/WinRestoreKit.Application/Properties/AssemblyInfo.cs @@ -1,5 +1,4 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("WinRestoreKit")] -[assembly: InternalsVisibleTo("WinRestoreKit.Wpf")] [assembly: InternalsVisibleTo("WinRestoreKit.Tests")] diff --git a/src/WinRestoreKit.Application/StartupDiagnostics.cs b/src/WinRestoreKit.Application/StartupDiagnostics.cs new file mode 100644 index 0000000..49ce3bc --- /dev/null +++ b/src/WinRestoreKit.Application/StartupDiagnostics.cs @@ -0,0 +1,33 @@ +using System; + +namespace WinRestoreKit +{ + /// + /// Builds the text shown when the app fails before its window exists. + /// + /// + /// Pure composition with no UI dependency, so it lives in the framework-neutral Application + /// layer and the shell (WinForms or WPF) wraps its startup construction in a try/catch that + /// shows this text and rethrows. The rethrow is what leaves the WER / Event Log record with the + /// real stack; the text is the only artifact the user reads. + /// + internal static class StartupDiagnostics + { + /// + /// Plain concatenation and a total result on a null argument: this runs on the way out of a + /// startup failure, so a NullReferenceException or FormatException raised while DESCRIBING the + /// first failure would replace the only diagnostic the user is ever going to see. The + /// exception's own message is included verbatim - it goes to a MessageBox, which (unlike + /// LogHelper) has no Console.WriteLine fallback, so a brace in the text must not be read as a + /// format placeholder. + /// + internal static string DescribeStartupFailure(Exception ex) + { + if (ex == null) + return "WinRestoreKit could not start. No exception details are available."; + + return "WinRestoreKit could not start." + Environment.NewLine + Environment.NewLine + + ex.GetType().FullName + ": " + (ex.Message ?? string.Empty); + } + } +} diff --git a/src/WinRestoreKit.Core/WinRestoreKit.Core.csproj b/src/WinRestoreKit.Core/WinRestoreKit.Core.csproj index 8f77238..b26c51f 100644 --- a/src/WinRestoreKit.Core/WinRestoreKit.Core.csproj +++ b/src/WinRestoreKit.Core/WinRestoreKit.Core.csproj @@ -44,7 +44,6 @@ - diff --git a/src/WinRestoreKit.Tests/AppRestoreDialogTests.cs b/src/WinRestoreKit.Tests/AppRestoreDialogTests.cs index 7afbd57..728e7b8 100644 --- a/src/WinRestoreKit.Tests/AppRestoreDialogTests.cs +++ b/src/WinRestoreKit.Tests/AppRestoreDialogTests.cs @@ -3,9 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Text; -using System.Windows.Forms; using WinRestoreKit; -using Views; using Xunit; namespace WinRestoreKit.Tests @@ -130,14 +128,6 @@ public void Describe_DistinguishesUnknownStartedProcessFromNeverStarted() Assert.DoesNotContain("could not run winget", unknown); } - [Fact] - public void WinFormsCloseRulesRemainPresentationSpecific() - { - Assert.True(RestAppsForm.ShouldDeferClose(true, CloseReason.UserClosing)); - Assert.False(RestAppsForm.ShouldDeferClose(true, CloseReason.ApplicationExitCall)); - Assert.False(RestAppsForm.CanOwnADialog(false, false, false)); - Assert.Contains("timeout", RestAppsForm.StoppingText, StringComparison.OrdinalIgnoreCase); - } [Fact] public void RestoreDialogCoreSeamRemainsInteractive() diff --git a/src/WinRestoreKit.Tests/BackupDestinationLifecycleTests.cs b/src/WinRestoreKit.Tests/BackupDestinationLifecycleTests.cs index b79e51f..79ee13d 100644 --- a/src/WinRestoreKit.Tests/BackupDestinationLifecycleTests.cs +++ b/src/WinRestoreKit.Tests/BackupDestinationLifecycleTests.cs @@ -6,7 +6,6 @@ using System.Reflection; using System.Linq; using System.Threading.Tasks; -using Views; using Xunit; namespace WinRestoreKit.Tests diff --git a/src/WinRestoreKit.Tests/BackupPageViewTests.cs b/src/WinRestoreKit.Tests/BackupPageViewTests.cs deleted file mode 100644 index 44f9b08..0000000 --- a/src/WinRestoreKit.Tests/BackupPageViewTests.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System; -using Conf; -using System.Collections.Generic; -using System.Linq; -using System.Windows.Forms; -using Views; -using WinRestoreKit; -using Xunit; - -namespace WinRestoreKit.Tests -{ - public class BackupPageViewTests - { - [Fact] - public void Capture_RequestsSelectedConcreteScopeModulesWithoutOwningRunUi() - { - using (BackupPageView view = new BackupPageView()) - { - Assert.False(view is IRunUi); - - foreach (CustomCheckbox scope in Descendants(view).OfType()) - scope.Checked = false; - - CustomCheckbox explorerScope = view.Controls - .Find("scopeToggle0", true) - .OfType() - .Single(); - explorerScope.Checked = true; - - IReadOnlyList requestedModules = null; - view.StartBackupRequested = (modules, snapshotName, compression, destination) => - requestedModules = modules; - - view.Controls.Find("captureButton", true).OfType