From 89ad58cb54384efc02cffcfb90e3663c73fa89c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20Kr=C3=BChlmann?= Date: Tue, 28 Apr 2026 14:47:12 +0200 Subject: [PATCH] feat: show pending copyright notices panel on the broadcaster dashboard Fetches NOTIFIED reports on page load and renders an amber warning panel above the main content. Each notice shows the asset ID, optional resolution note, date, a link to the admin console, and a dismiss button that transitions the report to RESOLVED. Panel hides itself when all notices are cleared. WebSocket subscription refreshes the list live on COPYRIGHT_WARNING messages. --- .../resources/static/js/copyright-notices.js | 137 ++++++++++++++++++ src/main/resources/templates/dashboard.html | 18 +++ 2 files changed, 155 insertions(+) create mode 100644 src/main/resources/static/js/copyright-notices.js diff --git a/src/main/resources/static/js/copyright-notices.js b/src/main/resources/static/js/copyright-notices.js new file mode 100644 index 0000000..ac189ba --- /dev/null +++ b/src/main/resources/static/js/copyright-notices.js @@ -0,0 +1,137 @@ +/** + * Copyright notices panel for the broadcaster dashboard. + * + * On load: fetches any pending (NOTIFIED) copyright notices and renders them. + * Dismiss button: acknowledges the notice via API (→ RESOLVED) and removes it. + * WebSocket: subscribes to the channel topic and refreshes on COPYRIGHT_WARNING. + */ +(function () { + const panel = document.getElementById("copyright-notices-panel"); + const list = document.getElementById("copyright-notices-list"); + + if (!panel || !list || typeof broadcaster === "undefined") return; + + // ── Helpers ─────────────────────────────────────────────────────────────── + function csrfHeaders() { + const token = document.querySelector("meta[name='_csrf']")?.content ?? ""; + const header = document.querySelector("meta[name='_csrf_header']")?.content ?? "X-XSRF-TOKEN"; + return { [header]: token }; + } + + function escHtml(str) { + if (!str) return ""; + return String(str) + .replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); + } + + function formatDate(iso) { + if (!iso) return ""; + try { return new Date(iso).toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" }); } + catch { return iso; } + } + + // ── Render ──────────────────────────────────────────────────────────────── + function renderNotices(notices) { + if (!notices || notices.length === 0) { + panel.classList.add("hidden"); + return; + } + panel.classList.remove("hidden"); + list.innerHTML = ""; + for (const notice of notices) { + list.appendChild(buildNoticeItem(notice)); + } + } + + function buildNoticeItem(notice) { + const li = document.createElement("li"); + li.className = "copyright-notice-item"; + li.dataset.reportId = notice.id; + li.innerHTML = ` + + + `; + li.querySelector("[data-dismiss]").addEventListener("click", () => dismissNotice(notice.id, li)); + return li; + } + + // ── API calls ───────────────────────────────────────────────────────────── + async function loadNotices() { + try { + const resp = await fetch(`/api/channels/${encodeURIComponent(broadcaster)}/copyright-notices`); + if (!resp.ok) return; + renderNotices(await resp.json()); + } catch (_) { /* non-critical — silently skip */ } + } + + async function dismissNotice(reportId, li) { + const btn = li.querySelector("[data-dismiss]"); + if (btn) btn.disabled = true; + try { + const resp = await fetch( + `/api/channels/${encodeURIComponent(broadcaster)}/copyright-notices/${encodeURIComponent(reportId)}/dismiss`, + { method: "POST", headers: csrfHeaders() } + ); + if (!resp.ok) { + if (btn) btn.disabled = false; + return; + } + li.classList.add("copyright-notice-dismissed"); + li.addEventListener("transitionend", () => { + li.remove(); + if (list.children.length === 0) panel.classList.add("hidden"); + }, { once: true }); + // Fallback in case transition doesn't fire + setTimeout(() => { + if (li.parentNode) { + li.remove(); + if (list.children.length === 0) panel.classList.add("hidden"); + } + }, 400); + } catch (_) { + if (btn) btn.disabled = false; + } + } + + // ── WebSocket — refresh when a new COPYRIGHT_WARNING arrives ────────────── + function connectWebSocket() { + if (typeof SockJS === "undefined" || typeof Stomp === "undefined") return; + try { + const socket = new SockJS("/ws"); + const stomp = Stomp.over(socket); + stomp.debug = () => {}; + stomp.connect({}, () => { + stomp.subscribe(`/topic/channel/${broadcaster}`, (frame) => { + try { + const msg = JSON.parse(frame.body); + if (msg.type === "COPYRIGHT_WARNING") { + loadNotices(); + } + } catch (_) {} + }); + }); + } catch (_) {} + } + + // ── Init ────────────────────────────────────────────────────────────────── + loadNotices(); + connectWebSocket(); +})(); diff --git a/src/main/resources/templates/dashboard.html b/src/main/resources/templates/dashboard.html index 5545345..37dc55e 100644 --- a/src/main/resources/templates/dashboard.html +++ b/src/main/resources/templates/dashboard.html @@ -14,6 +14,8 @@ crossorigin="anonymous" referrerpolicy="no-referrer" /> + +
@@ -37,6 +39,21 @@ + + +
@@ -192,5 +209,6 @@ const broadcaster = /*[[${channel}]]*/ ""; +