Files
d4rk_media/ROADMAP.md
T
D4rkst3randClaude Opus 5 e173c3bf7b docs: Stand nach dem Smoke-Test, und der Stolperstein beim lokalen Start
Der Durchstich steht: anmelden, Token anlegen, Bild hochladen, unter der
zurueckgegebenen URL abrufen -- Byte fuer Byte identisch, auch unter dem
Dateihost.

server/.env.example ist die Datei, die .gitignore ohnehin schon erwartet
hat. Wichtigster Eintrag darin ist FILES_HOST: ohne ihn nimmt der Dienst
den Host aus PUBLIC_URL, lokal also localhost. Dann gilt jede Anfrage als
Anfrage an den Dateiwirt, und dort gibt es absichtlich weder Dashboard
noch API -- /health antwortet 404, die Anmeldung 405 "hier gibt es nur
Dateien". Nichts ist kaputt, der Dienst tut genau das, was er soll, nur
eben unter dem einen Namen, den man lokal hat. In Produktion sind es zwei
verschiedene Namen und der Standard stimmt.

Das dev-Skript liest die .env jetzt mit --env-file (funktioniert unter
tsx, nachgeprueft).

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

9.9 KiB

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.

Zwei Hostnamen, ein Prozess.

fivecdn.d4rkst3r.de nur Dateien. Kein Dashboard, keine API, nichts anzumelden
fivemanage.d4rkst3r.de Dashboard und API

Das hält die öffentliche Adresse frei von Angriffsfläche und die URLs hübsch: https://fivecdn.d4rkst3r.de/vehicles/adder.webp, ohne /f/-Präfix. Die Vorlage im Handy bleibt damit schlicht {model}.webp.

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:

cp server/.env.example server/.env
npm --prefix server ci && npm --prefix server run dev

Der Stolperstein beim lokalen Start: ohne FILES_HOST nimmt der Dienst den Host aus PUBLIC_URL — lokal also localhost. Dann gilt jede Anfrage als Anfrage an den Dateiwirt, und dort gibt es absichtlich weder Dashboard noch API: /health antwortet 404, die Anmeldung 405 „hier gibt es nur Dateien". Nichts ist kaputt. In Produktion sind es zwei verschiedene Namen und der Standard stimmt; lokal gehört ein Name hin, den niemand aufruft. Steht samt Begründung in server/.env.example.

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.

Als Nächstes

2 · Dockerfile und Compose. Ein Abbild, das Server und gebaute Oberfläche ausliefert; ein Volume für data/; ein Port für NPM. In Portainer aus diesem Repo deploybar — Compose path nimmt EINE Datei, Ergänzungen werden stillschweigend ignoriert. Das hat uns bei Lite einen Nachmittag gekostet.

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.