Commit Graph
45 Commits
Author SHA1 Message Date
D4rkst3randClaude Opus 5 40c446f859 feat: Papierkorb, Abrufzaehler, WebP ohne Adressaenderung, ZIP hochladen
Vier Dinge, alle am laufenden Dienst gemessen.

PAPIERKORB. Loeschen war endgueltig -- bei einem Knopf "alle 3861 waehlen"
direkt neben "loeschen" ist das die falsche Haerte, und die Sicherung half nur
bis zum letzten naechtlichen Lauf. Die Datei wandert jetzt nach
/data/papierkorb, der Datensatz in eine eigene Tabelle, nach 30 Tagen raeumt
der Dienst stuendlich selbst auf. Der Ablagename traegt eine laufende Nummer:
unter demselben Pfad koennen nacheinander verschiedene Dateien gelegen haben,
und beide sollen zurueckholbar sein. Geprueft: loeschen -> oeffentlich 404,
Eintrag im Papierkorb, zurueckholen -> 200 samt neu gerechneter Vorschau.

ABRUFZAEHLER. media.abrufe und zuletzt_abgerufen. Gezaehlt wird im Speicher und
alle 30 Sekunden weggeschrieben -- ein UPDATE je Kachel waeren bei einer
Galerieseite 120 Schreibvorgaenge. Der 304 zaehlt mit (der Aufrufer WOLLTE die
Datei, er hatte sie nur schon), HEAD nicht. Was vor dem Einbau lag, steht als
"nie geholt" da, auch wenn es taeglich benutzt wurde -- die Karte im
Speicherbericht sagt das auch dazu, statt eine Zahl zu zeigen, die luegt.

WEBP OHNE ADRESSAENDERUNG. Der Pfad ist die Adresse: aus items/foo.png darf
nicht items/foo.webp werden. Die sparsame Fassung liegt deshalb DANEBEN und
wird unter DERSELBEN Adresse ausgeliefert, wenn der Aufrufer per Accept sagt,
dass er WebP versteht. Nachgemessen an einer Datei: 6448 Bytes fuer curl, 2758
fuer einen Browser. Ueber den ganzen Bestand: 3606 Fassungen, 199,3 MB ->
33,1 MB, gespart 166,2 MB. Bei 29 lohnt WebP nicht -- dort bleibt es beim
Original.

Zwei Dinge haengen daran, beide gegengeprueft: "Vary: Accept" (ohne die Zeile
legt ein Zwischenspeicher die WebP-Fassung fuer alle ab, auch fuer die, die sie
nicht lesen koennen) und getrennte ETags mit "-w" (sonst bekaeme jemand auf
seinen If-None-Match hin ein 304 fuer das falsche Bild). Gemessen: derselbe
ETag mit Accept -> 304, ohne Accept -> 200 mit dem PNG.

ZIP HOCHLADEN. Ein Archiv wird ausgepackt statt abgelegt, die Ordner darin
bleiben erhalten und haengen sich hinter den gewaehlten Zielordner. fflate und
nicht adm-zip: reines JavaScript, keine native Bibliothek -- dieses Projekt hat
schon einen halben Abend an einer ABI-Nummer verloren. Grenzen: 5000 Eintraege,
256 MB entpackt, geprueft VOR dem Entpacken (eine Zip-Bombe waere sonst schon
im Speicher). Zip-Slip gemessen: "../../../../etc/passwd" und
"..\..\windows\hosts" abgelehnt, die harmlose Datei im selben Archiv abgelegt.

UND EIN KNOPF, DER GELOGEN HAT. Der erste Nachruestlauf ueber alle Dateien
brauchte mehr als 90 Sekunden -- genau da gibt Nginx Proxy Manager auf. Der
Aufrufer sah einen 504, waehrend die Arbeit im Hintergrund weiterlief und
fertig wurde. Ein Knopf, der Erfolg als Fehler meldet, ist schlimmer als einer
ohne Rueckmeldung.

Beide Wartungsknoepfe laufen deshalb in Runden zu 300 Stueck: der Server meldet
"offen" und "fertig", die Oberflaeche ruft erneut auf und zeigt dabei "280
erzeugt, noch 129 offen ...". Gemessen: 7,8 s je Runde statt 90+ am Stueck. Der
Vorschau-Knopf hatte dieselbe Wand und wurde mitgezogen, obwohl er noch nicht
dagegengelaufen war.

Nebenbei: der Verwaisten-Sucher kennt jetzt auch /data/webp (gleiche Schleife,
Ordner im Ergebnis vorangestellt), Verschieben und Loeschen nehmen die sparsame
Fassung mit, und das Loeschen eines Ordners raeumt thumbs/ und webp/ hinterher.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 21:40:55 +02:00
D4rkst3randClaude Opus 5 e4a125e1c6 fix: ein Upload durfte den ganzen Dienst umbringen -- und tat es
BEIM UEBERNEHMEN VON 3670 DATEIEN STARB DER DIENST. Nicht einmal, sondern in
Schleife: acht Neustarts, nach aussen 502 vom Proxy.

    TypeError [ERR_INVALID_STATE]: ReadableStream is already closed
        at ReadableByteStreamController.close (node:internal/webstreams/...)

DIE URSACHE WAR MEINE EIGENE AENDERUNG von vor einer Stunde. Um PowerShells
Formular-Rumpf zu vertragen, hatte ich `c.req.parseBody()` durch
`c.req.raw.formData()` ersetzt -- und damit Hono den Rumpf weggenommen, den es
selbst verwaltet. Beim Aufraeumen der Antwort schliesst dann jemand einen
Strom, der schon zu ist. Die Ausnahme faellt in einem Microtask an, also
AUSSERHALB jedes try/catch, und Node beendet den Prozess.

Beim Test mit 57 Dateien fiel das nicht auf. Bei 3670 schon.

Jetzt geht der Normalfall wieder ueber Hono; der eigene Weg gilt nur noch fuer
die Antwort mit Anfuehrungszeichen an der Grenzmarkierung, fuer die er gedacht
war.

UND EIN NETZ DARUNTER: process.on('uncaughtException') faengt, was ausserhalb
jedes try/catch anfaellt, und laesst den Dienst weiterlaufen. Die Abwaegung
steht im Code: das ist NICHT in jedem Fall richtig -- der Prozess kann danach
kaputt sein. Hier ueberwiegt das Weiterlaufen, weil dieser Dienst keinen
Zustand im Speicher haelt (alles in SQLite und auf der Platte) und ein Dienst,
der wegen EINER Anfrage fuer alle weg ist, der schlechtere Tausch waere. Laut
wird es trotzdem.

DER SCHADEN WAR REPARIERBAR, und zwar mit dem Werkzeug von heute Nachmittag:
206 Dateien waren durch, 2 lagen auf der Platte OHNE Datensatz (geschrieben,
dann starb der Prozess vor dem Eintrag), dazu eine verwaiste Vorschau. Der
Verwaisten-Finder hat sie gefunden, "aufnehmen" hat sie eingetragen -- keine
Datei verloren.

Und die Fehlermeldung im Uebernahmeskript verschwieg die Ursache: sie meldete
"Conversion from JSON failed ... Unexpected character <", obwohl die Wahrheit
ein 502 war. Jetzt wird der HTTP-Code mitgelesen, bei 502 sauber abgebrochen
und gesagt, dass ein neuer Lauf einfach weitermacht.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 21:04:27 +02:00
D4rkst3randClaude Opus 5 5d35381952 feat: Dateien aus der Nextcloud uebernehmen
tools/uebernehmen.ps1 holt einen Nextcloud-Ordner in den Dienst. Unterordner
bleiben erhalten, -Probe zeigt erst nur, was passieren wuerde.

UEBER DAS DASHBOARD UND NICHT UEBER DIE TOKEN-API. Nextcloud-Dateien heissen
"Brand Logo (final).PNG" -- checkPath lehnt das mit Recht ab. Der
Dashboard-Weg biegt den Namen bereits vorhersagbar zurecht. Dieselbe Regel an
einer Stelle ist besser als dieselbe Regel an zweien, von denen eine irgendwann
abweicht.

WIEDERHOLBAR ueber overwrite=false: was schon da ist, meldet der Dienst je
Datei als "Pfad ist belegt". Nachgemessen mit 57 Marken -- zweiter Lauf: 0
uebernommen, 57 waren schon da.

DREI FEHLER BEIM BAUEN, alle durch Messen gefunden:

1. Ein kaputter Formular-Rumpf ergab einen NACKTEN 500, aufgefangen nur von der
   Auffanglinie. Jetzt 400 mit Grund.

2. PowerShells `Invoke-RestMethod -Form` scheitert am Dienst. Ich hatte die
   Ursache zuerst falsch: nicht (nur) die Grenzmarkierung in
   Anfuehrungszeichen, die RFC 2046 erlaubt und die wir jetzt vertragen --
   sondern der Rumpf selbst. Mitgeschnitten:

       Content-Disposition: form-data; name=file; filename=a.png

   RFC 7578 verlangt name="file" MIT Anfuehrungszeichen, .NET laesst sie weg,
   und Nodes Parser besteht darauf. Das steht im RUMPF; dafuer braeuchte es
   einen eigenen Multipart-Parser, und der waere unverhaeltnismaessig. Die
   Werkzeuge nehmen deshalb curl.exe -F.

