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 @@
+
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) => {