Files
SilverMetal/windows/welcome/src/SilverOS.Welcome.UI/Components/Steps/ApplyStep.razor
sysadmin f44fa150e2 fix(first-boot): run hardening from toolbox, repair branding online re-apply, bake winget into image, Apply UX
Three regressions surfaced by VM 102 validation, plus the winget reliability fix:

- Hardening never ran. SetupComplete.cmd DEFERS hardening to the toolbox when the
  Welcome app is present ("hardening deferred to SilverOS Welcome"), but ApplyService
  only did apps->bitlocker->done — the call was dropped in the collector slim-down, so
  all 8 modules were staged-but-never-executed. Add IHardeningService/HardeningService
  and run it (with the flavour's module selection) as the last Apply step.

- Branding disappeared. Apply-Branding.ps1 -Mode Online crashed looking for
  C:\branding.manifest.json (param default's $PSScriptRoot came back unrooted under
  -File), so the post-OOBE re-apply never ran and personalization reverted. Resolve the
  manifest/assets robustly in the body, falling back to the script's own directory.

- Apps didn't install. The runtime winget bootstrap failed silently on IoT LTSC
  (exit 1, no diag). Provision App Installer + VCLibs + UI.Xaml into the offline image
  at build time (Add-AppxProvisionedPackage) so winget is present at first boot. The
  runtime bootstrap remains as a non-fatal fallback.

- Apply UX looked hung. Add a continuous progress-bar sheen + spinner + "this can take
  several minutes" hint, and make the percentages monotonic (apps 30->70, bitlocker 75,
  hardening 90, done 100).

Tests: 32 passing (ApplyService now verifies apps->bitlocker->hardening order + that
hardening receives the flavour modules).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 01:34:07 +01:00

161 lines
5.5 KiB
Plaintext

@using SilverOS.Welcome.Core.Apps
@using SilverOS.Welcome.Core.Preconfig
@inject IApplyService ApplyService
@inject IAppCatalog AppCatalog
@inject IPreconfigStore PreconfigStore
@inject WizardState State
<div class="step apply-step">
<div class="apply-header">
<h1>Applying Configuration</h1>
<p class="step-subtitle">Your SilverOS is being configured. This may take a few minutes.</p>
</div>
@if (_errorMessage is not null)
{
<div class="apply-error">
<div class="apply-error-icon">&#x26A0;</div>
<p class="apply-error-title">Configuration failed</p>
<p class="apply-error-detail">@SanitiseForDisplay(_errorMessage)</p>
<button class="btn-primary btn-retry" @onclick="() => _ = StartAsync()">Retry</button>
</div>
}
else if (!_running && !_complete)
{
<div class="apply-ready">
<p class="apply-ready-text">Ready to apply your selections. Click Start to begin.</p>
<button class="btn-primary btn-start" @onclick="() => _ = StartAsync()" disabled="@_running">Start</button>
</div>
}
else
{
<div class="apply-progress-container">
<div class="apply-stage-row">
@if (!_complete)
{
<span class="apply-spinner" aria-hidden="true"></span>
}
<div class="apply-stage-label">@_stageLabel</div>
</div>
<div class="apply-progress-track">
<div class="apply-progress-bar @(_complete ? "" : "working")" style="width: @(_percent)%"></div>
</div>
<div class="apply-percent-label">@(_percent)%</div>
@if (!_complete)
{
<p class="apply-hint">
Installing your apps and applying security hardening — this can take several
minutes. Please leave the device powered on.
</p>
}
</div>
@if (_complete)
{
<div class="apply-complete">
<div class="apply-complete-icon">&#x2713;</div>
<p>Configuration complete.</p>
</div>
}
}
</div>
@code {
[Parameter] public EventCallback OnComplete { get; set; }
[Parameter] public EventCallback<bool> 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…";
private string? _errorMessage;
private const int ErrorDisplayMaxLength = 200;
/// <summary>
/// Strips newlines and caps length so a multi-line or huge error message
/// cannot dump raw output into the UI.
/// </summary>
private static string SanitiseForDisplay(string message)
{
var single = message.ReplaceLineEndings(" ").Trim();
return single.Length <= ErrorDisplayMaxLength
? single
: 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
// (e.g. rapid double-click on Retry).
if (_running) return;
_running = true;
_complete = false;
_errorMessage = null;
_percent = 0;
_stageLabel = "Preparing…";
StateHasChanged();
await OnRunningChanged.InvokeAsync(true);
var apps = AppCatalog.Load(Path.Combine(AppContext.BaseDirectory, "apps"))
.All.Where(a => State.SelectedApps.Contains(a.Id)).ToList();
// D1: Apply is now apps+bitlocker only (account via Setup, hardening via SetupComplete).
// D2 owns the full UI rewire (run-mode / preseed); this passes the 3-arg request from
// existing State fields so the app keeps compiling.
var req = new ApplyRequest(
Flavour: State.Flavour!,
BitLockerPin: State.BitLockerPin,
Apps: apps);
var progress = new Progress<ApplyProgress>(p =>
{
_ = InvokeAsync(() =>
{
_percent = p.Percent;
_stageLabel = p.Stage;
StateHasChanged();
});
});
try
{
await ApplyService.RunAsync(req, progress);
_complete = true;
_running = false;
_percent = 100;
// Apply succeeded: wipe the BitLocker pin from the preconfig and stamp the
// configured marker so the next launch opens toolbox-home instead of re-applying.
PreconfigStore.ClearPin();
PreconfigStore.MarkConfigured();
StateHasChanged();
await OnRunningChanged.InvokeAsync(false);
await OnComplete.InvokeAsync();
}
catch (Exception ex)
{
_running = false;
_errorMessage = ex.Message;
StateHasChanged();
await OnRunningChanged.InvokeAsync(false);
}
}
}