fix(setup): stop "setup complete" from lying about a half-done install
Three coupled defects in the first-run wizard flow, all surfacing as
"it said complete but nothing was set up":
1. Zero-app installs sailed through. With no apps ticked, setup was just
config+finalize, finished in seconds having installed nothing, and
fast-forwarded to an empty App Center. Add a self-contained in-wizard
confirm ("Install with no apps?") before submitting. Can't reuse the
shared confirmation-dialog component — it isn't loaded this early in
boot — so the dialog is rendered by the wizard itself and mounted on
<body> to escape the aurora surface's FX-stacking rule.
2. finalize declared success unconditionally. It never inspected the
per-app install tasks, so a failed app still yielded "your install is
ready" + a redirect. Pass the setup group id to `setup finalize`; it
now rolls up the group's app-install task results and logs a clear
partial/failed verdict. The WebUI completion watcher gates the welcome
toast + App Center hand-off on the whole group succeeding, not just on
finalize completing — a failed app now keeps the user on the tasks page
with an error toast instead of a false all-clear.
3. Setup task count was confusing (banner "of 2" while the page listed 3).
On dev/git installs the topbar's dev-mode auto-enable raced the wizard's
own CFG_DEV_MODE write and spawned a second, un-grouped config-update
task mid-setup. Skip that auto-enable while a setup handoff is active
(the wizard already persists CFG_DEV_MODE); it runs on the next load if
still needed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
This commit is contained in:
parent
f4ba7ffe51
commit
e3ec256265
@ -239,7 +239,9 @@ router.post('/save', requireAuth, async (req, res) => {
|
||||
}
|
||||
|
||||
const finalizeId = await enqueueTask({
|
||||
command: `libreportal setup finalize`,
|
||||
// Pass the group id so finalize can inspect this run's app-install tasks
|
||||
// and report whether every selected app actually installed.
|
||||
command: `libreportal setup finalize ${setupGroup}`,
|
||||
type: 'setup-finalize',
|
||||
setupGroup,
|
||||
setupRole: 'finalize'
|
||||
|
||||
@ -936,6 +936,62 @@ body.setup-wizard-open {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* "No apps selected" confirm — a self-contained dialog layered over the
|
||||
wizard card (the shared confirmation-dialog component isn't loaded this
|
||||
early in boot). Backdrop dims the wizard; the box mirrors .setup-card. */
|
||||
.setup-confirm {
|
||||
/* Fixed + explicit offsets (not `inset`/`absolute`) so it reliably covers
|
||||
the whole wizard regardless of the flex flow it's appended into. */
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 10000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
background: rgba(var(--bg-rgb), 0.55);
|
||||
backdrop-filter: blur(4px);
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
.setup-confirm.active { opacity: 1; }
|
||||
|
||||
.setup-confirm-box {
|
||||
width: min(440px, 100%);
|
||||
background: rgba(var(--bg-rgb), 0.92);
|
||||
border: 1px solid rgba(var(--accent-rgb), 0.28);
|
||||
border-radius: 16px;
|
||||
padding: 26px 24px 22px;
|
||||
text-align: center;
|
||||
box-shadow: 0 24px 60px rgba(0, 0, 0, 0.45);
|
||||
transform: translateY(8px) scale(0.98);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
.setup-confirm.active .setup-confirm-box { transform: translateY(0) scale(1); }
|
||||
|
||||
.setup-confirm-icon { font-size: 34px; line-height: 1; margin-bottom: 10px; }
|
||||
|
||||
.setup-confirm-title {
|
||||
margin: 0 0 8px;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.setup-confirm-text {
|
||||
margin: 0 0 20px;
|
||||
font-size: 13.5px;
|
||||
line-height: 1.5;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.setup-confirm-actions { display: flex; gap: 10px; }
|
||||
.setup-confirm-actions .setup-btn-back { flex: 1; }
|
||||
.setup-confirm-proceed { flex: 1.4; }
|
||||
|
||||
/* Top-nav disabled state — applied while setup isn't complete. */
|
||||
.topbar-nav.setup-needed .nav-item {
|
||||
opacity: 0.35;
|
||||
|
||||
@ -94,6 +94,15 @@
|
||||
groupTasks.set(t.id, t);
|
||||
}
|
||||
|
||||
// True if any task in the setup group ended in a non-success terminal state.
|
||||
// The finalize task can complete cleanly while an earlier app-install failed —
|
||||
// in that case setup is "done" but NOT "ready", so we must not show the
|
||||
// welcome toast or fast-forward to the App Center as if all went well.
|
||||
function groupHadFailure() {
|
||||
return Array.from(groupTasks.values())
|
||||
.some(t => t && (t.status === 'failed' || t.status === 'cancelled'));
|
||||
}
|
||||
|
||||
function refreshBanner() {
|
||||
if (!isOnTasksPage()) { removeBanner(); return; }
|
||||
const handoff = readHandoff();
|
||||
@ -186,7 +195,9 @@
|
||||
if (!handoff || !handoff.finalizeTaskId) return;
|
||||
if (detail.taskId !== handoff.finalizeTaskId) return;
|
||||
|
||||
const success = detail.status === 'completed';
|
||||
// Success requires BOTH that finalize completed AND that no app in the
|
||||
// group failed — otherwise we'd announce "ready" over a half-installed set.
|
||||
const success = detail.status === 'completed' && !groupHadFailure();
|
||||
clearHandoff();
|
||||
|
||||
// Defer to the next frame so the tasks-manager listener (registered on the
|
||||
@ -214,7 +225,7 @@
|
||||
|| (window.taskEventBus && window.taskEventBus.getTask(handoff.finalizeTaskId));
|
||||
if (!task) return;
|
||||
if (task.status === 'completed' || task.status === 'failed' || task.status === 'cancelled') {
|
||||
const success = task.status === 'completed';
|
||||
const success = task.status === 'completed' && !groupHadFailure();
|
||||
clearHandoff();
|
||||
removeBanner();
|
||||
reEnableNav();
|
||||
|
||||
@ -702,6 +702,23 @@ class SetupWizard {
|
||||
|
||||
const apps = Array.from(this.container.querySelectorAll('.setup-app input[type=checkbox][data-app]:checked'))
|
||||
.map(cb => cb.dataset.app);
|
||||
|
||||
// Zero apps is valid but almost always a mistake — the whole point of the
|
||||
// wizard is to stand up a *working* install. Without a guard, setup would
|
||||
// finish in seconds having installed nothing and fast-forward the user into
|
||||
// an empty App Center wondering why "setup complete" left them with nothing.
|
||||
// Confirm first, with a self-contained in-wizard dialog (the shared
|
||||
// confirmation-dialog component isn't loaded this early in boot, so we
|
||||
// can't rely on window.showConfirmation here). _zeroAppsConfirmed lets the
|
||||
// confirm callback re-enter submit() and skip this branch the second time.
|
||||
if (apps.length === 0 && !this._zeroAppsConfirmed) {
|
||||
this._submitting = false;
|
||||
this.showZeroAppConfirm(
|
||||
() => { this._zeroAppsConfirmed = true; this.submit(); }, // Continue
|
||||
() => { this.showStep(3); } // Go back to apps
|
||||
);
|
||||
return;
|
||||
}
|
||||
// Sub-options live under their parent app and only matter if the parent
|
||||
// is installed. Shape: { crowdsec: { dashboard: true }, ... }
|
||||
const appOptions = {};
|
||||
@ -767,6 +784,60 @@ class SetupWizard {
|
||||
}
|
||||
}
|
||||
|
||||
// Self-contained "no apps selected" confirm. Rendered inside the wizard's
|
||||
// own full-screen shell (so it inherits the aurora aesthetic and doesn't
|
||||
// depend on the shared confirmation-dialog component, which hasn't loaded
|
||||
// this early in boot). Calls onProceed / onCancel exactly once.
|
||||
showZeroAppConfirm(onProceed, onCancel) {
|
||||
// Never stack two.
|
||||
document.querySelector('.setup-confirm')?.remove();
|
||||
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className = 'setup-confirm';
|
||||
overlay.innerHTML = `
|
||||
<div class="setup-confirm-box" role="dialog" aria-modal="true" aria-labelledby="sw-confirm-title">
|
||||
<div class="setup-confirm-icon" aria-hidden="true">🌑</div>
|
||||
<h3 class="setup-confirm-title" id="sw-confirm-title">Install with no apps?</h3>
|
||||
<p class="setup-confirm-text">
|
||||
You haven't selected any apps, so nothing will be installed. Setup will
|
||||
still finish, but you'll start with an empty install — you can always add
|
||||
apps from the App Center later.
|
||||
</p>
|
||||
<div class="setup-confirm-actions">
|
||||
<button type="button" class="setup-btn-back" data-act="cancel">← Pick apps</button>
|
||||
<button type="button" class="setup-launch setup-confirm-proceed" data-act="proceed">
|
||||
<span>Continue with no apps</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
// Mount on <body>, not the wizard: the wizard is an `.aurora-bg` surface
|
||||
// whose `.aurora-bg > :not(.aurora-stars){position:relative}` FX-stacking
|
||||
// rule would otherwise force this fixed overlay back to `relative` and
|
||||
// collapse it into the flex flow. A modal belongs above everything anyway.
|
||||
document.body.appendChild(overlay);
|
||||
// Entrance on next frame so the CSS transition plays.
|
||||
requestAnimationFrame(() => overlay.classList.add('active'));
|
||||
|
||||
let done = false;
|
||||
const close = (cb) => {
|
||||
if (done) return;
|
||||
done = true;
|
||||
overlay.classList.remove('active');
|
||||
setTimeout(() => overlay.remove(), 250);
|
||||
if (typeof cb === 'function') cb();
|
||||
};
|
||||
overlay.querySelector('[data-act="proceed"]').addEventListener('click', () => close(onProceed));
|
||||
overlay.querySelector('[data-act="cancel"]').addEventListener('click', () => close(onCancel));
|
||||
// Backdrop click / Escape cancels (same as "Pick apps").
|
||||
overlay.addEventListener('click', (e) => { if (e.target === overlay) close(onCancel); });
|
||||
const onKey = (e) => {
|
||||
if (e.key === 'Escape') { document.removeEventListener('keydown', onKey); close(onCancel); }
|
||||
};
|
||||
document.addEventListener('keydown', onKey);
|
||||
setTimeout(() => overlay.querySelector('[data-act="proceed"]')?.focus(), 260);
|
||||
}
|
||||
|
||||
handoffToTasks({ firstTaskId, finalizeTaskId, setupGroup, installName, taskIds }) {
|
||||
try {
|
||||
sessionStorage.setItem('libreportal_setup_handoff', JSON.stringify({
|
||||
|
||||
@ -142,6 +142,15 @@ class TopbarComponent {
|
||||
// fields they need to manage their install. Idempotent — runs once per
|
||||
// load; the no-op case (already on, or on release) is silent.
|
||||
async autoEnableDevModeIfNeeded() {
|
||||
// Don't fire a config-update task while the Setup Wizard is still handing
|
||||
// off. The wizard already persists CFG_DEV_MODE via its own setup-config
|
||||
// task; a second loose task here (dev/git installs race the not-yet-
|
||||
// regenerated configs.json and auto-enable) shows up as an extra,
|
||||
// un-grouped row and throws off the setup progress count. If dev mode is
|
||||
// genuinely still needed, this runs on the next normal load.
|
||||
try {
|
||||
if (sessionStorage.getItem('libreportal_setup_handoff')) return;
|
||||
} catch { /* sessionStorage unavailable — fall through to normal behaviour */ }
|
||||
try {
|
||||
const r = await fetch(`/data/config/generated/configs.json?t=${Date.now()}`);
|
||||
if (!r.ok) return;
|
||||
|
||||
@ -23,7 +23,9 @@ cliHandleSetupCommands()
|
||||
;;
|
||||
|
||||
"finalize")
|
||||
setupApplyFinalize
|
||||
# Optional arg: the setup group id, so finalize can roll up the
|
||||
# per-app install results for this run.
|
||||
setupApplyFinalize "$arg1"
|
||||
;;
|
||||
|
||||
"status")
|
||||
|
||||
@ -89,6 +89,11 @@ setupApplyConfig()
|
||||
|
||||
setupApplyFinalize()
|
||||
{
|
||||
# setup_group is passed by the WebUI finalize task so we can read back the
|
||||
# sibling app-install tasks and tell "install ready" apart from "install
|
||||
# ran but an app failed". Empty when finalize is invoked standalone.
|
||||
local setup_group="$1"
|
||||
|
||||
isNotice "Initializing backup engine..."
|
||||
if declare -f installResticHost >/dev/null 2>&1; then
|
||||
installResticHost
|
||||
@ -111,6 +116,45 @@ setupApplyFinalize()
|
||||
fi
|
||||
|
||||
setupWizardMarkComplete
|
||||
|
||||
# Roll up the per-app install results for this setup group. Each ticked app
|
||||
# ran as its own `app install` task; the host daemon writes their terminal
|
||||
# status back into the task JSON before it ever reaches this finalize task
|
||||
# (FIFO, one at a time), so by now every sibling is settled. We only LOG the
|
||||
# verdict here — finalize itself still succeeds (it did finalize) and the
|
||||
# failed app's own task row is already red; the WebUI watcher is what gates
|
||||
# the "your install is ready" hand-off on this same group-level result.
|
||||
if [[ -n "$setup_group" ]]; then
|
||||
local tasks_dir="${containers_dir}libreportal/frontend/data/tasks"
|
||||
local total=0 failed=0 failed_names="" f
|
||||
if [[ -d "$tasks_dir" ]]; then
|
||||
for f in "$tasks_dir"/task_*.json; do
|
||||
[[ -f "$f" ]] || continue
|
||||
local grp role
|
||||
grp=$(jq -r '.setupGroup // empty' "$f" 2>/dev/null)
|
||||
role=$(jq -r '.setupRole // empty' "$f" 2>/dev/null)
|
||||
[[ "$grp" == "$setup_group" && "$role" == "app" ]] || continue
|
||||
total=$((total + 1))
|
||||
local st ec app
|
||||
st=$(jq -r '.status // empty' "$f" 2>/dev/null)
|
||||
ec=$(jq -r '.exit_code // .exitCode // empty' "$f" 2>/dev/null)
|
||||
app=$(jq -r '.app // "app"' "$f" 2>/dev/null)
|
||||
if [[ "$st" == "failed" || "$st" == "cancelled" \
|
||||
|| ( -n "$ec" && "$ec" != "0" && "$ec" != "null" ) ]]; then
|
||||
failed=$((failed + 1))
|
||||
failed_names+="${failed_names:+, }$app"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
if [[ "$total" -eq 0 ]]; then
|
||||
isNotice "No apps were selected — add apps any time from the App Center."
|
||||
elif [[ "$failed" -eq 0 ]]; then
|
||||
isSuccessful "All $total selected app(s) installed successfully."
|
||||
else
|
||||
isError "$failed of $total app(s) failed to install: ${failed_names}. Setup is marked complete — retry the failed app(s) from the App Center."
|
||||
fi
|
||||
fi
|
||||
|
||||
isSuccessful "Setup Wizard complete — your install is configured and ready."
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user