3. PowerShell-Falle: curl.exe gibt zeilenweise ein ARRAY zurueck, und
   -match/-notmatch darauf FILTERT, statt zu pruefen. Ein nicht-leeres Ergebnis
   gilt als wahr -- damit meldete das Skript "Kein Verzeichnis gefunden" bei
   einer tadellosen Antwort mit HTTP 207. Jetzt wird vorher zusammengefuegt,
   auch in sichern.ps1, wo dieselbe Falle nur deshalb nicht zuschlug, weil die
   Ausgabe eine Zeile hat.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 20:59:59 +02:00
D4rkst3randClaude Opus 5 51071fc292 docs: README und .env.example auf den Stand -- und Embeds im Stil des Bots
DIE README BESCHRIEB NOCH DEN VORGAENGER: "Fivemanage Lite mit MySQL und
MinIO", Ausprobieren ueber docker-compose.local.yml, vier Geheimnisse, davon
drei fuer Dienste, die es nicht mehr gibt. Jetzt beschreibt sie, was da ist:
ein Container, SQLite, Dateien auf einer Platte -- Starten, was der Dienst
kann, die eine Regel und wie sie im Code durchgesetzt wird, Betrieb (Sicherung,
Wiki, Zurueckspielen), was bewusst fehlt, und die zwei Dinge, die Zeit gekostet
haben.

Und die .env.example nannte DB_PASSWORD, MINIO_ROOT_PASSWORD und
API_TOKEN_HMAC_SECRET -- drei Werte fuer Dienste, die seit Wochen weg sind, und
KEINEN der acht, die das Compose wirklich liest. Nachgezaehlt: jetzt sind alle
acht genannt, zwei als Pflicht, sechs als Optional mit dem Grund dahinter.

