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 @@
+
+
+
+Vue d'ensemble de l'activité et des données de l'application.
+
+
+ ${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.)')}
+
+
+ La date de dernière connexion est approximative (dernière synchronisation du profil, pas un journal de connexion précis).
+
${me.authentikAccountUrl ? `
Mon compte` : ''}
diff --git a/src/auth/routes.js b/src/auth/routes.js
index fa2fb93..219acbc 100644
--- a/src/auth/routes.js
+++ b/src/auth/routes.js
@@ -138,7 +138,6 @@ router.get(
try {
const userinfo = await client.fetchUserInfo(oidcConfig, tokens.access_token, claims.sub);
picture = userinfo.picture || picture;
- console.log('[auth] userinfo claims received:', Object.keys(userinfo), '— picture:', userinfo.picture || '(none)');
} catch (err) {
console.warn('[auth] failed to fetch userinfo for avatar:', err.message);
}
diff --git a/src/repositories/setlistsRepo.js b/src/repositories/setlistsRepo.js
index 53b5450..e8307ac 100644
--- a/src/repositories/setlistsRepo.js
+++ b/src/repositories/setlistsRepo.js
@@ -130,6 +130,26 @@ async function removeSong(setlistId, setlistSongId) {
]);
}
+async function getStats() {
+ const { rows } = await pool.query(`
+ SELECT
+ COUNT(*) FILTER (WHERE concert_date < CURRENT_DATE)::int AS past_concerts,
+ COUNT(*) FILTER (WHERE concert_date >= CURRENT_DATE)::int AS upcoming_concerts,
+ COALESCE((
+ SELECT AVG(song_count)
+ FROM (
+ SELECT sl.id, COUNT(ss.id) AS song_count
+ FROM setlists sl
+ LEFT JOIN setlist_songs ss ON ss.setlist_id = sl.id
+ WHERE sl.concert_date < CURRENT_DATE
+ GROUP BY sl.id
+ ) t
+ ), 0)::numeric(10,1) AS avg_songs_per_concert
+ FROM setlists
+ `);
+ return rows[0];
+}
+
module.exports = {
findNext,
findHistory,
@@ -142,4 +162,5 @@ module.exports = {
replaceSongs,
addSong,
removeSong,
+ getStats,
};
diff --git a/src/repositories/songsRepo.js b/src/repositories/songsRepo.js
index bb0e8e1..99a8cbd 100644
--- a/src/repositories/songsRepo.js
+++ b/src/repositories/songsRepo.js
@@ -75,6 +75,16 @@ async function removeTutorial(songId, tutorialId) {
]);
}
+async function getStats() {
+ const { rows } = await pool.query(`
+ SELECT COUNT(*)::int AS total_songs,
+ COUNT(DISTINCT NULLIF(artist, ''))::int AS distinct_artists,
+ (SELECT COUNT(*)::int FROM song_tutorials) AS tutorials_added
+ FROM songs
+ `);
+ return rows[0];
+}
+
module.exports = {
findAll,
findById,
@@ -84,4 +94,5 @@ module.exports = {
findTutorials,
addTutorial,
removeTutorial,
+ getStats,
};
diff --git a/src/repositories/suggestionsRepo.js b/src/repositories/suggestionsRepo.js
index 1ab6138..efe25f3 100644
--- a/src/repositories/suggestionsRepo.js
+++ b/src/repositories/suggestionsRepo.js
@@ -84,6 +84,18 @@ async function removeVote(suggestionId, userId) {
]);
}
+async function getStats() {
+ const { rows } = await pool.query(`
+ SELECT COUNT(*)::int AS total,
+ COUNT(*) FILTER (WHERE status = 'pending')::int AS pending,
+ COUNT(*) FILTER (WHERE status = 'approved')::int AS approved,
+ COUNT(*) FILTER (WHERE status = 'rejected')::int AS rejected,
+ (SELECT COUNT(*)::int FROM suggestion_votes) AS total_votes
+ FROM suggestions
+ `);
+ return rows[0];
+}
+
async function promoteToSong(suggestionId, addedBy) {
const client = await pool.connect();
try {
@@ -134,4 +146,5 @@ module.exports = {
upsertVote,
removeVote,
promoteToSong,
+ getStats,
};
diff --git a/src/repositories/usersRepo.js b/src/repositories/usersRepo.js
index 7bed991..0f34bc2 100644
--- a/src/repositories/usersRepo.js
+++ b/src/repositories/usersRepo.js
@@ -32,4 +32,19 @@ async function getActivityStats(userId) {
};
}
-module.exports = { findById, upsertFromClaims, getActivityStats };
+async function findAllWithActivity() {
+ const { rows } = await pool.query(`
+ SELECT u.id, u.name, u.avatar_url, u.is_admin, u.created_at, u.updated_at,
+ COALESCE(s.count, 0) AS songs_added,
+ COALESCE(sg.count, 0) AS suggestions_proposed,
+ COALESCE(v.count, 0) AS votes_cast
+ FROM users u
+ LEFT JOIN (SELECT added_by, COUNT(*)::int AS count FROM songs GROUP BY added_by) s ON s.added_by = u.id
+ LEFT JOIN (SELECT suggested_by, COUNT(*)::int AS count FROM suggestions GROUP BY suggested_by) sg ON sg.suggested_by = u.id
+ LEFT JOIN (SELECT user_id, COUNT(*)::int AS count FROM suggestion_votes GROUP BY user_id) v ON v.user_id = u.id
+ ORDER BY (COALESCE(s.count, 0) + COALESCE(sg.count, 0) + COALESCE(v.count, 0)) DESC, u.name
+ `);
+ return rows;
+}
+
+module.exports = { findById, upsertFromClaims, getActivityStats, findAllWithActivity };
diff --git a/src/routes/admin.js b/src/routes/admin.js
new file mode 100644
index 0000000..0e5137b
--- /dev/null
+++ b/src/routes/admin.js
@@ -0,0 +1,56 @@
+const express = require('express');
+const usersRepo = require('../repositories/usersRepo');
+const songsRepo = require('../repositories/songsRepo');
+const suggestionsRepo = require('../repositories/suggestionsRepo');
+const setlistsRepo = require('../repositories/setlistsRepo');
+const { requireAdmin } = require('../auth/middleware');
+const asyncHandler = require('../lib/asyncHandler');
+
+const router = express.Router();
+
+router.get(
+ '/stats',
+ requireAdmin,
+ asyncHandler(async (req, res) => {
+ const [users, songs, suggestions, concerts] = await Promise.all([
+ usersRepo.findAllWithActivity(),
+ songsRepo.getStats(),
+ suggestionsRepo.getStats(),
+ setlistsRepo.getStats(),
+ ]);
+
+ res.json({
+ userCount: users.length,
+ users: users.map((u) => ({
+ id: u.id,
+ name: u.name,
+ avatarUrl: u.avatar_url,
+ isAdmin: u.is_admin,
+ createdAt: u.created_at,
+ lastSeenAt: u.updated_at,
+ songsAdded: u.songs_added,
+ suggestionsProposed: u.suggestions_proposed,
+ votesCast: u.votes_cast,
+ })),
+ songs: {
+ totalSongs: songs.total_songs,
+ distinctArtists: songs.distinct_artists,
+ tutorialsAdded: songs.tutorials_added,
+ },
+ suggestions: {
+ total: suggestions.total,
+ pending: suggestions.pending,
+ approved: suggestions.approved,
+ rejected: suggestions.rejected,
+ totalVotes: suggestions.total_votes,
+ },
+ concerts: {
+ pastConcerts: concerts.past_concerts,
+ upcomingConcerts: concerts.upcoming_concerts,
+ avgSongsPerConcert: concerts.avg_songs_per_concert,
+ },
+ });
+ })
+);
+
+module.exports = router;
diff --git a/src/routes/index.js b/src/routes/index.js
index 4343b45..428185c 100644
--- a/src/routes/index.js
+++ b/src/routes/index.js
@@ -6,6 +6,7 @@ const suggestionsRoutes = require('./suggestions');
const setlistsRoutes = require('./setlists');
const musicSearchRoutes = require('./musicSearch');
const calendarRoutes = require('./calendar');
+const adminRoutes = require('./admin');
const router = express.Router();
@@ -16,5 +17,6 @@ router.use('/suggestions', suggestionsRoutes);
router.use('/setlists', setlistsRoutes);
router.use('/music-search', musicSearchRoutes);
router.use('/calendar', calendarRoutes);
+router.use('/admin', adminRoutes);
module.exports = router;