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>
35 lines
1010 B
TypeScript
35 lines
1010 B
TypeScript
// Server components call the API directly (container network); the browser uses same-origin /api.
|
|
const base = typeof window === 'undefined' ? (process.env.API_INTERNAL_URL ?? 'http://127.0.0.1:3001') : '';
|
|
|
|
export class ApiError extends Error {
|
|
constructor(
|
|
readonly status: number,
|
|
path: string,
|
|
) {
|
|
super(`API ${status} for ${path}`);
|
|
}
|
|
}
|
|
|
|
export async function apiGet<T>(path: string): Promise<T> {
|
|
let res: Response;
|
|
try {
|
|
res = await fetch(`${base}${path}`, { cache: 'no-store' });
|
|
} catch {
|
|
throw new ApiError(503, path);
|
|
}
|
|
if (!res.ok) throw new ApiError(res.status, path);
|
|
return (await res.json()) as T;
|
|
}
|
|
|
|
/** Like apiGet, but resolves to null for 404 (unknown player, board …). */
|
|
export async function apiFind<T>(path: string): Promise<T | null> {
|
|
try {
|
|
return await apiGet<T>(path);
|
|
} catch (e) {
|
|
if (e instanceof ApiError && e.status === 404) return null;
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
export const avatarUrl = (uuid: string) => `/api/avatar/${uuid}`;
|