Compare commits

..
3 Commits
Author SHA1 Message Date
D4rkst3randClaude Opus 5 5f7f17ce7e feat: der Dienst steht oeffentlich, und die Pfadmeldung sagt jetzt die Regel
Der Stack laeuft unter https://fivemanage.d4rkst3r.de, hinter NPM mit
Let's-Encrypt-Zertifikat, Container auf Port 9101.

DER NAME IST EINE UMKEHR, und sie steht ausfuehrlich im ROADMAP: hier stand,
dass fivecdn und fivemanage wegfallen, weil wir kein Fivemanage betreiben. Das
Argument gilt weiter -- es ist nur teurer als das, was dagegen steht. Der Name
war bereits fertig verkabelt: DNS da, NPM-Host da, Zertifikat gueltig bis
09.11.2026, Ziel host.docker.internal:9101, und dieser Port wurde frei, als die
Lite-Container gingen. Unser Stack auf 9101 kostet damit null Klicks. Der
Wechsel auf media.d4rkst3r.de bleibt billig, weil keine Adresse in der
Datenbank steht.

Ueber die oeffentliche Adresse durchgemessen: Anmeldung durch echtes HTTPS
(der Fall, den curl bisher nicht bezeugen konnte), Token mit Praefix-Fessel,
403 mit Text fuer anderswo/ und fuers Loeschen, Upload und Abruf Byte fuer
Byte identisch.

Und ein Befund aus dem Fotostudio-Anschluss: die Pfadmeldung war irrefuehrend.
SEGMENT verlangt als ERSTES Zeichen einen Buchstaben oder eine Ziffer, die
Meldung sprach aber nur von "nicht mit Punkt beginnend" -- ein fuehrender
Unterstrich faellt ebenso durch. Kein erfundener Fall: das Fotostudio legt
seine Leerbilder als "_plate" ab. Jetzt nennt die Meldung die Regel.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 17:08:36 +02:00
D4rkst3randClaude Opus 5 7bfd3458a1 docs: drei Angaben im ROADMAP waren falsch, alle drei nachgemessen
1. Es gibt keine zweite Maschine. Gebaut wurde auf DEM Server -- hier laufen
   Gitea, Portainer, NPM und die Nextcloud. Die Notiz "auf dieser Maschine
   gibt es weder Docker noch WSL" stammt aus der Zeit vor Docker Desktop.
   Zum Betrieb fehlt damit kein Umzug, sondern nur der oeffentliche Auftritt:
   DNS, ein Host in NPM mit TLS, ein echtes Passwort.

2. Die Portliste war unvollstaendig -- 8443 (Nextcloud AIO) fehlte. Neu
   gemessen aus "docker ps", nicht aus der Erinnerung. 9100/9101 sind frei,
   seit die Lite-Container weg sind; wir bleiben trotzdem auf 9102.

3. "Bis dahin bleibt der Lite-Stack als Vergleichsmassstab laufen" gilt nicht
   mehr: die Container sind geloescht. Was liegen blieb, sind drei Volumes und
   die Abbilder. Alle drei Volumes schreibgeschuetzt durchgezaehlt -- KEIN
   einziges Bild, minio besteht aus 244 KB reiner .minio.sys. Loeschen kostet
   also nichts; es ist nur noch nicht getan.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 16:50:57 +02:00
D4rkst3randClaude Opus 5 7cc89512d8 docs: Schritt 2 ist gemessen -- das Abbild ist gebaut und gelaufen
Der Bau stand ungeprueft im Repo, und alles daran war Behauptung: ob
`npm ci` die fertige Binaerdatei zieht, ob das Volume greift, ob der
Healthcheck etwas taugt. Jetzt nachgesehen, auf einer Maschine mit Docker
Desktop 29.6.2:

- better-sqlite3 kommt als Binaerdatei, nicht aus node-gyp (datiert Mai 2025,
  kein Makefile daneben). Node 22, ABI 127 -- die Festnagelung traegt.
- Derselbe Durchstich wie in Schritt 1, diesmal gegen den Container: 24
  Pruefungen, keine durchgefallen. Am Grundgeruest war nichts zu aendern.
