From b70127a57b5a3375376c975d0b81f9f69ca4120a Mon Sep 17 00:00:00 2001
From: anima
Date: Sun, 8 Nov 2026 02:16:03 +0100
Subject: [PATCH] add admin docs
---
docs/admin/api.md | 130 +++++++++++++++++++++
docs/admin/docker.md | 249 +++++++++++++++++++++++++++++++++++++++
docs/admin/executor.md | 256 +++++++++++++++++++++++++++++++++++++++++
3 files changed, 635 insertions(+)
create mode 100644 docs/admin/api.md
create mode 100644 docs/admin/docker.md
create mode 100644 docs/admin/executor.md
diff --git a/docs/admin/api.md b/docs/admin/api.md
new file mode 100644
index 0000000..4038c8e
--- /dev/null
+++ b/docs/admin/api.md
@@ -0,0 +1,130 @@
+# Game-Server-API (`/server.api/...`)
+
+Der Game-Server läuft separat vom Backend und lauscht lokal auf Port `7192`.
+Seine Admin-Endpunkte liegen unter `/server.api/` und sind **nicht** die
+öffentliche JSON-API des Backends (`/api`).
+
+## Endpunkte
+
+| Methode | Endpunkt | Zweck |
+|---|---|---|
+| `POST` | `/server.api/eval` | JavaScript im Game-Server ausführen |
+| `POST` | `/server.api/cupdate` | Shells eines online Characters setzen |
+| `POST` | `/server.api/shutdown` | Game-Server herunterfahren |
+| `POST` | `/server.api/new_friend` | intern: Freund hinzufügen |
+| `POST` | `/server.api/lost_friend` | intern: Freund entfernen |
+
+Alle diese Endpunkte verlangen `spass` = `keys.ACCESS_MASTER`. Der Key erlaubt
+beliebige Codeausführung im Game-Server und darf niemals öffentlich werden.
+
+## `ACCESS_MASTER` lesen
+
+```sh
+cd /data/ai/gaming/adventureland
+
+ACCESS_MASTER="$(docker compose exec -T al-game node -e \
+ 'process.stdout.write(require("/app/secretsandconfig/keys").ACCESS_MASTER)')"
+```
+
+Den Wert nicht in Chat, Logs oder Commits schreiben.
+
+## `/server.api/eval`
+
+Felder:
+
+- `spass` – `ACCESS_MASTER`
+- `code` – JavaScript, wird im Game-Server ausgeführt
+- `data` – JSON-String, im Code als `data` verfügbar
+
+Der Code läuft im Modul-Scope des Game-Servers. Verfügbar sind unter anderem
+`players`, `name_to_id`, `get_player(name)`, `add_item`, `resend`,
+`calculate_player_stats` und `output`.
+
+### Gold geben (live)
+
+```sh
+curl -sS -X POST http://127.0.0.1:7192/server.api/eval \
+ --data-urlencode "spass=$ACCESS_MASTER" \
+ --data-urlencode 'code=var p=get_player("CharName"); if(p){ p.gold += 100000; resend(p,"reopen+u+cid"); output={ok:true,gold:p.gold}; } else { output={ok:false,reason:"offline"}; }' \
+ --data-urlencode 'data={}'
+```
+
+### Item geben (live)
+
+```sh
+curl -sS -X POST http://127.0.0.1:7192/server.api/eval \
+ --data-urlencode "spass=$ACCESS_MASTER" \
+ --data-urlencode 'code=var p=get_player("CharName"); if(p){ var n=add_item(p,{name:"hpot0",q:100},{announce:false}); resend(p,"reopen+u+cid"); output={ok:true,slot:n}; } else { output={ok:false,reason:"offline"}; }' \
+ --data-urlencode 'data={}'
+```
+
+`add_item` prüft Inventarplatz und stapelt stackbare Items. `announce:false`
+verhindert eine globale Fundmeldung.
+
+### XP geben (live)
+
+```sh
+curl -sS -X POST http://127.0.0.1:7192/server.api/eval \
+ --data-urlencode "spass=$ACCESS_MASTER" \
+ --data-urlencode 'code=var p=get_player("CharName"); if(p){ p.xp += 100000; resend(p,"reopen"); output={ok:true,level:p.level,xp:p.xp}; } else { output={ok:false,reason:"offline"}; }' \
+ --data-urlencode 'data={}'
+```
+
+`resend(p,"reopen")` ruft `calculate_player_stats` auf und führt Level-Ups aus.
+
+## `/server.api/cupdate`
+
+Felder:
+
+- `spass` – `ACCESS_MASTER`
+- `id` – Character-Name (`player.id` ist der Name, nicht `CH_...`)
+- `cash` – neuer Gesamtstand der Shells
+- `ncash` – Betrag für die „received shells“-Meldung; wird **nicht** addiert
+
+`cupdate` setzt `player.cash` auf `cash`. Es ist kein Inkrement. Deshalb zuerst
+den neuen Gesamtstand in MongoDB schreiben und dann `cupdate` aufrufen.
+
+```sh
+# 1. Account-Cash erhöhen
+docker compose exec al-mongo mongosh adventureland --eval \
+ 'db.user.updateOne({"info.email":"du@example.com"}, { $inc: { cash: 1000 } })'
+
+# 2. Neuen Gesamtstand lesen
+NEW_TOTAL="$(docker compose exec -T al-mongo mongosh adventureland --quiet --eval \
+ 'db.user.findOne({"info.email":"du@example.com"},{cash:1}).cash')"
+
+# 3. Online-Character aktualisieren
+curl -sS -X POST http://127.0.0.1:7192/server.api/cupdate \
+ --data-urlencode "spass=$ACCESS_MASTER" \
+ --data-urlencode 'id=CharName' \
+ --data-urlencode "cash=$NEW_TOTAL" \
+ --data-urlencode 'ncash=1000'
+```
+
+Ohne online Character reicht der MongoDB-Wert; beim nächsten Login wird
+`user.cash` geladen.
+
+## Wichtige Hinweise
+
+- **Shells** sind accountweit (`user.cash`). Ein `/cupdate` ändert nur die
+ laufende Kopie im Game-Server; die DB muss separat aktualisiert werden.
+- **Gold** ist pro Character (`character.info.gold` / `player.gold`) oder
+ accountweit in der Bank (`user.info.gold`).
+- `eval` ist mächtig und ungefiltert. Nur lokal oder hinter einer
+ vertrauenswürdigen Admin-Schicht betreiben.
+- `aadd_item` nicht verwenden: Es ruft `reopen()` auf, das im Node-Port nicht
+ definiert ist. Stattdessen `add_item` + `resend`.
+- Der Game-Server speichert live geänderte Character-Daten regelmäßig. Für
+ dauerhafte Änderungen an Offline-Characters die MongoDB-Variante verwenden;
+ für laufende Characters `eval`.
+- `ACCESS_MASTER` rotieren, bevor der Port öffentlich erreichbar ist. Der
+ Default steht in `files/config/keys.js`.
+
+## Quellen
+
+- `source-repos/adventureland_mongodb/node/server.js` – `server_api`-Router,
+ `/eval`, `/cupdate`, `/shutdown`; `add_item`, `resend`
+- `source-repos/adventureland_mongodb/adventure_functions.js` –
+ `server_url`, `server_eval`, `server_eval_safe`, `character_eval`
+- `files/config/options.js` – `api_path: "/server.api/"`, `local_port: 7192`
+- `source-repos/adventureland_mongodb/README.md` – Admin-/Executor-Hinweise
diff --git a/docs/admin/docker.md b/docs/admin/docker.md
new file mode 100644
index 0000000..7489fd5
--- /dev/null
+++ b/docs/admin/docker.md
@@ -0,0 +1,249 @@
+# Docker- und MongoDB-Admin
+
+Diese Variante arbeitet direkt auf den Containern und der MongoDB. Sie ist
+nützlich für Offline-Änderungen, Recovery und Prüfungen. Für laufende
+Characters ist die Game-Server-API (`docs/admin/api.md`) oder der
+Admin-Executor (`docs/admin/executor.md`) meist besser geeignet.
+
+## Stack
+
+| Service | Aufgabe | Port |
+|---|---|---|
+| `al-mongo` | MongoDB mit Replica-Set `rs0` | intern `27017` |
+| `al-backend` | Express-Backend, Web-Client, `/api` | `8090` |
+| `al-game` | Socket.IO-Game-Server, `/server.api` | `7192` |
+
+MongoDB ist im Compose-Setup **nicht** auf den Host veröffentlicht. Der Zugriff
+läuft über `docker compose exec`.
+
+## MongoDB-Shell
+
+```sh
+cd /data/ai/gaming/adventureland
+
+docker compose exec al-mongo mongosh adventureland
+```
+
+Einzelnes Kommando:
+
+```sh
+docker compose exec al-mongo mongosh adventureland --eval \
+ 'db.user.findOne({ "info.email": "du@example.com" }, { _id: 1, name: 1, cash: 1, "info.slots": 1 })'
+```
+
+## Account und Character finden
+
+```sh
+docker compose exec al-mongo mongosh adventureland --eval \
+ 'db.user.findOne(
+ { "info.email": "du@example.com" },
+ { _id: 1, name: 1, cash: 1, "info.gold": 1, "info.slots": 1, "info.characters": 1 }
+ )'
+```
+
+```sh
+docker compose exec al-mongo mongosh adventureland --eval \
+ 'db.character.findOne(
+ { "info.name": "CharName" },
+ { _id: 1, owner: 1, level: 1, xp: 1, online: 1, server: 1, "info.gold": 1, "info.items": 1 }
+ )'
+```
+
+## Shells geben
+
+Shells sind accountweit und liegen in `user.cash`.
+
+```sh
+docker compose exec al-mongo mongosh adventureland --eval \
+ 'db.user.updateOne(
+ { "info.email": "du@example.com" },
+ { $inc: { cash: 1000 } }
+ )'
+```
+
+Danach neu einloggen oder den neuen Stand per `/server.api/cupdate` an online
+Characters schicken (siehe `docs/admin/api.md`).
+
+## Bank-Gold geben
+
+Bank-Gold ist accountweit und liegt in `user.info.gold`.
+
+```sh
+docker compose exec al-mongo mongosh adventureland --eval \
+ 'db.user.updateOne(
+ { "info.email": "du@example.com" },
+ { $inc: { "info.gold": 100000 } }
+ )'
+```
+
+Neu einloggen bzw. Bank betreten. Ist der Character bereits in der Bank, wird
+`player.user.gold` live nicht automatisch aktualisiert.
+
+## Character-Gold geben
+
+Character-Gold liegt in `character.info.gold`.
+
+> Nur bearbeiten, solange der Character **offline** ist (`online: false`,
+> `server: ""`). Sonst überschreibt der Game-Server den Wert beim nächsten Sync.
+
+```sh
+docker compose exec al-mongo mongosh adventureland --eval \
+ 'db.character.updateOne(
+ { "info.name": "CharName" },
+ { $inc: { "info.gold": 100000 } }
+ )'
+```
+
+## Item ins Character-Inventar
+
+Item-Namen stehen in `design/items.js` (`G.items`), zum Beispiel `hpot0`,
+`mpot0`, `helmet`, `shoes`. Das Inventar fasst maximal 42 Einträge.
+
+```sh
+docker compose exec al-mongo mongosh adventureland --eval \
+ 'db.character.updateOne(
+ { "info.name": "CharName" },
+ { $push: { "info.items": { name: "hpot0", q: 100 } } }
+ )'
+```
+
+Das ist ein roher Push: kein Stacking, keine Inventarprüfung. Für laufende
+Characters `add_item` per Game-Server-API verwenden.
+
+## Item in die Bank
+
+Bank-Packs liegen in `user.info.items0` bis `user.info.items47`.
+
+```sh
+docker compose exec al-mongo mongosh adventureland --eval \
+ 'db.user.updateOne(
+ { "info.email": "du@example.com" },
+ { $push: { "info.items0": { name: "hpot0", q: 100 } } }
+ )'
+```
+
+Pack-Größe beachten (bis 42). Neu einloggen bzw. Bank betreten.
+
+## Charakterplätze setzen
+
+```sh
+docker compose exec al-mongo mongosh adventureland --eval \
+ 'db.user.updateOne(
+ { "info.email": "du@example.com" },
+ { $set: { "info.slots": 18 } }
+ )'
+```
+
+Maximum 18. Der Wert wird bei jeder Anfrage gelesen; kein Neustart nötig.
+
+## Admin setzen
+
+```sh
+docker compose exec al-mongo mongosh adventureland --eval \
+ 'db.user.updateOne(
+ { "info.email": "du@example.com" },
+ { $set: { admin: true } }
+ )'
+```
+
+Danach neu einloggen. Der Admin-Executor ist dann unter
+`http://localhost:8090/admin/executor` erreichbar.
+
+## GM setzen
+
+GM ist eine Character-Rolle, kein Web-Admin.
+
+```sh
+docker compose exec al-mongo mongosh adventureland --eval \
+ 'db.character.updateOne(
+ { "info.name": "CharName" },
+ { $set: { role: "gm", "info.p.role": "gm" } }
+ )'
+```
+
+Danach den Character neu einloggen.
+
+## Keys lesen
+
+`ACCESS_MASTER`, `BOT_MASTER` und `SERVER_MASTER` stehen in
+`files/config/keys.js` und sind im Container unter
+`/app/secretsandconfig/keys.js` gemountet.
+
+```sh
+docker compose exec -T al-game node -e \
+ 'console.log(require("/app/secretsandconfig/keys").ACCESS_MASTER)'
+```
+
+Den Wert nicht in Chat, Logs oder Commits schreiben. Vor öffentlichem Betrieb
+rotieren und `al-backend` sowie `al-game` neu starten.
+
+## Backup und Restore
+
+Backup:
+
+```sh
+docker compose exec -T al-mongo mongodump \
+ --db adventureland --archive=/tmp/al-backup.gz
+
+docker compose cp al-mongo:/tmp/al-backup.gz ./al-backup.gz
+```
+
+Restore (destruktiv, überschreibt die aktuelle DB):
+
+```sh
+docker compose cp ./al-backup.gz al-mongo:/tmp/al-backup.gz
+
+docker compose exec -T al-mongo mongorestore \
+ --archive=/tmp/al-backup.gz --drop
+```
+
+Vor jedem Restore den aktuellen Stand sichern. `--drop` entfernt vorhandene
+Collections.
+
+## Logs und Status
+
+```sh
+docker compose ps
+docker compose logs -f al-backend
+docker compose logs -f al-game
+docker compose logs -f al-mongo
+```
+
+## Neustart und Rebuild
+
+Nach Änderungen an `files/config/`:
+
+```sh
+docker compose restart al-backend al-game
+```
+
+Nach Änderungen am Image oder an Build-Patches:
+
+```sh
+docker compose build
+docker compose up -d
+```
+
+Nach Änderungen an `files/custom/game.js` reicht in der Regel ein Neustart der
+beiden App-Container.
+
+## Sicherheit
+
+- MongoDB ist nicht auf dem Host veröffentlicht; nicht zusätzlich exposen.
+- `ACCESS_MASTER` erlaubt Codeausführung im Game-Server. Nicht öffentlich
+ erreichbar machen.
+- `al-backend` und `al-game` sind im Compose-Setup an alle Host-Interfaces
+ gebunden. Für öffentlichen Betrieb Firewall/Caddy davorsetzen.
+- Default-Keys aus `files/config/keys.js` vor öffentlichem Zugriff ersetzen.
+- Offline-Characters vor DB-Änderungen ausloggen; sonst überschreibt der
+ Game-Server die Werte.
+
+## Quellen
+
+- `docker-compose.yml` – Services, Ports, Volumes
+- `Dockerfile` – Build-Patches und Runtime
+- `files/config/keys.js` – Admin-Keys
+- `files/config/options.js` – Ports, Serverdefinition, Limits
+- `source-repos/adventureland_mongodb/adventure_functions.js` –
+ `character_to_info`, `update_character`, `update_user_data`, `user_to_server`
+- `source-repos/adventureland_mongodb/api.js` – `create_character_api`
diff --git a/docs/admin/executor.md b/docs/admin/executor.md
new file mode 100644
index 0000000..a8533e5
--- /dev/null
+++ b/docs/admin/executor.md
@@ -0,0 +1,256 @@
+# Admin-Executor (`/admin/executor`)
+
+Der Admin-Executor ist die bequemste Variante für administrative Änderungen.
+Er führt **Backend-JavaScript** aus – nicht Game-Server-JavaScript – und hat
+Zugriff auf die Backend-Funktionen und die MongoDB.
+
+## Zugang
+
+Voraussetzung ist `user.admin = true` oder `Local: true` zusammen mit
+`unsecure_admin: true` in `files/config/options.js`.
+
+`user.admin` direkt setzen:
+
+```sh
+cd /data/ai/gaming/adventureland
+
+docker compose exec al-mongo mongosh adventureland --eval \
+ 'db.user.updateOne({"info.email": "du@example.com"}, { $set: { admin: true } })'
+```
+
+Danach im Browser neu einloggen und öffnen:
+
+```text
+http://localhost:8090/admin/executor
+```
+
+Der frühere Weg `/admin/make/user/admin` ist im Code deaktiviert
+(`common_engine/admin.js` enthält ein frühes `return;`).
+
+> **Sicherheit:** Der Executor erlaubt beliebige Codeausführung im Backend.
+> `unsecure_admin: true` macht jeden lokalen Besucher zum Admin. Niemals
+> zusammen mit öffentlichem Zugriff verwenden.
+
+## Was im Executor verfügbar ist
+
+Neben normalem JavaScript stehen unter anderem zur Verfügung:
+
+- `req` – der Express-Request des Executor-Aufrufs
+- `output` – wird als `[OUTPUT]` an die Oberfläche zurückgegeben
+- `inspect` – wird als JSON zurückgegeben
+- `console.log()` / `console.error()` – landen in `[LOGS]`
+- `db` – MongoDB-Handle
+- `get_user_by_email(email)`, `get_character(name)`, `get(id)`
+- `save(entity)`, `tx()`, `tx_get()`, `tx_save()`
+- `update_characters(user, reason, name, shells)`
+- `character_eval(character, code, data)`
+- `server_eval(server, code, data)`, `server_eval_safe(server, code, data)`
+- `keys` – die Server-Keys, inklusive `ACCESS_MASTER`
+
+`character_eval` führt Code auf dem Game-Server aus. Dort heißt der laufende
+Character `player`. Ist der Character offline, passiert nichts.
+
+## Beispiele
+
+### Shells geben (accountweit)
+
+Shells liegen in `user.cash`. `update_characters` schickt den neuen Stand an
+alle online Characters.
+
+```js
+var u = await get_user_by_email("du@example.com");
+if (!u) throw new Error("user not found");
+
+var amount = 1000;
+u.cash = (u.cash || 0) + amount;
+await save(u);
+
+await update_characters(u, "", "", amount);
+
+output = { email: u.info.email, cash: u.cash };
+```
+
+### Bank-Gold geben (accountweit)
+
+Bank-Gold liegt in `user.info.gold`.
+
+```js
+var u = await get_user_by_email("du@example.com");
+if (!u) throw new Error("user not found");
+
+var amount = 100000;
+u.info.gold = (u.info.gold || 0) + amount;
+await save(u);
+
+output = { bank_gold: u.info.gold };
+```
+
+Danach neu einloggen bzw. die Bank betreten. Steht der Character bereits in der
+Bank, ist `player.user.gold` live veraltet; dann zusätzlich per
+`character_eval` aktualisieren oder neu einloggen.
+
+### Character-Gold geben
+
+Character-Gold liegt in `character.info.gold`.
+
+```js
+var ch = await get_character("CharName");
+if (!ch) throw new Error("character not found");
+
+var amount = 100000;
+
+if (ch.online || ch.server) {
+ await character_eval(
+ ch,
+ "player.gold += " + amount + "; resend(player, 'reopen+u+cid');"
+ );
+ output = { character: ch.info.name, mode: "live" };
+} else {
+ ch.info.gold = (ch.info.gold || 0) + amount;
+ await save(ch);
+ output = { character: ch.info.name, gold: ch.info.gold, mode: "db" };
+}
+```
+
+### Item geben
+
+Live nutzt `add_item` Stacking und Inventarprüfung. Offline wird direkt in
+`character.info.items` geschrieben; dort gibt es keine Prüfung.
+
+```js
+var ch = await get_character("CharName");
+if (!ch) throw new Error("character not found");
+
+var item = { name: "hpot0", q: 100 };
+
+if (ch.online || ch.server) {
+ await character_eval(
+ ch,
+ "add_item(player, " + JSON.stringify(item) + ", {announce:false});" +
+ "resend(player, 'reopen+u+cid');"
+ );
+ output = { character: ch.info.name, item: item, mode: "live" };
+} else {
+ ch.info.items = ch.info.items || [];
+ ch.info.items.push(item);
+ await save(ch);
+ output = { character: ch.info.name, item: item, mode: "db" };
+}
+```
+
+Item-Namen stehen in `design/items.js` (`G.items`), zum Beispiel `hpot0`,
+`mpot0`, `helmet`, `shoes`. Das Inventar fasst maximal 42 Einträge.
+
+### XP geben
+
+```js
+var ch = await get_character("CharName");
+if (!ch) throw new Error("character not found");
+
+var amount = 100000;
+
+if (ch.online || ch.server) {
+ await character_eval(
+ ch,
+ "player.xp += " + amount + "; resend(player, 'reopen');"
+ );
+ output = { character: ch.info.name, mode: "live" };
+} else {
+ ch.xp = (ch.xp || 0) + amount;
+ ch.info.xp = ch.xp;
+ await save(ch);
+ output = { character: ch.info.name, xp: ch.xp, mode: "db" };
+}
+```
+
+`resend(player, "reopen")` ruft `calculate_player_stats` auf und führt
+Level-Ups aus. Offline passiert das beim nächsten Login.
+
+### Item in die Bank legen
+
+Bank-Packs liegen in `user.info.items0` bis `user.info.items47`.
+
+```js
+var u = await get_user_by_email("du@example.com");
+if (!u) throw new Error("user not found");
+
+u.info.items0 = u.info.items0 || [];
+u.info.items0.push({ name: "hpot0", q: 100 });
+await save(u);
+
+output = { pack: "items0", count: u.info.items0.length };
+```
+
+Danach neu einloggen bzw. die Bank betreten. Pack-Größe beachten (bis 42).
+
+### Charakterplätze setzen
+
+```js
+var u = await get_user_by_email("du@example.com");
+if (!u) throw new Error("user not found");
+
+u.info.slots = 18;
+await save(u);
+
+output = { slots: u.info.slots };
+```
+
+`info.slots` wird bei jeder Anfrage gelesen; ein Neustart ist nicht nötig.
+Maximum sind 18.
+
+### Account verifizieren
+
+```js
+var u = await get_user_by_email("du@example.com");
+if (!u) throw new Error("user not found");
+
+u.info.verified = true;
+await save(u);
+
+output = { verified: true };
+```
+
+### GM setzen
+
+GM ist eine Character-Rolle, kein Web-Admin.
+
+```js
+var ch = await get_character("CharName");
+if (!ch) throw new Error("character not found");
+
+ch.role = "gm";
+ch.info.p = ch.info.p || {};
+ch.info.p.role = "gm";
+await save(ch);
+
+output = { character: ch.info.name, role: "gm" };
+```
+
+Danach den Character neu einloggen.
+
+## Grenzen
+
+- Der Executor ist **kein** Satz benannter Befehle wie `/give shells`.
+ Es ist eine Codebox.
+- Er läuft im Backend. Für Funktionen, die es nur im Game-Server gibt
+ (`players`, `add_item`, `resend`), `character_eval` bzw. `server_eval`
+ verwenden.
+- Offline-Änderungen an `character.info.*` werden beim nächsten Sync des
+ Game-Servers überschrieben, wenn der Character online ist. Dann live
+ arbeiten oder den Character vorher ausloggen.
+- `aadd_item` aus `node/server.js` nicht verwenden: Es ruft eine nicht
+ definierte `reopen()`-Funktion auf. Stattdessen `add_item(...)` +
+ `resend(...)`.
+
+## Quellen
+
+- `source-repos/common_engine/admin.js` – `/admin/executor`, `execute_api`,
+ `is_admin`
+- `source-repos/common_engine/htmls/admin_executor.html` – Oberfläche
+- `source-repos/adventureland_mongodb/adventure_functions.js` –
+ `get_user_by_email`, `get_character`, `update_characters`,
+ `character_eval`, `server_eval`, `update_character`, `update_user_data`,
+ `user_to_server`
+- `source-repos/adventureland_mongodb/node/server.js` – `add_item`,
+ `calculate_player_stats`, `resend`
+- `source-repos/adventureland_mongodb/design/items.js` – Item-Definitionen