Bestenliste, Devlog-Filter, Galerie und leere Bereiche
Deploy / check (push) Has been cancelled
Deploy / deploy (push) Has been cancelled

Vier Stellen, die beim Durchsehen der Seiten aufgefallen sind.

Die Bestenliste zeigte nur Namen — und für Platz 1 bis 3 denselben Pokal
ohne Nummer, wodurch die Reihenfolge nur an der Position erkennbar war.
Jetzt: Rangnummer, Avatar und die ersten drei Plätze farblich abgesetzt.
Die Avatare kommen aus dem Member-Cache, der durch den GuildMembers-Intent
ohnehin gefüllt ist — das kostet keinen einzigen Discord-Aufruf. Wer nicht
im Cache liegt, bekommt seinen Anfangsbuchstaben statt eines leeren Kreises.

Die Projekt-Chips im Devlog-Archiv waren bloß Beschriftung. Jetzt filtern
Chips mit Anzahl nach Projekt (ECOGAME / D4RKBOT) — man will meist nur
eins von beiden lesen. Serverseitig über LIKE auf die erste Zeile, wo das
Projekt ohnehin steht; das spart eine Spalte und eine Migration. Eine
aktive Suche schlägt den Filter, sonst müsste die Volltextsuche beides
verbinden, ohne dass jemand danach fragt.

Galerie: Vorschaubilder von 240 auf 340 Pixel, und Name plus Datum stehen
dauerhaft am Bild statt nur beim Überfahren — am Handy gab es kein Hover
und damit nie eine Zuordnung.

Events, Changelog, Galerie und Level endeten bei wenig Inhalt mit einem
einzelnen Satz in einer sonst leeren Seite; das sah nach Fehler aus statt
nach „noch nichts da". Jetzt steht dort ein Block mit Symbol, Erklärung
was hier erscheinen wird, und Verweisen auf belebte Bereiche.

Dazu ein Sprung-nach-oben-Knopf im Devlog-Archiv und im Changelog, die
beide sehr lang werden.

Beim Testen gefunden: ESCAPE '\' in einem JS-String kommt als leeres
Zeichen in SQLite an ("ESCAPE expression must be a single character") —
der Backslash müsste doppelt entwertet werden. Jetzt ist das Escape-Zeichen
ein Ausrufezeichen, das die Falle gar nicht erst aufmacht.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-31 17:44:57 +02:00
co-authored by Claude Fable 5
parent c6977f9c4a
commit 2e58268f8d
11 changed files with 314 additions and 32 deletions
+40 -4
View File
@@ -1,7 +1,8 @@
// REST-API fürs Webinterface: Devlogs öffentlich, Commits + Settings nur für den Admin
import { EmbedBuilder } from 'discord.js';
import {
listDevlogs, searchDevlogs, getDevlog, listCommits, listReleases, archiveStats,
listDevlogs, listDevlogsByProject, devlogHeads,
searchDevlogs, getDevlog, listCommits, listReleases, archiveStats,
getSetting, setSetting, createApiKey, listApiKeys, deleteApiKey,
listGallery, listPlaytesters, topWishes, commitHeatmap,
createRoleMenu, updateRoleMenu, getRoleMenu, listRoleMenus, deleteRoleMenu,
@@ -84,10 +85,28 @@ export function registerApiRoutes(app, client) {
});
// Devlog-Archiv — öffentlich (wie der Discord-Kanal); ?q= für Volltextsuche
// Projekte im Archiv (D4RKBOT, ECOGAME …) samt Anzahl — für die Filter-Chips.
// Das Projekt steht in der ersten Zeile, dieselbe Regel wie im Frontend.
app.get('/api/devlog-projects', async () => {
const counts = new Map();
for (const head of devlogHeads()) {
const name = head.match(/^Devlog\s*[—–-]\s*(.+)$/i)?.[1]?.trim();
if (name) counts.set(name, (counts.get(name) ?? 0) + 1);
}
return {
projects: [...counts].map(([name, n]) => ({ name, n })).sort((a, b) => b.n - a.n),
};
});
app.get('/api/devlogs', async (request) => {
const { limit, offset, page } = paging(request);
const q = (request.query.q ?? '').trim();
const { items, total } = q ? searchDevlogs(q, limit, offset) : listDevlogs(limit, offset);
const project = (request.query.project ?? '').trim();
// Suche schlägt Filter — beides gleichzeitig würde die FTS-Abfrage verkomplizieren,
// ohne dass jemand danach fragt.
const { items, total } = q
? searchDevlogs(q, limit, offset)
: (project ? listDevlogsByProject(project, limit, offset) : listDevlogs(limit, offset));
// images: JSON-Spalte → fertige URLs fürs Frontend
const mapped = items.map(({ images, ...rest }) => ({
...rest,
@@ -382,8 +401,25 @@ export function registerApiRoutes(app, client) {
return { deleted: deleteService(Number(request.params.id)), services: listServices() };
});
// Level-Bestenliste + Server-Aktivität — öffentlich
app.get('/api/levels', async () => ({ levels: topLevels(50) }));
// Level-Bestenliste + Server-Aktivität — öffentlich.
// Avatare kommen aus dem Member-Cache: der ist durch den GuildMembers-Intent
// ohnehin gefüllt, kostet also keinen API-Aufruf. Wer nicht drin ist,
// bekommt kein Bild — die Liste soll nicht 50 Abrufe an Discord auslösen.
app.get('/api/levels', async () => {
const members = new Map();
for (const guild of client.guilds.cache.values()) {
for (const [id, member] of guild.members?.cache ?? []) {
if (!members.has(id)) members.set(id, member);
}
}
return {
levels: topLevels(50).map((row) => ({
...row,
avatar: members.get(row.user_id)?.displayAvatarURL?.({ size: 64 }) ?? null,
display_name: members.get(row.user_id)?.displayName ?? row.username,
})),
};
});
app.get('/api/serverstats', async () => ({
days: activityRange(30),
members: [...client.guilds.cache.values()].reduce((sum, g) => sum + (g.memberCount ?? 0), 0),