feat: ATM10-Statistikportal

Statistikportal für den All-the-Mods-10-Server (Minecraft 1.21.1, NeoForge 21.1):

- mod/: serverseitige NeoForge-Mod atmstats – Ereignisse, Live-Zähler, Servergesundheit,
  FTB-Dateien; Outbox auf der Festplatte, signierter gzip-Versand per HTTPS mit Backoff
- backend/: Fastify + PostgreSQL – idempotenter Ingest, Projektionen, Parser für SNBT,
  FTB Quests/Teams, world/stats, world/advancements und latest.log; Ranglisten, Snapshots,
  Retention, öffentliche API, SSE-Live-Updates, Admin mit argon2/CSRF, SFTP-Worker
- web/: Next.js-Dashboard – Übersicht, Ranglisten, Spielerprofile, ATM10, Server,
  Datenquellen, Verwaltung
- shared/: Protokoll v1 und API-Typen; tools/: Simulator, Log-Anonymisierer, RCON-Client
- deploy/: Docker Compose hinter Traefik, Backup; docs/: Architektur, Datenquellen, Betrieb

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
lucashahmannandClaude Opus 5.5 committed 2026-10-13 09:33:25 +02:00
1 parent 56d2078e6a
commit e66f89f49e
214 files changed
+27077

No files matched your search

+59
View File
@@ -0,0 +1,59 @@
// Verwaltungs-CLI:
// cli admin:create <benutzer> Passwort aus ADMIN_PASSWORD oder interaktiv
// cli token:create neues Ingest-Token (wird nur einmal angezeigt)
// cli migrate Datenbankmigrationen ausführen
import { createInterface } from 'node:readline/promises';
import { createAdmin } from './auth/admin.ts';
import { createPool } from './db/pool.ts';
import { migrate } from './db/migrate.ts';
import { createIngestToken } from './ingest/tokens.ts';
async function readPassword(): Promise<string> {
if (process.env.ADMIN_PASSWORD) return process.env.ADMIN_PASSWORD;
const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: true });
// Hide typed characters.
(rl as unknown as { _writeToOutput: (s: string) => void })._writeToOutput = (s: string) => {
if (s.includes('Passwort')) process.stdout.write(s);
};
const pw = await rl.question('Passwort (min. 12 Zeichen): ');
rl.close();
process.stdout.write('\n');
return pw;
}
async function main() {
const [command, arg] = process.argv.slice(2);
const url = process.env.DATABASE_URL;
if (!url) throw new Error('DATABASE_URL is required');
const pool = createPool(url, 2);
try {
await migrate(pool);
switch (command) {
case 'migrate':
console.log('Migrationen ausgeführt.');
break;
case 'admin:create': {
if (!arg) throw new Error('usage: admin:create <benutzer>');
await createAdmin(pool, arg, await readPassword());
console.log(`Admin "${arg}" angelegt bzw. Passwort aktualisiert.`);
break;
}
case 'token:create': {
const token = await createIngestToken(pool);
console.log('Neues Ingest-Token (in config/atmstats-server.toml als token eintragen):');
console.log(token);
break;
}
default:
console.log('Befehle: migrate | admin:create <benutzer> | token:create');
process.exitCode = 1;
}
} finally {
await pool.end();
}
}
main().catch((err) => {
console.error((err as Error).message);
process.exit(1);
});