fix(welcome): apply re-entrancy guard, scrub error output, lock nav during apply, offline-bundle fonts

- ApplyStep: guard StartAsync against double-invocation (_running check at top)
- ApplyService: replace raw StdErr dump with scrubbed message (exit code + first non-empty line, ≤200 chars)
- ApplyStep: SanitiseForDisplay strips newlines and caps error at 200 chars before rendering
- ApplyStep: add OnRunningChanged EventCallback<bool>; Routes.razor disables Back while _applyRunning
- Routes.razor: AdvanceToDone uses _stepTitles.Length - 1 instead of magic literal 5
- app.css: replace Google Fonts CDN @import with local @font-face rules; bundle DM Mono (300/400/500 + italic 300) and Inter (300/400/500) latin woff2 files under wwwroot/fonts/

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
sysadmin
2026-06-09 03:34:34 +01:00
parent ea5adacac3
commit 346abc3676
11 changed files with 109 additions and 8 deletions

View File

@@ -46,7 +46,7 @@
<PrefsStep />
break;
case 4:
<ApplyStep OnComplete="AdvanceToDone" />
<ApplyStep OnComplete="AdvanceToDone" OnRunningChanged="@(v => { _applyRunning = v; StateHasChanged(); })" />
break;
case 5:
<DoneStep />
@@ -57,7 +57,7 @@
<div class="wizard-footer">
<button class="btn-secondary"
disabled="@(_currentStep == 0)"
disabled="@(_currentStep == 0 || _applyRunning)"
@onclick="Back">
Back
</button>
@@ -81,6 +81,7 @@
private int _currentStep = 0;
private bool _loading = true;
private bool _applyRunning = false;
private string? _error;
private IReadOnlyList<FlavourManifest> _flavours = Array.Empty<FlavourManifest>();
private AccountStep? _accountStep;
@@ -128,8 +129,7 @@
void AdvanceToDone()
{
// Called by ApplyStep when configuration completes successfully.
// Step index 5 = Done.
_currentStep = 5;
_currentStep = _stepTitles.Length - 1;
StateHasChanged();
}
}

View File

@@ -12,7 +12,7 @@
<div class="apply-error">
<div class="apply-error-icon">&#x26A0;</div>
<p class="apply-error-title">Configuration failed</p>
<p class="apply-error-detail">@_errorMessage</p>
<p class="apply-error-detail">@SanitiseForDisplay(_errorMessage)</p>
<button class="btn-primary btn-retry" @onclick="() => _ = StartAsync()">Retry</button>
</div>
}
@@ -45,6 +45,7 @@
@code {
[Parameter] public EventCallback OnComplete { get; set; }
[Parameter] public EventCallback<bool> OnRunningChanged { get; set; }
private bool _running;
private bool _complete;
@@ -52,14 +53,33 @@
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] + "…";
}
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 req = new ApplyRequest(
Flavour: State.Flavour!,
@@ -86,6 +106,7 @@
_running = false;
_percent = 100;
StateHasChanged();
await OnRunningChanged.InvokeAsync(false);
await OnComplete.InvokeAsync();
}
catch (Exception ex)
@@ -93,6 +114,7 @@
_running = false;
_errorMessage = ex.Message;
StateHasChanged();
await OnRunningChanged.InvokeAsync(false);
}
}
}

View File

@@ -8,8 +8,79 @@
arrival, not scattered micro-animations.
=================================================================== */
/* ── Web Font ───────────────────────────────────────────────────────── */
@import url('https://fonts.googleapis.com/css2?family=DM+Mono:ital,wght@0,300;0,400;0,500;1,300&family=Inter:wght@300;400;500&display=swap');
/* ── Web Fonts (bundled locally — offline-safe, no CDN dependency) ──── */
/* DM Mono — latin subset only; weights 300/400/500 + italic 300 */
@font-face {
font-family: 'DM Mono';
font-style: italic;
font-weight: 300;
font-display: swap;
src: url('../fonts/dm-mono-italic-300.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122,
U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'DM Mono';
font-style: normal;
font-weight: 300;
font-display: swap;
src: url('../fonts/dm-mono-300.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122,
U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'DM Mono';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('../fonts/dm-mono-400.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122,
U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'DM Mono';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url('../fonts/dm-mono-500.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122,
U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* Inter — latin subset only; weights 300/400/500 */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 300;
font-display: swap;
src: url('../fonts/inter-300.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122,
U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('../fonts/inter-400.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122,
U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url('../fonts/inter-500.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122,
U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* ── Design Tokens ──────────────────────────────────────────────────── */
:root {

View File

@@ -16,7 +16,15 @@ public sealed class ApplyService(IProcessRunner runner, IAccountService accounts
var script = Path.Combine(hardeningDir, "Invoke-Hardening.ps1");
var res = await runner.RunAsync("powershell.exe",
$"-NoProfile -ExecutionPolicy Bypass -File \"{script}\" -Modules {mods} -ParamsJson \"{pjson}\"", ct);
if (res.ExitCode != 0) throw new InvalidOperationException($"hardening failed: {res.StdErr}");
if (res.ExitCode != 0)
{
// Only expose exit code + first non-empty stderr line (capped) — never raw full stderr.
var firstLine = res.StdErr
.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)
.FirstOrDefault()?.Trim() ?? string.Empty;
if (firstLine.Length > 200) firstLine = firstLine[..200];
throw new InvalidOperationException($"Hardening failed (exit {res.ExitCode}): {firstLine}");
}
progress.Report(new("Creating your account", 55));
await accounts.CreateAccountsAsync(req.Username, req.Password, req.AdminPassword, ct);