Commit graph

1155 commits

Author SHA1 Message Date
647aa684db Vektor-Basemap: zentrale Karte (pages/map.js) integrieren — sie umging UI.map.create
- map.js _addBasemap: Vektor-Layer (Flag) mit Raster-Fallback, eigener Basemap-Code
- Theme-Wechsel baut Vektor-Layer mit passendem Flavor neu (kein CSS-Filter bei Vektor)
- ui.js: UI.map.vectorEnabled()/vectorLayer() exponiert für Karten mit eigenem Layer-Mgmt
- APP_VER bump
2026-06-05 08:28:11 +02:00
b2262a8e86 Vektor-Basemap: ?vectormap=1/0 früh in boot.js erfassen (überlebt Query-Stripping beim Boot) + APP_VER bump 2026-06-05 07:57:21 +02:00
9006c85434 Bump APP_VER 1173→1174 (Vektor-Basemap-Integration) 2026-06-04 22:26:42 +02:00
2b5afcf0ae Tile-Server: Vektor-Basemap in PWA integrieren (protomaps-leaflet, Feature-Flag)
- ui.js Map.create: Basemap-Swap OSM-Raster→PMTiles-Vektorlayer hinter Flag
  'by_vector_map' (?vectormap=1/0). Leaflet+markercluster+Marker unverändert,
  sauberer Raster-Fallback bei Fehler. Attribution Pflicht eingeblendet.
- map-vector.js: protomaps-leaflet paintRules/labelRules für OpenMapTiles-Schema
  (Light+Dark), Labels per Canvas-Text → keine Glyphs nötig. Quelle /tiles/dach.pmtiles.
- protomaps-leaflet 4.0.1 vendored.
- Makefile: 'make tiles' (download→merge -H+time-filter dedup→planetiler) + 'make tiles-deploy'
  (atomarer Swap, ENV=prod für Produktion).
2026-06-04 21:53:07 +02:00
a561759034 Tile-Server-Spike: MapLibre-Testseite /maplibre-test (vendored maplibre-gl+pmtiles)
- /maplibre-test rendert bayern.pmtiles per pmtiles-Protokoll, minimaler
  Geometrie-Style (OpenMapTiles-Layer, keine Glyphs), Touren-Demo als GeoJSON-Line