EMBEDS IM STIL DES BOTS. Auf den Hinweis hin in d4rkbot/src/embeds.js
nachgesehen: dort gibt es eine zentrale Embed-Fabrik mit Markenfarbe
(#f5c518), Fusszeile "D4RKST3R // <TAG>" und Zeitstempel. Zwei Dienste
desselben Hauses sollen in einem Kanal nicht wie zwei Fremde aussehen -- also
uebernommen statt neu erfunden, samt Feldern statt Fliesstext und Discords
Zeitmarken (<t:...:R>), die "vor 3 Stunden" in der Zeitzone des Lesers
anzeigen.

EINE ABWEICHUNG, mit Absicht: der Bot faerbt alles in der Markenfarbe, hier
faerbt die SCHWERE. Eine Warnung, die aussieht wie jede andere Nachricht, ist
eine Warnung, die man ueberliest -- und diese Meldungen gibt es nur, weil
jemand sie sehen soll.

Die Testnachricht schickt jetzt echte Zahlen mit. So sieht man nicht nur, DASS
etwas ankommt, sondern auch, ob es lesbar ist.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 20:38:36 +02:00
D4rkst3randClaude Opus 5 c9cd8eaa84 feat: Token-Grenzen, Speicher nach Art, Aufbewahrungsbericht, Discord-Meldungen
KEINE TOKEN-ARTEN, SONDERN TOKEN-GRENZEN. Fivemanage hat Token-Typen; wir haben
schon ein Rechtesystem, es steht nur auf einer anderen Achse (Praefix und
Loeschrecht). Was wirklich fehlte, sind die drei Schrauben, die die Frage
beantworten "was passiert, wenn DIESER Token abhandenkommt":

  Groessengrenze   ein Token fuer Fahrzeugbilder braucht keine 64 MB
  erlaubte Arten   nur Bilder, kein Video
  Ablaufdatum      fuer eine Anbindung, die man mal ausprobiert

Alle drei am laufenden Dienst durchgemessen: 600 KB gegen 512 KB -> 413 mit
beiden Zahlen; ein mp4 gegen einen Bild-Token -> 403 mit Art UND MIME im Text;
ausserhalb des Praefix -> 403; abgelaufen -> 401 mit DEM DATUM. "Abgelaufen"
und "unbekannt" auseinanderzuhalten spart eine halbe Stunde Suche nach einem
Tippfehler, den es nicht gibt.

Mehr als die Dienstgrenze zu erlauben wird abgelehnt statt still gekappt -- es
waere eine Behauptung, denn durchgelassen wird ohnehin die schaerfere.

SPEICHER NACH ART: ein Balken fuer die Verhaeltnisse, eine Tabelle fuer die
Zahlen, dazu die groessten Dateien.

AUFBEWAHRUNG VORBEREITET, NICHT GEBAUT. Automatisches Loeschen zu bauen, bevor
man weiss, was da liegt, ist der Weg, wie man Daten verliert. Stattdessen der
Bericht davor: wie viel ist aelter als 7/30/90/365 Tage, was liegt doppelt
(gleicher SHA-256 unter zwei Pfaden) und wie viel Platz das kostet. "Alles
aelter als 90 Tage loeschen" ist eine Behauptung, solange niemand weiss, wie
viel das waere. Diese Seite loescht nichts.

DISCORD-MELDUNGEN -- und der Grund ist nicht Discord, sondern eine Luecke, die
wir heute selbst gebaut haben: die Sicherung laeuft nachts um halb fuenf, und
wenn sie aufhoert zu laufen, merkt es niemand. Der Dienst sieht jetzt
STUENDLICH nach und meldet, wenn der letzte Lauf gescheitert oder aelter als 26
Stunden ist (24 waeren zu knapp, 48 zu spaet). Hoechstens EINMAL je Zustand --
eine Meldung, die stuendlich wiederkommt, wird weggeklickt, und dann auch die,
die zaehlt.

Ein Webhook und keine Bot-Anbindung: eine Adresse, kein Token, keine
Berechtigungen. Die Adresse wird geprueft (nur echte discord.com-Webhooks),
und der Testknopf steht gleich daneben -- ein Webhook, den man eintraegt und
erst in drei Wochen im Fehlerfall ausprobiert, ist einer, der dann nicht geht.

Aus deren Doku mitgenommen und NICHT gebaut: presigned URLs (kurzlebige
signierte Adressen fuer Client-Uploads). Waere der richtige Weg fuer
Spieler-Screenshots -- solange keine Spieler hochladen, ist es Vorrat.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 20:30:03 +02:00
D4rkst3randClaude Opus 5 37bf6a68ed feat: das Konto zieht hinter den Namen, und die Ansicht merkt sich
DAS KONTO WAR EIN EIGENER REITER -- gleichrangig neben Galerie und Verlauf,
obwohl man sein Passwort zweimal im Jahr aendert und die Galerie taeglich
oeffnet. Jetzt haengt es hinter dem eigenen Namen oben rechts, zusammen mit dem
Abmelden. Das ist der Platz, an dem jeder danach sucht.

Der Pfad /konto BLEIBT bestehen: ein Lesezeichen darauf soll nicht ins Leere
fuehren, nur weil der Reiter verschwunden ist. Dafuer gibt es jetzt eine
zweite, kleine Liste "versteckt" neben den Reitern -- Seiten mit Adresse, aber
ohne Knopf.

Das Menue schliesst bei Klick daneben und bei Escape. Ein Menue, das offen
bleibt, bis man genau den Knopf wieder trifft, steht im Weg.

DIE ANSICHT MERKT SICH. Seitengroesse, Sortierung und Richtung in der Galerie,
Seitengroesse im Verlauf: wer 120 pro Seite einstellt, meint das dauerhaft und
nicht bis zum naechsten F5. Der Auffrisch-Schalter tat das schon von Hand und
geht jetzt denselben Weg.

BEWUSST NICHT gemerkt werden Filter -- Ordner, Suche, Medienart. Ein gemerkter
Filter fuehrt zu der Frage "warum sehe ich nichts", und die Antwort steht dann
in einer Leiste, auf die niemand schaut, weil er sie nicht selbst gesetzt hat.
Was die Ansicht formt, wird gemerkt; was die Menge einschraenkt, nicht.

localStorage kann werfen (privates Fenster, voller Speicher). Dann verhaelt
sich der Haken wie ein gewoehnlicher Zustand, statt die Seite mitzureissen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 20:12:49 +02:00
D4rkst3randClaude Opus 5 66c5de1e31 docs: das Wiki gibt es jetzt -- und es kommt aus docs/
Gitea legt das Wiki-Repo erst mit der ersten Seite an; Klon und Push antworten
davor mit 500, obwohl has_wiki auf true steht. Angelegt ueber die API
(POST /api/v1/repos/<besitzer>/<repo>/wiki/new) -- danach ist es ein
gewoehnliches Git-Repo und laesst sich klonen wie jedes andere.

  https://git.d4rkst3r.de/D4rkst3r/d4rk_media/wiki

DIE QUELLE BLEIBT docs/ IM REPO. Ein Wiki hat keinen Zusammenhang mit dem Code:
niemand sieht, ob die Anleitung noch zu dem passt, was der Dienst tut. Unter
docs/ wandert sie im selben Commit mit der Aenderung, die sie beschreibt.
tools/wiki.ps1 traegt sie danach hinaus -- EINSEITIG, und die Wiki-Seite sagt
das auch: wer dort tippt, verliert es beim naechsten Lauf. Unschoen, aber
ehrlicher als zwei Quellen, die auseinanderlaufen.

Zwei Seiten: Home (Zuschnitt, was der Dienst kann, was zu tun ist, wenn etwas
nicht geht) und API (Endpunkte, Kopfzeilen, Fehlerantworten, Grenzen -- und die
beiden Proxy-Fallen von heute).

Nachgemessen: beide Seiten sind ohne Anmeldung lesbar (200), ein zweiter Lauf
des Skripts erkennt "bereits aktuell", und eine Aenderung kommt an.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 20:09:35 +02:00
D4rkst3randClaude Opus 5 28404b78c2 feat: Code mit Farbe, Anleitung im Repo, Galerie mit Seitengroesse
CODE MIT FARBE, ohne Bibliothek. Ein Hervorheber wie highlight.js wiegt mehr
als diese ganze Oberflaeche und kann fuenfzig Sprachen, von denen hier drei
vorkommen. Was Code lesbar macht, sind drei Unterscheidungen -- Kommentar,
Zeichenkette, Schluesselwort. Der Rest ist Zierde. Also ein Tokenizer in einem
Ausdruck, der seine Grenzen kennt: verschachtelte Sonderfaelle faerbt er
falsch, und bei zehn Zeilen Beispiel ist das der richtige Tausch.

DIE ANLEITUNG liegt als docs/API.md IM REPO und damit unter Versionskontrolle,
statt nur im Wiki zu stehen, wo niemand sieht, wann sie zuletzt stimmte. Sie
nennt nicht nur die Endpunkte, sondern was schiefgehen kann und wie die Antwort
dann aussieht -- samt der beiden Proxy-Fallen, die uns heute begegnet sind.

Das Gitea-Wiki gibt es noch NICHT: Gitea legt das Wiki-Repo erst an, wenn die
erste Seite ueber die Oberflaeche erstellt wird (davor antwortet der Klon mit
500). Die API-Seite verlinkt es trotzdem schon -- und sobald es existiert,
schiebt ein Befehl docs/API.md hinein.

GALERIE: die Seitengroesse ist waehlbar (30/60/120/200 statt fester 60), und
die Ordnerspalte rueckt ab 1536 px nach LINKS AUS dem Hauptbereich heraus --
15rem Spalte plus 1rem Abstand sind genau die 16rem, um die dort nach links
gezogen wird. Damit steht das Raster auf derselben Breite wie jede andere
Seite, statt um die Spaltenbreite schmaler zu sein. Darunter stapelt es wie
bisher; auf einem 1280er Schirm gaebe es keinen Rand, in den etwas ruecken
koennte.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 20:05:35 +02:00
D4rkst3randClaude Opus 5 67d2f4ec93 feat: verschieben, Ordner, Adressvorlagen, API-Seite mit Export
VERSCHIEBEN. Dateien in einen anderen Ordner legen -- mit einer Warnung, die
OBEN steht und nicht im Kleingedruckten: der Pfad IST die oeffentliche Adresse,
nach dem Verschieben fuehrt die alte URL ins Leere. Das ist kein Fehler,
sondern der Preis vorhersagbarer Adressen; man muss es nur vorher wissen.

Der Umzug ist ein `rename` und kein Kopieren-dann-Loeschen: auf derselben
Platte ist das unteilbar. Die Vorschau zieht mit, sonst zeigte die Galerie am
neuen Ort nichts und am alten das Bild einer Datei, die dort nicht mehr liegt.
Eine belegte Zieladresse wird NICHT still ueberschrieben -- da verschwaende
sonst eine fremde Datei, und der Verlauf zeigte nur einen Umzug.

Der Verlauf kennt dafuer eine neue Spalte `von` und eine neue Art `move`; ohne
sie stuende dort nur das Ziel und die Frage "wo war die vorher" waere nicht
mehr zu beantworten.

ORDNER anlegen und entfernen. Leere Ordner gibt es sonst nicht -- sie sind der
Teil eines Pfades vor dem letzten Schraegstrich, mehr nicht. Jetzt sind sie ein
echtes Verzeichnis, und /folders liest Tabelle und Platte zusammen. Ein Ordner
MIT Inhalt wird nicht mitgeloescht: das waere ein Knopf, der hunderte Dateien
wegraeumt, ohne sie zu zeigen.

ADRESSVORLAGEN. Der Bauplan einer oeffentlichen Adresse, benannt und
gespeichert (vehicles/{model}.webp). Sie werden auf der Startseite und der
API-Seite mit ausgefuellter Adresse gezeigt. Ohne Platzhalter oder mit ".." im
Muster wird abgelehnt -- beides nachgemessen.

API-SEITE. Anleitung fuer die eigenen Skripte, und der Punkt daran: die
Beispiele stehen MIT DER ECHTEN ADRESSE drin statt mit example.com. Wer
abschreibt, schreibt keinen Tippfehler ab. Dazu Export in drei Formen -- JSON,
CSV (Semikolon und CRLF, sonst steht in Excel alles in einer Spalte) und eine
fertige Lua-Tabelle Name -> Adresse, die man in eine Resource legt.

DREI FEHLER BEIM MESSEN GEFUNDEN:
- rm mit recursive:false wirft bei einem Verzeichnis EISDIR und sagt damit
  etwas ganz anderes, als der Fall ist. rmdir scheitert von selbst, wenn der
  Ordner nicht leer ist -- genau das gewuenschte Verhalten.
- In der Lua-Ausgabe war "\'" gemeint und "\'" geschrieben: in JavaScript ist
  das schlicht ein Apostroph, der Backslash faellt weg. Ein Modellname mit
  Apostroph haette die erzeugte Datei zerlegt.
- Der Export mit Ordnerfilter antwortete mit einem NACKTEN 500 (kaputtes
  ESCAPE in der SQL-Zeile). Ausgerechnet das.

Deshalb hat der Dienst jetzt eine AUFFANGLINIE: app.onError gibt einen Text
statt "Internal Server Error". Wir haben das bisher an den bekannten Stellen
einzeln abgefangen -- und prompt hat ein Tippfehler die Luecke gefunden. Eine
Stelle schliesst sie fuer alles, woran heute niemand denkt.

Dazu: die Verlaufstabelle hat feste Spaltenbreiten (colgroup) und ueberall
dieselben Abstaende -- vorher hatten Kopf und Koerper verschiedene, und der
Pfad wurde abgeschnitten, obwohl daneben Platz frei war. Und die Anmeldeseite
hat zwei weiche Lichter statt einer leeren Flaeche.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 20:01:01 +02:00
D4rkst3randClaude Opus 5 32d69c9945 fix: der Inhalt hat ueberall dieselbe Breite
Konto und Einstellungen standen auf max-w-2xl, alle anderen Seiten auf der
Containerbreite -- beim Wechseln sprang der Inhalt sichtbar. Ein Fehler beim
Bauen, kein Entwurf.

Die Breite steht jetzt an EINER Stelle (BREITE in App.tsx) und gilt fuer
Kopfzeile und Inhalt. Vorher stand sie zweimal da, und zwei Seiten setzten
zusaetzlich ihre eigene: drei Orte fuer eine Zahl, die immer gleich sein soll.

Formulare werden stattdessen INNERHALB ihrer Karte begrenzt. Das war der
eigentliche Grund fuer die Ausnahme -- ein Passwortfeld ueber 1280 Pixel sieht
albern aus. Nur gehoert diese Begrenzung ans Formular und nicht an die Seite.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 19:45:37 +02:00
D4rkst3randClaude Opus 5 2efc04b0b2 feat: nach Medienart filtern -- und die Endung schlaegt das Achselzucken
FILTER. Eine Leiste ueber der Galerie: Alles, Bilder, Video, Ton, Dokumente,
Anderes -- mit der Anzahl AM KNOPF. Ein Knopf "Video", hinter dem nichts liegt,
kostet sonst einen Klick, um das herauszufinden; leere Arten sind deshalb
sichtbar, aber gesperrt.

Die Uebersetzung Art -> SQL steht an EINER Stelle (ARTEN), weil zwei Stellen
zwangslaeufig auseinanderlaufen: die Liste zeigte dann etwas anderes, als der
Zaehler daneben behauptet. "Anderes" ist bewusst die VERNEINUNG der uebrigen
und keine eigene Liste -- sonst faellt ein neuer Typ durch alle Filter und ist
nirgends zu finden. Nachgemessen: die Summe der fuenf Arten ergibt die
Gesamtzahl.

/media/ids nimmt denselben Filter. Ohne das waehlt "alle waehlen" mehr aus, als
zu sehen ist -- und geloescht wuerde Unsichtbares.

DER FUND BEIM MESSEN: clip.mp4, ton.mp3, handbuch.pdf und notiz.txt landeten
ALLE unter "Anderes". Ein Formularteil ohne erkannte Art traegt
"application/octet-stream", und mein Code hat das der Endungstabelle
vorgezogen. Was der Absender ueber die Art sagt, gilt weiterhin -- aber nicht
sein Achselzucken. Danach: mp4 -> video/mp4, mp3 -> audio/mpeg, pdf und txt ->
Dokumente, und .zip bleibt richtigerweise "Anderes".

POLITUR. Ein ruhiger Verlauf im Hintergrund statt einer flachen Flaeche (ein
einfarbiges Dunkel sieht auf einem grossen Monitor aus wie ein Ladefehler),
Bildlaufleisten im Ton der Oberflaeche, ein Balken unter der aktiven Seite,
Karten mit einem Hauch Tiefe, ein spuerbarer Druckpunkt an den Knoepfen, und
Kacheln, die sich beim Darueberfahren leicht heben. Der Seitenwechsel blendet
auf -- und wer prefers-reduced-motion gesetzt hat, bekommt nichts davon.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 19:43:58 +02:00
D4rkst3randClaude Opus 5 4e613dd40d feat: ein Einstellungen-Tab -- und die Sicherung meldet sich zurueck
Die Zugangsdaten der Sicherung standen in einer Textdatei auf der Platte. Jetzt
stehen sie im Panel, zusammen mit der Discord-Einrichtung, die vom Konto
dorthin umgezogen ist.

DER EIGENTLICHE GEWINN IST NICHT DIE BEQUEMLICHKEIT. Eine Textdatei sagt
niemandem, ob die Sicherung heute Nacht gelaufen ist -- und eine Sicherung, die
still aufhoert zu laufen, ist der eigentliche Schaden, nicht die eine Nacht, in
der sie fehlt. Das Skript schreibt sein Ergebnis deshalb nach jedem Lauf in die
Datenbank, und die Karte zeigt es als Erstes: wann, ob erfolgreich, wie viele
Dateien, wie gross, und ob es in der Nextcloud gelandet ist oder nur lokal.
Nachgemessen: {"dateien":228,"groesse":12063419,"hoch":false,"ok":true}.

DAS PASSWORT GEHT NICHT UEBER DIE API. Das Skript laeuft auf dem Wirt, nicht im
Dienst -- es liest die Werte per `docker exec` DIREKT aus media.db. Ueber HTTP
geht das Passwort nur beim Eintragen im Browser, und das ueber TLS. Zurueck gibt
die API nur "hatPasswort: true", wie beim Discord-Geheimnis.

nextcloud.txt neben den Sicherungen gilt weiter und hat VORRANG: wer sie
angelegt hat, soll nicht suchen muessen, warum sie ploetzlich ignoriert wird.

Zwei Fussangeln beim Melden, beide beim Bauen aufgefallen:
- Der JSON-Text geht ueber die STANDARDEINGABE in den Container und nicht als
  Argument -- Anfuehrungszeichen ueberleben den Weg durch zwei Shells nicht.
- Auch dieses Hier-Dokument braucht das -replace "`r": dieselbe CRLF-Falle wie
  bei der Pruefung.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 19:39:16 +02:00
D4rkst3randClaude Opus 5 74cf56ce81 feat: eine Startseite, und die Sicherung geht in die Nextcloud
STARTSEITE. Wer das Dashboard oeffnet, landete bisher in der Galerie -- mit 228
Kacheln und ohne Antwort auf die erste Frage, die man beim Oeffnen hat: laeuft
alles? Jetzt zuerst eine Uebersicht: Zahlen, was heute dazukam, ein
Bilderstreifen der letzten Uploads, die letzten acht Schritte aus dem Verlauf,
die groessten Ordner, die Zugaenge -- und die ADRESSVORLAGE zum Kopieren, weil
das die haeufigste Frage ueberhaupt ist. Die Galerie zieht auf /galerie um.

Alles in EINER Antwort (/api/dash/uebersicht, gemessen 14 ms). Vier
Einzelabfragen waeren vier Runden ueber den Proxy und koennten auseinander-
laufen: Dateizahl aus der einen und letzte Ereignisse aus der naechsten Antwort
sind zwei verschiedene Zeitpunkte.

Der Router zog dabei nach src/router.ts um. Der Grund ist mehr als Geschmack:
die Startseite braucht eine Navigationsfunktion, die anderen Seiten nicht --
und sobald EINE Seite eine Eigenschaft mehr hat, passt die gemeinsame Liste in
App.tsx nicht mehr. Genau daran ist der erste Versuch gescheitert (TypeScript
hat es gefangen). Als Modulfunktion hat wieder jede Seite dieselbe Signatur.

SICHERUNG IN DIE NEXTCLOUD. Bisher lagen die Archive auf derselben Platte wie
die Daten -- gegen einen Plattenausfall half das gar nicht. Jetzt gehen sie per
WebDAV hinauf, mit curl statt rclone: Nextcloud spricht WebDAV, curl spricht
WebDAV, und curl ist ohnehin da.

Auch dort wird NACHGEZAEHLT: die Kopie wird zurueckgeholt und die Pruefsumme
verglichen. Ein PUT, der 201 sagt, hat nichts bewiesen -- abgeschnittene
Uebertragungen sehen genauso aus. Und ausgeduennt wird ebenfalls, sonst
waechst die Nextcloud still voll.

Die Zugangsdaten liegen NEBEN den Sicherungen (C:\backup\d4rk_media\
nextcloud.txt), nicht im Repo: sie gehoeren zur Maschine, nicht zum Code. Fehlt
die Datei, sagt das Skript das einmal und sichert lokal weiter -- eine
Sicherung, die wegen eines fehlenden Passworts GAR NICHT stattfindet, waere der
schlechtere Tausch.

Dabei einmal reingefallen: die Pruefung brach mit "sh: set: Illegal option -"
ab, nachdem ein Werkzeug die Datei mit CRLF gespeichert hatte -- die Shell im
Container liest dann "set -e\r". Sieht aus wie ein Tippfehler und ist keiner.
Das Skript entfernt die Wagenruecklaeufe jetzt selbst.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 19:27:09 +02:00
D4rkst3randClaude Opus 5 84cb3ee200 feat: Galerie fuer 900 Dateien, und ein Finder fuer Verwaiste
SORTIERUNG. Nach Pfad, Groesse, Anlage- oder Aenderungsdatum, auf- und
absteigend. Bisher gab es genau eine Reihenfolge (zuletzt geaendert), was bei
228 Dateien schon nicht mehr reicht.

Die Sortierung kommt vom Aufrufer und geht in SQL, also wird sie NIEMALS
durchgereicht, sondern gegen eine feste Liste getauscht. Nachgemessen mit
"sort=; DROP TABLE media--": faellt auf den Standard zurueck, Datenbank
unveraendert. Nach dem Pfad wird ohne Ruecksicht auf Gross- und Kleinschreibung
sortiert, sonst stuende "Adder" vor "adder" und dazwischen nichts.

ALLE TREFFER WAEHLEN. GET /media/ids liefert nur die IDs zur aktuellen
Auswahl -- ohne das braucht das Leeren eines Ordners mit 900 Bildern fuenfzehn
Seiten Klicken. Gedeckelt auf 5000, und wer daran stoesst, bekommt es GESAGT
(gekappt: true) statt still eine halbe Auswahl.

VERWAISTE. Ein Knopf zaehlt drei Arten von Abweichung: Dateien ohne Datensatz,
Datensaetze ohne Datei, Vorschauen ohne Bild. ERST ZAEHLEN, DANN FRAGEN -- die
Suche veraendert nichts.

Und das Aufraeumen geht in ZWEI Richtungen, wobei die schonende die
Voreinstellung ist: "aufnehmen" gibt Dateien ohne Datensatz einen, sie sind ja
da und werden ausgeliefert -- sie fehlen nur in der Liste. Erst "aufraeumen"
entfernt etwas, und auch dann nur Verweise auf Dateien, die es nicht mehr gibt,
und abgeleitete Vorschauen. Beides landet im Verlauf.

Am laufenden System durchgespielt: Datei am Dienst vorbei ins Volume gelegt ->
gefunden; aufgenommen -> steht in der Galerie; Datei hinter dem Ruecken
geloescht -> Datensatz ohne Datei gefunden; aufgeraeumt -> weg. Verlauf hat
beide Schritte mit user:admin.

(Der erste Versuch dieses Tests hat gelogen: Git Bash schreibt einen absoluten
Pfad in `docker exec` zu einem Windows-Pfad um -- rm suchte nach
"C:/Program Files/Git/data/...". `rm -f` schweigt dabei. Mit `sh -c` drumherum
bleibt der Pfad heil.)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 19:10:59 +02:00
D4rkst3randClaude Opus 5 4f149e365f feat: Discord-Bild und -Name im Dashboard
Wer ueber Discord hereinkommt, sieht sein Bild in der Kopfzeile, auf der
Kontoseite und neben seinen Eintraegen im Verlauf.

DIE ADRESSE BAUT DER DIENST, nicht die Oberflaeche. Gespeichert wird nur der
Bildstempel; das URL-Schema von Discord steht an EINER Stelle (avatarUrl in
db.ts) und nicht in jeder Ansicht, die ein Bild zeigt. Es hat drei Faelle, und
alle drei stehen dort:

  - Stempel faengt mit "a_" an  -> bewegtes Bild, muss als .gif angefragt werden
  - Stempel vorhanden           -> .png mit gewuenschter Groesse
  - kein Stempel                -> Discords Ersatzbild, dessen Nummer sich aus
                                   der ID ergibt: (id >> 22) % 6. Das ist die
                                   NEUE Rechnung; die alte ueber den
                                   Diskriminator gilt seit den Pseudonymen
                                   nicht mehr. Gegengerechnet an drei IDs.

Bild und Anzeigename werden BEI JEDER ANMELDUNG nachgezogen: wer sein Bild
wechselt, saehe sonst monatelang das alte und fragte sich, ob er im richtigen
Konto sitzt. Der Benutzername im Dashboard bleibt dagegen, wie er ist -- an ihm
haengen die Eintraege im Verlauf.

Der Avatar-Baustein kennt drei Faelle und keinen kaputten: Bild, sonst die
Initialen auf einer Farbe, die sich aus dem Namen ergibt (damit sie beim
Neuladen nicht springt), und fuer einen Token einen Schluessel -- ein Token hat
kein Gesicht, ein Platzhalterbild daneben waere eine Behauptung. Laedt das Bild
nicht (CDN nicht erreichbar, Stempel veraltet), faellt es auf die Initialen
zurueck statt ein kaputtes Bildsymbol zu zeigen.

Nachgemessen: cdn.discordapp.com liefert das Ersatzbild (200, image/png), die
Form der Bildadresse stimmt, und ein Passwort-Konto bekommt sauber avatar:null.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 18:59:42 +02:00
D4rkst3randClaude Opus 5 23405d68d3 feat: mehrere Rollen duerfen herein, nicht nur eine
Wer EINE der eingetragenen Rollen hat, kommt ins Dashboard. Mehrere sind der
Normalfall, sobald mehr als eine Person Zugang haben soll -- Admin und Fotograf
sind selten dieselbe Rolle.

Die bisherige einzelne Rolle geht NICHT verloren: der alte Schluessel
(discord.role_id) wird weitergelesen, bis die Liste einmal gepflegt wird; dann
raeumt das Speichern ihn weg. Zwei Quellen fuer dieselbe Frage laufen sonst
auseinander. Nachgemessen: die vorhandene Rolle stand nach dem Umbau
unveraendert drin, bereit blieb true.

Getrennt wird an Komma, Semikolon, Leerzeichen und Zeilenumbruch -- wer drei
IDs untereinander aus Discord kopiert, soll sie nicht erst zu einer Zeile
zusammenschieben. Doppelte fallen zusammen.

UND DIE SCHLECHTE WIRD BEIM NAMEN GENANNT: "123 ist keine Rollen-ID (17 bis 20
Ziffern)" statt "Rollen-ID ungueltig". Bei fuenf Eintraegen ist die zweite
Meldung eine Suchaufgabe.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 18:53:28 +02:00
D4rkst3randClaude Opus 5 1c5bf20eb0 feat: Medien im Fenster ansehen, und eine Auffrischung, die nichts wegzieht
ANSEHEN OHNE NEUEN TAB. Klick auf eine Kachel oeffnet den Betrachter: Bilder,
Videos, Ton und PDF direkt, alles andere mit ehrlichem Hinweis und
Herunterladen-Knopf -- ein Betrachter, der bei einer .zip ein schwarzes Feld
zeigt, sieht aus wie ein kaputter Betrachter. Pfeiltasten blaettern, Escape
schliesst; Adresse kopieren, neuer Tab und Loeschen sitzen in der Kopfzeile.

