UX-Paket: Navbar aufgeräumt, /server-Seite, Lightbox, Skeletons, Audit-Log, Reroll
- Navbar entschlackt: nur noch Devlog · Roadmap · Server · Community · Setup; Galerie/Level/Events/Changelog im Community-Dropdown, Profil/Commits/Logout im Avatar-Menü; Mobile-Burger mit Drawer - Öffentliche /server-Seite: Live-Status-Karten mit Spieler-Balken, Map, Connect-Button und 24h-Spielerzahl-Chart (SVG); Monitor sampelt jede Abfrage in player_history (7 Tage Retention), GET /api/servers liefert nur öffentliche Felder (keine Query-URLs/Hosts) - Lightbox: Galerie- und Devlog-Bilder öffnen im Overlay mit Pfeiltasten, Buttons, Wisch-Gesten und Zähler statt neuem Tab - Skeleton-Loader: schimmernde Platzhalter auf Devlogs, Roadmap, Galerie, Level, Changelog und Server statt "Lade …" - Team-Audit-Log: audit_log-Tabelle (max 500 Einträge), Logging bei Settings/Composer/Vorlagen/Rollen-Menüs/Gameservern/Team/API-Keys/ Branding, GET /api/auditlog (Owner) + Liste im Team-Tab - Giveaway: 🔁 Reroll-Button (Admin-only) + 👥 Teilnehmerliste am Gewinner-Post Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -14,7 +14,9 @@ import {
|
||||
saveWebAdmin, webAdminScopes, listWebAdmins, deleteWebAdmin,
|
||||
saveTemplate, listTemplates, deleteTemplate,
|
||||
getLevelRow, isPlaytester, saveWish, toggleWishVote, myWishVotes,
|
||||
logAudit, listAudit, playerHistory,
|
||||
} from '../db.js';
|
||||
import { lastResults, GAME_ICONS } from '../bot/server-monitor.js';
|
||||
import { publishAppForm, unpublishAppForm, MAX_QUESTIONS } from '../bot/app-forms.js';
|
||||
import { sanitizeEmbed } from './api-v1.js';
|
||||
import { publishRoleMenu, unpublishRoleMenu, MAX_ENTRIES } from '../bot/role-menus.js';
|
||||
@@ -122,6 +124,29 @@ export function registerApiRoutes(app, client) {
|
||||
});
|
||||
app.get('/api/heatmap', async () => ({ days: commitHeatmap() }));
|
||||
|
||||
// Öffentliche Server-Status-Seite: letzte Query-Ergebnisse + 24h-Verlauf
|
||||
// (bewusst ohne query_url/host/port — nur was Besucher sehen dürfen)
|
||||
app.get('/api/servers', async () => ({
|
||||
servers: listGameservers().map((s) => {
|
||||
const r = lastResults.get(s.id);
|
||||
return {
|
||||
id: s.id,
|
||||
name: s.name,
|
||||
type: s.type,
|
||||
icon: GAME_ICONS[s.type] ?? '🎮',
|
||||
address: s.address || null,
|
||||
connect_url: s.connect_url || null,
|
||||
online: r?.online ?? null, // null = noch nie gecheckt
|
||||
players: r?.players ?? null,
|
||||
max: r?.max ?? null,
|
||||
map: r?.map ?? null,
|
||||
ping: r?.ping ?? null,
|
||||
checkedAt: r?.checkedAt ?? null,
|
||||
history: playerHistory(s.id, 24),
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
||||
// Level-Bestenliste + Server-Aktivität — öffentlich
|
||||
app.get('/api/levels', async () => ({ levels: topLevels(50) }));
|
||||
app.get('/api/serverstats', async () => ({
|
||||
@@ -515,6 +540,7 @@ ${rssItems}
|
||||
}
|
||||
|
||||
request.log.info('Settings per Web-UI aktualisiert');
|
||||
logAudit(getSessionUser(request), 'settings', Object.keys(body).join(', ').slice(0, 300));
|
||||
return { ok: true, settings: currentSettings() };
|
||||
});
|
||||
|
||||
@@ -581,6 +607,7 @@ ${rssItems}
|
||||
if (!menu) return;
|
||||
const id = createRoleMenu(menu);
|
||||
request.log.info(`Rollen-Menü ${id} erstellt`);
|
||||
logAudit(getSessionUser(request), 'rollen-menü erstellt', menu.title);
|
||||
return { ok: true, menu: menuToJson(getRoleMenu(id)) };
|
||||
});
|
||||
|
||||
@@ -601,6 +628,7 @@ ${rssItems}
|
||||
request.log.warn(`Menü ${id}: Discord-Update fehlgeschlagen: ${error.message}`);
|
||||
}
|
||||
}
|
||||
logAudit(getSessionUser(request), 'rollen-menü geändert', menu.title);
|
||||
return { ok: true, published, menu: menuToJson(getRoleMenu(id)) };
|
||||
});
|
||||
|
||||
@@ -622,6 +650,7 @@ ${rssItems}
|
||||
await unpublishRoleMenu(client, menu);
|
||||
deleteRoleMenu(menu.id);
|
||||
request.log.info(`Rollen-Menü ${menu.id} gelöscht`);
|
||||
logAudit(getSessionUser(request), 'rollen-menü gelöscht', menu.title);
|
||||
return { deleted: true };
|
||||
});
|
||||
|
||||
@@ -802,6 +831,7 @@ ${rssItems}
|
||||
const server = parseServerBody(request, reply);
|
||||
if (!server) return;
|
||||
const id = createGameserver(server);
|
||||
logAudit(getSessionUser(request), 'server hinzugefügt', server.name);
|
||||
return { ok: true, server: getGameserver(id) };
|
||||
});
|
||||
|
||||
@@ -812,6 +842,7 @@ ${rssItems}
|
||||
const server = parseServerBody(request, reply);
|
||||
if (!server) return;
|
||||
updateGameserver({ ...server, id });
|
||||
logAudit(getSessionUser(request), 'server geändert', server.name);
|
||||
return { ok: true, server: getGameserver(id) };
|
||||
});
|
||||
|
||||
@@ -827,6 +858,7 @@ ${rssItems}
|
||||
await message?.delete().catch(() => {});
|
||||
}
|
||||
deleteGameserver(server.id);
|
||||
logAudit(getSessionUser(request), 'server gelöscht', server.name);
|
||||
return { deleted: true };
|
||||
});
|
||||
|
||||
@@ -964,11 +996,13 @@ ${rssItems}
|
||||
}
|
||||
await message.edit(payload);
|
||||
request.log.info(`Composer: Nachricht ${messageId} bearbeitet`);
|
||||
logAudit(getSessionUser(request), 'composer bearbeitet', `#${channel.name ?? channelId}`);
|
||||
return { ok: true, edited: true, message_id: String(messageId) };
|
||||
}
|
||||
|
||||
const message = await channel.send(payload);
|
||||
request.log.info(`Composer: Nachricht ${message.id} gesendet`);
|
||||
logAudit(getSessionUser(request), 'composer gesendet', `#${channel.name ?? channelId}`);
|
||||
return { ok: true, edited: false, message_id: message.id };
|
||||
});
|
||||
|
||||
@@ -992,6 +1026,7 @@ ${rssItems}
|
||||
if (target === 'avatar') await client.user.setAvatar(buffer);
|
||||
else await client.user.setBanner(buffer);
|
||||
request.log.info(`Bot-${target} aktualisiert (${(buffer.length / 1024).toFixed(0)} KB)`);
|
||||
logAudit(getSessionUser(request), 'branding', `bot-${target} geändert`);
|
||||
return { ok: true };
|
||||
} catch (error) {
|
||||
request.log.error({ err: error }, `Bot-${target} fehlgeschlagen`);
|
||||
@@ -1019,6 +1054,7 @@ ${rssItems}
|
||||
return reply.code(400).send({ error: 'Vorlage ist leer' });
|
||||
}
|
||||
saveTemplate(name, payload);
|
||||
logAudit(getSessionUser(request), 'vorlage gespeichert', name);
|
||||
return { ok: true, templates: listTemplates() };
|
||||
});
|
||||
|
||||
@@ -1210,14 +1246,22 @@ ${rssItems}
|
||||
}
|
||||
saveWebAdmin(userId, username, scopes);
|
||||
request.log.info(`Team-Mitglied ${username} (${userId}): ${scopes.join(',')}`);
|
||||
logAudit(getSessionUser(request), 'team geändert', `${username}: ${scopes.join(',')}`);
|
||||
return { ok: true, admins: listWebAdmins() };
|
||||
});
|
||||
|
||||
app.delete('/api/webadmins/:id', async (request, reply) => {
|
||||
if (requireAdmin(request, reply)) return;
|
||||
logAudit(getSessionUser(request), 'team entfernt', String(request.params.id));
|
||||
return { deleted: deleteWebAdmin(String(request.params.id)), admins: listWebAdmins() };
|
||||
});
|
||||
|
||||
// Audit-Log: die letzten Team-/Owner-Aktionen im Webinterface (nur Owner)
|
||||
app.get('/api/auditlog', async (request, reply) => {
|
||||
if (requireAdmin(request, reply)) return;
|
||||
return { entries: listAudit(100) };
|
||||
});
|
||||
|
||||
// --- API-Keys (Admin) — für /api/v1/* ---
|
||||
|
||||
const VALID_SCOPES = ['message', 'dm', 'roles', 'read'];
|
||||
@@ -1237,6 +1281,7 @@ ${rssItems}
|
||||
}
|
||||
const { id, key } = createApiKey(name, scopes);
|
||||
request.log.info(`API-Key '${name}' erstellt (Scopes: ${scopes.join(',')})`);
|
||||
logAudit(getSessionUser(request), 'api-key erstellt', name);
|
||||
// Klartext-Key nur in dieser einen Antwort!
|
||||
return { id, key, name, scopes };
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user