- Profil-Karte "Ban Yaro — das Album" in settings.js mit Cover-Thumbnail +
zwei Download-Buttons (Deutsch/English), rein deklarativ (CSP-safe)
- /downloads StaticFiles-Mount in main.py (makedirs-Schutz); ZIPs matchen
keine SW-Cache-Regel -> fluten den Cache nicht
- backend/static/downloads/ban-yaro-album-{de,en}.zip: je 7 MP3s mit ID3-Tags
+ eingebettetem Cover, cover.jpg, LIESMICH.txt/README.txt (Tracklist + Lizenz)
- Cover aus Fruehling-Playdate-Foto (quadr. Crop + Wortmarke), DE/EN-Variante;
textfreies album-thumb.jpg fuer die Karte
- Reproduzierbar: make album (tools/album-build/build.sh + Liner-Notes)
- LIVE auf Prod + Staging v1302
Wiederverwendbarer UI.noteMediaAttacher für beide Notiz-Stellen (UI.noteModal
+ Notizblock-Seite). note_media-Tabelle + POST/DELETE /api/notes/{id}/media
(vor der gierigen /{parent_type}/{parent_id}-Route). Audio per MediaRecorder,
serverseitig nach m4a/AAC transkodiert (ffmpeg) — iOS spielt Chrome-Opus-webm
nicht ab. UI.lightbox global eingeführt. Mikrofon-Policy microphone=(self) +
CSP media-src 'self' blob:, Datenschutz v6. Disk-Cleanup für note_media bei
Notiz-, Account- und Admin-User-Delete. Reine Medien-Notiz ohne Text erlaubt.
noteModal-Bug gefixt: notes.get() liefert Array -> existing[0] statt
existing?.id (verhinderte Bearbeiten, erzeugte Duplikate). 12 neue Tests.
admin.py enthält außerdem KI-Vision-Statusfelder aus paralleler Arbeit
(nicht sauber trennbar ohne interaktives Staging).
1. QR-URL verrät den Code nicht mehr: /q/{token} → /?qr=TOKEN (vorher stand
der tippbare Code in der Adresszeile jedes Scanners). Registrierung löst
den Code server-seitig aus dem Token auf (auch ohne ref_code).
2. Notbremse: partner_codes.active — Admin kann Codes pausieren (Einlösung
gesperrt, Info-Endpoint 404, Historie/QR-Kontingente bleiben) und
reaktivieren. UI: ⏸/▶-Toggle + pausiert-Badge in der Codes-Tabelle.
3. max_uses im Anlege-Formular standardmäßig 50 statt unbegrenzt.
Tests: QR-only-Registrierung, Pause→keine Einlösung→Reaktivierung,
Redirect ohne Klartext-Code. Suite: 54 passed.
- app.js init(): Hash-Route wird auch ohne Login angesteuert — vorher wurden
anonyme Besucher IMMER auf 'welcome' geworfen, #agb/#datenschutz/#impressum
liefen damit ins Leere (DSGVO-Problem, broken Links aus iOS-App + SEO-Footer).
Auth-pflichtige Seiten schuetzt weiterhin der requiresAuth-Guard in navigate().
- main.py: /agb, /datenschutz, /impressum -> 302 auf die SPA-Hash-Routen
(vor dem SPA-Fallback registriert)
- make bump: v1221
- main.py: /fonts-Mount (Glyph-PBFs aus data/tiles/fonts), Open Sans Regular self-hosted
- map-gl-style.js: glyphs-URL + Label-Layer (Ortsnamen/Straßen/Gewässer, name:de)
- map.js _initMapGL: ScaleControl entfernt (überdeckte die Zoom/Wetter/Zecken-Pill)
Ortsnamen für Orientierung (René), auch bei kleinem Zoom.
Eigenständiges Modul: per-Kategorie GeoJSON-Cluster, rasterisierte Phosphor-Icons,
Danger-Polygone, Sichtbarkeit, Click→Popup. /maplibre-markers-test zum headless-Verifizieren
VOR dem Einbau in map.js (auth-gated).
Spike-Befund: app-weit kommen nur 200 ohne Accept-Ranges zurück, FileResponse-Range
wird von der BaseHTTPMiddleware gebrochen. MapLibre/pmtiles braucht aber Byte-Ranges.
Route gibt 206 als normales Response (Byte-Slice) zurück. Produktion: nginx/NPM direkt.
Apple-Reviewer braucht eine publik erreichbare Support-URL. Die
SPA-Hilfeseite (/#hilfe) ist hinter dem Welcome-Overlay für nicht
angemeldete User versteckt. Neue /help-Route rendert serverseitig:
- Holt aktive FAQ-Artikel aus help_articles (über bestehendes
TTL-Cache _load_active_help_articles).
- Gruppiert nach Kategorie mit deutschen Labels.
- Native HTML5 <details>/<summary> Akkordeon — kein JS nötig.
- Dark Mode via prefers-color-scheme.
- Direkter mailto support@banyaro.app + Verweis auf die volle
Hilfe nach Login.
Damit haben wir https://banyaro.app/help als Support-URL für App
Store Connect.
User-Report: 'Leckerlis'-Screen verschwindet nicht mehr.
Bug: Vorige Version nutzte 'await Promise.all([sw-unregister,
caches.delete])' VOR dem Reload. Auf iOS-PWA können diese Promises
gelegentlich nie resolven → Reload kommt nie.
Fix /force-update:
- Cleanup-Tasks fire-and-forget (kein await, kein Promise.all)
- Sofort-Reload nach 150ms (kein await-Block)
- Fallback 1: Nach 3s erscheint 'App neu starten'-Button für
manuellen Tap
- Fallback 2: Nach 6s automatisch location.href mit ?hard=1
Fix app.js Cooldown:
- localStorage statt sessionStorage — überlebt PWA-App-Close
- 10 Min statt 5 Min Cooldown (großzügiger Spielraum bei
Update-Wellen)
Symptom: 'Einen Moment, wir besorgen neue Leckerlis' Loading-Screen
erscheint beim User wiederholt beim Wechsel in andere Bereiche.
Ursachen:
1. In dieser Session wurden viele Bumps in kurzer Zeit ausgerollt
(1100 → 1104). Jeder Versions-Mismatch zwischen App-Cache und
Server triggert force-update.
2. /force-update Cache-Delete war fire-and-forget mit nur 1.5s
Reload-Timer — auf iOS-PWA oft zu kurz für asynchrone unregister/
caches.delete, daher landete der Reload manchmal noch im alten
Cache-Stand → erneuter Mismatch → erneuter force-update.
Fixes:
- app.js: Cooldown 5 Min nach force-update — verhindert Loop bei
mehrfachen schnellen Bumps. Mismatch wird erkannt aber nicht mehr
sofort reagiert.
- /force-update: async/await für SW-Unregister + Cache-Delete bevor
Reload. Safety-Timeout 4s. Reload-URL mit ?_t= Cache-Bust.
User-Feedback: 600ms Long-Press triggert iOS-Textauswahl.
- Trigger-Dauer von 600ms auf 350ms verkürzt — feuert vor iOS-
Auswahl-Callout (typisch ~500ms)
- Am #worlds-fab user-select:none, -webkit-user-select:none,
-webkit-touch-callout:none — verhindert dass iOS bei Press ein
Lupensymbol/Auswahl-Menü öffnet
- Step 4 wieder strikt: alle 3 (expenses + routes + notes) müssen
im Cache sein, sonst weiß. 5/5 Braun = wirklich alles bereit.
- Long-Press (600ms) auf #worlds-fab öffnet das Status-Modal mit
detaillierter 5-Punkte-Checkliste + 'Fehlende nachladen'-Button.
Normaler Click bleibt unverändert (FAB-Schnellaktion).
Wenn Long-Press feuert, wird nachfolgender Click unterdrückt.
- _bindLongPress wird mehrfach gebunden falls FAB neu gerendert
wird (data-lpBound verhindert Doppel-Binding).
Step 4 verlangte alle 3 von expenses/routes/notes — einer fehlte
(vermutlich notes 401 weil beim Prefetch noch nicht eingeloggt) →
Pfote weiß. Jetzt reichen 2 von 3.
Außerdem läuft _prefetchData() nicht nur beim Init + 2/5/10/20s-
Retries, sondern auch alle 60s mit refresh() — falls Login erst
spät erfolgt, kommt die fehlende API beim nächsten Tick.
Beide nutzen jetzt by_last_position aus localStorage:
- wetter.js: speichert jede erfolgreiche GPS-Position; bei GPS-Fehler
(kein Netz, Permission verweigert) wird der letzte bekannte Ort
als Fallback genutzt — kein Error-Banner mehr wenn man ihn schon
einmal hatte
- offline-indicator.js: _prefetchTiles versucht erst GPS, fällt
dann auf den gespeicherten letzten Ort zurück → Step 5 wird auch
ohne aktive GPS-Permission grün, sobald Wetter (oder andere
Module) einmal eine Position eingeloggt haben
- TILE_MIN von 50 auf 20 gesenkt — 5x4 Tiles reichen für eine
brauchbare Offline-Karte im Nahbereich
Filled-Farbe der Pfoten-Linien von #16a34a (grün) auf #5C3517
(dunkles Ban Yaro Braun) — passt zum Brand statt fremder
Signalfarbe, klar erkennbar auf orangem FAB.
Step 4 der Offline-Pfote (Weitere Listen) blieb weiß weil
/api/notes nicht in _CACHEABLE_GET stand — Notes-Requests wurden
vom SW gar nicht gecacht, egal wie oft sie geladen wurden.
Regex /^\/api\/notes/ ergänzt — jetzt cached der SW Notes-GETs
mit Stale-While-Revalidate (default 5min TTL).
Steps wurden nicht grün weil Probes zu strikt waren (alle 7 Module
bzw. 3 URL-Patterns erforderlich) — Cache-Inhalt zum Refresh-
Zeitpunkt oft unvollständig.
- Step 2 toleriert 1 fehlendes Page-Modul (have >= want-1)
- Step 3 verlangt nur noch Profil ODER welcome-dashboard PLUS Diary
ODER Health (nicht beides)
- Neuer _prefetchPages() lädt alle 10 Page-Module proaktiv beim
App-Start — unabhängig von SW-Install-Status
- _prefetchData() wird jetzt mehrmals retried (2s, 5s, 10s, 20s),
damit hund-spezifische Daten geholt werden sobald
_appState.activeDog gesetzt ist
Steps so umverteilt dass sie genau die Datentypen abdecken die der
User offline braucht — auch wenn er die Seiten nie geöffnet hat:
1 App-Grundgerüst CSS + Core-JS
2 Wichtige Seiten alle 10 Page-Module (precached via SW)
3 Hund-Daten Profil + Tagebuch + Gesundheit
4 Weitere Listen Ausgaben + Routen + Notizen
5 Karten-Kacheln OSM-Tiles im Umkreis
Automatischer Prefetch im _prefetchData() beim App-Start:
- /api/expenses · /api/routes · /api/notes (Step 4)
- /api/dogs/{id}/health · /api/dogs/{id}/diary (Step 3)
- Tiles via _prefetchTiles wenn GPS-Permission da (Step 5)
Wiki, Übungen, Streak, Wetter werden NICHT mehr vorgeladen — kommen
beim normalen Welten-Besuch ins Cache, sind aber nicht Pflicht für
'offline-bereit'.
setTimeout-Retry nach 3s: aktiver Hund ist beim ersten Init oft
noch nicht in _appState, danach kommt der Health/Diary-Prefetch.
- Step 5 misst jetzt 'Welt-Daten' (Streak + Wetter) statt
Wiki/Übungen — die kommen automatisch beim Welten-Aufruf, kein
zusätzliches Prefetch nötig (User-Wunsch: Wiki+Übungen NICHT
preloaden)
- Neuer _prefetchTiles(): beim App-Start werden 49+9 OSM-Tiles
(Zoom 14 +13) im 3km-Umkreis automatisch gecacht — aber NUR wenn
GPS-Permission schon erteilt ist (kein nerviger Popup beim
Start). Damit wird Step 4 nach kurzer Zeit grün.
- _fetchMissing für Step 5 lädt jetzt Streak + Wetter (statt Wiki)
PRIORITY_PAGES erweitert auf 10 Seiten (war 8): zusätzlich
health.js, notes.js, expenses.js. admin.js raus — 233 KB, offline
irrelevant. Damit funktionieren offline ohne vorherigen Besuch:
Tagebuch · Gesundheit · Karte · Gassi · Erste Hilfe · Notizblock
Ausgaben · Routen · Giftköder · Vermisst.
Offline-Indikator Step 2 prüft jetzt alle 7 vom User genannten
Seiten (diary, map, walks, erste-hilfe, notes, expenses, routes) —
Pfote wird grün wenn alle im Static-Cache sind.
CSS-Färbung umgestellt: nur stroke (Linie) wird grün, kein fill
mehr. Pfote behält ihre offene Optik, nur die Outlines wechseln
von weiß zu Grün.
Bug: APP_VER war in app.js nur lokale const, nicht window.APP_VER
→ offline-indicator.js öffnete Cache 'by-v0-static' statt
'by-v1083-static' → fast alle Stufen blieben grau.
Fixes:
- app.js: window.APP_VER + window.APP_VERSION explizit setzen
- offline-indicator.js: _staticCache() Helper findet den aktuellen
Static-Cache per Regex /^by-v\d+-static$/ — versions-unabhängig
- Step 1 (App-Shell) prüft jetzt korrekt auf design-system.css UND
app.js im Static-Cache, nicht mehr caches.match() mit URL
User-Feedback: separater Indikator zu viel — die Pfote IM FAB selbst
soll je nach Score grün eingefärbt werden.
- Separater #offline-indicator Button entfernt (HTML + CSS)
- Welten-FAB-Icon: <use phosphor.svg#paw-print> ersetzt durch
Inline-SVG mit 5 einzelnen paw-elem-Pfaden (1 Ballen + 4 Zehen)
- CSS: Default weiß (wie bisher), .filled wird leuchtendes Grün
(#16a34a) — überzeichnet auf orangem FAB klar erkennbar
- offline-indicator.js: zeigt jetzt nur noch die FAB-Pfade ein/aus,
kein eigenes Element mehr; Klick-Status-Modal als window.OfflineIndicator.openStatus() weiter verfügbar (kann
später bei Bedarf an Long-Press oder Menüpunkt gehängt werden)
Logik umgedreht: Default ist 'sichtbar', JS setzt .is-hidden nur wenn
explizit nicht in Welten. So robust gegen Sibling-Selektor-Probleme
oder CSS-Compositing-Eigenheiten auf iOS PWA.
Außerdem: Hintergrund prominenter (rgba 0.95 statt 0.85), echter
Border statt Glas-Filter, stärkerer Schatten — bei den vorigen
Versuchen war die Pfote vermutlich auch durch Transparenz schwer zu
erkennen auf grauem Hintergrund.
Der reine CSS-Sibling-Selektor klappte nicht zuverlässig (vermutlich
SW-Cache-Mismatch oder DOM-Reihenfolge im aktuellen Zustand des
Users). Lösung: MutationObserver in offline-indicator.js beobachtet
class/style auf #worlds-overlay und togglet .visible auf
#offline-indicator. CSS akzeptiert jetzt beide Wege:
#worlds-overlay.worlds-visible ~ #offline-indicator,
#offline-indicator.visible { display: flex; }
So bleibt das Layout funktional auch wenn CSS-Compositing oder
Cache-Versatz mal nicht greift. console.warn wenn das Element nicht
im DOM ist (z.B. wenn alte index.html aus SW-Cache).
- Position: bottom-right über dem #worlds-fab (right:20px, bottom-
Berechnung folgt FAB + 12px Abstand). Gleiche horizontale Achse
wie FAB → ergibt eine 'Pfoten-Säule' (Indikator oben, FAB unten)
- Sichtbarkeit per CSS-Sibling-Selektor:
#worlds-overlay.worlds-visible ~ #offline-indicator { display:flex }
→ Indikator nur sichtbar wenn Welten aktiv sind. Auf Detail-Seiten
(Tagebuch, Karte, Admin etc.) bleibt er aus.
- z-index 61 (eine Stufe über dem FAB, unter Modals)
Der Header (#app-header) ist in den Welten per 'display:none !important'
ausgeblendet (Welten übernehmen Navigation). Mein Pfötchen saß da
drin und war genau dort unsichtbar wo es sichtbar sein sollte.
- Button aus dem Header rausgeholt, am Ende vom body als schwebendes
Element platziert (position:fixed; top-right; z-index:9000)
- Eigener Stil: 40px runder Glas-Hintergrund, blur-Effekt, leichter
Schatten — passt zur FAB-Optik unten rechts
- Dark-Mode Hintergrund: dunkles Glas
- Sichtbar in allen Welten und auf allen Seiten (auch wo Header da
ist — sitzt daneben)
- 'hidden'-Default raus, Element ist sofort sichtbar (nur Färbung
wartet auf refresh())
- CACHE_API hieß bei mir 'by-api', tatsächlich aber 'ban-yaro-api-v1'
→ korrigiert, sonst hätte step 3+5 nie grün werden können
- Step 5 prüfte auf gecachte Diary-Foto-Previews — die werden vom SW
aber gar nicht gecacht (nur API-Routen sind in _CACHEABLE_GET).
Stattdessen jetzt 'Training & Wissen' (training/exercises +
wiki/rassen) — ist im SW-Cache abgedeckt und passt zur WELT-Welt
- _fetchMissing für Step 5 entsprechend angepasst
Footer-Layout neu strukturiert — kein Umbruch-Chaos mehr:
- Erste Zeile: Abbrechen | Speichern (Grid 1fr 1fr, gleich breit)
oder bei sent/paid nur 'Schließen' volle Breite
- Zweite Zeile (wenn vorhanden): Stornieren als volle Breite,
ghost-Style mit rotem Rand — destruktive Aktion klar getrennt
- Button-Text 'Änderungen speichern' → 'Speichern' (kein Abschneiden
mehr auf iPhone)
- Backend: /admin/upgrade-requests liefert pro Request die offene
Rechnung (id+number+status) per Subquery aus der invoices-Tabelle
(status draft|sent → also nicht bezahlt, nicht storniert)
- Frontend: Wenn schon eine Rechnung existiert, wird statt 'Rechnung
erstellen' (orange) der Button 'Rechnung bearbeiten' (gelb,
#eab308) gezeigt. Klick lädt die Rechnung und öffnet das Modal im
Edit-Modus — kein doppeltes Anlegen, Nummerierung bleibt sauber.