Umlaute statt Umschreibungen

Oberflaeche, Fehlermeldungen und Kommentare schrieben durchgehend ae, oe, ue
und ss. Dafuer gab es keinen Grund - die Anwendung ist deutschsprachig und
laeuft durchgehend in UTF-8.

Betroffen waren 55 Dateien, darunter alle sichtbaren Texte: Guthaben-Zaehler,
Import, Nutzerverwaltung, Passwortwechsel und die Zwei-Faktor-Einrichtung.

Der Name einer Migration bleibt im README-Beispiel bewusst bei ASCII, weil
daraus ein Verzeichnisname wird.
This commit is contained in:
2026-09-18 16:57:39 +02:00
parent b9364151b0
commit 724f68644b
49 changed files with 256 additions and 254 deletions
+4 -4
View File
@@ -3,7 +3,7 @@ import { prisma } from '../db.js';
import { env } from '../env.js';
export interface ActionContext {
// Beide null fuer Vorgaenge ohne bekanntes Konto, etwa einen fehlgeschlagenen
// Beide null für Vorgänge ohne bekanntes Konto, etwa einen fehlgeschlagenen
// Login auf einen Benutzernamen, den es gar nicht gibt.
actorId: number | null;
actorName: string | null;
@@ -38,9 +38,9 @@ export async function recordAudit(
}
/**
* Entfernt Audit-Eintraege, die aelter sind als die eingestellte Frist. Die
* Guthaben-Historie bleibt unangetastet - sie ist die Quelle der Wahrheit fuer
* bezahlte Ansprueche und hat eine andere Aufbewahrung als das Protokoll.
* Entfernt Audit-Einträge, die älter sind als die eingestellte Frist. Die
* Guthaben-Historie bleibt unangetastet - sie ist die Quelle der Wahrheit für
* bezahlte Ansprüche und hat eine andere Aufbewahrung als das Protokoll.
*/
export async function cleanupOldAuditEntries(): Promise<number> {
if (env.AUDIT_RETENTION_DAYS === 0) return 0;
+19 -19
View File
@@ -15,13 +15,13 @@ export interface CharacterResult {
character: Character;
balances: Balances;
// Zeigt an, dass ein Topf nach der Buchung im Minus steht. Die Aktion wird
// trotzdem ausgefuehrt, das Ergebnis gehoert aber sichtbar gemacht.
// trotzdem ausgeführt, das Ergebnis gehört aber sichtbar gemacht.
overdrawn: boolean;
}
// Fuer ein Redesign zaehlen nur die beiden Redesign-Toepfe.
// Für ein Redesign zählen nur die beiden Redesign-Töpfe.
const REDESIGN_KINDS = [CreditKind.REDESIGN_PAID, CreditKind.REDESIGN_KULANZ] as const;
// Beim Wiederbeleben eines reworkten CCs darf zusaetzlich CC-Guthaben gezogen werden.
// Beim Wiederbeleben eines reworkten CCs darf zusätzlich CC-Guthaben gezogen werden.
const REVIVE_KINDS = [CreditKind.CC, ...REDESIGN_KINDS] as const;
export type RedesignKind = (typeof REDESIGN_KINDS)[number];
@@ -37,7 +37,7 @@ async function loadCharacter(id: number): Promise<Character> {
return character;
}
/** Neuer CC. Kostet ein CC-Guthaben, Kulanz ist dafuer nicht einsetzbar. */
/** Neuer CC. Kostet ein CC-Guthaben, Kulanz ist dafür nicht einsetzbar. */
export async function createCharacter(
playerId: number,
input: { name: string; loreText?: string | undefined; note?: string | undefined },
@@ -87,8 +87,8 @@ export async function createCharacter(
/**
* Redesign als Lineage: der alte CC geht auf INACTIVE/REWORKED, der neue
* entsteht als eigener Datensatz in derselben Kette. Damit greift spaeter
* automatisch die Regel, dass ein reworkter CC nur bezahlt zurueckkommt.
* entsteht als eigener Datensatz in derselben Kette. Damit greift später
* automatisch die Regel, dass ein reworkter CC nur bezahlt zurückkommt.
*/
export async function redesignCharacter(
characterId: number,
@@ -165,7 +165,7 @@ export async function redesignCharacter(
};
}
/** Deaktivierung ohne Guthaben-Effekt, etwa beim Serverputz wegen Inaktivitaet. */
/** Deaktivierung ohne Guthaben-Effekt, etwa beim Serverputz wegen Inaktivität. */
export async function deactivateCharacter(
characterId: number,
reason: InactiveReason,
@@ -179,7 +179,7 @@ export async function deactivateCharacter(
if (reason === InactiveReason.REWORKED) {
throw badRequest(
'USE_REDESIGN',
'Ein Rework entsteht ueber die Redesign-Aktion, nicht ueber eine Deaktivierung',
'Ein Rework entsteht über die Redesign-Aktion, nicht über eine Deaktivierung',
);
}
@@ -208,9 +208,9 @@ export async function deactivateCharacter(
}
/**
* Gratis-Reaktivierung. Moeglich, solange der CC nicht redesignt wurde. Der
* Grund des Verschwindens spielt dabei keine Rolle - ob Spieler-Inaktivitaet
* oder eine zeitweise entfernte Einheit: der CC existiert unveraendert weiter
* Gratis-Reaktivierung. Möglich, solange der CC nicht redesignt wurde. Der
* Grund des Verschwindens spielt dabei keine Rolle - ob Spieler-Inaktivität
* oder eine zeitweise entfernte Einheit: der CC existiert unverändert weiter
* und kostet deshalb kein Guthaben.
*/
export async function reactivateFree(
@@ -250,7 +250,7 @@ export async function reactivateFree(
}
/**
* Bezahlte Reaktivierung, ausschliesslich fuer redesignte CCs: deren Original
* Bezahlte Reaktivierung, ausschließlich für redesignte CCs: deren Original
* existiert nicht mehr, es muss also neu erschaffen werden. Kostet ein
* Redesign- oder ein CC-Guthaben, der Nutzer entscheidet welches.
*/
@@ -268,11 +268,11 @@ export async function reactivatePaid(
if (existing.inactiveReason !== InactiveReason.REWORKED) {
throw conflict(
'IS_FREE',
'Dieser CC existiert unveraendert weiter und kommt gratis zurueck. Eine Guthaben-Buchung waere hier falsch.',
'Dieser CC existiert unverändert weiter und kommt gratis zurück. Eine Guthaben-Buchung wäre hier falsch.',
);
}
if (!REVIVE_KINDS.includes(useKind)) {
throw badRequest('INVALID_KIND', 'Ungueltige Guthaben-Art fuer eine Reaktivierung');
throw badRequest('INVALID_KIND', 'Ungültige Guthaben-Art für eine Reaktivierung');
}
const character = await prisma.$transaction(async (tx) => {
@@ -341,9 +341,9 @@ export async function updateCharacter(
}
/**
* Nimmt einen irrtuemlich angelegten CC komplett zurueck: Verbrauchsbuchungen
* werden gegengebucht und ein durch das Redesign deaktivierter Vorgaenger
* kehrt in den aktiven Zustand zurueck.
* Nimmt einen irrtümlich angelegten CC komplett zurück: Verbrauchsbuchungen
* werden gegengebucht und ein durch das Redesign deaktivierter Vorgänger
* kehrt in den aktiven Zustand zurück.
*/
export async function revertCharacter(
characterId: number,
@@ -359,7 +359,7 @@ export async function revertCharacter(
if (existing.successors.length > 0) {
throw conflict(
'HAS_SUCCESSORS',
'Dieser CC hat Nachfolger in der Redesign-Kette. Bitte zuerst den juengsten CC zuruecknehmen.',
'Dieser CC hat Nachfolger in der Redesign-Kette. Bitte zuerst den jüngsten CC zurücknehmen.',
);
}
@@ -384,7 +384,7 @@ export async function revertCharacter(
amount: -entry.amount,
source: CreditSource.REVERSAL,
groupId: entry.groupId,
note: `Ruecknahme "${existing.name}": ${reason}`,
note: `Rücknahme "${existing.name}": ${reason}`,
reversalOfId: entry.id,
createdById: ctx.actorId,
},
+9 -9
View File
@@ -8,7 +8,7 @@ export interface Balances {
CC: number;
REDESIGN_PAID: number;
REDESIGN_KULANZ: number;
// Fuer ein Redesign zaehlen beide Redesign-Toepfe zusammen.
// Für ein Redesign zählen beide Redesign-Töpfe zusammen.
redesignTotal: number;
}
@@ -72,8 +72,8 @@ export interface PackageRowParams {
/**
* Baut die Ledger-Zeilen eines gekauften Pakets. Die Regel, dass ein CC-Paket
* immer auch ein bezahltes Redesign enthaelt, steht nur hier, damit manuelle
* Eingabe und Import nicht auseinanderlaufen koennen.
* immer auch ein bezahltes Redesign enthält, steht nur hier, damit manuelle
* Eingabe und Import nicht auseinanderlaufen können.
*/
export function buildPackageRows(
target: 'CC_PACKAGE' | 'REDESIGN_PACKAGE',
@@ -102,7 +102,7 @@ export function buildPackageRows(
/**
* Ein CC-Paket erzeugt immer beide Guthaben: einen CC und ein bezahltes
* Redesign. Beide Zeilen teilen sich eine groupId, damit ein Storno sie
* gemeinsam zurueckholt.
* gemeinsam zurückholt.
*/
export async function bookCcPackage(
playerId: number,
@@ -196,7 +196,7 @@ export async function bookKulanz(
return { groupId, balances: await getBalances(playerId) };
}
/** Freie Korrekturbuchung. Vorbehalten fuer Administratoren. */
/** Freie Korrekturbuchung. Vorbehalten für Administratoren. */
export async function bookAdjustment(
playerId: number,
kind: CreditKind,
@@ -236,9 +236,9 @@ export async function bookAdjustment(
}
/**
* Storniert eine Buchung als Gegenbuchung, nicht als Loeschung: die Historie
* bleibt vollstaendig lesbar. Verbrauchszeilen sind ausgenommen, die werden
* ueber die zugehoerige CC-Aktion zurueckgenommen.
* Storniert eine Buchung als Gegenbuchung, nicht als Löschung: die Historie
* bleibt vollständig lesbar. Verbrauchszeilen sind ausgenommen, die werden
* über die zugehörige CC-Aktion zurückgenommen.
*/
export async function reverseGroup(
ledgerId: number,
@@ -251,7 +251,7 @@ export async function reverseGroup(
if (anchor.source === CreditSource.CONSUME) {
throw conflict(
'CONSUME_NOT_REVERSIBLE',
'Verbrauchsbuchungen werden ueber die zugehoerige CC-Aktion zurueckgenommen, nicht direkt storniert',
'Verbrauchsbuchungen werden über die zugehörige CC-Aktion zurückgenommen, nicht direkt storniert',
);
}
if (anchor.source === CreditSource.REVERSAL) {
+13 -13
View File
@@ -34,7 +34,7 @@ export interface ParsedFile {
const MAX_ROWS = 20000;
// Deutsche Tabellen-Exporte kommen haeufig mit Semikolon statt Komma.
// Deutsche Tabellen-Exporte kommen häufig mit Semikolon statt Komma.
function detectDelimiter(sample: string): string {
const firstLine = sample.split(/\r?\n/, 1)[0] ?? '';
const candidates = [',', ';', '\t', '|'];
@@ -78,7 +78,7 @@ export function parseUpload(filename: string, content: Buffer): ParsedFile {
} catch {
throw badRequest('INVALID_JSON', 'Die JSON-Datei konnte nicht gelesen werden');
}
// Manche Exporte verpacken die Liste in ein Huellobjekt.
// Manche Exporte verpacken die Liste in ein Hüllobjekt.
const list = Array.isArray(data)
? data
: typeof data === 'object' && data !== null
@@ -86,7 +86,7 @@ export function parseUpload(filename: string, content: Buffer): ParsedFile {
: undefined;
if (!Array.isArray(list)) {
throw badRequest('INVALID_JSON', 'Erwartet wird eine Liste von Eintraegen');
throw badRequest('INVALID_JSON', 'Erwartet wird eine Liste von Einträgen');
}
rows = list
.filter((entry): entry is Record<string, unknown> => typeof entry === 'object' && entry !== null)
@@ -107,7 +107,7 @@ export function parseUpload(filename: string, content: Buffer): ParsedFile {
}
}
if (rows.length === 0) throw badRequest('NO_ROWS', 'Die Datei enthaelt keine Datenzeilen');
if (rows.length === 0) throw badRequest('NO_ROWS', 'Die Datei enthält keine Datenzeilen');
if (rows.length > MAX_ROWS) {
throw badRequest('TOO_MANY_ROWS', `Die Datei hat mehr als ${MAX_ROWS} Zeilen`);
}
@@ -165,9 +165,9 @@ function parseDate(value: string | undefined): Date | null {
}
/**
* Dampft die gespeicherten Rohzeilen auf die Spalten ein, die tatsaechlich
* zugeordnet wurden. Ein Shop-Export enthaelt oft deutlich mehr - Mailadresse,
* Rechnungsanschrift, Zahlungsart -, und davon braucht die App nichts. Laeuft,
* Dampft die gespeicherten Rohzeilen auf die Spalten ein, die tatsächlich
* zugeordnet wurden. Ein Shop-Export enthält oft deutlich mehr - Mailadresse,
* Rechnungsanschrift, Zahlungsart -, und davon braucht die App nichts. Läuft,
* sobald ein Import abgeschlossen oder verworfen ist.
*/
async function pruneRawRows(
@@ -204,8 +204,8 @@ async function pruneRawRows(
}
/**
* Rechnet fuer jede Zeile aus, was beim Buchen passieren wuerde. Wird vor der
* Bestaetigung angezeigt und beim Commit unveraendert ausgefuehrt.
* Rechnet für jede Zeile aus, was beim Buchen passieren würde. Wird vor der
* Bestätigung angezeigt und beim Commit unverändert ausgeführt.
*/
export async function applyMapping(importId: number, mapping: ColumnMapping, ctx: ActionContext) {
const record = await prisma.import.findUnique({
@@ -229,7 +229,7 @@ export async function applyMapping(importId: number, mapping: ColumnMapping, ctx
raw: row.raw as Record<string, string>,
}));
// Die Datei kann eine andere Schreibweise liefern als die Datenbank fuehrt.
// Die Datei kann eine andere Schreibweise liefern als die Datenbank führt.
const rawKeys = rawRows.map((r) => (r.raw[mapping.playerKey] ?? '').trim()).filter(Boolean);
const playerKeys = [
...new Set(rawKeys.flatMap((key) => {
@@ -262,7 +262,7 @@ export async function applyMapping(importId: number, mapping: ColumnMapping, ctx
: [],
);
// Faengt Dubletten innerhalb derselben Datei ab.
// Fängt Dubletten innerhalb derselben Datei ab.
const seenInFile = new Set<string>();
const updates: Prisma.PrismaPromise<unknown>[] = [];
const summary: Record<Plan, number> = {
@@ -356,7 +356,7 @@ export async function getImport(importId: number, includeRows = true) {
return record;
}
/** Fuehrt den zuvor angezeigten Plan aus. Alles oder nichts. */
/** Führt den zuvor angezeigten Plan aus. Alles oder nichts. */
export async function commitImport(importId: number, ctx: ActionContext) {
const record = await prisma.import.findUnique({
where: { id: importId },
@@ -375,7 +375,7 @@ export async function commitImport(importId: number, ctx: ActionContext) {
}
const result = await prisma.$transaction(async (tx) => {
// Mehrere Zeilen koennen denselben neuen Spieler betreffen.
// Mehrere Zeilen können denselben neuen Spieler betreffen.
const createdPlayers = new Map<string, number>();
let booked = 0;
+8 -8
View File
@@ -90,7 +90,7 @@ export async function getPlayerDetail(playerId: number) {
/**
* Bringt die Eingabe auf die kanonische 64-Bit-Form. Ohne das legt derselbe
* Spieler zwei Datensaetze an, sobald eine Quelle STEAM_0:... liefert und die
* Spieler zwei Datensätze an, sobald eine Quelle STEAM_0:... liefert und die
* andere die 64-Bit-Zahl.
*/
function canonicalSteamId(input: string | null | undefined): string | null {
@@ -169,10 +169,10 @@ export async function updatePlayer(playerId: number, input: Partial<PlayerInput>
}
/**
* Loescht einen Spieler samt allem, was an ihm haengt. Spuren in den
* Import-Zeilen und in frueheren Audit-Eintraegen werden dabei mit entfernt,
* damit nach der Loeschung wirklich nichts Personenbezogenes uebrig bleibt.
* Die Audit-Eintraege selbst bleiben als Vorgang bestehen, nur ohne Inhalt.
* Löscht einen Spieler samt allem, was an ihm hängt. Spuren in den
* Import-Zeilen und in früheren Audit-Einträgen werden dabei mit entfernt,
* damit nach der Löschung wirklich nichts Personenbezogenes übrig bleibt.
* Die Audit-Einträge selbst bleiben als Vorgang bestehen, nur ohne Inhalt.
*/
export async function deletePlayer(playerId: number, ctx: ActionContext): Promise<void> {
const existing = await prisma.player.findUnique({
@@ -198,7 +198,7 @@ export async function deletePlayer(playerId: number, ctx: ActionContext): Promis
},
});
// Fruehere Audit-Eintraege zu diesem Spieler behalten den Vorgang, nicht den Inhalt.
// Frühere Audit-Einträge zu diesem Spieler behalten den Vorgang, nicht den Inhalt.
await tx.auditLog.updateMany({
where: { targetType: 'player', targetId: String(playerId) },
data: { details: { redacted: true } },
@@ -210,8 +210,8 @@ export async function deletePlayer(playerId: number, ctx: ActionContext): Promis
action: 'player.delete',
targetType: 'player',
targetId: playerId,
// Bewusst ohne Name und SteamID: der Eintrag soll die Loeschung belegen,
// nicht die geloeschten Daten konservieren.
// Bewusst ohne Name und SteamID: der Eintrag soll die Löschung belegen,
// nicht die gelöschten Daten konservieren.
details: {
removedCharacters: existing._count.characters,
removedLedgerRows: existing._count.ledger,
+5 -5
View File
@@ -36,8 +36,8 @@ export interface ScrapeSummary {
const externalIdFor = (packageBoughtId: number) => `gext:${packageBoughtId}`;
/**
* Holt die Profile der uebergebenen Spieler und legt daraus einen Import an,
* der wie ein Datei-Import in der Vorschau bestaetigt wird. Es wird nichts
* Holt die Profile der übergebenen Spieler und legt daraus einen Import an,
* der wie ein Datei-Import in der Vorschau bestätigt wird. Es wird nichts
* automatisch gebucht.
*/
export async function scrapePlayers(
@@ -170,7 +170,7 @@ export async function scrapePlayers(
return summary;
}
// --- Handpruefung der unklaren Eintraege ---
// --- Handprüfung der unklaren Einträge ---
export type Resolution = 'CC_PACKAGE' | 'REDESIGN_PACKAGE' | 'REDESIGN_KULANZ' | 'IGNORE';
@@ -182,7 +182,7 @@ export async function listUnmatched(onlyOpen = true) {
});
}
/** Spieler mit offenen Eintraegen, fuer den Hinweis auf der Uebersicht. */
/** Spieler mit offenen Einträgen, für den Hinweis auf der Uebersicht. */
export async function unmatchedOverview() {
const open = await prisma.unmatchedPackage.findMany({
where: { resolved: false },
@@ -208,7 +208,7 @@ export async function unmatchedOverview() {
}
/**
* Loest einen unklaren Eintrag auf. Wird er als Paket eingestuft, entsteht
* Löst einen unklaren Eintrag auf. Wird er als Paket eingestuft, entsteht
* sofort die passende Buchung - inklusive der Regel, dass ein CC-Paket auch
* ein Redesign mitbringt.
*/