Gezeigt wird IMMER die Vollfassung, nie die Vorschau: 320 Pixel reichen fuer
eine Kachel, aber nicht dafuer, ein Bild zu beurteilen -- und genau dazu klickt
man darauf.

Damit musste die Auswahl umziehen: der Klick auf die Kachel oeffnet jetzt, das
Kaestchen oben links waehlt aus. Es erscheint beim Darueberfahren und bleibt
sichtbar, solange etwas gewaehlt ist.

AUTOMATISCH AUFFRISCHEN, aber nicht stumpf. Ein billiger Fuehler
(/api/dash/stand: drei Zahlen, GEMESSEN 6 ms je Abfrage ueber TLS und Proxy)
wird alle fuenf Sekunden gefragt; erst wenn er sich aendert, wird richtig
nachgeladen. Ein Tab im Hintergrund fragt gar nicht.

Die dritte Zahl ist kein Zierrat: nach einem LOESCHEN gehen Anzahl und
neuester Zeitstempel auf die alten Werte zurueck -- nur die Ereignis-ID zaehlt
weiter. Ohne sie bliebe eine geloeschte Datei in der Galerie stehen.
Nachgemessen.

UND ES ZIEHT NICHTS WEG: wer etwas ausgewaehlt hat, ein Bild offen hat oder
geblaettert ist, bekommt keinen Neuaufbau unter den Haenden, sondern einen
Knopf "Neues da -- laden". Eine Liste, die sich beim Klicken selbst neu
sortiert, ist der Grund, warum Leute Auto-Refresh abschalten. Der Schalter
merkt sich seinen Zustand.

