Files
d4rk_media/ROADMAP.md
T
D4rkst3randClaude Opus 5 70210c0363 feat: Dockerfile und Compose -- geschrieben, aber nie gebaut
Auf dieser Maschine gibt es weder Docker noch WSL. Das Abbild ist also
ungebaut und der Container nie gelaufen; das gehoert auf eine Maschine
mit Docker, bevor jemand darauf baut.

Was trotzdem geprueft ist:

Node 22 ist festgenagelt, und das ist kein Zufall. better-sqlite3 11.10.0
liefert fertige Binaerdateien nur fuer ABI 108 (Node 20), 115, 127
(Node 22) und 131 -- Node 24 hat ABI 137 und fehlt. Der naheliegende
Griff zur neuesten Version faellt damit auf node-gyp zurueck und braucht
python3, make und g++ im Abbild. Aus den Veroeffentlichungen des Projekts
abgelesen, nicht aus dem Gedaechtnis.

node dist/index.js -- der Startbefehl des Abbilds -- laeuft und antwortet
auf /health. Bis hierher war der Dienst nur je ueber tsx gestartet
worden. Der Healthcheck gibt Exitcode 0 bei laufendem und 1 bei totem
Dienst.

Dazu eine Falle, die im Compose steht, weil sie teuer ist:
NODE_ENV=production setzt das Sitzungs-Cookie auf Secure. Wer den
Container ohne HTTPS aufruft, bekommt auf die Anmeldung 200 -- und ist
trotzdem nicht angemeldet, weil der Browser das Cookie still verwirft.
Die naechste Anfrage ist 401. Im Browser gegen eine LAN-Adresse
nachgemessen. Die naheliegende Gegenprobe ueber localhost fuehrt in die
Irre: das gilt Browsern als sicherer Kontext und funktioniert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 16:11:25 +02:00

271 lines
13 KiB
Markdown

