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

68 lines
2.1 KiB
JavaScript

// 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 };
})();