Kein SSE, und das mit Absicht: dafuer braeuchte NPM zusaetzlich
`proxy_buffering off` -- ein weiterer Handgriff im Proxy, der vergessen werden
kann und dann zu einer Verbindung fuehrt, die scheinbar haengt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 18:49:02 +02:00
D4rkst3randClaude Opus 5 dffec7714c feat: Discord-Anmeldung -- im Panel einrichtbar, nicht in der .env
Auf Wunsch ueber die Oberflaeche statt ueber Umgebungsvariablen. Der Gewinn ist
nicht nur Bequemlichkeit: das Panel legt die REDIRECT-URI zum Kopieren hin, und
genau daran scheitert ein OAuth-Anlauf fast immer.

  settings.ts   Schluessel/Wert in SQLite, mit Umgebung als Rueckfall. Wer
                schon Stack-Variablen gesetzt hat, behaelt sie; das Panel
                uebersteuert. In der Umgebung bleibt, was den Dienst ueberhaupt
                erreichbar macht (PUBLIC_URL, PORT, DATA_DIR) -- wer das per
                Formular verstellen koennte, sperrt sich mit einem Klick aus.
  discord.ts    Der Weg in drei Schritten, ohne Bibliothek und OHNE BOT-TOKEN:
                die Rolle wird mit dem Merkmal des BENUTZERS geholt
                (guilds.members.read). Kein Bot, keine Bot-Rechte, kein zweites
                Geheimnis, das ablaufen kann.

DAS GEHEIMNIS GEHT NIE HERAUS -- auch nicht an eine angemeldete Sitzung. Das
Panel bekommt nur "hatGeheimnis: true". Ein Formular, das den Client-Schluessel
im Klartext zurueckschickt, streut ihn in jeden Browser-Cache. Ein leeres Feld
heisst "unveraendert", nicht "loeschen"; zum Entfernen gibt es "-".

JEDER ABBRUCH IST SICHTBAR: kein weisser Bildschirm, sondern zurueck aufs
Formular mit dem Grund im Adressfeld -- "Du bist nicht auf diesem Server",
"Dir fehlt die noetige Rolle", oder bei 4xx vom Tausch der Hinweis auf die
Redirect-URI. Die IDs werden schon beim Speichern geprueft (17-20 Ziffern),
damit ein halber Link nicht erst beim Anmelden auffaellt.

Das Passwort bleibt als Notausgang. Ein ueber Discord angelegtes Konto bekommt
KEIN brauchbares Passwort ("nur-ueber-discord"): verifyPassword scheitert an
allem, was nicht dem scrypt-Format entspricht -- nachgesehen, nicht vermutet.

Dazu der Verlauf, wie gewuenscht: Suche nach Pfad (mit derselben
LIKE-Maskierung wie in der Galerie -- Suche nach "_" findet 0 statt allem),
waehlbare Seitengroesse (25/50/100/200, Standard 50) und mehr Luft zwischen
"Groesse" und "Von wem".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 18:44:45 +02:00
D4rkst3randClaude Opus 5 0b295a2302 feat: Verlauf -- wer hat wann was abgelegt, ersetzt oder geloescht
Neuer Reiter, und dahinter eine eigene Tabelle. WARUM NICHT EINFACH
media.created_at -- drei Gruende, jeder allein wuerde reichen:

  1. media kennt nur den JETZIGEN Stand. Was geloescht wurde, ist weg, samt
     der Frage, wer es geloescht hat.
  2. Ein Ueberschreiben hebt updated_at an und ueberschreibt damit die
     Auskunft, wann die Datei urspruenglich kam.
  3. media.token_id ist beim Upload aus dem Dashboard NULL -- dort hat niemand
     einen Token, sondern eine Sitzung. Das "von wem" stand also nirgends.

Das ist NICHT das Logging, das die ROADMAP ablehnt: dort geht es um
Anfrage-Protokolle samt ClickHouse. Hier sind es ein paar Zeilen je Upload in
derselben SQLite-Datei, und nach einem halben Jahr raeumt pruneEvents auf.

DER VERLAUF STARTET NICHT LEER. Beim ersten Start wird er aus dem Bestand
nachgetragen (68 Eintraege) -- ein Tab, der am ersten Tag leer ist, obwohl 68
Bilder in der Ablage liegen, sieht aus wie ein kaputter Tab. Die nachgetragenen
Eintraege sind als solche gekennzeichnet: Zeitpunkt stimmt, Urheber steht als
"vor der Aufzeichnung", weil ihn niemand mehr kennt. Die Oberflaeche sagt das
unter der Tabelle, statt es zu verschweigen.

logEvent wirft nie: ein Verlauf, der einen Upload scheitern laesst, waere die
Buchhaltung, die das Geschaeft verhindert.

