{# … #}-Django-Kommentare sind IMMER ein Bug — Django entfernt nur einzeilige. Im <script> brechen sie das gesamte JS (v0.15.7: Vue-App crashte, Import-Button postete ins Leere), im HTML erscheinen sie als sichtbarer Text. Regel: immer {% comment %} verwenden.{{ }} braucht {% verbatim %} — und Django-Kommentare innerhalb von {% verbatim %} erscheinen wörtlich.max-h-[85vh]) können im lokal gebauten CSS fehlen → kritische Layout-Werte inline als style setzen. Ebenso: parallele hidden flex-Klassen, deren Gewinner von der CSS-Reihenfolge abhängt → Inline-Display für Modals.100vh ist am Handy höher als das Bild — mobile Browser rechnen 100vh mit eingeklappter Adressleiste. Solange die Leiste sichtbar ist, liegt der untere Rand einer Fläche mit 100vh unter ihr (iPhone 13: rund 80 px). Am Handy heisst das: Die letzten Zeilen einer Liste oder der Knopf am Seitenende sind erst zu sehen, wenn die Leiste einklappt. Seit v0.28.174 folgt der CluCore-Rahmen in base_app.html darum der sichtbaren Höhe: Klasse app-frame am Rahmen, dazu body{min-height:100dvh}.app-frame{height:100dvh} direkt im Kopf der Vorlage. Warum so gebaut: Die Regel steht in der Vorlage und nicht als Tailwind-Klasse h-dvh, damit die Rahmenhöhe nicht von einem frisch gebauten output.css abhängt (Punkt 3); h-screen und min-h-screen bleiben als Rückfall für Browser ohne dvh. Warum nicht nur auf einzelnen Seiten: v0.28.173 hatte es zuerst nur im Import gelöst; dieselbe Ursache galt aber für jede CluCore-Seite. Folge: In CluCore klappt die Adressleiste am Handy nicht mehr ein, weil das Dokument selbst nicht mehr scrollt; dafür ist immer alles erreichbar. Regel: neue volle Höhen mit dvh rechnen und die Rahmenhöhe nie in einer Seite setzen (Test apps/core/tests_rahmen.py). Feste Flächen mit voller Höhe (v0.28.175): Weil das Dokument nicht mehr scrollt, bleibt die Leiste stehen, und eine Fläche mit position: fixed und 100vh ragt immer unter sie. Die Bearbeiten-Fenster der Tabelle (cluhu-table.js), die Bildansicht (_lightbox.html) und die Programm-Lightbox (programm-lightbox.js) geben ihre Höhe darum zweifach an: zuerst mit vh, danach mit dvh. Der Browser nimmt die letzte Angabe, die er versteht. In Vue geht das mit einer Liste: maxHeight: ['calc(100vh - …)', 'calc(100dvh - …)']. Noch mit vh: Dialoge mit einem Anteil der Höhe (max-h-[80vh], max-h-[90vh]; am Handy höchstens rund 7 px zu hoch) und das Kalender-Raster (liegt im scrollenden Seiteninhalt, alles bleibt erreichbar). ⚠️ Der Prüfbrowser (Playwright) hat keine bewegliche Adressleiste, dort ist 100vh gleich 100dvh; die Wirkung zeigt nur ein echtes Gerät.output.css hat seit v0.16.73 den Cache-Buster ?v={{CLUHU_VERSION}} (Context-Processor) — davor zeigte iOS/Safari tagelang alten Stand („Canvas zerfiel").?v=0.16.xx in Templates) — bei „unerklärlichen" Frontend-Bugs zuerst Hard-Refresh probieren. Mehrfach war ein vermeintlicher Bug nur Browser-Cache.visibleColumns/columnOrder aus alten Sessions machen neue Spalten unsichtbar (_mergeColumnOrder behebt das teilweise).Person.user ist SET_NULL, die Tenant-Middleware verlangt aber user.person. Person eines Users löschen = User ausgesperrt. AuthRoleAssignment hat zudem CASCADE auf Person. Deshalb: Bulk-Deletes schonen Login-Personen immer.OnboardingVerification überlebt User-Deletes — beim Komplett-Wipe eines Test-Users die Verification per E-Mail separat löschen.ClubAddon.active ist nicht massgeblich — billing_status zählt (Details).?filter[…]=) werden in der Session gemerkt und bleiben bis zum manuellen Entfernen aktiv.5.0 → wird zu '5' normalisiert.MAX_PROBE_ROWS ab — Dateien wurden stillschweigend abgeschnitten. Zusätzlich vergiftete der erste IntegrityError die ganze Transaktion → seither Savepoint pro Zeile.select_for_update-Status-Claiming.pattern-Attribut blockierte einen Submit clientseitig komplett (gar kein Request) — Server-Validation ist die Single Source of Truth, pattern entfernt. (Ursprung: Bestätigungstext „ALLE LOESCHEN" ohne Umlaut vs. Eingabe mit Umlaut.){% csrf_token %} passierte → @ensure_csrf_cookie + Token aus Cookie lesen.@click.prevent auf Checkboxen: Browser stellt den Zustand nach dem Microtask zurück → Checkboxen wirken eingefroren. Lösung: deterministisches Vanilla-JS.ALLOWED_IMPORT_EXTS → HTTP 500) — seither AST-Vergleich als Vollständigkeits-Check.from .models zeigte auf die falsche App → ImportError erst zur Laufzeit im Import-Pfad.| URL/Parameter | Wirkung |
|---|---|
/clucore/mitglieder/zuordnung/?view=canvas|list|firmen |
Canvas / Simpel-Modus / Firmen-Canvas |
?filter[member_number]=… / ?filter[person_number]=… |
„Springen"-Filter, funktioniert auf jeder Liste |
/clucore/mitglieder/?manual=1 |
Empty-State überspringen |
/kalender/antwort/<token>/?a=zusagen|absagen |
Magic-Link mit Vorauswahl |
00000 = Owner; Mitgliedernummern-Autovergabe ab max+1 (min. 1000)status=passiv + exited=True; Austrittsdatum impliziert exitedmember_reset_all räumt auch Waisen ab (Personen ohne Login, verwaiste Adressen/Telefone, leere Feld-Definitionen)Person.objects und Member.objects liefern Gelöschte mit. Löschen setzt nur archived_at; jede Abfrage «alle Personen des Vereins» muss archived_at__isnull=True selbst filtern. Der Dubletten-Finder tat das bis v0.28.176 nicht (Meldung «109 mögliche Duplikate» nach einem zweiten Import). Stand 13.11.2026 lesen gut zwanzig weitere Stellen ohne diesen Filter, unter anderem Auswahllisten in Ehrungen, Newsletter, J+S und J+M sowie Zähler in apps/core/hints.py: einzeln prüfen, ob Gelöschte dort erscheinen dürfenfail_silently — kein Retry/Queuelink_address_member, link_phone_number_member, ClubImportColumnMap, Batch.member_display_name_columnsDer runserver-Autoreloader im web-Container stirbt bei langer Laufzeit still — der Live-Prozess serviert dann tagealten Python-Code, während DB-Migrationen und Templates aktuell sind. Symptome: kaputte Seiten, extrem langsame Requests, nginx-504 (so beim Mitglieder-Import erlebt). ⚠️ manage.py shell-Tests sehen das NICHT (frischer Prozess mit aktuellem Code).
Regel: Nach jedem Merge mit Python-Änderungen cd /srv/cluhu/code && docker compose restart web. Diagnose-Trick: die Ordnernamen unter /app/import_logs/ tragen die CLUHU_VERSION, die der Live-Prozess geladen hat — mit apps/core/views.py vergleichen.
CLUHU_VERSION stand wochenlang auf v0.16.80, obwohl die Changelog-Einträge weiterliefen: verkettete sed-Bumps (s/v0.16.94/v0.16.95/ …) liefen teils im falschen Verzeichnis ins Leere — und weil jeder Bump das Muster des vorherigen erwartet, schlugen ALLE Folge-Bumps still fehl. Folge: der ?v=-Cache-Buster für CSS/JS war eingefroren; Browser zeigten teils alten Stand trotz Hard-Reload.
Regel: Versions-Bumps (und alle verketteten Ersetzungen) mit absolutem Pfad und assert count==1 (Python) statt stummem sed; Ergebnis per grep belegen.
/clucore/* und /api/* sind nur noch für Rollen mit access_level='clucore' erreichbar (CluCoreGateMiddleware). Jede neue API unter /api/ ist automatisch Admin-only — Endpoints für Portal-User gehören unter /portal/api/. Ausnahmen des Gates: logout, password-reset, /portal/*, /zugang/*, Kalender-Magic-Links/ICS.Client.login() funktioniert nicht mit dem TenantEmailBackend (Django übergibt kein request → kein request.club). In Tests: Login-POST auf /clucore/ (LoginView) oder force_login.{# #}-Kommentare sind NICHT mehrzeilig. Ein mehrzeiliger {# … #}-Kommentar wird als Text gerendert — in den Faktura-PDFs erschien er als Leerseite vor jeder Rechnung (gefixt v0.19.4). Mehrzeilig immer {% comment %}.member.display_name wird per Signal vom Hauptkontakt überschrieben (Person-Save/Link-Save synct den Namen). Tests und Filter dürfen sich nicht auf manuell gesetzte Member-Namen verlassen — Testdaten über Personen-Nachnamen isolieren.floatformat lokalisiert (de-ch → Komma): Beträge in PDFs serverseitig CH-formatieren (Punkt, Tausender-Apostroph), nicht im Template.run_invoice_automations läuft täglich 05:15 via Host-crontab (yves), Log /srv/cluhu/compose/faktura_automations.log — die erste Cron-Infrastruktur des Projekts (sonst ist alles lazy)./zugang/<token>/) sind Einmalgebrauch und nur als SHA-256-Hash gespeichert; Gültigkeit hängt von der Rolle zum Versandzeitpunkt ab (clucore 3 Wochen / portal 3 Monate).