diff --git a/windows/welcome/src/SilverOS.Welcome.UI/Components/Routes.razor b/windows/welcome/src/SilverOS.Welcome.UI/Components/Routes.razor
index a5ebfe6..25efb8d 100644
--- a/windows/welcome/src/SilverOS.Welcome.UI/Components/Routes.razor
+++ b/windows/welcome/src/SilverOS.Welcome.UI/Components/Routes.razor
@@ -60,7 +60,7 @@ else
break;
case 4:
-
+
break;
case 5:
@@ -104,6 +104,7 @@ else
private bool _loading = true;
private bool _applyRunning = false;
private bool _toolboxHome = false;
+ private bool _autoApply = false;
private string? _error;
private IReadOnlyList _flavours = Array.Empty();
@@ -167,6 +168,12 @@ else
if (pre.Bitlocker.Enable && !string.IsNullOrEmpty(pre.Bitlocker.Pin))
State.BitLockerPin = pre.Bitlocker.Pin;
+
+ // First run with a preconfig: skip the manual walkthrough. Jump straight to the
+ // Apply step and signal it to auto-start (spec §4d: auto-runs once, shows progress
+ // + recovery key, then Done).
+ _currentStep = 4;
+ _autoApply = true;
}
private void ReRunSetup()
diff --git a/windows/welcome/src/SilverOS.Welcome.UI/Components/Steps/ApplyStep.razor b/windows/welcome/src/SilverOS.Welcome.UI/Components/Steps/ApplyStep.razor
index 323ec3b..aae2437 100644
--- a/windows/welcome/src/SilverOS.Welcome.UI/Components/Steps/ApplyStep.razor
+++ b/windows/welcome/src/SilverOS.Welcome.UI/Components/Steps/ApplyStep.razor
@@ -50,8 +50,10 @@
@code {
[Parameter] public EventCallback OnComplete { get; set; }
[Parameter] public EventCallback OnRunningChanged { get; set; }
+ [Parameter] public bool AutoStart { get; set; }
private bool _running;
+ private bool _autoStarted;
private bool _complete;
private int _percent;
private string _stageLabel = "Preparing…";
@@ -71,6 +73,18 @@
: single[..ErrorDisplayMaxLength] + "…";
}
+ protected override async Task OnAfterRenderAsync(bool firstRender)
+ {
+ // First-run auto-apply: when the host jumps straight to this step with AutoStart,
+ // kick off the same apply the Start button would, exactly once. The manual path
+ // (AutoStart=false) is untouched.
+ if (firstRender && AutoStart && !_autoStarted)
+ {
+ _autoStarted = true;
+ await StartAsync();
+ }
+ }
+
public async Task StartAsync()
{
// Re-entrancy guard: prevent a second overlapping apply if already running
diff --git a/windows/welcome/tests/SilverOS.Welcome.Tests/ApplyStepTests.cs b/windows/welcome/tests/SilverOS.Welcome.Tests/ApplyStepTests.cs
index 755f1ea..1b95fdd 100644
--- a/windows/welcome/tests/SilverOS.Welcome.Tests/ApplyStepTests.cs
+++ b/windows/welcome/tests/SilverOS.Welcome.Tests/ApplyStepTests.cs
@@ -73,6 +73,33 @@ public class ApplyStepTests : TestContext
Assert.True(completed);
}
+ [Fact]
+ public void AutoStart_triggers_apply_once_without_a_button_click()
+ {
+ var apply = new Mock();
+ apply.Setup(a => a.RunAsync(It.IsAny(), It.IsAny>(), It.IsAny()))
+ .Returns(Task.CompletedTask);
+ var state = new WizardState
+ {
+ Flavour = new FlavourManifest { Id = "daily-driver", Hardening = new() { Modules = new[] { "00" } } },
+ BitLockerPin = "123456"
+ };
+ Services.AddSingleton(state);
+ Services.AddSingleton(apply.Object);
+ AddCatalog(Services);
+ AddPreconfig(Services);
+
+ // AutoStart=true should fire StartAsync from OnAfterRenderAsync on first render,
+ // with no Start button click.
+ var cut = RenderComponent(p => p.Add(s => s.AutoStart, true));
+
+ cut.WaitForAssertion(() =>
+ apply.Verify(a => a.RunAsync(
+ It.Is(r => r.Flavour.Id == "daily-driver"),
+ It.IsAny>(),
+ It.IsAny()), Times.Once));
+ }
+
[Fact]
public async Task Shows_error_and_retry_button_when_apply_fails()
{