Commit Graph
3 Commits
Author SHA1 Message Date
StefanandClaude Opus 5 adedaa9a0d Hell/Dunkel-Umschaltung und Versionsangabe in der Fusszeile
Farbschema folgt zunächst der Systemeinstellung, lässt sich per Knopf in
der Kopfzeile umstellen und wird dann gemerkt. Bei gesperrtem Speicher
(privates Fenster) greift wieder die Systemeinstellung.

theme.js steht bewusst OHNE defer und vor dem Inhalt: es setzt die
Farbklasse, bevor gezeichnet wird. Mit defer blitzte beim Laden kurz die
helle Fassung auf. Eine eigene Datei statt eines <script>-Blocks, weil die
CSP nur script-src 'self' erlaubt - inline wäre blockiert.

Fusszeile zeigt Version oder Commit, gleiches Muster wie die Kantone-App:
die CI reicht beides als Build-Argument durch, lokal erscheint der
Build-Zeitpunkt. Der ändert sich bei jedem Neubau und zeigt damit sofort,
ob wirklich der neue Stand läuft.

Beim Ergänzen der Dunkel-Varianten hatte mein Ersetzer die eben
eingefügten dark:-Klassen selbst nochmal getroffen und daraus Paare wie
"dark:text-slate-300 dark:text-slate-600" gemacht - bereinigt.

81 Tests, alle grün. Neu darunter: die Fusszeile zeigt Tag bzw. Commit,
theme.js steht vor <body> und ohne defer, der Umschalter ist da.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GR4bNaj9GtRu57J4Niii8o
2026-08-29 23:35:35 +02:00
StefanandClaude Opus 5 75b95b38e6 Phase 4: Oberfläche mit Galerie, Erfassung und Reservierung
Jinja2 mit HTMX, Tailwind über die Standalone-Binärdatei im Docker-Build -
kein Node.js, kein npm. Mobile-First, Tippflächen mindestens 44 px hoch.

Seiten: Galerie mit Filterleiste, Detailseite, Erfassungsformular,
Stapel-Import mit Entwurfsliste, Anmeldung. HTMX tauscht beim Filtern nur
die Liste aus statt die ganze Seite neu zu laden.

Aus den Entscheidungen im Plan folgt konkret:

- Der Stapel-Import steht ZUERST im Formular: wer zwanzig Teile hat, will
  nicht zwanzigmal ein Formular ausfüllen. Bewusst ohne capture-Attribut,
  damit sich wahlweise fotografieren oder aus der Mediathek wählen lässt -
  mit capture ginge nur je ein Foto pro Vorgang.
- Grösse, Kategorie, Geschlecht, Saison und Zustand werden vom zuletzt
  gespeicherten Teil übernommen. Beim Abarbeiten einer Kiste sind das fünf
  Felder weniger pro Stück.
- Nach dem Nachtragen eines Entwurfs geht es direkt zum nächsten, nicht auf
  die Detailseite - das ist der eigentliche Arbeitsablauf.
- Titel bleibt leer erlaubt; angezeigt wird dann Kategorie und Grösse.
- reserved_by erscheint öffentlich nur als "reserviert". Den Namen sieht
  ausschliesslich der angemeldete Betreiber.
- Nach dem Reservieren wird der Freigabe-Link angezeigt, als vollständige
  Adresse zum Aufbewahren.
- Post/Redirect/Get überall, damit ein Neuladen nicht erneut abschickt.
- CSRF-Token in jedem Formular, zusätzlich zu SameSite=Lax.

HTMX liegt lokal im Repository, nicht vom CDN: die CSP erlaubt nur
script-src 'self', und ein CDN-Aufruf gäbe die IP jedes Besuchers weiter -
dieselbe Überlegung wie bei Swagger und bei den Schriften der Kantone-App.
Ein Test prüft für alle Seiten, dass nichts von aussen geladen wird.

Nebenbei TemplateResponse auf die aktuelle Signatur umgestellt (64
Deprecation-Warnungen weniger).

77 Tests, alle grün. Zusätzlich der ganze Weg gegen den Container:
anmelden, drei Fotos als Stapel, Entwurf nachtragen, Werte werden für das
nächste Teil übernommen, als Gast reservieren, Name bleibt verborgen,
Selbst-Freigabe über den Link.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GR4bNaj9GtRu57J4Niii8o
2026-08-29 23:29:56 +02:00
StefanandClaude Opus 5 99ba74330c Phase 3: Anmeldung, Rate-Limits und Sicherheits-Header
Lesen darf jeder, schreiben nur der Betreiber - mit der bewussten Ausnahme
des Reservierens. Dazu Dockerfile und Compose-Datei, damit sich das lokal
ausprobieren lässt.

Beim Bauen sind zwei Fehler aufgefallen, die ohne Test nicht aufgefallen
wären:

1. slowapi zählt pro URL-Pfad. Weil jedes Kleidungsstück eine eigene URL
   hat, bekam jedes seinen eigenen Zähler - ein Skript hätte also den
   gesamten Bestand reservieren können, ohne je an ein Limit zu stossen.
   Genau der Missbrauch, gegen den das Limit gedacht ist. Behoben mit
   shared_limit und festem scope.
2. Der erste Anlauf des Tests machte fünf Anfragen gegen ein Limit von
   fünf und konnte damit gar nichts zeigen. Geprüft wird jetzt der
   tatsächlich ausgelieferte Standardwert, mit mehr Anfragen als erlaubt.

Weiter umgesetzt:

- Passwort als bcrypt-Hash aus der Umgebung, einmal beim Start gebildet
  und gemerkt. Bei jeder Anfrage neu gehasht liesse sich die Anwendung
  sonst ohne Anmeldung lahmlegen - bcrypt ist absichtlich langsam.
- Ohne hinterlegtes Passwort bleibt der Erfassungsbereich gesperrt (503)
  statt offen zu stehen. Kein mitgeliefertes Standardpasswort.
- Sitzung als signiertes Cookie, HttpOnly, SameSite=Lax (blockt
  seitenfremde POSTs), Secure abschaltbar nur fürs lokale Testen,
  Abmeldung nach zwei Stunden Ruhe.
- CSP mit script-src 'self', nosniff, frame-ancestors none, dazu noindex
  und robots.txt: die Galerie ist frei zugänglich, soll aber nicht
  dauerhaft im Suchindex stehen.
- Der Betreiber darf Reservierungen ohne Token aufheben, damit sich eine
  Missbrauchswelle aufräumen lässt.
- uvicorn mit --proxy-headers: hinter einem Reverse-Proxy zählte sonst
  alles auf dessen IP, und ein einzelner Besucher sperrte alle aus.
- Container läuft nicht als root; Code gehört root, nur Daten und Bilder
  dem Dienstbenutzer.

Umgebungsvariablen heissen jetzt ausdrücklich englisch (ADMIN_PASSWORD,
SECRET_KEY, ...), passend zur Anleitung und zur Kantone-App.

59 Tests, alle grün. Zusätzlich gegen den laufenden Container geprüft.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GR4bNaj9GtRu57J4Niii8o
2026-08-29 23:00:23 +02:00