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>
This commit is contained in:
parent
501edda217
commit
b1983dec56
@ -86,6 +86,7 @@
|
|||||||
<script src="/js/utils/ui-helpers.js"></script>
|
<script src="/js/utils/ui-helpers.js"></script>
|
||||||
<script src="/js/utils/router.js"></script>
|
<script src="/js/utils/router.js"></script>
|
||||||
<script src="/js/utils/data-loader.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/eo-modal.js"></script>
|
||||||
<script src="/js/components/dashboard.js"></script>
|
<script src="/js/components/dashboard.js"></script>
|
||||||
<script src="/js/system/system-loader.js"></script>
|
<script src="/js/system/system-loader.js"></script>
|
||||||
|
|||||||
@ -94,6 +94,7 @@ class BackupPage {
|
|||||||
this.applyActiveTabUi(this.currentTab);
|
this.applyActiveTabUi(this.currentTab);
|
||||||
this.bindEvents();
|
this.bindEvents();
|
||||||
await this.refreshAll();
|
await this.refreshAll();
|
||||||
|
await window.Dismissible?.load();
|
||||||
this.render();
|
this.render();
|
||||||
this.updatePageHeader();
|
this.updatePageHeader();
|
||||||
this.updatePrimaryAction();
|
this.updatePrimaryAction();
|
||||||
@ -213,7 +214,7 @@ class BackupPage {
|
|||||||
|
|
||||||
const dismissWarn = e.target.closest('[data-action="dismiss-config-warning"]');
|
const dismissWarn = e.target.closest('[data-action="dismiss-config-warning"]');
|
||||||
if (dismissWarn) {
|
if (dismissWarn) {
|
||||||
localStorage.setItem('libreportal:backup-config-warning-dismissed', '1');
|
window.Dismissible?.dismiss('backup-config-warning');
|
||||||
const banner = dismissWarn.closest('.backup-warning-banner');
|
const banner = dismissWarn.closest('.backup-warning-banner');
|
||||||
const divider = banner?.nextElementSibling;
|
const divider = banner?.nextElementSibling;
|
||||||
if (divider && divider.classList.contains('config-divider')) divider.remove();
|
if (divider && divider.classList.contains('config-divider')) divider.remove();
|
||||||
@ -830,10 +831,10 @@ class BackupPage {
|
|||||||
const body = document.getElementById('backup-configuration-body');
|
const body = document.getElementById('backup-configuration-body');
|
||||||
if (!body) return;
|
if (!body) return;
|
||||||
|
|
||||||
// The warning is a dismissible nudge, so its dismissed state lives in
|
// Dismissed state is persisted server-side via Dismissible
|
||||||
// localStorage (per-browser) rather than server config. Banner + its
|
// (data/ui-state.json), so it follows the user across browsers/devices.
|
||||||
// divider are omitted entirely once dismissed.
|
// Banner + its divider are omitted entirely once dismissed.
|
||||||
const warningDismissed = localStorage.getItem('libreportal:backup-config-warning-dismissed') === '1';
|
const warningDismissed = !!window.Dismissible?.isDismissed('backup-config-warning');
|
||||||
const warningHTML = warningDismissed ? '' : `
|
const warningHTML = warningDismissed ? '' : `
|
||||||
<div class="backup-warning-banner">
|
<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">
|
<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">
|
||||||
|
|||||||
67
containers/libreportal/frontend/js/utils/dismissible.js
Normal file
67
containers/libreportal/frontend/js/utils/dismissible.js
Normal 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 };
|
||||||
|
})();
|
||||||
Loading…
x
Reference in New Issue
Block a user