- maplibre-gl 4.7.1 + pmtiles 3.2.1 lokal vendored (CSP script-src 'self')
- CSP: worker-src blob: (MapLibre-Worker)
2026-06-04 20:42:35 +02:00
e5a2953a80 Tile-Server: HEAD-Support für /tiles (Dateigröße-Probe ohne 405) 2026-06-04 20:35:06 +02:00
bdadde8b98 Tile-Server: eigene Range-fähige /tiles-Route (StaticFiles liefert hinter BaseHTTPMiddleware kein 206)
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.
2026-06-04 20:31:57 +02:00
d9ecdb15fb Tile-Server-Spike: /tiles StaticFiles-Mount + tiles/ vom Tar/Git ausschließen
- main.py: geschützter /tiles-Mount (TILES_DIR, No-Op ohne Verzeichnis), Range-Requests via Starlette FileResponse
- Makefile: tiles/ aus TAR_EXCLUDE (546MB pmtiles nicht ins Staging-Tar)
- .gitignore: *.pmtiles/*.osm.pbf/tiles-build ausschließen
2026-06-04 20:27:33 +02:00
cde019cacf Docs: Übergabe Tile-Server (planetiler→PMTiles→MapLibre, Staging-Spike-Plan) 2026-06-04 19:57:59 +02:00
545b57c723 Fix: Account-Löschung FK-sicher über alle Tabellen (defer_foreign_keys + Introspektion)
Der alte delete_account löschte nur ~6 Tabellen und scheiterte am finalen
DELETE FROM users, sobald der User Zeilen in Non-Cascade-Tabellen hatte
(routes, places, walks, events, forum_threads, invoices …). Jetzt:
PRAGMA defer_foreign_keys + foreign_key_list-Introspektion (CASCADE automatisch,
NO-ACTION-Eigentum löschen, Actor/SET-NULL-Spalten nullen) plus user_id/owner_id-
Scan für Tabellen ohne formale FK. Regressionstest test_account_deletion.py.

Relevant für App-Store-Gl. 4 (In-App-Konto-Löschung muss zuverlässig funktionieren).
2026-06-04 19:21:18 +02:00
1448782564 UX: Welten-Editor — Hinweis dass ✕ ausblendet (nicht löscht)
Statischer Hinweis präzisiert + Toast beim ersten Ausblenden: Funktion bleibt
über 'Weitere Funktionen' (Ausgeblendete Funktionen) abrufbar, wird nicht gelöscht. SW v1173
2026-06-04 18:36:42 +02:00
258ccf84ee Feature: Alter fließt in Kalorienbedarf ein (Lebensphasen-Faktoren)
Welpe <4 Mon ×3.0, Junghund 4-12 Mon ×2.0 (Wachstum, überschreibt Aktivität),
Senior 7+ ×0.90, Hochbetagt 11+ ×0.85 des Erwachsenen-Faktors. Lebensphase wird
im Ergebnis angezeigt. SW v1172
2026-06-04 18:04:16 +02:00
cca9a9c70f Fix: Ernährungs-Rechner übernimmt Gewicht+Alter aus Hundeprofil
Falsche Feldnamen: dog.gewicht/dog.alter existieren nicht. Korrekt: gewicht_kg,
und Alter aus geburtstag berechnen (_alterJahre). Beide werden jetzt im Kalorien-
Rechner vorbefüllt. SW v1171
2026-06-04 17:59:34 +02:00
0e77c04eee Fix: gespeichertes Futter-Profil beim Öffnen sichtbar (war hinter Berechnung versteckt)
ernaehrung.js: Das Profil (Marke, Portionen, Notizen, Futter-Typ) wird beim Laden
direkt angezeigt, wenn gespeicherte Daten existieren — vorher nur nach Klick auf
'Kalorienbedarf berechnen'. Gespeicherter Tagesbedarf (kcal_tag) wird 1:1 wieder
gezeigt (kein Neu-Rechnen). _berechne → _showResult refaktoriert. SW v1170
2026-06-04 17:53:03 +02:00
3513aeadb0 Fix: Referral-Code überlebt App-Schließen (localStorage statt sessionStorage)
Werben-Zuordnung ging verloren, wenn die geworbene Person den ?ref=-Link öffnete,
die App schloss und sich erst später registrierte (sessionStorage flüchtig, v.a.
iOS-PWA). Jetzt: Code früh in boot.js nach localStorage (vor evtl. SW-Reload, der
die URL ersetzt), 30-Tage-Ablauf, Löschung nach Registrierung. SW v1169

Datenkorrektur separat: nacho_sarah Angie (id=4) als 2. Werbung zugeordnet.
2026-06-04 17:38:14 +02:00
7945087a6c Fix: Routen-Bewertungen (Kommentare) waren für niemanden sichtbar
UI.ratingStars lud die Bewertungen via API, speicherte/renderte das ratings-Array
aber nie — nur Durchschnitt+Anzahl. Jetzt wird die Liste aller Bewertungen mit
Kommentar (Name + Sterne + Text) angezeigt; nach dem Speichern neu geladen.
Backend war korrekt. SW v1168
2026-06-04 17:25:40 +02:00
78866206b4 Feature: Routenaufzeichnung übersteht App-Updates (Guard + Persistenz)
Stufe 1 (Guard): Während aktiver Aufzeichnung wird der SW-/Force-Update-Reload
aufgeschoben (window._byRecording → boot.js/_bySwReload + app.js force-update);
nach Stop/Speichern via window._byReloadIfPending() nachgeholt.

Stufe 2 (Persistenz): Track wird gedrosselt nach localStorage (RecStore) gesichert
und beim nächsten Öffnen der Karten-/Routen-Seite als 'Aufzeichnung fortsetzen?'
angeboten (Resume seedet Track+km+Startzeit). Schützt auch bei Crash/OS-Kill/
manuellem Reload. Greift in map.js UND routes.js. SW v1167
2026-06-04 17:13:23 +02:00
ddfb9474ef Bump auf v1166 (Versionen synchron halten) 2026-06-04 16:53:26 +02:00
959fd81a9b Fix: Forum-Cooldown blockierte JEDEN Post (Zeitzonen-Bug)
_check_post_limits verglich datetime.utcnow() (UTC) mit created_at, das aber
als Client-Lokalzeit (CEST=UTC+2) gespeichert wird → diff negativ → immer 429.
Jetzt rechnen Cooldown + Stunden-Limit in derselben Zeitbasis (Client-Zeit des
Requests). Backend-only, kein SW-Bump.
2026-06-04 16:50:45 +02:00
c07b1cc01b Fix: restliche CSP-blockierte Inline-Handler — Bild-Fallbacks (globaler data-fb Error-Handler) + Hover-Effekte (CSS-Utilities + data-hover-play)
App ist jetzt vollständig frei von Inline-Event-Handlern (onerror/onmouseenter/etc.).
data-fb Modi: hide/hide-parent/dim-grandparent/sibling/show-el/emoji/initials + data-fb-src.
Hover: .by-hover-lift/-surface2/-surface3 in utilities.css. SW v1165
2026-06-04 16:22:43 +02:00
2ddd8ac350 Fix: alle funktionalen Inline-Event-Handler → addEventListener/Delegation (von CSP-Härtung 65cfa25 app-weit blockiert)
Chat (senden/öffnen/löschen/Foto), Tagebuch-Buch, KI-Berichte, Wiki-Moderation,
Events-Detail, Walks-Lightbox, Routen-Foto, Navigations-CTAs (data-page),
Presse-Copy + Züchter-Landing (externes JS). 35x UI.modal.close → data-modal-close,
28x totes event.stopPropagation entfernt. Verbleibend: kosmetische onerror/Hover. SW v1164
2026-06-04 13:59:27 +02:00
152fde716c Fix: Freunde Annehmen/Ablehnen/Chat per Event-Delegation statt Inline-onclick (von CSP-Härtung 65cfa25 blockiert), SW v1163 2026-06-04 13:35:53 +02:00
55b354e865 Freunde: Annehmen/Ablehnen-Buttons mit Text-Label (Icon-only war für Nutzer nicht erkennbar) + Fix /apifriends/same-breed Slash-Bug, SW v1162 2026-06-04 12:27:01 +02:00
f5b5bb2289 Streak: entkoppelt vom App-Öffnen — nur noch echte Touren zählen
- update_streak NICHT mehr in GET /achievements/me (lief bei jedem Öffnen).
  Streak wird nur noch von create_route (aufnehmen) + record_walk (ablaufen)
  hochgezählt.
- Angezeigter Streak korrekt: nur gültig wenn letzte Tour heute/gestern war,
  sonst 0 (gerissen) — ohne den gespeicherten Wert zu verändern.
2026-06-04 09:40:12 +02:00
667ed91f33 Routen: Bestätigungs-Toast nach Ablaufen + "X× gelaufen · zuletzt" auf der Karte
- record_walk gibt total_km zurück → Toast "🐾 X km gezählt · Lebenswerk Y km".
- Nachtrag-Toast (_flushPendingNavWalk) zeigt ebenfalls km + Lebenswerk.
- list_routes liefert my_walk_count + my_last_walked → Routen-Karte zeigt
  "🐾 X× gelaufen · zuletzt heute/gestern/vor N Tagen".
Macht für Angie sichtbar, dass das Ablaufen einer gespeicherten Route mitzählt.
2026-06-04 09:34:03 +02:00
91624dac25 Fix: gelaufene km bei Routen-Navigation gehen verloren wenn nicht über
In-App-Zurück geschlossen wird (Angie-Bug)

Bisher wurde walked_km NUR in _closeNav (In-App-Zurück-Pfeil) gespeichert.
Wer die Navigation anders verlässt (Handy sperren/Home/PWA schließen, oder
nur den Dim-Entsperrpfeil + normal schließen), verlor die km.

- Fortschritt laufend in localStorage sichern (überlebt App-Kill).
- _recordNavWalk() Einmal-Guard, aufgerufen von _closeNav UND pagehide.
- _flushPendingNavWalk() trägt beim nächsten App-Start einen nicht
  gespeicherten Walk nach.
- Fehler nicht mehr still verschlucken: bleibt in localStorage → Retry.
2026-06-04 09:09:07 +02:00
684ffa3b46 OSM-Verknüpfung: In-App-Hilfe „Konto erstellen" (umgebungsabhängig)
- /osm-auth/status liefert signup_url + sandbox-Flag (Sandbox-URL auf Staging,
  echte OSM in Prod).
- Settings-OSM-Karte: ausklappbare Hilfe "Noch kein OSM-Konto? Was ist das?"
  mit Erklärung, 3-Schritt-Anleitung, Sandbox-Testphasen-Hinweis und
  "Kostenloses OSM-Konto erstellen"-Link zur richtigen Instanz.
2026-06-03 22:04:42 +02:00
b4cafc0363 OSM-Verknüpfung: Post-Link-Redirect auf Einstellungen (/#settings) statt App-Root 2026-06-03 22:02:42 +02:00
9afbf24535 OSM-Beiträge: "Hund willkommen?" 👍/👎 (dog=yes/no) + Umdrehen
- dog=no zusätzlich zu dog=yes (Pächterwechsel → Ort nicht mehr hundefreundlich).
- Map-Popup: ein "Hund willkommen?"-Block mit Daumen hoch/runter statt zwei
  Buttons. Beide rufen /dog-friendly mit welcome=true|false.
- Backend generisch: tag_value yes|no; vorhandene Markierung mit anderem Wert
  wird umgedreht (Update statt 409); submit_dog_tag(value); Confirm/Revert prüft
  gegen den jeweiligen tag_value; Changeset-Kommentar wertabhängig.
2026-06-03 21:49:44 +02:00
57849515ea OSM-Beiträge: Map-Button (dog=yes), Changeset-Upload, Confirm/Pro-Job
- Map-Popup: "Hund war willkommen"-Button (dog=yes) für Restaurant/Hotel/
  Shop/Tierarzt/Hundesalon → POST /osm-contrib/dog-friendly.
- OSM-Changeset-Upload (write_api): Element holen (node/way) → dog=yes →
  Changeset create/upload/close; idempotent; best-effort beim Tap.
- OSM-Endpunkte konfigurierbar (OSM_OAUTH_BASE/OSM_API_BASE) — Staging gegen
  Dev-Sandbox, KEINE echten Edits auf Produktiv-OSM.
- Scheduler-Job (täglich 03:40): Pending-Retry + Revert-Überleben (7 Tage) →
  confirmed/rejected; Pro-Freischaltung (100 confirmed = 1 Jahr, idempotent via
  osm_pro_grants). HINWEIS: is_premium/subscription direkt gesetzt — vor Prod
  mit Billing abgleichen.
- Native Attestierung/Sensoren: bewusst NICHT (iOS-App-Thema, nicht PWA).
2026-06-03 21:40:50 +02:00
dc9c0d2cc0 Build 1156: SW-Cache-Bust für OSM-Verknüpfung/dog=yes-UI (Frontend-Änderung) 2026-06-03 21:24:11 +02:00
1cfaa0264f OSM-Beiträge: dog=yes-Erfassung mit GPS/Zeit-Anti-Fraud + Gamification-Zähler
- Tabelle osm_contributions (status pending→submitted→confirmed/rejected).
- Router /api/osm-contrib: POST /dog-friendly (Anti-Fraud: GPS-Beleg über
  kürzliche eigene Tour ≤50m + Verweil-Proxy, Tour-Recency 48h, Tages-Cap,
  Dedup, Positions-Sanity), GET /status (Zähler).
- Settings-UI: Zähler "X Orte eingetragen · noch Y bis Badge/Pro".
- OSM-Changeset-Upload + Pro-Freischaltung + Geräte-Attestierung folgen separat.
2026-06-03 21:20:32 +02:00
46caa05020 OSM-Verknüpfung (Modell A): OAuth2-Fundament für Nutzer-Beiträge
- Tabelle user_osm (access_token verschlüsselt at rest via Fernet,
  Schlüssel aus JWT_SECRET abgeleitet oder OSM_TOKEN_KEY).
- Router /api/osm-auth: authorize (signierter state mit user_id+CSRF),
  callback (Code-Tausch + OSM-Name holen + speichern), status, unlink.
- Profil-UI (Settings): "OSM-Konto verknüpfen" / verknüpft-als / trennen,
  hundehalter-spezifische Motivation.
- cryptography in requirements.
- Basis für dog=yes-Beiträge + Gamification/Pro (folgt). Staging-Branch.

ENV nötig: OSM_CLIENT_ID, OSM_CLIENT_SECRET (Redirect-URI default staging).
2026-06-03 21:14:36 +02:00
4bc7454258 POI-Karte: Offline-Import aus OSM statt Live-Overpass-Scan (Build 4)
- osm.py: Live-Scanner deaktiviert — /pois liest nur noch aus DB,
  /analyze ist No-Op. Behebt wiederholte OSM-Banns (Tile-Load + Scanning).
- tools/osm-extract: Extraktion (pyosmium) + Loader (schützt user_edited)
  + Docker-Refresh-Job mit osmium-tags-filter-Vorstufe (RAM-schonend).
- docker-compose.osm.yml: Refresh-Service (mem_limit 4g), monatlich via
  DSM-Aufgabenplaner.
2026-06-03 20:44:32 +02:00
214543559c Marketing-Cockpit (MARKETING.md) + Flyer (HTML-Quellen & Print-PDFs); unsplash/ ignoriert 2026-06-03 17:24:47 +02:00
10e39ed135 Karten-Ausbau (OSM), Forum-Erweiterung, UI-Komponenten, Refactor Tagebuch/Gassi (DRY), Landing/SEO — APP_VER 1155 2026-06-03 17:24:47 +02:00
2d907f6370 Öffentliche /help-Seite — Server-rendered FAQ ohne Login
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.
2026-05-30 19:34:12 +02:00
d23d696745 Tagesfoto-Cache validieren + bei Diary/Media-Löschung mit-bereinigen
Bug: daily_photo_cache zeigte auf gelöschte Tagebuch-Foto-URLs, weil
Löschen eines Eintrags oder einzelnen Medien-Items den Cache nicht
mit-bereinigte. Heim-Tab in der iOS-App lud dann 404 → kein Tagesbild.

Fix in dogs.py /welcome-dashboard:
- Bevor das Cache-Foto zurückgegeben wird, prüfen ob die URL noch in
  diary_media existiert. Wenn nicht: Cache-Eintrag löschen und neu
  wählen → selbstheilend für alte verwaiste Einträge.

Fix in diary.py:
- delete_diary: vor dem CASCADE-Delete von diary_media die URLs
  sammeln und alle daily_photo_cache-Zeilen darauf löschen.
- delete_media_item: gleicher Cleanup für die eine URL.

Cache ist klein (max 1 Eintrag pro Hund pro Tag) — Hygiene-Cleanup
ist günstig und macht das System defensiv.
2026-05-30 19:00:56 +02:00
f934560a12 Routenübersicht: immer Karten-Preview statt erstem Foto, Kamera-Badge bei Routen mit Bildern
Bisher: wenn eine Route Fotos hatte, zeigte die Karten-Übersicht das
erste Foto statt der Mini-Map → kein Vergleich der Tracks auf einen
Blick möglich.

Jetzt: die Mini-Map ist immer da, ein kleiner Kamera-Badge oben rechts
zeigt die Anzahl Fotos an (analog zum Tagebuch). Die Fotos sind weiter
unverändert in der Route-Detail-Ansicht zu sehen.

CSS: .rk-card-preview bekommt position:relative für das Badge.
2026-05-30 18:16:46 +02:00
4cfce1051f Pro-Badge im Profil: zeigt jetzt korrekten Tier statt veraltetem is_premium
- settings.js Header-Badge unter dem Namen leitet jetzt aus
  subscription_tier ab (analog _tierCard / has_pro_access): Admin/
  Moderator, Züchter, Pro, sonst 'Kostenlos'. Vorher las nur das alte
  is_premium-Flag, was beim Admin-Upgrade nicht mitgezogen wurde.
- admin.py fulfill_upgrade_request setzt jetzt is_premium synchron mit
  subscription_tier (1 für pro/breeder, sonst 0). Hält Login-Response,
  /auth/me und Reports konsistent.
2026-05-30 18:10:57 +02:00
8d2cc279ae Navigation: worlds-back-Pfeil sichtbar bei Page-zu-Page-Sprung
Bisher: App.navigate() rief nur dann Worlds.hide() (und damit
worlds-back-visible) wenn Worlds gerade sichtbar war. Wer aus dem
Onboarding direkt nach #dog-profile navigiert (kein vorheriges
Worlds-Anzeigen) hatte keinen Zurück-Pfeil zu den Welten + FAB —
saß auf dem Profil fest.

Fix: in navigate() unabhängig vom Worlds-State die Klasse
worlds-back-visible setzen, sobald ein eingeloggter User auf einer
nicht-welcome/onboarding-Seite ist. Bump 1136→1137.
2026-05-30 18:00:14 +02:00
bd9acda084 User-Löschen: Upgrade-Anfragen + Hund-Daten mit aufräumen (Admin + Self-Delete)
- admin.py delete_user: löscht jetzt auch Hund-zentrierte Daten (diary,
  health, training_sessions, training_streaks, expenses), dogs,
  upgrade_requests, push_subscriptions, notifications, forum_posts
  bevor der User-Row weg ist. Vorher: nur DELETE FROM users → Waisen in
  allen FK-Tabellen.
- profile.py delete_account: gleicher Cleanup-Set, vergisst jetzt
  upgrade_requests nicht mehr.
- admin.py Dashboard-Counter 'Zu Erledigen': JOIN users, damit
  verwaiste Anfragen nicht mehr im Header-Badge erscheinen (Liste
  selbst filtert sie schon korrekt via JOIN). Bump 1135→1136.
2026-05-30 17:51:24 +02:00
6bc63e3818 dog-profile: 'Später erinnern'-Button auf Standalone-Anlage-Seite
Wer aus dem Onboarding (Schritt 1 'Los geht's' navigiert direkt auf
#dog-profile) keinen Hund anlegen will, war bisher in der Form
festgehängt — kein Skip, kein Zurück.

Jetzt: ghost-Button unter dem Submit, setzt by_onboarding_done und
schickt zurück auf die Welten/Welcome. Bumpe auf 1135.
2026-05-30 17:38:12 +02:00
7a10db2da4 Bump APP_VER 1133 → 1134 (SW-Cache-Bust für Onboarding/Welcome-Fix) 2026-05-30 17:28:29 +02:00
b5b1510565 Release v1.6.0 2026-05-30 17:22:34 +02:00
fb9620fbcb Onboarding: 'Los geht's' navigiert direkt zum vollen Hunde-Profil-Formular statt Mini-Wizard. Welcome-'Hund anlegen' nutzt echten Event-Listener statt inline onclick. 2026-05-30 17:19:11 +02:00
2d43618dc8 Onboarding-Wizard: kein Stuck-State mehr bei vorhandenem Hund 2026-05-30 17:07:24 +02:00
0685f3b8ef Welcome-Dashboard: Tagesfoto wird serverseitig pro Tag gecacht
Neue Tabelle daily_photo_cache(dog_id, datum, photo_url) hält die Wahl
fest, sobald irgendein Client (PWA oder iOS) das erste Mal heute fragt.
Alle nachfolgenden Aufrufe liefern dieselbe URL — kein Kippen mehr, wenn
zwischendrin neue Fotos hochgeladen werden und die tick%len-Rotation auf
einen anderen Index zeigt. CREATE TABLE IF NOT EXISTS inline, daher kein
Migrations-Eingriff nötig.
2026-05-30 12:55:48 +02:00
9b0c286aa3 Expenses: GET /api/expenses/categories liefert die Kategorien
KATEGORIE_META als Dict mit id → {label, color} (preserved Order).
KATEGORIEN bleibt als Set für Validierung. Neuer Endpunkt liefert
[{id, label, color}, …] als Single Source of Truth für PWA und mobile
Clients — bisher war die Liste in JS und Python dupliziert.

Mobile-Client (banyaro-ios) holt die Liste jetzt dynamisch.
2026-05-30 12:31:59 +02:00
7b3041fc94 DRY: Notiz-Modal zentral in UI.noteModal (11 divergierte Kopien entfernt, ~750 Z. weniger); Fix: Founder-Race in jobs.py atomar + founder_number, SW v1133 2026-05-29 10:51:42 +02:00