Sechs Ruecklaeufer aus dem ersten Praxistest:
Das Archiv reicht ueber 25 Seiten und enthaelt vieles, was kein CC-Antrag ist.
Aufgenommen werden jetzt nur Threads von Spielern, die im CC-Manager mit
passendem Forum-Namen erfasst sind. Angepinnte Ankuendigungen tragen die
Labels "Wichtig" oder "Information" und fallen darueber raus.
Der Titel wird bewusst nirgends ausgewertet - weder zum Erkennen noch zum
Aussortieren. Antraege heissen mal "CC Antrag | Name", mal "The Duke Rang
Update"; daran ist nichts abzulesen.
Eine Reaktivierung laeuft ebenfalls ueber einen Forums-Antrag und fehlte als
Entscheidung. Sie ist jetzt waehlbar und kostet nichts, solange der CC nicht
redesignt wurde - die bestehende Regel entscheidet das, nicht der Dialog.
Der Titel in der Liste verlinkt jetzt auf den Thread im Forum. Die Labels
stehen als Kontext daneben, ohne die Entscheidung vorwegzunehmen.
Wer einen Antrag von Hand eintraegt, kann beim Anlegen oder Redesignen den
Thread-Link angeben. Der Antrag gilt damit als erledigt und taucht nicht
erneut als offen auf.
Manuelle Guthaben-Buchungen fragen vorher nach: bisher buchte ein einzelner
Klick sofort, ohne zu sagen, was genau passiert. Der Dialog nennt die Wirkung
und nimmt eine Notiz entgegen.
Die erste Migration wurde nachtraeglich mehrfach erweitert, statt Aenderungen
anzuhaengen. Auf einer bestehenden Installation gilt sie als angewendet und
wird uebersprungen - die seither hinzugekommenen Tabellen fehlten dort also
schlicht, ohne dass beim Start etwas auffiel. Die Antragsseite lief dadurch
in einen internen Fehler.
Die neue Migration ergaenzt alles Fehlende und ist durchgehend mit
IF NOT EXISTS geschrieben, weil bestehende Datenbanken je nach Zeitpunkt der
Einrichtung unterschiedliche Staende haben. Geprueft gegen eine frische
Datenbank, gegen eine auf altem Stand und auf mehrfaches Anwenden.
Der Testharness wendet jetzt alle Migrationen der Reihe nach an statt nur der
ersten - sonst faellt so etwas beim naechsten Mal wieder nicht auf.
Ausserdem scheiterte das Buchen komplett, wenn eine Bestellung zwischenzeitlich
ueber einen anderen Import gebucht worden war: die eindeutige Bestell-ID kippte
die gesamte Transaktion. Solche Zeilen werden jetzt uebersprungen und gezaehlt,
die uebrigen laufen durch.
Dazu fehlten in der Oberflaeche die Wege zum Bearbeiten und Loeschen eines
Spielers, obwohl es die Endpunkte gab. Beides ist jetzt auf der Spielerseite
erreichbar; das Loeschen verlangt den Namen als Bestaetigung und benennt
vorher, was mit verschwindet.
Der GExtension-Abgleich liefert die Käufe, der Verbrauch steht im Forum. Die
App liest jetzt die drei CC-Boards und sammelt die Anträge.
Gelesen wird nur die Board-Übersicht: sie führt bereits den Ersteller, nicht
nur den letzten Antwortenden. Damit bleibt ein Lauf bei wenigen Anfragen statt
einer je Thread. Unterforen und globale Kästen tragen ebenfalls Thread-Links,
deshalb liest der Parser strikt entlang der Listeneinträge - sonst landen
Changelogs und Bewerbungen in der Antragsliste.
Klassifiziert wird ausschließlich von Hand. Weder Titel noch Board sind
verlässlich genug: Threads werden nicht immer verschoben, ein genehmigter
Antrag kann weiter im Antrags-Board liegen. Das Board steht deshalb nur als
Hinweis an der Zeile.
Angenommene Anträge rufen dieselben Aktionen wie die Knöpfe auf der
Spielerseite, es gibt keinen zweiten Buchungsweg mit eigenen Regeln. Wandert
ein bereits entschiedener Thread später in ein anderes Board, erscheint das
als Hinweis zum Abhaken - der CC-Status ändert sich nie von allein.
Die Thread-ID bleibt beim Verschieben stabil und traegt deshalb die
Wiedererkennung; die Board-Historie wird mitgeschrieben. Zurückgelassene
Verweise erkennt der Parser und zählt sie nicht als eigenen Antrag.
Der Abgleich bucht bewusst nichts, sondern legt eine Vorschau zur Bestaetigung
an. Nur fuehrte von dort kein Weg weiter: die Import-Seite zeigte allein den
Datei-Upload, und die Rueckmeldung sagte lediglich "abgeglichen". Die
gefundenen Pakete lagen damit unsichtbar herum und der Knopf wirkte, als tue
er nichts.
Die Import-Seite listet jetzt alle Importe, die noch auf Bestaetigung warten,
und oeffnet sie in derselben Vorschau wie einen frischen Upload. Die Meldung
nach dem Abgleich nennt die Zahl der Funde und den naechsten Schritt.
Ausserdem beruecksichtigt der Abgleich jetzt offene Vorschauen: bisher pruefte
er nur gegen bereits gebuchte Pakete. Ein zweiter Lauf legte dieselben Zeilen
deshalb ein weiteres Mal an, und beim Buchen kollidierten beide Importe an der
eindeutigen Bestell-ID - der zweite schlug komplett fehl.
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.
Die maxAge-Option von fastify-static setzt den Cache-Header fuer jede Datei,
auch fuer die index.html. Der setHeaders-Callback, der das fuer HTML wieder
aufheben sollte, kam dagegen nicht an: ausgeliefert wurde durchgehend
public, max-age=31536000.
Damit bekam ein Browser nach einem Update weiter die alte Oberflaeche - ein
Jahr lang, ohne Fehlermeldung, obwohl der Server laengst neue Dateien hatte.
Genau so ist der Abgleich-Knopf nach dem Deploy unsichtbar geblieben.
Jetzt setzt die App beide Faelle selbst: HTML immer neu holen, Assets
unbegrenzt behalten. Deren Dateinamen enthalten einen Inhalts-Hash, sie
aendern sich bei neuem Inhalt ohnehin.
Drei Tests pruefen die Header, weil ein Fehler an dieser Stelle nirgends
auffaellt - er sieht aus, als waere das Update nicht angekommen.
Der Abgleich liest GExtension-Profile und ist auf deren Seitenaufbau gebaut.
Damit niemand in einer anderen Community lange sucht, warum nichts passiert,
steht das jetzt als Warnhinweis am Anfang des Abschnitts: bei einer anderen
GExtension-Instanz reicht GEXTENSION_BASE_URL, bei einem voellig anderen
Shop-System muesste der Parser angepasst werden. Der Rest der App laeuft
unabhaengig davon.
Die Adresse war bisher fest im Code verdrahtet und kommt jetzt aus der
Umgebung. Der Parser selbst bleibt frei von Konfiguration und Datenbank,
damit er ohne beides testbar ist - die Adresse reicht der Service durch.
Die Profilseiten sind ohne Login abrufbar und betten die Kaeufe als JSON ins
HTML ein. Gelesen wird dieses JSON statt der gerenderten Tabelle: das ist
unempfindlich gegen Layout-Aenderungen und liefert zusaetzlich Kaufdatum,
Betrag und den Storno-Status. Ein Headless-Browser ist nicht noetig.
Der Abgleich bucht nichts von sich aus. Eindeutige Treffer landen als
regulaerer Import in der bestehenden Vorschau, die per package_bought_id
dedupliziert. Titel, die nach CC klingen, sich aber keiner Guthaben-Art
sicher zuordnen lassen, gehen in eine Handpruefung statt geraten zu werden -
sichtbar als Hinweis auf der Uebersicht und als Box beim Spieler.
Beachtet werden dabei: stornierte Kaeufe werden uebersprungen, der Web-
Bereich enthaelt nie CC-Pakete, und ein Redesign-Titel zaehlt als Redesign,
auch wenn Custom Character darin vorkommt. Letzteres war auch in den
Standard-Mappings des Datei-Imports falsch herum einsortiert.
Der Lauf geht bewusst nur ueber bereits erfasste Spieler und haelt eine
Sekunde Abstand zwischen den Anfragen, da die Seite fremde Infrastruktur ist.
Voraussetzung dafuer ist, dass SteamIDs auf eine Schreibweise gebracht
werden: die Legacy-Form, die Steam3-Form und die 64-Bit-Zahl bezeichnen
dieselbe Person, wurden bisher aber als verschiedene Spieler gefuehrt.
Gespeichert wird jetzt immer die 64-Bit-Form, Eingabe und Suche akzeptieren
alle drei.
Drei Befunde aus einer Durchsicht der gespeicherten personenbezogenen Daten:
Der Import legte bisher jede Spalte der hochgeladenen Datei dauerhaft ab,
auch Mailadressen oder Rechnungsdaten, die nie zugeordnet wurden. Nach dem
Buchen oder Verwerfen bleiben jetzt nur noch die gemappten Spalten stehen.
Ein fehlgeschlagener Login protokollierte den eingetippten Benutzernamen.
Wer sein Passwort ins Namensfeld tippt, hatte es damit im Klartext im
Audit-Log. Der Name wird nur noch gespeichert, wenn das Konto existiert.
Das Loeschen eines Spielers liess Spuren in den Import-Zeilen und in den
Details frueherer Audit-Eintraege zurueck. Beides wird jetzt mit abgeraeumt;
die Vorgaenge bleiben als Eintrag bestehen, nur ohne Inhalt.
Dazu eine Aufbewahrungsfrist fuer das Audit-Log (Standard 12 Monate, ueber
AUDIT_RETENTION_DAYS einstellbar). Die Guthaben-Historie ist davon nicht
betroffen - sie bleibt die Quelle der Wahrheit fuer bezahlte Ansprueche.
Ein CC, der nicht redesignt wurde, existiert unveraendert weiter und kommt
deshalb immer gratis zurueck - egal ob der Spieler inaktiv war oder die
Einheit voruebergehend aus dem Spiel genommen wurde. Bisher kostete der Fall
OTHER ein Guthaben, was die Regel unnoetig verkompliziert hat.
Damit der zweite Fall brauchbar wird, haelt inactiveNote den Grund im Klartext
fest ("Einheit bis Maerz entfernt"); die Notiz steht an der CC-Karte und wird
beim Reaktivieren wieder geleert.
Mehrstufiges Image: gebaut wird mit allen Abhaengigkeiten, ausgeliefert wird
nur die Laufzeit. Migrationen laufen beim Containerstart. Der App-Port wird
bewusst nicht nach aussen gemappt - davor gehoert ein Reverse Proxy, der HTTPS
terminiert.
Der Backup-Service schreibt taeglich einen pg_dump und raeumt Dumps aelter als
14 Tage weg.
Die Tests starten Postgres per PGlite und sprechen es ueber einen lokalen Port
an, laufen also ohne Docker und ohne installierte Datenbank.
Abgedeckt sind die Guthaben-Regeln, der CC-Lebenszyklus samt Reaktivierung,
Stornos und Ruecknahmen, Anmeldung mit CSRF- und Rollenpruefung sowie der
Import mit Dedup.
Eingaben werden mit zod validiert, jede aendernde Aktion schreibt ins
Audit-Log. Beim ersten Start entsteht ein Administrator samt Startwerten fuer
die Produkterkennung; solange ein Erstpasswort gilt, bleiben schreibende Wege
bis auf den Passwortwechsel gesperrt.
Nicht-API-Pfade fallen auf die SPA zurueck, damit Deep-Links funktionieren.
Die beiden zentralen Regeln stehen jeweils an genau einer Stelle:
Ein CC-Paket erzeugt immer CC +1 und REDESIGN_PAID +1. Die Zeilen dafuer baut
buildPackageRows, das sowohl die manuelle Eingabe als auch der Import nutzen -
so koennen beide Wege nicht auseinanderlaufen.
Ein CC kommt nur dann gratis zurueck, wenn er wegen Spieler-Inaktivitaet
entfernt wurde. Ein reworkter CC existiert im Original nicht mehr und kostet
deshalb ein Redesign- oder CC-Guthaben.
Ueberzogene Toepfe werden zugelassen statt blockiert; die Aktion meldet das
ueber ein overdrawn-Flag zurueck. Storniert wird per Gegenbuchung, nie durch
Loeschen. Der Import berechnet den Plan vorab und dedupliziert ueber die
Bestell-ID, sowohl gegen bereits gebuchte als auch innerhalb einer Datei.
Passwoerter mit Argon2id, im Cookie steht nur ein Token und in der Datenbank
dessen Hash. Das CSRF-Token wird per HMAC aus dem Sitzungstoken abgeleitet
statt separat gespeichert - ohne das httpOnly-Cookie ist es nicht herstellbar.
TOTP ist optional und wird erst nach einem bestaetigten Code scharf geschaltet,
damit sich niemand bei der Einrichtung aussperrt.
Guthaben werden als Kontobuch gefuehrt: jede Bewegung ist eine Zeile, der
Stand eines Topfes ist deren Summe. Zusammengehoerige Zeilen teilen sich eine
groupId, damit ein CC-Paket als Ganzes storniert werden kann; ein unique
Constraint auf reversalOfId verhindert doppelte Stornos.
CCs bilden ueber lineageId und predecessorId eine Redesign-Kette. Der Grund
einer Deaktivierung entscheidet spaeter ueber die Reaktivierungskosten.
Fastify mit Prisma im Server-Workspace, React mit Vite im Web-Workspace.
Das Frontend baut direkt nach server/public, damit am Ende ein einzelnes
Image API und Oberflaeche ausliefert.