diff --git a/public/admin.html b/public/admin.html new file mode 100644 index 0000000..668ee42 --- /dev/null +++ b/public/admin.html @@ -0,0 +1,24 @@ + + + + + +Administration — Octane + + + + +
+ +
+
+
+
Chargement…
+
+ + + + + + + diff --git a/public/css/style.css b/public/css/style.css index 85dcc3e..122cdb8 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -1294,3 +1294,32 @@ a.back-link:hover { color: var(--accent); } } .vote-review-detail { margin-top: 0.75rem; } + +/* ---------- Admin dashboard ---------- */ + +.admin-user-list { display: flex; flex-direction: column; gap: 0.6rem; } + +.admin-user-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.75rem; + border: 1px solid var(--border); + border-radius: var(--radius-sm); + padding: 0.6rem 0.85rem; + flex-wrap: wrap; +} + +.admin-user-identity { + display: flex; + align-items: center; + gap: 0.75rem; +} + +.admin-user-counts { + display: flex; + gap: 0.9rem; + color: var(--muted); + font-size: 0.88rem; + white-space: nowrap; +} diff --git a/public/js/admin.js b/public/js/admin.js new file mode 100644 index 0000000..06f1881 --- /dev/null +++ b/public/js/admin.js @@ -0,0 +1,75 @@ +function formatDate(dateStr) { + const d = new Date(dateStr); + return d.toLocaleDateString('fr-FR', { year: 'numeric', month: 'long', day: 'numeric' }); +} + +function showError(message) { + document.getElementById('error').innerHTML = `
${escapeHtml(message)}
`; +} + +function statTile(value, label) { + return ` +
+
${value}
+
${escapeHtml(label)}
+
+ `; +} + +function adminUserRowTemplate(u) { + return ` +
+
+ ${avatarHtml(u, 'avatar-sm')} +
+
${escapeHtml(u.name)}${u.isAdmin ? ' admin' : ''}
+
Membre depuis ${formatDate(u.createdAt)} · Dernière connexion ${formatDate(u.lastSeenAt)}
+
+
+
+ 🎵 ${u.songsAdded} + 💡 ${u.suggestionsProposed} + ✔ ${u.votesCast} +
+
+ `; +} + +(async function init() { + const me = await initNav('admin'); + if (!me.isAdmin) { + window.location.href = '/index.html'; + return; + } + + try { + const stats = await api.get('/api/admin/stats'); + const container = document.getElementById('content'); + container.innerHTML = ` +

Administration

+

Vue d'ensemble de l'activité et des données de l'application.

+ +

Vue d'ensemble

+
+ ${statTile(stats.userCount, 'Utilisateurs')} + ${statTile(stats.songs.totalSongs, 'Morceaux au répertoire')} + ${statTile(stats.songs.distinctArtists, 'Artistes différents')} + ${statTile(stats.songs.tutorialsAdded, 'Tutoriels ajoutés')} + ${statTile(stats.suggestions.pending, 'Suggestions en attente')} + ${statTile(stats.suggestions.approved, 'Suggestions approuvées')} + ${statTile(stats.suggestions.totalVotes, 'Votes exprimés')} + ${statTile(stats.concerts.pastConcerts, 'Concerts passés')} + ${statTile(stats.concerts.upcomingConcerts, 'Concerts à venir')} + ${statTile(stats.concerts.avgSongsPerConcert, 'Morceaux / concert (moy.)')} +
+ +

Activité des utilisateurs

+

La date de dernière connexion est approximative (dernière synchronisation du profil, pas un journal de connexion précis).

+
+ ${stats.users.map(adminUserRowTemplate).join('')} +
+ `; + } catch (err) { + showError(err.message); + } +})(); diff --git a/public/js/nav.js b/public/js/nav.js index 7033a2c..c1d224f 100644 --- a/public/js/nav.js +++ b/public/js/nav.js @@ -31,6 +31,7 @@ async function initNav(activePage) { Prochain concert Disponibilités Historique + ${me.isAdmin ? `Administration` : ''}