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:
1 parent
56d2078e6a
commit
e66f89f49e
214 files changed
+27077
No files matched your search
@@ -0,0 +1,45 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { advancementsFileToEvents, statsFileToEvent } from '../src/parsers/vanilla.ts';
|
||||
|
||||
const fx = (p: string) => readFileSync(join(import.meta.dirname, 'fixtures', p), 'utf8');
|
||||
const ALICE = '308a693c-2bd8-4d6b-8790-670aef5be601';
|
||||
|
||||
describe('statsFileToEvent', () => {
|
||||
it('stats file maps play_time', () => {
|
||||
const e = statsFileToEvent(ALICE, fx(`stats/${ALICE}.json`), 1760300000000);
|
||||
expect(e.type).toBe('stats_snapshot');
|
||||
if (e.type !== 'stats_snapshot') return;
|
||||
expect(e.full).toBe(true);
|
||||
expect(e.ts).toBe(1760300000000);
|
||||
expect(e.name).toBeNull();
|
||||
expect(e.stats['minecraft:custom']?.['minecraft:play_time']).toBe(1175736);
|
||||
expect(e.stats['minecraft:mined']?.['mekanism:enrichment_chamber']).toBe(2);
|
||||
});
|
||||
|
||||
it('id stable for same content', () => {
|
||||
const a = statsFileToEvent(ALICE, fx(`stats/${ALICE}.json`), 1);
|
||||
const b = statsFileToEvent(ALICE, fx(`stats/${ALICE}.json`), 1);
|
||||
const c = statsFileToEvent(ALICE, fx(`stats/${ALICE}.json`), 2);
|
||||
expect(a.id).toBe(b.id);
|
||||
expect(a.id).not.toBe(c.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('advancementsFileToEvents', () => {
|
||||
it('emits done, non-recipe advancements with earliest criterion time', () => {
|
||||
const events = advancementsFileToEvents(ALICE, fx(`advancements/${ALICE}.json`));
|
||||
const ids = events.map((e) => (e.type === 'advancement' ? e.advancementId : ''));
|
||||
expect(ids.sort()).toEqual(['minecraft:story/enter_the_nether', 'minecraft:story/root']);
|
||||
const nether = events.find((e) => e.type === 'advancement' && e.advancementId === 'minecraft:story/enter_the_nether')!;
|
||||
expect(nether.ts).toBe(Date.parse('2026-10-12T12:42:23+02:00'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('advancement names', () => {
|
||||
it('file-derived advancements carry no name so they never overwrite the player name', () => {
|
||||
const events = advancementsFileToEvents(ALICE, fx(`advancements/${ALICE}.json`));
|
||||
expect(events.every((e) => e.type === 'advancement' && e.name === null)).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user