Bisher blieb eine Buchung, wie sie beim Anlegen entstand - auch wenn der Eintrag danach an eine ganz andere Stelle wanderte. Damit stand ein CC, der in eine Kette geschoben wurde, weiter als gekauftes Paket im Konto, obwohl er inzwischen ein Redesign war. Jetzt folgt die Buchung der Position: Das unterste Glied ist der gekaufte CC und verbraucht ein CC-Paket, alles darueber sind Redesigns. Wandert ein Eintrag, wandert seine Bedeutung mit - in beide Richtungen. Wer mitten in eine Kette rutscht, wird zum Redesign und gibt sein CC-Guthaben zurueck; wer an den Anfang kommt oder herausgeloest wird, belegt einen eigenen Platz und kostet wieder ein Paket. Umgebucht wird ueber eine Gegenbuchung, nicht durch Ueberschreiben. Reicht das Guthaben nicht, geht das Konto ins Minus und zeigt es an - wie bei jedem Vorab-CC. Zwischen bezahltem Redesign und Kulanz wird nicht gewechselt: das ist eine Entscheidung, keine Folge der Reihenfolge. Eintraege ohne Buchung bleiben unangetastet. Dabei zaehlt die Buchung dessen, dem die Kette gehoert - der Spieler, der unten steht. Bei einem fremden Gruppen-CC in seiner Kette ist das seine Beitrittsbuchung; die des Erstellers bleibt, denn fuer den ist es sein eigener Platz in seiner eigenen Kette. Damit liess sich der Gruppen-CC ueberhaupt erst einreihen: Der Besitzer-Check fragte nach dem Ersteller, und der ist bei einem Gruppen-CC ein anderer als der, dessen Seite man gerade ordnet. Jetzt entscheidet die Beteiligung - wer an beidem beteiligt war, darf einreihen, ein Unbeteiligter nicht. Fehler beim Ziehen stehen ausserdem neben den Ketten statt am Seitenkopf: wer unten zieht, sieht dort oben nichts.
31 KiB
CC-Manager
Selbst gehostete Web-App zur Verwaltung von Custom Characters (CCs) auf einem SCP-RP-Server. Sie führt pro Spieler ein Konto über gekaufte Ansprüche und deren Verbrauch, damit die Frage „darf der gerade einen neuen CC?" nicht mehr von Hand abgeglichen werden muss.
Das Prinzip
Jeder Spieler hat drei Guthaben-Töpfe:
| Topf | Wird gefüllt durch | Wird verbraucht durch |
|---|---|---|
| CC-Guthaben | jedes CC-Paket (+1) | ein neuer CC |
| Redesign-Guthaben (bezahlt) | jedes CC-Paket (+1, immer inklusive) und jedes Redesign-Paket (+1) | ein Redesign |
| Kulanz-Redesign | manuell eingetragene Kulanz (+1) | ein Redesign |
Zwei Regeln sind fest verdrahtet:
- Ein CC-Paket erzeugt beide Guthaben. Der inklusive Redesign wird automatisch mitgebucht, in derselben Transaktion und mit gemeinsamer Gruppen-ID - ein Storno nimmt beide zurück.
- Der Grund entscheidet über die Rückkehr. Solange ein CC unverändert existiert, kommt er gratis zurück - gleich ob der Spieler inaktiv war oder die Einheit zeitweise entfernt wurde. Wurde er redesignt oder entzogen, kostet die Rückkehr ein Redesign- oder CC-Guthaben. Ist er im Rollenspiel gestorben, entscheidet darüber ausschließlich ein Administrator, mit Begründung.
Guthaben werden als Kontobuch geführt: jede Bewegung ist eine eigene Zeile, der Stand eines Topfes ist deren Summe. Das macht jeden Stand nachvollziehbar und Korrekturen sauber - storniert wird per Gegenbuchung, nie durch Löschen. Ein überzogenes Konto ist erlaubt (kommt vor, wenn ein Redesign vor dem zugehörigen Paket erfasst wird), wird aber rot markiert.
Ein Redesign legt einen neuen CC-Datensatz an und verkettet ihn mit dem alten. So bleibt die Kette „A → B → C" sichtbar und die Reaktivierungsregel greift automatisch richtig: Will man später A zurück, ist A ein redesignter CC und kostet entsprechend.
Warum ein CC inaktiv wurde, lässt sich beim Deaktivieren als Klartext festhalten („Einheit bis März entfernt") - der Grund steht dann an der Karte, statt nur als Kategorie. Vier Gründe stehen zur Wahl:
| Grund | Rückkehr |
|---|---|
| Spieler-Inaktivität | gratis |
| Anderer Grund (z. B. Einheit zeitweise entfernt) | gratis |
| Entzogen | kostet ein Guthaben |
| Im Rollenspiel gestorben | nur durch einen Administrator, mit Begründung |
Ketten: ein Platz, viele Redesigns
Ein CC-Paket ist ein Platz, und jeder Platz hat seine eigene Geschichte: erster CC, Redesign, noch ein Redesign. Auf der Spielerseite steht je Platz eine Spalte, von unten nach oben gelesen - unten der erste CC, darüber seine Redesigns, die Pfeile zeigen in Richtung des jüngsten Eintrags.
Gefunden wird selten alles auf einmal. Ein Redesign taucht Jahre später auf, weil sein Antrag in einem anderen Board lag oder weil der Spieler es für den Beitritt zu einem Gruppen-CC benutzt hat, dessen Antrag jemand anders geschrieben hat. Dann steht in der Kette schon „Redesign 3 → Redesign 5", und Redesign 4 fehlt.
Deshalb lässt sich jeder Eintrag mit der Maus an seinen Platz ziehen. Beim Ziehen erscheinen zwischen den Einträgen Ablagestellen - auch mitten in einer Kette und in jeder anderen Kette desselben Spielers. Ein nachgetragener CC wird also schlicht dorthin gezogen, wo er hingehört. Herauslösen stellt einen Eintrag wieder allein, wenn sich herausstellt, dass er gar nicht in diese Kette gehörte.
Die Stelle in der Kette bestimmt, was ein Eintrag kostet. Das unterste Glied ist der CC, der gekauft wurde - es verbraucht ein CC-Paket. Alles darüber sind seine Redesigns und verbrauchen Redesign-Guthaben. Wandert ein Eintrag, wandert seine Bedeutung mit: Ein eigenständiger CC, den du mitten in eine Kette schiebst, ist von da an ein Redesign, und das CC-Guthaben kommt zurück. Ziehst du einen Eintrag an den Anfang oder löst ihn heraus, belegt er einen eigenen Platz und kostet wieder ein CC-Paket.
Umgebucht wird über eine Gegenbuchung, nicht durch Überschreiben - in der Historie bleibt sichtbar, was passiert ist. Reicht das Guthaben nicht, geht das Konto ins Minus und zeigt das an. Zwischen bezahltem Redesign und Kulanz wird dabei nicht gewechselt: Das ist eine Entscheidung, keine Folge der Reihenfolge, und steht unter Bezahlart. Einträge ganz ohne Buchung bleiben unangetastet.
Eines folgt daraus von selbst: Wer einen Nachfolger hat, wurde durch ihn ersetzt. Anders ist eine Kette nicht zu lesen. Deshalb trägt jedes Glied außer dem obersten den Vermerk Redesignt, und wer ans Ende rückt, verliert ihn wieder - er stammte ja von einem Nachfolger, der jetzt woanders steht. Ein Platz entsteht dabei nicht aus dem Nichts: Wird ein CC dadurch wieder aktiv, muss er gedeckt sein wie jeder andere.
Über Bearbeiten lassen sich Name, Beschreibung und der verknüpfte Forums-Antrag nachtragen - die Thread-Nummer genügt, die volle Adresse geht auch. Über Bezahlart wird umgestellt, ob ein Redesign aus einem gekauften Paket oder aus Kulanz kam; umgebucht wird über eine Gegenbuchung, in der Historie bleibt beides sichtbar.
Bei einem Gruppen-CC gilt das für die eigene Buchung: Wer ihn in seine Kette einreiht, stellt damit seinen eigenen Beitritt um - die Buchung des Erstellers bleibt unberührt, für den ist es sein eigener Platz in seiner eigenen Kette. Einreihen darf, wer an beidem beteiligt war.
Am Fuß einer Kette kann ein CC stehen, der jemand anderem gehört - der Gruppen-CC, aus dem sich der Spieler herausredesignt hat. Er war darin, und das bleibt sichtbar; verschieben oder entscheiden lässt sich daran nichts, denn es ist der Platz eines anderen. Frühere Mitgliedschaften, die zu keinem seiner Plätze führen, stehen darunter unter Früher genutzt.
Verweise an einem CC
An jedem Eintrag hängen beliebig viele Verweise - das Steam-Addon des Erstellers, ein Modell, eine Absprache. Eingetragen wird ein Name und eine Adresse; in der Liste steht dann der Name als Link. Ändern und entfernen geht über denselben Knopf Verweise.
Gruppen-CCs
Manche CCs teilen sich mehrere Spieler. Für jeden gelten dabei dieselben Regeln wie für einen eigenen CC: der Platz kostet ein eigenes Guthaben - entweder aus dem CC-Paket oder über ein Redesign, je nachdem, worüber der Spieler hinzukommt.
CC-Pakete sind Plätze
Ein CC-Paket ist ein Platz für einen gleichzeitig aktiven CC. Zwei Pakete sind zwei Plätze, und so weiter. Ein Redesign tauscht einen CC aus, es schafft keinen zusätzlichen Platz: fünf Millionen Redesigns auf ein Paket bleiben ein gleichzeitig aktiver CC.
Geprüft wird das an einer Stelle, für alle Wege zugleich - erschaffen, redesignen, reaktivieren, zurückholen, einem Gruppen-CC beitreten. Jeder aktive CC braucht eine eigene Buchung im CC-Topf. Wer einen Platz belegen will, der nicht gedeckt ist, bekommt eine Absage mit dem Hinweis, welcher CC dafür weichen müsste.
Ein überzogenes Konto bleibt davon unberührt: Einen CC einzutragen, bevor das Paket verbucht ist, geht weiter - die Buchung ist ja da, nur das Guthaben fehlt noch, und das Minus steht sichtbar auf der Spielerseite. Der Unterschied ist, ob ein Platz gebucht wurde oder aus dem Nichts entsteht.
Über Nutzer aufnehmen auf der Spielerseite kommt jemand dazu; das Guthaben wird von seinem Konto gebucht, nicht vom Konto des Gründers. Wer die Gruppe verlässt, wird über das ✕ an seinem Namen ausgetragen - verbrauchtes Guthaben gibt es dabei nicht zurück, denn bis dahin hat er den CC genutzt. Für eine Fehleingabe ist das Storno der Buchung der richtige Weg.
Welcher Weg hineinführt, macht einen Unterschied:
| Bezahlt über | Was passiert |
|---|---|
| CC-Paket | Ein Platz kommt hinzu. Bestehende CCs des Spielers bleiben unberührt. |
| Redesign (bezahlt oder Kulanz) | Der Platz ersetzt einen eigenen CC, genau wie jedes andere Redesign. Welcher aufgegeben wird, muss angegeben werden. |
Das ist dieselbe Regel wie oben, nur an einem Gruppen-CC: über ein Redesign entsteht kein zusätzlicher Platz. Der aufgegebene CC muss dem Spieler gehören, aktiv sein muss er nicht: auch ein längst stillgelegter CC lässt sich redesignen. Ist er noch aktiv, wird er dabei inaktiv gesetzt (Grund Redesignt); war er es schon nicht mehr, bleibt er, wie er ist. Wer gar keinen eigenen CC hat, kann nicht über ein Redesign beitreten - dafür braucht es ein CC-Paket.
Ausgetretene bleiben als „früher: …" sichtbar, damit nachvollziehbar ist, wer den CC wann genutzt hat. Der letzte verbliebene Nutzer lässt sich nicht austragen - dann wird der CC deaktiviert.
Nachtragen, was längst passiert ist
Beim Nacherfassen kommt heraus, dass ein Spieler schon vor drei Redesigns bei einem fremden Gruppen-CC dabei war. Dafür gilt: An einem stillgelegten CC wird nachgetragen, nicht beigetreten. Der Knopf heißt dort Nutzer nachtragen, und die Guthaben-Angabe ist freigestellt - die Buchung kann längst im Konto stehen. Ein Zeitraum lässt sich mitgeben.
Wer weiter unten in derselben Kette schon steht, kommt auch an einem laufenden CC ohne neue Buchung dazu: Sein Platz wurde einmal bezahlt, und ein Redesign tauscht ihn aus, statt einen zweiten zu verlangen. Genau das tut ein Redesign im laufenden Betrieb auch - es nimmt die Gruppe mit. Für alle anderen bleibt die Angabe Pflicht.
Damit das nicht vier Klicks für vier Kettenglieder bedeutet, trägt ein Aufruf den Spieler ab einem Glied in die ganze weitere Kette ein. Gebucht wird dabei genau einmal, beim Einstieg.
Einzelner Nutzer oder ganze Gruppe
Bei einem CC mit mehreren Nutzern gibt es zwei Ebenen, und die Knöpfe sagen, welche gemeint ist:
| Knopf | Wirkung |
|---|---|
| Anteil redesignen | Dieser Spieler steigt aus und bekommt einen eigenen CC. Die Gruppe läuft unberührt weiter. |
| Gruppe redesignen | Der ganze CC wandert in einen neuen; alle bisherigen Nutzer sind auch dort dabei. |
| Nur diesen Nutzer | Dieser Platz fällt weg - geputzt, entzogen oder im Rollenspiel gestorben. Der CC bleibt bestehen. |
| Deaktivieren | Der ganze CC wird inaktiv, für alle. |
Ein Ausstieg per Redesign ist ein Tausch wie jeder andere: Der Platz in der Gruppe geht weg, der eigene CC kommt an seine Stelle - es kommt keiner hinzu. Wohin jemand gewechselt ist, steht danach bei den früheren Nutzern („jetzt …").
Einzelne Nutzer können unabhängig voneinander ausfallen. Bei einem Trio kann einer im Rollenspiel sterben, während die beiden anderen weiterspielen; ebenso können alle nacheinander wegfallen. Für die Rückkehr gelten dieselben Regeln wie bei einem eigenen CC: geputzt kommt gratis zurück, entzogen kostet ein Guthaben, und über einen Gestorbenen entscheidet ein Administrator mit Begründung. Wer ausgefallen ist, sieht seinen CC weiterhin auf seiner Spielerseite - sonst gäbe es nichts, worüber er zurückkehren könnte.
Ein freiwilliger Austritt ist etwas anderes. Wer über das ✕ ausgetragen wird, hat keinen Anspruch auf Rückkehr; ein Wiedereintritt ist ein neuer Beitritt und kostet ein Guthaben.
Ein Redesign nimmt die Gruppe mit: Wer zum Zeitpunkt des Redesigns dabei war, ist es danach auch. Bezahlt wird es von dem, der es beantragt.
Schnellstart
Vorausgesetzt wird Docker mit Compose-Plugin (oder DockGe).
git clone https://git.harlelux.de/Harlekin/CC-Manager.git
cd CC-Manager
cp .env.example .env
In der .env mindestens DB_PASSWORD und SESSION_SECRET setzen:
openssl rand -base64 48 # für SESSION_SECRET
Dann starten:
docker compose up -d --build
docker compose logs -f app
Beim ersten Start legt die App einen Administrator an. Ist BOOTSTRAP_ADMIN_PASSWORD leer,
erzeugt sie ein Einmal-Passwort und schreibt es einmalig ins Log - dort steht es in der
Ausgabe oben. Beim ersten Login ist ein Passwortwechsel Pflicht; bis dahin sind alle anderen
Seiten gesperrt.
Für DockGe: Repo-Inhalt als Stack anlegen, .env daneben, fertig. Datenbank-Migrationen laufen
bei jedem Containerstart automatisch mit.
HTTPS
Der App-Port wird bewusst nicht nach außen gemappt. Davor gehört ein Reverse Proxy
(Nginx Proxy Manager, Traefik oder Caddy), der HTTPS terminiert und intern auf app:3000
weiterleitet. So ist nur Port 443 offen und die Zertifikate laufen über Let's Encrypt.
Damit der Proxy die App erreicht, hängt der app-Service zusätzlich im Netzwerk des Proxys.
Die compose.yaml erwartet es unter dem Namen nginx_default - so heißt es bei Nginx Proxy
Manager. Heißt es bei dir anders, setz PROXY_NETWORK in der .env:
PROXY_NETWORK=mein-proxy-netz
Das Netzwerk muss existieren, bevor der Stack startet; angelegt wird es vom Proxy. Betreibst du
gar keinen Proxy in Docker, entferne den networks-Block am Ende der compose.yaml samt der
networks-Liste beim app-Service und mappe stattdessen einen Port - dann aber bitte HTTPS
anderweitig davorsetzen.
Zu
$in der.env: Docker Compose deutet$nameals Variable und ersetzt es durch nichts, wenn es die Variable nicht gibt. Ein Passwort mit$kommt dadurch gekürzt in der App an - ohne Fehler, nur mit einer leicht zu übersehenden Warnung. Entweder$vermeiden oder als$$schreiben.
Läuft die App hinter einem Proxy, muss TRUST_PROXY=true gesetzt bleiben - sonst landen im
Audit-Log die Proxy-IPs statt der echten. In Produktion werden Cookies automatisch auf Secure
gesetzt; nur zum Testen ohne TLS lässt sich das über COOKIE_SECURE=false abschalten.
Backups
Der backup-Service im Stack schreibt beim Start und danach alle 24 Stunden einen
pg_dump nach ./backups/ und räumt Dumps älter als 14 Tage weg. Die Daten bilden bezahlte
Ansprüche ab - das Verzeichnis gehört auf eine Sicherung außerhalb des Hosts.
Wiederherstellen:
gunzip -c backups/ccmanager-2025-01-15-0300.sql.gz | \
docker compose exec -T db psql -U ccuser -d ccmanager
Import
Kaufdaten lassen sich als CSV oder JSON hochladen; das Format ist nicht festgelegt. Der Ablauf:
- Datei hochladen - Spalten und Beispielzeilen werden angezeigt.
- Spalten zuordnen (Spieler-Kennung, Produktname, optional Bestell-ID und Datum). Die üblichen Spaltennamen werden vorausgewählt.
- Vorschau prüfen: für jede Zeile steht da, was passieren würde.
- Buchen.
Welcher Produktname welches Guthaben ergibt, steht in der Mapping-Tabelle unter Verwaltung → Produkt-Mappings (Teiltreffer, Groß-/Kleinschreibung egal). Neue Produkte lassen sich dort jederzeit ergänzen, ohne den Code anzufassen.
Wird eine Bestell-ID zugeordnet, bucht ein zweiter Import dieselben Käufe nicht erneut - Dubletten werden sowohl gegen bereits gebuchte Bestellungen als auch innerhalb der Datei erkannt. Ohne Bestell-ID entfällt dieser Schutz; darauf weist das Formular hin.
Kulanz-Redesigns tauchen in keinem Shop-Export auf und werden immer von Hand eingetragen.
GExtension-Abgleich
Auf eine bestimmte Instanz zugeschnitten. Dieser Teil liest Profile von
gextension.senseigaming.deund ist auf deren Seitenaufbau gebaut. Läuft bei euch eine andere GExtension-Instanz, genügtGEXTENSION_BASE_URLin der.env. Nutzt ihr ein anderes Shop- oder Spendensystem, funktioniert der Abgleich nicht - dafür müssteserver/src/lib/gextension.tsangepasst werden. Der Rest der App ist davon unabhängig und läuft auch ohne diesen Teil; Pakete lassen sich immer von Hand oder per Datei-Import eintragen.
Der Abgleich holt die gekauften Pakete eines Spielers direkt aus seinem GExtension-Profil, damit sie nicht abgetippt werden müssen.
Was er tut: Für jeden Spieler, der im CC-Manager erfasst ist und eine SteamID hinterlegt hat, ruft er die öffentliche Profilseite ab und liest die Paket-Liste. Auslösen lässt sich das auf der Übersicht (alle Spieler) oder auf einer Spielerseite (nur dieser eine) - jeweils als Admin.
Was er nicht tut: Er sucht keine neuen Spieler. Wer nicht in der Datenbank steht, wird nie abgefragt, auch wenn er bei GExtension gekauft hat. Neue Käufer legst du von Hand an, danach findet der Abgleich ihre Pakete. Gebucht wird ebenfalls nichts von allein - eindeutige Treffer landen in derselben Vorschau wie der Datei-Import und warten auf Bestätigung.
Erkannt wird anhand von Titel und Bereich: Nur der SCP-RP-Bereich führt CC-Pakete, und ein
Titel mit „Redesign" zählt als Redesign, auch wenn „Custom Character" darin vorkommt.
Stornierte Käufe werden übersprungen. Doppelbuchungen verhindert die Bestell-ID von GExtension.
Ungeklärte Einträge: Klingt ein Titel nach CC oder Redesign, passt aber in kein Muster (etwa eine einmalige Sonderaktion), wird nicht geraten. Der Eintrag landet in einer Handprüfung: sichtbar als Hinweis auf der Übersicht und als Box auf der Spielerseite, wo ein Admin ihn als CC-Paket, gekauftes Redesign, Kulanz oder „nichts davon" einordnet. Bei CC-Paket entsteht automatisch auch das inklusive Redesign-Guthaben.
Verbrauchszähler: Meldet GExtension „CC-Paket oder Redesign genutzt | N Mal", wird das beim Spieler angezeigt, aber nie automatisch verrechnet - es ist eine Lesehilfe zum Abgleich mit dem eigenen Ledger, mehr nicht.
Der Abgleich läuft im Hintergrund, mit Fortschrittsbalken und einem Protokoll, das Profil für Profil mitschreibt - sowohl beim Abgleich aller Spieler als auch beim einzelnen. Er hält eine Sekunde Abstand zwischen den Anfragen und weist sich per User-Agent aus. Weist GExtension eine Anfrage wegen zu vieler Zugriffe ab, wird gewartet und erneut versucht, statt den Spieler zu überspringen; das Tempo bleibt für den Rest des Laufs gedrosselt. Die Seite gehört dem Serverbetreiber, nicht dieser App - ein offizieller Datenzugang wäre der sauberere Weg, falls er zu haben ist.
Forums-Anträge
Ebenfalls auf eine bestimmte Instanz zugeschnitten: gelesen wird ein WoltLab-Forum (Burning Board) unter
senseigaming.de/forum, konfigurierbar überFORUM_BASE_URL. Ein anderes Forensystem bräuchte einen eigenen Parser inserver/src/lib/forum.ts.
Während der GExtension-Abgleich die Käufe liefert, steht der Verbrauch im Forum: Wurde ein Antrag angenommen, abgelehnt, war es ein neuer CC oder ein Redesign? Unter Anträge liest die App die drei CC-Boards aus und sammelt die Threads.
Entschieden wird ausschließlich von Hand. Titel und Board werden bewusst nicht ausgewertet - Threads werden nicht immer zuverlässig verschoben, ein längst genehmigter Antrag kann noch im Antrags-Board liegen. Das aktuelle Board steht deshalb nur als Hinweis an der Zeile.
Pro Antrag wählt ein Admin: angenommen als neuer CC, angenommen als Redesign (mit Auswahl, welcher CC ersetzt wurde und woraus bezahlt wird), angenommen als Reaktivierung (ein inaktiver CC kehrt zurück - gratis, sofern er nicht redesignt wurde), abgelehnt oder kein Antrag. Die beiden angenommenen Fälle lösen exakt dieselben Buchungen aus wie die Knöpfe auf der Spielerseite - es gibt keinen zweiten Buchungsweg mit eigenen Regeln.
Gelesen wird dabei ausdrücklich der Ersteller, nicht der letzte Antwortende. Wer sein Konto gelöscht hat, steht in der Liste ohne Profil-Link da - wer dann einfach den ersten Profil-Link der Zeile nimmt, erwischt den letzten Antwortenden und ordnet den Antrag dem Falschen zu. Ändert sich der Ersteller eines bereits bekannten Threads, wird seine Zuordnung neu bestimmt; eine von Hand gesetzte bleibt dabei stehen.
Aus dem Abgleich kommt nebenbei die Adresse des Forumsprofils mit; auf der Spielerseite führt der Forum-Name dann direkt dorthin. Die SteamID verlinkt entsprechend auf das GExtension-Profil.
Zugeordnet wird über den Forumsnamen des Erstellers gegen das Feld Forum-Name beim Spieler - und über die früheren Konten, die auf der Spielerseite nachgetragen werden können. Wer seinen Steam- oder Forumsaccount gewechselt hat, behält so seine Ansprüche. Steht der Ersteller nirgends, landet der Antrag unter Ohne zugeordneten Spieler: erfasst und durchsuchbar, aber außerhalb der Arbeitsliste, denn ohne Spieler lässt sich nichts buchen. Von dort ordnest du ihn von Hand zu.
Darüber steht Ersteller ohne Spieler: dieselben Anträge, aber nach Ersteller zusammengefasst. Wer dreiundzwanzig Anträge gestellt hat, fällt dort auf, während er zwischen fünfhundert Einzelanträgen untergeht. Ein Klick auf Spieler anlegen öffnet das Formular mit bereits eingetragenem Forum-Namen; danach wandern seine Anträge von selbst in die Arbeitsliste.
Beide Listen lassen sich durchsuchen und wahlweise mit dem neuesten oder dem ältesten Antrag zuerst anzeigen; die untere blättert seitenweise.
Legst du später einen Spieler mit passendem Forum-Namen an - oder trägst den Namen nach, oder ergänzt ein früheres Konto -, wandern seine wartenden Anträge von selbst in die Arbeitsliste. Ein erneuter Abgleich ist dafür nicht nötig. Bereits zugeordnete Anträge bleiben dabei unberührt.
Ist ein Antrag entschieden, hängt sein Thread am entstandenen CC: auf der Spielerseite führt ein Klick auf den CC-Namen direkt zum Forumsbeitrag.
Einen Spieler an einem Stück abarbeiten
Auf der Spielerseite stehen seine Anträge unter Forums-Anträge, ältester zuerst, jeder als Link ins Forum. Offene lassen sich dort direkt entscheiden - dieselbe Entscheidung wie auf der Anträge-Seite, nur ohne den Spieler dort erst suchen zu müssen. Danach steht der Antrag in der Anträge-Liste nicht mehr offen.
Genauso mit den Käufen: Liegen Pakete dieses Spielers noch in einer Import-Vorschau, erscheinen sie unter Offene Pakete aus dem Abgleich und können einzeln oder am Stück gebucht werden. Was hier gebucht ist, steht in keiner Vorschau mehr zur Buchung an - auch nicht, wenn dieselbe Bestellung in einem zweiten Import liegt. Wer ein Paket gar nicht buchen will, legt es weg; es verschwindet dann aus der Vorschau, ohne das Konto zu berühren.
Angepinnte Ankündigungen (Changelogs, Regelwerke) liegen mit in den Boards. Sie werden über ihre Labels Wichtig und Information aussortiert. Der Titel wird dagegen nie ausgewertet: CC-Anträge heißen mal „CC Antrag | Name", mal „"The Duke" Rang Update" - daran ist nichts zu erkennen.
Trägst du einen Antrag von Hand ein - über Neuer CC oder Redesign auf der Spielerseite - kannst du dort den Thread-Link angeben. Der Antrag gilt damit als erledigt und taucht beim nächsten Abgleich nicht erneut auf.
Ein Thread behält seine ID, auch wenn er zwischen den Boards verschoben wird - daran hängt die Wiedererkennung. Wandert ein bereits entschiedener Antrag später in ein anderes Board, etwa ins Archiv, erscheint das oben als Hinweis. Es ändert nie automatisch den CC-Status: dafür ist das Signal zu unzuverlässig.
Was von allein läuft
Beide Abgleiche laufen nach Zeitplan, ohne dass jemand einen Knopf drücken muss. Eingestellt wird das unter Verwaltung → Zeitplan, je Lauf einzeln:
| Einstellung | Bedeutung |
|---|---|
| läuft automatisch | Aus heißt: nur noch von Hand, über den Knopf auf der jeweiligen Seite. |
| Abstand | In Minuten, mindestens 15 und höchstens eine Woche. Daneben steht, was das in Stunden bedeutet. |
| Ruhende Konten | Nur beim Käufe-Abgleich: der seltenere Takt für Spieler, bei denen lange nichts gekauft wurde. |
| Ruhend ab | Nach wie vielen Tagen ohne Kauf jemand als ruhend gilt. |
| Frühestens ab | Nur beim Forumslauf: eine Stunde, vor der nicht gelaufen wird. Mit einem täglichen Abstand liegt der Lauf damit verlässlich nachts. |
Ab Werk: Käufe alle zwei Stunden, ruhende Konten alle sechs, ruhend nach drei Jahren; das Forum einmal täglich, frühestens ab vier Uhr.
Der seltenere Takt für ruhende Konten spart den Großteil der Anfragen: Wer seit Jahren nichts kauft, fängt selten von einer Stunde auf die andere damit an, und es sind mit der Zeit die meisten Spieler. Maßgeblich ist das Kaufdatum aus dem Shop, nicht der Zeitpunkt der Eingabe hier.
Gebucht wird dabei nichts von allein. Neue Pakete landen in der Import-Vorschau, neue Anträge in der Liste - entschieden wird weiterhin von Hand. Auf der Übersicht steht, wie viele Pakete auf eine Buchung warten und bei wem.
Darunter steht, wann zuletzt gelaufen wurde und was dabei herauskam. Änderungen greifen sofort, ohne Neustart; der Stand der Läufe steht in der Datenbank, ein Neustart stößt also nicht alles erneut an.
Wie lange ein Abgleich dauert
Das Archiv allein umfasst über zwanzig Seiten. Zwischen den Abrufen liegt gut eine Sekunde, ein voller Lauf braucht also rund eine halbe Minute. Bremst das Forum den Zugriff aus, wird die angegebene Wartezeit eingehalten, der Abstand für den Rest des Laufs verdoppelt und die Seite erneut versucht - der Lauf dauert dann länger, liest aber trotzdem alles.
Deshalb läuft der Abgleich im Hintergrund, mit einem Fortschrittsbalken und einem Protokoll, das Seite für Seite mitschreibt. Gelesen wird zuerst von jedem Board die erste Seite - erst danach steht der Umfang fest, und der Balken zeigt einen ehrlichen Anteil statt einer Schätzung. Der Lauf geht weiter, auch wenn du die Seite verlässt. Bricht ein Board doch ab, bleibt erhalten, was bis dahin gelesen wurde; die Meldung nennt das Board und die Seite, bis zu der es gereicht hat.
Die Spielerliste eingrenzen
Über der Tabelle stehen zwei Auswahlfelder. Eingegrenzt wird auf Spieler mit aktivem CC, auf solche mit ausschließlich stillgelegten, auf die Beteiligten eines Gruppen-CCs, auf Spieler ganz ohne CC oder auf die offenen Problemfälle. Sortiert wird nach Namen, nach der Zahl der CC-Einträge oder nach dem Zeitpunkt der Anlage.
Maßgeblich ist überall die Mitgliedschaft, nicht wer den CC angelegt hat: Wer einem Gruppen-CC angehört, zählt dazu, auch ohne ihn erstellt zu haben.
Problemfälle
Nicht jeder Fall lässt sich beim Eintragen sauber klären: ein CC-Antrag, zu dem sich kein Paket finden lässt. Ein Spieler ohne GExtension-Eintrag. Angaben, die einander widersprechen.
Solche Spieler werden trotzdem angelegt und dabei als Problemfall vorgemerkt, mit einer Notiz, was offen ist. Der Vermerk lässt sich auch nachträglich setzen, über Als Problem merken auf der Spielerseite, und ebenso wieder abräumen.
Die Markierung ist bewusst folgenlos: an Guthaben, CCs und Buchungen ändert sie nichts, sie sperrt nichts und verhindert nichts. Sie ist ein Merkzettel mit Griff. Der Gedanke dahinter: geklärt wird, wenn es darauf ankommt - und wenn der Spieler nie wiederkommt, kommt es nie darauf an. Bis dahin geht die Spur wenigstens nicht verloren.
Auf der Übersicht tragen betroffene Spieler ein Kennzeichen, und über Nur offene Problemfälle lässt sich die Liste darauf eingrenzen.
Umzug auf einen anderen Server
Unter Import steht für Administratoren Alles sichern und zurückholen: der gesamte Bestand als eine einzelne Tabelle - Spieler, Guthaben, CCs samt Ketten, frühere Konten, Mitgliedschaften und Verweise. Gedacht für den Wechsel des Servers oder den Umstieg auf ein anderes Programm.
Weil verschiedene Satzarten in einer Datei stehen, sagt die erste Spalte satzart, worum es in
der Zeile geht (SPIELER, KONTO, GUTHABEN, CC, MITGLIED, VERWEIS); die übrigen Spalten
bedeuten je nach Satzart etwas anderes. Der Spieler steht in jeder Zeile, damit jede Zeile für
sich verständlich bleibt und eine Umsortierung in der Tabelle den Bestand nicht zerreißt.
Beim Einspielen wird ergänzt, nicht überschrieben: Was es schon gibt, bleibt unangetastet. Erkannt wird ein Spieler an seiner SteamID, ersatzweise am Anzeigenamen; eine Buchung an Spieler, Art, Menge, Anlass, Zeitpunkt und Notiz. Zweimal dieselbe Datei einzuspielen ändert deshalb nichts. Zeilen, die sich nicht zuordnen lassen, werden mit Zeilennummer gemeldet, ohne den Lauf abzubrechen - eine krumme Zeile darf nicht neunhundert gute aufhalten.
Rollen
Es gibt keine öffentliche Registrierung. Konten legt ausschließlich ein Administrator an.
Der Zugang aus der Erstinstallation ist geschützt: Weder seine Rolle noch sein Zustand
lassen sich ändern, sein Passwort nicht von außen zurücksetzen, und löschen lässt er sich nicht -
auch nicht durch sich selbst. Damit bleibt ein Weg in die Anwendung offen, wenn sich jemand aus
Versehen die Rechte entzieht oder ein Konto abhandenkommt. Sein eigenes Passwort ändert er
weiterhin selbst, unter Konto. Wer das nicht will, setzt BOOTSTRAP_ADMIN_EDITABLE=true; dann
gilt er wie jeder andere Zugang.
- Viewer - darf nur lesen.
- Editor - Spieler anlegen und bearbeiten, gekaufte Pakete eintragen, CCs erstellen, redesignen und reaktivieren, Datei-Import, GExtension- und Forums-Abgleich samt Entscheidung über Anträge.
- Admin - zusätzlich Kulanz vergeben, Korrekturbuchungen, unklare Pakete auflösen, Spieler löschen, Nutzerverwaltung, Produkt-Mappings und Audit-Log.
Die Grenze verläuft zwischen erfassen und gewähren: Wer einen Kauf einträgt, hält fest, was ohnehin passiert ist. Kulanz dagegen ist eine Entscheidung des Teams und bleibt Administratoren vorbehalten - ebenso das Auflösen unklarer Pakete, weil dabei Kulanz vergeben werden kann.
Jede ändernde Aktion landet im Audit-Log. Für Administratoren lässt sich unter Mein Konto eine Zwei-Faktor-Anmeldung per TOTP einrichten.
Entwicklung
Gebraucht werden Node 20+ und eine erreichbare PostgreSQL-Instanz.
npm install
cp server/.env.example server/.env # DATABASE_URL auf die lokale DB zeigen lassen
npm run migrate:dev
npm run dev # API auf :3000, Frontend auf :5173
Das Frontend leitet /api im Entwicklungsbetrieb an den API-Port weiter, Anmeldung und Cookies
funktionieren also wie in Produktion.
Die Tests brauchen keine laufende Datenbank - sie starten eine Postgres-Instanz im Speicher:
npm test
Abgedeckt sind die Guthaben-Regeln, der CC-Lebenszyklus samt Reaktivierungslogik, Korrekturen und Stornos, Anmeldung mit CSRF- und Rollenprüfung sowie der Import mit Dedup.
Aufbau
server/
prisma/schema.prisma Datenmodell und Migrationen
src/services/ Fachlogik: Ledger, CC-Lebenszyklus, Import, Abgleich
src/routes/ HTTP-Endpunkte
src/auth/ Passwörter, Sitzungen, CSRF, Rollen, TOTP
src/lib/ SteamID-Umrechnung, GExtension-Parser
test/ Integrationstests gegen eine echte Postgres-Instanz
web/
src/pages/ Login, Übersicht, Spieler-Detail, Import, Verwaltung
src/components/ Wiederverwendete Bausteine
Backend ist Fastify mit Prisma, Frontend eine React-SPA. Gebaut wird beides in ein einziges Image: Fastify liefert die API und die fertigen Frontend-Dateien aus.
Nach Änderungen am Datenmodell:
npm run migrate:dev -- --name kurze-beschreibung
Der Name wird zum Verzeichnisnamen der Migration - dort also besser bei ASCII bleiben.
Sicherheit
- Passwörter mit Argon2id gehasht, Mindestlänge 12 Zeichen.
- Sitzungen serverseitig; im Cookie steht nur ein Token, in der Datenbank dessen Hash.
Cookie ist
httpOnly,SameSite=Laxund in ProduktionSecure. - CSRF-Schutz für alle ändernden Anfragen über ein aus der Sitzung abgeleitetes Token.
- Alle Eingaben werden validiert, alle Datenbankzugriffe laufen parametrisiert über das ORM.
- Rate-Limit auf dem Login, generische Fehlermeldung ohne Hinweis auf existierende Konten.
- Sicherheits-Header inklusive Content-Security-Policy.
- Ein gesperrtes Konto verliert seine offenen Sitzungen sofort; der letzte aktive Administrator kann sich nicht selbst entfernen.
Lizenz
GPL-3.0-or-later, siehe LICENSE.