Nachgemessen: Upload, Ersetzen und Loeschen aus dem Dashboard erscheinen als
user:admin, die Uploads des Fotostudios als token:d4rk_photostudio; beim
Loeschen wird die Groesse VOR dem Loeschen geholt, sonst stuende dort nichts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 18:33:52 +02:00
D4rkst3randClaude Opus 5 3ddab937e7 feat: hochladen aus dem Dashboard -- Bilder, Videos, Ton, PDF
POST /api/dash/upload, eigener Weg neben /api/upload. Der dortige haengt an
einem Bearer-Token; wer im Dashboard sitzt, hat eine SITZUNG und keinen Token,
und einen Token nur zum Hochladen anzulegen waere ein Geheimnis mehr, das
jemand verlieren kann.

Mehrere Dateien auf einmal, und JEDE bekommt ihr eigenes Ergebnis mit Pfad,
Groesse und Grund im Fehlerfall. Ein "3 von 5 hochgeladen" ohne die Namen der
zwei anderen ist genau die Sorte Rueckmeldung, wegen der wir hier neu bauen.
Eine kaputte Datei bricht die anderen nicht ab.

parseBody({ all: true }) ist noetig: ohne das liefert Hono bei mehreren Feldern
gleichen Namens nur das LETZTE, und aus fuenf Dateien wuerde eine.

DATEINAMEN WERDEN ZURECHTGEBOGEN, und zwar sichtbar. checkPath laesst
"Mein Video (final) 2.MP4" mit Recht nicht durch, also macht safeFilename
daraus "mein-video-final-2.mp4" -- Umlaute werden umschrieben und nicht
weggeworfen ("Grüße" -> "gruesse", nicht "gr-e"). Die Ergebnisliste zeigt den
neuen Pfad neben dem alten Namen.

BEIM MESSEN GEFUNDEN: bleibt vom Namen nichts uebrig -- "ЖУРНАЛ.txt",
"   .png", "★★★.txt" --, hiessen bisher ALLE "datei.txt", und die zweite
Datei ueberschrieb die erste stillschweigend. Jetzt haengen die ersten acht
Stellen des SHA-256 daran: gleicher Inhalt ergibt denselben Namen,
verschiedener Inhalt verschiedene. Nachgemessen mit drei solchen Namen.

Der Fortschrittsbalken laeuft ueber XMLHttpRequest und nicht fetch: nur der
kennt upload.onprogress, und bei einem 40-MB-Video ist ein Balken der
Unterschied zwischen "es tut sich was" und "ist es abgestuerzt?".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 18:29:33 +02:00
D4rkst3randClaude Opus 5 f7086c231c feat: Ordneruebersicht in der Galerie -- und der Proxy haelt keine Bilder mehr fest
ORDNERUEBERSICHT. Eine Spalte neben der Galerie mit allen Ordnern, auf allen
Ebenen, mit Anzahl und Groesse; ein Klick filtert. Neu dafuer:

- GET /api/dash/folders -- die Ordner werden in JavaScript zusammengezaehlt
  und nicht in SQL: fuer eine Baumansicht braucht es JEDE Ebene, und das waere
  in SQLite eine rekursive Abfrage, die niemand mehr liest.
- GET /api/dash/media?folder=... als EIGENER Filter neben der Suche. Die Suche
  findet mit LIKE %...% auch "alt/vehicles/adder.webp"; ein Ordner, der Dinge
  zeigt, die nicht darin liegen, ist keiner. Hier ist der Anfang verankert.
- Die LIKE-Sonderzeichen werden maskiert: ein Ordner "fotos_alt" wuerde sonst
  auch "fotosXalt" treffen, weil _ in LIKE ein beliebiges Zeichen ist.

Die Zahlen sind kumulativ -- "vehicles" enthaelt, was in "vehicles/showroom"
liegt. Alles andere ueberrascht in einer Baumansicht. Nachgemessen ueber drei
Ebenen: vehicles (24) > showroom (2) > tief (1), Filter und Zaehlung stimmen,
und nach dem Aufraeumen steht wieder vehicles (22).

Flache Liste mit Einrueckung statt aufklappbarem Baum: bei einer Handvoll
Ordner ist ein Aufklappmechanismus mehr Bedienung als Nutzen -- und was man
nicht aufklappen muss, kann man auch nicht zugeklappt uebersehen.

Dazu: der NPM-Zwischenspeicher ist zu. Der Schnipsel steht im Feld "Custom
Nginx Configuration"; NPM setzt ihn innerhalb des server-Blocks ein, eine
location ist dort also gueltig. Nachgemessen: cache-control kommt wieder von
uns, und ein ueberschriebenes Bild ist sofort das neue.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 18:22:51 +02:00
D4rkst3randClaude Opus 5 aa2575bdf4 feat: Sicherung, Anmeldebremse, Vorschaubilder -- und Schritt 5
SICHERUNG (tools/sichern.ps1, taeglich 04:30 als geplante Aufgabe).
Die Datenbank wird nicht kopiert, sondern ueber SQLites eigene
Sicherungsschnittstelle herausgeholt (server/src/backup.ts): im WAL-Modus
liegt das Zuletzte noch nicht in media.db, und selbst alle drei Dateien zu
kopieren ist nicht sicher, wenn waehrenddessen geschrieben wird. Die Bilder
kommen aus einem NUR LESEND eingehaengten Volume dazu.

Und sie prueft sich selbst: auspacken, Datenbank oeffnen, Medien/Token/
Benutzer zaehlen, mit dem laufenden Dienst vergleichen, sonst Fehler. Einmal
wirklich zurueckgespielt -- leeres Volume, zweiter Dienst, Anmeldung mit dem
echten Passwort, Bild abgerufen, Byte fuer Byte identisch. Eine Sicherung, die
nie zurueckgespielt wurde, ist eine Hoffnung.

ANMELDEBREMSE. Das Formular steht oeffentlich, und scrypt macht einen Versuch
teuer -- aber teuer ist nicht selten. Fuenf freie Versuche je Adresse, dann
Sperre ab 30 s mit Verdopplung bis 15 min, 429 samt Retry-After und einem Text,
der sagt wie lange. Erfolg setzt zurueck. Nur im Speicher: wer sich aussperrt,
startet den Container neu. Durchgemessen bis zur Erholung.

VORSCHAUBILDER. sharp erzeugt beim Upload eine 320er WebP-Fassung unter
/data/thumbs, ausgeliefert unter /t/<pfad>. Gemessen: 131502 -> 13078 Bytes,
Faktor 10; eine Galerieseite faellt von 7,5 MB auf 766 KB. KEINE Spalte in der
Datenbank -- ob es eine Vorschau gibt, sagt das Dateisystem, und die Galerie
faellt bei 404 aufs Vollbild zurueck. Eine zweite Wahrheit, die auseinander-
laufen kann, gibt es damit gar nicht erst. Ein Knopf zieht Fehlendes nach und
nennt Zahlen statt "fertig".

SCHRITT 5. Die Fivemanage-Dateien liegen in legacy/ mit Erklaerung; unsere
docker-compose.media.yml heisst jetzt docker-compose.yml. Drei Volumes und drei
Abbilder geloescht, rund 670 MB -- nachgezaehlt war vorher, dass kein Bild
darin lag.

Nebenbefund: `npm i sharp` scheitert auf diesem Rechner nicht an sharp, sondern
an better-sqlite3 -- der Host laeuft auf Node 24 (ABI 137), node-gyp uebernimmt
und findet keine Bauwerkzeuge. Genau die Falle aus dem Dockerfile. Eingetragen
wurde mit --package-lock-only, gebaut wird im Abbild auf Node 22.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 18:00:14 +02:00
D4rkst3randClaude Opus 5 6c8ead8e15 feat: das Dashboard -- und die eine Regel steht im Code, nicht im Vorsatz
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>
2026-08-11 17:38:38 +02:00
D4rkst3randClaude Opus 5 65828b526c docs: Schritt 3 ist im Spiel gelaufen -- der Durchstich steht
/studio shot adder, und das Bild lag Sekunden spaeter unter
https://fivemanage.d4rkst3r.de/f/vehicles/adder.webp. Von der Dienstseite
gegengeprueft statt der Logzeile geglaubt: 132908 Bytes hier wie dort,
derselbe SHA-256, ausgeliefert als image/webp, erste Bytes RIFF....WEBP --
ein echtes WebP und kein falsch benanntes PNG.

Damit ist die letzte Frage aus dem Pruefstand beantwortet: PerformHttpRequest
verhaelt sich in FiveM wie die Faelschung, Kopfzeilen und base64-Rumpf kommen
unveraendert an, der Rueckruf erreicht den Event-Handler.

Als ungeprueft benannt bleibt, was ungeprueft ist: ein ganzer Serienlauf ueber
900 Fahrzeuge, und Upload.put -- die wartende Form, die der Lauf nicht nimmt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 17:21:54 +02:00
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
D4rkst3randClaude Opus 5 076d909f62 docs: CLAUDE.md -- die Arbeitsweise gehoert ins Repo, nicht in ein Gedaechtnis
"Pruefen statt raten" stand bisher nur in einer Notiz auf einer Maschine.
Wer dieses Projekt auf einem anderen Rechner aufmacht, faengt sonst ohne
sie an -- und genau dort wird gleich weitergearbeitet.

