+
+
+
+
+
+
+
+ Pilote tout le site (films, checklist, statistiques).
+
+
+
+
Année active
+
+
Basé sur les années présentes dans les films.
+
+
+ Appliquer
+
+
+
-
-
Dates configurées
-
—
+
+
+
+
+
+
+
+
+ Afficher “Source” + “Progress” (admins)
+
+
+ Recharger
+ Enregistrer
+
+
+
+ Contrôle l’affichage du bloc debug dans player.html (admins uniquement).
+
+
+
+
+
+
+
+
+
+
+
+ Configure la date de cérémonie pour le compte à rebours (Films & Checklist). Défaut: 15 mars.
+
+
+
+
+
Année
+
+
Basé sur les années présentes dans les films.
+
+
+
Date
+
+
Format: YYYY-MM-DD.
+
+
+ Effacer
+ Enregistrer
+
+
+
+
+
Dates configurées
+
—
+
+
+
diff --git a/public/js/app_control.js b/public/js/app_control.js
index 11c1ffe..b5ade3a 100644
--- a/public/js/app_control.js
+++ b/public/js/app_control.js
@@ -41,6 +41,11 @@ 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 playerUiShowStatusEl = document.getElementById('player_ui_show_status');
+ 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 +137,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 +157,67 @@ 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 (!playerUiShowStatusEl) return;
+ // Single checkbox controls both flags.
+ playerUiShowStatusEl.checked = !!value.showSource && !!value.showProgress;
+ }
+
+ function getPlayerUiFormValue() {
+ const enabled = !!playerUiShowStatusEl?.checked;
+ return normalizePlayerUi({
+ showSource: enabled,
+ showProgress: enabled,
+ });
+ }
+
+ async function loadPlayerUiIntoUi() {
+ // If the section isn't present (older HTML), skip gracefully.
+ if (!playerUiShowStatusEl) 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/public/js/app_player.js b/public/js/app_player.js
index de77bbc..99d9381 100644
--- a/public/js/app_player.js
+++ b/public/js/app_player.js
@@ -139,6 +139,20 @@ async function fetchActiveOscarYear() {
}
}
+async function fetchPlayerAdminStatusUi() {
+ try {
+ const res = await fetch('/api/settings/player-admin-status-ui', { method: 'GET' });
+ if (!res.ok) return { showSource: true, showProgress: true };
+ const data = await res.json().catch(() => ({}));
+ return {
+ showSource: data?.showSource === undefined ? true : !!data.showSource,
+ showProgress: data?.showProgress === undefined ? true : !!data.showProgress,
+ };
+ } catch (_) {
+ return { showSource: true, showProgress: true };
+ }
+}
+
async function fetchPlaybackProgress(movieId, token) {
try {
const res = await fetch(`/api/movies/${encodeURIComponent(movieId)}/progress`, {
@@ -868,8 +882,9 @@ window.onload = async function () {
}
pageLoader.setProgress(14);
- // Load the active Oscar year in parallel with the movie fetch.
+ // Load the active Oscar year (and player UI flags) in parallel with the movie fetch.
const activeYearPromise = fetchActiveOscarYear();
+ const playerUiPromise = fetchPlayerAdminStatusUi();
let decoded = null;
try {
@@ -889,8 +904,6 @@ window.onload = async function () {
if (decoded.admin) {
const adminLink = document.getElementById('admin-control-link');
if (adminLink) adminLink.classList.remove('d-none');
- const adminStatus = document.getElementById('player-admin-status');
- if (adminStatus) adminStatus.classList.remove('d-none');
}
const logoffBtn = document.getElementById('log-off');
@@ -927,10 +940,20 @@ window.onload = async function () {
return;
}
- const activeYear = await activeYearPromise;
+ const [activeYear, playerUi] = await Promise.all([activeYearPromise, playerUiPromise]);
setHeader(movie, { activeYear });
pageLoader.setProgress(58);
+ // Admin-only status block (Source + Progress) can be globally disabled via settings.
+ if (decoded?.admin) {
+ const enabled = !!playerUi?.showSource && !!playerUi?.showProgress;
+ const adminStatus = document.getElementById('player-admin-status');
+ if (adminStatus) {
+ if (enabled) adminStatus.classList.remove('d-none');
+ else adminStatus.classList.add('d-none');
+ }
+ }
+
const resolved = resolveMovieSource(movie);
if (!resolved?.src) {
setSourceLabel('—');
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) => {