96 Commits
Author SHA1 Message Date
D4rkst3randClaude Opus 5 06285a3a53 Ein Token darf jetzt mehrere Ordner -- statt einem oder allen
Das Fotostudio schreibt nach vehicles/ UND items/, der Token war aber auf
"vehicles" gefesselt. Damit blieb nur die Wahl zwischen "einer davon" und
"darf ueberall": das eine lehnt die Haelfte der Uploads mit 403 ab, das andere
erlaubt auch sicherungen/.

Gemessen und dabei aufgefallen: config.upload.lua setzt kein clothingPath,
also gilt die Vorgabe items/{model}.png -- und die durfte dieser Token nicht.
Kleidungs-Uploads aus dem Studio sind also stillschweigend an 403 gescheitert.

Die Spalte bleibt ein Textfeld, mehrere Werte stehen durch Komma darin. Damit
gilt JEDER bestehende Token unveraendert weiter: ein Wert ohne Komma ist eine
Liste mit einem Eintrag. Kein Migrationsschritt, kein Sonderfall.

Geprueft mit acht Faellen, darunter zwei, die scheitern MUESSEN: items/ unter
der Fessel "vehicles" (403) und "vehiclesXYZ/a.png" unter "vehicles" -- das
angehaengte / war schon vorher da und bleibt, sonst wuerde ein Praefix seinen
eigenen Namensvetter durchlassen.

Die 403-Meldung nennt jetzt alle erlaubten Ordner. Vorher haette sie bei
mehreren Fesseln "vehicles,items/" geschrieben -- einen Ordner, den es nicht
gibt. Wer abgewiesen wird, soll lesen koennen, wohin er darf.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 13:57:17 +02:00
D4rkst3randClaude Opus 5 c9e5bb3c31 Verschieben hatte denselben Fehler -- und Ordner entfernen gab es nur im Dienst
VERSCHIEBEN: derselbe Griff wie beim Loeschen, dieselbe Wirkung.

    onClick={() => setVerschieben(items.filter((i) => gewaehlt.has(i.id)))}

`items` ist die offene Seite; von 3565 gewaehlten Bildern wanderten 60. Ich
hatte gestern nur das Loeschen repariert und diese Stelle nicht gesucht --
zwei Knoepfe mit demselben Muster, und ich habe einen davon uebersehen.

Jetzt haelt der Auftrag auch hier `ids` (alle) und `namen` (die benennbaren)
getrennt, und verschoben wird in Haeppchen zu 200 mit Fortschritt im Knopf.
Was nicht ging, wird benannt statt verschwiegen.

ORDNER ENTFERNEN: die Route gab es (`DELETE /folders/:pfad`), die
Schnittstelle auch (`ordnerEntfernen` in api.ts) -- nur wurde sie NIRGENDS
aufgerufen. Eine Funktion ohne Knopf ist fuer den, der sie sucht, nicht
vorhanden; dieselbe Lehre steht schon ueber "Neuer Ordner" in derselben Datei.

Der Knopf sitzt jetzt neben dem Umbenennen, erst beim Darueberfahren sichtbar.
Die REGEL bleibt beim Dienst: er lehnt einen Ordner mit Inhalt ab und sagt,
wieviele Dateien darin liegen. Sie in der Oberflaeche zu wiederholen hiesse,
sie zweimal zu pflegen -- und die zweite Fassung ist irgendwann die falsche.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 13:44:38 +02:00
D4rkst3randClaude Opus 5 8a17b8b974 Loeschen loeschte nur die sichtbare Seite
Gemeldet: alle 3565 Bilder eines Ordners gewaehlt, auf "3565 loeschen"
gedrueckt -- weg waren 60. Sechzig ist die Seitengroesse, und darin steckt der
ganze Fehler:

    onClick={() => setLoeschen(items.filter((i) => gewaehlt.has(i.id)))}

`items` ist die OFFENE SEITE. Der Knopf zaehlte die Auswahl, geloescht wurde
die Schnittmenge mit dem gerade Sichtbaren. Die Absicht dahinter war gut --
der Dialog will die Pfade zeigen, und die kennt er nur fuer geladene Eintraege
-- aber das Ergebnis war eine Zahl, die etwas anderes versprach als sie tat.

Jetzt haelt der Auftrag beides getrennt: `ids` sind ALLE Gewaehlten, `namen`
nur die benennbaren. Der Dialog zeigt bis zu zwoelf Pfade und sagt "und N
weitere" -- ohne eine Vollstaendigkeit vorzutaeuschen, die er nicht hat.

IN HAEPPCHEN ZU 200. Der Dienst verschiebt jede Datei einzeln; 3565 in einer
Anfrage laufen minutenlang und fallen beim Proxy in die Zeitgrenze -- dann
waere die Haelfte weg und die Oberflaeche meldete einen Fehler. Nach jeder
Runde steht im Knopf, wie weit es ist: bei etwas, das eine Minute dauert, ist
Stillstand von Haengen nicht zu unterscheiden. Bricht eine Runde ab, wird
abgebrochen und die Zahl genannt, die schon durch ist.

UND DER WARNTEXT WAR FALSCH. Dort stand "Es gibt keine Sicherung, aus der sie
zurueckkommt" -- aus der Zeit vor dem Papierkorb. Nachgesehen: `inPapierkorb`
VERSCHIEBT die Datei und nimmt den Eintrag zurueck, wenn das Verschieben
scheitert; 30 Tage Aufbewahrung. Eine Warnung, die schlimmer klingt als die
Lage, haelt vom Aufraeumen ab und ist damit so schaedlich wie eine zu harmlose.
Der Knopf heisst jetzt "In den Papierkorb".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 11:55:26 +02:00
D4rkst3r a99e792194 gitignore: die Arbeitsliste 2026-08-22 23:48:50 +02:00
D4rkst3randClaude Opus 5 a7e9332bad Die Arbeitsliste gehoert nicht ins Repo
fehlende-item-bilder.txt war eine Momentaufnahme fuer den Betreiber und ist
seit der Umbenennung ohnehin ueberholt. Die Datei bleibt lokal liegen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 23:48:50 +02:00
D4rkst3randClaude Opus 5 a373b962d0 Beim Hochladen bleibt die Schreibweise -- sie war eine stille Verstuemmelung
Der Betreiber setzte in FiveM `inventory:imagepath` auf das Portal, und die
meisten Item-Icons blieben leer. Gemessen:

    WEAPON_COMBATPISTOL.png  -> 404
    weapon_combatpistol.png  -> 200

ox_inventory fragt Item-Bilder unter dem EXAKTEN Item-Namen ab; seine Waffen
heissen WEAPON_*. Der Dienst laeuft auf Linux, dort ist die Schreibweise ein
Unterschied. Zur Gegenprobe die mitgelieferten Bilder von ox_inventory selbst:
113 von 198 sind grossgeschrieben.

Die Ursache lag hier: `zaehmen()` machte beim Hochladen per Fenster jeden
Namen klein -- "vorhersagbar". Fuer die ZEICHEN war das richtig (Leerzeichen,
Klammern, Umlaute haben in einer Adresse nichts verloren), fuer die
SCHREIBWEISE war es nie noetig: checkPath erlaubt [A-Za-z0-9] seit jeher, und
in einer URL ist ein Grossbuchstabe kein Problem.

Also bleibt die Schreibweise jetzt stehen. Umlaute werden weiter ersetzt, nun
in beiden Formen -- sonst wuerde aus "Gruen.png" ein "gr-n.png", dieselbe
stille Verstuemmelung eine Ebene tiefer. Die ENDUNG bleibt klein: daran haengt
nur der Dateityp, und ".PNG" waere eine Adresse, die niemand so tippt.

Dazu 109 vorhandene Bilder umbenannt (ueber moveFile, damit Vorschau und WebP
mitwandern; jede Umbenennung steht im Ereignisprotokoll). Vorher: 0 Kollisionen
im Trockenlauf. Nachher gemessen: WEAPON_COMBATPISTOL 200, und 156 von 226
Items haben ein exakt passendes Bild statt 47.

EIGENER FEHLER, DER DAS VERZOEGERT HAT: mein erster Abgleich verglich beide
Seiten kleingeschrieben und hat damit genau den Unterschied wegnormalisiert,
auf den es ankam -- ich meldete 56 leere Felder statt 174. Eine Pruefung, die
das Falsche misst, ist schlimmer als keine.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 23:48:36 +02:00
D4rkst3randClaude Opus 5 0f0097ba3c Die Freigabeseite zaehlte ihre eigene Kappung als Gesamtzahl
Gemeldet: die Uebersichtsseite zeigt 2000 Bilder, die Galerie 3685. Gemessen
am Ordner "items": es sind WIRKLICH 3685. Es fehlt keine Datei -- die Seite
zaehlte falsch.

Der Grund stand in zwei Zeilen nebeneinander:

    ... ORDER BY path COLLATE NOCASE LIMIT 2000
    anzahl: zeilen.length,

Die Zahl war also nicht die Anzahl der Dateien, sondern die Anzahl der
geholten Zeilen -- und damit machte die Seite aus ihrer eigenen Kappung eine
Aussage ueber den Bestand. Genau der Fall aus CLAUDE.md: eine Auskunft, die
nicht alles sehen kann, meldet nicht die halbe Zahl als ganze.

Jetzt: COUNT(*) getrennt fuer die wahre Zahl, die Grenze bleibt (20000, damit
eine Freigabe auf einen riesigen Ordner den Dienst nicht erschlaegt) und wird
als `gezeigt` mit ausgeliefert. Sind beide gleich, ist die Liste vollstaendig.

DABEI EINEN EIGENEN FEHLER GEMACHT UND ER IST LEHRREICH: die neue Abfrage bekam
EINEN Backslash im ESCAPE statt zwei, und SQLite antwortete mit "ESCAPE
expression must be a single character". Meine Gegenprobe mit grep suchte nach
derselben falschen Fassung und meldete deshalb "passt". Erst `cat -v` neben der
funktionierenden Zeile hat es gezeigt -- eine Pruefung, die das Falsche misst,
ist schlimmer als keine.

Gemessen nach dem Ausrollen: anzahl 3685, gezeigt 3685, items 3685.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 23:11:28 +02:00
D4rkst3randClaude Opus 5 ab7605a6d2 Der Waechter kennt jetzt die Verwaltungszugaenge von Spieleservern
Der Betreiber hat es angemahnt, waehrend nebenan am Spiele-Panel gearbeitet
wurde: "vergiss nicht die ports an den waechter weiter zu geben."

Nachgesehen und gemessen -- die offenen Ports des Panels standen bereits in
der Basis (25566, 26014), keine Abweichung. Dabei ist etwas anderes
aufgefallen.

AUF 10011 LAUSCHT ts3server.exe AUF ALLEN ADRESSEN, aus C:\Server\TS3, seit dem
09.07.2026. Und eine handangelegte Regel laesst herein:

    Regelname:   TS3_10011 (TCP)
    Richtung:    Eingehend
    Profile:     Domaene, Privat, Oeffentlich
    Remote-IP:   Beliebig
    Aktion:      Zulassen

Das ist kein Spielport. Ueber ServerQuery verwaltet man einen TeamSpeak-Server
komplett -- Rechte, Konten, Kennwoerter. Im Panel nebenan ist derselbe Port
deshalb ausdruecklich an 127.0.0.1 gebunden und bekommt KEINE Firewallregel.

DER WAECHTER SCHWIEG DAZU, weil 10011 beim Uebernehmen des Standes schon
dastand -- also genau der Fall, fuer den die HEIKEL-Liste gedacht ist: "Port
3000 war auch schon immer so."

Dazu drei weitere aus derselben Familie, bevor sie hier auftauchen: Minecraft
RCON (25575), Rust RCON (28016) und die Palworld-REST-API (8212, kann kicken
und bannen). Im Panel stehen alle drei auf `veroeffentlichen: 'lokal'`; wenn
einer von ihnen je auf allen Adressen lauscht, ist etwas an einem Server von
Hand eingerichtet worden.

Gemessen nach der Aenderung (-Still): der Waechter meldet 10011 jetzt neben
RPC, SMB, MySQL, RDP und WinRM. Was er weiterhin NICHT sieht, ist UDP -- und
die meisten Spielports sind UDP. Das steht als naechstes an.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 23:03:12 +02:00
D4rkst3randClaude Opus 5 0a7d83aee3 docs: der Proxy konnte seit dem Aufraeumen nicht mehr schreiben
Beim Anlegen eines NPM-Benutzers kam "Internal Error". Im Container-Log stand
"attempt to write a readonly database" -- und der Prozess hielt einen Zeiger
auf /data/database.sqlite (deleted).

DAS WAR DAS AUFRAEUMEN NACH DEM VORFALL SELBST. Die Datenbank wurde am
12.08. um 22:11 per docker cp zurueckgeschrieben, und docker cp ERSETZT die
Datei statt sie zu ueberschreiben. Der seit dem 01.08. laufende Prozess zeigte
danach ins Leere. Rechte, Eigentuemer und Platz waren dabei alle in Ordnung --
die Meldung "readonly" fuehrt in die Irre.

Fuenfzehn Stunden unbemerkt, weil nur Schreibvorgaenge betroffen waren und
niemand welche ausloeste. Gekostet haette es ab dem 03.09. die
Zertifikatsverlaengerungen und ab dem 03.10. gueltige Zertifikate fuer proxy,
portainer und cdn -- ohne Meldung, ohne dass jemand etwas geaendert haette.

Behoben durch Neustart, 4 s Ausfall. Danach zeigt der Zeiger wieder richtig,
alle sechs Seiten antworten, der Benutzer liess sich anlegen.

Und der zweite Teil der Lehre steht auch dort: der Waechter haette das nicht
gefunden. Er zaehlt Ports und rechnet Pruefsummen, er versucht nirgends zu
schreiben.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 13:51:17 +02:00
D4rkst3randClaude Opus 5 4af8366517 fix: der Merker wird erst gesetzt, wenn die Meldung wirklich rausging
Beim ersten echten Meldetest lief das Skript ohne -Webhook. Es hat den Alarm
angezeigt, NICHTS gesendet -- und den Zustand trotzdem als "gemeldet"
abgehakt. Der naechste Lauf MIT Webhook waere still geblieben.

