Merge claude/1

This commit is contained in:
librelad 2026-07-06 18:31:15 +01:00
commit 35c37b5086
7 changed files with 199 additions and 4 deletions

View File

@ -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'

View File

@ -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;

View File

@ -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();

View File

@ -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({

View File

@ -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;

View File

@ -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")

View File

@ -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."
}