mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-18 16:46:17 +00:00
feat(admin): render dismissible announcement banners in the admin UI
Fifth piece of #419: wires up the announcements endpoint added in the previous commit. A banner container sits outside the tab-content divs (index.html) so it stays visible across all tabs, not just one. Fetched once on page load alongside settings/version/devices; dismissing calls the server-side dismiss endpoint (not a client-only localStorage flag, so it stays dismissed across sessions/devices) and removes it from the DOM immediately. Manually verified end-to-end against a running instance: the banner container renders, the admin-gate notice appears by default, dismissing it removes it from subsequent /api/announcements responses. No Go test coverage — this is frontend-only wiring of already-tested endpoints. Refs #419
This commit is contained in:
@@ -31,6 +31,8 @@
|
||||
Admin & Setup console: migration, settings, accounts, and diagnostics.
|
||||
</p>
|
||||
|
||||
<div id="announcements-banner"></div>
|
||||
|
||||
<div class="tabs">
|
||||
<div class="tab-buttons">
|
||||
<button class="tab-btn active" onclick="openTab(event, 'tab-overview')">
|
||||
|
||||
@@ -830,6 +830,46 @@ async function startSync() {
|
||||
}
|
||||
}
|
||||
|
||||
// fetchAnnouncements loads and renders the active announcements for this
|
||||
// area (see #419 design doc, _/i419/design-admin-area-auth-gate.md). Not
|
||||
// tab-scoped: the container lives outside the tab-content divs so a banner
|
||||
// stays visible regardless of which tab is open.
|
||||
async function fetchAnnouncements() {
|
||||
const container = document.getElementById("announcements-banner");
|
||||
if (!container) return;
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/announcements?target=admin");
|
||||
if (!response.ok) return;
|
||||
|
||||
const data = await response.json();
|
||||
const announcements = data.announcements || [];
|
||||
|
||||
container.innerHTML = announcements.map(a => `
|
||||
<div class="announcement-banner announcement-${a.level || "info"}" data-announcement-id="${a.id}" style="display:flex; align-items:flex-start; justify-content:space-between; gap:12px; padding:10px 14px; margin-bottom:10px; border-radius:4px; background:#e7f3ff; border:1px solid #b6d9f7; color:#1a4a6e; font-size:0.9em;">
|
||||
<span>${a.message}</span>
|
||||
<button onclick="dismissAnnouncement('${a.id}')" title="Dismiss" style="background:none; border:none; cursor:pointer; font-size:1.1em; line-height:1; color:inherit; flex-shrink:0;">×</button>
|
||||
</div>
|
||||
`).join("");
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch announcements", error);
|
||||
}
|
||||
}
|
||||
|
||||
// dismissAnnouncement records the dismissal server-side (so it stays
|
||||
// dismissed across sessions/devices — not a client-only localStorage flag)
|
||||
// and removes it from the DOM immediately rather than waiting on a refetch.
|
||||
async function dismissAnnouncement(id) {
|
||||
try {
|
||||
await fetch(`/api/announcements/${encodeURIComponent(id)}/dismiss`, {method: "POST"});
|
||||
} catch (error) {
|
||||
console.error("Failed to dismiss announcement", error);
|
||||
}
|
||||
|
||||
const el = document.querySelector(`[data-announcement-id="${id}"]`);
|
||||
if (el) el.remove();
|
||||
}
|
||||
|
||||
async function fetchVersion() {
|
||||
try {
|
||||
const response = await fetch("/api/setup/version");
|
||||
@@ -1797,6 +1837,7 @@ function formatXML(xml) {
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
const cfg = await fetchSettings();
|
||||
fetchVersion();
|
||||
fetchAnnouncements();
|
||||
const deviceCount = await fetchDevices();
|
||||
// Only sweep automatically on a cold start (no devices known yet). When
|
||||
// devices are already in the store, rely on the cached list plus the
|
||||
|
||||
Reference in New Issue
Block a user