Ein Merker bedeutet "dieser Zustand wurde berichtet". Wenn nichts berichtet
wurde, darf er nicht gesetzt werden -- sonst verschluckt ausgerechnet der
Fehlerfall die Meldung, fuer die das Ganze da ist.

Jetzt vier Faelle, sauber getrennt:

    alles gut         -> Merker leeren, keine Nachricht noetig
    schon gemeldet    -> still bleiben
    kein Webhook      -> sagen, dass NICHTS gemeldet wurde, Merker nicht setzen
    Webhook da        -> senden; NUR bei HTTP 200/204 den Merker setzen

Der letzte Punkt ist der zweite Teil derselben Sache: schlaegt das Senden fehl,
bleibt der Merker leer und der naechste Lauf versucht es erneut. Eine Meldung,
die im Netz haengengeblieben ist, ist keine Meldung.

Nachgemessen, beide Richtungen:

    Alarm ohne Webhook  -> "NICHTS gemeldet", Merker vorhanden: False
    derselbe mit Webhook -> "Gemeldet (HTTP 204)", Merker gesetzt: True

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 11:09:16 +02:00
D4rkst3randClaude Opus 5 befa393b84 fix: das Alter kommt aus dem NAMEN, nicht aus dem Zeitstempel
Der erste echte Lauf auf dem Heimrechner sagte:

    d4rk_media-2026-08-11-2307.tar.gz, 0 Tage alt

Ein Archiv vom 11. August, an einem 13. August, mit "0 Tage alt" -- und
ausserdem war es gar nicht das neueste. Beides derselbe Fehler: ich habe nach
LastWriteTime sortiert und gerechnet, und die setzt der Nextcloud-Client auf
den Zeitpunkt des HERUNTERLADENS.

Damit war der Alarm wertlos. Er haette nie ausgeloest, weil frisch geholte
Archive immer taufrisch aussehen -- also genau in dem Fall geschwiegen, fuer
den er gebaut ist.

Jetzt wird das Datum aus dem Dateinamen gelesen (d4rk_media-2026-08-13-0929,
d4rkbot-2026-08-12). Das ist das Datum der SICHERUNG, und es ueberlebt jedes
Kopieren. Faellt der Name aus dem Muster, bleibt der Zeitstempel als Notnagel.

Nachgemessen mit Dateien, die ALLE den heutigen Zeitstempel tragen:

    neuestes erkannt: d4rk_media-2026-08-13-0929   0 Tage
    dieses entfernt -> d4rkbot-2026-08-12          1.4 Tage

Ausserdem sagt die Zeile jetzt, wieviel in der QUELLE liegt: "0 neu, 3 in der
Ablage, 5 in der Quelle" beantwortet die Frage, ob der Client ueberhaupt schon
durch ist -- "3 insgesamt" allein tat das nicht.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 10:27:38 +02:00
D4rkst3randClaude Opus 5 9e4132cc7e fix: Platzhalter erkennen, statt den Rechner damit lahmzulegen
Der Heimrechner ist beim ersten Lauf haengengeblieben. Ursache:

    d4rk_media-2026-08-11-2307.tar.gz  327030051  Archive, ReparsePoint
    d4rk_media-2026-08-12-0430.tar.gz  352258706  4199968

4199968 = 0x401620:
    0x400000  RECALL_ON_DATA_ACCESS   beim Zugriff erst herunterladen
    0x001000  OFFLINE                 Inhalt liegt nicht hier
    0x000400  ReparsePoint
    0x000200  SPARSE_FILE
    0x000020  Archive

Der Nextcloud-Client hatte "virtuelle Dateien" aktiviert: im Ordner steht ein
Verweis mit der richtigen GROESSE, aber ohne Inhalt. robocopy fasst alle an,
und jedes Anfassen loest einen Download aus -- bei fuenf Archiven 1,7 GB auf
einmal, waehrend Windows blockierte.

Das Skript prueft das jetzt VORHER und bricht ab, ohne etwas zu kopieren oder
zu loeschen. Mit dem Hinweis, wo man es abstellt, und woran man es im Explorer
sieht: Wolken-Symbol statt gruenem Haken.

An einer Datei mit gesetztem OFFLINE-Attribut nachgemessen -- erkannt und
abgebrochen; ohne das Attribut laeuft es normal durch.

Die Lehre ist dieselbe wie den ganzen Tag: eine Groessenangabe ist keine Datei.
robocopy hat die 336 MB geglaubt, die dastanden, statt zu pruefen, ob sie da
sind.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 10:22:53 +02:00
D4rkst3randClaude Opus 5 89d0ec2b14 fix: das Abholskript sagt jetzt, WORAN es haengt
Gemeldet: "danach nur ==> Nachsehen und dann passiert nichts mehr."

Zwischen der Ueberschrift und der ersten Ausgabe liefen drei Schritte, die
alle drei minutenlang blockieren koennen -- Test-Path, New-Item und
Resolve-Path, wenn ein Pfad auf eine Netzfreigabe oder eine schlafende Platte
zeigt. Welcher davon stand, war von aussen nicht zu sehen.

Jetzt sagt jeder Schritt VORHER, was er tut. Eine Ausgabe, die erst nach dem
Blockieren kommt, hilft genau dann nicht, wenn man sie braucht.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 10:06:11 +02:00
D4rkst3randClaude Opus 5 0f3f63749c feat: tools/heim-abholen.ps1 -- die Auswaertskopie, die der Server nicht loeschen kann
Der Weg nach draussen fuehrt jetzt ueber die Nextcloud, die ohnehin jede Nacht
ein geprueftes Archiv bekommt: der Desktop-Client zieht den Ordner auf die
Platte zu Hause. Serverseitig ist dafuer nichts noetig -- kein Konto, kein
Schluessel, kein Skript.

WARUM DAS ALLEIN NICHT REICHT: der Client gleicht in BEIDE Richtungen ab.
Verschwinden die Archive auf dem Server -- Trojaner, Fehlgriff, kaputter
Auftrag --, loescht er sie auch zu Hause. Der synchronisierte Ordner ist eine
Kopie, aber keine Sicherung.

Dieses Skript kopiert daraus in einen zweiten Ordner, den der Client nicht
kennt, und bricht damit die Kette: was einmal dort liegt, bleibt liegen, bis
die Aufbewahrungsfrist es wegraeumt. Der Server kann es nicht mehr erreichen.

robocopy OHNE /MIR, und das ist der ganze Punkt -- /MIR wuerde das Ziel dem
Quellordner angleichen und dort Geloeschtes mitloeschen.

Es weigert sich ausserdem, wenn das Ziel IM synchronisierten Ordner liegt: dann
wuerde der Client die Kopien wieder hochladen und alles waere umsonst.

UND ES MELDET SICH, WENN NICHTS MEHR ANKOMMT. Ein Kopierauftrag, der still
aufhoert, ist genau der Schaden, gegen den er gebaut ist: man glaubt, man haette
eine Sicherung, und merkt es an dem Tag, an dem man sie braucht. Gemeldet wird
nur bei Zustandswechsel -- eine taegliche "alles in Ordnung"-Meldung liest nach
einer Woche niemand mehr, und dann auch die eine nicht, auf die es ankommt.

An Wegwerf-Ordnern durchgespielt, alle Faelle einzeln:

    drei Archive holen                    -> 3 neu
    Datei in der QUELLE geloescht         -> bleibt im Ziel        <- der Punkt
    Aufbewahrung auf 2                    -> aeltestes entfernt
    seit 8 Tagen nichts Neues             -> Alarm
    zweiter Lauf                          -> bleibt still
    neues Archiv kommt an                 -> Entwarnung
    Ziel im Quellordner                   -> abgelehnt

Der vierte Test hat beim ersten Anlauf nichts gemessen: ich hatte die KOPIEN
gealtert statt der Quelle, und robocopy hat sie korrekt neu geholt. Ein Test,
der das Falsche misst, meldet "in Ordnung" und wird geglaubt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 09:49:06 +02:00
D4rkst3randClaude Opus 5 6e654e0ef8 fix: die Sicherung laedt wieder hoch -- ueber localhost statt ueber Cloudflare
Der naechtliche Lauf scheiterte mit HTTP 413. Nachgemessen mit drei Groessen,
statt geraten:

    1 KB -> 201 | 60 MB -> 201 | 340 MB -> 413, "Server: cloudflare"

cdn.d4rkst3r.de laeuft ueber Cloudflare, und das deckelt Uploads im Gratistarif
bei 100 MB. Das Archiv ist 336 MB. Weder der Proxy (client_max_body_size 0) noch
die Nextcloud (16 GB) waren es.

Bestellt war ein eigener Name ohne Cloudflare. Vorher aber geprueft, ob es den
ueberhaupt braucht -- und er braucht es nicht: die Nextcloud laeuft auf
DERSELBEN Maschine. Ueber http://localhost:11000 verlaesst der Verkehr die
Rueckschleife nicht, und "localhost" steht schon in trusted_domains. Kein DNS,
kein Zertifikat, kein Proxy, und nebenbei fuenfzehnmal schneller:

    ueber 192.168.100.1:11000   340 MB in 59 s
    ueber localhost:11000       340 MB in  4 s

Kein TLS dabei, und das ist in Ordnung: die Verbindung geht nicht hinaus.

Ganzer Lauf zur Gegenprobe: 4630 Eintraege, 335,9 MB, zweite Platte identisch,
hochgeladen 201, zurueckgeholt und verglichen identisch. Statusseite sagt
wieder "sicherung ok an 3 Orten".

WICHTIG, DAMIT NIEMAND ES FALSCH LIEST -- und das steht auch im Skript: diese
Kopie ist KEINE Auswaertskopie. Die Nextcloud liegt auf derselben Platte wie
die Daten. Sie ist die dritte Kopie, nicht der Weg aus dem Haus. Der fehlt
weiterhin.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 09:32:44 +02:00
D4rkst3randClaude Opus 5 f138df44a7 feat: tools/waechter.ps1 -- zaehlt offene Tueren und rechnet Dateien nach
Die Statusseite war waehrend des Einbruchs die ganze Zeit gruen. Sie waere auch
gruen geblieben, wenn der Miner gelaufen waere. Es fehlte kein Bildschirm, auf
dem man es haette sehen koennen -- es fehlte etwas, das VON SICH AUS Bescheid
sagt. Deshalb kein Frontend, sondern zwei Pruefungen mit Meldeweg.

  1. Ports, die auf allen Adressen lauschen, gegen einen hinterlegten Stand.
     Port 3000 stand elf Tage offen; das haette am ersten Tag gemeldet werden
     koennen. Dazu eine feste Liste heikler Ports, die auch dann warnt, wenn
     der Zustand "schon immer so" war -- 3000 war auch schon immer so.
  2. Pruefsummen ueber sshd_config, administrators_authorized_keys, beide
     Compose-Dateien und Giteas app.ini und .gitconfig. Der Angriff WAR eine
     Datei, die sich um 21:13:57 aenderte.

Gemeldet wird nur bei Aenderung, mit Merker in den Einstellungen. Ein Waechter,
der taeglich meckert, wird stummgeschaltet und ist dann schlechter als keiner.

VOR DEM EINRICHTEN AN BEIDEN FAELLEN BEWIESEN statt behauptet:

    Testport 45999 geoeffnet  ->  "NEU offen: 45999"
    Datei geaendert           ->  Meldung raus, HTTP 204
    zurueckgestellt           ->  "Wieder in Ordnung (Merker geleert)"

Und er prueft zuerst, ob er ueberhaupt etwas sehen kann: findet er keine
Objekte, meldet er "ungueltig" statt "sauber". Das ist die Lehre aus zwei
Suchen in dieser Nacht, die genau das verwechselt haben.

Die Statusseite zeigt den Stand mit; meldet der Waechter sich zwei Stunden
nicht, ist DAS der Befund -- er laeuft alle 15 Minuten.

Im Bericht ausserdem nachgetragen: die Inventur von aussen (NPM-Verwaltung und
MariaDB standen offen, beide jetzt gesperrt), die abgeschalteten Ueberbleibsel
von Caddy und cloudflared, und zwei Irrtuemer von mir -- "Docker geht an der
Firewall vorbei" und "der MariaDB-root hat kein Passwort". Beide waren aus
Messungen geschlossen, die die Frage nicht beantworten konnten.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 09:22:44 +02:00
D4rkst3randClaude Opus 5 70260060c9 fix: ein Tabulator im Hilfetext -- der Pfad liess sich nicht kopieren
Die Zeile am Ende zeigte

    Get-Content 'C:\Users\...\SSH-Tunnel	unnel.log' -Tail 20

statt \tunnel.log. Aus dem Backslash-t ist beim Erzeugen der Datei ein echter
Tabulator geworden -- und der Pfad, den man kopieren soll, fuehrt damit ins
Leere.

Das ist heute Nacht die dritte Ersetzung, die an einer Escape-Ebene
gescheitert ist (nach dem \n im Compose und dem \n des Angreifers, an dem sein
eigener Schadcode zerbrach). Deshalb steht dort jetzt Join-Path statt eines
zusammengesetzten Textes: wo kein Backslash im Text steht, kann auch keiner
umgedeutet werden.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 01:35:48 +02:00
D4rkst3randClaude Opus 5 a36fcec57e fix: kein Fenster OHNE Administratorrechte -- VBS-Starter statt S4U
Der S4U-Weg aus dem letzten Commit war richtig gedacht und praktisch unbrauchbar:
das Eintragen einer S4U-Aufgabe braucht erhoehte Rechte, und in einem
gewoehnlichen PowerShell-Fenster scheitert es mit "Zugriff verweigert". Ein
Werkzeug, das Administratorrechte verlangt, benutzt am Ende niemand -- und
genau darum ging es ja: was umstaendlich ist, wird umgangen.

