-
-
Liste des utilisateurs
-
- — utilisateur(s)
-
+
+ i
+ — utilisateur(s)
-
-
+
+
@@ -559,63 +599,53 @@
-
-
-
Films
-
Gestion et ajout des films (par année).
+
+
+ i
+ Ajout de films : cette section récupère automatiquement les infos du film via l’imdb_id (OMDb).
+
+
+
+
+
+
+
+
-
- -
-
-
- -
-
-
- -
-
-
-
-
-
-
-
-
-
-
Gérer les films
-
Filtre, sélection et suppression en lot.
-
-
- —
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
@@ -647,149 +677,15 @@
-
-
-
-
-
Ajouter un film
-
- Le serveur récupère automatiquement titre/plot/poster via OMDb avec l’imdb_id.
-
-
-
-
-
-
-
-
-
-
-
Notes rapides
-
- - Auto: utilise
video_src → embed_src → vod_link.
- - Server file: mets
video_file (chemin relatif sous public/video).
- - Low quality: mets
video_file_low (ex: LOW/mon_film.LOW.mp4).
- - Édition: le bouton “Éditer” ouvre une modale avec des onglets Info / Player.
-
-
-
-
-
-
Astuce
-
- Dans “Gérer”, la recherche filtre la table localement (sans requête API). Le “Tout sélectionner” ne coche que les lignes visibles.
-
-
-
-
-
-
-
-
Backup / Restore
-
Sauvegarde et restauration de la base MongoDB.
+
+ i
+ Sauvegarde et restauration de la base MongoDB.
@@ -820,8 +716,8 @@
Format .ndjson.gz, stocké sur le serveur.
-
-
+
+
@@ -869,7 +765,7 @@
-
+
@@ -917,9 +813,9 @@
-
-
Catégories Oscars
-
Créez et gérez les catégories manuellement.
+
+ i
+ Créez et gérez les catégories manuellement.
@@ -1024,9 +920,9 @@
-
-
Gagnants
-
Définis un ou plusieurs gagnants par année (égalité possible).
+
+ i
+ Définis un ou plusieurs gagnants par année (égalité possible).
@@ -1075,9 +971,9 @@
-
-
-
+
+
+
@@ -1156,8 +1052,8 @@
@@ -1193,8 +1089,8 @@
diff --git a/public/css/app.css b/public/css/app.css
index 7b57f14..7b24d65 100644
--- a/public/css/app.css
+++ b/public/css/app.css
@@ -580,6 +580,75 @@ footer {
color: #111827;
}
+ /* Admin tabs: visually connect tabs with their content */
+ #adminTabs {
+ border-bottom: 0;
+ gap: 0.35rem;
+ padding-bottom: 0;
+ }
+
+ #adminTabs .nav-link {
+ background-color: var(--app-surface);
+ border: 1px solid var(--app-border);
+ }
+
+ #adminTabs .nav-link:hover {
+ background-color: var(--app-surface);
+ }
+
+ #adminTabs .nav-link.active {
+ background-color: var(--app-surface);
+ border-color: var(--app-border);
+ border-bottom-color: transparent;
+ position: relative;
+ top: 1px;
+ z-index: 2;
+ }
+
+ #adminTabs .nav-link.active::after {
+ content: "";
+ position: absolute;
+ left: 1px;
+ right: 1px;
+ bottom: -3px;
+ height: 3px;
+ background: var(--app-surface);
+ }
+
+ #adminTabs .nav-link:not(.active) {
+ border-color: var(--app-border);
+ }
+
+ #adminTabs + .admin-tabs-content {
+ background-color: transparent;
+ border: 0;
+ border-radius: 0;
+ padding: 0;
+ margin-top: -1px;
+ padding-top: 0 !important;
+ }
+
+ /* Hide top border under the active admin tab */
+ #adminTabs + .admin-tabs-content > .tab-pane.active {
+ position: relative;
+ }
+
+ #adminTabs + .admin-tabs-content > .tab-pane.active::before {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: 0;
+ height: 1px;
+ width: calc(100% + 2px);
+ margin-left: -1px;
+ background: transparent;
+ pointer-events: none;
+ }
+
+ #adminTabs + .admin-tabs-content .tab-pane .card:first-of-type {
+ border-top-color: transparent;
+ }
+
body.bg-light {
background-color: var(--app-bg) !important;
@@ -1083,6 +1152,18 @@ footer {
color: #fff;
background: rgba(79, 124, 255, 0.18);
border-color: rgba(79, 124, 255, 0.4);
+ border-bottom-color: transparent;
+ }
+
+ html[data-theme="dark"] #adminTabs .nav-link:not(.active) {
+ border-color: var(--app-border);
+ }
+
+ html[data-theme="dark"] #adminTabs .nav-link.active::after,
+ html[data-theme="dark"] #editMovieTabs .nav-link.active::after {
+ background: var(--app-surface);
+ bottom: -3px;
+ height: 3px;
}
html[data-theme="dark"] #adminTabsContent .table,
diff --git a/public/js/app_control.js b/public/js/app_control.js
index 11c1eb9..df5271c 100644
--- a/public/js/app_control.js
+++ b/public/js/app_control.js
@@ -1,5 +1,6 @@
window.onload = async function () {
const token = localStorage.getItem('auth_token');
+
if (!token) {
window.location.href = '/';
return;
@@ -38,7 +39,7 @@ window.onload = async function () {
const form = document.getElementById('add-movie-form');
const yearInput = document.getElementById('oscar_year');
- const activeYearInput = document.getElementById('active_oscar_year');
+ const activeYearInput = document.getElementById('active_oscar_year') || document.getElementById('oscar_date_year');
const saveActiveYearBtn = document.getElementById('save-active-year');
// Player UI (settings tab): show/hide admin status blocks in player.html
@@ -49,9 +50,6 @@ window.onload = async function () {
// Version control tab elements
const adminVersionTabBtn = document.getElementById('admin-version-tab');
const appVersionAlertEl = document.getElementById('app_version_alert');
- const appVersionActiveSelectEl = document.getElementById('app_version_active_select');
- const appVersionReloadBtn = document.getElementById('app_version_reload');
- const appVersionSetActiveBtn = document.getElementById('app_version_set_active');
const appVersionPreviewEl = document.getElementById('app_version_preview');
const appVersionNewVersionEl = document.getElementById('app_version_new_version');
const appVersionNewMessageEl = document.getElementById('app_version_new_message');
@@ -419,40 +417,10 @@ window.onload = async function () {
}
function renderAppVersionUI() {
- if (!appVersionActiveSelectEl || !appVersionListEl) return;
+ if (!appVersionListEl) return;
const versions = Array.isArray(appVersionState.versions) ? appVersionState.versions : [];
const activeId = appVersionState?.active?.id ? String(appVersionState.active.id) : '';
- const previous = String(appVersionActiveSelectEl.value || '');
- appVersionActiveSelectEl.innerHTML = '';
-
- if (!versions.length) {
- const opt = document.createElement('option');
- opt.value = '';
- opt.textContent = 'Aucune version (crée-en une)';
- opt.disabled = true;
- opt.selected = true;
- appVersionActiveSelectEl.appendChild(opt);
- appVersionActiveSelectEl.disabled = true;
- if (appVersionSetActiveBtn) appVersionSetActiveBtn.disabled = true;
- } else {
- versions.forEach((v) => {
- const opt = document.createElement('option');
- opt.value = String(v?.id || '');
- const dateLabel = v?.dateISO ? formatDateTime(v.dateISO) : '—';
- const msg = String(v?.message || '').trim();
- const shortMsg = msg.length > 80 ? `${msg.slice(0, 77)}...` : msg;
- opt.textContent = `${v?.version || '—'} — ${dateLabel}${shortMsg ? ` — ${shortMsg}` : ''}`;
- appVersionActiveSelectEl.appendChild(opt);
- });
-
- const stillExists = versions.map((v) => String(v?.id || '')).includes(previous);
- const desired = activeId || (stillExists ? previous : String(versions[0]?.id || ''));
- appVersionActiveSelectEl.value = desired;
- appVersionActiveSelectEl.disabled = false;
- if (appVersionSetActiveBtn) appVersionSetActiveBtn.disabled = false;
- }
-
if (!versions.length) {
appVersionListEl.textContent = '—';
return;
@@ -465,6 +433,9 @@ window.onload = async function () {
const msg = escapeHtml(String(v?.message || '').trim());
const versionLabel = escapeHtml(String(v?.version || '—'));
const activeBadge = isActive ? '
Active' : '';
+ const activateBtn = isActive
+ ? ''
+ : `
`;
// IMPORTANT: keep markup compact (no preserved whitespace surprises).
return (
@@ -476,8 +447,9 @@ window.onload = async function () {
`
` +
`
${escapeHtml(dateLabel)}${msg ? ` — ${msg}` : ''}
` +
`
` +
- `
` +
- `
` +
+ `
` +
+ `${activateBtn}` +
+ `` +
`
` +
`
`
);
@@ -485,6 +457,29 @@ window.onload = async function () {
appVersionListEl.innerHTML = `
${rows}
`;
+ appVersionListEl.querySelectorAll('button[data-app-version-activate]').forEach((btn) => {
+ btn.addEventListener('click', async () => {
+ const id = btn.getAttribute('data-app-version-activate');
+ if (!id) return;
+ btn.disabled = true;
+ const old = btn.textContent;
+ btn.textContent = '...';
+ try {
+ hideAppVersionAlert();
+ appVersionState = await setActiveAppVersion(id);
+ appVersionLoadedOnce = true;
+ renderAppVersionUI();
+ await refreshAppVersionPreview();
+ setAppVersionAlert('success', 'Version active mise à jour.');
+ } catch (err) {
+ setAppVersionAlert('danger', err.message || 'Erreur réseau');
+ } finally {
+ btn.disabled = false;
+ btn.textContent = old;
+ }
+ });
+ });
+
appVersionListEl.querySelectorAll('button[data-app-version-delete]').forEach((btn) => {
btn.addEventListener('click', async () => {
const id = btn.getAttribute('data-app-version-delete');
@@ -513,7 +508,7 @@ window.onload = async function () {
async function loadAppVersionTab(options = {}) {
const force = !!options.force;
- if (!appVersionActiveSelectEl || !appVersionListEl) return;
+ if (!appVersionListEl) return;
if (appVersionLoadedOnce && !force) return;
try {
hideAppVersionAlert();
@@ -1103,7 +1098,7 @@ window.onload = async function () {
return (
`
` +
`
${name}${ptsLabel}
` +
- `
` +
+ `
` +
`
`
);
}).join('');
@@ -1235,7 +1230,7 @@ window.onload = async function () {
return /^tt\d{5,}$/.test(String(value || '').trim());
}
- document.getElementById('reset-form').addEventListener('click', function () {
+ document.getElementById('reset-form')?.addEventListener('click', function () {
form.reset();
responseEl.classList.add('d-none');
});
@@ -1632,8 +1627,8 @@ window.onload = async function () {
${formatBytes(b?.sizeBytes)} |
${formatDateTime(b?.mtime)} |
-
-
+
+
|
`;
dbBackupListBody.appendChild(tr);
@@ -1802,42 +1797,7 @@ window.onload = async function () {
if (adminVersionTabBtn) {
adminVersionTabBtn.addEventListener('shown.bs.tab', () => loadAppVersionTab({ force: false }));
}
- if (appVersionReloadBtn) {
- appVersionReloadBtn.addEventListener('click', async () => {
- appVersionReloadBtn.disabled = true;
- const old = appVersionReloadBtn.textContent;
- appVersionReloadBtn.textContent = 'Chargement...';
- try {
- await loadAppVersionTab({ force: true });
- setAppVersionAlert('success', 'Rechargé.');
- } finally {
- appVersionReloadBtn.disabled = false;
- appVersionReloadBtn.textContent = old;
- }
- });
- }
- if (appVersionSetActiveBtn && appVersionActiveSelectEl) {
- appVersionSetActiveBtn.addEventListener('click', async () => {
- const id = String(appVersionActiveSelectEl.value || '').trim();
- if (!id) return;
- appVersionSetActiveBtn.disabled = true;
- const old = appVersionSetActiveBtn.textContent;
- appVersionSetActiveBtn.textContent = '...';
- try {
- hideAppVersionAlert();
- appVersionState = await setActiveAppVersion(id);
- appVersionLoadedOnce = true;
- renderAppVersionUI();
- await refreshAppVersionPreview();
- setAppVersionAlert('success', 'Version active mise à jour.');
- } catch (err) {
- setAppVersionAlert('danger', err.message || 'Erreur réseau');
- } finally {
- appVersionSetActiveBtn.disabled = false;
- appVersionSetActiveBtn.textContent = old;
- }
- });
- }
+ // Active version is handled inside the history list (✓).
async function handleCreateAppVersion(activate) {
const version = String(appVersionNewVersionEl?.value || '').trim();
@@ -1990,18 +1950,17 @@ window.onload = async function () {
const year = parseYear(yearInput.value);
const imdb_id = document.getElementById('imdb_id').value.trim();
const category = document.getElementById('category').value.trim();
- const cam = !!document.getElementById('cam_flag')?.checked;
- const vod_link = document.getElementById('vod_link').value.trim();
- const player_mode = (document.getElementById('player_mode')?.value || 'auto').trim();
- const video_src = document.getElementById('video_src')?.value?.trim() || '';
- const video_file = document.getElementById('video_file')?.value?.trim() || '';
- const video_file_low = document.getElementById('video_file_low')?.value?.trim() || '';
- const embed_src = document.getElementById('embed_src')?.value?.trim() || '';
- const subtitleFileInput = document.getElementById('subtitle_file');
- const subtitleFile = subtitleFileInput?.files?.[0] || null;
- const subtitle_lang = normalizeSubtitleText(document.getElementById('subtitle_lang')?.value, 24);
- const subtitle_label = normalizeSubtitleText(document.getElementById('subtitle_label')?.value, 80);
- const subtitle_default = !!document.getElementById('subtitle_default')?.checked;
+ const cam = false;
+ const vod_link = '';
+ const player_mode = 'auto';
+ const video_src = '';
+ const video_file = '';
+ const video_file_low = '';
+ const embed_src = '';
+ const subtitleFile = null;
+ const subtitle_lang = '';
+ const subtitle_label = '';
+ const subtitle_default = false;
if (!year) {
showResponse('warning', 'Année invalide. Exemple attendu: 2026');
@@ -2158,7 +2117,7 @@ window.onload = async function () {
}
// Oscar date year dropdown (settings tab)
- if (oscarDateYearSelect && oscarDateValueInput && saveOscarDateBtn && clearOscarDateBtn) {
+ if (oscarDateYearSelect && oscarDateValueInput && saveOscarDateBtn) {
const previousOscarYearSelection = String(oscarDateYearSelect.value || '');
oscarDateYearSelect.innerHTML = '';
@@ -2172,7 +2131,7 @@ window.onload = async function () {
oscarDateYearSelect.disabled = true;
oscarDateValueInput.disabled = true;
saveOscarDateBtn.disabled = true;
- clearOscarDateBtn.disabled = true;
+ if (clearOscarDateBtn) clearOscarDateBtn.disabled = true;
} else {
cleanedYears.forEach((y) => {
const opt = document.createElement('option');
@@ -2191,7 +2150,7 @@ window.onload = async function () {
oscarDateYearSelect.disabled = false;
oscarDateValueInput.disabled = false;
saveOscarDateBtn.disabled = false;
- clearOscarDateBtn.disabled = false;
+ if (clearOscarDateBtn) clearOscarDateBtn.disabled = false;
oscarDateYearSelect.value = desired;
}
}
diff --git a/public/user-stat.html b/public/user-stat.html
index fd056ec..7058692 100644
--- a/public/user-stat.html
+++ b/public/user-stat.html
@@ -35,9 +35,23 @@
.stats-tabs .nav-link {
font-weight: 400;
}
+ .stats-tabs {
+ border-bottom: 0;
+ }
.stats-tab-panel {
+ padding-top: 0;
+ }
+ .stats-tab-panel > .tab-pane:not(#pane-ranking) {
padding-top: 1rem;
}
+ #pane-ranking .user-table {
+ margin-top: 0.25rem;
+ }
+ .stats-tab-panel .table,
+ .stats-tab-panel .table th,
+ .stats-tab-panel .table td {
+ font-size: 0.9rem;
+ }
@@ -111,7 +125,7 @@