From 30a168e853fa22a87379c63af9395c4af5dc0e50 Mon Sep 17 00:00:00 2001 From: sysadmin Date: Wed, 10 Jun 2026 09:06:02 +0100 Subject: [PATCH] perf(welcome): cut first-boot cold-start + add loading affordance 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 --- .../installer/autounattend/autounattend.xml | 10 +++- windows/installer/build.ps1 | 16 ++++++ windows/welcome/runtime/webview2/README.md | 44 +++++++++++++++ .../src/SilverOS.Welcome.App/MainPage.xaml | 56 ++++++++++++++++--- .../src/SilverOS.Welcome.App/MainPage.xaml.cs | 28 ++++++++++ .../src/SilverOS.Welcome.App/MauiProgram.cs | 17 ++++++ .../SilverOS.Welcome.App.csproj | 10 ++++ .../SilverOS.Welcome.App/wwwroot/css/app.css | 52 +++++++++++++++++ .../SilverOS.Welcome.App/wwwroot/index.html | 16 +++++- 9 files changed, 240 insertions(+), 9 deletions(-) create mode 100644 windows/welcome/runtime/webview2/README.md diff --git a/windows/installer/autounattend/autounattend.xml b/windows/installer/autounattend/autounattend.xml index 79ca977..40f1145 100644 --- a/windows/installer/autounattend/autounattend.xml +++ b/windows/installer/autounattend/autounattend.xml @@ -118,11 +118,19 @@ render when launched as a bare Shell Launcher shell). Configure-Kiosk.ps1 bakes the silent-elevation UAC policy + the lockdown (Keyboard Filter, DisableTaskMgr, hidden taskbar); the wizard runs fullscreen-topmost on top. + + Launch is via a single hidden-window PowerShell (no `cmd /c` wrapper): the + old `cmd /c powershell ...` spawned an extra process AND flashed a visible + console window on the bare first-boot desktop — which itself read as "the + machine is doing something broken" before the wizard appeared. `-WindowStyle + Hidden` + dropping the cmd shim removes that flash and one process off the + critical path. Elevation (-Verb RunAs) is still required for ApplyService + (account/BitLocker/hardening) and is silent thanks to the baked UAC policy. --> 1 - cmd /c powershell -NoProfile -ExecutionPolicy Bypass -Command "Start-Process -FilePath 'C:\Program Files\SilverOS\Welcome\SilverOS.Welcome.App.exe' -Verb RunAs" + powershell -NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -Command "Start-Process -FilePath 'C:\Program Files\SilverOS\Welcome\SilverOS.Welcome.App.exe' -Verb RunAs" Launch SilverOS Welcome elevated diff --git a/windows/installer/build.ps1 b/windows/installer/build.ps1 index 4656af2..6147606 100644 --- a/windows/installer/build.ps1 +++ b/windows/installer/build.ps1 @@ -183,6 +183,22 @@ function Copy-WelcomePayload { } else { Write-Warning " No apps dir found at $appsDir -- image will ship with no app catalog." } + # Stage the fixed-version WebView2 runtime, if vendored, next to the app. + # Cold-start + air-gap: a fixed-version runtime is just files (no installer + # step at first boot) and removes the dependency on whether IoT Enterprise LTSC + # ships WebView2 at all. Operator populates windows\welcome\runtime\webview2\ + # with an EXTRACTED "Microsoft Edge WebView2 Fixed Version" distribution (the + # folder that contains msedgewebview2.exe) -- handled like the drivers dir: + # absent is allowed (VM/dev test), in which case the app falls back to Evergreen. + $wv2Src = Join-Path $WindowsDir 'welcome\runtime\webview2' + if (Test-Path (Join-Path $wv2Src 'msedgewebview2.exe')) { + $wv2Dest = Join-Path $dest 'webview2' + $null = New-Item -ItemType Directory -Force $wv2Dest + Copy-Item (Join-Path $wv2Src '*') $wv2Dest -Recurse -Force + Write-Host " Staged fixed-version WebView2 runtime to $wv2Dest" + } else { + Write-Warning " No fixed-version WebView2 runtime at $wv2Src (expected msedgewebview2.exe) -- image will rely on the Evergreen runtime being present at first boot. See windows\welcome\runtime\webview2\README.md." + } # --- Guard: verify the payload actually landed in the mounted image ------- $stagedExe = Join-Path $dest 'SilverOS.Welcome.App.exe' if (-not (Test-Path $stagedExe)) { diff --git a/windows/welcome/runtime/webview2/README.md b/windows/welcome/runtime/webview2/README.md new file mode 100644 index 0000000..e5af8bd --- /dev/null +++ b/windows/welcome/runtime/webview2/README.md @@ -0,0 +1,44 @@ +# Fixed-version WebView2 runtime (vendored) + +The SilverOS Welcome wizard is a MAUI Blazor Hybrid app — it needs the **Microsoft +Edge WebView2 Runtime**. IoT Enterprise LTSC images frequently ship **without** it, +and even when present, the Evergreen runtime adds first-boot cold-start cost (registry +probe; on-demand install if absent). To make first boot fast *and* air-gapped, we bake +a **fixed-version** runtime here and point the app at it via +`WEBVIEW2_BROWSER_EXECUTABLE_FOLDER` (see `MauiProgram.cs`). + +## What goes in this folder + +The **extracted** contents of a "Microsoft Edge WebView2 Fixed Version" distribution — +i.e. this directory must directly contain `msedgewebview2.exe` (plus its sibling DLLs, +`*.pak`, locales, etc.). The build (`windows/installer/build.ps1`, +`Copy-WelcomePayload`) detects `msedgewebview2.exe` and copies the whole folder to +`C:\Program Files\SilverOS\Welcome\webview2\` inside the image. + +``` +windows/welcome/runtime/webview2/ +├── README.md <- this file (the only thing committed) +├── msedgewebview2.exe <- you add these +├── *.dll +├── *.pak +└── ... +``` + +## How to obtain it + +1. Download the **Fixed Version** (x64) CAB from the official WebView2 distribution + page: → "Fixed Version". + Match the channel/arch to the target (x64, since the app publishes `win-x64`). +2. Expand the CAB and copy the inner runtime folder's contents here so that + `msedgewebview2.exe` sits directly in this directory. +3. Pin the version in `windows/installer/inputs.manifest.json` alongside the other + baked inputs (SBOM hygiene). + +## If you skip this + +The build does **not** fail — it logs a warning and the image relies on whatever +Evergreen runtime is present at first boot. Fine for a quick VM smoke test; **not** +recommended for shipped LTSC media (risk of a blank/hung wizard and slower cold start). + +> The runtime binaries are **not** committed (large, Microsoft-redistributable, version- +> pinned per build). Only this README is tracked. diff --git a/windows/welcome/src/SilverOS.Welcome.App/MainPage.xaml b/windows/welcome/src/SilverOS.Welcome.App/MainPage.xaml index 2343cb6..18d7a6c 100644 --- a/windows/welcome/src/SilverOS.Welcome.App/MainPage.xaml +++ b/windows/welcome/src/SilverOS.Welcome.App/MainPage.xaml @@ -5,12 +5,54 @@ xmlns:components="clr-namespace:SilverOS.Welcome.App.Components;assembly=SilverOS.Welcome.UI" x:Class="SilverOS.Welcome.App.MainPage"> - - - - - + + + + + + + + + + + + + + + diff --git a/windows/welcome/src/SilverOS.Welcome.App/MainPage.xaml.cs b/windows/welcome/src/SilverOS.Welcome.App/MainPage.xaml.cs index c0a0877..fb562ae 100644 --- a/windows/welcome/src/SilverOS.Welcome.App/MainPage.xaml.cs +++ b/windows/welcome/src/SilverOS.Welcome.App/MainPage.xaml.cs @@ -4,6 +4,8 @@ namespace SilverOS.Welcome.App; public partial class MainPage : ContentPage { + bool _splashDismissed; + public MainPage() { InitializeComponent(); @@ -20,7 +22,13 @@ public partial class MainPage : ContentPage { var wv = e.WebView; // Microsoft.UI.Xaml.Controls.WebView2 wv.NavigationCompleted += (a, b) => + { Diag.Log($"WV2 NavigationCompleted ok={b.IsSuccess} status={b.WebErrorStatus}"); + // First completed navigation = the WebView has content on screen. + // Drop the native splash so the (visually identical) in-page splash + // carries through Blazor's final boot without a flash of blank. + if (b.IsSuccess) DismissSplash(); + }; if (wv.CoreWebView2 is not null) wv.CoreWebView2.ProcessFailed += (a, b) => Diag.Log("WV2 ProcessFailed: " + b.ProcessFailedKind); @@ -29,6 +37,26 @@ public partial class MainPage : ContentPage #endif } + // Fade the native splash out once, then collapse it so it never intercepts input. + void DismissSplash() + { + if (_splashDismissed) return; + _splashDismissed = true; + MainThread.BeginInvokeOnMainThread(async () => + { + try + { + await SplashOverlay.FadeTo(0, 250, Easing.CubicOut); + } + catch { /* fade is cosmetic — never block on it */ } + finally + { + SplashOverlay.IsVisible = false; + SplashOverlay.InputTransparent = true; + } + }); + } + void OnUrlLoading(object? sender, UrlLoadingEventArgs e) => Diag.Log("UrlLoading: " + e.Url); } diff --git a/windows/welcome/src/SilverOS.Welcome.App/MauiProgram.cs b/windows/welcome/src/SilverOS.Welcome.App/MauiProgram.cs index 322c36f..4647f55 100644 --- a/windows/welcome/src/SilverOS.Welcome.App/MauiProgram.cs +++ b/windows/welcome/src/SilverOS.Welcome.App/MauiProgram.cs @@ -17,6 +17,23 @@ public static class MauiProgram Directory.CreateDirectory(wv2); Environment.SetEnvironmentVariable("WEBVIEW2_USER_DATA_FOLDER", wv2); + // Cold-start + air-gap: prefer a fixed-version WebView2 runtime baked next to + // the app (build.ps1 stages it under .\webview2). This removes two first-boot + // costs at once: (1) the Evergreen-runtime registry probe, and (2) the risk + // that IoT Enterprise LTSC ships WITHOUT WebView2 entirely — in which case the + // wizard would block/blank waiting on an on-demand install. If the baked folder + // is absent (VM/dev test), fall through to whatever Evergreen runtime exists. + var fixedRuntime = Path.Combine(AppContext.BaseDirectory, "webview2"); + if (File.Exists(Path.Combine(fixedRuntime, "msedgewebview2.exe"))) + { + Environment.SetEnvironmentVariable("WEBVIEW2_BROWSER_EXECUTABLE_FOLDER", fixedRuntime); + Diag.Log("WebView2: using baked fixed-version runtime at " + fixedRuntime); + } + else + { + Diag.Log("WebView2: no baked runtime found; relying on installed Evergreen"); + } + var builder = MauiApp.CreateBuilder(); builder .UseMauiApp() diff --git a/windows/welcome/src/SilverOS.Welcome.App/SilverOS.Welcome.App.csproj b/windows/welcome/src/SilverOS.Welcome.App/SilverOS.Welcome.App.csproj index 0375b8f..e8ee1bf 100644 --- a/windows/welcome/src/SilverOS.Welcome.App/SilverOS.Welcome.App.csproj +++ b/windows/welcome/src/SilverOS.Welcome.App/SilverOS.Welcome.App.csproj @@ -25,6 +25,16 @@ None 10.0.19041.0 + + + true diff --git a/windows/welcome/src/SilverOS.Welcome.App/wwwroot/css/app.css b/windows/welcome/src/SilverOS.Welcome.App/wwwroot/css/app.css index 946b769..313c8b9 100644 --- a/windows/welcome/src/SilverOS.Welcome.App/wwwroot/css/app.css +++ b/windows/welcome/src/SilverOS.Welcome.App/wwwroot/css/app.css @@ -303,6 +303,58 @@ h1:focus { outline: none; } gap: 1rem; } +/* ── Boot splash (pre-Blazor; mirrors the native MAUI splash) ───────── */ +.sm-boot { + position: fixed; + inset: 0; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 1.4rem; + background: var(--clr-void); + z-index: 2000; +} + +.sm-boot-title { + font-family: var(--font-mono); + font-size: 2.6rem; + font-weight: 300; + letter-spacing: -0.02em; + color: var(--clr-text-hi); +} + +.sm-boot-kicker { + font-family: var(--font-mono); + font-size: 0.8rem; + letter-spacing: 0.55em; + text-indent: 0.55em; /* balance the trailing letter-spacing */ + color: var(--clr-accent); +} + +.sm-boot-spinner { + width: 34px; + height: 34px; + border: 3px solid var(--clr-border-hi); + border-top-color: var(--clr-accent); + border-radius: 50%; + animation: sm-spin 0.8s linear infinite; +} + +@keyframes sm-spin { + to { transform: rotate(360deg); } +} + +.sm-boot-text { + font-family: var(--font-ui); + font-size: 0.9rem; + color: var(--clr-text-mid); +} + +@media (prefers-reduced-motion: reduce) { + .sm-boot-spinner { animation: none; } +} + /* ── Loading / error states ─────────────────────────────────────────── */ .loading { font-family: var(--font-mono); diff --git a/windows/welcome/src/SilverOS.Welcome.App/wwwroot/index.html b/windows/welcome/src/SilverOS.Welcome.App/wwwroot/index.html index 03fe4fa..4924e5e 100644 --- a/windows/welcome/src/SilverOS.Welcome.App/wwwroot/index.html +++ b/windows/welcome/src/SilverOS.Welcome.App/wwwroot/index.html @@ -15,7 +15,21 @@
-
Loading...
+ +
+
+
SilverOS
+
WELCOME
+ +
Preparing your setup…
+
+
An unhandled error has occurred.