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>
161 lines
5.5 KiB
Plaintext
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">⚠</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">✓</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);
|
|
}
|
|
}
|
|
}
|