Hands-on VM testing showed auto-apply skipped the app picker entirely -- the user
couldn't review/adjust apps before install. Land first-run on the Apps step instead
(pre-checked with the collector flavour's defaults); the user adjusts then walks
Apps -> Prefs -> Apply -> Done. The collector already owns account + flavour, so
Welcome/Flavour are skipped. Reverses the earlier auto-apply behavior per operator
feedback.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Welcome wizard showed nothing until WebView2 cold-started and Blazor
booted, so the whole startup cost presented as a blank window long enough
that operators thought first boot had failed.
- Native MAUI splash overlay (renders in the first frame, no WebView2/JIT
dependency) + a visually identical in-page splash inside #app, so the
native -> webview -> Blazor handoff reads as one continuous loading
screen. Fades out on first successful WV2 NavigationCompleted.
- PublishReadyToRun=true (publish-only) to remove first-run JIT on the
one-shot cold-disk path. R2R header verified present after publish.
- Fixed-version WebView2 runtime baked offline next to the exe (build.ps1
stages it, app points WEBVIEW2_BROWSER_EXECUTABLE_FOLDER at it). Removes
the Evergreen registry probe and the LTSC "no WebView2 at all" risk flagged
in welcome-app-spec.md; air-gap friendly. Absent => falls back to Evergreen.
- De-flash launch: drop the `cmd /c` wrapper and add -WindowStyle Hidden in
autounattend FirstLogonCommands (kills the console flash + one process).
Verified: Release build clean, win-x64 self-contained publish succeeds with
R2R confirmed, 38/38 tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
On IoT LTSC winget is absent, so Process.Start('winget') throws Win32Exception
('cannot find the file specified') rather than returning non-zero. That throw
propagated out of InstallAsync and failed the entire Apply ('Configuration failed').
AppInstaller is now fully exception-safe: a TryRunAsync wrapper converts launch
throws into a failed run, winget is resolved defensively (PATH -> bootstrap+re-probe
-> WindowsApps alias path) and when unavailable the installer skips apps and marks
them not-installed instead of throwing. Per-app launch throws are isolated too.
Two new tests cover probe-throws-skips and per-app-throw-isolated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Insert AppsStep as wizard index 2 (renumbering Account/Prefs/Apply/Done
to 3-6), load the app catalog alongside flavours, seed the per-role
default selection on entering the step, and register IAppCatalog in DI.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Live e2e: in the sm-bootstrap session the taskbar showed and Win/Start worked.
- Keyboard Filter EXEMPTS administrators by default and sm-bootstrap is an admin, so
Win/Start/Alt-Tab etc. were never blocked. Set WEKF_Settings
DisableKeyboardFilterForAdministrators=false so the filter applies to it.
- Auto-hide the taskbar (default-user StuckRects3, inherited by sm-bootstrap) so it
doesn't peek over the fullscreen wizard.
- TearDownAsync now Disable-LocalUser's sm-bootstrap in-session (immediate) so it's
unusable at once; the deferred SYSTEM task still deletes it on next boot (SAM-confirmed
the delete works now).
Verified: Configure-Kiosk parses under Windows PowerShell 5.1 (ASCII-clean); welcome 29/29.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Found by reading the unencrypted VM disk after run #7:
1. Online branding never ran: Apply-Branding.ps1 had a UTF-8 em-dash in a Write-Warning
STRING; Windows PowerShell 5.1 (SetupComplete) reads .ps1 as ANSI, mangled it, broke
the string terminator -> whole script failed to parse -> lock/login/wallpaper branding
never re-applied. Fix: ASCII-ify the em-dash AND save the branding scripts UTF-8-with-BOM
so PS5.1 always decodes them correctly (verified parses under PS5.1 + PS7).
2. sm-bootstrap never removed: TearDownAsync used schtasks /tr with an inline -EncodedCommand,
which silently fails past the ~261-char /tr limit, so the cleanup task was never created
(confirmed NO_TASK on disk). Fix: Register-ScheduledTask (no length limit).
3. Done step: show a QR code of the BitLocker recovery key (QRCoder) for phone backup, and
lay key+QR side-by-side so the Restart button no longer overflows below the fold.
Verified: welcome solution builds, 29/29 tests; branding Pester 6/6 unit (offline-integration
needs elevation, runs in CI).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- BitLocker: remove -SkipHardwareTest so BitLocker validates the TPM+PIN unseal via
its hardware test on the next reboot (the wizard's end-of-flow reboot) before
encrypting — fixes the E_FVE_SECURE_BOOT_CHANGED / PCR-11 drop-to-recovery on the
first post-enroll boot. The PIN now works first time instead of needing recovery.
- Done step now DISPLAYS the 48-digit BitLocker recovery key (read from the file the
enrollment saves) with a 'save this' warning — previously it was never surfaced.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
VM e2e findings on the real-user desktop:
1. Lock/login screen + wallpaper NOT branded (OEM About WAS) — Windows resets the
offline-baked personalization (PersonalizationCSP / default-user wallpaper / FVE)
during OOBE, same class as the UAC reset. Fix: stage windows/branding/ into the
image and re-run Apply-Branding -Mode Online from SetupComplete (post-OOBE, as
SYSTEM) where it sticks. OEM About re-asserted harmlessly.
2. sm-bootstrap account still present after onboarding — TearDownAsync's in-session
Remove-LocalUser no-ops (can't delete the account you're logged in as). Fix: keep
the best-effort in-session attempt, but DEFER the real removal to a SYSTEM
AtStartup scheduled task that runs on next boot (sm-bootstrap not logged on),
removes the account + Win32_UserProfile, then deletes itself.
(Network 'no adapter' in the VM was a Proxmox NIC-model regression to virtio — fixed
by switching the VM to Intel e1000; not a SilverMetal change.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
VM e2e: full wizard ran end-to-end and enrolled TPM+PIN, but BitLockerService only
created TPM+PIN with NO recovery protector — a forgotten/mistyped PIN bricks the
drive (hit exactly that on the VM). Add a RecoveryPassword protector and save the
48-digit key to ProgramData AND the unencrypted EFI System Partition (readable even
when the OS volume is locked, e.g. for offline recovery/verification).
PRODUCT TODO (follow-up): escrow the recovery key to SilverSync + display it in the
wizard's Done step so the end-user records it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
5th VM e2e: with the kiosk fully working mechanically (SL engages, silent UAC,
app launches fullscreen as the shell), the MAUI/WebView2 wizard STILL renders
blank — WebView2 never initializes when the app is the bare Shell Launcher shell
with no Explorer (the same app rendered fine in the earlier build launched with
Explorer present). Operator decision: pivot.
- autounattend.xml: restore FirstLogonCommands to launch the wizard elevated over
the normal (Explorer) first-logon session — where WebView2 works.
- Configure-Kiosk.ps1: drop Shell-Launcher-as-shell entirely; keep the lockdown —
Keyboard Filter (Win/Start/lock/task-switch/Task-Mgr/Alt+F4), DisableTaskMgr /
LockWorkstation / FastUserSwitch, and silent-elevation UAC. The wizard runs
fullscreen-topmost over the locked-down Explorer (covers the taskbar).
- RevertKioskAsync: disable the Keyboard Filter rules for the real user (no SL to
undo); keep escape-policy + secure-UAC restore. Tests updated.
Keeps the diagnostics from #10 (welcome.log) to confirm the wizard renders.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
4th e2e showed a UAC consent prompt for the unsigned Welcome app — the offline-baked
ConsentPromptBehaviorAdmin=0 is reset by Windows during OOBE. Re-assert it (and
PromptOnSecureDesktop=0) ONLINE in Configure-Kiosk.ps1, which runs right before the
sm-bootstrap autologon, so 'Start-Process -Verb RunAs' elevates silently. RevertKioskAsync
restores SECURE UAC (ConsentPromptBehaviorAdmin=2, PromptOnSecureDesktop=1) for the real user.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
4th VM e2e: kiosk shell engages + the app launches fullscreen, but the Blazor
wizard renders BLANK and the kiosk chrome didn't apply (title bar present) — the
app didn't crash, so there's no log to read. Two changes:
1) ApplyKioskChrome made defensive (null-guard HWND/AppWindow, FullScreen presenter
only, returns bool) and wrapped in try/catch at the call site, so a chrome
failure can never stall app/WebView startup (the likely cause of the blank).
2) Always-on file log at C:\ProgramData\SilverMetal\welcome.log: app ctor, window
create, chrome result, unhandled exceptions, and the BlazorWebView/WebView2
lifecycle (Initialized, NavigationCompleted, ProcessFailed). If the wizard is
still blank next run, this pinpoints whether WebView2 env creation failed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
VM e2e caught a reboot loop: Configure-Kiosk used `Invoke-CimMethod -InputObject $wesl`
for SetDefaultShell/SetCustomShell, but WESL_UserSetting exposes STATIC methods and
Get-CimInstance returns null — so those calls threw "InputObject is null" while the
class-level SetEnabled($true) had already succeeded. Result: Shell Launcher enabled with
NO shell configured -> every logon (incl. OOBE defaultuser0) gets a broken shell -> the
"Why did my PC restart?" OOBE loop.
Fix: call SetEnabled/SetDefaultShell/SetCustomShell all class-level (-Namespace/-ClassName).
Setting the DEFAULT shell to explorer.exe is what keeps OOBE/normal logons alive; only
sm-bootstrap gets the kiosk launcher. Added GetCustomShell verification + a fail-open
rollback (SetEnabled false + RunOnce launch of the Welcome app) so a WMI hiccup can never
brick the box again. Same class-level fix applied to BootstrapService.RevertKioskAsync.
Found via VM 102 disk logs (silvermetal-firstboot.log + silvermetal-kiosk.log).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the full-viewport .wizard rule with a fixed-inset frosted-glass card
(backdrop-filter blur, semi-transparent surface, rounded corners, inset shadow)
so the Mercury void/gradient wall shows behind and around the card. Adds
@keyframes sm-rise entrance animation and a body::after SILVERMETAL wordmark
watermark on the wall. Targets the real .wizard class in SilverOS.Welcome.UI/
Components/Routes.razor — no App-project markup touched.
Delete wwwroot/css/silvermetal.css (sm-* selectors targeting no markup),
remove its <link> from index.html, and restore App-project stock Layout/Pages
components (MainLayout, NavMenu, Home) to their pre-Phase-C state at 2d8b651.
These files are unused dead-code — the wizard shell lives in SilverOS.Welcome.UI.
AccountStep now exposes OnValidityChanged EventCallback<bool> and fires it at the end of every Validate() call (including OnInitialized). Routes.razor drops the @ref/IsValid polling pattern in favour of _accountValid updated via the callback + StateHasChanged, matching the existing OnRunningChanged pattern used by ApplyStep. Adds 5 bUnit regression tests covering: initial-invalid, all-valid, re-invalid on clear, short/non-numeric PIN, and pre-populated state on Back→Forward re-mount.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ApplyStep: guard StartAsync against double-invocation (_running check at top)
- ApplyService: replace raw StdErr dump with scrubbed message (exit code + first non-empty line, ≤200 chars)
- ApplyStep: SanitiseForDisplay strips newlines and caps error at 200 chars before rendering
- ApplyStep: add OnRunningChanged EventCallback<bool>; Routes.razor disables Back while _applyRunning
- Routes.razor: AdvanceToDone uses _stepTitles.Length - 1 instead of magic literal 5
- app.css: replace Google Fonts CDN @import with local @font-face rules; bundle DM Mono (300/400/500 + italic 300) and Inter (300/400/500) latin woff2 files under wwwroot/fonts/
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds SilverOS.Welcome.App (net9.0-windows10.0.19041.0 only), registers
all Core services in MauiProgram.cs, and introduces WizardState scoped
service for the wizard host.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
powershell.exe -File binds a single-quoted comma list like '00','03','05' as ONE string element,
not a [string[]] array, so Invoke-Hardening.ps1's -contains filter matched nothing and all
hardening modules were silently skipped.
Fix: adopt a CSV-split contract — Invoke-Hardening.ps1 now accepts [string]$Modules and splits
on ',' internally ($ModuleList = $Modules -split ','); ApplyService passes a bare CSV token
(e.g. 00,03,05) with no surrounding quotes. Empirically verified via ProcessStartInfo: candidate
(a) '00','03','05' → COUNT=1 (bug); candidate (b) 00,03,05 → single string, correctly split by
the script; candidate (c) space-separated → PS positional-parameter error. PARSE OK confirmed.
Adds ApplyServiceHardeningIntegrationTests: copies the real Invoke-Hardening.ps1 into a temp
dir with harmless dummy 0*.ps1 stubs, runs ApplyService with the real ProcessRunner for modules
["00","05"], and asserts ran.txt contains RAN 00 and RAN 05 but NOT RAN 03 or RAN 07.
Test fails on the old encoding and passes with the fix (regression-checked).