Jetzt startet die Aufgabe wscript.exe mit einem winzigen VBS, und dessen
Run-Aufruf hat den Fenstermodus 0. Das heisst wirklich unsichtbar, im
Unterschied zu "-WindowStyle Hidden", das bei Konsolenprogrammen in einer
interaktiven Sitzung nur halb wirkt. Besondere Rechte braucht es nicht, weil
nichts Besonderes passiert.

Der Anmeldetyp bleibt damit Interactive; das Fenster verhindert der Starter und
nicht der Anmeldetyp.

Das Protokoll aus dem letzten Commit bleibt und wird jetzt erst recht gebraucht:
sichtbar ist nichts mehr, also muss nachlesbar sein, was ssh sagt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 01:33:03 +02:00
D4rkst3randClaude Opus 5 a494f472d1 fix: der Tunnel laeuft jetzt OHNE Fenster -- S4U statt Interactive
Gemeldet: "durch dein ssh script habe ich trotzdem ein powershell fenster
offen, gemerkt habe ich das weil ich es gerade geschlossen habe und die
verbindung weg war."

Genau so ist es. -LogonType Interactive laesst die Aufgabe IN der
Anmeldesitzung laufen; ein Konsolenprogramm bekommt dort ein Fenster, und
"-WindowStyle Hidden" haelt es nur halb zurueck. Wer es zumacht, kappt den
Tunnel, ohne zu wissen was er tut -- und das ist keine Randerscheinung, sondern
passiert genau einmal und dann jedes Mal wieder.

S4U ("Service for User") laeuft AUSSERHALB der Sitzung: es gibt gar keinen
Bildschirm, auf dem etwas erscheinen koennte. Auf lokale Dateien darf es
weiterhin zugreifen, und mehr braucht der Schluessel unter %USERPROFILE% nicht.
Ein Passwort muss dafuer nirgends gespeichert werden.

DAZU GEHOERT EIN PROTOKOLL, sonst tauscht man ein stoerendes Fenster gegen eine
stille Fehlfunktion: ohne Sitzung sieht niemand mehr, was ssh sagt. Ein
abgelehnter Schluessel oder ein besetzter Port waere unsichtbar -- der Tunnel
stuende einfach nicht, und niemand wuesste warum. tunnel.log haelt jeden
Verbindungsversuch fest und wird bei 1 MB verworfen.

Das Skript sagt am Ende auch, wie man ohne Fenster nachsieht:
Get-NetTCPConnection -LocalPort 13389 -State Listen, und die letzten Zeilen des
Protokolls.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 01:27:06 +02:00
D4rkst3randClaude Opus 5 8e3a13b374 docs: RDP ist zu -- von aussen gemessen, nicht angenommen
Test-NetConnection vom Heimrechner: TcpTestSucceeded False. Das war der letzte
offene Punkt der Nacht.

Die Messung vom Server aus taugte dafuer NICHT: Verkehr an die eigene
oeffentliche Adresse durchlaeuft die Eingangsregeln nicht und meldete
faelschlich "erreichbar". Zweimal in derselben Nacht in dieselbe Falle getappt,
einmal bei Port 3000 und einmal bei 3389 -- beide Male hat erst die Messung von
DRAUSSEN das Urteil gebracht.

Dazu: sshd nimmt nur noch Schluessel (Passwort UND keyboard-interactive aus,
vor dem ersten Match-Block), RDP per Firewall gesperrt, Zugang ueber
tools/tunnel-einrichten.ps1 als Hintergrundaufgabe.

Seitdem laufen die Angriffe ins Leere: "invalid user root ... [preauth]", und
alle 12 erfolgreichen Anmeldungen kommen von einer einzigen IP.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 01:09:06 +02:00
D4rkst3randClaude Opus 5 15f4c7bc3d fix: der Tunnel scheiterte am Benutzernamen, und die Meldung zeigte woandershin
Der Standard fuer -Benutzer war $env:USERNAME -- der Name am HEIMRECHNER. Auf
dem Server heisst das Konto anders (hier "az319" gegen "Darkster"). Das Skript
lief sauber durch, trug die Aufgabe ein, und der Tunnel kam trotzdem nicht.

SCHLIMMER ALS DER FEHLER WAR DIE DIAGNOSE. Am Ende stand fest verdrahtet
"Haeufigste Ursache: der oeffentliche Schluessel ist noch nicht hinterlegt" --
eine GERATENE Ursache, und sie war falsch. Wer dem folgt, prueft den Schluessel,
findet ihn in Ordnung und sucht weiter an der falschen Stelle.

Jetzt wird gemessen: schlaegt der Tunnel fehl, baut das Skript selbst eine
SSH-Verbindung auf und wertet aus, was der Server antwortet -- "Permission
denied" (dann Name ODER Schluessel, beides benannt), nicht erreichbar,
Weiterleitung verweigert, oder ssh kommt durch und es liegt am Ziel-Port.

Und -Benutzer hat keinen Standard mehr. Fehlt er, wird gefragt: "Wie heisst
dein Konto AUF DEM SERVER? (nicht der Name hier am PC)". Einmal fragen ist
besser als einmal in die Irre fuehren.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 01:03:53 +02:00
D4rkst3randClaude Opus 5 b82d315f8d feat: tools/tunnel-einrichten.ps1 -- der Tunnel als Hintergrundaufgabe
LAEUFT AUF DEM HEIMRECHNER, nicht auf dem Server.

Seit dem 13.08.2026 ist RDP am Server gesperrt; hinein geht es nur noch durch
einen SSH-Tunnel mit Schluessel. Von Hand hiesse das: vor jeder Sitzung ein
PowerShell-Fenster oeffnen, einen Befehl tippen, das Fenster offen lassen.

DAS HAELT NIEMAND DURCH, UND WAS NIEMAND DURCHHAELT, WIRD UMGANGEN -- am Ende
steht dann wieder ein offener Port. Das Skript macht den Tunnel deshalb zu
etwas, das man nicht sieht: eine Aufgabe, die beim Anmelden startet, sich bei
Abbruch selbst neu aufbaut und kein Fenster zeigt. Danach verbindet man sich
wie immer, nur auf localhost statt auf die Server-Adresse.

Drei Sachen, die es prueft statt sie anzunehmen:

  - Ist ein SSH-Client da? Sonst der Nachinstallationsbefehl statt eines
    kryptischen Fehlers.
  - Gibt es einen Schluessel? Sonst anlegen UND den oeffentlichen Teil gross
    ausgeben, damit klar ist, was zu schicken ist.
  - Ist der lokale Port frei? Das ist der Fehler, der uns heute Nacht eine
    halbe Stunde gekostet hat: 3389 ist unter Windows selbst belegt, die
    Verbindung landet dann auf dem EIGENEN PC, und die Meldung lautet
    "Konsolensitzung bereits aktiv" -- man sucht den Fehler ueberall, nur
    nicht beim Port. Standard ist deshalb 13389.

Die Schleife um ssh ist Absicht: bei Zwangstrennung, Standby oder kurzem
Aussetzer endet ssh, und ohne Schleife waere der Tunnel weg, ohne dass es
jemand merkt -- bis die naechste Verbindung scheitert.
ExitOnForwardFailure=yes gehoert dazu, sonst laeuft ssh scheinbar, waehrend die
Weiterleitung gar nicht steht.

Gedacht auch fuer Mitbenutzer, die sich mit Rechnern nicht auskennen: einmal
ausfuehren, oeffentlichen Schluessel schicken, fertig. Danach sehen sie vom
Tunnel nie wieder etwas.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 00:55:59 +02:00
D4rkst3randClaude Opus 5 9727eae0f5 docs: Einbruch in den Gitea vom 12.08.2026 -- Hergang, Messwerte, Reparatur
Kein Ausfall, kein Datenverlust, kein laufender Schadcode -- aber ein echter
Schreibzugriff von aussen.

Um 21:13:56 rief jemand /api/internal/manager/add-logger auf, legte einen
Datei-Logger auf die .gitconfig, liess die Nutzlast als Logzeile hineinschreiben
und entfernte den Logger eine Sekunde spaeter wieder. Eingeschleust war ein
packObjectsHook, der bei JEDEM Klonen einen Monero-Miner nachgeladen und per
crontab verankert haette.

WARUM TROTZDEM NICHTS LIEF: die Nutzlast beginnt mit "[uploadpack]\n", wobei
das \n als TEXT in der Datei steht statt als Umbruch. Git bricht mit "bad
config line 23" ab, der Hook wurde nie gueltig. Nachgemessen: kein Prozess,
keine Datei, keine crontab-Zeile, nichts in den Repos, kein Docker-Socket im
Container, keine fremden Konten oder Token. Der einzige Schaden war, dass alle
Git-Operationen ausfielen -- daran ist es aufgefallen.

