Takt und Schalter der beiden Abgleiche standen in der Umgebung und liessen sich nur mit einem Neustart aendern. Jetzt stehen sie unter Verwaltung -> Zeitplan, je Lauf einzeln: ob er automatisch laeuft, in welchem Abstand, beim Kaeufe-Abgleich zusaetzlich der seltenere Takt fuer ruhende Konten und ab wann ein Konto als ruhend gilt, beim Forumslauf die Stunde, vor der nicht gelaufen wird. Die Abstaende stehen in Minuten - damit laesst sich alles ausdruecken -, und daneben steht, was das in Stunden bedeutet, damit niemand im Kopf rechnet. Unter einer Viertelstunde geht nicht: beide Abgleiche greifen auf Server zu, die der Community gehoeren. Aenderungen greifen sofort, ohne Neustart. Die Vorgaben sind die bisherigen Werte, sie stehen in der Migration. SCHEDULE_ENABLED und FORUM_SCAN_HOUR entfallen damit.
509 lines
27 KiB
Markdown
509 lines
27 KiB
Markdown
# 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:
|
|
|
|
1. **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.
|
|
2. **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.
|
|
|
|
Das Verschieben **ordnet nur ein**: Es entscheidet nichts, bucht nichts und ändert an keinem
|
|
Status etwas. Wer aktiv war, bleibt aktiv.
|
|
|
|
### 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.
|
|
|
|
### 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).
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
openssl rand -base64 48 # für SESSION_SECRET
|
|
```
|
|
|
|
Dann starten:
|
|
|
|
```bash
|
|
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 `$name` als 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:
|
|
|
|
```bash
|
|
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:
|
|
|
|
1. Datei hochladen - Spalten und Beispielzeilen werden angezeigt.
|
|
2. Spalten zuordnen (Spieler-Kennung, Produktname, optional Bestell-ID und Datum). Die üblichen
|
|
Spaltennamen werden vorausgewählt.
|
|
3. **Vorschau** prüfen: für jede Zeile steht da, was passieren würde.
|
|
4. 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.de` und ist auf deren Seitenaufbau gebaut. Läuft bei euch eine
|
|
> andere GExtension-Instanz, genügt `GEXTENSION_BASE_URL` in der `.env`. Nutzt ihr ein
|
|
> *anderes* Shop- oder Spendensystem, funktioniert der Abgleich **nicht** - dafür müsste
|
|
> `server/src/lib/gextension.ts` angepasst 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 über `FORUM_BASE_URL`. Ein
|
|
> anderes Forensystem bräuchte einen eigenen Parser in `server/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.
|
|
|
|
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.
|
|
|
|
## 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.
|
|
|
|
- **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.
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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=Lax` und in Produktion `Secure`.
|
|
- 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](LICENSE).
|