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:
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<div class="apply-error">
|
||||
<div class="apply-error-icon">⚠</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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user