DER WEG HINEIN: Gitea lag mit 0.0.0.0:3000 direkt im Internet, am Proxy vorbei.
Damit war /api/internal/* von aussen erreichbar, geschuetzt nur durch das
INTERNAL_TOKEN. Woher der Angreifer es hatte, liess sich NICHT klaeren -- es
steht in keinem Repo, und RDP war es nicht (2756 Fehlversuche in 14 Tagen, aber
alle 33 erfolgreichen Anmeldungen von der IP des Betreibers). Genau deshalb ist
die Antwort nicht "Token tauschen", sondern den Weg zumachen.

GEAENDERT: .gitconfig gesaeubert; Port 3000 gar nicht mehr veroeffentlicht; NPM
haengt jetzt mit in gitea_default und spricht gitea:3000 an (dafuer musste der
nginx-Resolver auf Dockers DNS 127.0.0.11 umgestellt werden); INTERNAL_TOKEN
und SECRET_KEY getauscht (gefahrlos: 2FA und Anmeldequellen jeweils 0);
/api/internal/ gibt am Proxy 404; frisches Abbild statt des elf Tage alten.

Gegengeprueft: Port 3000 von aussen 000, /api/internal/ 404, git.d4rkst3r.de
200, LFS-Direktiven alle drei erhalten, Push geht beim ersten Versuch, Repos
identisch mit den lokalen Klonen.

DIE LEHRE: der offene Port war nie noetig -- der Proxy haette von Anfang an
uebers interne Netz gehen koennen. Und aufgefallen ist es nicht, weil etwas
ueberwacht wurde, sondern weil der Angreifer geschludert hat. Haette er sein \n
richtig gesetzt, liefe der Miner noch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 00:13:31 +02:00
D4rkst3r 14fadf5f30 chore: Probe nach dem Abdichten von Gitea 2026-08-13 00:12:11 +02:00
D4rkst3r 4430dc2944 chore: Probe nach dem Saeubern der Gitea-Konfiguration 2026-08-13 00:00:06 +02:00
D4rkst3randClaude Opus 5 ab93e0ae4e feat: die Archive lassen sich aus dem Panel holen
Einstellungen -> "Sicherungen holen": die fertigen Archive mit Datum, Groesse
und einem Knopf. Der erste Schritt, der eine Kopie VON DIESEM RECHNER HERUNTER
bringt -- bisher haette man sich dafuer per Fernwartung auf den Server klicken
muessen, und genau daran hat es gelegen. Nicht Unwille, sondern
Umstaendlichkeit.

Der Ordner haengt NUR LESEND in den Container. Ein Dienst, der seine eigenen
Sicherungen loeschen kann, ist genau das Werkzeug, das man einem uebernommenen
Dienst nicht in die Hand geben will. Nachgemessen im Container:

    touch /sicherungen/probe  ->  Read-only file system

Und es gibt NUR EINE FORM VON NAMEN -- nicht "alles ausser .. und /", sondern
ein Muster, das genau auf die eigenen Archive passt. Wer einen Pfad
hineinschmuggeln will, muesste ihn erst so benennen. Das ist der Unterschied
zwischen dicht gemacht und dicht gebaut:

    ../../etc/passwd                -> 404
    ..%2f..%2fdata%2fmedia.db       -> 400
    media.db                        -> 400
    <archiv>.tar.gz.evil            -> 400
    ohne Anmeldung                  -> 401

Der ganze Weg einmal wirklich gegangen, nicht nur die Kopfzeilen: 336 MB in
15,9 s durch den Proxy, sha256 auf der Platte gleich sha256 heruntergeladen,
tar tzf zeigt 4639 Eintraege. Gestroemt und nicht in den Speicher gelesen --
sonst haetten Dienst UND Proxy die 336 MB gleichzeitig im Arbeitsspeicher.

Jeder Griff steht im Verwaltungs-Verlauf. Geloggt wird der GRIFF und nicht der
Erfolg: ein abgebrochener Download taucht ebenfalls auf, und das ist gewollt --
wer spaeter fragt, wer sich den Bestand geholt hat, will die Anfrage sehen.

Die fuenf neuen Pruefungen sind in tools/rundgang.py mit drin; der steht jetzt
bei 29 in Ordnung, 0 nicht.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 15:46:03 +02:00
D4rkst3randClaude Opus 5 3a89acaa41 docs: der Server steht im Rechenzentrum -- das dreht die Bewertung um
Ich hatte "gleiches Haus" geschrieben und damit die Platte am Router als
halben Schritt abgetan. Falsch: der Rechner ist ein Windows-Root-Server im
Rechenzentrum, bedient von zu Hause ueber mRemoteNG. Ein Ziel beim Betreiber
daheim sind also zwei Gebaeude, zwei Netze, zwei Stromkreise, zwei Anbieter --
besser wird ein Sicherungsziel kaum.

Gemessen, was dafuer schon dasteht:

    sshd        Running, Automatic, 0.0.0.0:22 und [::]:22
    Firewall    "OpenSSH SSH Server (sshd)" -> Allow
    sshd_config Subsystem sftp sftp-server.exe
    je Nacht    d4rk_media 335,9 MB + d4rkbot 0,9 MB = 336,9 MB

Auf der Serverseite fehlt nichts ausser einem Konto, das nur lesen darf. Der
Rest ist ein Skript auf dem Heimrechner, das HOLT statt zu bekommen -- ein
Ziel, in das der Server schreiben darf, ist auch eines, das ein uebernommener
Server loeschen kann.

Fund am Rande: in sshd_config steht kein PasswordAuthentication no. Der
OpenSSH-Standard ist yes -- auf Port 22, offen ins Internet, mit AllowGroups
einschliesslich administrators.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 15:40:31 +02:00
D4rkst3randClaude Opus 5 6bd1c7a55e docs: Richtigstellung -- "drei Orte" sind zwei Platten, und keiner ist auswaerts
Nachgemessen, nachdem die Frage aufkam, wie Sicherungen aus dem Haus kommen.
Das Ergebnis widerspricht dem, was ich heute frueh selbst hier eingetragen habe.

ALLE Docker-Volumes liegen in einer einzigen Datei:

    C:\Users\Darkster\AppData\Local\Docker\wsl\disk\docker_data.vhdx   51,5 GB

Das ist Platte 0. Darin: die Daten von d4rk_media, die Datenbank des Bots UND
DIE NEXTCLOUD. cdn.d4rkst3r.de und media.d4rkst3r.de loesen beide auf
88.218.224.10 auf, und die vierzehn nextcloud-aio-Container laufen auf genau
dieser Maschine.

    C:\backup\d4rk_media   Platte 0   dieselbe wie die Daten
    D:\backup\d4rk_media   Platte 1   echt getrennt
    Nextcloud              Platte 0   dieselbe wie die Daten

Die Nextcloud-Kopie stand hier als "die einzige Trennung". Sie ist in Wahrheit
die am WENIGSTEN getrennte von allen dreien -- und der Grund fuer den Irrtum ist
der uebliche: eine Adresse mit eigenem Namen und eigenem Zertifikat SIEHT wie
ein anderer Ort aus. Nachgesehen hatte das niemand.

Was die drei Ziele wirklich abdecken: eine Platte stirbt -> D: faengt es auf,
das war den Aufwand wert. Rechner tot, Feuer, Diebstahl, Verschluesselung ->
alle drei Kopien sind gleichzeitig weg.

"sicherung ok an 3 Orten" auf der Statusseite bleibt woertlich richtig -- sie
zaehlt Ablageorte, nicht Platten und nicht Gebaeude. Wer daraus "also sicher"
liest, liest mehr hinein als dasteht.

In docs/ideen.md steht das Thema wieder offen, mit den Kandidaten und der
Eigenschaft, auf die es dabei ankommt: HOLEN statt SCHICKEN. Ein Ziel, in das
der Server selbst schreiben darf, ist auch eines, das ein uebernommener Server
loeschen kann.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 15:27:36 +02:00
D4rkst3randClaude Opus 5 608b3b3097 feat: tools/rundgang.py -- 24 Pruefungen gegen den laufenden Dienst
WOZU, wo doch jede Aenderung einzeln gemessen wird: weil sie einzeln gemessen
wird. An einem Tag mit sechs Neustarts fasst niemand mehr alles an, und die
Sachen, die keiner mehr anfasst, sind genau die, die kaputtgehen -- ein WebP,
das nach einem Umbau nicht mehr ausgehandelt wird, meldet sich nicht.

Kein Ersatz fuer die Tests: er prueft nicht, ob eine Funktion das Richtige
rechnet, sondern ob der Dienst, so wie er gerade laeuft, die Sachen noch tut,
die er koennen muss. Deshalb geht er ueber HTTP und nicht am Code vorbei. Nur
lesend -- kein Upload, kein Loeschen, keine Einstellung wird angefasst.

ZUERST kommt, was OHNE ANMELDUNG DICHT SEIN MUSS (acht Wege, alle 401). Das ist
die Sorte Fehler, die man nicht sieht: eine Route, die versehentlich vor dem
Waechter registriert wird, funktioniert praechtig -- fuer alle. Genau das ist
hier schon zweimal passiert.

Dann Bestand, /f/ mit WebP-Aushandlung an derselben Adresse und Vary: Accept,
beide CSV-Exporte auf nackte LF, Papierkorb-Deckel, Sicherungsziele und die
Statusseite.

ZWEI FEHLSCHLAEGE BEIM BAUEN, beide im Test und nicht im Dienst -- sie stehen
als Kommentar drin, damit der naechste sie nicht wiederholt:

  1. Ohne Filter zog er vehicles/asbo.webp und verglich WebP mit WebP. Gleiche
     Groesse, Test rot, Dienst richtig.
  2. Der Filter hiess dann "q=" statt "query=". Unbekannte Parameter ignoriert
     der Dienst und liefert ALLES -- was wie ein kaputter Filter aussieht und
     ein kaputter Aufruf ist.

Ein Test, der das Falsche misst, ist schlimmer als keiner: er wird geglaubt.

Gemessen: 24 in Ordnung, 0 nicht.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 09:01:23 +02:00
D4rkst3randClaude Opus 5 1baa67c891 fix: die geplante Sicherung zeigte auf einen Pfad mit Versionsnummer
Gefunden beim Nachsehen, ob die Sicherung ueberhaupt laeuft -- sie lief
(12.08.2026 04:30:01, Ergebnis 0), aber sie startete pwsh.exe so:

    C:\Program Files\WindowsApps\Microsoft.PowerShell_7.6.4.0_x64__...\pwsh.exe

DIE VERSIONSNUMMER STEHT IM PFAD. Beim naechsten PowerShell-Update heisst der
Ordner anders, die Aufgabe findet ihre pwsh.exe nicht mehr, und die Sicherung
hoert auf zu laufen -- an einem Tag, an dem niemand etwas an ihr geaendert hat.
Der Waechter haette es nach 26 Stunden gemeldet; angenehmer ist es, wenn es gar
nicht erst passiert.

Ersetzt durch den stabilen Ausfuehrungsalias unter LOCALAPPDATA. Der geht, weil
die Aufgabe als Darkster und Interactive laeuft -- fuer einen Dienst unter
SYSTEM waere er wertlos gewesen, und das ist vorher nachgesehen worden.

Gemessen, und zwar der ganze Weg und nicht nur der Start: Aufgabe von Hand
gestartet -> Ergebnis 0, sicherung.letzte sagt ok:true, zweit:true, hoch:true,
4630 Dateien. Argumente unveraendert, Zeitplan unveraendert.

Ausserdem die Uebung aus der NEUEN Kopie gefahren, nicht nur aus der alten --
sonst waere das zweite Ziel eine Datei und kein Sicherungsziel:

    pwsh -File tools\zurueckspielen.ps1 -Ordner D:\backup\d4rk_media
    -> 4630 Eintraege, 4630 Dateien, drei Dateien durch den Dienst geholt:
       identische Bytes. Die Uebung ist bestanden.

Die Aenderung an der geplanten Aufgabe liegt ausserhalb des Repos; sie steht
deshalb in der ROADMAP, damit sie nach einem Neuaufsetzen nicht fehlt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 08:56:41 +02:00
D4rkst3randClaude Opus 5 74e2128afb feat: die Sicherung liegt an drei Orten statt an zweien
Der letzte offene Punkt aus docs/ideen.md. Ein ZWEITES LAUFWERK, einstellbar im
Panel unter Einstellungen -> Sicherung; sichern.ps1 legt das Archiv dort ab,
vergleicht die Pruefsumme und duennt auch dort aus.

WARUM DAS NICHT DASSELBE IST WIE DIE NEXTCLOUD. Die hilft, wenn der ganze
Rechner weg ist -- aber wer 335 MB, spaeter 100 GB, zurueckholen muss, laedt sie
ueber die Leitung. Das zweite Laufwerk ist in Minuten zurueckgespielt. Zwei
Fragen, zwei Antworten, deshalb zwei Ziele.

UND ES WARNT, WENN ES DIESELBE PLATTE IST -- das ist der eigentliche Inhalt.
Ein zweiter Ordner auf C: sieht im Panel genauso gruen aus wie eine echte zweite
Platte und hilft gegen gar nichts. Verglichen wird die PHYSISCHE Platte und
nicht der Laufwerksbuchstabe: zwei Partitionen derselben NVMe sterben zusammen.
Nachgemessen in beide Richtungen:

    C:\backup gegen D:\backup  ->  Nr. 0 gegen Nr. 1  ->  "andere Platte"
    C:\backup gegen C:\Users   ->  Nr. 0 gegen Nr. 0  ->  Warnung

Auf diesem Rechner sind das zwei getrennte NVMe zu je 954 GB.

GEMESSEN AN EINEM ECHTEN LAUF, keinem Trockentest: 4630 Eintraege, 335,9 MB,
auf beiden lokalen Zielen und in der Nextcloud dieselbe Pruefsumme
FFCE4D0A7310... -- identisch.

Geprueft wird mit der Pruefsumme und nicht mit der Dateigroesse: eine
abgebrochene Kopie auf eine volle Platte hat oft genau die richtige Laenge und
trotzdem Nullen am Ende. Schlaegt das Kopieren oder der Vergleich fehl, ist der
ganze Lauf gescheitert (exit 1, ok:false) -- ein Ziel, das still ausfaellt, ist
genau das, wogegen das hier gebaut ist.

Der Bericht traegt jetzt zweit:true. Fehlt das Feld, ist KEIN zweites Ziel
eingerichtet -- es steht absichtlich nicht als false da, denn das laese sich wie
"hat nicht geklappt".

Die Statusseite zaehlt die Orte mit: "sicherung ok an 3 Orten" statt nur "ok".
Eine Sicherung, die es nur einmal gibt, ist gruen und trotzdem eine, die ein
Plattenausfall mitnimmt -- das gehoert dorthin, wo jemand hinsieht.

Der Pfad wird beim Speichern auf seine FORM geprueft (Laufwerksbuchstabe oder
UNC-Freigabe): der Dienst laeuft im Container und kann ihn nicht nachschlagen,
aber ein relativer Pfad ist mit Sicherheit ein Tippfehler -- und ein Tippfehler
in einem Sicherungsziel faellt sonst erst auf, wenn man die Sicherung braucht.
Nachgemessen: "backup/woauchimmer" -> 400 mit Text.

Und zurueckspielen.ps1 sagt im Kopf, was zu tippen ist, wenn genau diese Platte
das Problem ist. Das ist der Fall, fuer den das Ganze da ist, und niemand soll
ihn um vier Uhr nachts erst herleiten muessen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 08:50:44 +02:00
D4rkst3randClaude Opus 5 bbb74138c8 feat: Verlauf als CSV, Meldung bei Upload mit Drossel, Deckel auf dem Papierkorb
Abschnitt 3 aus docs/ideen.md, bis auf die Sicherungsziele.

VERLAUF ALS CSV. Der Export konnte nur den Bestand; wer nachrechnen wollte,
was ueber einen Zeitraum passiert ist, sass vor einer Tabelle, die nach 500
Zeilen umblaettert. Der Knopf sitzt in der Reiterleiste und holt den Verlauf,
DER GERADE OFFEN IST -- zwei Knoepfe nebeneinander waeren die Frage "welchen
von beiden?" an einer Stelle, wo die Antwort schon auf dem Bildschirm steht.

Semikolon und CRLF wie beim Bestands-Export; das war dort nachgemessen worden.
Gemessen an der ausgelieferten Datei: 5371 Zeilen, kein einziges nacktes LF.
Ohne Anmeldung 401 -- die Route liegt hinter dem Waechter, was nach dem
Sitzungs-Fehler von vorgestern ausdruecklich nachgeprueft wurde.

MELDUNG BEI UPLOAD -- die Drossel ist der eigentliche Inhalt. Jeder Upload
schiebt eine Frist von zwei Minuten nach hinten; erst nach der Ruhe geht EINE
Nachricht raus. Ohne das waere die Funktion ein Schaden: 900 Fahrzeugbilder
ergaeben 900 Nachrichten, Discord drosselt Webhooks, und wer danach eine echte
Meldung bekommt, sieht sie nicht mehr.

Gemessen: drei Uploads mit zehn Sekunden Abstand -> eine Meldung ueber drei
Dateien. Ein einzelner Upload -> eine Meldung ueber eine Datei.

Nur Token-Uploads, absichtlich. Was ueber das Dashboard hereinkommt, hat gerade
jemand selbst hochgeladen und bestaetigt bekommen. Zurueckholen aus dem
Papierkorb meldet gar nichts: das ist kein Neues, das ist ein Wiedergefundenes.

Und derselbe Fehler wie bei 'platte' lauerte schon wieder: die Liste der
Anlaesse steht im Server UND in der Oberflaeche, und der Server verwirft beim
Speichern alles, was in seiner Liste fehlt -- ein Kreuzchen, das sich setzen
laesst und beim naechsten Laden weg ist. Diesmal beide angefasst, in beiden
steht jetzt ein Verweis auf die andere. Nachgemessen: vier geschickt, vier
gespeichert.

DECKEL AUF DEM PAPIERKORB. 30 Tage UND 20 GB, was zuerst greift. Wer 200 GB
loescht, haelt sie sonst 30 Tage doppelt und merkt es erst, wenn die Platte
voll ist. Geraeumt wird das AELTESTE zuerst -- dessen Versehen waere am
ehesten schon aufgefallen.

Ueber PAPIERKORB_MAX_MB verstellbar, und das nicht aus Bequemlichkeit: ein
Deckel von 20 GB laesst sich nicht pruefen, ohne 20 GB zu loeschen. Mit
0.0002 MB nachgemessen -- vier Eintraege zu 422 Bytes, drei entfernt, uebrig
blieb der NEUESTE. Danach zurueck auf 20 GB, gemessen 21474836480.

NEBENBEI, DREIMAL DIESELBE SORTE FEHLER: "0.0 MB" fuer 422 Bytes, "0 KB" fuer
106 Bytes, "0.0 MB von 0.0 MB" beim Deckel. Es gab drei Byte-Formatierer, jeder
mit anderer Untergrenze. Jetzt einer in meldung.ts, von Bytes bis GB, exportiert
und von dash.ts mitbenutzt. Nachgemessen ueber alle Stufen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 08:25:50 +02:00
D4rkst3randClaude Opus 5 6a49713821 feat: die Oberflaeche zu den vier Werkzeugen
Bisher liessen sie sich nur ueber die API benutzen. Jetzt stehen sie dort, wo
man sie sucht.

SPEICHER bekommt zwei Karten:

"Wozu fehlt ein Bild?" -- Muster und Namensliste hinein, beide Richtungen
heraus. Das Muster kommt aus den ADRESSVORLAGEN und wird als Knopfreihe
angeboten, damit hier nicht dieselbe Zeichenkette ein zweites Mal getippt wird.

"Suchen und Ersetzen in Pfaden" -- und dort gibt es KEINEN Knopf, der es sofort
tut. Erst zeigen, dann fragen, dann machen. Die Vorschau nennt die Zahl, die
die Entscheidung traegt: wie oft die betroffenen Dateien zusammen geholt
wurden. Null heisst, es schaut niemand zu; viertausend heisst, es schaut jemand
zu. Gibt es auch nur ein Problem, verschwindet der Ausfuehren-Knopf ganz --
nicht ausgegraut, sondern weg, denn ein Problem verhindert ohnehin alles.

EINSTELLUNGEN bekommt "Einstellungen mitnehmen". Herunterladen ist der
harmlose Knopf; "mit Geheimnissen" ist rot und fragt nach, mit dem Satz, dass
die Datei das Discord-Geheimnis und das Nextcloud-Passwort im Klartext traegt
und der Download im Verwaltungs-Verlauf steht.

TOKEN bekommen das Feld "Bilder verkleinern auf" -- im Anlege-Formular und im
Bearbeiten-Dialog, mit dem Hinweis, dass kleinere Bilder NICHT aufgeblasen
werden. In der Liste steht es als "max 512 px" neben den anderen Grenzen.

Zwei eigene Stolperer beim Bauen, beide vom Uebersetzer gefangen: ein Patch
brach vor dem Schreiben ab, sodass die Haelfte der API-Aufrufe fehlte -- und in
FehlendeKarte blieb ein `note` stehen, das niemand mehr benutzte.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 08:03:04 +02:00
D4rkst3randClaude Opus 5 381feade4e feat: fehlende Bilder finden, Massen-Umbenennen, Bilder verkleinern, Einstellungen mitnehmen
FEHLENDE BILDER. Die Frage, die bisher niemand beantwortet hat: das Fotostudio
weiss, was es aufgenommen hat, der Dienst weiss, was liegt, und die Differenz
rechnet keiner aus -- weshalb "das Handy zeigt bei manchen Autos kein Bild"
erst im Spiel auffaellt.

POST /api/dash/fehlende nimmt ein Muster mit genau EINEM Platzhalter und eine
Liste (als Array oder als Textblock aus einem Textfeld) und gibt beide
Richtungen zurueck: was fehlt, und was daliegt ohne in der Liste zu stehen.
Beides bedeutet etwas anderes -- fehlende sind Arbeit fuers Fotostudio,
ueberzaehlige sind Platz, den man zurueckbekommt.

Gesucht wird KLEINGESCHRIEBEN, weil so abgelegt wird: wer "WEAPON_SMG"
hereinreicht, meint weapon_smg.png und bekaeme sonst ein "fehlt", das gelogen
waere. Gemessen an echten Namen: 6 geprueft, 4 da, 2 fehlen, 985 ueberzaehlig.

MASSEN-UMBENENNEN mit Vorschau. Bei 3578 Items ist eine Umbenennung von Hand
keine. DIE VORSCHAU IST DER EIGENTLICHE TEIL: was hier passiert, ist dieselbe
Warnung wie beim Ordner-Umbenennen -- jede alte Adresse gibt danach 404 --, nur
mal tausend.

Geprueft wird VOR dem ersten Schritt: Kollisionen untereinander, belegte Ziele,
unzulaessige Pfade. Und EIN Problem verhindert ALLES; eine halbe Umbenennung
ist der Zustand, aus dem niemand mehr herausfindet, weil die Grenze nirgends
steht. Gemessen: Vorschau ueber 134 Dateien fasst nichts an; eine erzwungene
Kollision gibt 409 und laesst alle drei Dateien stehen; der saubere Lauf
benennt zwei um, Vorschau wandert mit, ein Eintrag im Verlauf statt zwei.

BILDER BEIM ABLEGEN VERKLEINERN, je Token einstellbar. NICHT als Schutz -- das
waere gemessen falsch, ein 12000x12000-PNG kostet 374 ms und 47 MB, weil
libvips kachelweise arbeitet. Es geht um Plattenplatz und Ladezeit: ein
Bildschirmfoto ist 3840 breit und wird auf einem Handy auf 400 gezeigt.
withoutEnlargement ist dabei entscheidend, sonst blaeht die Einstellung kleine
Icons auf.

DABEI WAERE MIR EIN ECHTER FEHLER UNTERLAUFEN: der sha256 wird weiter oben aus
dem Original gerechnet (fuer den Ersatzpfad, wenn kein X-Path kam). Wird das
Bild verkleinert, ist das eine ANDERE Datei -- ein Datensatz mit dem Hash des
Originals waere schlicht falsch, und jede spaetere Gegenprobe verglichen gegen
etwas, das nirgends liegt. Gemessen nach der Korrektur: 3000x2000 -> 512x341,
82 KB -> 3 KB, und Datenbank und Platte stimmen in Groesse und Hash ueberein.

EINSTELLUNGEN MITNEHMEN. Export als JSON, Import zurueck. GEHEIMNISSE GEHEN
STANDARDMAESSIG NICHT MIT: unter den Schluesseln liegen das
Discord-Client-Geheimnis und das Nextcloud-Passwort, und eine Datei, die man
"mal eben" exportiert und in einen Chat zieht, ist genau der Weg, auf dem so
etwas abhandenkommt. Wer sie braucht, sagt ?geheim=1 -- und der Hinweis steht
dann IN der Datei, nicht nur auf dem Bildschirm, von dem er beim Weiterreichen
abfaellt. Der Export mit Geheimnissen steht im Verwaltungs-Verlauf.

Beim Import ueberschreiben LEERE WERTE NICHTS: eine Datei ohne Geheimnisse --
der Normalfall -- traegt sie als leere Zeichenkette, und die darf ein
vorhandenes Passwort nicht loeschen. Gemessen: nach einem Import mit leerem
client_secret stehen die 32 Zeichen unveraendert da.

Nicht mitgenommen wird sicherung.letzte -- das ist der Bericht des letzten
Laufs, kein Einstellungswert. In eine frische Anlage eingespielt behauptete er
dem Wachhund eine Sicherung, die es dort nie gab.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 07:53:31 +02:00
D4rkst3randClaude Opus 5 29e516559a feat: Sitzungen beenden, Verwaltungs-Verlauf -- und ein Kaestchen, das log
DER FEHLER ZUERST, denn er ist meiner von gestern: die Liste der
Meldungsanlaesse stand ZWEIMAL da -- als Standard in meldung.ts und noch einmal
als Weissliste beim Speichern in dash.ts. Beim Einbau des Plattenwaechters habe
ich nur die erste angefasst.

Ergebnis: das Kaestchen "Der Platz auf der Platte wird knapp" liess sich
ankreuzen, der Server warf den Wert beim Speichern weg, und beim naechsten
Laden war es wieder aus. Ohne ein Wort dazu. Nachgemessen am laufenden Dienst:

    geschickt    ["sicherung","verwaiste","platte"]
    gespeichert  ["sicherung","verwaiste"]

Die Liste steht jetzt an EINER Stelle (ANLAESSE in meldung.ts) und wird von
beiden benutzt. Gegengeprueft: alle drei kommen an.

SITZUNGEN BEENDEN. Wer sich an einem fremden Rechner angemeldet hat und es
spaeter merkt, hatte keine Moeglichkeit das zurueckzunehmen -- ein geaendertes
Passwort half nicht, die Sitzungen haengen an einer eigenen Tabelle und
ueberleben es. Der Knopf steht im Kontomenue und nur dann, wenn es ueberhaupt
eine zweite Sitzung gibt. Gemessen: 87 offen, 86 beendet, meine lebt.

Dabei in die eigene Falle getreten: der Weg lag zuerst bei den anderen
/auth-Wegen -- und die stehen mit Absicht VOR der Wache, weil man sich anmelden
koennen muss, ohne angemeldet zu sein. Dort ist c.get('user') leer, und der Weg
antwortete mit "Cannot read properties of undefined". Hono setzt Middleware und
Handler in der Reihenfolge ihrer Anmeldung zusammen; der Pfad sagt darueber
nichts.

VERWALTUNGS-VERLAUF. Der Datei-Verlauf beantwortet "was ist mit den Dateien
passiert". Wer den Discord-Webhook geaendert, einen Token angelegt oder ein
Konto entfernt hat, stand nirgends. Bei zwei Konten verschmerzbar -- sobald ein
drittes ueber die Discord-Rolle von selbst entsteht, ist es die erste Frage.

Eigene Tabelle und nicht events: dort traegt jede Zeile path und size, und eine
Einstellungsaenderung muesste path mit etwas fuellen, das kein Pfad ist.

WERTE STEHEN DORT NIE, nur Schluesselnamen. Unter den Einstellungen liegen das
Discord-Geheimnis und das Nextcloud-Passwort; ein Verlauf, der sie mitschreibt,
macht aus einer Tabelle mit einem Passwort eine Tabelle mit allen, die es je
gab.

Beim Einbau derselbe Fehlertyp wie gestern beim SVG: im SQL-Kommentar standen
Backticks, und das Schema liegt in einem Template-Literal -- ein Backtick
beendet es. Der Uebersetzer hat es gefangen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 07:43:57 +02:00
D4rkst3randClaude Opus 5 a91c4261de fix: pruneEvents lief nie von selbst -- und eine Liste, was noch ginge
DIE FUNKTION GIBT ES SEIT DEM ERSTEN TAG und sie raeumt Ereignisse aelter als
180 Tage weg. Aufgerufen wurde sie aber nur von /maintenance/prune-sessions --
einem Weg, den nicht einmal die Oberflaeche anbietet. Die dokumentierte
Aufbewahrung griff damit NIE, und die Tabelle wuchs fuer immer.

Zum Vergleich: pruneSessions haengt seit jeher an einem stuendlichen Takt. Das
eine war verdrahtet, das andere nicht, und der Unterschied fiel niemandem auf,
weil beide in derselben Zeile des Wartungswegs stehen.

Gemessen: nach EINEM Tag mit Umzug und Serienlauf standen 5355 Zeilen in
events. Laeuft jetzt taeglich, mit zwei Minuten Verzoegerung nach dem Start --
nicht stuendlich wie die Sitzungen, denn ein DELETE ueber ein halbes Jahr hat
es nicht eilig.

Dazu docs/ideen.md: was beim Bauen und Messen wirklich aufgefallen ist, nach
Dringlichkeit sortiert. Mit einem eigenen Abschnitt fuer das, was gemessen
wieder heruntergefallen ist -- Doppelte (0,0 MB verschwendet), Aufraeumen nach
Alter (alle Stufen auf 0), eine Grenze fuer Bildmasse (12000x12000 kostet 374 ms
und 47 MB, libvips streamt). Damit sie nicht beim naechsten Mal wieder
aufschlagen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 01:21:09 +02:00
D4rkst3randClaude Opus 5 e8efa2431d feat: Favicon, Token bearbeiten, und Hochladen per Adresse
FAVICON. /favicon.ico antwortete mit 200 und text/html -- die SPA-Rueckfall-
route lieferte index.html als Icon aus. Ein leeres Blatt im Tab und 2 KB umsonst
bei jedem Aufruf, nachgemessen bevor es hier steht.

Jetzt ein SVG in den Farben der Oberflaeche (Grund #0b0d10, Akzent #4ea3ff,
dieselbe Plattenform wie in der Kopfzeile), dazu PNG in 32, 180 und 512 -- AUS
DERSELBEN SVG-Datei gerechnet und nicht zweimal gezeichnet. Plus ein
web-manifest, damit das Symbol auf einem Telefon-Startbildschirm stimmt.

Der erste Anlauf war kaputt, und zwar fuer JEDEN Parser: im SVG-Kommentar stand
"--color-grund", und XML verbietet den doppelten Bindestrich in Kommentaren.
Aufgefallen beim Rechnen der PNG, nicht erst im Browser.

TOKEN BEARBEITEN. Kontingent, Groessengrenze, Arten, Ablauf und Praefix liessen
sich nur beim ANLEGEN setzen -- wer einem bestehenden Token nachtraeglich eine
Grenze geben wollte, musste ihn neu anlegen und damit den Schluessel in jedem
Skript tauschen. Fuer eine Zahl in einer Tabelle der falsche Preis.

PATCH /api/dash/tokens/:id, jedes Feld einzeln. WEGGELASSEN heisst UNVERAENDERT,
null heisst ausdruecklich "keine Grenze" -- ohne diesen Unterschied liesse sich
eine einmal gesetzte Grenze nie wieder loeswerden. Der Schluessel selbst bleibt
unberuehrt, und das ist keine Vorsicht: in der Tabelle steht nur sein Hash.

HOCHLADEN PER ADRESSE. Der Dienst holt die Datei selbst. Das ist die
gefaehrlichste Funktion in diesem Dienst und steht deshalb in einer eigenen
Datei (holen.ts) -- ein Server, der eine vom Benutzer genannte Adresse abruft,
ist ein Angriff mit eigenem Namen.

Geprueft wird das Schema, der Anschluss und die AUFGELOESTE IP -- nicht der
Name. Ein Namensfilter waere einer fuer den, der ihn nicht umgehen will. Und
jede Umleitung wird SELBST gelaufen (redirect: 'manual') und neu geprueft;
liesse man fetch folgen, waere genau dort die Luecke.

Durchgemessen am laufenden Dienst, fuenfzehn Faelle:

    Erfolgsfall                                     abgelegt, 131956 B
    127.0.0.1 / localhost / [::1] / 0.0.0.0         abgelehnt
    10.x / 172.20.x / 192.168.x / 169.254.169.254   abgelehnt
    127.0.0.1.nip.io  (oeffentlicher Name, private IP)   abgelehnt
    192.168.2.1.nip.io / 169.254.169.254.nip.io         abgelehnt
    file:// und gopher://                           abgelehnt
    Anschluss 8080 / 9101                           abgelehnt

Die nip.io-Faelle sind der eigentliche Beleg: ein oeffentlich aufloesbarer Name,
der auf eine private Adresse zeigt, ist der Standardweg um einen Namensfilter
herum -- und faellt hier durch, weil die IP geprueft wird.

Nicht abschliessend geprueft: eine Umleitung, die ins Private zeigt. Mir fehlt
ein oeffentlicher Umleiter, der das tut (httpbingo lehnt es mit 403 ab). Der
Code laeuft die Kette selbst und ruft je Sprung dieselbe Pruefung -- belegt ist
also die Pruefung, nicht die Kette.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 01:18:27 +02:00
D4rkst3randClaude Opus 5 463a821199 feat: der Dienst heisst media.d4rkst3r.de -- beide Namen liefern weiter aus
PUBLIC_URL umgestellt. FILES_HOST ist leer, der Dienst achtet also gar nicht
auf den Hostnamen, und in NPM stehen beide auf DEMSELBEN Host -- die drei
^~-Bloecke gelten damit fuer beide. Nachgemessen: unter beiden Namen health 200
und dasselbe Bild, bytegleich.

Nachgezogen:

    7 Stellen in den Ressourcen   d4rk_phone/config.lua (2)
                                  d4rk_photostudio/config.upload.lua
                                  d4rk_photostudio/config.upload.example.lua (2)
                                  d4rk_photostudio/settings.json (2)
    2 Watchdog-Eintraege im Bot   inklusive Anzeigename
   16 Stellen in Doku/Werkzeugen  README, docs/API.md, Home.md,
                                  npm-advanced.conf, proxy-pruefen.ps1,
                                  uebernehmen.ps1, .env.example

Die Freigabe-Links zeigen von selbst auf den neuen Namen -- sie werden aus
PUBLIC_URL gebaut und stehen nirgends gespeichert. Die alten funktionieren
weiter.

DIE ROADMAP BEHAELT DEN ALTEN NAMEN, an sechzehn Stellen und mit Absicht: dort
steht die Geschichte, warum er ueberhaupt "fivemanage" hiess. Sie zu ersetzen
machte aus einer Begruendung Unsinn.

ZWEI DINGE, DIE DABEI AUFFIELEN.

Cloudflare stand vor dem neuen Namen und brach die WebP-Auslieferung: der
Cache-Schluessel kennt Accept nicht, also bekam ein Aufrufer OHNE
"Accept: image/webp" die WebP-Fassung aus dem Zwischenspeicher -- unsere
eigenen Bytes unter einem .png-Namen. Nach dem Umstellen auf DNS-only gemessen
und in Ordnung: ohne Accept image/png 12295 B, mit Accept image/webp 2270 B,
Server: openresty, vary: Accept, dreimal hintereinander stabil.

Und der Lua-Syntaxtest schlug bei d4rk_phone/config.lua fehl -- an Zeile 93,
`customModel = \`prop_player_phone_02\``. Das ist kein Schaden, sondern eine
CfxLua-Erweiterung: Backticks sind dort ein joaat-Hash und in Standard-Lua 5.4
ein Syntaxfehler. 85 davon stecken in der Datei. luac -p taugt fuer
CfxLua-Dateien also nicht; geprueft wurde stattdessen, dass genau die zwei
URL-Zeilen anders sind.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 01:02:28 +02:00
D4rkst3randClaude Opus 5 72fd330ae6 feat: der Dienst nennt sich selbst -- und Cloudflare bricht die WebP-Auslieferung
DER NAME KAM VIERMAL FEST AUS DEM QUELLTEXT: Kopfzeile, Anmeldeseite, Fusszeile
einer Freigabe, Tab-Titel -- und zwar als "d4rk_media", was der Name des
Repositorys ist und nicht der des Dienstes. Wer ihn aendern wollte, musste vier
Stellen finden und eine davon uebersehen.

Er kommt jetzt vom Server, aus der Einstellung marke.name -- DERSELBEN, die
schon in der Fusszeile der Discord-Meldungen steht ("D4RKST3R // MEDIA"). Ein
zweiter Name fuer die Oberflaeche waere ein zweiter, der beim naechsten Mal
anders lautet als der erste.

Ausgeliefert wird er ueber /api/dash/auth/config (oeffentlich, die
Anmeldeseite hat noch keine Sitzung) und in der Freigabe-Antwort (die Seite ist
oeffentlich und holt sonst nichts, was ihn kennen koennte). Angezeigt wird
"<marke> Media" -- die Marke allein waere falsch, sie steht ueber allem, was
hier laeuft.

EINE STELLE BLEIBT ABSICHTLICH: der Schluessel im Speicher des Browsers
(merker.ts) heisst weiter "d4rk_media.". Wer ihn umbenennt, wirft still alle
gemerkten Ansichtseinstellungen weg, und niemand versteht, warum die Galerie
ploetzlich wieder bei 60 Kacheln steht. Steht als Kommentar daneben.

---

UND DER NEUE NAME HAENGT HINTER CLOUDFLARE, was die WebP-Auslieferung bricht.

NPM ist sauber, sogar besser als vorgeschlagen: beide Namen auf DEMSELBEN Host,
die drei ^~-Bloecke gelten damit fuer beide. Der DNS-Eintrag geht aber ueber
Cloudflare (188.114.97.3, Server: cloudflare), waehrend fivemanage direkt auf
88.218.224.10 zeigt.

Gemessen, dieselbe Datei:

    fivemanage (direkt)      ohne Accept  ->  image/png   12295 B   richtig
                              mit Accept  ->  image/webp   2270 B   richtig

    media (Cloudflare)     1. mit Accept  ->  image/webp   2270 B   MISS
                           2. mit Accept  ->  image/webp   2270 B   HIT
                           3. OHNE Accept ->  image/webp   2270 B   HIT   FALSCH

Und andersherum genauso -- wer zuerst kommt, praegt den Zwischenspeicher fuer
alle. Es sind unsere eigenen Bytes (2270 = unsere WebP-Fassung), also kein
Polish, sondern ein Cache-Schluessel ohne Accept. Vary: Accept wird an den
Client durchgereicht, aber nicht zum Unterscheiden benutzt.

Fuer ein Lua-Skript heisst das: items/foo.png liefert WebP-Bytes unter einem
.png-Namen, und das faellt erst auf, wenn irgendwo ein Bild nicht laedt.

PUBLIC_URL ist deshalb NICHT umgestellt. Genau davor stand die Warnung im
letzten Commit, und sie war keine Vorsicht, sondern eine Vorhersage.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 00:43:29 +02:00
D4rkst3randClaude Opus 5 cf2e3d2387 docs: der Name wird media.d4rkst3r.de -- entschieden, mit gemessenem Preisschild
Der alte Name bleibt daneben stehen und zeigt auf denselben Dienst. Kein
Umleiten, kein Stichtag: beide Namen funktionieren, und die Skripte lassen sich
in Ruhe umstellen statt an einem Abend alle gleichzeitig.

Moeglich ist das, weil FILES_HOST leer ist -- der Dienst achtet gar nicht auf
den Hostnamen. Nachgemessen: GET /health mit "Host: media.d4rkst3r.de" an Port
9101 gibt bereits 200.

WAS ES KOSTET, gemessen statt geschaetzt. In diesem Dienst: EINE Variable. Von
4630 Medienpfaden und 15 Einstellungen enthaelt keine einzige den Hostnamen --
die ROADMAP behauptet das seit Monaten, jetzt ist es geprueft.

Ausserhalb, vollstaendig durchsucht:

    d4rk_phone/config.lua                 imageUrl, brandLogoUrl
    d4rk_photostudio/config.upload.lua    base   (+ .example)
    d4rk_photostudio/settings.json        zwei Bildquellen
    d4rkbot, ueberwachte Dienste          zwei Eintraege
    Freigabe-Links                        zwei, bereits herausgegeben

UND ZWEI FALLSTRICKE IM DNS, beide nachgemessen:

Der Platzhalter *.d4rkst3r.de hilft nicht -- er zeigt auf 185.194.237.210 und
damit woandershin als fivemanage (88.218.224.10). Es braucht einen eigenen
A-Eintrag.

Und der neue Name gehoert NICHT hinter Cloudflare: hub.d4rkst3r.de antwortet
von 188.114.96.3 (Cloudflare), fivemanage direkt vom Server. Ginge media ueber
Cloudflare, staende ein ZWEITER Zwischenspeicher davor, der nach Dateiendung
greift -- und wir haetten genau die Probleme wieder, die der NPM-Block gerade
geloest hat: alte Vorschauen und ein weggeworfenes Vary: Accept.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 00:35:47 +02:00
D4rkst3randClaude Opus 5 3f706b27e1 feat: /status fuer die Statusseite -- und der Fehler, den es dabei gefunden hat
/health gab es und bleibt, wie es ist: app.get('/health', c => c.json({ok:true})).
Diese Zeile beweist genau eines -- der Prozess nimmt Anfragen an. Daran haengt
der HEALTHCHECK des Containers, und DORT ist billig richtig: eine schwere
Pruefung, die bei einer langsamen Platte einmal ausfaellt, liesse Docker den
Container neu starten, also genau dann, wenn er unter Last steht.

Fuer eine Statusseite ist das zu duenn -- sie stuende auf Gruen, waehrend die
Platte voll ist und kein Upload mehr angenommen wird.

/status sieht deshalb wirklich nach: Datenbank (eine echte Abfrage, nicht "die
Datei ist da"), Platte (schreiben UND wieder loeschen, der einzige Beweis),
Ausliefern (eine zufaellige Datei aus der Datenbank auf der Platte nachmessen),
Bestand, Platz und Sicherung. 200 wenn der Dienst sein Geschaeft tut, 503 wenn
nicht; ?streng=1 laesst auch eine Beeintraechtigung rot werden -- WELCHES von
beiden richtig ist, weiss nur, wer die Statusseite betreibt.

Oeffentlich, aber wortkarg: keine Dateizahlen, Groessen, Pfade, Tokennamen,
Benutzer. Und mit einer Zehn-Sekunden-Bremse -- ein oeffentlicher Endpunkt, der
auf die Platte schreibt, waere sonst ein Verstaerker.

UND DABEI FIEL EIN FEHLER IN MEINEM EIGENEN ZURUECKSPIEL-SKRIPT AUF.

Der harte Weg sollte an einem Wegwerf-Container geprueft werden. Der traf
zufaellig auf ein GEBRAUCHTES Volume, und die Zahlen waren eindeutig:

    media.db aus dem Archiv, mit altem WAL daneben :     0 Zeilen
    dieselbe Datei ohne die beiden Begleiter       :  4452 Zeilen
    Dateien auf der Platte                        :  4452

zurueckspielen.ps1 entfernte media.db, aber NICHT media.db-wal und
media.db-shm. Die liegen bei einem echten Zurueckspielen immer da -- der
laufende Dienst arbeitet im WAL-Modus. SQLite spielt das WAL der ALTEN
Datenbank ueber die NEUE, und heraus kommt der schlimmste denkbare Zustand: der
Dienst kommt hoch, /health ist gruen, die Mediathek ist leer, waehrend alle
Dateien danebenliegen.

Drei Konsequenzen:

1. Die Aufraeumzeile steht jetzt an EINER Stelle und nimmt media.db-wal,
   media.db-shm und *.tmp mit. Uebung und Ernstfall fahren denselben Befehl --
   zwei Fassungen waeren zwei, von denen die geuebte die harmlosere ist.

2. Die Uebung TAEUSCHT JETZT EINE BESTEHENDE INSTALLATION VOR, bevor sie
   zurueckspielt: Container starten, warten bis media.db-wal daliegt, stoppen,
   und erst dann einspielen. In ein leeres Volume zu spielen probt den Fall,
   der nie eintritt.

3. /status erkennt den Zustand selbst -- "kein Eintrag in der Datenbank, aber
   Dateien auf der Platte". Am kaputten Container gemessen:

       /health sagt:  HTTP 200
       /status sagt:  HTTP 503

Der geuebte Lauf danach, ueber eine vorgetaeuschte Installation:

    im Volume liegt jetzt: files media.db media.db-shm media.db-wal
    4452 Medieneintraege, 4452 Dateien -- gleich viele
    ok  items/shushi.png · items/weedbud_1.png · items/cc-castella.png
    Die Uebung ist bestanden.

Gefunden beim Ueben und nicht im Ernstfall. Genau dafuer gibt es sie.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 00:04:26 +02:00
D4rkst3randClaude Opus 5 338f8e59cf feat: die Sicherung laesst sich jetzt zurueckspielen -- und wurde es auch
Bis hierhin war sie GEPRUEFT, nicht ERPROBT. sichern.ps1 packt sein Archiv aus,
oeffnet die Datenbank, zaehlt, laedt hoch, holt zurueck und vergleicht die
Pruefsumme. Das beantwortet "ist die Datei heil?".

Es beantwortet nicht die Frage, auf die es ankommt: "kommt daraus wieder ein
laufender Dienst?" Dazwischen liegen Handgriffe, die man sonst zum ersten Mal
an dem Abend macht, an dem man sie unter Druck macht -- wie das Volume heisst,
dass der Container stehen muss, wem die Dateien gehoeren muessen, dass thumbs/
und webp/ abgeleitet sind und neu gerechnet werden wollen.

Ein Backup, das man nie zurueckgespielt hat, ist kein Backup, sondern eine
Datei.

DIE UEBUNG IST DER STANDARD, und sie ist der eigentliche Gewinn: Wegwerf-
Volume anlegen, Archiv hineinspielen, einen ZWEITEN Container davorhaengen
(ohne veroeffentlichten Port -- der Uebungslauf soll sich mit nichts anlegen,
was schon laeuft), auf /health warten, nachzaehlen, und dann der Teil, auf den
es ankommt: drei zufaellige Dateien DURCH den Dienst holen und ihr SHA-256
gegen den vergleichen, der in der Datenbank steht. Danach alles abraeumen, im
finally -- ein liegengebliebenes Uebungs-Volume mit 300 MB findet sonst niemand
wieder.

Gelaufen, nicht behauptet:

    4452 Medieneintraege, 316,3 MB, 2 Token, 2 Benutzer, 0 im Papierkorb
    4452 Dateien -- gleich viele wie Eintraege
    ok  items/velum2.png                  113567 B
    ok  items/flesh-dolphin-2star.png       3623 B
    ok  items/burgermeat.png                4269 B
    Die Uebung ist bestanden.

Der echte Dienst lief die ganze Zeit weiter und war danach unveraendert
(health 200, dasselbe Volume, 4542 Dateien).

DER ERNSTFALL verlangt ein getipptes ZURUECKSPIELEN und sagt vorher, was
passiert. Er loescht thumbs/ und webp/ mit: Abgeleitetes stehenzulassen waere
schlimmer als es neu zu rechnen -- die Galerie zeigte sonst die Vorschau einer
Datei, die dort nicht mehr liegt. Loeschen und Einspielen laufen in EINEM
Container-Aufruf, damit dazwischen niemand den Dienst wieder startet. Geht
etwas schief, sagt das Skript, dass der Container steht, und wie man ihn
startet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 23:50:46 +02:00
D4rkst3randClaude Opus 5 7877c0f7b9 docs: der Proxy-Block sitzt -- gemessen, nicht geglaubt
Eingetragen und gespeichert. tools/proxy-pruefen.ps1 sagt jetzt fuenf von
fuenf:

    [ok]  /f/  Cache-Control                        public, max-age=300
    [ok]  /t/  Cache-Control                        public, max-age=60
    [ok]  /api/ mit Token -> 200                    HTTP 200
    [ok]  /api/ dieselbe Adresse ohne Token -> 401  HTTP 401
    [ok]  /api/exists?pfad ohne Token -> 401        HTTP 401

Und die Folgen, die daran hingen, einzeln gegengeprueft:

    /api/exists/…webp ohne Token     200 mit voller Auskunft  ->  401
    dieselbe Adresse MIT Token       401 (zwischengespeichert) ->  200
    Vorschau nach erneutem Upload    bis zu 4 h alt            ->  max-age=60
    Vary auf /f/                     wurde entfernt            ->  vary: Accept

Die WebP-Aushandlung am selben Bild: ohne Accept image/png mit 12295 B, mit
Accept: image/webp dann image/webp mit 2270 B. Gleiche Adresse, 82 Prozent
weniger -- das ging vorher nur deshalb, weil /f/ als einziger Pfad schon
ausgenommen war.

Was bleibt: /api/exists?pfad=… ist weiterhin die empfohlene Form. Sie ist
dicht, OHNE von dieser Proxy-Einstellung abzuhaengen -- eine Zugangspruefung,
die an einer Zeile in einer fremden Oberflaeche haengt, ist keine. Der Fall hat
gerade gezeigt, wie schnell so eine Zeile fehlt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 23:41:51 +02:00
D4rkst3randClaude Opus 5 914f2fb854 docs: der NPM-Block zum Einfuegen, und ein Skript, das sagt ob er sitzt
docs/npm-advanced.conf enthaelt genau das, was in NPM unter Advanced ->
Custom Nginx Configuration gehoert: die drei ^~-Bloecke fuer /f/, /t/ und
/api/, samt Begruendung als Kommentar. Die ganze Datei ist einfuegbar --
nginx kennt '#'. Syntax gegengeprueft mit `nginx -t` in einem
Wegwerf-Container, und zwar mit DIESEM Dateiinhalt und nicht mit einer
abgetippten Kurzfassung.

/s/ steht bewusst NICHT drin: die Schluessel bestehen aus einem festen
Alphabet ohne Punkt, eine Freigabe-Adresse endet also nie auf eine
Bildendung und faellt gar nicht erst in die assets.conf-Regel. /assets/
ebenfalls nicht -- die Dateinamen der gebauten Oberflaeche tragen einen Hash
und aendern sich nie unter demselben Namen, dort ist der Zwischenspeicher
richtig.

tools/proxy-pruefen.ps1 misst danach, ob es gewirkt hat, und sagt sonst, was
zu tun ist. Beim Schreiben hat es gleich eine SCHWAECHE IN SICH SELBST
gezeigt: die erste Fassung prueft "/api/ ohne Token -> 401" und meldete
Erfolg -- der 401 war aber nur ein zwischengespeicherter, den der Proxy an
alle ausliefert. Gemessen bekam ihn sogar ein Aufrufer MIT gueltigem Token.

Verlaesslich ist nur das Paar auf einer FRISCHEN, zufaelligen Adresse:

    GET /api/exists/probe-xyz.webp   mit Token    -> 200
    GET /api/exists/probe-xyz.webp   ohne Token   -> 401  gut
                                                  -> 200  Block fehlt

Aktueller Stand, gemessen:

    [ok]  /f/  Cache-Control                     public, max-age=300
    [!!]  /t/  Cache-Control                     max-age=10608
    [ok]  /api/ mit Token -> 200                 HTTP 200
    [!!]  /api/ dieselbe Adresse ohne Token      HTTP 200 -- der Proxy gibt sie weiter
    [ok]  /api/exists?pfad ohne Token -> 401     HTTP 401

Die letzte Zeile ist der Grund, warum die Abfrageform gebaut wurde: sie ist
dicht, ohne von dieser Proxy-Einstellung abzuhaengen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 23:34:02 +02:00
D4rkst3randClaude Opus 5 458775f9d9 feat: Konten sichtbar, Kontingent je Token, Plattenwaechter, Umbenennen im Verlauf
KONTEN SICHTBAR. Es gab zwei -- "admin" und eines aus Discord -- und KEINE
Stelle, an der man sie sehen konnte. Weil jeder mit der Rolle auf dem
Discord-Server beim ersten Anmelden still ein Konto bekommt, waeren aus zwei
unbemerkt zwanzig geworden: man erfaehrt es nicht, und wegnehmen ging auch
nicht.

Jetzt eine Liste mit Weg (Discord/Passwort), angelegt, zuletzt da und offenen
Sitzungen. Die Spalte "Weg" ist dabei die wichtigere Auskunft als der Name: ein
Discord-Konto haengt an einer Rolle, die jemand anders vergibt.

Zwei Sperren, beide mit Grund: das EIGENE Konto laesst sich hier nicht
entfernen -- das ist keine Bevormundung, sondern die Vermeidung des einen
Klicks, nach dem niemand mehr hereinkommt -- und das LETZTE auch nicht. Die
Rueckfrage sagt bei einem Discord-Konto dazu, dass die naechste Anmeldung ein
neues anlegt, solange die Rolle bleibt; sonst haelt jemand das Entfernen fuer
eine Aussperrung.

"zuletzt da" wird hoechstens einmal je Stunde geschrieben. Bei jedem Aufruf
waere es ein UPDATE je Anfrage, und das Dashboard stellt beim Blaettern durch
3600 Bilder eine Menge davon.

KONTINGENT JE TOKEN. max_bytes gab es laengst, aber es galt JE DATEI: ein Token
mit "hoechstens 2 MB je Bild" konnte trotzdem die Platte fuellen, es brauchte
nur genug Bilder. quota_bytes gilt fuer alles zusammen, gezaehlt ueber
media.token_id -- also ueber das, was wirklich liegt, statt ueber einen
mitlaufenden Zaehler, der auseinanderlaufen kann.

Auf BEIDEN Wegen geprueft, eigene API und Fivemanage-Weg: eine Grenze, die nur
an einer von zwei Tueren haengt, ist keine.

Die erste Fassung meldete "haelt 0.0 MB von 0.0 MB, und diese Datei braucht 0.0
MB" -- formal richtig, praktisch die Meldung, die genau die Frage nicht
beantwortet, wegen der man sie liest. Die Einheit waechst jetzt mit: "haelt 12
KB von 20 KB, und diese Datei braucht 12 KB."

PLATTENWAECHTER. Meldet bei 80, 90 und 95 Prozent, je Stufe genau einmal; wird
wieder Platz frei, meldet der naechste Engpass erneut. Der Grund steht in der
Meldung: eine volle Platte ist der eine Zustand, in dem gleichzeitig nichts mehr
hereinkommt UND die Sicherung nicht mehr schreiben kann.

Gemessen, und die Abweichung zu df ist gewollt:

    gesamt         1006,9 GB
    frei (bavail)   909,5 GB  ->  9,7 % belegt   <- was der Waechter sieht
    frei (bfree)    960,7 GB  ->  4,6 % belegt
    df sagt                        5 %

bavail zieht die ext4-Reserve ab, an die der Dienst als Nicht-root ohnehin nicht
herankommt. Er warnt damit etwas frueher -- die richtige Richtung. Wer spaeter
den Unterschied zu df sucht, findet ihn als Kommentar an Ort und Stelle.

Dabei fiel ein SCHLAFENDER FEHLER auf: der Merker gegen Wiederholungen lag unter
EINEM Schluessel fuer alle Wachhunde. Mit zwei davon haetten sie sich
gegenseitig ueberschrieben -- einer meldet, der andere haelt daraufhin seinen
eigenen Zustand fuer neu und meldet auch, oder schweigt, obwohl sich etwas
geaendert hat. Der Merker haengt jetzt am Anlass.

UMBENENNEN IM VERLAUF. Stand dort als "verschoben". Technisch stimmt das, fuer
den Leser nicht: verschoben heisst "liegt woanders", umbenannt heisst "heisst
anders, liegt noch da" -- und genau diesen Unterschied schlaegt man im Verlauf
nach. Eigene Art mit eigenem Zeichen. Die Filterleiste kennt jetzt auch move und
rename, die vorher gar nicht filterbar waren.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 23:26:22 +02:00
D4rkst3randClaude Opus 5 06e924daf5 fix: die Sicherung liess den Papierkorb aus -- ein Versprechen, das sie nicht halten konnte
GEFUNDEN BEIM NACHSEHEN, nicht beim Suchen: das Sicherungsskript packte

    tar czf ... -C /data files -C /data/.sicherung media.db

also die Bilder und die Datenbank. Die EINTRAEGE des Papierkorbs stehen in
media.db und waren damit laengst gesichert -- die DATEIEN lagen unter
/data/papierkorb und waren es nicht.

Nach einem Zurueckspielen haette der Papierkorb also Zeilen gezeigt, deren
"zurueckholen" ins Leere greift: die Oberflaeche verspricht etwas, das die
Platte nicht hergibt. Genau die Sorte stiller Abweichung, wegen der dieses
Projekt Sicherungen ueberhaupt gegenprueft.

Er ist jetzt im Archiv, und er kostet fast nichts: was darin liegt, ist
hoechstens 30 Tage alt, danach raeumt der Dienst selbst auf. Die Abfrage
`if [ -d /data/papierkorb ]` davor ist noetig -- hat noch nie jemand etwas
geloescht, gibt es den Ordner gar nicht, und tar braeche mit "Cannot stat" ab.
Ein `mkdir` als Ausweg ginge nicht: das Volume haengt nur lesend.

Gemessen an einem echten Lauf, nicht angenommen:

    d4rk_media-2026-08-11-2307.tar.gz -- 311,9 MB
    in der Sicherung: 4452 Medieneintraege, 4452 Dateien, 2 Token, 2 Benutzer
    hochgeladen (201), zurueckgeholt und verglichen: identisch
    Inhalt:  4459 files/   1 media.db   1 papierkorb/

UND DER PAPIERKORB SAGT ES JETZT SELBST. Die Liste prueft je Zeile, ob die
Datei wirklich noch daliegt (hoechstens 500 stat-Aufrufe), und zeigt fehlende
durchgestrichen samt Erklaerung. Die Sicherung nimmt ihn zwar inzwischen mit --
aber ein Papierkorb, der etwas verspricht, muss es halten koennen, und wenn
nicht, soll er es SAGEN statt es beim Druecken herauszufinden.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 23:11:38 +02:00
D4rkst3randClaude Opus 5 92b49a59a5 fix: /api/exists gab ohne Token Auskunft -- der Proxy hatte sie zwischengespeichert
GEMESSEN, nicht vermutet: ein GET auf

    https://fivemanage.d4rkst3r.de/api/exists/vehicles/adder.webp

antwortet OHNE Authorization-Kopf mit 200 und voller Auskunft -- Pfad, Groesse,
SHA-256, Zeitpunkt. Nicht weil die Pruefung fehlt, sondern weil Nginx Proxy
Manager die berechtigte Antwort weggelegt hat und sie danach an jeden ausliefert.

Der Grund steht in dessen assets.conf und greift auf JEDE URL mit Bildendung:

    location ~* ^.*\.(css|js|jpe?g|gif|png|webp|...)$ {
        proxy_cache public-cache;
        proxy_cache_key $host$request_uri;          <- ohne Authorization
        proxy_ignore_headers Set-Cookie Cache-Control Expires ...;
        proxy_cache_valid any 30m;
    }

Unser eigenes Cache-Control hilft nicht, proxy_ignore_headers wirft es weg. Und
es geht auch andersherum: landet zuerst ein 401 im Zwischenspeicher, bekommen
ihn 30 Minuten lang alle -- auch die mit gueltigem Token.

Der Block "location ^~ /api/ { proxy_cache off; }" schliesst das. Aber eine
ZUGANGSPRUEFUNG, DIE AN EINER ZEILE IN EINER FREMDEN OBERFLAECHE HAENGT, IST
KEINE: sie ueberlebt kein Neuanlegen des Proxy-Hosts und keinen Umzug.

Deshalb eine zweite Form, die von sich aus dicht ist:

    GET /api/exists?pfad=vehicles/adder.webp

Die Abfrage zaehlt fuer die location-Regel nicht mit, die URL endet also nie auf
eine Bildendung. Gemessen: mit Token 200, ohne Token 401, und KEINE
Cache-Control-Kopfzeile vom Proxy -- die Regel greift nicht.

Die alte Form bleibt, weil das Fotostudio sie benutzt (server/upload.lua:185).
Dokumentation und die API-Seite im Dashboard nennen ab jetzt nur noch die neue.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 23:00:27 +02:00
D4rkst3randClaude Opus 5 44ad7dc6b3 feat: Umbenennen, Fivemanage-Sprache, Freigabe-Links, sichtbare Zahlen
UMBENENNEN gab es gar nicht -- verschieben ja, umbenennen nirgends. Jetzt fuer
Datei und Ordner. Die WARNUNG ist dabei der eigentliche Teil: beim Verschieben
wandert eine Datei, beim Umbenennen aendert sich IHRE ADRESSE, und die steht
womoeglich in einem Skript, das niemand mehr im Kopf hat. Der Dialog zeigt
deshalb die Abrufzahl und beim Ordner die Zahl der betroffenen Dateien, BEVOR
gedrueckt wird -- eine Datei mit viertausend Abrufen umzubenennen ist etwas
anderes als eine mit null, und der Dienst ist die einzige Stelle, die den
Unterschied kennt.

Am echten Fall geprueft, dem Kollisionsfund: items/coiloverss.png (hielt die
+-Fassung) -> items/coiloverss-plus.png, alte Adresse 404, neue 200 mit 4375 B,
Vorschau mitgewandert. Ordner: probe -> beispiele, 5 Dateien, alle Pfade in
einer Transaktion umgeschrieben.

Ein Fallstrick dabei: thumbPath haengt ".webp" an. Fuer eine Datei richtig, fuer
einen ORDNER Unsinn -- der Vorschauordner heisst thumbs/vehicles und nicht
thumbs/vehicles.webp. Dafuer gibt es jetzt moveFolder.

DIE 93 KOLLISIONEN SIND ERLEDIGT. tools/kollisionen.ps1 rechnet dieselbe
Zaehmung auf der Quelle nach und zeigt, was zusammenfaellt -- es aendert nichts,
damit die Entscheidung auf Zahlen steht. Ergebnis: 91 von 93 sind dieselbe Datei
zweimal (WEAPON_SMG.png und weapon_smg.png, byteweise gleich gross), da fehlte
nichts. Echt verloren waren ZWEI, beide mit + im Namen. Beide nachgeholt.

FIVEMANAGE-SPRACHE. POST /api/image|video|audio (v1) und POST /api/v3/file (v3),
Schluessel nackt im Authorization-Kopf. Auf einem laufenden Server stecken die
Fivemanage-Aufrufe in einem Dutzend fremder Ressourcen; sie alle umzuschreiben
tut niemand, und deshalb bliebe dieser Dienst ungenutzt daneben stehen. So ist
der Umzug eine Zeile je Skript: die Adresse.

Beide Formen sind AUS ECHTEM CODE abgelesen und nicht geraten -- fivemanage/sdk
fuer v3, Awleks/Devm-Camera fuer den aelteren Weg ueber screenshot-basic.
Gemessen: v1 -> {url,id,path}, v3 -> {status:"ok",data:{id,url}}, ohne
Schluessel 401, ein Video an /api/image -> 415 mit Grund.

Dabei bin ich in eine Falle gelaufen, vor der im eigenen Repo ein Kommentar
warnt: die Token-Wache hing an use('*'), und der Einhaengepunkt ist /api -- also
galt sie auch fuer /api/dash daneben. Das Dashboard bekam 401 auf die ANMELDUNG.
Wortwoertlich derselbe Fehler steht in upload.ts als Kommentar, weil er dort
schon einmal passiert ist. Gemerkt hat es der Gegentest, nicht der Kopf.

FREIGABE-LINKS. /s/<schluessel> zeigt einen Ordner ohne Anmeldung. Was dabei
ausdruecklich dabeisteht, in der Karte und in der Rueckfrage vor dem
Zurueckziehen: FREIGEGEBEN WIRD DIE LISTE, NICHT DER INHALT. Die Dateien sind
ohnehin oeffentlich; ein zurueckgezogener Link macht sie nicht wieder privat, er
nimmt nur die Uebersicht weg. Ohne diesen Satz zieht jemand einen Link zurueck
und glaubt, etwas sei verschwunden.

Die Antwort ist abgemessen: Name, Groesse, Art, Adresse, Laenge. NICHT Hash,
Token, Zeitpunkte, Abrufzahlen, IDs -- nichts davon braucht, wer einen Katalog
ansieht, und jedes davon waere eine Auskunft ueber den Betrieb. Ein unbekannter
Schluessel und ein zurueckgezogener geben dieselbe Antwort.

ZAHLEN, DIE SCHON DA WAREN. Sortieren nach Abrufen und nach "zuletzt geholt";
die Kachel zeigt dann auch diese Zahl statt Groesse und Datum, denn nach etwas
zu ordnen, das man nirgends sieht, ist eine Reihenfolge ohne Begruendung. Und je
Token, was damit abgelegt wurde: media.token_id wird seit dem ersten Tag
geschrieben und war NIRGENDS zu sehen. Gemessen: d4rk_photostudio haelt 810
Dateien / 99,5 MB, 3640 liegen ohne Token da.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 22:53:22 +02:00
D4rkst3randClaude Opus 5 d125c6bf72 feat: Vorschau fuer Video und Ton, Ordner anlegen dort, wo man es sucht
Mit den ersten echten Clips fiel auf, was bei 3600 Bildern nie auffiel: alles,
was kein Bild ist, bekam ein FRAGEZEICHEN auf die Kachel -- ausgerechnet das
Zeichen fuer "unbekannt" auf einer voellig gesunden MP4.

Jetzt: ein Standbild aus dem Clip (ein Zehntel hinein, weil viele mit Schwarz
oder einem Einblender beginnen und eine schwarze Kachel so aussagekraeftig ist
wie gar keine), bei Ton das eingebettete Titelbild, dazu ein Abspielzeichen und
die LAUFZEIT in der Ecke. Fehlt eine Vorschau, kommt ein Zeichen, das die ART
benennt, statt eines Fragezeichens.

DER PREIS IST FFMPEG IM ABBILD, und der wurde zweimal gemessen und zweimal
gedrueckt:

    vorher                                 460 MB
    apt-get install ffmpeg                1,11 GB
    statisches ffmpeg + ffprobe            835 MB
    statisches ffmpeg allein               648 MB

Debians Paket schaltet jeden Filter frei und zieht die Abhaengigkeiten mit:
libllvm15 (114 MB), Mesa samt libgl1-mesa-dri, libz3 und libflite1 -- ein
SPRACHSYNTHESIZER. Alles fuer Filter, die ein Dienst ohne Bildschirm nie
anfasst; --no-install-recommends half nicht, es sind harte Abhaengigkeiten.

ffprobe flog danach auch noch raus: 135 MB fuer eine einzige Aufgabe, die
Laenge zu nennen -- die steht auch im Kopf, den ffmpeg beim Oeffnen druckt.
Gegengeprueft an beiden Dateien, auf zwei Nachkommastellen identisch:

    ffprobe  29.767967   |  ffmpeg  Duration: 00:00:29.77
    ffprobe 645.642449   |  ffmpeg  Duration: 00:10:45.64

Der Preis dafuer ist ebenfalls zu benennen: die Fassung haengt jetzt an einer
Zeile im Dockerfile und nicht mehr an apt. Vertretbar, weil ffmpeg hier nur
eigene, bereits abgelegte Dateien anfasst und nicht am Netz haengt.

EIN FEHLER, DER ZWEIMAL DERSELBE WAR. "Keine Bildspur" ist bei Ton der
Normalfall -- die meisten MP3 tragen kein Titelbild. Der erste Anlauf erkannte
das am WORTLAUT der Fehlermeldung, und der ist zwischen den Fassungen anders:

    ffmpeg 5.1  "Stream map '0:v:0' matches no streams."
    ffmpeg 7.1  "Failed to set value '0:v:0' for option 'map'"

Ergebnis: eine gesunde MP3 wurde als "1 fehlgeschlagen" gezaehlt, und dann
sucht jemand einen Fehler, den es nicht gibt. Gelesen wird jetzt die
SPURENLISTE ("Stream #0:1: Video:"), die seit jeher so dasteht -- und die kommt
aus demselben Aufruf wie die Laenge, also ein Blick in die Datei statt drei.
Gemessen: fehler 0, ohneBild 1.

Beide Wege durchgeprueft. Video: 320x180, Durchschnittsfarbe RGB(97,140,155) --
echtes Bild und kein schwarzer Vorspann. Ton mit angehaengtem Titelbild:
Vorschau erzeugt, dauer 8.05, Kachel zeigt 0:08.

UND ZWEI ORDNER-LUECKEN, beide beim Benutzen gefunden.

"Wie lege ich neue Ordner an?" -- gar nicht, ausser man wusste es: der Knopf
sass NUR im Verschieben-Dialog. Wer einen leeren Ordner wollte, musste erst
Dateien auswaehlen und "Verschieben" druecken. Eine Funktion, die man nur ueber
eine andere Funktion erreicht, ist fuer den, der sie sucht, nicht vorhanden.
Jetzt steht sie unter dem Ordnerbaum, mit der Angabe, IN welchem Ordner sie
entsteht -- sonst tippt jemand "waffen", landet in "items/waffen" und sucht
danach an der Wurzel.

Zielordner beim Hochladen: vorher galt stillschweigend der in der Galerie
angeklickte. Richtig, solange man es weiss, und ein Fallstrick, sobald man es
nicht weiss -- wer nach einer Suche hochlaedt, steht in "Alle" und legt an der
Wurzel ab. Der angeklickte bleibt die Voreinstellung, ist jetzt aber sichtbar
und aenderbar, samt "neuer Ordner" direkt daneben.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 22:22:39 +02:00
D4rkst3randClaude Opus 5 7b35ef5698 feat: Betrachter fuer Lesbares, und aus dem <video> wird ein Player
Bild, Video, Ton und PDF konnte der Betrachter schon. Was fehlte, war
ausgerechnet die Art, die man am ehesten nur lesen und nicht herunterladen
will: eine .md landete im Zweig "laesst sich hier nicht anzeigen".

LESBARES. Markdown gesetzt, .lua und .json eingefaerbt, CSV als Tabelle, alles
Uebrige mit Zeilennummern. Der Tokenizer ist DERSELBE wie fuer die Schnipsel
auf der API-Seite und nicht ein zweiter -- zwei waeren zwei, die auseinander
laufen. Die Zeilennummern liegen in einer eigenen Spalte: so nimmt ein
Markieren mit der Maus sie nicht mit.

Die CSV-Zerlegung kann Anfuehrungszeichen und doppelte darin. Das ist der Teil,
den ein split(';') falsch macht, und CSV aus Excel hat ihn regelmaessig; das
Trennzeichen wird aus der Kopfzeile geraten.

NICHT ALLES WIRD GEHOLT. Die Groesse steht in der Datenbank, also wissen wir
vorher, worauf wir uns einlassen: ueber 512 KB kommt per Bereichsanfrage nur
der Anfang, und das steht auch da. Ein Betrachter, der bei einem 400-MB-
Protokoll den Tab abschiesst, ist schlimmer als einer, der die Datei gar nicht
erst oeffnet. Gemessen: Range: bytes=0-49 -> 206, 50 Bytes.

MARKDOWN OHNE BIBLIOTHEK, und das ist kein Geiz. Der uebliche Weg heisst marked
plus DOMPurify und endet bei dangerouslySetInnerHTML -- damit haengt die
Sicherheit dieser Seite an der Frage, ob die Filterliste vollstaendig ist. Hier
entsteht NIE eine HTML-Zeichenkette: der Text wird zu React-Knoten, und ein
<script> in einer hochgeladenen .md erscheint als die acht Zeichen, die es ist.
Im ganzen ui/ steht kein einziges dangerouslySetInnerHTML.

Die eine Luecke, die React nicht schliesst, ist href: [klick](javascript:...)
kaeme durch. Zwoelf Faelle in Node durchgeprueft, alle bestanden -- http,
https, mailto und relative Ziele durch; javascript:, JavaScript:, mit
Leerzeichen davor, data:, vbscript:, file: abgelehnt und als Text stehen
gelassen.

DER PLAYER. Die Bedienelemente bleiben die des Browsers: ein selbstgebauter
Schieber sieht in jedem Browser anders falsch aus, kennt keine
Tastatursteuerung und keine Untertitel. Drumherum kam, was der Browser nicht
mitbringt.

Lautstaerke ueber Dateien hinweg -- <video> setzt sie bei jedem neuen Element
auf 1 zurueck, und wer sich durch zwanzig Clips klickt, stellt sie sonst
zwanzigmal leise, waehrend der einundzwanzigste ungefragt wieder hochfaehrt.

Weiter zum naechsten Stueck, abschaltbar, und nur wenn das Naechste auch etwas
zum Abspielen ist: nach einem Lied ungefragt bei einem Bild zu landen waere
schlechter als stehenzubleiben.

Tastatur an EINER Stelle: Leertaste haelt an, Pfeile springen fuenf Sekunden,
Umschalt+Pfeil wechselt die Datei. Bei allem anderen blaettern die Pfeile wie
bisher. Dass das so ist, steht als Zeile unter dem Bild -- eine unsichtbare
Sonderregel fuehlt sich an wie ein Fehler.

SERVER. Die Typtabelle kennt jetzt md, markdown, csv, lua, xml, yml, yaml,
toml, ini, cfg, sql und log -- alle als text/plain. Nicht als text/markdown
(braechte nichts) und erst recht nicht als text/html: wer einen Upload-Token
hat, koennte damit eine Seite unter unserem Namen veroeffentlichen. html, css
und js stehen deshalb weiterhin NICHT drin.

Der Betrachter entscheidet trotzdem nach der Endung und erst danach nach der
gemeldeten Art. Grund steht in der Datenbank: items/readme.md lag schon da und
trug application/octet-stream, weil sie vor der erweiterten Tabelle hochgeladen
wurde. Wer nur auf die Art schaut, zeigt die alte README nicht an und eine neue
schon -- und sucht den Unterschied an der falschen Stelle.

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