From d448d34f67522397bd012dacdb20232a646946fb Mon Sep 17 00:00:00 2001 From: librelad Date: Thu, 28 May 2026 18:25:24 +0100 Subject: [PATCH] feat(backup): auto-refresh the backup page when a backup/restore finishes The page has no live feed, so a completed backup/restore wasn't reflected until a manual Refresh or re-navigation. Subscribe to the TaskEventBus 'taskCompleted' event and repaint on backup/restore completions. Debounced to coalesce the burst when several per-app tasks finish together; only the mounted instance reacts and a stale listener removes itself. The Refresh button stays as a manual pull. Co-Authored-By: Claude Opus 4.7 Signed-off-by: librelad --- .../js/components/backup/backup-page.js | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/containers/libreportal/frontend/js/components/backup/backup-page.js b/containers/libreportal/frontend/js/components/backup/backup-page.js index 20aa75b..e426601 100644 --- a/containers/libreportal/frontend/js/components/backup/backup-page.js +++ b/containers/libreportal/frontend/js/components/backup/backup-page.js @@ -98,6 +98,8 @@ class BackupPage { this.engines = []; // [{id,name,supported_types}, ...] — fetched once this.taskManager = (typeof TaskManager !== 'undefined') ? new TaskManager() : null; this.eventBound = false; + this._taskRefreshTimer = null; + this._onTaskCompleted = this._onTaskCompleted.bind(this); } async init() { @@ -111,6 +113,29 @@ class BackupPage { this.updatePrimaryAction(); } + /* Backups/restores complete asynchronously and the page has no live feed, + so repaint when one finishes (snapshot lists, last-backup times, sizes). + The Refresh button stays as a manual pull. Debounced because picking + several apps queues a task each, which finish in a burst. Only the + mounted instance reacts; a stale listener removes itself. */ + _onTaskCompleted(e) { + if (window.backupPage !== this || !document.getElementById('backup-page')) { + window.removeEventListener('taskCompleted', this._onTaskCompleted); + return; + } + const d = e?.detail || {}; + const cmd = d.task?.command || ''; + const relevant = d.action === 'backup' || d.action === 'restore' + || /^libreportal\s+(backup|restore)\b/.test(cmd); + if (!relevant) return; + clearTimeout(this._taskRefreshTimer); + this._taskRefreshTimer = setTimeout(() => { + if (window.backupPage === this && document.getElementById('backup-page')) { + this.refreshAll().then(() => this.render()); + } + }, 600); + } + /* Read the active tab slug from window.location, supporting both /backup?=dashboard (the legacy libreportal ?= form used on /config) and /backup?backup=dashboard (standard query string) so links from @@ -146,6 +171,8 @@ class BackupPage { if (this.eventBound) return; this.eventBound = true; + window.addEventListener('taskCompleted', this._onTaskCompleted); + // Browser back/forward is handled by the SPA's popstate listener — // pushTabToUrl includes a `route` field in state so the SPA's // handler picks it up and re-runs handleBackup, which re-parses