Schritt 4. React 18, TypeScript, Vite 5, Tailwind 4 (CSS-first), Lucide, Zustand; kein Konsta. Quelle in server/ui, Build nach server/web (gitignored), im Abbild von einer eigenen Docker-Stufe gebaut. JEDER KNOPF GIBT RUECKMELDUNG -- und zwar erzwungen, nicht vorgenommen: - api.ts haelt den EINZIGEN fetch und GENAU EINE Fehlerklasse. Der Fivemanage-Fehler war, zwei zu haben: geworfen wurde `new Error`, geprueft auf `instanceof ApiError`, und damit verschwand jeder Fehlschlag lautlos. - store.ts haelt run(): Aktion rein, Erfolgstext rein, und es meldet bei Erfolg diesen und bei Fehlschlag den Text DES DIENSTES. Wer darueber geht, kann keinen stillen Knopf bauen. - Jeder wartende Knopf ist gesperrt und zeigt einen Kreisel. Der zweite Klick eine Sekunde spaeter war der Weg zu neunzehn gleichnamigen Organisationen. - Fehlermeldungen bleiben stehen, bis jemand sie wegklickt. Ein Fehler, der von allein verschwindet, ist einer, den niemand gelesen hat. @vitejs/plugin-react ist auf ^4 festgenagelt: die 6 verlangt vite ^8. In den peerDependencies nachgesehen -- Vite 5.4.21 liegt in der Schnittmenge von plugin-react@4 und @tailwindcss/vite@4. Am laufenden Container nachgemessen: / kommt als text/html, assets/*.js als text/javascript, *.css als text/css (die MIME-Falle aus Schritt 1 traegt), und /tokens, /speicher, /konto liefern dieselbe index.html -- die SPA-Rueckfall- route tut, wofuer sie gebaut wurde. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
470 lines
23 KiB
Markdown
470 lines
23 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.** Dashboard und API liegen an der Wurzel, die Dateien unter `/f/`:
|
|
|
|
```
|
|
https://fivemanage.d4rkst3r.de/ Dashboard und API
|
|
https://fivemanage.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.
|
|
|
|
> **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
|
|
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: 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.
|
|
|
|
### ✅ Fertig — Schritt 2, das Abbild ist gebaut und gelaufen
|
|
|
|
Gebaut, gestartet, durchgemessen — auf einer Maschine mit Docker (Desktop
|
|
29.6.2, WSL2). Derselbe Durchstich wie in Schritt 1, diesmal gegen den
|
|
**Container** statt gegen `tsx`: 24 Prüfungen, keine durchgefallen. Am
|
|
Grundgerüst war nichts zu ändern; der Bau hat keinen einzigen Befund erzeugt.
|
|
|
|
**So läuft er:**
|
|
|
|
```bash
|
|
export PUBLIC_URL=http://localhost:9102 ADMIN_PASSWORD=…
|
|
docker compose -f docker-compose.media.yml up -d --build
|
|
```
|
|
|
|
**Was jetzt gemessen ist und vorher nur behauptet war:**
|
|
|
|
- **`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
|
|
> `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.
|
|
> 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**,
|
|
Ergänzungen werden stillschweigend ignoriert. Das hat uns bei Lite einen
|
|
Nachmittag gekostet.
|
|
|
|
> **Und noch eine Annahme, die beim Nachsehen fiel: es gibt gar keine zweite
|
|
> Maschine.** Gebaut und gelaufen ist auf **dem Server** — hier laufen Gitea,
|
|
> Portainer, NPM und die Nextcloud. Die alte Notiz „auf dieser Maschine gibt
|
|
> es weder Docker noch WSL" galt vor der Installation von Docker Desktop und
|
|
> 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.
|
|
|
|
### ✅ Fertig — Schritt 3, der Fotostudio lädt selbst hoch
|
|
|
|
**Am 11.08.2026 im Spiel gelaufen.** `/studio shot adder`, und die Konsole sagte
|
|
in zwei Zeilen, was passiert ist:
|
|
|
|
```
|
|
[photostudio] Upload nach https://fivemanage.d4rkst3r.de — Dienst antwortet.
|
|
[photostudio] adder freigestellt abgelegt (129 KB)
|
|
[photostudio] adder hochgeladen: https://fivemanage.d4rkst3r.de/f/vehicles/adder.webp
|
|
```
|
|
|
|
Von der Dienstseite gegengeprüft, statt der Logzeile zu glauben: 132 908 Bytes
|
|
hier wie dort, **derselbe SHA-256** (`f14dfc5b…5515e`), ausgeliefert als
|
|
`image/webp`, und die ersten Bytes sind `RIFF….WEBP` — ein echtes WebP und
|
|
kein falsch benanntes PNG.
|
|
|
|
Damit ist auch die letzte offene Frage aus dem Prüfstand beantwortet:
|
|
`PerformHttpRequest` verhält sich in FiveM wie die Fälschung, Kopfzeilen und
|
|
base64-Rumpf kommen unverändert an, und der Rückruf erreicht den
|
|
Event-Handler.
|
|
|
|
**Was noch niemand gemessen hat:** ein ganzer Serienlauf über 900 Fahrzeuge —
|
|
also 900 Uploads hintereinander, mit dem, was dabei an Fristen und
|
|
Gleichzeitigkeit auftreten kann. Und `Upload.put`, die wartende Form, ist
|
|
ungenutzt: der Lauf nimmt `Upload.send`. Sie steht da, weil die ROADMAP sie so
|
|
benannt hat, und ist damit ungeprüfter Code.
|
|
|
|
Der Quelltext 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.
|
|
|
|
**Damit ist erreicht, worum es ursprünglich ging.** Ein Bild entsteht im Spiel,
|
|
wird freigestellt, landet auf der Platte und liegt Sekunden später unter einer
|
|
vorhersagbaren Adresse — ohne Fivemanage, ohne PostgreSQL, ohne MinIO, ohne
|
|
einen einzigen Handgriff von Hand dazwischen.
|
|
|
|
### ✅ Fertig — Schritt 4, das Dashboard
|
|
|
|
Liegt in `server/ui`, gebaut nach `server/web` (gitignored), im Abbild von
|
|
einer eigenen Docker-Stufe. React 18, TypeScript, Vite 5, Tailwind 4
|
|
(CSS-first), Lucide, Zustand — **kein Konsta**, wie geplant.
|
|
|
|
Anmeldung · Galerie mit Vorschau, Suche, Mehrfachauswahl, Blättern · Einzel-
|
|
und Sammellöschung mit Nachfrage, die die Pfade zeigt · Speicherverbrauch nach
|
|
Ordnern · Token-Verwaltung mit Klartext genau einmal · Passwortwechsel.
|
|
|
|
> **Vite 5 bleibt, aber `@vitejs/plugin-react` ist auf `^4` festgenagelt.**
|
|
> Die 6 verlangt `vite ^8`. In den `peerDependencies` nachgesehen, nicht
|
|
> geraten: `@tailwindcss/vite@4` nimmt `^5.2 || ^6 || ^7 || ^8`, und
|
|
> `plugin-react@4` nimmt `^4.2 || ^5 || ^6 || ^7`. Vite 5.4.21 liegt in beiden
|
|
> Schnittmengen.
|
|
|
|
**Wie die eine Regel durchgesetzt wird, und nicht nur gemeint ist.** In
|
|
`api.ts` steht der einzige `fetch` und genau eine Fehlerklasse — der
|
|
Fivemanage-Fehler war, zwei zu haben (`new Error` geworfen, `instanceof
|
|
ApiError` geprüft, jeder Fehlschlag verschluckt). In `store.ts` steht `run()`:
|
|
es nimmt die Aktion und den Erfolgstext, meldet bei Erfolg diesen und bei
|
|
Fehlschlag den Text **des Dienstes**. Wer darüber geht, kann keinen stillen
|
|
Knopf bauen. Jeder Knopf, der wartet, zeigt einen Kreisel und ist gesperrt —
|
|
der zweite Klick eine Sekunde später war der Weg zu neunzehn gleichnamigen
|
|
Organisationen.
|
|
|
|
Nachgemessen am laufenden Container: `/` liefert die Oberfläche als
|
|
`text/html`, `assets/*.js` als `text/javascript`, `assets/*.css` als
|
|
`text/css` (die MIME-Falle aus Schritt 1 trägt also), und `/tokens`,
|
|
`/speicher`, `/konto` geben dieselbe `index.html` zurück — die
|
|
SPA-Rückfallroute tut, wofür sie gebaut wurde.
|
|
|
|
### ⚠️ Der Proxy hält Bilder fest — 30 Minuten lang
|
|
|
|
**Beim Nachmessen der Zwischenspeicher-Kopfzeilen gefunden, und es trifft
|
|
genau den Fotostudio-Ablauf.** NPMs `assets.conf` greift über eine Regex auf
|
|
*jede* URL, die auf `.css .js .png .webp …` endet — also auch auf
|
|
`/f/vehicles/adder.webp`. Sie setzt `proxy_cache public-cache`,
|
|
`proxy_cache_valid any 30m` und wirft unsere Kopfzeilen weg
|
|
(`proxy_ignore_headers Cache-Control`, `proxy_hide_header Cache-Control`).
|
|
|
|
Reproduziert, nicht vermutet:
|
|
|
|
```
|
|
Bild hochladen → abrufen: "ERSTE-FASSUNG"
|
|
dieselbe Datei neu → abrufen über NPM: "ERSTE-FASSUNG" ← alt
|
|
abrufen am Dienst: "ZWEITE-FASSUNG" ← neu
|
|
```
|
|
|
|
**Ein neu fotografiertes Fahrzeug sieht bis zu 30 Minuten lang aus wie das
|
|
alte**, obwohl Upload, Protokoll und Datenbank alle „fertig" sagen. Das ist
|
|
die Sorte Fehler, die man drei Ebenen tiefer sucht.
|
|
|
|
Der Gegenschnipsel gehört in NPM, Proxy Host `fivemanage.d4rkst3r.de` →
|
|
*Advanced*:
|
|
|
|
```nginx
|
|
# Dateien nicht zwischenspeichern -- sie koennen sich unter derselben
|
|
# Adresse aendern. Das ^~ ist der ganze Trick: eine gewoehnliche
|
|
# Praefix-Location VERLIERT gegen die Regex aus assets.conf.
|
|
location ^~ /f/ {
|
|
proxy_cache off;
|
|
include conf.d/include/proxy.conf;
|
|
}
|
|
```
|
|
|
|
Dass `^~` nötig ist und ein blankes `location /f/` nicht reicht, ist in einem
|
|
Wegwerf-nginx nachgemessen: ohne `^~` gewinnt die Regex, mit `^~` das Präfix.
|
|
|
|
### ⬜ Als Nächstes
|
|
|
|
**5 · Umzug.** Root-`docker-compose.yml` wird unsere; die Fivemanage-Dateien
|
|
wandern nach `legacy/`.
|
|
|
|
**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
|
|
|
|
- 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?
|
|
**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
|
|
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.
|