window.confirm und window.prompt sind raus - alle vierzehn Stellen. Sie sahen nicht nur fremd aus: Wer sie im Browser abschaltet, bekommt stillschweigend "nein" zurueck, und eine Aktion passiert einfach nicht, ohne dass jemand erfaehrt warum. Stattdessen gibt es einen Dialog-Dienst mit drei Formen - bestaetigen, abfragen, waehlen -, der sich wie der Rest der Anwendung anfuehlt. Die Wahl kam dazu, weil eine Stelle ein Ja/Nein fuer eine Auswahl missbrauchte: "OK = Kulanz, Abbrechen = bezahltes Redesign". Das fuehrt jeden in die Irre, der es zum ersten Mal sieht. Neu: Steht der bezahlte Redesign-Topf im Minus, waehrend Kulanz uebrig ist, erscheint "Minus mit Kulanz ausgleichen". Es bucht so viele Uebergaenge um, wie beide Seiten hergeben - die aeltesten zuerst, damit das Ergebnis wiederholbar ist. Unterm Strich aendert sich nichts, nur die Verteilung. Ebenfalls neu: Ein Redesign laesst sich ausnahmsweise aus dem CC-Topf bezahlen. Damit das Ausrichten an den Ketten so eine Entscheidung nicht beim naechsten Umsortieren wieder einebnet, tragen von Hand gewaehlte Buchungen jetzt einen Merker. Dabei aufgefallen: Die Tests wurden gar nicht typgeprueft - tsconfig schloss nur src ein. Sieben Aufrufe von bookKulanz hatten vertauschte Argumente und liefen trotzdem durch, weil JavaScript das hinnimmt; sie pruefte damit etwas anderes als gedacht. Tests haengen jetzt in der Typpruefung.
631 lines
35 KiB
Markdown
631 lines
35 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.
|
||
|
||
**Die Stelle in der Kette bestimmt, was ein Eintrag kostet**, und die Rechnung ist schlicht:
|
||
|
||
> So viele **CC-Pakete** wie Ketten. So viele **Redesigns** wie Einträge minus Ketten.
|
||
|
||
Drei Ketten mit zusammen sechzehn Einträgen sind also drei Pakete und dreizehn Redesigns. Anders
|
||
gesagt: Wer keinen Vorgänger hat, ist der Anfang einer Kette und damit ein gekaufter CC; wer
|
||
einen hat, ist dessen Redesign. In welcher Linie ein Eintrag geführt wird, spielt dabei keine
|
||
Rolle - die Kette ist die Kette. 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.
|
||
|
||
Daraus folgt auch der Fall mit dem fremden Gruppen-CC: Wer aus einem herausredesignt, dessen
|
||
eigener Strang hat einen Vorgänger - den Gruppen-CC. Also ist sein erstes Glied kein Kauf,
|
||
sondern ein Redesign. Bezahlt wurde der Platz beim Beitritt, und zwar einmal.
|
||
|
||
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. Es gibt zwei Ausnahmen von dieser Regel, beide bewusst zu treffen:
|
||
|
||
- Ein **Redesign aus dem CC-Topf**. Möglich, aber nicht der Regelfall - es verbraucht einen
|
||
ganzen Platz statt eines Redesigns. Im Dialog steht das dabei.
|
||
- Eine **von Hand geänderte Bezahlart** über den Knopf *Bezahlart*.
|
||
|
||
Beide werden als bewusste Entscheidung vermerkt, und das Ausrichten an den Ketten lässt sie
|
||
stehen. Sonst würde jede Ausnahme beim nächsten Umsortieren wieder eingeebnet.
|
||
|
||
Stimmt ein Bestand nicht mehr - etwa weil er aus einer Zeit stammt, in der eine Buchung blieb,
|
||
wo sie beim Anlegen entstand -, richtet **Buchungen nachrechnen** auf der Spielerseite alles
|
||
nach dieser Regel aus. Angefasst wird nur, was nicht passt; umgebucht wird über eine
|
||
Gegenbuchung, und zwischen bezahltem Redesign und Kulanz wird nichts gewechselt.
|
||
|
||
Über *Bezahlart* wird umgestellt, woraus ein Eintrag
|
||
bezahlt wurde - aus einem CC-Paket, einem bezahlten Redesign oder aus Kulanz; 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.
|
||
|
||
**Was einem Spieler gehört, steht immer in seinen Ketten** - auch wenn seine Mitgliedschaft
|
||
daran beendet wurde. Es bleibt sein CC und seine Geschichte. Unter *Früher genutzt* landen nur
|
||
CCs, die jemand anderem gehören und zu keinem seiner Plätze mehr führen.
|
||
|
||
Am Fuß einer Kette kann ein CC stehen, der jemand anderem gehört - der Gruppen-CC, aus dem sich
|
||
der Spieler herausredesignt hat. Er trägt das Kennzeichen *war dabei* samt Zeitraum und
|
||
Besitzer, ist aber **genauso bedienbar wie jeder andere Eintrag**: ziehen, bearbeiten,
|
||
Bezahlart, Verweise. Beim Nacherfassen steht erst am Ende fest, welches Redesign wohin führte -
|
||
da muss sich auch einordnen lassen, was einem nicht gehört. 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*.
|
||
|
||
## Guthaben von Hand eintragen
|
||
|
||
Auf der Spielerseite stehen drei Paare: **+ CC-Guthaben**, **+ Redesign-Paket**, **+ Kulanz** -
|
||
und daneben je ein **−**, das die zuletzt von Hand eingetragene Zugabe dieser Art zurücknimmt.
|
||
Für den vertippten oder doppelten Klick, ohne in der Historie nach der richtigen Zeile zu suchen.
|
||
|
||
**Von Hand vergeben heißt nur der Platz.** Ein im Shop gekauftes Paket bringt ein Redesign mit,
|
||
und so bucht es der Abgleich auch - hier geht es um Einzelfälle, in denen jemand einen Platz
|
||
bekommt, ohne dafür gekauft zu haben. Wer ein volles Paket nachtragen will, klickt zusätzlich
|
||
auf *+ Redesign-Paket*. In der Historie steht *CC-Guthaben (von Hand)*, damit zu sehen ist, dass
|
||
kein Kauf dahintersteht.
|
||
|
||
Zurückgenommen wird über eine Gegenbuchung; in der Historie bleibt beides stehen. Bei einem
|
||
CC-Paket gehen beide Zeilen mit zurück, das CC-Guthaben und das darin enthaltene Redesign.
|
||
**Aus einem Import gebuchte Pakete rührt das nicht an** - die gehören zu einer Bestellung und
|
||
werden dort storniert, wo dieser Zusammenhang sichtbar ist.
|
||
|
||
Steht der **bezahlte Redesign-Topf im Minus, während Kulanz übrig ist**, erscheint *Minus mit
|
||
Kulanz ausgleichen*. Es bucht so viele Übergänge von „bezahlt" auf „Kulanz" um, wie beide Seiten
|
||
hergeben - die ältesten zuerst. Unterm Strich ändert sich nichts, nur die Verteilung auf die
|
||
Töpfe. Das ist derselbe Vorgang wie ein Dutzend Mal *Bezahlart* zu drücken, nur in einem Zug.
|
||
|
||
In den Kästen oben steht neben dem offenen Stand, wie viel jemals hereinkam: *0 von 3* liest
|
||
sich anders als *0 von 0*. Das eine heißt aufgebraucht, das andere nie etwas gehabt.
|
||
|
||
## 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).
|
||
|
||
```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.
|
||
|
||
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.
|
||
|
||
```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).
|