Dazu die vier Faelle aus diesem Repo, die erst beim Messen aufgefallen
sind, als Beleg statt als Behauptung: der Einhaengepunkt in c.req.path,
das Sternchen, das kein Parameter ist, das try/catch um createReadStream,
das Secure-Cookie, das ueber localhost funktioniert und ueber eine
LAN-Adresse nicht.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 16:31:11 +02:00
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
D4rkst3randClaude Opus 5 5edbe6c45f feat: ein Name statt zwei, und PUBLIC_URL ist nur noch der Ursprung
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/.

Der Ein-Namen-Betrieb war bis jetzt nicht ausdrueckbar. Der Kommentar in
config.ts versprach "leer lassen, wenn alles unter einem Namen laufen
soll", aber optional() faellt bei leerem Wert auf den Host aus PUBLIC_URL
zurueck -- und dann gilt JEDE Anfrage als Anfrage an den Dateiwirt: kein
Dashboard, keine API, /health gibt 404 und die Anmeldung 405 "hier gibt
es nur Dateien". Nichts ist kaputt, und niemand kommt darauf. Deshalb ist
der Ein-Namen-Betrieb jetzt der Standard und der zweite Name die Ansage.

PUBLIC_URL ist nur noch der Ursprung; das /f haengt der Dienst selbst an.
Wer es mitschriebe, bekaeme Adressen mit /f/f/, und das faellt erst auf,
wenn das erste Bild fehlt. Praefix und Route haengen jetzt an derselben
Stelle (config.filePrefix), damit die zurueckgegebene Adresse und die
Route, die sie ausliefert, nicht auseinanderlaufen koennen.

Beide Betriebsarten durchgemessen: mit einem Namen kommt
.../f/vehicles/adder.png zurueck und liefert die Datei; mit gesetztem
FILES_HOST kommt sie ohne Praefix, liegt unter dem Dateihost, ist am
Dashboard-Namen zusaetzlich unter /f/ erreichbar, und unter dem Dateihost
gibt es weiterhin keine API. Welche Betriebsart laeuft, sagt der Dienst
in der zweiten Startzeile.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 15:58:20 +02:00
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
D4rkst3randClaude Opus 5 cfd0f0747a fix: sechs Befunde aus dem ersten Start des Servers
Das Grundgeruest war typgeprueft, aber nie gelaufen. Beim ersten Start
haben sich sechs Dinge gezeigt, alle nachgemessen statt vermutet:

