admin: Übersicht-Dashboard, aufgewertete Nutzerverwaltung, Wiki-Autoren

- Neue /api/stats (Admin, read-only): Inhalte/Nutzer/Dialog-Kennzahlen
- Übersicht-View als Admin-Dashboard: Stat-Karten (klickbar) + Schnellzugriff
- Nutzerverwaltung: Avatar-Initiale, angelegt/zuletzt-aktiv, Rolle beim Anlegen,
  Inline-Passwort (statt prompt), Filter, Rollen-Badge; API liefert last_sign_in_at
- Wiki im Editor anlegbar: Typ 'Wiki-Seite' + Gruppe-Feld → content/wiki/<slug>.md;
  files.js klassifiziert wiki als eigene 'kind' (eigene Sidebar-Gruppe)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-04 12:09:10 +02:00
parent d9ba2f7bbe
commit 9c9b7e03bd
7 changed files with 203 additions and 35 deletions
+47
View File
@@ -0,0 +1,47 @@
import { Hono } from 'hono';
import { supabase } from '../supabase.js';
import { listEntries } from '../files.js';
import { requireAdmin, roleOf } from '../auth.js';
// Kennzahlen für die Admin-Übersicht. Nur Admins; rein lesend.
const stats = new Hono();
stats.use('*', requireAdmin);
stats.get('/', async (c) => {
// Inhalte aus dem Dateisystem zählen.
const content = { beitraege: 0, entwuerfe: 0, wiki: 0, seiten: 0, rubriken: 0 };
try {
for (const e of await listEntries()) {
if (e.kind === 'beitrag') { content.beitraege++; if (e.draft) content.entwuerfe++; }
else if (e.kind === 'wiki') content.wiki++;
else if (e.kind === 'rubrik') content.rubriken++;
else content.seiten++;
}
} catch { /* Filesystem nicht lesbar → 0 */ }
// Nutzer nach Rolle.
const users = { total: 0, admin: 0, editor: 0, user: 0 };
try {
const { data } = await supabase.auth.admin.listUsers();
for (const u of data?.users || []) { users.total++; users[roleOf(u)] = (users[roleOf(u)] || 0) + 1; }
} catch { /* GoTrue nicht erreichbar */ }
// Dialog-Zähler (effizient: head + count, keine Zeilen laden).
const count = async (table, filter) => {
try {
let q = supabase.from(table).select('*', { count: 'exact', head: true });
if (filter) q = filter(q);
const { count: n } = await q;
return n || 0;
} catch { return 0; }
};
const [forums, threads, comments] = await Promise.all([
count('forums'),
count('threads', (q) => q.eq('deleted', false)),
count('comments', (q) => q.eq('deleted', false)),
]);
return c.json({ content, users, dialog: { forums, threads, comments } });
});
export default stats;