feat: eigener Medien-Dienst statt Fivemanage Lite — Server-Grundgeruest

Ein Container statt vier: Hono, SQLite, Dateien auf einer Platte. Kein
PostgreSQL, kein MinIO, kein ClickHouse. Das ist nicht die kleine Loesung,
sondern die, die man nicht pflegt — eine Sicherung ist ein cp -a.

Der Ausloeser war nicht der Funktionsumfang von Lite, sondern dessen
Zustand. web/src/utils/http-util.ts wirft jeden Fehler als new Error(...),
die Hooks pruefen auf "instanceof ApiError" — damit wird jeder Fehlschlag
lautlos verschluckt. "Create organization" meldet weder Erfolg noch
Misserfolg; das Ergebnis waren 19 gleichnamige Organisationen, die sich auch
nicht loeschen lassen, weil die Route fehlt. Von allem, was Lite darueber
hinaus kann, haben wir nichts gebraucht.

Zwei Hostnamen, ein Prozess: unter FILES_HOST gibt es ausschliesslich
Dateien — kein Dashboard, keine API, nichts anzumelden. Das haelt die
oeffentliche Adresse frei von Angriffsflaeche und die URLs huebsch, ohne
/f/-Praefix. Die Vorlage im Handy bleibt {model}.webp.

Der Pfad ist der Schluessel: X-Path legt die Datei genau dort ab, wo der
Aufrufer sie erwartet. Bei Lite haetten wir die zurueckgegebenen Adressen in
einer urls.json mitschleppen muessen — vorhersagbare URLs sind fuer das
Fotostudio die ganze Voraussetzung.

storage.ts bereinigt Pfade NICHT, es lehnt sie ab. Ein zurechtgebogener Pfad
legt die Datei unter einem anderen Namen ab als dem erwarteten, und das faellt
erst auf, wenn das Bild fehlt.

Typprueft. Noch nicht gestartet, noch nicht getestet — das ist der naechste
Schritt und steht in ROADMAP.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-11 15:20:48 +02:00
co-authored by Claude Opus 5
parent b1576cefb0
commit e36db8c015
12 changed files with 2333 additions and 0 deletions
+156
View File
@@ -0,0 +1,156 @@
# 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
```
Typprüfung läuft durch. **Noch nicht gestartet und nicht getestet.**
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.
### ⬜ Als Nächstes
**1 · Smoke-Test.** Server lokal starten, Token anlegen, ein Bild hoch- und
wieder abrufen. Vor Docker, vor allem anderen.
**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.