From a227184982075c37fe93983f92b93b0fde1b3f3c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 22 Dec 2025 21:04:26 +0000 Subject: [PATCH] feat: Add player admin UI settings Co-authored-by: mathieumoran1 --- public/control.html | 29 +++++++++++ public/js/app_control.js | 106 +++++++++++++++++++++++++++++++++++++++ routes/settings.js | 53 ++++++++++++++++++++ 3 files changed, 188 insertions(+) diff --git a/public/control.html b/public/control.html index b38825a..0cc2fed 100644 --- a/public/control.html +++ b/public/control.html @@ -119,6 +119,35 @@ +
+
+

Lecteur (player)

+

+ Affiche/masque le bloc “Source” et “Progress” dans player.html (visible uniquement pour les admins). +

+ +
+
+
+ + +
+
+
+
+ + +
+
+ +
+ + +
+
+
+
+

Date des Oscars (par année)

diff --git a/public/js/app_control.js b/public/js/app_control.js index 11c1ffe..81062fd 100644 --- a/public/js/app_control.js +++ b/public/js/app_control.js @@ -41,6 +41,12 @@ window.onload = async function () { const activeYearInput = document.getElementById('active_oscar_year'); const saveActiveYearBtn = document.getElementById('save-active-year'); + // Player UI (settings tab): show/hide admin status blocks in player.html + const playerUiShowSourceEl = document.getElementById('player_ui_show_source'); + const playerUiShowProgressEl = document.getElementById('player_ui_show_progress'); + const playerUiReloadBtn = document.getElementById('player_ui_reload'); + const playerUiSaveBtn = document.getElementById('player_ui_save'); + // Version control tab elements const adminVersionTabBtn = document.getElementById('admin-version-tab'); const appVersionAlertEl = document.getElementById('app_version_alert'); @@ -132,6 +138,11 @@ window.onload = async function () { bodyHtml: '', }); + const DEFAULT_PLAYER_UI = Object.freeze({ + showSource: true, + showProgress: true, + }); + function showResponse(kind, message) { responseEl.classList.remove('d-none', 'alert-success', 'alert-danger', 'alert-warning'); responseEl.classList.add(kind === 'success' ? 'alert-success' : kind === 'warning' ? 'alert-warning' : 'alert-danger'); @@ -147,6 +158,66 @@ window.onload = async function () { .replace(/'/g, '''); } + function normalizePlayerUi(value) { + const v = value && typeof value === 'object' && !Array.isArray(value) ? value : {}; + const showSource = v.showSource === undefined ? DEFAULT_PLAYER_UI.showSource : !!v.showSource; + const showProgress = v.showProgress === undefined ? DEFAULT_PLAYER_UI.showProgress : !!v.showProgress; + return { showSource, showProgress }; + } + + async function fetchPlayerUi() { + const res = await fetch('/api/settings/player-admin-status-ui', { method: 'GET' }); + const data = await res.json().catch(() => ({})); + if (!res.ok) { + const msg = data.message || data.error || `Erreur (${res.status})`; + throw new Error(msg); + } + return normalizePlayerUi(data); + } + + async function savePlayerUi(payload) { + const res = await fetch('/api/settings/player-admin-status-ui', { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}`, + }, + body: JSON.stringify(payload), + }); + const data = await res.json().catch(() => ({})); + if (!res.ok) { + const msg = data.message || data.error || `Erreur (${res.status})`; + throw new Error(msg); + } + return normalizePlayerUi(data); + } + + function setPlayerUiForm(value) { + if (!playerUiShowSourceEl || !playerUiShowProgressEl) return; + playerUiShowSourceEl.checked = !!value.showSource; + playerUiShowProgressEl.checked = !!value.showProgress; + } + + function getPlayerUiFormValue() { + return normalizePlayerUi({ + showSource: !!playerUiShowSourceEl?.checked, + showProgress: !!playerUiShowProgressEl?.checked, + }); + } + + async function loadPlayerUiIntoUi() { + // If the section isn't present (older HTML), skip gracefully. + if (!playerUiShowSourceEl) return; + try { + const value = await fetchPlayerUi(); + setPlayerUiForm(value); + } catch (err) { + // Keep defaults so admin can still set something. + setPlayerUiForm(DEFAULT_PLAYER_UI); + showResponse('warning', err.message || 'Impossible de charger les réglages du lecteur.'); + } + } + async function copyToClipboard(text) { const t = String(text || '').trim(); if (!t) return false; @@ -2337,6 +2408,41 @@ window.onload = async function () { hookModal100Inputs(); await loadCompletionModalIntoUi(); + await loadPlayerUiIntoUi(); + + if (playerUiReloadBtn) { + playerUiReloadBtn.addEventListener('click', async () => { + playerUiReloadBtn.disabled = true; + const old = playerUiReloadBtn.textContent; + playerUiReloadBtn.textContent = 'Chargement...'; + try { + await loadPlayerUiIntoUi(); + showResponse('success', 'Réglages du lecteur rechargés.'); + } finally { + playerUiReloadBtn.disabled = false; + playerUiReloadBtn.textContent = old; + } + }); + } + + if (playerUiSaveBtn) { + playerUiSaveBtn.addEventListener('click', async () => { + playerUiSaveBtn.disabled = true; + const old = playerUiSaveBtn.textContent; + playerUiSaveBtn.textContent = 'Enregistrement...'; + try { + const payload = getPlayerUiFormValue(); + const saved = await savePlayerUi(payload); + setPlayerUiForm(saved); + showResponse('success', 'Réglages du lecteur enregistrés.'); + } catch (err) { + showResponse('danger', err.message || 'Erreur réseau'); + } finally { + playerUiSaveBtn.disabled = false; + playerUiSaveBtn.textContent = old; + } + }); + } if (saveActiveYearBtn && activeYearInput) { saveActiveYearBtn.addEventListener('click', async () => { diff --git a/routes/settings.js b/routes/settings.js index 4d39c99..386e1ad 100644 --- a/routes/settings.js +++ b/routes/settings.js @@ -13,6 +13,7 @@ const COMPLETION_MODAL_KEY = "completion_modal_content"; const WINNERS_BY_YEAR_KEY = "winners_by_year"; const OSCAR_DATES_BY_YEAR_KEY = "oscar_date_by_year"; const APP_VERSION_KEY = "app_version_control"; +const PLAYER_ADMIN_STATUS_UI_KEY = "player_admin_status_ui"; const crypto = require("crypto"); @@ -23,6 +24,12 @@ const DEFAULT_COMPLETION_MODAL = Object.freeze({ bodyHtml: "", }); +const DEFAULT_PLAYER_ADMIN_STATUS_UI = Object.freeze({ + // Controls the admin-only debug/status block in player.html + showSource: true, + showProgress: true, +}); + function parseOscarYear(raw) { if (raw === undefined || raw === null || raw === "") return null; const n = Number(raw); @@ -146,6 +153,13 @@ function normalizeCompletionModal(rawValue) { }; } +function normalizePlayerAdminStatusUi(rawValue) { + const v = rawValue && typeof rawValue === "object" && !Array.isArray(rawValue) ? rawValue : {}; + const showSource = v.showSource === undefined ? DEFAULT_PLAYER_ADMIN_STATUS_UI.showSource : !!v.showSource; + const showProgress = v.showProgress === undefined ? DEFAULT_PLAYER_ADMIN_STATUS_UI.showProgress : !!v.showProgress; + return { showSource, showProgress }; +} + function createId() { try { // Node 16+ / 18+: randomUUID @@ -431,6 +445,45 @@ router.get("/app-version", async (req, res) => { } }); +// Public: get player admin status UI flags (source/progress visibility) +router.get("/player-admin-status-ui", async (req, res) => { + try { + const existing = await Setting.findOne({ key: PLAYER_ADMIN_STATUS_UI_KEY }).select("value"); + const normalized = normalizePlayerAdminStatusUi(existing?.value); + return res.status(200).json(normalized); + } catch (_) { + return res.status(200).json(DEFAULT_PLAYER_ADMIN_STATUS_UI); + } +}); + +// Admin-only: update player admin status UI flags +// Body: { showSource?: boolean, showProgress?: boolean } +router.put("/player-admin-status-ui", async (req, res) => { + const token = req.headers.authorization?.split(" ")[1]; + if (!token) return res.status(401).json({ message: "Authentication token is required" }); + + try { + const decoded = jwt.verify(token, process.env.JWT_SECRET); + if (!decoded.admin) { + return res.status(403).json({ message: "You do not have admin privileges" }); + } + + const normalized = normalizePlayerAdminStatusUi(req.body); + const updated = await Setting.findOneAndUpdate( + { key: PLAYER_ADMIN_STATUS_UI_KEY }, + { $set: { value: normalized } }, + { upsert: true, new: true } + ).select("value"); + + return res.status(200).json(normalizePlayerAdminStatusUi(updated?.value)); + } catch (err) { + if (err instanceof jwt.JsonWebTokenError) { + return res.status(401).json({ message: "Invalid or expired token" }); + } + return res.status(500).json({ error: err.message }); + } +}); + // Admin-only: create a new version entry (optionally activates if none or if activate=true) // Body: { version: "1.2.3", message?: "..." } router.post("/app-version", async (req, res) => {