Compare commits

..

2 Commits

Author SHA1 Message Date
librelad
a03d7031b1 Merge claude/1 2026-05-23 00:25:15 +01:00
librelad
b1983dec56 feat(webui): server-side dismissible UI notices (Dismissible helper)
Add a reusable Dismissible helper that persists 'hide this permanently' state server-side in data/ui-state.json via the existing authenticated /read-file + /write-file endpoints. It's a direct file write — no task is created (nothing in the task manager) and no system scan runs — so it sidesteps the heavyweight config_update path entirely and works across browsers/devices. The backup config-backup warning now dismisses through Dismissible instead of localStorage; any future notice can opt in with Dismissible.isDismissed(id)/dismiss(id).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-23 00:25:15 +01:00
3 changed files with 74 additions and 5 deletions

View File

@ -86,6 +86,7 @@
<script src="/js/utils/ui-helpers.js"></script>
<script src="/js/utils/router.js"></script>
<script src="/js/utils/data-loader.js"></script>
<script src="/js/utils/dismissible.js"></script>
<script src="/js/components/eo-modal.js"></script>
<script src="/js/components/dashboard.js"></script>
<script src="/js/system/system-loader.js"></script>

View File

@ -94,6 +94,7 @@ class BackupPage {
this.applyActiveTabUi(this.currentTab);
this.bindEvents();
await this.refreshAll();
await window.Dismissible?.load();
this.render();
this.updatePageHeader();
this.updatePrimaryAction();
@ -213,7 +214,7 @@ class BackupPage {
const dismissWarn = e.target.closest('[data-action="dismiss-config-warning"]');
if (dismissWarn) {
localStorage.setItem('libreportal:backup-config-warning-dismissed', '1');
window.Dismissible?.dismiss('backup-config-warning');
const banner = dismissWarn.closest('.backup-warning-banner');
const divider = banner?.nextElementSibling;
if (divider && divider.classList.contains('config-divider')) divider.remove();
@ -830,10 +831,10 @@ class BackupPage {
const body = document.getElementById('backup-configuration-body');
if (!body) return;
// The warning is a dismissible nudge, so its dismissed state lives in
// localStorage (per-browser) rather than server config. Banner + its
// divider are omitted entirely once dismissed.
const warningDismissed = localStorage.getItem('libreportal:backup-config-warning-dismissed') === '1';
// Dismissed state is persisted server-side via Dismissible
// (data/ui-state.json), so it follows the user across browsers/devices.
// Banner + its divider are omitted entirely once dismissed.
const warningDismissed = !!window.Dismissible?.isDismissed('backup-config-warning');
const warningHTML = warningDismissed ? '' : `
<div class="backup-warning-banner">
<svg class="backup-warning-banner-icon" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">

View File

@ -0,0 +1,67 @@
// Dismissible — reusable "hide this UI element permanently" store.
//
// State is persisted server-side in data/ui-state.json via the authenticated
// /read-file + /write-file endpoints, so a dismissal follows the user across
// browsers and devices. It is a direct file read/write: no task is created
// (nothing appears in the task manager) and no system scan runs.
//
// Usage:
// await Dismissible.load(); // once, before reading state (e.g. in a page init)
// if (!Dismissible.isDismissed('my-id')) { ...render the thing... }
// Dismissible.dismiss('my-id'); // on close — persists, fire-and-forget
// Dismissible.restore('my-id'); // bring it back
window.Dismissible = (() => {
const FILE = 'ui-state.json';
let dismissed = new Set();
let loaded = false;
async function load() {
if (loaded) return;
try {
const res = await fetch(`/read-file?path=${encodeURIComponent(FILE)}`);
if (res.ok) {
const data = JSON.parse(await res.text());
if (Array.isArray(data?.dismissed)) dismissed = new Set(data.dismissed);
}
// A 404 just means nothing has been dismissed yet — leave the set empty.
} catch (_) {
// On any error, fail open: treat nothing as dismissed so notices still show.
}
loaded = true;
}
function isDismissed(id) {
return dismissed.has(id);
}
async function dismiss(id) {
await load();
if (dismissed.has(id)) return;
dismissed.add(id);
await persist();
}
async function restore(id) {
await load();
if (!dismissed.has(id)) return;
dismissed.delete(id);
await persist();
}
async function persist() {
try {
await fetch('/write-file', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
path: FILE,
content: JSON.stringify({ dismissed: [...dismissed] }, null, 2)
})
});
} catch (err) {
console.warn('Dismissible: failed to persist state', err);
}
}
return { load, isDismissed, dismiss, restore };
})();