1. Die Token-Pruefung hing an '*' und galt damit auch fuer /api/dash/*,
   das daneben liegt. Das Dashboard bekam "Token fehlt oder ist
   unbekannt" auf die Anmeldung, obwohl es nie einen Token haben kann.
   Sie haengt jetzt an den drei eigenen Pfaden.

2. DELETE /api/media/... und GET /api/exists/... sahen am Ziel vorbei.
   c.req.path traegt den Einhaengepunkt mit, das replace(/^\/media\//)
   schnitt ihn nicht weg -- aus vehicles/adder.png wurde
   api/media/vehicles/adder.png. Loeschen fand nie etwas, exists meldete
   immer false. Jetzt :pfad{.+}; Hono liefert den Parameter fertig
   dekodiert (an einer Probe gemessen), ein zweites decodeURIComponent
   waere eine Dekodierung zu viel.

3. Verzeichnisdurchstieg in der SPA-Rueckfallroute: GET /..%5Cpackage.json
   hat unter Windows die Datei ausgeliefert. Hono reicht %5C durch,
   path.join behandelt den Backslash dort als Trenner, und eine
   Eindaemmung gab es nicht. Unter Linux traegt genau dieser Angriff
   nicht -- Glueck, keine Abwehr. Jetzt dieselbe resolve-Pruefung wie in
   storage.ts.

4. Die Auskunft "Oberflaeche 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.

5. CSS und JS kamen als application/octet-stream -- die MIME-Tabelle
   kennt nur Medientypen, das Dashboard haette weder Stylesheet noch
   Modul geladen. Die Oberflaeche bekommt eine eigene Tabelle: in der
   geteilten fehlt html mit Absicht, sonst koennte jeder mit einem
   Upload-Token eine Seite unter fivecdn.d4rkst3r.de veroeffentlichen.

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.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 15:39:33 +02:00
D4rkst3randClaude Opus 5 e36db8c015 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>
2026-08-11 15:20:48 +02:00
D4rkst3randClaude Opus 5 b1576cefb0 docs: "Create organization" tut nichts — neu laden hilft
Die Organisation wird angelegt. NewOrganizationRoute.tsx ruft nur mutate(data)
ohne onSuccess, ohne Navigation, ohne Meldung — die Seite bleibt einfach
stehen. Im Container-Log steht dazu nichts, weil der Handler nur im Fehlerfall
protokolliert; genau dieses Schweigen ist der Hinweis, dass es geklappt hat.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 15:04:12 +02:00
D4rkst3randClaude Opus 5 0b45f31c15 docs: zwei Eigenheiten festhalten, die sonst zweimal Zeit kosten
Das Tracing-Rauschen alle fuenf Minuten ist kein Fehler und laesst sich in
beta.23 nicht abstellen: otlpEndpoint ist eine Konstante, ENV waehlt nur
zwischen TLS und unverschluesselt. Da die Adresse localhost lautet, hilft auch
kein Jaeger-Container daneben — der muesste sich den Netzwerk-Namensraum
teilen.

Wichtiger: wenn ein Knopf in der Oberflaeche nichts tut, steht der Grund im
Log und nie auf dem Bildschirm. http-util.ts wirft jeden Fehler als
new Error(...) weiter, die Hooks pruefen auf "instanceof ApiError" — das ist
danach nie wahr. 401, 500 und ein falscher Rumpf sehen alle gleich aus.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 15:03:27 +02:00
D4rkst3randClaude Opus 5 e2e12409b8 fix: Host-Ports in die Grunddatei, hostports.yml entfaellt
Der Stack lief, meldete alle Container gesund — und beide Adressen gaben 502.
Grund: kein einziger Port veroeffentlicht. Die standen in
docker-compose.hostports.yml, und Portainer hat die nicht angewandt.

Das war mein Anleitungsfehler. Ich hatte geschrieben, man koenne unter
"Compose path" zwei Pfade mit Komma angeben. Portainer nimmt dort EINE Datei.
Die Ergaenzung wurde nicht etwa abgelehnt, sondern stillschweigend ignoriert —
die schlechteste Art zu scheitern, weil danach alles gesund aussieht.

Eine Trennung, die man nicht anwenden kann, ist keine. Die Ports stehen jetzt
in docker-compose.yml, wo dieser Server sie ohnehin braucht.

docker-compose.proxynet.yml bleibt als der sauberere Weg fuer einen Proxy im
selben Docker-Netz, jetzt aber mit dem Hinweis, dass er ueber Portainer-aus-
Repo nicht zu haben ist. Hier trifft er nicht zu: NPM liegt auf 172.17.0.3 im
Standard-Bridge-Netz.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 14:57:33 +02:00
D4rkst3randClaude Opus 5 65a04af8a1 fix: BUCKET_DOMAIN ergaenzt, minio-init entfernt
Zwei Funde beim Lesen von pkg/storage/s3/s3.go und der .env.template des
Projekts.

BUCKET_DOMAIN fehlte. AWS_ENDPOINT ist nur der Weg, auf dem Lite die Dateien
HINLEGT — http://minio:9000, Containername im internen Netz, und das ist
richtig so. Die oeffentliche Adresse, die Lite nach dem Upload zurueckgibt,
baut es aus BUCKET_DOMAIN. Die Variable steht nicht im README des Projekts,
nur in dessen .env.template (BUCKET_DOMAIN=http://localhost:9000/lite-dev,
also Endpunkt plus Bucket). Ohne sie waere jeder Upload scheinbar geglueckt
und das Bild von aussen nicht abrufbar — ein Fehler, der erst im Handy
auffaellt und dort nach einem CDN-Problem aussieht.

minio-init faellt weg, und zwar nicht nur als Ballast: Lite legt den Bucket
beim Start selbst an und setzt dabei die oeffentliche Leserichtlinie. Es
ueberspringt beides, wenn der Bucket schon existiert ("already exists.
skipping creation and policy application"). Mein Init-Container kam ihm zuvor
und haette damit genau die Richtlinie verhindert, die er ersetzen sollte. Dass
es trotzdem funktioniert haette — mc anonymous set download tut dasselbe —
macht es nicht besser, sondern nur unauffaelliger.

AWS_REGION folgt jetzt deren Vorlage (eu-west-1 statt auto). Fuer MinIO ist
sie gleichgueltig, das SDK verlangt aber eine.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 14:50:52 +02:00
D4rkst3randClaude Opus 5 ca85c4f135 fix: PostgreSQL statt MySQL — beta.23 kennt DB_DRIVER nicht
"panic: pgdriver: invalid scheme: lite"

Die Anwendung geht direkt in den PostgreSQL-Treiber; DB_DRIVER wird nicht
ausgewertet. Die MySQL-DSN (lite:pass@tcp(db:3306)/...) liest der Treiber als
URI und findet als Schema den Benutzernamen.

Der Fehler war meiner, und die Ursache lehrreich: ich hatte MySQL aus deren
docker-compose.test.yml uebernommen, weil die als einzige zeigt, wie der
App-Container verdrahtet wird. Sie benutzt aber fivemanage/lite:latest von
Docker Hub — und diese Reihe steht bei beta.16 still. Das README sagt
postgres://, und es hat recht. Ich habe der aelteren Datei mehr geglaubt als
der Doku, weil sie konkreter aussah.

Damit faellt auch die Behauptung, MySQL genuege und man koenne den vorhandenen
QBox-Server mitbenutzen. Stimmt fuer diese Fassung nicht.

Das Volume heisst jetzt pgdata statt db: wer den Stack schon mit MySQL laufen
hatte, bekommt eine frische Ablage, statt dass PostgreSQL ueber ein
MySQL-Verzeichnis stolpert. Das alte bleibt als Waise liegen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 14:43:48 +02:00
D4rkst3randClaude Opus 5 c5b66b466a fix: an alle Adressen binden, sonst erreicht NPM die Dienste nicht
Ich hatte die Ports an 127.0.0.1 gebunden — enger und auf den ersten Blick
richtiger. Nur erreicht host.docker.internal das auf diesem Server nicht: die
Anfrage kommt aus dem NPM-Container ueber die Docker-Bruecke herein, nicht
ueber Loopback, und ein an 127.0.0.1 gebundener Port nimmt sie nicht an.

Der Beleg stand die ganze Zeit in der Containerliste: d4rkbot (3080),
cdn-files (8090) und portainer sind alle ohne 127.0.0.1-Praefix
veroeffentlicht, und NPM erreicht sie. So laeuft es hier, also laeuft es so.

Damit macht die FIREWALL den Port zu und nicht die Bindung — das steht jetzt
ausdruecklich in beiden Dateien, mitsamt den Befehlen zum Nachsehen. Wer weiss,
dass Loopback bei ihm erreicht wird, setzt BIND_ADDR=127.0.0.1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 14:37:14 +02:00
D4rkst3randClaude Opus 5 d0f173d67d fix: freie Host-Ports, und der Weg ist jetzt belegt statt vermutet
Die Containerliste des Servers beantwortet beides.

Der NPM-Container hat 172.17.0.3 und liegt damit im Standard-Bridge-Netz,
nicht in "web". Meine Vermutung, "web" sei das Proxy-Netz, war falsch — es
gilt Weg B ueber den Host, so wie d4rkbot auf 3080 und cdn-files auf 8090
schon laufen. Im README steht das jetzt als Feststellung und nicht mehr als
Wahrscheinlichkeit.

Und die Ergaenzung wollte ausgerechnet 9000 und 8080 oeffnen — beide vergeben,
an Portainer und an nextcloud-aio. Jetzt 9100 und 9101, und ueber
MINIO_HOST_PORT und LITE_HOST_PORT einstellbar: die naechste Kollision ist dann
eine Variable und keine Dateiaenderung. Im Container bleibt es bei 9000 und
8080; nur die Seite zum Host wandert.

Belegt waren: 80, 81, 443, 2224, 3000, 3080, 3478, 8000, 8080, 8090, 8443,
9000, 9443, 11000.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 14:34:32 +02:00
D4rkst3randClaude Opus 5 9c690c46b9 fix: das externe Netz war fest verdrahtet und existierte nicht
"network proxy declared as external, but could not be found".

Die Hauptdatei verlangte ein externes Netz namens proxy, das es auf diesem
Server gar nicht gibt — und die Ergaenzung fuer host.docker.internal half
nicht, weil die Basis es trotzdem forderte. Das war die falsche Reihenfolge:
der Weg nach aussen ist gerade das, was sich je Aufbau unterscheidet, und
gehoert damit nicht in die Basis.

Jetzt ist docker-compose.yml neutral — von aussen nicht erreichbar, und das
ausdruecklich. Dazu waehlt man eine von zwei Ergaenzungen:

  docker-compose.proxynet.yml    Proxy haengt im selben Netz (http://minio:9000)
  docker-compose.hostports.yml   Proxy geht ueber den Host

Die Netzliste dieses Servers zeigt kein "proxy", aber ein "web" — ein
Bridge-Netz ohne Stack, also die uebliche Konvention fuer einen Reverse Proxy.
PROXY_NETWORK steht deshalb auf "web", mit dem Hinweis, es nachzusehen statt zu
glauben.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 14:31:05 +02:00
D4rkst3randClaude Opus 5 44822e2831 fix: das Image liegt auf ghcr.io, nicht auf Docker Hub
'docker.io/fivemanage/lite:0.1.0-beta.23: not found' — und zwar zu Recht.

Ich hatte die Registry aus deren docker-compose.test.yml uebernommen
(fivemanage/lite:latest, also Docker Hub) und den Tag aus den GitHub-Releases
(beta.23). Jede Haelfte fuer sich stimmte, die Kombination nicht.

Nachgesehen statt geraten:

  ghcr.io/fivemanage/lite     beta.18 bis beta.23 und latest
  docker.io/fivemanage/lite   endet bei beta.16

Docker Hub wird offenbar nicht mehr gepflegt. Das README des Projekts hat es
richtig: dort steht ghcr.io.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 14:27:39 +02:00
D4rkst3randClaude Opus 5 d33db5eb79 README: die Geheimnisse in PowerShell erzeugen
Der bisherige Befehl war Bash und lief in Windows PowerShell 5.1 nicht — dort
gibt es weder printf noch openssl. Die PowerShell-Fassung steht jetzt zuerst,
Bash daneben.

Zwei Fallen sind darin vermerkt, beide bereits hineingetreten:

  RandomNumberGenerator::Fill gibt es erst ab .NET 6; Windows PowerShell 5.1
  laeuft auf .NET Framework und kennt nur ::Create().GetBytes().

  Set-Content -Encoding utf8 schreibt in 5.1 ein BOM voran. Dann heisst die
  erste Variable nicht DB_PASSWORD, und der Stack startet mit einer fehlenden
  Angabe, deren Grund man nicht sieht. Der Inhalt ist ohnehin ASCII.

Dazu der Hinweis, dass Get-Random hier nicht taugt: es ist nicht
kryptografisch sicher, und erzeugt wird ein Token-Signaturgeheimnis.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 14:20:48 +02:00
D4rkst3randClaude Opus 5 ba5c733e27 Ergaenzung fuer einen Reverse Proxy ausserhalb des Docker-Netzes
Die Compose war fuer den sauberen Weg gebaut: NPM im selben Netz, kein Port
offen. Auf diesem Server laeuft NPM aber anders — Bot und Hub haengen dort an
`http://host.docker.internal:3080`, also ueber den Host. So wie sie war, haette
NPM `minio` gar nicht gefunden.

docker-compose.hostports.yml legt deshalb die beiden noetigen Ports offen, und
zwar an 127.0.0.1 gebunden: damit ist der Port vom Internet aus zu, und nur der
Host kommt heran.

Mit einem Vorbehalt, der in der Datei steht: ob `host.docker.internal` eine
Bindung an 127.0.0.1 ueberhaupt erreicht, haengt an der Docker-Fassung. Unter
Linux fuehrt der Weg ueber die Docker-Bruecke statt ueber Loopback, dann muss
die Bindung 0.0.0.0 lauten und die Firewall den Port zumachen. Klappt es nicht,
ist die Bindung nicht der Fehler, sondern der Weg.

Sauberer bleibt Weg A — NPM ins Netz `proxy` haengen. Dann braucht es diese
Datei gar nicht.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 14:16:52 +02:00
D4rkst3randClaude Opus 5 72f61dbd07 Medien-Stack: Fivemanage Lite mit MySQL und MinIO
Ein Ablageort fuer alle Resourcen statt Nextcloud plus rclone von Hand. Der
Unterschied ist nicht der Speicher — den gibt es schon — sondern ein
Schreibweg ueber HTTP: sobald ein Script hochladen soll, fehlt er, und Lite
bringt ihn mit, samt Token je Resource und einer Oberflaeche zum Nachsehen.

Zusammengestellt aus fivemanage/lite: dessen README, der
deployments/docker-compose.yml (Entwicklung, startet die App gar nicht) und der
deployments/docker-compose.test.yml (zeigt die App-Verdrahtung und dass MySQL
genuegt und ClickHouse nicht Pflicht ist).

Zwei Fassungen, und der Unterschied ist Absicht:

  docker-compose.yml         fuer den Betrieb ueber Portainer aus diesem Repo.
                             Keine offenen Ports, nur der Reverse Proxy
                             spricht mit der App.
  docker-compose.local.yml   zum Ausprobieren auf Docker Desktop. Ports offen,
                             kein Proxy. Eine beta.23 gehoert erst auf einen
                             Rechner, an dem nichts haengt.

Ohne ClickHouse und Jaeger: beides ist Logging und Tracing, fuer das Ablegen
von Bildern nicht noetig, und ClickHouse ist eine schwere Abhaengigkeit.

Ein Init-Container legt den Bucket an und stellt ihn auf oeffentlich lesbar.
Ohne diesen Schritt schlaegt der erste Upload fehl, und die Meldung nennt den
Grund nicht.

Healthchecks mit depends_on/condition, sonst startet die App gegen eine
Datenbank, die noch nicht antwortet, und beendet sich — beim ersten Hochfahren
jedes Mal.

NICHT laufen gelassen: Docker war von hier nicht erreichbar. Das YAML ist
geprueft, das Compose-Schema nicht.

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