Files
SilverMetal/windows/welcome/src/SilverOS.Welcome.UI/Components/Steps/ApplyStep.razor

148 lines
5.0 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-label">@_stageLabel</div>
<div class="apply-progress-track">
<div class="apply-progress-bar" style="width: @(_percent)%"></div>
</div>
<div class="apply-percent-label">@(_percent)%</div>
</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);
}
}
}