diff --git a/containers/libreportal/backend/routes/setup-routes.js b/containers/libreportal/backend/routes/setup-routes.js index 717875a..90ea102 100644 --- a/containers/libreportal/backend/routes/setup-routes.js +++ b/containers/libreportal/backend/routes/setup-routes.js @@ -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' diff --git a/containers/libreportal/frontend/core/setup/css/setup-wizard.css b/containers/libreportal/frontend/core/setup/css/setup-wizard.css index e7f3277..148c8ca 100755 --- a/containers/libreportal/frontend/core/setup/css/setup-wizard.css +++ b/containers/libreportal/frontend/core/setup/css/setup-wizard.css @@ -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; diff --git a/containers/libreportal/frontend/core/setup/js/setup-completion-watcher.js b/containers/libreportal/frontend/core/setup/js/setup-completion-watcher.js index d6377e0..0e5fe5d 100644 --- a/containers/libreportal/frontend/core/setup/js/setup-completion-watcher.js +++ b/containers/libreportal/frontend/core/setup/js/setup-completion-watcher.js @@ -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(); diff --git a/containers/libreportal/frontend/core/setup/js/setup-wizard.js b/containers/libreportal/frontend/core/setup/js/setup-wizard.js index 6e1e75d..54b7e92 100755 --- a/containers/libreportal/frontend/core/setup/js/setup-wizard.js +++ b/containers/libreportal/frontend/core/setup/js/setup-wizard.js @@ -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 = ` +
+ 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. +
+