# d4rk_media — Roadmap
Eigener Medien-Dienst für alle FiveM-Ressourcen: Dateien annehmen, ablegen,
ausliefern, in einem Dashboard verwalten.
**Warum nicht Fivemanage Lite.** Der Stack lief am Ende — PostgreSQL, MinIO,
Dashboard, alles grün. Was an ihm nicht zu reparieren war, ist die Qualität
der Oberfläche: `http-util.ts` wirft jeden Fehler als `new Error(…)`, die
Hooks prüfen auf `instanceof ApiError`, und damit wird **jeder** Fehlschlag
lautlos verschluckt. `Create organization` gibt weder Erfolg noch Misserfolg
zurück — das Ergebnis waren 19 gleichnamige Organisationen, weil ein Knopf
ohne Rückmeldung wie ein kaputter Knopf aussieht. Löschen geht nicht, die
Route existiert nicht. Das ist kein Bedienfehler und keine fehlende Funktion,
das ist 0.1.0-beta.
Dazu kam: von dem, was Lite über eine Dateiablage hinaus kann — Mandanten,
strukturiertes Logging, ClickHouse — haben wir nichts gebraucht und alles
mitgeschleppt.
---
## Der Zuschnitt
**Ein Container.** SQLite statt PostgreSQL, Dateien auf einer Platte statt
MinIO. Das ist nicht die kleine Lösung, sondern die, die man nicht pflegt:
kein zweiter Dienst, kein Passwort dazwischen, und eine Sicherung ist ein
`cp -a` über ein Volume. Wenn daraus je Millionen Zeilen werden, wird
`server/src/db.ts` ausgetauscht — die Aufrufer merken davon nichts.
**Ein Name.** `fivecdn.d4rkst3r.de` und `fivemanage.d4rkst3r.de` fallen weg —
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://media.d4rkst3r.de/f/vehicles/adder.webp
```
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
Anmeldeformular.
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
ist gemessen. Der Wechsel ist billig: die Adressen stehen **nirgends in der
Datenbank**, sie werden bei jeder Antwort aus `PUBLIC_URL` gebaut.
**Der Pfad bleibt in beiden Fällen der Schlüssel.** `vehicles/adder.webp`
bleibt `vehicles/adder.webp`, die Vorlage im Handy also `{model}.webp`. Nur der
Teil davor ändert sich, und der steht in einer Variablen.
**Der Pfad ist der Schlüssel.** `X-Path: vehicles/adder.webp` legt die Datei
genau dort ab. Vorhersagbare URLs sind für das Fotostudio die ganze
Voraussetzung — bei Lite hätten wir die zurückgegebenen Adressen in einer
`urls.json` mitschleppen müssen.
**Nicht nachgebaut wird das Logging.** Genau dafür schleppt Fivemanage
ClickHouse mit, und genau da ist der Wartungsaufwand echt. Wenn Logs kommen
sollen, ist das ein eigenes Projekt mit eigener Entscheidung.
---
## Stand
### ✅ Fertig — Server-Grundgerüst
```
server/src/config.ts Einstellungen, alle aus der Umgebung
server/src/db.ts SQLite: users, sessions, tokens, media
server/src/auth.ts scrypt, Sitzungen, Token-Hashes
server/src/storage.ts Pfadprüfung, atomares Schreiben, MIME
server/src/routes/upload.ts die API für Skripte
server/src/routes/dash.ts die API fürs Dashboard
server/src/index.ts Host-Weiche, Dateiauslieferung, Start
```
Die API für Skripte:
```
POST /api/upload Bearer-Token
X-Path vehicles/adder.webp (optional)
X-Encoding base64 (für Lua)
X-Overwrite false (Standard: ja)
→ { url, path, size, sha256, mime, replaced }
DELETE /api/media/<pfad> Token braucht can_delete
GET /api/exists/<pfad> für "nur fehlende" im Serienlauf
```
Drei Rumpfformen — `multipart/form-data`, Base64, rohe Bytes. Base64 ist für
FiveM: `PerformHttpRequest` schickt einen Lua-String, und im Fotostudio kommt
das Bild ohnehin als Data-URL aus dem NUI.
Die API fürs Dashboard: Anmeldung, Passwortwechsel, Medienliste mit Suche und
Paginierung, Einzel- und Sammellöschung, Statistik nach Ordnern,
Token-Verwaltung.
### ✅ Fertig — Schritt 1, der Smoke-Test
Gestartet, durchgemessen, repariert. Der Durchstich steht: anmelden → Token
anlegen → Bild hochladen → unter der zurückgegebenen URL abrufen, Byte für Byte
identisch, auch unter dem Dateihost.
**So läuft er lokal:**
```bash
cp server/.env.example server/.env
```
```bash
npm --prefix server ci && npm --prefix server run dev
```
> **Der Stolperstein, den es nicht mehr gibt.** Anfangs war der Standard für
> `FILES_HOST` der Host aus `PUBLIC_URL`. Bei zwei Namen ist das richtig, bei
> einem tödlich: dann galt *jede* Anfrage als Anfrage an den Dateiwirt,
> `/health` gab 404 und die Anmeldung 405 „hier gibt es nur Dateien". Nichts
> war kaputt, und niemand kommt darauf. Seit der Entscheidung für einen Namen
> ist der Ein-Namen-Betrieb der Standard und der zweite Name die Ansage —
> lokal läuft es damit ohne Sonderbehandlung. Welche Betriebsart gilt, sagt
> der Dienst in der zweiten Startzeile.
**Was der Test gefunden hat** — alles im Grundgerüst, alles repariert:
1. **Die Token-Prüfung hing an `'*'`** und galt damit auch für `/api/dash/*`,
das daneben liegt. Das Dashboard bekam „Token fehlt oder ist unbekannt" auf
die Anmeldung, obwohl es nie einen Token haben kann. Jetzt hängt sie an den
drei eigenen Pfaden.
2. **`DELETE /api/media/…` und `GET /api/exists/…` sahen am Ziel vorbei.**
`c.req.path` trägt den Einhängepunkt mit, das `replace(/^\/media\//)` schnitt
ihn nicht weg — aus `vehicles/adder.png` wurde `api/media/vehicles/adder.png`.
Löschen fand nie etwas, `exists` meldete immer `false`. Jetzt `:pfad{.+}`;
Hono liefert den Parameter fertig dekodiert (nachgemessen), ein zweites
`decodeURIComponent` wäre eine Dekodierung zu viel gewesen.
3. **Verzeichnisdurchstieg in der SPA-Rückfallroute.**
`GET /..%5Cpackage.json` hat unter Windows die Datei ausgeliefert: Hono
reicht `%5C` durch, `path.join` behandelt den Backslash dort als Trenner,
und eine Eindämmung gab es nicht. Unter Linux trägt genau dieser Angriff
nicht — Glück, keine Abwehr. Jetzt dieselbe `resolve`-Prüfung wie in
`storage.ts`.
4. **Die Auskunft „Oberfläche ist nicht gebaut" war unerreichbar.**
`createReadStream` meldet eine fehlende Datei *asynchron*; das `try/catch`
darum fing nichts. Ergebnis war ein leerer 200 samt ENOENT im Log. Jetzt
wird vorher nachgesehen.
5. **CSS und JS kamen als `application/octet-stream`** — die MIME-Tabelle kennt
nur Medientypen. Das Dashboard hätte weder Stylesheet noch Modul geladen.
Die Oberfläche hat jetzt eine **eigene** Tabelle: in der geteilten fehlt
`html` mit Absicht, sonst könnte jeder mit einem Upload-Token eine Seite
unter `fivecdn.d4rkst3r.de` veröffentlichen. Nachgemessen: eine hochgeladene
`.html` kommt weiterhin als `octet-stream` mit `nosniff`.
6. **Kaputtes JSON endete als nackter „Internal Server Error".** Ausgerechnet
das — ein 500 ohne ein Wort dazu ist der Fehler, wegen dem wir hier neu
bauen. Jetzt 400 mit Text.
**Was sonst nachgemessen wurde und trägt:** alle drei Rumpfformen (multipart,
Base64, rohe Bytes) liefern denselben SHA-256, auch mit `data:`-Vorspann · der
Hash-Pfad ohne `X-Path` · Präfix-Fessel und `can_delete` je Token · `..`,
versteckte Dateien und leerer Rumpf werden abgelehnt · `X-Overwrite: false`
gibt 409, Überschreiben `replaced: true` · 413 vor dem Einlesen · ETag/304,
HEAD, Bereichsanfragen und 416 · unter dem Dateihost gibt es weder API noch
`/f/`-Präfix · Medienliste, Suche, Statistik nach Ordnern.
Nicht getestet: Docker, das Dashboard (gibt es noch nicht), und echte Last.
### ✅ Fertig — ein Name statt zwei
`PUBLIC_URL` ist jetzt nur noch der Ursprung; das `/f` hängt der Dienst selbst
an. Wer es mitschriebe, bekäme Adressen mit `/f/f/` — und das fiele erst auf,
wenn das erste Bild fehlt. Prefix und Route werden deshalb an genau **einer**
Stelle entschieden (`config.filePrefix`), damit die zurückgegebene Adresse und
die Route, die sie ausliefert, nicht auseinanderlaufen können.
Beide Betriebsarten sind durchgemessen: mit einem Namen kommt
`…/f/vehicles/adder.png` zurück und liefert die Datei; mit gesetztem
`FILES_HOST` kommt sie ohne Präfix, liegt unter dem Dateihost, ist am
Dashboard-Namen zusätzlich unter `/f/` erreichbar, und unter dem Dateihost gibt
es weiterhin keine API.
### ⬜ Als Nächstes
**2 · Dockerfile und Compose — geschrieben, aber NIE GEBAUT.**
`server/Dockerfile` und `docker-compose.media.yml` stehen. Auf der Maschine,
auf der sie entstanden sind, gibt es weder Docker noch WSL — das Abbild ist
also ungebaut und der Container nie gelaufen. Das ist der nächste Handgriff,
und er gehört auf eine Maschine mit Docker.
Was daran trotzdem geprüft ist:
- **Node 22 ist festgenagelt, mit Grund.** `better-sqlite3` 11.10.0 liefert
fertige Binärdateien nur für ABI 108 (Node 20), 115, 127 (Node 22) und 131.
**Node 24 hat ABI 137 und fehlt** — der naheliegende Griff zur neuesten
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
Binärdatei zieht, ob das Volume greift, ob der Bau überhaupt durchläuft.
> **Beim Ausprobieren:** `NODE_ENV=production` setzt das Sitzungs-Cookie auf
> `Secure`. Wer den Container ohne HTTPS aufruft, bekommt auf die Anmeldung
> **200 — und ist trotzdem nicht angemeldet**, weil der Browser das Cookie
> still verwirft; die nächste Anfrage ist 401. Im Browser gegen eine
> LAN-Adresse nachgemessen. Die naheliegende Gegenprobe über `localhost`
> führt in die Irre: das gilt Browsern als sicherer Kontext und funktioniert.
In Portainer aus diesem Repo deploybar — **`Compose path` nimmt EINE Datei**,
Ergänzungen werden stillschweigend ignoriert. Das hat uns bei Lite einen
Nachmittag gekostet. Host-Port ist 9102, solange der Lite-Stack 9100/9101
belegt.
**3 · `server/upload.lua` im Fotostudio.** `Upload.put(pfad, bytes) → url`.
Token in `config.upload.lua`, server-only, gitignored, mit committetem
`.example` — nie im NUI, dort hat es jeder Spieler im Speicher. An den
Serienlauf hängen, damit freigestellte Bilder nach der Freigabe sofort
hochgehen.
Nach diesem Schritt ist das erreicht, worum es ursprünglich ging.
**4 · Dashboard.** React 18, TypeScript, Vite 5, Tailwind 4 (CSS-first),
Lucide, Zustand. **Kein Konsta** — das ist für die Handy-Oberfläche richtig,
für ein Dashboard am Monitor nicht.
Anmeldung · Galerie mit Vorschau, Suche, Mehrfachauswahl · Löschen ·
Speicherverbrauch nach Ordnern · Token-Verwaltung, Klartext genau einmal.
> **Die eine Regel:** jeder Knopf gibt Rückmeldung, auch der erfolgreiche.
> Das ist der Fehler, an dem wir bei Fivemanage einen Nachmittag verloren
> haben, und er ist billig zu vermeiden.
**5 · Umzug.** Root-`docker-compose.yml` wird unsere; die Fivemanage-Dateien
wandern nach `legacy/`. Erst wenn unserer trägt — bis dahin bleibt der
Lite-Stack als Vergleichsmaßstab laufen.
### ⬜ Später, wenn es sich lohnt
- Vorschaubilder serverseitig (spart dem Dashboard das Laden von 900 Vollbildern)
- Namensräume je Ressource, mit eigenem Token und eigener Statistik
- Spieler-Metadaten an einem Upload (wer, wann, welcher Server)
- Aufräumregeln: was älter als X ist und zu keinem Datensatz gehört
- Mehrere Benutzer statt nur `admin`
---
## Offene Entscheidungen
- **Sicherung.** Ein Volume mit Bildern und `media.db`. Restic gegen die
Nextcloud? Oder reicht ein `docker cp` vor größeren Änderungen?
- **Bilder aus dem Spiel** (Screenshots, Clips von Spielern) — dafür brauchte
es Größenbegrenzungen je Token und vermutlich eine Warteschlange. Erst
planen, wenn es ansteht.
---
## Was aus dem Fivemanage-Anlauf hängen bleibt
Steht ausführlich im README, hier nur die Kurzfassung — es sind alles Dinge,
die uns wieder begegnen:
1. **Portainer nimmt unter `Compose path` eine Datei.** Ergänzungen werden
stillschweigend übergangen: alle Container gesund, alles 502.
2. **Host-Ports an `0.0.0.0` binden, nicht an `127.0.0.1`.** NPM liegt im
Standard-Bridge-Netz und kommt über die Docker-Brücke, nicht über Loopback.
Zu macht der Port die Firewall.
3. **Belegt auf diesem Server:** 80, 81, 443, 2224, 3000, 3080, 3478, 8000,
8080, 8090, 9000, 9443, 11000. Unsere: 9100 und 9101.
4. **`9101:8080` ist keine Kollision.** Nur die linke Zahl muss einmalig sein.
5. **Ein Dienst, der ohne Konfiguration hochfährt, kostet mehr Zeit als
einer, der sich weigert.** Deshalb bricht `config.ts` beim Start ab.