← throway
# throway — Releases

**Current version:** `1.54.1`

A disposable file store. Upload a file — or a bundle of files (e.g. a
website) — and get a short-lived URL. No auth. Nothing permanent.

---

## 1.53.3 — 2026-10-04

### 413-Meldungen aus der Konfiguration statt hartkodiert

„too large (max 5MB)" stand an **sieben Stellen** hartkodiert in store.py
(5× Früh-/Spätcheck im Upload/Edit-Pfad, 1× Import-Fehlermeldung, 1×
„bundle too large (pool max 100MB)") — dieselbe Drift-Klasse, die 1.53.0
für die /api-Notes fixte, in den Response-Bodies übersehen. Fund bei der
Issue-#1-Verifikation. Steigt `THROWAWAY_MAX_FILE_BYTES` per Env, logten
die Meldungen falsch. Jetzt: aus `MAX_FILE` bzw. `THROW_POOL_SIZE`
berechnet. (pics.py war bereits korrekt: `_fmt(PICS_MAX_FILE)`.)

Neu: `tests/test_413_message.py` (3 Tests) — Env-Max 2MB vs Default 5MB,
beide Wege gedeckt: Frühcheck per Raw-Socket (der Server antwortet 413,
ohne den Body zu lesen — dokumentiert nebenbei das Nicht-Drain-Verhalten
des Files-Pfads, das pics seit 1.20 beherrscht) und Post-read via PATCH.

Tests: **164 passed** (161 unverändert + 3 neue) · validate 28/16/50 PASS.

---

## 1.53.2 — 2026-10-04

### Doc-Wahrheit: Whole-Dir-Delete auf offenen Dirs ist offen

AGENTS.md behauptete „Whole-dir **delete always** needs the dir's token —
even on open dirs". Der Code (`dirs.py delete`) gatest nur `write=1`-Dirs
(`_write_guard`) und retained Dirs (Retain-Token) — offene Dirs sind per
`DELETE /d/<key>` frei löschbar, `rmtree` ohne jedes Token. Bewiesen durch
Session 01a0fc8a (Token-loser DELETE auf Produktion → 200) beim Aufräumen
ihres `throway-version-drift`-Dirs.

Kein Verhaltenschange — die Doku auf die Code-Wahrheit gebracht:

- **AGENTS.md**: Satz korrigiert — Token-Pflicht gilt für write-geschützte
  und retained Dirs; offene Dirs sind offen löschbar („use &write=1 to
  protect"). Die AGENTS.md-Kopie liegt auch im Deploy-Root als
  Agent-Kontext — ein Agent, der der alten Aussage glaubte, verließ sich
  auf einen Schutz, der nicht existierte.
- **/help/dirs** (dirs.py-Topic): Warnzeile an der DELETE-Zeile —
  „open dirs: anyone with the URL can do this — protect with &write=1;
  retained dirs need the retain token".

Tests: 161 passed unverändert · validate 28/16/50 PASS.

---

## 1.53.1 — 2026-10-04

### Retro-Guards: die Session-Lehren der Review-Woche als Checks

Kein Verhaltenschange — fünf Fundgebungen aus den Releases 1.49.0–1.53.0,
jede verifiziert, jede jetzt mechanisch bewacht oder aufgeschrieben:

- **Atomic-Write-Guard verschärft**: die alte Regex sah die with-Block-Form
  (`with open(..) as f: json.dump(..)`) nicht — genau so blieben pics
  `save_meta`/`save_gallery` monatelang nicht-atomar unentdeckt (1.52.0-
  Fund). Seit 1.52.0 gehen ALLE JSON-Writes durch `throway/storage.py`,
  also die enge Regel: `json.dump` existiert nur noch dort. Negativ getestet
  (die with-Block-Form failt jetzt rot).
- **conftest-Session-Root**: In-process-Importe von store.py crashen ohne
  `THROWAWAY_ROOT` auf Dev-Maschinen (OSError read-only /srv, passiert im
  1.53.0-Fidelity-Loop) — conftest setzt jetzt einen Tempdir-Default;
  Subprocess-Tests überschreiben explizit.
- **CONTEXT.md-Pointer in AGENTS.md**: die Architektur-Karte (Namespace/Kit/
  storage/Homepage-Begriffe) war über kein Steering-Dokument erreichbar.
- **CJK-Glitch-Guard**: LLM-Glitch-Klasse in deutscher Agent-Prosa
  („ Leser“ statt „Leser“, 1.52.0 von Hand gefangen) — release-check fängt
  CJK/Hangul in CONTEXT/RELEASES/AGENTS/API jetzt deterministisch. Negativ
  getestet.
- **Beweis-Pflicht je Änderungsklasse** in CODING_RULES.md (Review-Standard):
  verhaltensidentisch → Byte-Diff · Renn-Fix → Konkurrenz-Test N×M exakt ·
  Extraktion → Suite unverändert (+ Naht-Alarm) · Renderer → Fidelitäts-
  Beweis. Die Protokolle aus 1.49–1.53, einmal aufgeschrieben.

---

## 1.53.0 — 2026-10-04

### Contract als Daten — der Slice (Architektur-Review, Kandidat 5)

`throway/contract.py`: die dünnen Wahrheiten haben jetzt ein Home und
pro Fläche einen Renderer. Bewusst NUR der Slice (Q1b) — die Voll-Vision
(HELP-Prosa + /api + API.md aus einer Struktur) wäre eine falsche Naht:
Tutorial-Register und Maschinen-Register sind zu 80% verschiedener Content.

* **`limits()`** — alle Live-Werte (4h, 14d, 5 MB, Pools, Rate-Limits…)
  als eine Map; substituiert HELP-Bodies (`_render_help_body`), /api-Notes
  und generiert die API.md-Limits-Tabelle. Hartkodierte Note-Werte
  („MAX 5MB", „max 14 days", „default 7d") sind jetzt Platzhalter — die
  letzte Drift-Klasse im Spec ist zu.
* **`errors()`** — die 9 stabilen Error-Codes als Daten (code, http,
  meaning, retry). Renderer: HELP-„errors"-Body (generiert; Spalten jetzt
  einheitlich — 413/429 waren hand-schief, Worte unveraendert) und ein
  NEUER additiver `errors`-Block in /api (alte Agents unberührt).
* **API.md Marker-Ansatz** (Q3b): Prosa bleibt handgeschrieben, die
  Limits-Tabelle zwischen `<!-- gen:limits -->`-Markern wird von
  `scripts/gen-api-md.py` aus dem Contract generiert; `--check` + neuer
  release-check-Guard („API.md frisch") + Test machen Hand-Edits an den
  Zahlen strukturell rot. Zuvor war API.md von KEINEM Validator gedeckt.
* `substitute()` ersetzt Platzhalter, laesst Unbekannte und Literal-Braces
  (`{ts,file,action,bytes}`) unberührt — sicher auf jeden Note-String.
* Alle 94 Validator-Checks bleiben (Q5) — sie testen Verhalten, nicht nur
  Drift; nur der neue Frische-Guard kam dazu.

Tests: **161 passed** (153 unverändert + 8 neue in test_contract.py —
u.a. „jeder HELP-Platzhalter ist in limits()") · validate 28/16/50 PASS.

---
## 1.52.0 — 2026-10-04

### Shared storage mechanics (Architektur-Review, Kandidat 4)

Eine Mechanik statt dreier: `throway/storage.py` — `atomic_json(path, obj)`
und `locked(ns)` (pro-Namespace-RLock um load→mutate→save). Alle drei
Namespaces sitzen darauf; Pools, Accounting und Eviction bleiben pro
Namespace (LRU vs 507). Erster Review-Kandidat mit nachweislicher
Robustheit statt nur Struktur:

* **pics: drei nicht-atomare Writes gefixt** — `save_gallery` UND `save_meta`
  (beide plain `open("w")+json.dump` — das 1.45.2-Datenverlust-Muster,
  `save_meta` beim Fact-Finding neu entdeckt) jetzt via `atomic_json`
* **pics: neun Mutatoren serialisiert** — likes ×2, comments ×2,
  `create_gallery` (Create-or-Get-Rennen), `touch_gallery` (Sliding),
  `moderate`, `reorder`, `remove_gallery` — alles `@locked("pics")`
* **store: `_idem_put` Inline-Duplikat ersetzt** und `_save_stats` atomar
  (best-effort bleibt); neuer gelockter `_bump_stats` ersetzt die zwei
  Inline-Stats-RMW in `_store`/Bundle-Speicherung (Parallels-Uploads
  verloren Zähler) — Kit `bump_stats` delegiert nur noch
* **store: Meta-RMW serialisiert** — `_retain_flip`, `_retain_meta`,
  `_file_tags` jetzt `@locked("files")`
* **dirs: `_LOCK` delegiert** an `storage.lock_for("dirs")` — 8 Mutations-
  Sites unberührt, Mechanik hat jetzt ein Home
* Bewusst ungelockt (dokumentiert): `store_pic` (Bildverarbeitung zu
  teuer zum Serialisieren; Dedupe-Rennen kostet allenfalls ein Dublett)
  und die Sweeps (locken pro Löschung via `remove_gallery`)
* `tests/test_storage.py` (6 Tests): Atomicität (kein `.part`-Rest,
  In-Place-Overwrite), **Lost-Update-Beweis** (8 Threads × 200 RMW =
  exakt 1600), RMW-auf-Datei, Lock-Identität pro Namespace, Reentränz

**Beweis-Anmerkung:** Anders als 1.51.0 ist kein Byte-Diff möglich — der
Fix ändert Verhalten im Rennfall (von falsch auf richtig). Beweis = Suite
unverändert grün + der deterministische Lost-Update-Test.

Tests: **153 passed** (147 unverändert + 6 neue) · validate 28/16/50 PASS.

---
## 1.51.0 — 2026-10-04

### Homepage-UI als eigenes Modul (Architektur-Review, Kandidat 3)

Die komplette Upload-UI (Dropzone-JS 510 Z., Homepage-CSS ~120 Regeln,
HTML-Assembly, DROPZONE-CDN-Consts) lebt jetzt in `throway/index.py` —
als reines Präsentationsmodul nach mdrender-Präzedenz:

* **`page(stats) -> str`** — deterministisch über einen flachen Stats-Dict
  (Pool-Belegung, Session-Zähler, Version, ttl, prefix, max_mb,
  Agent-Beschreibung); kein Handler, kein Kit, keine I/O
* Der Handler behält einen Mini-Dispatcher (~20 Z.): sweep() + Stats
  sammeln + senden; UA-Split (Agent → Klartext-Help) bleibt im Routing
* CSS als benannte Konstante `_INDEX_CSS` — der CSS-Balance-Guard im
  release-check deckt jetzt 6 Blöcke (5× pics + Homepage, previously
  wäre die größte CSS-Masse unguarded gewesen)
* `tests/test_index.py` (7 Tests, mdrender-Stil): Marker, Stats-Rendering,
  `__MAX_MB__`-Substitution, Agent-Text escaped, JS-Konstante bleibt str,
  Inline-Scripts bestehen `node --check`, CSS-Klammer-Balance
* Totes Gerüst entsorgt: `_IndexMixin` (leere Klasse) + der
  `Handler._index`-Monkey-Patch
* store.py: 3220 → 2459 Zeilen (nur noch Routing + Engine + Kit)

**Beweis:** Homepage-Antwort vor/nach der Migration byte-identisch
(normalisiert auf die Versions-Badge, cmp-geprüft).

Tests: **147 passed** (140 unverändert + 7 neue) · validate 28/16/50 PASS.

---

## 1.50.0 — 2026-10-04

### pics zieht aufs Request-Kit nach (Q7b aus dem 1.49.0-Grilling)

Die Naht ist jetzt komplett: **beide Namespace-Module (dirs, pics)
sitzen auf demselben Kit** — pics bekam bisher den Handler (`h`) UND
lazy-importierte das store-Modul. Beide Leaks enden hier:

* `pics.get/post` bekommen das Kit statt `h`; das `(h, store, root)`-
  Threading (16 Dispatch-/Helper-Funktionen + 14 Rendering-Funktionen
  mit `store`-Parameter) kollabiert zu `(kit, root)` bzw. `(kit, …)`
* 49× `h._send`, 7× `h._client_ip`, 6× `h._is_agent` u. a. → Kit-Methoden
* Kit erweitert um `client_ip`, `root`, `header(name, default)`,
  `read_raw(n)` (Streaming/Drain), `fetch_remote` + `FetchError`
* `store_pic`: Stats-Bump als optionaler `bump`-Parameter — der lazy
  `import store` verschwindet; nebenher fixt das die stille Side-Effect-
  Falle, dass der Direktaufruf-Test (test_pics.py:1068) bisher ins
  **Repo**-stats.json schrieb

Verhalten identisch; Verhaltens-Suite unverändert grün.

Tests: **140 passed** · validate 28/16/50 PASS.

---

## 1.49.0 — 2026-10-04

### Dir-Extraktion: `throway/dirs.py` + Request-Kit (Architektur-Review)

Der komplette Dir-Cluster (667 Zeilen: create/add/get/edit/delete/listing/
history/browse/share + Flip-Routen + `?share=`) lebt jetzt im eigenen
Namespace-Modul `throway/dirs.py` (1034 Zeilen mit Interface-Vertrag im
Docstring) — exakt dem pics-Muster folgend, das CODING_RULES vorschreibt.
store.py schrumpft 3942 → 3188 Zeilen und behält nur Routing + Engine.

**Request-Kit als Geburts-Interface** (Geburts-Naht der Module): dirs
bekommt statt des Handlers ein explizites Kit (Senden, Request-Kontext,
geteilte Helfer, Konfig) — die 1.45.5-Bugklasse „Response-Plumbing ist
tragend, aber privat" endet an einer dokumentierten Naht. pics migriert
später nach (bewusst, Q7).

**Engine-Hooks wie bei pics:** `dirs.sweep/ns_total/evict_units` —
Pool-Accounting und Sweep laufen über die Modul-Schnittstelle, das
Dir-Meta-Format gehört exklusiv dem Modul.

**Nebenher gefixt:**
* `show_flip` läuft jetzt unter dem Mutations-Lock (der 1.45.2-Fix,
  für diese Route nachgerüstet)
* pics-Galerienamen validieren über `dirs.valid_name` (vorher
  `store._valid_name` — dieselbe Regel, jetzt eine Heimat)
* `CONTEXT.md` neu: Domain-Glossar (Dir, Share, Show-dir, Retention,
  Write token, Pool, Namespace, Querschnitts-Modul, Request-Kit, Agent)
* `test_retain` Sweep-Check folgt dem Umzug (`store.dirs.sweep` —
  intern, aber mit korrektem ROOT-Reload; alle 140 Verhaltenstests
  unverändert grün)

Tests: **140 passed** (unveränderte Suite — HTTP bleibt die Testfläche).

## 1.48.2 — 2026-10-03

### Code-Review 1.45.4→1.48.1 (2 Sub-Agents: Standards + Spec) — Fixes

**Spec-Achse (echte Abweichungen):**
* P6-Override matcht jetzt **exakte Query-Params** — `?notjson=1`
  erzwang fälschlich JSON (Substring-Falle).
* P5-Bridge meldet bei einzelner Übergroß-Datei präzise
  `too large (max 5MB)` statt pauschal `pool max 100MB`.

**Standards-Achse:**
* `/help/errors` substituiert pics-Limit + 507-Retry **live**
  (`{PICS_MAX_FILE_MB}`, `{PICS_RETRY_AFTER}` aus `pics.py`-
  Konstante — vorher hartcodiert 30MB/300s, drift-gefährdet da
  env-tunable).
* History-Add-Loop **dedupliziert**: lebt jetzt einmal im Erfolgs-
  pfad von `_dir_write_files` (nur neu geschriebene, deduped Namen —
  vorher 3 identische Loops an Aufrufern). Erster Entwurf loggte
  versehentlich alle Bestandsdateien mit — Suite fing es.
* CODING_RULES: Flächen-Zählung präzisiert (3 Flächen: code, API.md,
  README.md; live Werte nie hartcodieren).

Dokumentiert-behalten (bewusste Entscheidungen, nicht geändert):
P5-Add-auf-existierendem-Dir (retry-safe, in /api+AGENTS dokumentiert)
und das breite close-on-error (konservativ).

Suite 140/140, matrix 28/28, gaps 16/16.

---

## 1.48.1 — 2026-10-03

### google-photos-import Stufe 0 (Issue-Audit)

Audit des Issues: Stufe 1 (Server-`?url=`-Import + Dropzone-URL-Drop +
Paste-Import) war bereits vollständig umgesetzt und bewacht
(test_url_import_into_gallery u.a.) — der Plan hinkte dem Code hinterher.
Neu ist nur der Stufe-0-Hinweis in der Dropzone: Bild-Links (auch aus
Google-Photos-Tabs) ziehen/einfügen. Suite 138/138. Stufe 2 (Picker API)
wartet auf Google-Cloud-Zugangsdaten (siehe Issue).

---

## 1.48.0 — 2026-10-03

### AGENTS.md strukturell verschlankt (Issue throway-agents-slim)

595 → 138 Zeilen: Semantik + Pointer statt API-Beispielwüsten. Der
Dienst serviert die Details selbst (`/help/*`, `/api` — PRIO 1), und die
gedoppelte API im AGENTS.md war schon zweimal gelogen (Bundle-Delete-
Doku-Lüge, Duplikat-Zeilen). Kernregeln (Lifetime-Clamps, create-or-get,
write-Gates, once/retain-Kombos, Fehler-Retry-Semantik) bleiben —
Beispiel-Responses und curl-Varianten raus. Reduziert die Release-
Doku-Pflicht von 5 auf 3 Flächen (CODING_RULES aktualisiert); Deploy-
Ritual synct AGENTS.md/API.md/README.md jetzt explizit mit (lubu-Kopie
ist Agent-Kontext). Suite 137/137.

---

## 1.47.0 — 2026-10-03

### SOTA-Agent-Punkte P3/P5/P6 (Issue throway-sota-punkte)

P2 (Idempotenz) war bereits mit 1.44.0 erledigt — Status in PUNKTE.md
korrigiert. Neu:

- **P3 `/help/errors`:** jedes Fehler-`code` + Retry-Strategie als
  Topic (nur 429/507 sind „später wieder"; alles andere heißt: Request
  falsch). `/api` verweist darauf.
- **P5 Multipart-Bridge:** Parts auf `POST /?dir=1[&name=]` → initial
  files; auf EXISTIERENDEM named Dir → **add** wie `POST /d/<key>`
  (vorher still verworfen = Datenverlust-Falle für Retry-Loops). Write
  gates gelten; History loggt jetzt auch create-Adds pro Datei.
- **P6 `?json=1`/`?html=1`:** erzwingt die Listing-Repräsentation auf
  `/d/<key>`, `/d`, history, browse — gegen die UA-Heuristik.

Suite 137/137 (+3 Tests), validate-matrix 28/28, validate-gaps 16/16.

---

## 1.46.0 — 2026-10-03

### Batterien permanent: validate-matrix + validate-gaps in CI

Die beiden Ad-hoc-Batterien aus den Validierungsrunden sind jetzt
Dauerbewacher als Skripte + CI-Jobs:

- `scripts/validate-matrix.py` (28 Checks): Ein-Response-Invariante über
  die ganze Routen-Oberfläche inkl. chunked und 429 — bewacht die
  1.45.5/1.45.6-Bugklassen (Doppel-Response, Socket-Gift).
- `scripts/validate-gaps.py` (16 Checks): der /api-Vertrag außerhalb
  Suite/validate-live/Matrix — ttl-Clamps, download=1, Zip-Inhalte
  (entpackt, nicht nur Status), /d-Filter+sort, Leer-Upload, PATCH-404,
  idem=-Query — bewacht die 1.45.8-Klasse (stiller Vertragsbruch).

Beide spawnen eigene Server (Ports 8150/8152, atexit-Cleanup, hohes
Rate-Limit) und sind parallel-sicher. CI-Jobs `validate-matrix` +
`validate-gaps`. Keine Serveränderung.

---

## 1.45.8 — 2026-10-03

### Luecken-Batterie: once=1&share= war ein Vertragsbruch

Test-Inventur (133 Suite + 50 validate-live + 27 Matrix) gegen /api
gedifft: ttl=-Clamps (Upload min/max, Dir-TTL), download=1, Zip-INHALTE
(erstmals entpackt statt nur Status geprueft), /d-Filter+sort, Leer-
Upload, idem=-Query — alles korrekt. Ein Fund: `/api` + AGENTS.md
versprechen "once=1 not with &share=", der Code nahm es still an und
ignorierte once. Fix: 400 wie bei once+retain. Suite 134/134.

---

## 1.45.7 — 2026-10-03

### Testfix zu 1.45.6 (im Release-Commit versehentlich rot)

Der neue chunked-Regressionstest erwartete 411, lief aber durch
`_dir_add` ( ctype-Check → 400 kommt VOR dem Body-Read). Der adv4-Nachweis
traf durch den `/throway`-Prefix stattdessen den generischen Upload-Pfad
(411) — beide Pfade korrekt und single-response, nur die Testerwartung
war am falschen Pfad orientiert. Test deckt jetzt BEIDE Pfade (400 + 411,
je einmal, mit `Connection: close`). Keine Serveränderung. Suite 133/133.

---

## 1.45.6 — 2026-10-03

### Validierungsrunde 4: chunked-Loch im Close-on-Error

Adversarial-Runde gegen Protokoll-Ränder: `Transfer-Encoding: chunked`-
Requests enden in 411 (Body wird nie gelesen), aber die 1.45.5-Close-
Bedingung griff nur bei `Content-Length > 0` — der ungelesene Chunk-Body
wurde als nächste Anfrage geparsed: **Doppel-Response** (411 +
Phantom-400, raw bewiesen). Fix: Close auch bei `Transfer-Encoding:
chunked` im Request. Daneben verifiziert: Restart-Persistenz des
Retain-Gates, Müll-Request-Lines, CL-Lügen. Neue Batterie
`/tmp/matrix_validate.py` (27 Routen × 1-Response-Invariante) 27/27,
Suite 133/133.

---

## 1.45.5 — 2026-10-03

### Validierung der 1.45.4-Fixes fand die Wurzel + die offene Klasse

Die Raw-Socket-Batterie ("validate deine fixes") widerlegte zwei
Annahmen:

1. **Wurzel: `_send` gab nie True/False zurück.** Die Zeilen, die ich
   1.45.4 als Beleg las, gehörten `_rate`. Jedes `return self._send(...)`
   in `_dir_add` war also None — do_POST schob seit 1.45.2 hinter JEDER
   Dir-Add-Antwort (200/400/413) ein 404 nach; pre-1.45.2 war der
   Fall-Through unsichtbar-harmlos (Body konsumiert). Suite und
   validate-live lasen je nur die erste Response → grün. Fix: `_send`
   returnt True (False bei Hangup). Erfolgs-Dir-Add raw: exakt 1
   Statuszeile + Keep-Alive bleibt.
2. **Klasse offen: Error-Response mit ungelesenem Body vergiftet den
   Keep-Alive-Socket** (phantom-501 auf der Folgerequest) — bei PUT auf
   Binary (400), Non-Multipart-Add (400), Tags-401, … Fix: `_send`
   schließt bei code>=400 mit Content-Length>0 die Connection
   (`Connection: close`). GET-404 ohne Body behält Keep-Alive.

Batterie: 11/11 (Guard-401, PUT-binary, Non-Multipart, Tags-401,
GET-404-Kontrolle). Tests: Suite 132/132 (+Erfolgs-Add-Raw-Test,
Guard-Test auf close-Contract umgestellt).

---

## 1.45.4 — 2026-10-03

### Review-Fixes (code-review Skill, 2 Sub-Agents, Spec- + Standards-Achse)

1. **Doppel-Response bei write-gated Dir-Add (verifiziert per Raw-Socket):
   drei Responses auf eine Anfrage** — 401 (Guard) + 404 (1.45.2-
   Fall-through feuerte nach, weil `_dir_add` nach gesendetem 401 None
   lieferte) + 400 (ungelesener Multipart-Body galt als nächste Anfrage).
   Kein automatisierter Check sah es — Clients lesen die erste Response.
   Fix: Body wird VOR dem Guard gelesen (Keep-Alive-Socket bleibt sauber),
   Guard-Return ist True statt None. Neue Raw-Socket-Regressionstests
   (genau eine Statuszeile + Connection danach benutzbar).
   Verhaltenänderung: bei invalidem Non-Multipart-Add kommt jetzt der
   400 multipart-Fehler vor dem Guard-401.
2. **Flip-Block 5x dedupliziert** (`_retain_meta_dict`), **Middle Man
   `_retain_write_denied` entfernt** (6 Calls direkt aufs Modul),
   **Query-String-Parsing gebündelt** (`retain.query_string`).
3. **validate-live: Env-Fallback** — liest jetzt auch das singular
   `THROWAWAY_RETAIN_TOKEN` der Produktion (Drift-Befund).
4. **Doku:** create_dir-Note nennt show=1-First-Creation explizit;
   CODING_RULES: Cross-Cutting-vs-Namespace-Modul-Klärung.

Tests: Suite 131/131 (+Raw-Socket-Test), validate-live 50/50.

---

## 1.45.3 — 2026-10-02

### Bugfix: `POST /d?retain=1` erzeugte stille retained Muell-Files (hard-validate R2)

Die Flip-Route uebersprang reservierte Namen (`parts[0] != DIR_NS`) und
fiel in den Raw-Body-Upload-Pfad: aus einem Flip-Request auf `/d` wurde
eine *retained Leer-Datei* mit 200. `/pics` war durch pics.post (400)
und `_retain_flip`s internen Guard gedeckt. Fix: reservierte
Namespaces enden mit 404, Root ohne id faellt weiter durch (legitimer
Retain-Upload per Flag bleibt). Test:
`test_flip_on_reserved_names_404`.

---

## 1.45.2 — 2026-10-02

### Bugfix: Data-Loss-Race in der Dir-Maschinerie (Retro hard-validate, Runde 2)

12 parallele Adds auf ein Dir: alle 200, aber **3 Files verschollen,
Meta kannte nur 2, History nur 1 Eintrag**. Präexistent (Dir-Maschinerie),
durch Show-Dirs zum Alltagsrisiko (offener Write = parallele Schreiber
by design). Drei gestapelte Ursachen:

1. **Nicht-atomare Meta-Schreiben**: `json.dump` direkt aufs Ziel —
   concurrent Reader sahen halbe Dateien → `_dir_meta` = None.
2. **`_dir_add`-None fiel durch do_POST** in den Raw-Upload-Pfad: aus
   einem Dir-Add wurde *still ein Einzel-Upload mit 200* (daher schienen
   alle Adds "erfolgreich").
3. **Read-modify-write-Races** auf Meta `files`-Map und History
   (last-writer-wins).

Fix: `_atomic_json()` (tmp + `os.replace`, wie schon idem-map) für alle
14 Persistenz-Schreiben; `_dirlock`-Decorator (`RLock`) auf alle
Dir-Mutatoren; `/d/<key>`-POST endet bei None mit 404 statt Fall-through.

Tests: +2 (`test_parallel_adds_no_loss` mit 12-Thread-Hammer und
Dir-Antwort-Assertion je Add, `test_dir_add_missing_dir_404`);
Suite 129/129 grün.

---

## 1.45.1 — 2026-10-02

### Bugfixes aus der harten Live-Validierung (Retro hard-validate)

1. **Bundles waren per API unloeschbar** (praekexistent, Doku behauptete
   das Gegenteil): `DELETE /<id>` pruefte nur `isfile` — Bundles sind
   Dirs → 404, seit 1.44.0 auch mit Token. Mit Retention akut: retained
   Bundles waren immortal orphans (nur per ssh entfernbar). Fix: Bundle-
   Delete mit Retain-Gate. Regel daraus: **kein Create-Pfad ohne
   Delete-Pfad**.
2. **„Write implies retention" versagte bei `?share=`**: `_share_store`
   rief den Write-Guard nur bei `write_token`-Dirs; Token-Adds auf
   normalen Share-Dirs flippeden nicht. Fix: Guard immer + Meta-Neuladen
   danach (der Flip landete auf Disk, aber `_dir_write_files` schrieb das
   stale In-Memory-Meta zurueck).

Benigne Kanten (dokumentiert, kein Fix noetig): leerer Bearer zaehlt als
kein Token (Wegwerf-Upload); falscher Query-Token ohne Retention-Wunsch
→ fail-closed 401; HTTP/2 lowercased Response-Header (Validierung
case-insensitiv lesen).

Tests: +2 (`test_bundle_deletable_and_retained_gate`,
`test_share_token_add_implies_retention`); Suite 127/127 grün.

---

## 1.45.0 — 2026-10-02

### Feature: Show-Dirs — permanent + öffentlich beschreibbar (`&show=1`)

Issue throway-show-dirs (User-Spec): eine per Retain-Token **einmal
generierte** Dir, danach **unbegrenzt** nutzbar, und **jeder mit der URL
darf read+editen** — beliebige Dateitypen. Das Kollaborations-Pendant zu
1.44.0: reteniert (stabil) aber mit offenem Write-Modell.

- `POST /?dir=1&show=1[&name=<slug>]` + Token → Meta `{retain: true,
  open: true}`: nie expiry/eviction, öffentlich beschreibbar.
- `POST /d/<key>?show=1` + Token flippt Bestands-Dirs (idempotent);
  `show=1` auf Nicht-Dirs → 400.
- File-Level-Ops (add/edit/delete) ohne Token; **Whole-Dir-Delete nur mit
  Token** (schützt das Retention-Versprechen — sonst tötet jeder Joker
  die Show).
- `write_token` (falls bei Create gesetzt) hat weiter Vorrang vor `open`.
- Dir-Responses carry `"open": true`; History läuft mit (Accountability
  ohne Auth). `/api`: `show_dir`-Endpunkt, `&show=1` im create_dir-Note;
  `/help/retention` um SHOW-DIRS-Sektion erweitert.

Tests: `tests/test_show.py` (10 neue); Suite 125/125 grün.

---

## 1.44.0 — 2026-10-02

### Feature: Token-gated indefinite Retention (`retain`)

Issue throway-indefinite-token (from mac@skale-skills): Skill-Installs und
stabile Share-URLs brauchen URLs, die nicht nach 4h/7d sterben — ohne
zweiten Dienst. Lösung: ein serverseitiges **Retain-Token**
(`THROWAWAY_RETAIN_TOKEN`, Env, komma-getrennte Liste möglich; leer =
Feature aus) spaltet Unbeschränktheit vom Wegwerf-Prinzip ab:

- Token-autorisierte Uploads (`Authorization: Bearer <t>` oder dokumentiertes
  `?token=`) erzeugen Objekte **ohne Ablauf**: `expires_at: null`,
  `persistence.retention: "indefinite"`. Gilt für Singles, Bundles, Dirs,
  `?share=` und `?url=`-Importe.
- `POST /<id>?retain=1` (bzw. `/d/<key>?retain=1`) mit Token flippt
  **Bestandsobjekte** auf unbeschränkt (idempotent).
- Token-autorisierte `PUT`/`PATCH`/Dir-Writes machen das Ziel unbeschränkt
  — *write implies retention*.
- **Public read, token-gated write**: Lesen bleibt URL-basiert offen;
  Schreiben/Löschen auf retained Objekten braucht das Token (401).
- `&once=1` + Retention → 400.
- Sweep **und** Eviction überspringen retained Units (`retain`-Flag im
  Meta/Manifest); bei Pool-Druck weichen nur Disposable-Units.

Implementierung: neues Modul `throway/retain.py` (Token-Config,
constant-time-Vergleich, `request_retention`/`write_denied`, HELP-Topic
`retention`); store.py bekommt minimale Touchpoints (Upload-Pfade, Write-
Gates, `_meta_expired`/`_meta_retained`/`_dir_retain`-Helper, Listings
null-sicher). `/api`: `retention_token`-Limit, `retain`-Endpunkt,
`expires_at: str|null`-Schema; `/help/retention`. Sicherheits-Invarianten
gehalten: `hmac.compare_digest`, 401 statt Enumeration, Query-Token als
dokumentierter (Log-Warnhinweis), Header bevorzugt.

### Bugfix mitgeritten: Idempotenz-Key war still tot (seit 1.42.x)

Beim Retention-Umbau fiel F821 auf: `hashlib` war nie importiert,
`_read_json` nie definiert, und `_idem_put` wurde **nie aufgerufen** —
die try/except-Blöcke fraßen die NameErrors, der Replay lieferte
stillschweigend immer neue IDs. Es gab keinen Test dafür. Gefixt:
Import + `json.load` + Persistenz-Hook in `_send` (erste 200-JSON-
Antwort unter dem Key); Regressionstest `test_idempotency_replay`.

Tests: `tests/test_retain.py` (19 neue, HTTP-Level + in-process
sweep/evict-Immunität) + `test_idempotency_replay`; Suite 115/115 grün.

---

## 1.43.2 — 2026-10-02

### Bugfix (P4): RECEIVED-Stern lässt sich abwählen

Randfall aus dem Review (pics-star-received-abwaehlen): Ein Empfänger,
der ein Foto selbst gestarrt hat UND das Foto im geteilten `RECEIVED`-Set
liegt, konnte den Stern nicht aus seiner Auswahl entfernen — er blieb
gefüllt (wegen `RECEIVED`), obwohl er aus dem localStorage entfernt war.
Rein visuell, kein falsches Teilen.

Behoben über ein **session-lokales `DESEL`-Set** (nie in localStorage):
- `paintStar` blendet `RECEIVED` für ein abgewähltes Bild aus:
  `on = (starmine()[pid] || RECEIVED[pid]) && !DESEL[pid]`.
- Klick auf einen Stern, der im `RECEIVED`-Set liegt, setzt `DESEL[pid]`
  → der Stern wird leer, obwohl das Bild im geteilten Set bleibt.
- Die Empfänger-Markierung wird weiterhin **nie** in localStorage
  geschrieben (P1/P3-Invariante) — `shareSet()` baut die Auswahl nur aus
  eigenen Sternen und nimmt ein abgewähltes Foto nicht in die URL auf.
- Gilt für Grid **und** Lightbox (`#lbsst` nutzt `paintStar`).

Browser-Flow (rodney/CDP) verifiziert: Empfänger mit eigenem Stern +
`RECEIVED` kann den Stern abwählen (Grid + Lightbox), localStorage wird
geleert, `shareSet()`-URL enthält das Foto nicht mehr; fremde
`RECEIVED`-Sterne werden nie übernommen.

Suite: 93 Tests grün, ruff grün.

---

## 1.43.1 — 2026-10-02

### Bugfix (Review P1): Empfänger-Markierung im ?stars=-Link

Review-Befund (pics-star-set-teilen): Der geteilte Link sortierte die
gestarrten Fotos zwar korrekt zuerst, aber ein Empfänger ohne eigene
Sterne sah sie **nicht markiert** — die Markierung hing nur am
localStorage des aktuellen Besuchers. Kernanforderung war „derselbe
View“. Behoben:

- **P1:** Der Server injiziert die URL-PIDs als `RECEIVED`-Set ins JS;
  `paintStar` markiert eine Zelle, wenn sie im eigenen `starmine()`
  **oder** im `RECEIVED`-Set liegt. Der Empfänger sieht die geteilten
  Fotos jetzt mit gefülltem Stern — auch ohne sie selbst gestarrt zu
  haben.
- **P1/P3:** Die Empfänger-Markierung wird **nie** in localStorage
  geschrieben — der „Als Set teilen“-Button baut seine Auswahl weiterhin
  nur aus den eigenen Sternen, übernimmt also keine fremden.
- **P2:** Toten `STARS`-Regex entfernt (war nie verwendet); das
  `RECEIVED`-Set kommt aus der bereits injizierten `__STARS__`.
- **CSS:** `.grid a[data-star]` als statische Markierung ergänzt
  (vorher nur `.starred`, das ebenfalls nur via localStorage gesetzt
  wurde).

Browser-Flow (rodney/CDP) verifiziert: frischer Empfänger sieht die
getrennten Fotos markiert (Grid + Lightbox), localStorage bleibt leer,
Share-URL enthält nur eigene Sterne.

Suite: 92 Tests grün, ruff grün.

---

## 1.43.0 — 2026-10-02

### Feature: Fotos starren & als Set teilen (pics-star-set-teilen)

Galerie-Gäste können Lieblingsfotos starren und sie als Set über Social
Media teilen — ohne Server-State. Star ist bewusst etwas anderes als
Like: Like ist öffentlich (gezählt, rankt die Galerie), Star ist eine
**private** Auswahl zum Teilen.

- **Star = rein clientseitig** (localStorage `ty_stars_<gid>`): kein
  Server-State, keine Rate-Limits, keine Cleanup-Pflicht, kein
  „wer darf das Set löschen“-Problem. Analog zu den bestehenden
  Browser-Likes `ty_likes_<gid>`.
- **Zustandsloser Link statt Set-Objekt:**
  `/pics/g/<gid>?stars=<pid1>,<pid2>,<pid3>` — starrer PIDs zuerst (in
  Link-Reihenfolge), dann der Rest in gewohnter Ordnung (`?sort=likes`
  rankt weiterhin den Rest). Unbekannte/versteckte/abgelaufene PIDs
  werden still übersprungen — der Link 404t nie.
- **Browser-UI:** Stern-Button an jedem Thumb + im Lightbox (toggelt
  localStorage), markierte Zellen mit gefülltem Stern, Hinweis über dem
  Grid („★ Sterne deine Lieblingsfotos und teile sie als Set.“) und ein
  „Als Set teilen“-Button mit Zähler (sichtbar ab ≥ 1 Stern) via
  `navigator.share`/Copy-Fallback. Sterne überleben Reloads.
- **Agent-JSON** spiegelt die Auswahl als `selected: [pid, …]`.

Browser-Flow (rodney/CDP) verifiziert: Stern-Klick → localStorage →
Zähler → Share-URL → Reload-Persistenz → Empfänger sieht starrer-zuerst
mit gefüllten Sternen; Lightbox-Stern synchronisiert mit dem Grid.

Suite: 91 Tests grün, ruff grün.

---

## 1.42.0 — 2026-10-02

### P1 (SOTA-Review): Strukturierte Fehler + Retry-After

Fehler waren Status + Prosa (`{"error":"not found"}`); ein Agent
musste raten, ob „gleich nochmal" (429) oder „falscher Aufruf" (400)
oder „Pool voll" (507) gilt — und wusste nie, wie lange er warten
soll.

- **`code`-Feld in jeder JSON-Fehlerantwort** (stabile Maschinen-Codes:
  `bad_request`, `write_denied`, `not_found`, `length_required`,
  `too_large`, `rate_limited`, `pool_full`, `server_error`). 11 text/plain-
  404-Antworten auf strukturiertes JSON umgestellt, damit *jede*
  Fehlerantwort maschinenlesbar ist.
- **`Retry-After`-Header + `retry_after`-Feld**: 429 → 60s (Rate-Fenster),
  507 pics-Pool → 300s. Ein Agent wartet jetzt, statt zu raten.

Suite: 87 Tests grün, ruff grün. Nächster Punkt: P2 (Idempotenz).

---

## 1.41.1 — 2026-10-01

### Koordinations-Guardrail: Bumppush gegen parallele Sessions absichern

Der Vorfall aus 1.40.3 war nicht die Ursache, sondern die Symptom-
huelle: Zwei Sessions im selben Working-Tree haben sich die Versions-
kette gegenseitig überschrieben, ohne es zu merken. Der Übeltäter war
fehlende Koordination — also mechanisch gelöst statt dokumentiert:

- **`scripts/bumppush-check.sh`:** Ein Bumppush ist nur erlaubt, wenn
  `<handoff>/issues/active/` leer ist (kein fremder Agent arbeitet) oder
  der eigene Claim-Slug im Commit-Betreff steht. Verweigert sonst mit
  klarer Meldung (inkl. Hinweis auf stale Claims).
- **In `release-check.sh` verdrahtet**, läuft also in jedem Release
  und in CI; ohne Handoff-Struktur (CI-Runner) wird er übersprungen.
- **Regel in `AGENTS.md`** beim Bumppush-Abschnitt: wer an throway
  arbeitet, legt einen Claim in `.handoff/issues/active/` an. Der
  Mechanismus ist das vorhandene issues-Kanban (`issues start <slug>`),
  keine neue Infrastruktur.

Suite: 86 Tests grün, ruff grün.

---

## 1.41.0 — 2026-10-01

### Retro-Nachzug: FD-Lecks geschlossen, Repro ohne Proxy, Browser-Fallen dokumentiert

Die restlichen Retro-Punkte (P5/P6) — diesmal mit **echtem Runtime-Fund**:

- **FD-Lecks im Server-Code (P6):** `json.load(open(...))` an 15 Stellen
  in `store.py`/`pics.py` ließ das Handle bis zum GC offen. Unter
  Thumb-Last (1.40.0 hat den Warmer eingeführt) konnte das
  Datei-Deskriptoren aufbrauen. Alle auf `with open(...)` umgestellt —
  **Semantik unverändert** (die umgebenden try/except bleiben, ein
  erster Versuch mit einem `_read_json`-Helper änderte das
  Exception-Verhalten und brach 31 Tests — zurückgenommen).
  Der Test-Harness schloss sein `server.log` ebenfalls nie (117
  ResourceWarnings → 0 aus dem Harness).
- **`THROWAWAY_PREFIX` (P6):** Der URL-Prefix der generierten Links ist
  jetzt Env. Lokal `THROWAWAY_PREFIX=` → alle Links zeigen auf denselben
  Origin, **Browser-Tests brauchen keinen Reverse-Proxy mehr** (vorher
 _proxy.py_ pro Session). Prod unverändert (`/throway`).
- **CODING_RULES (P5):** Abschnitt „Browser-Tests: Fallen" — harte
  Reloads, Marker im geladenen DOM **exakt** prüfen, Chromes
  `src`+`loading=lazy` schlägt `srcset`, Auto-Height-iframes haben
  keinen sichtbaren Viewport. Plus Verweis auf `scripts/browser-probe.py`.

Suite: 86 Tests grün (`-W error::ResourceWarning` ohne Fehlschlag),
ruff grün.

---

## 1.40.3 — 2026-10-01

### Retro-Umsetzung: Agent-Umgebung gehärtet (P1–P4, nachgezogen)

Versions-Hinweis: Diese Änderungen landeten zunächst als Commit mit
Betreff „1.39.3" — **Kollision mit einer parallelen Session** (die
zwischenzeitlich 1.39.3/1.40.0–1.40.2 gelandet hatte; der Thumb-Warmer
1.40.0 baut direkt auf dem srcset aus 1.39.2 auf). Nachgezogen als
1.40.3, damit Commit-Betreff und VERSION wieder zusammenpassen.

Inhalt (kein Runtime-Code):

- **`scripts/patch.py` (P1):** Anker prüfen → ersetzen → im Speicher
  kompilieren → erst dann atomisch schreiben (der 1.39.0-Patch hatte
  `pics.py` mit SyntaxError auf der Platte hinterlassen). CODING_RULES
  um die Reihenfolge ergänzt.
- **`scripts/browser-probe.py` (P2):** Ein CDP-Helfer statt fünf
  Wegwerf-Scripts pro Session — emulate/open/eval/marker/net gegen den
  rodney-Chrome, legt fehlende Page-Targets selbst an. Verifiziert:
  Emulation 390×844@3x, Frame-eval, Marker-Exitcodes, Thumb-Netzwerk-
  Mitschnitt.
- **AGENTS.md entwirrt (P3):** Bumppush-Absatz sagte zweimal
  Gegenteiliges (Minor- vs. Patch-Default), rsync-Zeile widersprach
  CODING_RULES. Jetzt eine Semantik-Policy + Verweis auf den
  CODING_RULES-Deploy-Befehl.
- **ruff in CI (P4):** eigener `lint`-Job (E9+F), Kalibrierung in
  `ruff.toml`; 9 Auto-Fixes + 2 tote Test-Locals bereinigt.

Suite: 86 Tests grün, ruff grün.

---

## 1.40.2 — 2026-10-01

### Accept-Backlog 128 + nginx Thumb-Cache

Zwei letzte Latenz-Hebel aus dem Galerie-Burst-Profiling (der H2-Burst
eines Browsers feuert Dutzende Thumb-Requests quasi gleichzeitig):

- **`request_queue_size = 128`** (war Pythons Default 5): Überschläge im
  Listen-Backlog kosteten betroffene Requests ~1 s SYN-Retransmit —
  sichtbar als die ~1,2-s-Stalls im Galerie-Burst (loopback reproduzier-
  bar). Accept-Verhalten selbst unverändert.
- **nginx `proxy_cache` für Thumbs** (Server-Konfig, nicht Repo-Code):
  Zone `throway_thumbs` (300 MB, inactive 24 h) in
  `conf.d/throway-cache.conf`, map cacht NUR
  `/throway/pics/i/<id>?…thumb=…` — volle Bilder, Uploads, API, HTML und
  Admin-Routen laufen BYPASS zur App (Moderation bleibt live).
  `X-Cache-Status` als Response-Header. Validity 1 h = Trade-off:
  gelöschte/versteckte Bilder können bis 1 h als Thumb nachlesbar sein.
  Vorher nachher (H2-parallel über WAN): Ø TTFB 165 ms = reine RTT,
  Stalls nur noch Netzwerk-Jitter.

---

## 1.40.1 — 2026-10-01

### Perf: Auto-Sweep gedrosselt — kein Full-Directory-Scan pro Request

Der eigentliche TTFB-Killer unter dem Warmer: `sweep()` lief bei JEDEM
Bild- und Galerie-Request (`_serve`, `all_pics`) — ein Full-Scan über
ROOT/pics (listdir + je Bild ein .meta-JSON-Read, aktuell ~800 Dateien
auf der USB-HDD), multipliziert mit parallelen Browser-Requests.
Gemessen: TTFB-Ausreißer bis 5,6 s bei 8 parallelen HEADs, obwohl alle
Thumbs auf Platte lagen.

- Auto-Sweep jetzt pro Prozess gedrosselt (`SWEEP_INTERVAL`, Default
  30 s, env `THROWAWAY_PICS_SWEEP_INTERVAL`): erster Request swept,
  weitere 30 s lang nicht. Bei 90 Tagen TTL ist das mehr als präzise.
- Aufrufe mit explizitem `now=` (Store-Periodikum, Tests) umgehen die
  Drossel — Verhalten für Ablauf/Expiry unverändert (Tests grün).
- Unit-Test: gedrosselter Sweep löscht nicht, expliziter schon.
- Gemessen nach Deploy: TTFB aller 306 Thumb-Kombos (102 Bilder × 3
  Breiten, 8 parallel) bei ~RTT, keine Ausreißer mehr.

---

## 1.40.0 — 2026-10-01

### Pics: Thumb-Warmer — alle srcset-Breiten vor dem ersten Request

Nach 1.39.3 (429-Fix) blieb der letzte Langsam-Faktor: die
Cold-Generation. Der erste Besucher zahlte ~0,3–0,5 s CPU (Pillow
decode/resize/encode) pro (Bild, Breite) — bei 102 Bildern füllten
sich die unteren Grid-Zellen sichtbar langsamer als die oberen.

- Neuer Daemon-Worker (`pics-thumb-warmer`, FIFO-Queue): erzeugt alle
  Whitelist-Breiten (160/320/640) im Hintergrund — nach jedem
  Upload/Import und beim Serverstart (`warm_existing()` nachrechnen
  für Bestände; idempotent, nur fehlende Paare, Neustarts danach
  No-Ops). `sleep(50 ms)` zwischen Generierungen, Pillow gibt die GIL
  her — Requests bleiben bedient.
- DI statt `import store` im Worker: store hat modul-level
  Side-Effects (ROOT-Bootstrap), ein zweiter Import wäre ein zweiter
  Store. `pics.set_thumb_maker()` wird von store.py main registriert,
  Tests injizieren ihre eigene Maker-Funktion.
- Tests: Warmer erzeugt nach Upload alle 3 Breiten ohne Request
  (echte WebPs, korrekte Kanten); Backfill reicht gezielt NUR die
  gelöschte Breite nach.
- Effekt (Gemessen am Soundspritzer-Bestand): Cold Ø 1,3 s/Thumb →
  warm Ø ~0,3 s/Thumb (Netz-RTT), Galerien laden komplett vorgecached.

---

## 1.39.3 — 2026-10-01

### Fix: Galerie-429s — Thumb-Exemption deckt jetzt alle srcset-Breiten

Der Soundspritzer-Galerie-Report („Laden der Thumbs extrem langsam,
~30 s“): 1.39.2 stellte die Grid-Thumbs auf `?thumb=160|320|640` um
(srcset) — die Rate-Limit-Exemption in `do_GET` prüfte aber weiter
literal `"thumb=1"`. `thumb=320`/`thumb=640` fielen dadurch unter den
Zähler (`100 req/min` pro IP): jede Galerie mit >100 Bildern 429te ab
Request #101 für den Rest des 60-s-Fensters — Chrome zeigte die
übrigen Thumbs erst nach Ablauf des Fensters.

- Exemption jetzt `"thumb=" not in _q` (alle Whitelist-Breiten;
  volle Bilder, Uploads und API zählen weiterhin).
- Test `test_thumbs_dont_consume_rate_limit` läuft jetzt über alle
  vier Breiten (1|160|320|640) — 40 Thumb-Requests bei Limit 5.
- Gemessen vorher/nachher (Chrome, 102 Bilder): vorher avg 2,2 s/Thumb
  inkl. 429-Wartefenstern, Requests #101+ garnicht → nachher: Cold
  (Erzeugung on-the-fly) avg 1,3 s/Thumb, warm (Disk-Cache) avg 0,29 s
  — Rest-Optimierungsidee: Thumbs beim Upload vorwärmen.

---

## 1.39.2 — 2026-10-01

### Pics: Responsive Thumbnails (srcset/sizes, `?thumb=N`)

Aus dem SOTA-Review: die Grid-Thumbs lieferten nur `?thumb=1`
(96px). Auf einem ~162px-Gridfeld — erst recht bei 3x-DPI (525px
Bedarf) — war jedes Thumb pixelig.

- `?thumb=N` (N ∈ 160|320|640) serviert genau diese longest edge als
  WebP, lazy erzeugt und pro Breite gecacht (`<file>.thumb320` …).
  Whitelist: unbekannte Werte fallen auf den Default zurück.
- Grid-Thumbs: `srcset` (160w/320w/640w) + kalibriertes `sizes`
  entsprechend der echten Spaltenzahl, `<noscript>`-Fallback.
- **`src` = 640w**, bewusst der größte Kandidat: bei `loading=lazy`
  nimmt Chrome den `src`-Fallback zuerst und wertet `srcset` erst bei
  Re-Layout neu — ohne `src` lädt Chrome gar nichts (gemessen). Der
  Schärfetest ist billiger als die Bytes: 640px-WebP ≈ <1 KB/Bild,
  24 Bilder ≈ 16 KB pro Galerie-Seite. Browser ohne den Chrome-lazy-Bug
  (Safari/Firefox) wählen aus dem srcset die passend kleinere Variante.

Gemessen (CDP-Netzwerk-Mitschnitt): Mobile 390@3x und Desktop
1440@1x laden die 640er-Variante scharf; Kandidatenbreiten exakt.

---

## 1.39.1 — 2026-10-01

### Pics-Lightbox: W3C-ARIA-Dialog-Muster (a11y)

Review gegen den W3C ARIA APG Dialog-Modal Pattern ergab sechs
Lücken, die jetzt geschlossen sind:

- **`aria-modal=true`** (Pflicht laut APG): Screenreader nehmen den
  Hintergrund hinter dem Overlay jetzt als inaktiv wahr.
- **Fokusfalle**: Tab/Shift-Tab bleiben im Dialog (zyklisch über die
  sichtbaren Buttons) — vorher konnte die Tastatur aus der Lightbox in
  die Galerie flüchten, die Modalität war nur optisch.
- **Fokus-Rückgabe**: beim Schließen (Close, Esc, Backdrop) landet der
  Fokus wieder auf dem Thumbnail, das den Dialog geöffnet hat
  (APG-Kernregel).
- **`alt` an `#lbimg`** = Bildname; ein leeres `alt` ist bei einem
  Foto-Viewer falsch (es markiert das Bild als dekorativ).
- **Caption als `aria-live=polite`**: „3 von 24" wird beim Blättern
  vorgelesen.
- **Thumb-Anchor mit Accessible Name** („<name> – groß öffnen"); das
  Thumb-Bild ist dekorativ (`alt=''`), der Link hatte sonst keinen
  Namen.

Details: `open()` setzt den Fokus auf `#lb` (tabindex=-1), Escape liegt
deshalb im `keydown` des Containers (der dokumentweite Handler
überspringt Ziele innerhalb von `lb`), der Fokusfalle zählen sichtbare
Buttons (das Like-Clear hat `display:inline-flex` und ist auch bei
`hidden` sichtbar — ein `:not([hidden])`-Filter wäre falsch gewesen).

Browser-Nachweis (rodney): Fokus im Dialog nach Öffnen, 5× Tab bleibt
drin, Escape schließt, Fokus zurück auf dem Thumbnail. Suite: 82 Tests.

---

## 1.39.0 — 2026-10-01

### Pics-Embed: Smartes Lazy-Load (sichtbarer Ausschnitt statt iframe-Höhe)

Der Infinite-Scroll im `?embed=1` lud bei einer auto-height-Galerie
alles auf einmal nach: das Nachlade-Limit war `innerHeight+500` — im
auto-Height-iframe ist `innerHeight` aber die GESAMTE Inhaltshöhe
(2717 px / 102 Bilder), der Sentinel lag also immer im "Fenster". Bei
102 Bildern luden alle 5 Seiten sofort.

SOTA-Lever, der in dieser Architektur zur Verfügung steht: der
1.38.4-Handshake kennt den sichtbaren Ausschnitt bereits
(`throway:pics:viewport` mit `height`+`offset`). Das Inf-Script hört
mit, pflegt `visTop`/`visH` und lädt erst, wenn der Sentinel dem
sichtbaren Fenster nahe kommt (900 px Vorlauf) — event-getrieben
(Viewport-Nachricht) mit Intervall als Sicherheitsnetz. Grace-Periode
von 1.5 s, damit der Handshake den Erst-Tick nicht verpasst; ohne
Host-Nachrichten bleibt das alte `innerHeight`-Verhalten.

Zusätzlich: `next` in der Lightbox an der Grenze der geladenen Liste
lädt die nächste Seite nach und springt automatisch weiter, statt zu
wrappen — Lazy-Load bricht das Durchblättern nicht ab.

Browser-Nachweis (rodney, 30 Bilder, 390-px-iframe): ohne Scrollen
24 Thumbs, Seite 2 lädt erst bei Tiefscroll; Lightbox "24 / 30" →
next → "25 / 30". Suite: 81 Tests grün.

---

## 1.38.4 — 2026-10-01

### Pics-Embed: Lightbox jenseits der ersten 24 Bilder + sichtbare Position

Feldbefund soundspritzer.at/share (102 Bilder): Klick auf einen
Thumb ab #25 navigierte das iframe zum rohen Bild (kein Click-Handler —
die Pro-Anker-Bindung kannte nur die ersten 24) und tötete damit
Grid, Lightbox und Auto-Height in einem Zug; die Caption zeigte
deshalb immer "N / 24"; und die Lightbox öffnete am iframe-Anfang —
bei tief gescrollter Host-Seite tausende Pixel über dem Sichtbereich.

- **Click-Delegation** auf `.grid a` mit Live-Index-Lookup: jeder
  nachgeladene Thumb öffnet die Lightbox, kein iframe-Navigieren mehr.
- **LB-Liste wächst mit**: jede Embed-Seite trägt ihre Items als
  `<script type=application/json id=lbdata>`; der Infinite-Scroll-
  fetch übergibt sie an `window.__tyLbAdd` → Caption "N / 102".
- **Position**: `throway:pics:viewport` um `offset` erweitert (Host-
  Scrollposition des iframes, Dedupe auf Höhe+Offset); `#lb.top`
  folgt dem sichtbaren Ausschnitt und rückt beim Scrollen nach —
  die Lightbox bleibt unter den Augen, nicht am iframe-Anfang.

Browser-Nachweis (rodney, 30-Bilder-Repro): Klick auf Thumb #30 →
Lightbox "30 / 30", iframe-URL unverändert, Bild korrekt skaliert,
Box bei Host-Scrollposition. Suite: 80 Tests grün.

---

## 1.38.3 — 2026-10-01

### Agent-Copy: Markdown-Publishing discoverable (Help-Topic `markdown`)

Session-Feedback (soundspritzer-Session): Ein Agent fand nicht heraus,
wie man ein lebendes Markdown-Dokument auf throway veröffentlicht, und
hielt ein hochgeladenes Kanban-HTML für ein Service-Feature. Ursache:
Das mdrender-Verhalten (seit 1.28: `.md` rendert als HTML-Seite für
Browser, roh für Agenten/`?raw=1`) war in KEINER der Help-Topics und
nicht in der Homepage-Agent-Beschreibung dokumentiert.

- **Neues Topic `/help/markdown`**: Rendering-Verhalten, One-off- vs.
  Living-Doc-Rezept (named dir + PUT/PATCH + history), Hinweis
  "kein CMS" und "Boards auf throway = hochgeladene HTML-Dateien".
- **overview**: Markdown als Zweck + "Not an app platform"-Guard.
- **view**: `.md` in der Inline-Render-Liste; **dirs**: Rendering-Hinweis
  bei `GET /d/<key>/<file>`; **files**: Pointer unter `?share=`.
- **Homepage-Agent-Summary**: `?name=doc.md`-Usage-Zeile.
- **Docs-Drift behoben**: API.md nannte Topic `named_dirs`, das nie
  existierte. release-check.sh prüft jetzt zusätzlich, dass jedes
  HELP_ORDER-Topic in API.md erwähnt ist (hätte den Drift gefangen).

Suite: 79 Tests grün; release-check inkl. neuem Topics-Drift-Guard.

---

## 1.38.2 — 2026-10-01

### Pics: Lightbox im Embed auf den sichtbaren Viewport fixiert

Die Lightbox in `?embed=1`-Galerien war auf Mobile unbenutzbar: Das
Auto-Height-iframe (`throway:pics:height` an den Host) macht den
iframe-Viewport so hoch wie den ganzen Inhalt (gemessen 2717 px,
live 8826 px). `position:fixed;inset:0` spannte die Lightbox über die
ganze Höhe, das Bild kollabierte auf 0×0 (`max-height:80vh` = 80 %
davon, aber `max-width:100%` deckelt), Vor/Zurück/Caption/Like lagen
~1300 px unterhalb des sichtbaren Screens — nur das Close-X war
erreichbar.

Zweistufiger Fix:

1. **Host-Handshake:** Das Embed-Snippet postet
   `{type:"throway:pics:viewport", height: window.innerHeight}` an das
   iframe — auf `resize`, `scroll` und nach dem Auto-Height-Update,
   dedupliziert. Das iframe meldet `throway:pics:ready`, sobald sein
   Listener lebt (verhindert das verlorene erste Post bei langsamem
   Laden). Lightbox-Höhe = exakt die sichtbare Viewport-Höhe.
2. **Self-contained Fallback** für alte, bereits kopierte Snippets:
   ohne Host-Nachricht kappingt die Lightbox auf
   `min(innerHeight, screen.height)` — nie wieder Tausende Pixel.
   Dazu `inset:0` → `top/left/right + height`, Bild `max-height:80vh`
   → `80%` (relativ zur Box), alle Controls hängen an der Box statt
   am iframe-Viewport.

Browser-Nachweis (rodney + CDP, 390×844 Mobile-Emulation gegen ein
2717-px-Auto-Height-iframe): Legacy-Snippet → `#lb` 844 px, Bild
300×200, alle Controls im Sichtbereich. Neues Snippet → `#lb` =
Host-`innerHeight` (Handshake), per postMessage exakt steuerbar
(500-px-Test). Rendered JS `node --check` grün; Suite: 78 Tests grün.

---

## 1.38.1 — 2026-09-28

### Embed: helle Scrollbar im Dunkel-Host versteckt

Das Embed passt seine Höhe per postMessage an — eine Scrollleiste ist
dort nie nötig, aber Chrome zeichnet sie trotzdem (weiß auf dunklem
Host, sah unpoliert aus). `scrollbar-width:none` + Webkit-Pendant im
Embed-CSS. Scrollen (No-JS-Fallback) geht weiter per Rad/Geste.

---

## 1.38.0 — 2026-09-28

### Likes & Kommentare für Event-Galerien (Gästebuch, ohne Login)

Bilder liken, die beliebtesten wandern hoch; Galerie-Kommentare mit
eigenen Likes. Alles serverseitig gezählt, ohne Konto — Pseudonym per
Fingerprint (IP+UA, keyed mit dem Gallery-Token, nie exponiert).

- **Likes (Bilder)**: `POST /pics/i/<id>?like=1` — Toggle pro Besucher,
  Antwort `{id, likes, liked}`. Zähler + Fingerprints im Sidecar
  `g/<gid>.likes.json` (neueste 500 fps, RAM-schonend).
- **Kommentare**: `POST /pics/g/<gid>?comment=1` (Form oder JSON:
  name ≤ 40 optional, text ≤ 500) — 20 s Cooldown/Besucher (nur RAM,
  es wird keine IP persistiert), max 500/Galerie (env-tunbar).
  Browser-Form → 303 zurück zu `#comments`.
- **Kommentar-Likes**: `POST /pics/g/<gid>?clike=<cid>` — gleiches Modell.
- **Ranking**: `GET /pics/g/<gid>?sort=likes` — Likes zuerst, kuratierte
  Ordnung bricht Gleichstand; `?embed=1` behält es bei. Browser:
  gelikte Bilder FLIP-climben live, 30-s-Poll hält Zähler frisch
  (`GET ?likes=1` — Zähler ohne Bild-Payloads).
- **UI**: Herz-Button auf jedem Thumb + im Lightbox (synchronisiert),
  Kommentarformular + Neueste-zuerst-Liste auf Galerie & Embed; No-JS =
  Form-POST + Links (progressive enhancement). XSS-fest escaped,
  user content nur via textContent im JS.
- **Moderation**: Admin-Seite listet Kommentare mit Löschen-Button
  (`action=cdel`); Superadmin inklusive.
- **Bugfix nebenbei**: `gallery_sweep`/`all_galleries` behandeln die
  neuen Sidecars (`*.likes.json`, `*.comments.json`) nicht mehr als
  Gallery-Metas (wären sonst als „abgelaufen" gelöscht worden).
- Docs-Sweep: /api (4 neue Endpunkte), help `pics`, API.md, README.
  Tests: 77 grün (11 neu: Toggle/Fingerprint, Sortierung, Cooldown,
  Caps/Trim, Escaping, Admin-Delete, UI-Marker) + Browser-Flow via
  rodney (Klick-Like mit FLIP, Lightbox, AJAX-Kommentar) verifiziert.


## 1.37.1 — 2026-09-27

### Bugfix: Google-Import holte jede Größenvariante + den User-Icon

Live-Analyse der Share-Seite: Sie enthält pro Foto **mehrere
Größenvarianten** (96px-Thumbs, OG-Cover, Vollversion) plus den Avatar
(`/ogw/default-user=s83`). Der Scraper nahm alle mit, `_lh3_hq` blies
alle auf 2048px auf → jedes Foto mehrfach + User-Icon als
„unnamed.jpg". (Die frühere „Byte-Varianz"-Deutung war falsch — es waren
Größenvarianten.)

- **Basis-Dedupe im Scraper**: eine Variante pro Foto-Basispfad — die
  größte (param-los = Vollversion). Avatare (`/ogw/`, `/a/`) und UI-
  Icons (≤128px) fliegen raus.
- Test erweitert (Varianten/Avatar/Icon gefiltert, größte gewählt) — 66
  grün. Live in einer frischen Galerie verifiziert: nur echte Fotos,
  kein unnamed.jpg.

---

## 1.37.0 — 2026-09-27

### Duplikat-Erkennung (Content-Hash, pro Galerie)

Event-Wänden und Re-Imports sei Dank: identische Bilder werden nicht
zweitgespeichert.

- **SHA-256 der gespeicherten Bytes** pro Bild in der Meta. Upload eines
  bereits vorhandenen Bildes (gleiche Galerie) liefert die **existierende
  ID zurück** mit `duplicate: true` — idempotente Upload-Semantik statt
  Fehler. Andere Galerien sind bewusst nicht dedupliziert.
- Multipart-Batches: `duplicates: N` im Response; Album-Import
  (Share-Link): `duplicates` gezählt, nicht neu geholt; Uploader-Status
  zeigt „N duplikate übersprungen" / Zeile „duplikat — übersprungen".
- 1 neuer Test (gleiche ID + Flag, Count bleibt, anderes Bild speichert,
  Batch-Duplikate) — 66 grün.

---

## 1.36.2 — 2026-09-27

### Bugfix: Embed-Infinite-Scroll feuert zuverlässig

IntersectionObserver meldete im Embed-Kontext `isIntersecting:false`
trotz sichtbarem Sentinel (y=412 im 800px-Viewport, live reproduziert) —
das Nachladen startete nie. Ersetzt durch schlichtes Intervall-Polling
(400ms, räumt sich nach letzter Seite selbst ab, Initial-Check nach
150ms). Lokal bewiesen: 24 initial → alle 38 geladen, Sentinel entfernt;
Host-Seite: iframe wächst auf exakte Inhaltshöhe (739px für 38 Bilder).

---

## 1.36.1 — 2026-09-27

### Bugfix: Auto-Höhe meldet echte Inhaltshöhe

`documentElement.scrollHeight` ist viewport-geklemmt — das Embed konnte
nie kleiner werden als die Starthöhe des iframes (642px statt ~450px
im Live-Test). Jetzt: `body.scrollHeight` mit 280px-Mindesthöhe.
Live gegen die Host-Seite verifiziert: iframe schrumpft auf
Inhaltshöhe, wächst beim Nachladen mit.

---

## 1.36.0 — 2026-09-27

### Smartes Embed: Infinite-Scroll + Auto-Höhe

- **Infinite Scroll im Embed**: IntersectionObserver am Sentinel lädt die
  nächste Embed-Seite per fetch und hängt den Grid an (initial 24 Bilder,
  rootMargin 500px — lädt VOR dem Erreichen des Randes). Paging-Links
  bleiben als No-JS-Fallback.
- **Auto-Höhe per postMessage**: Das Embed meldet seine Inhaltshöhe an
  die Host-Seite (`throway:pics:height`, ResizeObserver + nach jedem
  Nachladen). Das neue Embed-Snippet enthält die 2-Zeilen-Listener-
  Zeile — iframe wächst mit, keine doppelten Scrollbars, kein leerer
  Raum. Wer nur die iframe-Zeile nimmt, bekommt das Scroll-Verhalten
  wie zuvor.
- Snippet auf der Galerie-Seite aktualisiert (beide Zeilen, Klick
  selektiert alles).
- 1 Test erweitert (Sentinel/Observer/postMessage im Markup) — 65 grün;
  embed-JS per node --check, rodney-Live-Beweis mit Host-Seite folgt.

---

## 1.35.2 — 2026-09-27

### Bugfix: Erfolgsmeldung bei Album-Import (soundspritzer-Fall)

Der Galerie-Paste prüfte `x.d.id` — Album-Antworten haben aber
`{imported: N}` statt `id`: der Import lief erfolgreich durch, die Seite
meldete trotzdem „fehlgeschlagen: unbekannt" und lud nicht neu. Jetzt:
„importiert ✓ 38 bilder" + Auto-Reload. Live auf soundspritzer
verifiziert (38 Bilder drin, Meldung korrekt).

---

## 1.35.1 — 2026-09-27

### Bugfix: Ctrl+V auf der Galerie-Seite (Illegal invocation)

Der Paste-Handler rief `clipboardData.getData` abgekoppelt auf
(`(e.clipboardData.getData || …)(…)' — ohne Empfänger) → Chrome wirft
stillschweigend „Illegal invocation", der Import startete nie. Mein
übercleveres Defensivkonstrukt; jetzt schlicht gebunden. Mit echtem
Paste-Event gegen die soundspritzer-Galerie live verifiziert.

---

## 1.35.0 — 2026-09-27

### Google-Photos-Share-Links einfügen (Stufe 1.5)

„Diesen Link will ich reinkopieren" — jetzt geht das:

- **`?url=` versteht Share-Links** (`photos.app.goo.gl/…`): Kommt statt
  eines Bildes eine HTML-Seite zurück, wird sie nach den eingebetteten
  `lh3.googleusercontent.com`-Bild-URLs durchsucht (Server-gerendert,
  kein JS nötig), die Größenbegrenzung wird auf unser 2048-px-Budget
  angehoben (Auth-Tail bleibt) und **das ganze Album** wird sequenziell
  importiert — Pipeline wie immer (Pixel-Regel, GPS-Strip, Pool).
  Antwort: `{imported, failed, images, errors}`. Cap 100 Bilder/Link,
  Videos (andere Hosts) bleiben automatisch draußen.
- **Paste überall**: Link in die Galerie-Seite oder den Homepage-
  Gallery-Tab einfügen (⌘V) → Import läuft mit Statuszeile; ohne
  bestehende Galerie wird erst eine angelegt.
- 1 neuer Pure-Function-Test (Scrape+HQ-Rewrite) — 65 grün. Share-Link-
  Import live gegen einen echten Link verifiziert (siehe Release-Doku).
- Bekanntes Risiko (im Issue notiert): Google kann das Share-Seiten-
  Format ändern — der Scraper ist bewusst schmal (nur lh3-Muster).

---

## 1.34.0 — 2026-09-27

### Stufe 1 des Google-Photos-Plans: URL-Import in Galerien

- **`POST /pics/g/<gid>?url=<bild-url>`**: Server holt das Remote-Bild
  durch die normale Pics-Pipeline (Pixel-Regel, GPS-Strip, 30-MB-Cap,
  Pool-507). SSRF-Guard wie beim throway-Import (nur öffentliche Hosts,
  Redirect-geprüft), nur `image/*`. `_fetch_remote` ist jetzt größen-
  parametrisierbar; `THROWAWAY_ALLOW_PRIVATE_FETCH=1` als Test/Dev-
  Flucht (Prod-Default: an).
- **URL-Drops in beiden Dropzonen** (Galerie-Seite + Homepage-Gallery-
  Tab): Bild aus einem anderen Tab (z.B. Google Photos) auf die Dropzone
  ziehen → Server-Import mit Statuszeile. Homepage-Tab legt bei Bedarf
  erst die leere Galerie an.
- Stufe 0 (Copy-Paste aus Google Photos) funktioniert seit den Paste-
  Uploads ohnehin. Stufe 2 (Picker-API) bleibt im Issue-Backlog.
- 2 neue Tests (Import-Flow inkl. Nicht-Bild-400, SSRF-Default-Block) —
  64 grün. /api + /help/pics + AGENTS.md dokumentiert.

---

## 1.33.0 — 2026-09-27

### Galerien einbettbar (embed)

- **`?embed=1`** auf `GET /pics/g/<gid>`: minimale, chrome-lose Ansicht
  für `<iframe>` — nur Grid, kompakte Pagination und Lightbox; kein
  Header, kein Uploader, **transparenter Hintergrund** (Host-Seite
  scheint durch). Agents bekommen wie immer JSON (embed ist reines
  Browser-Rendering).
- **Copy-Paste-Snippet** auf jeder Galerie-Seite: „diese Galerie
  einbetten" klappt ein `<details>` auf mit fertiger iframe-Zeile
  (readonly-Input, Klick selektiert alles) — null zusätzliches JS.
- `/api` + `/help/pics` dokumentieren den Parameter. 1 neuer Test
  (Embed-Ansicht enthält/fehlt-X, Snippet, Agent-JSON) — 62 grün.

---

## 1.32.1 — 2026-09-27

### Bugfix: Gallery-Create aus dem Browser (gefunden durch echten Browser-Test)

Der `POST /pics?create=1` antwortete nur für Agent-UAs mit JSON — der
Browser-JS-Flow (Create-Button UND Upload-Create-on-first-file im
Homepage-Gallery-Tab) bekam HTML, `r.json()` scheiterte still. Meine
bisherigen Validierungen liefen per curl (= Agent-UA) und maskierten
das. Jetzt Content-Negotiation: JSON bei `Accept: application/json`
oder Agent-UA; beide JS-Fetches senden den Header. Rodney-Browser-Test
bestätigt: Button-Flow, Admin-Link-Anzeige, Space-Kuration vollständig
durchgeklickt. (Und der Checklisten-Schritt „VERSION bumpen" rächt
sich, wenn man ihn überspringt — nachgeholt.) 61 Tests grün.

---

## 1.32.0 — 2026-09-27

### Galerie-UX: leere Galerie anlegen + Admin-Tastatur-Kuration

- **„Create gallery"-Button** im Homepage-Gallery-Tab: Galerie ohne
  einziges Bild anlegen (Event morgen vorbereiten, Bilder später).
  Create-or-get wie immer — existiert der Name, erscheint der
  öffentliche Link mit Hinweis (Admin-Token nur beim Anlegen). Später
  hochgeladene Bilder landen automatisch in der angelegten Galerie.
- **Space-Kuration im Admin-Lightbox**: Admin blättert mit < > durch
  die Galerie und toggelt mit **Space** das aktuelle Bild zwischen
  sichtbar (+) und verborgen (−) — ohne den Viewer zu verlassen. Die
  Karte dahinter dimmt live, die Caption zeigt den Zustand
  („[+ visible] / [− hidden]"), Pfeil-Scroll wird unterbunden.
- Admin-Lightbox-JS nur auf Admin-Seiten ausgeliefert (public bleibt
  schlank); beide per node --check validiert. 61 Tests grün.

---

## 1.31.0 — 2026-09-27

### Homepage-UX-Pass (Vision-Review, improve-ux-Skill)

Screenshot-gestützter Review des `#text`-Zustands (User-Feedback:
„textfeld wirkt old style") — 5 Fixes, 3 Befunde ins Ledger verschoben:

- **Textarea war ungestylt** (Wurzel: `#createBox`-Selektor verwaist seit
  dem 1.22er-Tab-Umbau → Browser-Default). Jetzt: volle Panel-Breite,
  min-height 220px, Mono-Typo .95rem/1.6, 3px-Focus-Ring — statt
  180×110px-Stub in 790px Panel.
- **Feature-Grid**: 4. Karte (Pics) stand als Waisen-Karte in Reihe 2
  (minmax 220px) → `minmax(180px,1fr)`, vier Karten in einer Reihe.
- **Stats**: „1 FILES"-Plural-Bug → dynamisch; Pool-Meter unter 2%
  Belegung unsichtbar (0,06% real) → Mindestfüllung 2% + Title-Tooltip
  „pool: X of Y".
- **Agent-Info** (40 Zeilen Raw-Dump) begrub die Homepage → jetzt
  default zugeklappt.
- **„once"-Checkbox** kryptisch → „download once" + Tooltip; Share-Hint
  rutschte gequetscht neben das Input → eigene Zeile.

Verzögert (Ledger F6–F8): Casing-System, PUBLIC_BASE-Dokumentation,
Char-Counter im Text-Tab. Messtechnisch verifiziert via Headless-Chrome
(Textarea 761×225px, 4 Karten/Reihe, agents collapsed). 61 Tests grün.

---

## 1.30.0 — 2026-09-27

### pics ↔ throway-Integrationsreview: drei Nähte gefestigt

Systematischer Konsistenz-Check der pics-Integration. Befunde + Fixes:

- **Thumbs zählen nicht mehr aufs Rate-Limit** (der kritische Fund): Eine
  Galerieseite feuert 60 `?thumb=1`-Requests — zwei volle Seiten pro
  Minute liefen ab Request #101 in 429 (bewiesen im Lokaltest). Thumbs
  sind gecachte Mikro-WebPs und bleiben jetzt außerhalb des Zählers;
  Uploads, Vollbilder und API zählen weiter.
- **Cache-Control für Unveränderliches**: Thumbs `public, max-age=3600`,
  pics-Bilder `public, max-age=86400` (IDs sind bis zum Ablauf
  immutable) — Event-Gäste hinter NAT profitieren kollektiv. Throway-
  Einzeldateien bleiben bewusst ungecacht (PUT-editierbar). Nebenan
  bereinigt: doppelter Cache-Header im Thumb-Pfad, Header-Nach-`end_
  headers()`-Bug in `_serve_file`.
- **pics zählen in die Stats**: Galerie-Uploads bumpen jetzt
  stats.json + Since-Start-Zähler — die Homepage zeigt Galerie-Aktivität
  statt „0 files". Stats sind kosmetisch: Fehler dort killen nie einen
  Upload.
- Docs: AGENTS.md-Kompressionszeile auf die 1.26er-Pixel-Regel
  korrigiert (stand noch auf „recompress alles").
- 2 neue Tests (Thumb-Limit-Exemption + Cache-Header/Stats) — 61 grün.

Unverändert korrupt-frei bestätigt: Pool-Trennung (4 Stellen),
RESERVED_NAMES, Homepage-/Browse-Listing überspringen pics, Routing.

---

## 1.29.0 — 2026-09-27

### Opt-in Write-Token für DIRs (Issue: throway-dir-write-token)

Lesen bleibt offen, Schreiben auf Wunsch geschützt — rückwärtskompatibel:

- **Anlegen**: `POST /?dir=1&name=<name>&write=1` → Server generiert
  Token (48 hex); alternativ `&write=<eigenes>` (8–64 Zeichen
  [A-Za-z0-9._-]). **Token steht genau einmal im Create-Response**
  (`write_token` + Hinweis); create-or-get auf existierenden DIR
  verrät ihn nie.
- **Schreiben geschützt**: `POST /d/<key>`, `PUT/PATCH /d/<key>/<file>`,
  `DELETE /d/<key>[/<file>]` verlangen den Token — Header
  `X-Throway-Write` oder `?write=<token>`, constant-time verglichen.
  Ohne/falsch → 401. Auch das `?share=<name>`-Loch ist geschlossen.
- **Lesen offen**: Files, Listing, History, Zip bleiben ohne Token
  erreichbar. `write_protected: true` im Listing zeigt den Zustand an.
- **Bugfix nebenbei**: `do_PUT`/`do_PATCH`/`do_DELETE` strippen jetzt
  Query-Strings aus dem Pfad — `?write=…` (und jede andere Query) auf
  diesen Routen führte vorher zu 404. Nur GET konnte das je.
- 3 neue Tests (Token-Flow komplett, Backward-Compat + Leak-Guard,
  Custom-Token) — 59 grün. API-Spec + Help aktualisiert. Issue in
  Review.

---

## 1.28.0 — 2026-09-27

### DIR-Root serviert index.html inline (Issue: throway-dir-index-landing)

Parität mit Bundles: Enthält ein DIR eine `index.html`, bekommen Browser
an `GET /d/<key>` diese inline ausgeliefert statt der Listing-Seite —
der geteilte DIR-Link ist damit die Landing-Page (Kontext:
firmenindex-Agent teilt Report-DIRs mit gerendertem Report als
`index.html`).

- `<base>`-Injection wie bei Bundles, damit relative Links
  (`style.css`, `data.json`, …) gegen `/d/<key>/` auflösen.
- Kleiner Footer-Link „files & history" → `?listing=1` erzwingt die
  klassische Listing-Seite.
- Agents: unverändert JSON-Listing. DIRs ohne `index.html`: unverändert.
  `?zip=1`/`?download=1`: unverändert.
- 1 neuer Test (Browser-Root, ?listing=1, Agent-JSON, ohne-index) — 57
  grün. Issue in Review.

---

## 1.27.0 — 2026-09-27

### Markdown im Browser rendern (Issue: throway-md-render-browser)

`.md`-Dateien liefern Browsern jetzt **gerendertes, self-contained HTML**
statt Markdown-Quelltext (Kontext: firmenindex-Agent teilt Report-DIRs,
`report.md` ist das Primary-Artefakt):

- **Eigener Renderer** `throway/mdrender.py` — stdlib-only, kein JS, kein
  externes CSS: Überschriften (Titel aus `# `-Zeile), Absätze, Listen
  (verschachtelt, ordered/unordered), GFM-Tabellen, Code-Fences,
  Blockquotes, HR, Links (+bare Autolinks), **bold/italic/code**.
- **Sicherheit zuerst**: alles wird HTML-escaped, Link-Schemata auf
  http/https/mailto/relativ/#anchor beschränkt (kein `javascript:`),
  Code-Spans schützen Inline-Formatierung.
- **Agents bleiben raw** (UA-basiert wie immer); `?raw=1` ist der
  explizite Escape — auch für Browser. `?download=1` unangetastet.
- Wirkt auf **alle drei Serve-Pfade**: Einzeldatei `/<id>`, Bundle-Datei
  `/<fid>/<file>`, Dir-Datei `/d/<key>/<file>`. Footer-Link „raw:
  markdown" auf jeder gerenderten Seite.
- 10 Unit-Tests + Integrationstests über alle Pfade — 56 grün. Issue in
  Review (throway-md-render-browser).

---

## 1.26.0 — 2026-09-27

### Nur komprimieren, wenn die Pixel zu gross sind

Johanns Regel, jetzt wörtlich umgesetzt:

- **≤ 2048 px und web-freundliches Format (JPEG/PNG/WebP)** → Bild wird
  **byte-identisch** gespeichert. Kein Re-Encode, null Qualitätsverlust,
  null Generationen-Drift. Transparenz natürlich intakt.
- **JPEG-Metadaten lossless entfernt**: Auch im Keep-Original-Pfad werden
  EXIF (inkl. GPS!), XMP, Photoshop/IPTC und Kommentare aus dem Container
  geschnitten — reine Byte-Chirurgie an APP-Segmenten, **Pixel bleiben
  100% identisch** (verifiziert per Hash). JFIF + ICC-Farbprofil bleiben.
- **> 2048 px** → downscale auf 2048 px + WebP q90 mit Step-down bis
  ≤ 1 MB (wie 1.25.0; Floor q65, alpha-erhaltend).
- GIFs pass through weiter unangetastet (Animation), HEIC/AVIF müssen
  transcodieren (Browser können sie nicht anzeigen) — dabei fällt deren
  Metadaten ohnehin weg.
- Server-Pipeline und Browser-Pre-Resize (Homepage-Tab) sind jetzt
  konsistent: kleine Bilder gehen unberührt durch.
- 2 neue Tests (byte-identisch JPEG+PNG, lossless EXIF-Strip), 45 grün.
  RFQ FR-5 aktualisiert.

---

## 1.25.0 — 2026-09-27

### Komprimierung: HQ first, size second (1-MB-Cap)

Auf Johanns Rückmeldung: Fotos sollen hochwertig bleiben — Limit ~1 MB.

- **q90 statt q80** als Start-Qualität; nur wenn das Ergebnis **> 1 MB**
  ist, sinkt die Qualität in 5er-Schritten (90 → 85 → … → Floor 65) bis
  es passt. **Ergebnisse unter 1 MB bleiben auf q90** — keine unnötige
  Verschlechterung mehr bei bereits kleinen Fotos.
- **Alpha bleibt erhalten**: transparente PNGs (Logos, Grafiken) werden
  nicht mehr auf Weiß aufgefüllt, sondern als RGBA-WebP gespeichert.
- **EXIF/GPS wird konsequent entfernt** (vorher konnte der
  „keep-original"-Pfad bei winzigen Dateien EXIF inkl. GPS durchreichen —
  jetzt strippt jeder Pfad, Privacy by Default für eine öffentliche
  Galerie).
- Client-seitig (Homepage-Gallery-Tab) spiegelt der Browser-Shrink das
  Verfahren: 2048 px, q0.90, Step-down bis ≤ 1 MB (Floor 0.70) — spart
  Upload-Bandbreite bei gleichem Ergebnis.
- Env-tunbar: `THROWAWAY_PICS_QUALITY` (90), `THROWAWAY_PICS_QUALITY_FLOOR`
  (65), `THROWAWAY_PICS_TARGET_BYTES` (1 MB), `THROWAWAY_PICS_EDGE_PX`
  (2048).
- 3 neue Tests (Size-Cap, Alpha, EXIF-Stripping) — 44 grün. RFQ FR-5
  aktualisiert.

---

## 1.24.0 — 2026-09-27

### Schöneres Upload-Feld (Galerie + Homepage-Gallery-Tab)

Das rohe File-Input („No file chosen") ist weg — beide Upload-Felder sind
jetzt Dropzones im Stil der Hauptseite (aktuelle Upload-UX-Patterns):

- **Icon + zweizeilige Beschriftung**: „Bilder hierher ziehen oder
  klicken" + Constraints **vor der Auswahl** (Formate JPG/PNG/WebP/GIF/
  HEIC, max 30 MB, 1000+ auf einmal).
- **Drag-over-Feedback**: Rahmen + Hintergrund wechseln, sobald eine Datei
  über dem Feld schwebt (drop-zones-müssen-antworten-Regel).
- **Status statt „No file chosen"**: „3 ausgewählt — Upload startet…",
  dann Live-Zähler (X hochgeladen · Y ausstehend).
- **Drag & Drop jetzt auch auf der Galerie-Seite** (vorher nur Klick).
- Screenreader-freundlich: Input sr-only im Label-Wrapper
  (Klick überall im Feld öffnet den Picker, `:focus-within`-Outline).
- Homepage-Gallery-Tab: gleiche Optik (Icon, Sub-Line mit Formaten/Limit).

Reiner Frontend-Polish, API unverändert. Gerendertes JS per node --check
validiert, 41 Tests grün.

---

## 1.23.0 — 2026-09-27

### Galerie: Lightbox — Bilder weiterklicken (‹ › / Pfeiltasten / Swipe)

Klick auf ein Thumbnail öffnet jetzt die **Bildansicht** statt eines
nackten Bild-Loads:

- **‹ › Buttons** überblättern, **← → Pfeiltasten** ebenso,
  **Touch-Swipe** mobil; **Esc** oder Klick neben das Bild schließt.
- Caption mit Position + Dateiname („2 / 6 — capri.jpeg“).
- Nachbarbilder werden **vorgeladen** (flüssiges Blättern).
- Body-Scroll lockt während offen; ohne JS bleiben Thumbs normale Links
  (progressive enhancement).
- Auch im **Admin** — dort mit secret-scoped URLs, inkl. verborgener
  Bilder (nur im Admin sichtbar, public bleibt 404).
- Lightbox deckt pro Seite die 60 gerenderten Thumbs ab (Paginierung
  blättert weiter). Gerendertes JS per node --check validiert, 41 Tests
  grün.

---

## 1.22.1 — 2026-09-27

### Bugfix: `?name=` wird jetzt percent-dekodiert

Dateinamen wie `uniinfer-tu%40x.png` oder `PXL%20foto.jpg` landeten mit
`%40`/`%20` im Metadaten-Namen. `name` (throway-Upload, pics-Upload und
Galerie-Anzeigename) wird jetzt URL-dekodiert — Standard-Query-Verhalten.
1 Test neu, 40 grün. Reiner Bugfix → Patch-Bump.

---

## 1.22.0 — 2026-09-27

### Upload-Seite: ein Multi-Type-Uploader (Segmented Control)

Die Startseite hat jetzt **einen Uploader mit vier Typen** als Segmented
Control — nach aktueller Upload-UX-Praxis (per-file Status, Constraints
vor der Auswahl, Paste-Routing, stabiles Layout, ARIA-Tabs):

- **Files** (default): Drag & Drop / Klick / Paste wie gehabt — jetzt mit
  Constraint-Zeile („max 5 MB · lives 4h …") unter dem Panel.
- **Text** (ersetzt den „+"-Button): Textarea + filename/once/ttl/share
  direkt im Panel.
- **Link** (neu im UI): URL einfügen → server-seitiger Import
  (`POST /?url=`, API existierte schon, war nur UI-los).
- **Gallery**: Galerie-Name + listed + Drop/Paste von Bildern →
  **client-seitiges Downscaling auf ≤ 2048px WebP** vor dem Upload
  (10–20× weniger Bandbreite bei Handyfotos, EXIF-Rotation via
  createImageBitmap erhalten, GIFs unangetastet) → create-or-get der
  Galerie → sequenzielle Uploads mit **Per-File-Status** (queued /
  resizing / uploading / done ✓ / failed + Retry-Button pro Datei,
  429-Warteschleife). Nach dem Anlegen erscheinen Galerie-URL +
  **Admin-Link** mit Copy-Buttons (Einmal-Hinweis).

Details: Tab-Wechsel instant mit stabilem Rahmen, Panel-Zustände bleiben
beim Wechsel erhalten, Roving-Focus + Pfeiltasten (ARIA-Tab-Pattern),
aktiver Tab in der URL (Hash, Restore beim Laden), Paste-Routing
(Bilder → Gallery-Tab wenn aktiv, sonst Dropzone; Pastes in Formular-
felder werden nie gekapert). Mobile: Tabs scrollbar, Inputs volle Breite.
JS-Syntax per node --check geprüft; 39 Tests grün.

---

## 1.21.0 — 2026-09-27

### pics auf der throway-Startseite

Galerie-Anlegen ist jetzt Teil der Default-Site (`skale.dev/throway/`),
nicht mehr nur unter `/pics`:

- **Feature-Karte** in der Feats-Übersicht (Files / Bundles / Dirs / Pics)
  mit Link zum Galerie-Index.
- **Anlege-Formular** direkt unter den Upload-Controls: Name + „listed“-
  Checkbox + Button → `POST /pics?create=1` (gleicher Flow wie auf der
  pics-Seite, inkl. Einmal-Anzeige des Admin-Links).
- Nur UI-Integration, keine API-/Verhaltensänderung. 1 neuer Test
  (Homepage enthält Formular, Flow end-to-end) — 39 Tests grün.

---

## 1.20.0 — 2026-09-27

### pics: Multi-Galerie-Modell — jede:r kann Galerien anlegen (dirs-artig)

Aus *einer* Admin-Galerie (1.19.0) wird ein Verzeichnis beliebiger
Galerien — konzeptionell wie die throway-dirs: create-or-get-Namen,
unlistet per Default, `listed=1` für den öffentlichen Index, sliding
lifetime.

- **Anlegen**: `POST /pics?create=1[&name=<name>][&listed=1]` → JSON mit
  `id`, öffentlicher URL, **`admin_url` + `token` (genau einmal
  angezeigt)**. Dir-style Namen (`[a-z0-9-]`, 5–32 Zeichen, ≥1 Buchstabe,
  nicht reserviert) werden zum Schlüssel: `/pics/g/hochzeit-2026`
  (create-or-get, idempotent). Andere Namen sind Anzeigenamen auf frischer
  Hex-ID.
- **Token-Sicherheit**: Re-Create einer existierenden benannten Galerie
  liefert diese **ohne Token** zurück (`existed:true`) — Name erraten ist
  keine Admin-Übernahme.
- **Upload**: `POST /pics/g/<gid>?name=` (roh oder multipart-batch),
  sofort öffentlich, schiebt das Galerie-Ablaufdatum nach vorn.
- **Admin**: `GET/POST /pics/g/<gid>/<secret>` — hide/unhide/delete/up/down,
  `/json`-Listing inkl. hidden, Aktionen strikt auf Bilder der eigenen
  Galerie beschränkt (Cross-Galerie-Zugriff = No-op bzw. 404).
- **Superadmin**: der Env-Token (`THROWAWAY_PICS_ADMIN_TOKEN`) kuratiert
  zusätzlich **jede** Galerie (Betreiber-Pflicht bei Verstößen); ohne Env-Token gibt es keinen Superadmin, eigene Tokens funktionieren weiter.
- **Ablauf**: Bilder fest 90 Tage; Galerie lebt sliding 90 Tage ab letztem
  Upload, danach räumt der Sweep Galerie + Bilder gemeinsam ab.
- **Pool**: weiterhin ein gemeinsamer 20-GB-Pool über alle Galerien
  (voll → 507 Reject, nie Eviction).
- **Breaking** ggü. 1.19.0 (Feature war Stunden alt, 0 Bilder in Prod):
  `POST /pics` lädt nicht mehr in eine globale Galerie hoch, sondern
  braucht `?create=1` bzw. `POST /pics/g/<gid>`; alter Global-Admin-Route
  `/pics/<secret>` entfällt (Superadmin jetzt pro Galerie).
- Tests: 23 pics-Verhaltenstests neu/umgestellt (Gesamt 38 grün),
  `/api` + `/help/pics` + Docs aktualisiert.

---

## 1.19.0 — 2026-09-27

### pics — Event-Galerie (Upload, öffentliche Ansicht, Admin-Kuration)

Neues Feature-Set unter `/pics` (RFQ.md): eine kuratierte Bildgalerie für
Events — freier Upload für Besucher, sofort öffentlich, ein Admin mit
langlebiger Geheim-URL kuratiert (verbergen / löschen / neu sortieren).

- **Upload**: `POST /pics?name=bild.jpg` (roh) oder multipart (Batch).
  Recompression server-seitig: max 2048px, WebP q80, EXIF-Rotation beachtet,
  Originale verworfen. GIFs pass through (Animation bleibt). HEIC/AVIF via
  `pillow-heif`. Max 30 MB pro Bild.
- **Eigener Pool**: 20 GB in `ROOT/pics/` — voll = Uploads werden abgelehnt
  (507), niemals LRU-Eviction. Vollständig getrennt vom 100-MB-Werfen-Pool:
  `total_size()`, `_units()` und `evict()` sehen pics-Einheiten nie.
- **Lifetime**: fest 90 Tage, abgewickelt über `sweep()`.
- **Öffentliche Galerie**: HTML-Grid mit WebP-Thumbs (lazy), Paginierung,
  JS-Upload-Queue mit Retry + 429-Handling; JSON-Listing für Agenten.
- **Admin**: Geheim-Token als Pfad-Segment (`/pics/<secret>`, Env
  `THROWAWAY_PICS_ADMIN_TOKEN`, constant-time-Vergleich). Verbergen → 404
  für alle außer Admin; Löschen → Bytes+Meta+Thumb weg; Reihenfolge
  (up/down) persistent per `order`-Feld. Pool-Auslastungsanzeige.
- **Self-Service**: `/api` um pics_upload/pics_gallery/pics_image/pics_admin
  ergänzt; neues Help-Topic `/help/pics`; pics-Link in der Homepage-Nav.
- **Modularisierung (Schritt 1+2)**: pytest-Suite (34 Verhaltenstests,
  subprocess-basiert) als Regressionsnetz; neuer Code liegt als Modul
  `throway/pics.py` mit kleiner Schnittstelle (`get/post/sweep/api/HELP`)
  statt im Monolith. store.py nur an 8 Punkten minimal berührt.
- **Deploy**: Paket via `rsync` statt einzelner Datei (Ritual in AGENTS.md
  aktualisiert); neu: `pip install pillow-heif` auf lubu, Env
  `THROWAWAY_PICS_ADMIN_TOKEN` in der systemd-Unit setzen.

---

## 1.18.4 — 2026-09-21

### Download-once (Burn-after-Reading) als Option
- **„once“-Checkbox** in der Web-UI — für Upload und Create. Aktiviert
  löscht sich die Datei nach dem **ersten Download** selbst (zweiter GET →
  404).
- **Backend**: `&once=1` für Einzeldateien (roh-body + multipart). Setzt
  `once:true` im Meta; beim GET wird die Datei vor dem Senden entfernt
  (race-sicher, zweiter Zugriff bekommt 404). Nicht kombinierbar mit
  `&share=` (Dirs).
- Docs: `/api`-Spez, `/help/files`, `/help/limits`, `_home_help`, `API.md`,
  `AGENTS.md`, `README.md` um `&once=` ergänzt.

## 1.18.3 — 2026-09-21

### Share-Name wählbar (Upload & Create)
- **Share-Name-Feld** in der Web-UI — für Upload und Create. Optional einen
  einprägsamen Namen (z.B. `my-note`) wählen; der Inhalt landet dann unter
  `/d/<name>` (create-or-get, wie ein benannter Dir) statt unter einer
  zufälligen Hex-ID.
- **Backend**: `&share=<name>` für Einzeldatei-Uploads (roh-body und
  multipart). Nutzt die Dir-Mechanik: Sliding-Lifetime (default 7d, `&ttl=`
  clamped [4h,14d]), validiert wie benannte Dirs (5-32 Zeichen `[a-z0-9-]`,
  >=1 Buchstabe, nicht reserviert).
- Docs: `/api`-Spez, `/help/files`, `/help/limits`, `_home_help`, `API.md`,
  `AGENTS.md`, `README.md` um `&share=` ergänzt.

## 1.18.2 — 2026-09-21

### Create-Text hinter „+" neben Upload (statt Tabs)
- Die Tabs „Upload | Create" sind wieder raus — der bisherige Upload-Bereich
  bleibt unverändert. Stattdessen sitzt **neben dem Upload-Button ein kleines
  „+"**, das die Create-Text-Box ein-/ausklappt (Textarea + Name + Lebensdauer
  + Create-Button). Erneutes Klick auf „+" klappt sie wieder zu.

## 1.18.1 — 2026-09-21

### Create-Text + einstellbare Lebensdauer in der Web-UI
- **„Create“-Tab neben „Upload“**: In der Web-UI gibt es jetzt zwei Tabs —
  **Upload** (bisherige Dropzone) und **Create** (neuer). Im Create-Tab lässt
  sich Text direkt eintragen/pasten und per Klick als Datei hochladen
  (optional mit Name, z.B. `note.txt`). Der erzeugte Link ist sofort
  teilbar.
- **Lebensdauer setzbar**: Standard 4h, aber per Dropdown auf bis zu **14
  Tage (max)** verlängerbar — getrennt für Upload und Create. Der Wert wird
  als `&ttl=<h|d>` mitgeschickt.
- **Backend**: `&ttl=<h|d>` funktioniert jetzt auch für **Einzeldatei-**
  Uploads (roh-body und multipart), nicht nur für Dirs. Clamped auf
  `[4h, 14d]` (max 14 Tage), Default 4h. `expires_in`/`persistence`/`X-Expires`
  spiegeln die gewählte Lebensdauer.
- Docs: `/api`-Spez, `/help/files`, `/help/limits`, `_home_help`,
  `API.md` um `ttl=` für Einzeldateien ergänzt.

## 1.18.0 — 2026-09-19

### Agent-Hints überall (Konsistenz-Durchlauf)
- **Hint-Block auf allen Browser-Seiten** (bisher nur Dir-Listing):
  Bundle-Listing, `/d`-Browse, `/browse`-Files, Help-Index, Help-Topic und
  Dir-History bekommen denselben einklappbaren `<details class=agenthint>`-
  Block mit lauffähigen, absoluten curl-Zeilen. Neu als `_agent_hint()`-
  Helper (escaped selbst), CSS wanderte in `_BASE_CSS` (keine Duplikate).
- **`Link: <…/api>; rel="help"` Header** (RFC 8288) auf **allen JSON-
  Responses** (Listings, Upload-Result, Errors — via `_send`) und auf
  Datei-Downloads für Agent-UA (`_serve_file`): jede maschinenlesbare
  Antwort verrät nun, wo der Vertrag liegt.
- AGENTS.md: Selbst-Discovery-Absatz beschreibt die Hint-Blöcke jetzt
  seitenübergreifend inkl. Link-Header.

## 1.17.0 — 2026-09-19

- **Agent-Hint auf der Dir-HTML-Seite**: einklappbarer `<details>`-Block
  unter der Dateiliste mit lauffähigen curl-Zeilen (JSON-Listing via
  `-A curl`, Einzeldatei, `?zip=1`, PUT/PATCH-Edit, `/history`). Absolute
  URLs — jede Zeile überall copy-paste-bar. Für Menschen dezent
  eingeklappt, im Quelltext/Accessibility-Tree immer voll lesbar (kein
  Cloaking: auch für Menschen nützlich). Schließt die Lücke für Agenten,
  die mit Browser-UA auf der HTML-Seite landen; `-A curl`-Agenten bekommen
  weiterhin direkt das JSON-Listing.

## 1.16.0 — 2026-09-19

### Smartphone-UX (Mobile-First-Nachschlag für Browser-Seiten)
- **Viewport + theme-color überall**: alle HTML-Seiten (Dir-Listing, Bundle-Listing,
  Browse, Help, Help-Topic, History, Copy-for-Agents) hatten teils keinen
  Viewport-Meta → auf Smartphones zoome-out-Desktop-Layout. Jetzt durchgängig
  `<meta name=viewport>` + `theme-color` (einheitliche `_META_MOBILE`-Konstante).
- **Shared Base-CSS (`_BASE_CSS`)** für alle Sekundärseiten: Touch-Ziele ≥ 44px
  (Zeilen, Buttons, Back-Links), Dateinamen mit Overflow-Wrap/Ellipsis statt
  Überlauf, responsive Padding via `@media(max-width:560px)`.
- **Homepage mobil**: Upload-Button + Dir-Checkbox stapeln sich untereinander
  (voll breit, 46px), URL-Eingabefeld in der Result-Box mit 16px Font (verhindert
  iOS Auto-Zoom), kompaktere Dropzone, `touch-action:manipulation` auf Buttons.
- **Native Share-Sheet**: nach Upload erscheint auf Smartphones ein
  „⇗ share link"-Button (`navigator.share`) — Link direkt per WhatsApp/Mail teilen.

### Bild-Vorschauen (Thumbnails)
- **`?thumb=1`** auf Datei-URLs (Einzeldatei, Bundle-Datei, Dir-Datei): liefert eine
  kleine WebP-Vorschau (96px, quality 70, konfigurierbar via
  `THROWAWAY_THUMB_PX` / `THROWAWAY_THUMB_QUALITY`).
- **Hardware-schonend**: Thumbnail entsteht lazy beim ersten Request (Pillow,
  EXIF-Rotation beachtet — Handyfotos stehen richtig herum), wird als
  `<file>.thumb` next to the original gecacht (einmal CPU-Kosten, dann nur
  Lesezugriff), atomarer Replace gegen Races; Fallback auf Original-Bytes bei
  SVG/Nicht-Bild/Fehler. `loading=lazy` + `decoding=async`: Phones laden
  Thumbnails erst beim Scrollen und dann KBs statt MBs (1MB-JPEG → ~72B WebP).
- **Listings mit Vorschau**: Dir-, Bundle- und Browse-Listing zeigen für Bilder
  44px-Vorschaukästchen; menschenlesbare Größen (`1.0 MB` statt Bytes).
- **Aufräumen**: `.thumb`-Dateien zählen nie als Dateien in Listings/JSON/Zips,
  werden zusammen mit dem Original gelöscht (`_remove`, Sweep), Uploads mit
  reservierten Suffixen (`.meta/.history/.thumb/.thumbtmp`) neutralisiert
  (`_safe_name` hängt `_` an), direkter Zugriff auf Bookkeeping-Dateien → 404.

### Fixed
- **Zip sauber**: Dir-/Bundle-Zips enthielten bisher `<key>.history` (und hätten
  künftig auch `.thumb`-Caches mitgeliefert) — Zips enthalten jetzt nur die
  echten Nutzerdateien.
- **500er auf Legacy-Dir-Pfad behoben**: `GET /<dir-id>` (Bundle-Namespace, alte
  Dirs) crashte für Browser mit TypeError (`_dir_listing` bekam falsche Argumente).

### API-Notes
- `/api`: `download`, `download_bundle_file` und `get_dir_file` dokumentieren
  jetzt `?thumb=1`. Agents/JSON unverändert (keine Thumb-Einträge).

---

## 1.15.0 — 2026-09-17

- Repo-Versöhnung: die am Live-Dir entwickelten Features (Bundles/Zip/Minisites,
  Tags+browse, Agent-Endpoints, UA-Negotiation) zurück im main-Branch gemergt —
  Live-Stand == Git wieder.
- SEO: HTML-Head für Browser/Googlebot (Title, Description, Canonical, OpenGraph
  + og-image); Agenten erhalten weiter text/plain (UA-Negotiation).

## 1.14.0 — 2026-08-25

### Added
- **Tags on single files** (wie bisher schon bei Dirs): bei Upload und
  URL-Import per `&tag=<t>` (wiederholbar, max 5, `[a-z0-9-]`, 1–24
  Zeichen); stehen danach in der Upload-Response und in der File-Meta.
- **Tag-Update ohne Rewrite**: `POST /<id>?tag=a&tag=b&untag=c` ändert nur
  die Tags — Content und Expiry bleiben unangetastet.
- **`GET /browse`** — Filtern & Sortieren über alle lebenden Einzeldateien:
  `?tag=<t>[&tag=<t2>]` (UND-Filter), `&q=<substr>` (Name/Tag),
  `&sort=created|name|size|expires`, `&order=asc|desc`. JSON für Agents,
  HTML-Seite für Browser.
- Dokumentiert in `/api` (neue Endpoints `browse_files`, `tag_file`;
  Upload-Response mit `tags`) und `/help/files`.

---

## 1.13.0 — 2026-08-25

### Added
- **Import from URL**: `POST /?url=<encoded-url>[&name=<filename>]` — the
  server fetches the remote http(s) document itself and stores it like a
  normal upload (name from Content-Disposition / URL path, overridable via
  `&name=`; content type from response header, fallback via extension).
  Guard rails: max 5 MB, max 3 redirects (re-validated per hop), private/
  loopback/link-local/reserved hosts blocked (SSRF), 10s timeout.
- **URL as document format (`&link=1`)**: `POST
  /?url=<url>&link=1[&name=<name>]` stores the URL itself as a tiny,
  editable HTML redirect page — browsers are redirected via meta refresh,
  agents can `PUT`/`PATCH` it like any text file.
- Both documented in `/api` (new `import_url` endpoint) and `/help/files`.

---

## 1.12.3 — 2026-08-25

### Added
- **Build config**: alle wichtigen Betriebsparameter sind jetzt zentrale
  Konfigurationsvariablen am Dateianfang von `store.py` und per
  `THROWAWAY_*`-Env-Vars überschreibbar (z.B. im systemd-Unit):
  `THROWAWAY_ROOT`, `_POOL_BYTES`, `_MAX_FILE_BYTES`, `_RATE_LIMIT`,
  `_TTL_HOURS`, `_DIR_MIN_AGE`, `_DIR_MAX_AGE` (das 14d-Max),
  `_DIR_DEFAULT_AGE`, `_DIR_ABS_MAX`, `_HISTORY_LIMIT`, `_MAX_TAGS`.
  Defaults unverändert.
- **`/api`**: neues Feld `dir_ttl_seconds: {min, default, max}` — Agents
  können das 14d-Max nun direkt als Daten lesen statt aus Prosa zu raten;
  `create_dir.note` führt das MAX 14 days vorne an.

---

## 1.12.2 — 2026-08-25

### Changed
- **Docs: 14d dir-TTL-Maximum explizit gemacht** — AGENTS.md, API.md und die
  eingebetteten Agent-Texte (`/api`, `/help/limits`, write-for-agents)
  sagen jetzt ausdrücklich "**MAX 14 days**", damit Agents das Limit nicht
  nur aus der Clamp-Angabe erraten müssen.

---

## 1.12.1 — 2026-08-25

### Changed
- **AGENTS.md: Bumppush-Regel geschärft** — per Default ein **Patch-Release**
  (`x.y.z` → `x.y.z+1`); Minor für Features, Major für Breaking Changes.

---

## 1.12.0 — 2026-08-25

### Changed
- **Dirs: `ttl=` max raised from 7d to 14d.** `&ttl=` at dir creation is now
  clamped to `[4h, 14d]` (was `[4h, 7d]`). Default (no `ttl=`) stays 7 days;
  sliding behavior and the 30-day absolute cap are unchanged. Updated in the
  embedded help, `/api` spec, homepage copy, AGENTS.md, API.md, PRD.md,
  README.md.

## 1.11.0 — 2026-08-23

### Added
- **Paste-to-upload**: press **Ctrl+V** (or Cmd+V) anywhere on the homepage
  and any image on the clipboard is queued in the dropzone for upload — no
  need to save it to disk first. A page-wide `paste` listener grabs `file`
  items from the clipboard, wraps them in a named `File` (deriving a
  sensible name + extension from the MIME type, e.g. `pasted-…png`), and
  calls `dz.addFile()`. Text pastes are left untouched. Multiple pasted
  images queue together and upload in the same POST as any dropped files.

---

### Fixed
- **Homepage upload was completely broken** (click AND drop did nothing):
  the inline script had a JS syntax error (a ternary branch's `)` closed
  the outer paren, leaving `:d.bundle?` dangling), so the browser never
  ran any of it. Found via `node --check` on the extracted script.

### Changed
- **Homepage upload UI now uses [Dropzone.js](https://dropzone.dev)
  (5.9.3) via CDN.** Battle-tested click-to-browse + drag-and-drop with
  file previews, progress and per-file remove — replacing the hand-rolled
  dropzone. `maxFilesize` is injected from the server's `MAX_FILE`, so the
  client limit always matches the real one.
- **Homepage JS modularized**: small named functions (`esc`, `row`,
  `filesBlock`, `showResult`, …) instead of one nested string-concat
  expression; upload URL derives from `location.pathname`, so the page
  works under any mount point (root or `/throway` behind the proxy).
- Upload still sends **one multipart POST for the whole queue** → file,
  bundle, or dir (`?dir=1`) exactly as before. Result box unchanged.

### Note
- The homepage now loads Dropzone.js from jsDelivr; if the CDN is
  unreachable the upload card won't function (agents/curl unaffected).

---

## 1.9.4 — 2026-08-20

### Fixed
- **Homepage upload dropzone: drag-and-drop uploads now work.** The drop
  handler assigned the read-only `input.files = ev.dataTransfer.files` file
  list directly, which silently becomes an empty `FileList` in some
  browsers, so dropping a file onto the card showed an empty list and the
  upload reported "Choose at least one file". The handler now copies the
  dropped files into a fresh `DataTransfer` and assigns `input.files =
  dt.files`, the standard cross-browser technique (Chrome + Firefox).
  The file-picker path and direct API uploads already worked.
- **Version is now single-sourced from `store.py` `VERSION`.** The
  "Current version" line served on the releases page is derived from the
  code constant at request time, so it can never drift from the running
  build.

---

## 1.9.3 — 2026-08-20

### Fixed
- **Dir files with spaces in their names now fetch correctly.**
  `GET /d/<key>/<file>` (and `PUT`/`PATCH`/`DELETE`) now URL-decode the
  filename segment before matching, so a `%20` in the request matches the
  literal space in the stored name. Previously these returned `404` even
  when the filename was correctly percent-encoded; only space-free names
  worked. Same fix applied to per-file fetches from bundles.
- **Dir/bundle listings now return URL-encoded `files[].url` values**
  (e.g. `%20` instead of a raw space), so the URLs the server hands back
  actually work when fetched by programmatic consumers.

---

## 1.9.1 — 2026-08-18

### Changed
- **Homepage stats: two cards** — each shows files + size. Card 1 is the
  current live state ("now"); card 2 is activity since this server started
  ("since start"), a RAM-only counter reset on each restart. Replaces the
  old four cards (files now / stored / files ever / uploaded ever).

---

## 1.9.0 — 2026-08-18

### Changed
- **Unified the two dir types into one concept.** Mutable dirs (`/<dirid>`,
  sliding 4h/24h) and named dirs (`n/<name>`) are now a single **dir** under
  `/d/<key>`, addressable by an opaque id or a memorable name. One storage
  layout, one TTL model (fixed lifetime, default 7 days, `ttl=` clamped to
  [4h, 7d]), no duplicated code.
- All dir endpoints moved under `/d/`: `POST /d/<key>` (add files),
  `GET /d/<key>` (listing), `GET /d/<key>/<file>`, `PUT`/`PATCH`
  `/d/<key>/<file>` (edit/append), `DELETE /d/<key>[/<file>]`,
  `GET /d` (list `listed=1` dirs).
- **Edit history per dir.** `GET /d/<key>/history` returns the last
  `HISTORY_LIMIT` (50) entries, newest first — date, file, action
  (`add`|`put`|`append`|`delete`) and byte deltas. Lightweight by design:
  no full-text versions, no revert. JSON for agents, HTML for browsers.
- Removed the old `n/` namespace and the short-lived mutable dirs; existing
  real dirs were migrated from `n/<name>` to `d/<name>`.
- Responses carry `editable` (per file) and a `persistence` block (`type`,
  `expires_at`, `extendable_by`, `max_age`) so agents can discover
  editability and lifetime from the response instead of guessing.

---

### Changed
- **Canonical URL is now `https://skale.dev/throway`.** The front door moved
  to amd2 (`158.180.42.218`), which proxies `/throway/` to lubu and
  terminates TLS. `skale.dev` and `www.skale.dev` both point there; the root
  `/` redirects to `/throway/`.
- `PUBLIC_BASE` default is `https://skale.dev/throway`; all generated URLs
  (upload responses, dir/named-dir listings) use it. The old
  `https://lubu.skale.dev/throway` still works via the env override on lubu.
- Docs (AGENTS/API/README/PRD) updated to the new canonical URL.

---

## 1.6.1 — 2026-08-12

### Changed
- **Homepage now returns a structured `--help` summary to agents.** Curling
  the root (`GET /throway/` as a non-browser client) returns a compact usage
  overview plus pointers to where to get more — the full guide
  (`/write_for_agents`), the API index (`/api`), and per-topic help
  (`/help`, `/help/<topic>`). ~1 KB instead of the full ~5 KB blob.

---

## 1.6.0 — 2026-08-12

### Added
- **Modular, API-gatherable help** (`/help`). Instead of one giant
  copy-paste blob, help is split into topics served individually at
  `/help/<topic>` (`overview`, `files`, `bundles`, `dirs`, `named_dirs`,
  `view`, `edit`, `delete`, `limits`, `contract`).
  - `GET /help` → JSON index of topics (for agents) or an HTML list (browsers).
  - `GET /help/<topic>` → that topic as plain text (agents) or a rendered
    page (browsers). Unknown topics → 404.
  - Agents fetch only the pieces they need instead of one big blob.
- **Single source of truth**: the `/write_for_agents` description and the
  `/help/*` topics are assembled from the same `HELP` dict — no duplication.

---

## 1.5.0 — 2026-08-12

### Added
- **Named dirs** (`/n/<name>`). A mutable dir addressed by a memorable name
  instead of an opaque hex id, so a team of agents can remember and reuse one
  shared dir.
  - **Create-or-get**: `POST /?dir=1&name=<name>` is idempotent — any agent
    calling the same create converges on the shared dir.
  - **Naming ruling**: 5-32 chars, `[a-z0-9-]`, must contain a letter, no
    reserved words (`api`, `index`, `n`, `releases`, `llms`, `store`, …).
  - **Create flags** (immutable at create): `&listed=1` (appears in `GET /n`),
    `&tag=<t>` (up to 5 discoverability tags), `&ttl=<h|d>` (fixed lifetime).
  - **Fixed lifetime**: `expires_at` set at creation (default 7 days, `ttl=`
    overrides, clamped to [4h, 7d]) and **never moves** — add/edit/delete do
    not extend it.
  - **`updated_at`** tracks last add/edit/delete (does not affect lifetime).
  - **Full CRUD** on `n/<name>`: add files, fetch, zip, PUT/PATCH text edit,
    delete file or whole dir.
  - **Listing `GET /n`** (only `listed=1` dirs): filter by `?q=` (name/tag
    substring), `?created_after/before`, `?updated_after/before`; sort by
    `?sort=created|updated|name&order=asc|desc`. JSON for agents, HTML for
    browsers.
  - **Privacy**: unlisted by default; names never enumerated outside `GET /n`.

---

## 1.4.2 — 2026-08-12

### Fixed
- **Dir/bundle listing 404s in a browser.** The HTML listing pages for dirs
  and bundles (served at `/throway/<id>` with no trailing slash) now inject a
  `<base href="/throway/<id>/">` tag, so relative links (`a.txt`, `b.txt`)
  resolve against the dir/bundle directory instead of the parent path.
  Previously clicking any item in a dir or bundle listing 404'd.

### Changed
- `PUBLIC_BASE` is now overridable via the `THROWAWAY_PUBLIC_BASE` env var
  (default `https://skale.dev/throway`), making it easy to switch the public
  URL without editing code.

---

## 1.4.1 — 2026-08-12

### Changed
- Polished the web UI across all pages: a clean white design with drag &
  drop upload, one-click URL copy, a "create a dir" toggle, feature cards,
  and a stats grid on the landing page. Releases and bundle/dir listing
  pages match the same minimal white look.

---

## 1.4.0 — 2026-08-12

### Added
- **Mutable dirs.** Create a dir (`POST /?dir=1`), keep adding files to it
  (`POST /<dirid>`), and it's deleted **4h after the latest upload** (capped
  at 24h total). `GET /<dirid>` returns a JSON listing to agents / an HTML
  page to browsers; `?zip=1` downloads the whole dir; files are fetched and
  deleted individually.

### Fixed
- **Bundle sub-resource 404s in a browser.** A bundle's `index.html` is now
  served with an injected `<base href="/throway/<id>/">` tag, so relative
  URLs (`style.css`, `app.js`, images) resolve against the bundle directory
  instead of the parent path. Previously every multi-file bundle rendered
  unstyled/broken in a browser.

---

## 1.3.0 — 2026-08-12

### Fixed
- **Per-IP rate limiting.** The rate limiter now keys on the real client IP
  (via `X-Forwarded-For` / `X-Real-IP` set by nginx) instead of the socket
  peer, which behind nginx was always `127.0.0.1`. Previously the whole
  service shared one 100 req/min bucket — a single busy agent could exhaust
  it for everyone. Each real IP now gets its own bucket.
- **Memory-safe bundle downloads.** The bundle zip is streamed to a temp file
  and out in chunks instead of being built entirely in RAM. A bundle near the
  pool limit no longer allocates ~100 MB of memory per request.
- **Total bundle size cap.** A bundle can no longer exceed the 100 MB pool —
  returns `413` during upload instead of briefly blowing past the limit.

## 1.2.0 — 2026-08-12

### Fixed
- **Bundle MIME sniffing.** Bundle parts now get their content type from the
  file extension (like single-file uploads), so `.css`, `.js`, `.svg`, etc.
  serve correct MIME types instead of `application/octet-stream`. Browsers no
  longer block stylesheets/scripts in styled bundles.
- **HEAD requests.** `HEAD /throway/<id>` (and other routes) now return `200`
  with correct headers and `Content-Length` and no body, instead of `501`.

## 1.1.0 — 2026-08-12

### Added
- **Bundles.** Upload 2+ files in one multipart `POST` to create a bundle under
  a single URL. Browsers get `index.html` rendered inline (a real throwaway
  website); agents get a zip; each file is served at `/throway/<id>/<file>`.
  The whole bundle shares one 4-hour expiry and is evicted as one unit.
- **Inline rendering for text-like types.** Images plus text, HTML, JSON, PDF,
  SVG, and JavaScript now render inline in the browser; `?download=1` forces a
  download. Text responses include `charset=utf-8`.

### Fixed
- **Base URL.** Dropped the dead `:8001` port; the service is now reached at
  `https://lubu.skale.dev/throway`.

## 1.0.0 — 2026-08-10

### Added
- Initial release. A disposable, open, short-lived file store.
- Upload a single file (raw body or multipart) → 4-hour URL.
- Images render inline; other files download; `?download=1` forces download.
- Text files editable via `PUT` (replace) and `PATCH` (append); images immutable.
- Rolling 100 MB pool (oldest evicted first), 5 MB max file, 100 req/min per IP.
- Machine-readable contract at `/api` and agent descriptions at
  `/write_for_agents` and `/copy_for_agents`.

raw: markdown