Bestenliste, Devlog-Filter, Galerie und leere Bereiche
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:
+40
-4
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user