- Healthcheck meldet healthy; das Volume traegt ueber einen Containerwechsel;
  Compose bricht ohne PUBLIC_URL/ADMIN_PASSWORD ab, wie das :? verspricht.

Neu aufgefallen, weil der Container es sichtbar macht: im Volume liegen VIER
Dinge, nicht zwei. SQLite laeuft im WAL-Modus, und das WAL hielt nach wenigen
Uploads 152 KB, die in media.db noch nicht standen -- eine Sicherung nur ueber
media.db waere unvollstaendig. Steht jetzt bei den offenen Entscheidungen.

Und ein Zeuge weniger: curl taugt fuer die Secure-Cookie-Falle nicht, er
schickt das Cookie auch ueber http und meldet Erfolg. Was zaehlt, ist der
Browser.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 16:47:51 +02:00
2 changed files with 166 additions and 34 deletions
+158 -32
View File
@@ -27,19 +27,33 @@ kein zweiter Dienst, kein Passwort dazwischen, und eine Sicherung ist ein
`cp -a` über ein Volume. Wenn daraus je Millionen Zeilen werden, wird `cp -a` über ein Volume. Wenn daraus je Millionen Zeilen werden, wird
`server/src/db.ts` ausgetauscht — die Aufrufer merken davon nichts. `server/src/db.ts` ausgetauscht — die Aufrufer merken davon nichts.
**Ein Name.** `fivecdn.d4rkst3r.de` und `fivemanage.d4rkst3r.de` fallen weg — **Ein Name.** Dashboard und API liegen an der Wurzel, die Dateien unter `/f/`:
wir betreiben kein Fivemanage, also tragen unsere Adressen auch nicht dessen
Namen. Dashboard und API liegen an der Wurzel, die Dateien unter `/f/`:
``` ```
https://media.d4rkst3r.de/ Dashboard und API https://fivemanage.d4rkst3r.de/ Dashboard und API
https://media.d4rkst3r.de/f/vehicles/adder.webp https://fivemanage.d4rkst3r.de/f/vehicles/adder.webp
``` ```
Ein DNS-Eintrag, ein Host im Proxy. Aufgegeben ist damit eine Eigenschaft, die Ein DNS-Eintrag, ein Host im Proxy. Aufgegeben ist damit eine Eigenschaft, die
zwei Namen mitbrächten: die öffentliche Adresse trägt jetzt auch das zwei Namen mitbrächten: die öffentliche Adresse trägt jetzt auch das
Anmeldeformular. Anmeldeformular.
> **Und der Name ist `fivemanage`, obwohl hier lange das Gegenteil stand.**
> Der ursprüngliche Satz war: `fivecdn` und `fivemanage` fallen weg, wir
> betreiben kein Fivemanage und tragen deshalb nicht dessen Namen. Das Argument
> stimmt immer noch — es ist nur billiger als das, was dagegen steht, und das
> ist nachgemessen: der Name **war schon fertig verkabelt**. DNS-Eintrag da,
> Host in NPM da, Let's-Encrypt-Zertifikat gültig bis 09.11.2026, und er zeigte
> auf `host.docker.internal:9101` — einen Port, der frei wurde, als die
> Lite-Container gelöscht wurden. Unser Stack auf 9101 heißt: **kein
> DNS-Eintrag, kein Klick in NPM, kein neues Zertifikat.**
>
> Der Preis ist ein Name, der nach einem fremden Produkt klingt. Der Wechsel
> bleibt billig — die Adressen stehen nirgends in der Datenbank, sie werden bei
> jeder Antwort aus `PUBLIC_URL` gebaut. Ein späterer Umzug auf
> `media.d4rkst3r.de` ist ein DNS-Eintrag, ein NPM-Host und eine geänderte
> Variable.
Zwei Namen kann der Dienst weiterhin — `FILES_HOST` setzen, dann gibt es unter Zwei Namen kann der Dienst weiterhin — `FILES_HOST` setzen, dann gibt es unter
diesem Namen ausschließlich Dateien und die URLs kommen ohne `/f/` aus. Beides diesem Namen ausschließlich Dateien und die URLs kommen ohne `/f/` aus. Beides
ist gemessen. Der Wechsel ist billig: die Adressen stehen **nirgends in der ist gemessen. Der Wechsel ist billig: die Adressen stehen **nirgends in der
@@ -160,7 +174,7 @@ gibt 409, Überschreiben `replaced: true` · 413 vor dem Einlesen · ETag/304,
HEAD, Bereichsanfragen und 416 · unter dem Dateihost gibt es weder API noch HEAD, Bereichsanfragen und 416 · unter dem Dateihost gibt es weder API noch
`/f/`-Präfix · Medienliste, Suche, Statistik nach Ordnern. `/f/`-Präfix · Medienliste, Suche, Statistik nach Ordnern.
Nicht getestet: Docker, das Dashboard (gibt es noch nicht), und echte Last. Nicht getestet: das Dashboard (gibt es noch nicht) und echte Last.
### ✅ Fertig — ein Name statt zwei ### ✅ Fertig — ein Name statt zwei
@@ -176,27 +190,49 @@ Beide Betriebsarten sind durchgemessen: mit einem Namen kommt
Dashboard-Namen zusätzlich unter `/f/` erreichbar, und unter dem Dateihost gibt Dashboard-Namen zusätzlich unter `/f/` erreichbar, und unter dem Dateihost gibt
es weiterhin keine API. es weiterhin keine API.
### ⬜ Als Nächstes ### ✅ Fertig — Schritt 2, das Abbild ist gebaut und gelaufen
**2 · Dockerfile und Compose — geschrieben, aber NIE GEBAUT.** Gebaut, gestartet, durchgemessenauf einer Maschine mit Docker (Desktop
`server/Dockerfile` und `docker-compose.media.yml` stehen. Auf der Maschine, 29.6.2, WSL2). Derselbe Durchstich wie in Schritt 1, diesmal gegen den
auf der sie entstanden sind, gibt es weder Docker noch WSL — das Abbild ist **Container** statt gegen `tsx`: 24 Prüfungen, keine durchgefallen. Am
also ungebaut und der Container nie gelaufen. Das ist der nächste Handgriff, Grundgerüst war nichts zu ändern; der Bau hat keinen einzigen Befund erzeugt.
und er gehört auf eine Maschine mit Docker.
Was daran trotzdem geprüft ist: **So läuft er:**
- **Node 22 ist festgenagelt, mit Grund.** `better-sqlite3` 11.10.0 liefert ```bash
fertige Binärdateien nur für ABI 108 (Node 20), 115, 127 (Node 22) und 131. export PUBLIC_URL=http://localhost:9102 ADMIN_PASSWORD=
**Node 24 hat ABI 137 und fehlt** — der naheliegende Griff zur neuesten docker compose -f docker-compose.media.yml up -d --build
Version fällt auf `node-gyp` zurück und braucht python3, make und g++ im ```
Abbild. Aus den Veröffentlichungen des Projekts abgelesen, nicht geraten.
- **`node dist/index.js`** — der Startbefehl des Abbilds — läuft und antwortet
auf `/health`. Bis dahin war der Dienst nur je über `tsx` gestartet.
- **Der Healthcheck** gibt Exitcode 0 bei laufendem und 1 bei totem Dienst.
Ungeprüft bleibt alles, wofür es Docker braucht: ob `npm ci` im Abbild die **Was jetzt gemessen ist und vorher nur behauptet war:**
Binärdatei zieht, ob das Volume greift, ob der Bau überhaupt durchläuft.
- **`npm ci --omit=dev` zieht die fertige Binärdatei.** Im Abbild liegt
`better-sqlite3/build/Release/better_sqlite3.node`, datiert Mai 2025 —
heruntergeladen, nicht übersetzt; ein `Makefile` oder `obj.target` von
`node-gyp` steht nirgends daneben. `require('better-sqlite3')` läuft im
Laufzeit-Abbild, Node 22.23.2, ABI 127. **Die Festnagelung trägt.**
- **Der Bau läuft durch**, alle drei Stufen. Das Abbild: 373 MB auf der
Platte, 88,4 MB Inhalt.
- **Der Healthcheck greift.** Docker meldet `healthy`, Exitcode 0, kein
Fehlschlag — ohne `curl` im Abbild, `fetch` aus Node reicht.
- **Das Volume greift.** Container weggeworfen, einen neuen an dasselbe
Volume gehängt: die Datei aus dem ersten kommt weiter unter ihrer URL, und
`admin` wird nicht noch einmal angelegt. Nebenbei bestätigt, was im Compose
steht: ein geändertes `ADMIN_PASSWORD` setzt nichts zurück — die alte
Anmeldung gilt (200), die neue nicht (401).
- **Compose bricht ohne Variablen ab**, wie das `:?` verspricht, und zwar mit
dem Text, der danebensteht — erst `PUBLIC_URL`, dann `ADMIN_PASSWORD`. Mit
beiden fährt der Stack hoch und bindet `0.0.0.0:9102->8080`.
- **`Secure` steht wirklich im Cookie**, wenn `NODE_ENV=production` gilt:
`sid=…; Path=/; HttpOnly; Secure; SameSite=Lax`. Ohne Cookie oder mit einem
erfundenen gibt jede Dashboard-Route 401 — auch `/auth/me`.
**Neu aufgefallen, weil der Container es sichtbar macht:** im Volume liegen
**vier** Dinge, nicht zwei — `files/`, `media.db`, `media.db-shm` und
`media.db-wal`. SQLite läuft im WAL-Modus (`db.ts`), und das WAL war nach
wenigen Uploads 152 KB groß, mit Daten, die in `media.db` noch nicht standen.
Eine Sicherung, die nur `media.db` mitnimmt, ist deshalb unvollständig. Siehe
*Offene Entscheidungen*.
> **Beim Ausprobieren:** `NODE_ENV=production` setzt das Sitzungs-Cookie auf > **Beim Ausprobieren:** `NODE_ENV=production` setzt das Sitzungs-Cookie auf
> `Secure`. Wer den Container ohne HTTPS aufruft, bekommt auf die Anmeldung > `Secure`. Wer den Container ohne HTTPS aufruft, bekommt auf die Anmeldung
@@ -204,17 +240,88 @@ Binärdatei zieht, ob das Volume greift, ob der Bau überhaupt durchläuft.
> still verwirft; die nächste Anfrage ist 401. Im Browser gegen eine > still verwirft; die nächste Anfrage ist 401. Im Browser gegen eine
> LAN-Adresse nachgemessen. Die naheliegende Gegenprobe über `localhost` > LAN-Adresse nachgemessen. Die naheliegende Gegenprobe über `localhost`
> führt in die Irre: das gilt Browsern als sicherer Kontext und funktioniert. > führt in die Irre: das gilt Browsern als sicherer Kontext und funktioniert.
> Und `curl` taugt hier nicht als Zeuge — der schickt ein Secure-Cookie auch
> über `http` und meldet fröhlich Erfolg. Was zählt, ist der Browser.
In Portainer aus diesem Repo deploybar — **`Compose path` nimmt EINE Datei**, In Portainer aus diesem Repo deploybar — **`Compose path` nimmt EINE Datei**,
Ergänzungen werden stillschweigend ignoriert. Das hat uns bei Lite einen Ergänzungen werden stillschweigend ignoriert. Das hat uns bei Lite einen
Nachmittag gekostet. Host-Port ist 9102, solange der Lite-Stack 9100/9101 Nachmittag gekostet.
belegt.
**3 · `server/upload.lua` im Fotostudio.** `Upload.put(pfad, bytes) → url`. > **Und noch eine Annahme, die beim Nachsehen fiel: es gibt gar keine zweite
Token in `config.upload.lua`, server-only, gitignored, mit committetem > Maschine.** Gebaut und gelaufen ist auf **dem Server** — hier laufen Gitea,
`.example` — nie im NUI, dort hat es jeder Spieler im Speicher. An den > Portainer, NPM und die Nextcloud. Die alte Notiz „auf dieser Maschine gibt
Serienlauf hängen, damit freigestellte Bilder nach der Freigabe sofort > es weder Docker noch WSL" galt vor der Installation von Docker Desktop und
hochgehen. > ist überholt. Was zum Betrieb fehlt, ist deshalb nicht der Umzug auf einen
> anderen Rechner, sondern nur der **öffentliche Auftritt**: DNS-Eintrag für
> `media.d4rkst3r.de`, ein Host in NPM mit TLS, ein echtes Passwort.
Gemessen am 11.08.2026 belegt: 80, 81, 443, 2224, 3000, 3080, 3478, 8000,
8080, **8443**, 8090, 9000, 9443, 11000 — 8443 fehlte in der alten Liste.
**Der Host-Port ist 9101**, nicht die 9102 aus dem Compose-Standard: darauf
zeigt der fertige NPM-Host (siehe oben). Gesetzt wird er über `HOST_PORT` in
der `.env`, der Standard im Compose bleibt 9102.
### ✅ Fertig — der Dienst steht öffentlich
Seit dem 11.08.2026 läuft er unter **`https://fivemanage.d4rkst3r.de`**, hinter
NPM mit Let's-Encrypt-Zertifikat, Container auf 9101. Die Werte stehen in einer
`.env` neben dem Compose (gitignored); dieselben Werte sind in Portainer die
Stack-Variablen.
Durchgemessen über die öffentliche Adresse, nicht über localhost:
- Anmeldung durch echtes HTTPS — **das ist der Fall, den `curl` bisher nicht
bezeugen konnte**: `Secure` am Cookie stört jetzt nicht mehr, weil die
Verbindung wirklich TLS ist. 200, Cookie da, `/auth/me` 200.
- Token mit Präfix-Fessel `vehicles/` und ohne Löschrecht angelegt. Upload nach
`anderswo/`**403 mit Text**, Löschen → **403 mit Text**.
- Upload und Abruf über TLS und Proxy: **Byte für Byte identisch.**
> **Der 502, der keiner war.** Der erste Aufruf gab 502 in 78 ms. Die
> naheliegende Erklärung — „ein Moment, gleich geht's" — war falsch: der
> NPM-Host zeigte in dem Moment auf 9102, unser Container liegt auf 9101, also
> sofortige Ablehnung. Kein Timeout, keine DNS-Frage, kein Zufall. Ein 502
> binnen Millisekunden ist immer ein *Connection refused*, und dann stimmt eine
> Portnummer nicht.
### ⬜ Als Nächstes
**3 · `server/upload.lua` im Fotostudio — geschrieben, im Spiel noch nicht
gelaufen.** Liegt in `d4rk_photostudio` (eigenes Repo, `D:\FXServer\txData\…`):
```
server/upload.lua Upload.send(pfad, bytes, cb) und Upload.put(pfad, bytes) → url
config.upload.example.lua committet
config.upload.lua gitignored, enthaelt den Token
```
Hängt an `d4rk_photostudio:saveKeyed` — also genau an der Freigabe: das Bild
geht **erst auf die Platte, dann in den Dienst**, und ein Fehlschlag bricht
nichts ab, sondern landet im Protokoll des Laufs. Ein Lauf über 900 Fahrzeuge,
der an einem Netzhuster stirbt, wäre der teuerste denkbare Fehler.
Gemessen ist alles, was sich ohne FiveM messen lässt — und das ist mehr, als es
zunächst schien: `upload.lua` wurde in einer **echten Lua-5.3-VM** (fengari,
über Node) mit gefälschten FiveM-Funktionen ausgeführt. 30 Prüfungen, keine
durchgefallen: fehlende, kaputte und unvollständige `config.upload.lua`;
Adresse, Methode und alle vier Kopfzeilen; der Fehlertext des Dienstes statt
einer nackten Zahl; Status 0; und dass die Frist den Rückruf nicht ein zweites
Mal auslöst. Der Rumpf, den die VM erzeugt hat, ging danach **unverändert an
den echten Dienst** — hochgeladen, abgerufen, Byte für Byte identisch.
**Was offen bleibt, bis es einmal im Spiel lief:** ob `PerformHttpRequest`,
`SetTimeout` und `promise` sich in FiveM so verhalten wie die Fälschungen, und
ob der Event-Handler den Rückruf sauber erreicht. Dafür fehlt genau ein
Handgriff, und er braucht die Serverkonsole:
```
refresh
ensure d4rk_photostudio (bzw. restart d4rk_photostudio)
```
Beim Start sagt die Resource in einer Zeile, ob der Dienst antwortet. Danach
`/studio shot <modell>` im Spiel — das Bild muss unter
`https://fivemanage.d4rkst3r.de/f/vehicles/<modell>.webp` stehen.
Nach diesem Schritt ist das erreicht, worum es ursprünglich ging. Nach diesem Schritt ist das erreicht, worum es ursprünglich ging.
@@ -230,8 +337,23 @@ Speicherverbrauch nach Ordnern · Token-Verwaltung, Klartext genau einmal.
> haben, und er ist billig zu vermeiden. > haben, und er ist billig zu vermeiden.
**5 · Umzug.** Root-`docker-compose.yml` wird unsere; die Fivemanage-Dateien **5 · Umzug.** Root-`docker-compose.yml` wird unsere; die Fivemanage-Dateien
wandern nach `legacy/`. Erst wenn unserer trägt — bis dahin bleibt der wandern nach `legacy/`.
Lite-Stack als Vergleichsmaßstab laufen.
**Der Vergleichsmaßstab ist weg.** Am 11.08.2026 sind die Lite-Container
gelöscht worden — mit ihnen die Möglichkeit, im Zweifel nachzusehen, wie es
dort aussah. Was noch liegt und eine Entscheidung braucht:
```
Volumes fivemanager_db fivemanager_minio fivemanager_pgdata
Abbilder ghcr.io/fivemanage/lite:0.1.0-beta.23 minio/minio minio/mc
```
**Nachgesehen, bevor jemand fragt: es ist nichts drin.** Alle drei Volumes
schreibgeschützt eingehängt und durchgezählt — **kein einziges Bild**.
`fivemanager_minio` sind 244 KB und besteht ausschließlich aus `.minio.sys`;
der Eimer `media` hat Metadaten, aber kein einziges Objekt. Die anderen beiden
sind leere Datenbanken (MySQL 196 MB, PostgreSQL 47 MB — das ist ihr
Leergewicht). **Löschen kostet nichts**, es ist nur noch nicht getan.
### ⬜ Später, wenn es sich lohnt ### ⬜ Später, wenn es sich lohnt
@@ -247,6 +369,10 @@ Lite-Stack als Vergleichsmaßstab laufen.
- **Sicherung.** Ein Volume mit Bildern und `media.db`. Restic gegen die - **Sicherung.** Ein Volume mit Bildern und `media.db`. Restic gegen die
Nextcloud? Oder reicht ein `docker cp` vor größeren Änderungen? Nextcloud? Oder reicht ein `docker cp` vor größeren Änderungen?
**Was dabei feststeht:** wegen WAL gehören `media.db-wal` und `media.db-shm`
mit ins Kopieren, sonst fehlen die letzten Uploads. Sauberer wäre
`sqlite3 media.db ".backup …"` oder ein `wal_checkpoint(TRUNCATE)` davor —
beides ungeprüft, weil noch nicht entschieden ist, wie gesichert wird.
- **Bilder aus dem Spiel** (Screenshots, Clips von Spielern) — dafür brauchte - **Bilder aus dem Spiel** (Screenshots, Clips von Spielern) — dafür brauchte
es Größenbegrenzungen je Token und vermutlich eine Warteschlange. Erst es Größenbegrenzungen je Token und vermutlich eine Warteschlange. Erst
planen, wenn es ansteht. planen, wenn es ansteht.
+8 -2
View File
@@ -41,10 +41,16 @@ export function checkPath(raw: string): string {
for (const segment of segments) { for (const segment of segments) {
if (!SEGMENT.test(segment)) { if (!SEGMENT.test(segment)) {
// Die Meldung nennt die Regel so, wie sie ist. Vorher stand hier
// "nicht mit Punkt beginnend" — das ist nur die Haelfte: das erste
// Zeichen muss ein Buchstabe oder eine Ziffer sein, ein fuehrender
// Unterstrich faellt also ebenso durch. Aufgefallen an
// "_probe.webp", und das ist kein erfundener Fall: das Fotostudio
// legt seine Leerbilder als "_plate" ab.
throw new PathError( throw new PathError(
`"${segment}" ist als Pfadteil nicht erlaubt ` + `"${segment}" ist als Pfadteil nicht erlaubt ` +
'(Buchstaben, Ziffern, Punkt, Strich, Unterstrich; ' + '(erstes Zeichen: Buchstabe oder Ziffer; danach auch ' +
'nicht mit Punkt beginnend)', 'Punkt, Strich, Unterstrich)',
) )
} }
} }