diff --git a/.gitignore b/.gitignore index cbcf3ae..28e4c9f 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,3 @@ __pycache__/ /icons/ .claude/worktrees/ Ban Yaro - Google Play package/ -/unsplash/ diff --git a/MARKETING.md b/MARKETING.md deleted file mode 100644 index d232fe2..0000000 --- a/MARKETING.md +++ /dev/null @@ -1,72 +0,0 @@ -# đŸŸ Ban Yaro — Marketing-Cockpit - -**Single Source of Truth fĂŒrs Marketing.** Vor jeder Aktion hier prĂŒfen, danach updaten — so wird nichts doppelt gemacht, vergessen oder ĂŒbersehen. Pflege: RenĂ© + Claude. - -_Stand: 2026-06-03_ - -> Diese Datei = Planung & Checkliste. FĂŒr **Live-Daten** (User-Meilenstein, Kanal-Tracking) lohnt zusĂ€tzlich ein Marketing-Tab im **Admin-Bereich** — siehe „Ausbau" unten. - -## 📊 Kanal-Überblick -| Kanal / Bereich | Status | NĂ€chster Schritt | -|---|---|---| -| Flyer Print | 🟱 1000 gedruckt (03.06.) | lokal verteilen | -| Flyer Digital | 💡 Idee | Doppelseiten-PDF + Empfehlungs-QR | -| Lokal (Ebersberg) | ⬜ offen | TierĂ€rzte, Hundeschulen, FutterlĂ€den, Tierheim | -| Online-Communities | ⬜ offen | FB-Gruppen Landkreis EBE + nebenan.de | -| Empfehlung / Referral | 🟡 Infra da (`referral_code`) | Empfehlungs-QR + Tracking sichtbar machen | -| Influencer | 🟡 2 Runden (Mai), kaum Resonanz | Runde 3 erst ab ~50 aktiven Usern | -| Presse / Blogs | 🟡 1 Runde, kaum Resonanz | keine Massenwelle; Nische zuerst | -| Verzeichnisse / Listings | ⬜ offen | Product Hunt, PWA-Dirs, Google Business EBE | -| SEO / KI-Auffindbarkeit | 🟡 technisch optimiert | Backlinks (Blog-Testberichte) | -| Landing Page | 🟡 Redesign-Briefing da | 3 Einstiege, Outcomes statt Features | -| App Store (iOS) | 🟱 in Review (1.0 (3), 03.06.) | Freigabe abwarten | -| Play Store (Android) | 🔮 ON HOLD | 12 Closed-Tester / 14 Tage fehlen | -| Merch / NFC-Halsband | 💡 recherchiert | 20 Tags fĂŒr Beta (~33 €) | - -Legende: 🟱 lĂ€uft/erledigt · 🟡 angefangen · ⬜ offen · 💡 Idee · 🔮 blockiert - -## ⏳ Gates / Trigger (nicht zu frĂŒh starten) -- **Influencer & Presse Runde 3** erst ab **~50 aktiven Usern** — vorher zu frĂŒh (Großredaktionen fragen zuerst nach Zahlen). → Bei jeder Session aktuelle User-Zahl checken. -- iOS-App ist nativ gebaut & in Review — **ĂŒberholt** die alte „iOS erst ab 10k via Rork/PWABuilder"-Strategie. - -## 📋 Backlog (konkret als NĂ€chstes) -- [ ] **Flyer lokal verteilen (Ebersberg)** — TierĂ€rzte (Wartezimmer), Hundeschulen/Welpengruppen, FutterlĂ€den, Hundesalons, Tierheim, Hundewiesen-AushĂ€nge, hundefreundliche CafĂ©s. Persönlich erklĂ€ren; AufhĂ€nger: Giftköder-Radar + „Daten in Deutschland". **Lokal bĂŒndeln, nicht streuen** (Community-Dichte fĂŒr Gassi-Treffen/Giftköder). -- [ ] **Digitaler Doppelseiten-Flyer (PDF)** mit **Empfehlungs-QR** fĂŒr Online-/Gruppen-Verteilung. Quelle: `promotion/flyer_a5_*.html`. _Offene Frage: generischer `?ref=empfehlung`-Link vs. pro-User `referral_code`._ -- [ ] **Lokale FB-Gruppen + nebenan.de** — Flyer-Foto + Link posten. -- [ ] **Verzeichnisse** — Product Hunt, progressivewebappstore.com, pwafire.org/directory, Google Business (Ebersberg). -- [ ] **Landing-Page-Redesign** nach Briefing (3 Zielgruppen-Einstiege Hundebesitzer/ZĂŒchter/WelpenkĂ€ufer, Outcomes statt Features, ZĂŒchter-SaaS prominent, Datenschutz als Argument, GrĂŒnder-Story + Foto). -- [ ] **Messung einbauen** — „Wie hast du von uns gehört?" im Onboarding + QR-refs pro Kanal. - -## ✅ Erledigt -- [x] 1000 Flyer A5 (zweiseitig) gedruckt — 03.06.2026 -- [x] iOS-App nativ gebaut + eingereicht (1.0 (3), in Review) — Details im Repo `banyaro-ios` -- [x] Influencer-Outreach Runde 1 (5) + Runde 2 (13) — Mai 2026 -- [x] SEO-Grundlagen (llms.txt, Landing About-Section) - -## 📈 Messung — was bringt wirklich Nutzer? -- **Onboarding-Frage „Wie hast du von uns gehört?"** (1 Klick) = billigste & wichtigste Kontrolle. _(noch einzubauen)_ -- **QR-refs pro Ort/Kanal** (z. B. `banyaro.app/?ref=tierarzt-grafing`) → ab nĂ€chster Flyer-Charge. -- **`referral_code`** (in DB, `routes/auth.py`) → Empfehlungen zĂ€hlbar. -- Aktive User aktuell: _[aus Admin eintragen]_ - -## 🗂 Details je Kanal - -### Influencer -2 Runden im Mai gesendet (`partner@banyaro.app`; DKIM/SPF/DMARC aktiv), **kaum Resonanz** — zu frĂŒh (wenige User), teils falsche Adressen (z. B. GEO → richtig `chefredaktion@geo.de`). -**Runde 3:** keine Massenwelle ohne PR-Agentur; **Hundeschulen/-trainer zuerst** (kleines Netzwerk, empfehlen aktiv Tools, Trainingsfeature ist stark), persönliche Mails, AufhĂ€nger = neue Features + echte Nutzerzahlen. -→ **Wer schon kontaktiert wurde:** AI-Memory `project_influencer_outreach` (Runde 1: verpinscht, missyminzi, wanderlust_samoyed, viviundholly, doguniversity, dogstv; Runde 2: nami.and.tommy, brina.explores, heimatherzen, pfotentick, flummis_diary, verwolft, wildwildwilli, knutini_, ninja.vom.wolfstor, pupsonality, osman_theparson, babybearyuki, dogswiss). **Vor neuer Runde dort prĂŒfen.** - -### Play Store (Android TWA) -PWABuilder-Paket fertig (`Ban Yaro - Google Play package/`, Package `app.banyaro.twa`). **BLOCKER:** Google verlangt 12 Closed-Tester ĂŒber 14 Tage — Tester fehlen (Engpass, nicht die Technik). assetlinks.json + Play-Console-Eintrag stehen bereit. Nicht priorisieren bis Tester da. - -### Merch / NFC-Halsband -Tag recherchiert: **HID Laundry Tag 16 mm** (shopnfc, SKU RE-ICO2-16, ~1 €/Stk ab 500), fĂŒr `banyaro.app/hund/{id}`. Beta: 20 Stk (~33 €) an erste Nutzer. - -### Flyer -Print: A5 zweiseitig, Quelle `promotion/flyer_a5_allgemein.html` + `flyer_a5_rueckseite.html`, QR → banyaro.app. Vorderseite = alle Hundebesitzer, RĂŒckseite stark ZĂŒchter-fokussiert. - -## 🚀 Ausbau: Live-Tool im Admin-Bereich (optional) -Diese Datei deckt Planung/Checkliste ab (Claude pflegt sie). Der **Admin-Bereich** lohnt sich fĂŒr die Teile mit echten Daten: -- **User-Meilenstein-Anzeige** (aktive User) → blendet automatisch den „Outreach Runde 3"-Hinweis ein, sobald ~50 erreicht. -- **Kanal-Tracking**: Auswertung „Wie gehört?" + QR-ref-ZĂ€hler + `referral_code`-Statistik. -- Optional: das Kanal-Board (Status/Backlog) als editierbare Admin-Seite. diff --git a/VERSION b/VERSION index 0948691..41edc23 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1155 \ No newline at end of file +1141 \ No newline at end of file diff --git a/backend/database.py b/backend/database.py index ef3b9fa..08ac7db 100644 --- a/backend/database.py +++ b/backend/database.py @@ -356,18 +356,6 @@ def init_db(): ); CREATE INDEX IF NOT EXISTS idx_osm_pois_loc ON osm_pois(type, lat, lon); - -- OSM-Account-VerknĂŒpfung (OAuth2) je Nutzer — Basis fĂŒr OSM-BeitrĂ€ge - -- ("Hund war willkommen" → dog=yes) + spĂ€tere Gamification/Pro-Freischaltung. - -- access_token verschlĂŒsselt at rest (token_enc). - CREATE TABLE IF NOT EXISTS user_osm ( - user_id INTEGER PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE, - osm_uid INTEGER NOT NULL, - osm_name TEXT NOT NULL, - token_enc TEXT NOT NULL, - scopes TEXT, - linked_at TEXT NOT NULL DEFAULT (datetime('now')) - ); - -- VERLORENE HUNDE CREATE TABLE IF NOT EXISTS lost_dogs ( id INTEGER PRIMARY KEY AUTOINCREMENT, diff --git a/backend/main.py b/backend/main.py index 465231d..df5124d 100644 --- a/backend/main.py +++ b/backend/main.py @@ -227,7 +227,6 @@ from routes.walks import router as walks_router from routes.events import router as events_router from routes.sitting import router as sitting_router from routes.osm import router as osm_router -from routes.osm_auth import router as osm_auth_router from routes.forum import router as forum_router from routes.lost import router as lost_router from routes.knigge import router as knigge_router @@ -293,7 +292,6 @@ app.include_router(walks_router, prefix="/api/walks", tags=["Gassi-Tre app.include_router(events_router, prefix="/api/events", tags=["Events"]) app.include_router(sitting_router, prefix="/api/sitting", tags=["Sitting"]) app.include_router(osm_router, prefix="/api/osm", tags=["OSM"]) -app.include_router(osm_auth_router, prefix="/api/osm-auth", tags=["OSM-Auth"]) app.include_router(weather_router, prefix="/api/weather", tags=["Wetter"]) app.include_router(social_router, prefix="/api/social", tags=["Social"]) app.include_router(forum_router, prefix="/api/forum", tags=["Forum"]) @@ -513,11 +511,11 @@ async def sitemap(): urls = [ ("https://banyaro.app/", "weekly", "1.0"), ("https://banyaro.app/zuechter", "weekly", "0.9"), - ("https://banyaro.app/wurfboerse", "daily", "0.8"), + ("https://banyaro.app/info", "monthly", "0.8"), + ("https://banyaro.app/presse", "monthly", "0.7"), ("https://banyaro.app/wiki/rassen", "weekly", "0.8"), - ("https://banyaro.app/help", "monthly", "0.7"), ("https://banyaro.app/knigge", "monthly", "0.7"), - ("https://banyaro.app/partner", "monthly", "0.6"), + ("https://banyaro.app/wurfboerse", "daily", "0.8"), ] try: @@ -528,6 +526,12 @@ async def sitemap(): for r in rassen: urls.append((f"https://banyaro.app/wiki/rasse/{r['slug']}", "monthly", "0.7")) + events = conn.execute( + "SELECT id FROM events WHERE datum >= date('now') LIMIT 200" + ).fetchall() + for e in events: + urls.append((f"https://banyaro.app/api/events/{e['id']}", "weekly", "0.5")) + # Öffentliche ZĂŒchter-Profile breeders = conn.execute( "SELECT bp.zwingername FROM breeder_profiles bp " @@ -1344,47 +1348,12 @@ async def public_dog_page(dog_id: int): # ------------------------------------------------------------------ @app.get("/teilen/{token}") async def invite_page(token: str): - from fastapi.responses import HTMLResponse - with open(f"{STATIC_DIR}/index.html", encoding="utf-8") as _f: - _html = _f.read() - _html = _html.replace( - '', - '' - ) - return HTMLResponse(content=_html, headers={"Cache-Control": "no-store, no-cache"}) + return FileResponse(f"{STATIC_DIR}/index.html", headers={"Cache-Control": "no-store, no-cache"}) @app.get("/breeder/{zwingername}") async def breeder_profile_page(zwingername: str): - from fastapi.responses import HTMLResponse - from urllib.parse import unquote - from database import db as _db - import html as _html_mod - name = unquote(zwingername) - desc = f"HundezĂŒchter {_html_mod.escape(name)} auf Ban Yaro — Wurfbörse, Stammbaum und mehr." - try: - with _db() as conn: - bp = conn.execute( - "SELECT bp.rasse, bp.beschreibung FROM breeder_profiles bp " - "JOIN users u ON u.id = bp.user_id WHERE bp.zwingername=? AND u.rolle='breeder' LIMIT 1", - (name,) - ).fetchone() - if bp and bp["beschreibung"]: - desc = _html_mod.escape(bp["beschreibung"][:160]) - except Exception: - pass - with open(f"{STATIC_DIR}/index.html", encoding="utf-8") as _f: - _page = _f.read() - _page = _page.replace( - '', - f'' - ).replace( - 'Ban Yaro', - f'{_html_mod.escape(name)} — HundezĂŒchter auf Ban Yaro' - f'\n ' - f'\n ' - ) - return HTMLResponse(content=_page, headers={"Cache-Control": "no-store, no-cache"}) + return FileResponse(f"{STATIC_DIR}/index.html", headers={"Cache-Control": "no-store, no-cache"}) @app.get("/litters") @@ -1502,7 +1471,6 @@ async def ausweis_page(dog_id: int, request: Request): - Heimtierausweis – {esc(dog["name"])} -
@@ -1767,9 +1727,7 @@ async def help_page(): k for k in by_kat.keys() if k not in KAT_LABEL ] - import json as _json sections_html = "" - faq_items = [] for kat in kat_order: label = KAT_LABEL.get(kat, kat.replace("_", " ").title()) items = "".join( @@ -1778,13 +1736,6 @@ async def help_page(): for a in by_kat[kat] ) sections_html += f'

{_html.escape(label)}

{items}
' - for a in by_kat[kat]: - faq_items.append({ - "@type": "Question", - "name": a["frage"], - "acceptedAnswer": {"@type": "Answer", "text": a["antwort"]} - }) - faq_json_ld = _json.dumps(faq_items, ensure_ascii=False) html = f""" @@ -1793,8 +1744,6 @@ async def help_page(): Hilfe & FAQ — Ban Yaro - - - @@ -1907,8 +1851,6 @@ async def konto_loeschen(): Konto löschen — Ban Yaro - - - - -
-

Wurfbörse

-

Hundewelpen von geprĂŒften ZĂŒchtern

-
-
-

{count_text}

- {litters_html or '

Aktuell keine WĂŒrfe eingetragen.
Schau bald wieder vorbei!

'} - -
- -""" - return HTMLResponse(content=html, headers={"Cache-Control": "max-age=1800"}) - - # SPA Fallback — ALLE nicht-API-Routen gehen zur index.html @app.get("/{full_path:path}") async def spa_fallback(full_path: str): diff --git a/backend/requirements.txt b/backend/requirements.txt index d45b6f8..414ec32 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -7,7 +7,6 @@ pydantic[email]==2.10.6 bcrypt==4.3.0 PyJWT==2.10.1 httpx==0.28.1 -cryptography==44.0.0 openai==1.59.2 anthropic==0.49.0 pywebpush==2.0.0 diff --git a/backend/routes/forum.py b/backend/routes/forum.py index 33eb726..f8ee5e0 100644 --- a/backend/routes/forum.py +++ b/backend/routes/forum.py @@ -586,25 +586,6 @@ async def toggle_like(data: LikeBody, user=Depends(get_current_user)): return {"liked": liked, "count": count} -# ------------------------------------------------------------------ -# GET /api/forum/likes/{target_type}/{target_id} — Wer hat geliked? -# ------------------------------------------------------------------ -@router.get("/likes/{target_type}/{target_id}") -async def list_likers(target_type: str, target_id: int): - if target_type not in _LIKE_TABLE: - raise HTTPException(400, "UngĂŒltiger Typ.") - with db() as conn: - rows = conn.execute( - """SELECT u.name AS name, u.founder_number AS founder_number - FROM forum_likes fl - JOIN users u ON u.id = fl.user_id - WHERE fl.target_type = ? AND fl.target_id = ? - ORDER BY fl.id DESC""", - (target_type, target_id) - ).fetchall() - return [dict(r) for r in rows] - - # ------------------------------------------------------------------ # POST /api/forum/report # ------------------------------------------------------------------ diff --git a/backend/routes/osm.py b/backend/routes/osm.py index a6799de..5fc22b9 100644 --- a/backend/routes/osm.py +++ b/backend/routes/osm.py @@ -1,11 +1,6 @@ """ -BAN YARO — OSM POI-Daten + Community-Pins -Liest OSM-POIs aus der lokalen Tabelle osm_pois (monatlicher Offline-Import, -tools/osm-extract/), erlaubt Nutzern eigene Marker und Meldungen. - -Build 4: Live-Scannen gegen overpass-api.de ist DEAKTIVIERT (war Bann-Quelle). -Die Overpass-Hilfsfunktionen unten sind ungenutzt und können spĂ€ter entfernt -werden. /geocode nutzt weiterhin Nominatim fĂŒr die Adresssuche (geringe Last). +BAN YARO — OSM/Overpass POI-Cache + Community-Pins +Cacht OSM-Daten lokal, erlaubt Nutzern eigene Marker und Meldungen. """ import math @@ -196,9 +191,17 @@ async def get_pois( fetched_fresh = False if type in OSM_QUERIES: - # Scanner deaktiviert (Build 4): keine Live-Overpass-Abfragen mehr. - # POIs stammen aus dem monatlichen Offline-Import in die Tabelle - # osm_pois (tools/osm-extract/). Hier wird nur noch daraus gelesen. + tiles = _covering_tiles(south, west, north, east, CACHE_ZOOM) + stale = _stale_tiles(type, tiles) + + if stale and not fast: + async def _bg_fetch(poi_type, stale_tiles): + for (x, y) in stale_tiles: + await _fetch_and_store_tile(poi_type, x, y) + task = asyncio.create_task(_bg_fetch(type, stale)) + _bg_tasks.add(task) + task.add_done_callback(_bg_tasks.discard) + with db() as conn: reported = { row[0] for row in conn.execute( @@ -361,17 +364,24 @@ async def report_poi(body: ReportIn, user = Depends(get_current_user)): # ------------------------------------------------------------------ @router.post('/analyze') async def analyze_region( + background_tasks: BackgroundTasks, south: float = Query(...), west: float = Query(...), north: float = Query(...), east: float = Query(...), ): - # Scanner deaktiviert (Build 4): kein Live-Overpass-Warmup mehr. POIs - # kommen aus dem monatlichen Offline-Import (tools/osm-extract/). Endpoint - # bleibt als No-Op erhalten, damit bestehende Frontends nicht 404 laufen. - return {'status': 'offline-import', - 'message': 'POIs werden monatlich offline importiert — kein Live-Scan nötig.', - 'types': list(OSM_QUERIES.keys())} + tiles = _covering_tiles(south, west, north, east, CACHE_ZOOM) + + async def _warmup(): + tasks = [ + _fetch_and_store_tile(pt, x, y) + for pt in OSM_QUERIES + for (x, y) in _stale_tiles(pt, tiles) + ] + await asyncio.gather(*tasks) + + background_tasks.add_task(_warmup) + return {'status': 'gestartet', 'tiles': len(tiles), 'types': list(OSM_QUERIES.keys())} # ------------------------------------------------------------------ @@ -413,65 +423,3 @@ async def submit_poi_edit(osm_id: str, data: PoiEditCreate, poi[data.field], data.new_value.strip(), user["id"]) ) return {"status": "pending", "message": "Korrektur wurde zur PrĂŒfung eingereicht."} - - -# ------------------------------------------------------------------ -# Geocoding-Proxy GET /api/osm/geocode?q=
 -# Nominatim-Rate-Limit: 1 req/s — serverseitig throttled -# ------------------------------------------------------------------ -_nominatim_sem = asyncio.Semaphore(1) -_nominatim_last = 0.0 - -@router.get('/geocode') -async def geocode_search(q: str = Query(..., min_length=2, max_length=200)): - import time - global _nominatim_last - async with _nominatim_sem: - wait = 1.1 - (time.monotonic() - _nominatim_last) - if wait > 0: - await asyncio.sleep(wait) - _nominatim_last = time.monotonic() - try: - async with httpx.AsyncClient(timeout=6.0) as client: - resp = await client.get( - 'https://nominatim.openstreetmap.org/search', - params={ - 'q': q, - 'format': 'jsonv2', - 'limit': 6, - 'countrycodes': 'de,at,ch', - 'addressdetails': 1, - 'accept-language': 'de', - }, - headers={ - 'User-Agent': _OVERPASS_UA, - 'Referer': 'https://banyaro.app/', - } - ) - resp.raise_for_status() - data = resp.json() - except Exception as e: - logger.warning("Nominatim-Fehler: %s", e) - raise HTTPException(502, "Geocoding nicht verfĂŒgbar") - - out = [] - for r in data[:6]: - addr = r.get('address', {}) - short = ( - addr.get('amenity') or addr.get('shop') or addr.get('leisure') or - addr.get('road') or addr.get('village') or addr.get('town') or - addr.get('city') or r.get('name') or - r.get('display_name', '').split(',')[0] - ) - city = addr.get('city') or addr.get('town') or addr.get('village') or addr.get('municipality') or '' - state = addr.get('state', '') - subtitle = ', '.join(filter(None, [city, state])) - out.append({ - 'lat': float(r['lat']), - 'lon': float(r['lon']), - 'name': short, - 'subtitle': subtitle, - 'full': r.get('display_name', ''), - 'type': r.get('type', ''), - }) - return out diff --git a/backend/routes/osm_auth.py b/backend/routes/osm_auth.py deleted file mode 100644 index 0de97b8..0000000 --- a/backend/routes/osm_auth.py +++ /dev/null @@ -1,167 +0,0 @@ -""" -OSM-Account-VerknĂŒpfung via OAuth2 (Modell A: BeitrĂ€ge laufen unter dem -eigenen OSM-Account des Nutzers). Basis fĂŒrs spĂ€tere "Hund war willkommen" -(dog=yes) + Gamification/Pro-Freischaltung. - -Flow: - 1. Frontend ruft (eingeloggt) GET /api/osm-auth/authorize → bekommt die - OSM-Authorize-URL inkl. signiertem `state` (trĂ€gt die banyaro-user_id + - CSRF-Nonce, 10 Min gĂŒltig) und leitet den Browser dorthin. - 2. OSM leitet zurĂŒck auf GET /api/osm-auth/callback?code=&state= (ohne JWT — - daher die user_id aus `state`). Token-Tausch, OSM-Name holen, Token - verschlĂŒsselt in user_osm speichern, zurĂŒck in die App leiten. - 3. GET /status zeigt VerknĂŒpfungsstatus, POST /unlink trennt. - -ENV: OSM_CLIENT_ID, OSM_CLIENT_SECRET, OSM_REDIRECT_URI, OSM_POST_LINK_REDIRECT. -Token-SchlĂŒssel wird aus JWT_SECRET abgeleitet (oder OSM_TOKEN_KEY ĂŒberschreibt). -""" -import os -import base64 -import hashlib -import logging -from urllib.parse import urlencode -from datetime import datetime, timezone, timedelta - -import jwt -import httpx -from fastapi import APIRouter, Depends, HTTPException, Query -from fastapi.responses import RedirectResponse -from cryptography.fernet import Fernet, InvalidToken - -from database import db -from auth import get_current_user, JWT_SECRET, JWT_ALGO - -logger = logging.getLogger(__name__) -router = APIRouter() - -# --- OSM-OAuth2-Endpunkte --- -OSM_AUTHORIZE = "https://www.openstreetmap.org/oauth2/authorize" -OSM_TOKEN = "https://www.openstreetmap.org/oauth2/token" -OSM_USER_API = "https://api.openstreetmap.org/api/0.6/user/details.json" -OSM_SCOPES = "read_prefs write_api" - -CLIENT_ID = os.getenv("OSM_CLIENT_ID", "") -CLIENT_SECRET = os.getenv("OSM_CLIENT_SECRET", "") -REDIRECT_URI = os.getenv("OSM_REDIRECT_URI", "https://staging.banyaro.app/api/osm-auth/callback") -POST_LINK_REDIRECT = os.getenv("OSM_POST_LINK_REDIRECT", "/?osm=verknuepft") - -_STATE_TTL_MIN = 10 - -# Fernet-SchlĂŒssel zur Token-VerschlĂŒsselung: dediziertes OSM_TOKEN_KEY oder -# deterministisch aus JWT_SECRET abgeleitet (kein zusĂ€tzliches Secret nötig). -def _fernet() -> Fernet: - raw = os.getenv("OSM_TOKEN_KEY") - if raw: - return Fernet(raw.encode() if isinstance(raw, str) else raw) - key = base64.urlsafe_b64encode(hashlib.sha256(JWT_SECRET.encode()).digest()) - return Fernet(key) - -def _encrypt(token: str) -> str: - return _fernet().encrypt(token.encode()).decode() - -def _decrypt(token_enc: str) -> str: - return _fernet().decrypt(token_enc.encode()).decode() - - -# ------------------------------------------------------------------ -# GET /authorize — liefert die OSM-Authorize-URL (Frontend redirectet dorthin) -# ------------------------------------------------------------------ -@router.get("/authorize") -async def authorize(user=Depends(get_current_user)): - if not CLIENT_ID: - raise HTTPException(503, "OSM-Anbindung nicht konfiguriert (OSM_CLIENT_ID fehlt).") - state = jwt.encode( - {"uid": user["id"], - "exp": datetime.now(timezone.utc) + timedelta(minutes=_STATE_TTL_MIN), - "purpose": "osm-link"}, - JWT_SECRET, algorithm=JWT_ALGO, - ) - params = { - "response_type": "code", - "client_id": CLIENT_ID, - "redirect_uri": REDIRECT_URI, - "scope": OSM_SCOPES, - "state": state, - } - url = OSM_AUTHORIZE + "?" + urlencode(params) - return {"authorize_url": url} - - -# ------------------------------------------------------------------ -# GET /callback — OSM leitet hierher zurĂŒck (Browser-Redirect, kein JWT) -# ------------------------------------------------------------------ -@router.get("/callback") -async def callback(code: str = Query(...), state: str = Query(...)): - # 1) state verifizieren → banyaro-user_id (CSRF + Zuordnung) - try: - payload = jwt.decode(state, JWT_SECRET, algorithms=[JWT_ALGO]) - if payload.get("purpose") != "osm-link": - raise ValueError("falscher state-Zweck") - uid = int(payload["uid"]) - except Exception: - raise HTTPException(400, "UngĂŒltiger oder abgelaufener VerknĂŒpfungs-Link.") - - # 2) code → access_token tauschen - async with httpx.AsyncClient(timeout=15) as client: - tok = await client.post(OSM_TOKEN, data={ - "grant_type": "authorization_code", - "code": code, - "redirect_uri": REDIRECT_URI, - "client_id": CLIENT_ID, - "client_secret": CLIENT_SECRET, - }) - if tok.status_code != 200: - logger.warning("OSM-Token-Tausch fehlgeschlagen: %s %s", tok.status_code, tok.text[:200]) - raise HTTPException(502, "OSM-Token-Tausch fehlgeschlagen.") - access_token = tok.json().get("access_token") - if not access_token: - raise HTTPException(502, "OSM lieferte kein access_token.") - - # 3) OSM-IdentitĂ€t holen (uid + Anzeigename) - me = await client.get(OSM_USER_API, headers={"Authorization": f"Bearer {access_token}"}) - if me.status_code != 200: - raise HTTPException(502, "OSM-Nutzerdaten konnten nicht geladen werden.") - u = me.json().get("user", {}) - osm_uid, osm_name = u.get("id"), u.get("display_name") - if not (osm_uid and osm_name): - raise HTTPException(502, "OSM-Nutzerdaten unvollstĂ€ndig.") - - # 4) verschlĂŒsselt speichern (eine VerknĂŒpfung pro banyaro-User) - with db() as conn: - conn.execute( - """INSERT INTO user_osm (user_id, osm_uid, osm_name, token_enc, scopes, linked_at) - VALUES (?, ?, ?, ?, ?, datetime('now')) - ON CONFLICT(user_id) DO UPDATE SET - osm_uid=excluded.osm_uid, osm_name=excluded.osm_name, - token_enc=excluded.token_enc, scopes=excluded.scopes, - linked_at=excluded.linked_at""", - (uid, osm_uid, osm_name, _encrypt(access_token), OSM_SCOPES), - ) - logger.info("OSM verknĂŒpft: banyaro-user %s ↔ OSM '%s' (%s)", uid, osm_name, osm_uid) - return RedirectResponse(POST_LINK_REDIRECT, status_code=302) - - -# ------------------------------------------------------------------ -# GET /status — VerknĂŒpfungsstatus des eingeloggten Nutzers -# ------------------------------------------------------------------ -@router.get("/status") -async def status(user=Depends(get_current_user)): - with db() as conn: - row = conn.execute( - "SELECT osm_name, osm_uid, linked_at FROM user_osm WHERE user_id=?", - (user["id"],) - ).fetchone() - if not row: - return {"linked": False} - return {"linked": True, "osm_name": row["osm_name"], - "osm_uid": row["osm_uid"], "linked_at": row["linked_at"]} - - -# ------------------------------------------------------------------ -# POST /unlink — VerknĂŒpfung trennen (Token lokal löschen) -# ------------------------------------------------------------------ -@router.post("/unlink") -async def unlink(user=Depends(get_current_user)): - with db() as conn: - conn.execute("DELETE FROM user_osm WHERE user_id=?", (user["id"],)) - return {"status": "ok"} diff --git a/backend/static/css/components.css b/backend/static/css/components.css index 1f22ba8..1f724ec 100644 --- a/backend/static/css/components.css +++ b/backend/static/css/components.css @@ -3256,182 +3256,6 @@ html.modal-open { } } -/* Orts-Suche — Panel schiebt von oben rein wenn aktiv */ -.map-search-wrap { - position: absolute; - top: 0; - left: 0; - right: 0; - z-index: 1002; - padding: 8px 12px 4px; - background: rgba(255,255,255,0.97); - backdrop-filter: blur(8px); - box-shadow: 0 3px 14px rgba(0,0,0,0.18); - transform: translateY(-110%); - transition: transform 0.22s ease; - pointer-events: none; -} -.map-search-wrap.active { - transform: translateY(0); - pointer-events: auto; -} -:root[data-theme="dark"] .map-search-wrap { background: rgba(22,22,24,0.97); } -.map-search-row { - display: flex; - align-items: center; - gap: 8px; - background: var(--c-bg, #fff); - border-radius: var(--radius-full); - border: 1px solid var(--c-border, #e5e7eb); - padding: 8px 12px; -} -.map-search-input { - flex: 1; - border: none; - outline: none; - font-size: 15px; - font-family: inherit; - background: transparent; - color: var(--c-text); - min-width: 0; -} -.map-search-input::placeholder { color: #aaa; } -.map-search-clear { - background: none; - border: none; - padding: 4px; - cursor: pointer; - color: #999; - line-height: 1; - flex-shrink: 0; - border-radius: 50%; -} -.map-search-clear:hover { color: var(--c-text); background: var(--c-bg-subtle); } -.map-search-results { - background: var(--c-bg, #fff); - border-radius: 12px; - border: 1px solid var(--c-border, #e5e7eb); - margin-top: 6px; - margin-bottom: 4px; - overflow: hidden; - max-height: 240px; - overflow-y: auto; -} -.map-search-item { - padding: 10px 14px; - cursor: pointer; - border-bottom: 1px solid var(--c-border-light, rgba(0,0,0,0.05)); -} -.map-search-item:last-child { border-bottom: none; } -.map-search-item:hover, -.map-search-item:active { background: var(--c-primary-subtle, #fef3c7); } -.map-search-item-name { - font-size: 13px; - font-weight: 600; - color: var(--c-text); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} -.map-search-item-sub { - font-size: 11px; - color: var(--c-text-secondary); - margin-top: 1px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} -.map-search-loading, -.map-search-empty { - padding: 12px 14px; - font-size: 13px; - color: var(--c-text-secondary); - text-align: center; -} - -/* Speed Dial — Ein Trigger-Button, Sub-Buttons fĂ€chern nach oben auf */ -.map-speed-dial { - position: absolute; - bottom: calc(var(--safe-bottom) + 82px); - right: 20px; - z-index: 1000; - display: flex; - flex-direction: column; - align-items: flex-end; - gap: var(--space-2); -} -.map-sd-items { - display: flex; - flex-direction: column-reverse; /* unterste Item = erstes im DOM */ - align-items: flex-end; - gap: var(--space-2); - pointer-events: none; -} -.map-speed-dial.open .map-sd-items { pointer-events: auto; } - -.map-sd-item { - display: flex; - align-items: center; - gap: 10px; - opacity: 0; - transform: translateY(8px) scale(0.88); - transition: opacity 0.16s ease, transform 0.16s ease; -} -.map-speed-dial.open .map-sd-item { opacity: 1; transform: translateY(0) scale(1); } -.map-speed-dial.open .map-sd-item:nth-child(1) { transition-delay: 0ms; } -.map-speed-dial.open .map-sd-item:nth-child(2) { transition-delay: 50ms; } -.map-speed-dial.open .map-sd-item:nth-child(3) { transition-delay: 100ms; } -.map-speed-dial.open .map-sd-item:nth-child(4) { transition-delay: 150ms; } -.map-speed-dial.open .map-sd-item:nth-child(5) { transition-delay: 200ms; } - -.map-sd-label { - background: rgba(20,20,20,0.72); - color: #fff; - font-size: 12px; - font-weight: 600; - padding: 5px 11px; - border-radius: var(--radius-full); - white-space: nowrap; - backdrop-filter: blur(4px); - pointer-events: none; - letter-spacing: 0.01em; -} -.map-sd-btn { - width: 46px; - height: 46px; - border-radius: 50%; - background: #fff; - color: #C4843A; - border: 2px solid rgba(196,132,58,0.25); - box-shadow: 0 2px 8px rgba(0,0,0,0.22); - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - font-size: 1.1rem; - flex-shrink: 0; - transition: background 0.12s, color 0.12s; - -webkit-tap-highlight-color: transparent; -} -.map-sd-btn:hover, -.map-sd-btn:active { background: #fef3c7; } -.map-sd-btn.active { background: #C4843A; color: #fff; border-color: #C4843A; } -.map-sd-btn.map-fab--pin.active { background: var(--c-danger); border-color: var(--c-danger); color: #fff; } -#map-radar-btn.active { background: #1d4ed8; color: #fff; border-color: #1d4ed8; } -#map-temp-btn.active { background: #dc2626; color: #fff; border-color: #dc2626; } - -.map-sd-trigger { - transition: background 0.15s, transform 0.2s ease; -} -.map-speed-dial.open .map-sd-trigger { - background: #6b4a20; - transform: rotate(90deg); -} -.map-sd-icon-open { display: block; } -.map-sd-icon-close { display: none; } -.map-speed-dial.open .map-sd-icon-open { display: none; } -.map-speed-dial.open .map-sd-icon-close { display: block; } - /* FAB-Gruppe rechts unten — direkt ĂŒber dem ZurĂŒck-Button */ .map-fabs { position: absolute; diff --git a/backend/static/index.html b/backend/static/index.html index da37ccd..a85a5a8 100644 --- a/backend/static/index.html +++ b/backend/static/index.html @@ -86,14 +86,14 @@ Ban Yaro - + - - - - - + + + + + @@ -617,11 +617,11 @@ - - - - - + + + + + @@ -631,7 +631,7 @@ - + diff --git a/backend/static/js/api.js b/backend/static/js/api.js index 7fd420e..de39835 100644 --- a/backend/static/js/api.js +++ b/backend/static/js/api.js @@ -45,12 +45,9 @@ const API = (() => { throw new APIError(msg, 0, 'network'); } - // Versions-Check: Server meldet neue Version → beim nĂ€chsten navigate() aktualisieren. - // Ausnahme: _BY_SW_RELOAD = wir sind gerade von /force-update weitergeleitet worden. - // In dem Fall ist APP_VER kurzzeitig veraltet (SW-Cache lĂ€uft noch aus) — KEIN erneuter - // Pending setzen, sonst entsteht sofort ein Loop beim nĂ€chsten Seitenwechsel. + // Versions-Check: Server meldet neue Version → Banner anzeigen (einmalig) const serverVer = response.headers.get('x-app-version'); - if (serverVer && serverVer !== APP_VER && !window._byUpdatePending && !window._BY_SW_RELOAD) { + if (serverVer && serverVer !== APP_VER && !window._byUpdatePending) { window._byUpdatePending = true; window._byNewVersion = serverVer; } @@ -442,9 +439,6 @@ const API = (() => { like(targetType, targetId) { return post('/forum/like', { target_type: targetType, target_id: targetId }); }, - likers(targetType, targetId) { - return get(`/forum/likes/${targetType}/${targetId}`); - }, report(targetType, targetId, grund) { return post('/forum/report', { target_type: targetType, target_id: targetId, grund }); }, diff --git a/backend/static/js/app.js b/backend/static/js/app.js index ce91fdf..187a0e1 100644 --- a/backend/static/js/app.js +++ b/backend/static/js/app.js @@ -3,7 +3,7 @@ Router, State-Management, Navigation, Initialisierung. ============================================================ */ -const APP_VER = '1155'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '1141'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen const APP_VERSION = '1.6.0'; // ← semantische Version, wird bei make release gesetzt window.APP_VER = APP_VER; // global verfĂŒgbar fĂŒr andere Module (z.B. offline-indicator) window.APP_VERSION = APP_VERSION; diff --git a/backend/static/js/boot.js b/backend/static/js/boot.js index d1ef297..9f54463 100644 --- a/backend/static/js/boot.js +++ b/backend/static/js/boot.js @@ -57,10 +57,7 @@ if ('serviceWorker' in navigator) { if (!sw) return; sw.addEventListener('statechange', function() { if (sw.state === 'activated') { - if (sessionStorage.getItem('by_skip_sw_reload')) { - sessionStorage.removeItem('by_skip_sw_reload'); // einmalig konsumieren - return; - } + if (sessionStorage.getItem('by_skip_sw_reload')) return; window.location.replace('/?_t=' + Date.now()); } }); diff --git a/backend/static/js/pages/diary.js b/backend/static/js/pages/diary.js index 686007c..653b117 100644 --- a/backend/static/js/pages/diary.js +++ b/backend/static/js/pages/diary.js @@ -1306,7 +1306,37 @@ window.Page_diary = (() => {
-
+ + +
+
+ +
+ + +
+
+
+ + ${UI.escape(entry?.location_name || '')} + +
+
+
+ + +
+ +
${dogPickerHtml}
@@ -1508,15 +1538,140 @@ window.Page_diary = (() => { let _locLat = (entry?.gps_lat != null) ? entry.gps_lat : null; let _locLon = (entry?.gps_lon != null) ? entry.gps_lon : null; let _locName = entry?.location_name || null; + let _miniMap = null, _miniMarker = null; - // Location Picker (gemeinsame UI-Komponente) - setTimeout(() => { - const _diaryPicker = UI.locationPicker({ - containerId: 'diary-location-picker', - onSelect: (lat, lon, name) => { _locLat = lat; _locLon = lon; _locName = name; }, + const _pinSvg = ''; + const _mkIcon = () => L.divIcon({ html: _pinSvg, className: '', iconSize: [32,40], iconAnchor: [16,40] }); + + function _setName(name) { + _locName = name; + document.getElementById('diary-location-label').textContent = name; + document.getElementById('diary-location-chip-wrap').style.display = ''; + document.getElementById('diary-location-suggestions').style.display = 'none'; + } + + function _placeMarker(lat, lon) { + if (_miniMarker) { _miniMarker.setLatLng([lat, lon]); return; } + _miniMarker = L.marker([lat, lon], { draggable: false, icon: _mkIcon() }).addTo(_miniMap); + _miniMarker.on('dragend', () => { + const p = _miniMarker.getLatLng(); _locLat = p.lat; _locLon = p.lng; + document.getElementById('diary-location-btn-label').textContent = 'POI suchen'; }); - if (_locLat != null) _diaryPicker.setValue(_locLat, _locLon, _locName); - }, 50); + } + + document.getElementById('diary-location-clear')?.addEventListener('click', () => { + _locName = null; + document.getElementById('diary-location-chip-wrap').style.display = 'none'; + }); + const _clearBtn = document.getElementById('diary-coords-clear'); + let _clearPending = false; + _clearBtn?.addEventListener('click', () => { + if (!_clearPending) { + _clearPending = true; + _clearBtn.textContent = 'Wirklich entfernen?'; + _clearBtn.style.color = 'var(--c-danger)'; + setTimeout(() => { + if (_clearPending) { + _clearPending = false; + _clearBtn.textContent = 'Ort entfernen'; + _clearBtn.style.color = 'var(--c-text-muted)'; + } + }, 3000); + return; + } + _clearPending = false; + _clearBtn.textContent = 'Ort entfernen'; + _clearBtn.style.color = 'var(--c-text-muted)'; + _locLat = null; _locLon = null; _locName = null; + document.getElementById('diary-location-chip-wrap').style.display = 'none'; + document.getElementById('diary-location-suggestions').style.display = 'none'; + document.getElementById('diary-location-btn-label').textContent = 'POI suchen'; + if (_miniMarker) { _miniMarker.remove(); _miniMarker = null; } + if (_miniMap) { _miniMap.setView([48.0, 11.9], 7); _setMapEditing(false); } + }); + + let _mapEditing = false; + + function _setMapEditing(on) { + _mapEditing = on; + const lbl = document.getElementById('diary-map-edit-label'); + if (lbl) lbl.textContent = on ? 'Fertig' : 'Position Àndern'; + if (!_miniMap) return; + if (on) { + if (_miniMarker) _miniMarker.dragging.enable(); + } else { + if (_miniMarker) _miniMarker.dragging.disable(); + } + } + + document.getElementById('diary-map-edit-btn')?.addEventListener('click', () => { + _setMapEditing(!_mapEditing); + }); + + // Karte beim Formular-Open automatisch laden + UI.loadLeaflet().then(() => { + setTimeout(() => { + const lat = _locLat || 48.0, lon = _locLon || 11.9, zoom = _locLat ? 15 : 7; + _miniMap = L.map('diary-map-wrap', { + zoomControl: true, attributionControl: false, + dragging: true, scrollWheelZoom: false, + }).setView([lat, lon], zoom); + L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19 }) + .addTo(_miniMap); + _miniMap.invalidateSize(); + if (_locLat) { + _placeMarker(lat, lon); + _miniMarker.dragging.disable(); // Lesemodus: kein Drag + } + // Klick nur im Edit-Modus + _miniMap.on('click', e => { + if (!_mapEditing) return; + _locLat = e.latlng.lat; _locLon = e.latlng.lng; + _placeMarker(_locLat, _locLon); + if (!_mapEditing) _miniMarker.dragging.disable(); + document.getElementById('diary-location-btn-label').textContent = 'POI suchen'; + }); + }, 150); + }); + + async function _showSuggestions() { + const btn = document.getElementById('diary-location-btn'); + UI.setLoading(btn, true); + try { + let lat = _locLat, lon = _locLon; + if (lat == null || lon == null) { + const pos = await API.getLocation(); + lat = pos.lat; lon = pos.lon; + _locLat = lat; _locLon = lon; + if (_miniMap) { _miniMap.setView([lat, lon], 15); _placeMarker(lat, lon); } + document.getElementById('diary-location-btn-label').textContent = 'POI suchen'; + } + const suggestions = await API.diary.nearby(_appState.activeDog.id, lat, lon); + const sugEl = document.getElementById('diary-location-suggestions'); + if (suggestions.length === 0) { + sugEl.innerHTML = '

Keine Orte in der NĂ€he gefunden.

'; + } else { + sugEl.innerHTML = suggestions.map(s => ` + `).join(''); + sugEl.querySelectorAll('.diary-location-suggestion').forEach(el => { + el.addEventListener('click', () => _setName(el.dataset.name)); + }); + } + sugEl.style.display = ''; + } catch (err) { + UI.toast.error(err?.message?.includes('GPS') || lat == null + ? 'GPS nicht verfĂŒgbar.' : 'Ortssuche fehlgeschlagen.'); + } finally { + UI.setLoading(btn, false); + } + } + + document.getElementById('diary-location-btn')?.addEventListener('click', _showSuggestions); document.getElementById('diary-form-delete')?.addEventListener('click', async () => { const ok = await UI.modal.confirm({ diff --git a/backend/static/js/pages/forum.js b/backend/static/js/pages/forum.js index 512ed6d..e2dfe19 100644 --- a/backend/static/js/pages/forum.js +++ b/backend/static/js/pages/forum.js @@ -640,17 +640,6 @@ function _fmtDate(iso) { } catch (err) { UI.toast.error(err.message); } }); - // Liker-Liste anzeigen (Klick auf die Zahl) - const _thLikeCount = document.getElementById('thread-like-count'); - if (_thLikeCount) { - _thLikeCount.style.cursor = 'pointer'; - _thLikeCount.title = 'Wer hat geliked?'; - _thLikeCount.addEventListener('click', e => { - e.stopPropagation(); - if ((thread.likes || 0) > 0) _showLikers('thread', thread.id); - }); - } - // Report thread document.getElementById('thread-report-btn')?.addEventListener('click', () => { _showReportForm('thread', thread.id); @@ -823,9 +812,9 @@ function _fmtDate(iso) { // Like container.querySelectorAll('.forum-post-like:not([data-bound])').forEach(btn => { btn.dataset.bound = '1'; - const postId = parseInt(btn.dataset.postId); btn.addEventListener('click', async () => { if (!uid) { UI.toast.info('Bitte erst anmelden.'); return; } + const postId = parseInt(btn.dataset.postId); try { const res = await API.forum.like('post', postId); btn.classList.toggle('active', res.liked); @@ -833,16 +822,6 @@ function _fmtDate(iso) { if (countEl) countEl.textContent = res.count; } catch (err) { UI.toast.error(err.message); } }); - // Klick auf die Zahl → Liker-Liste - const countEl = btn.querySelector('.forum-post-like-count'); - if (countEl) { - countEl.style.cursor = 'pointer'; - countEl.title = 'Wer hat geliked?'; - countEl.addEventListener('click', e => { - e.stopPropagation(); - if (parseInt(countEl.textContent) > 0) _showLikers('post', postId); - }); - } }); // Report @@ -895,28 +874,6 @@ function _fmtDate(iso) { }); } - // ---------------------------------------------------------- - // Liker-Liste — wer hat geliked? - // ---------------------------------------------------------- - async function _showLikers(targetType, targetId) { - try { - const likers = await API.forum.likers(targetType, targetId); - if (!likers.length) { UI.toast.info('Noch keine Likes.'); return; } - const rows = likers.map(l => ` -
-
${UI.escape(_initial(l.name))}
- ${UI.escape(l.name || 'Unbekannt')} - ${l.founder_number ? `GrĂŒnder #${l.founder_number}` : ''} -
`).join(''); - UI.modal.open({ - title: `${UI.icon('heart')} ${likers.length} ${likers.length === 1 ? 'Like' : 'Likes'}`, - body: `
${rows}
`, - footer: ``, - }); - document.getElementById('likers-close')?.addEventListener('click', UI.modal.close); - } catch (err) { UI.toast.error(err.message); } - } - // ---------------------------------------------------------- // Report-Formular // ---------------------------------------------------------- diff --git a/backend/static/js/pages/map.js b/backend/static/js/pages/map.js index 00a4324..7b70409 100644 --- a/backend/static/js/pages/map.js +++ b/backend/static/js/pages/map.js @@ -59,7 +59,6 @@ window.Page_map = (() => { treffpunkt: [], community: [], zuechter: [], - hotel: [], }; const VISIBLE_KEY = 'by_map_visible_v1'; @@ -131,10 +130,6 @@ window.Page_map = (() => { interactive: false, }; - // Orts-Suche - let _searchTimer = null; - let _searchMarker = null; - let _overpassTimer = null; let _overpassActive = false; let _ringClosing = false; @@ -215,50 +210,13 @@ window.Page_map = (() => {
- -
-
- - - -
- -
- - -
-
- -
- Mein Standort - -
-
- Ort suchen - -
-
- Marker setzen - -
- ${App.hasPro(_appState?.user) ? ` -
- Regenradar - -
-
- Temperatur - -
- ` : ''} -
- +
+ + ${App.hasPro(_appState?.user) ? ` + + + ` : ''} +
@@ -331,19 +289,7 @@ window.Page_map = (() => { _saveVisible(); }); - // Speed Dial - const _sdEl = document.getElementById('map-speed-dial'); - document.getElementById('map-sd-trigger')?.addEventListener('click', e => { - e.stopPropagation(); - _sdEl?.classList.toggle('open'); - }); - // Klick auf Karte / außerhalb schließt Speed Dial - document.getElementById('central-map')?.addEventListener('pointerdown', () => { - _sdEl?.classList.remove('open'); - }); - document.getElementById('map-locate-btn').addEventListener('click', () => { - _sdEl?.classList.remove('open'); if (_userPos) { _map?.setView([_userPos.lat, _userPos.lon], 16); } else { @@ -351,54 +297,9 @@ window.Page_map = (() => { } }); - document.getElementById('map-pin-btn').addEventListener('click', () => { - _sdEl?.classList.remove('open'); - _togglePlacementMode(); - }); - document.getElementById('map-radar-btn')?.addEventListener('click', () => { - _sdEl?.classList.remove('open'); - _toggleRadar(); - }); - document.getElementById('map-temp-btn')?.addEventListener('click', () => { - _sdEl?.classList.remove('open'); - _toggleTemp(); - }); - - // Suche — FAB öffnet Panel - document.getElementById('map-search-btn')?.addEventListener('click', () => { - document.getElementById('map-speed-dial')?.classList.remove('open'); - const wrap = document.getElementById('map-search-wrap'); - const isOpen = wrap?.classList.contains('active'); - if (isOpen) { - _clearSearch(); - } else { - wrap?.classList.add('active'); - setTimeout(() => document.getElementById('map-search-input')?.focus(), 60); - document.getElementById('map-search-btn')?.classList.add('active'); - } - }); - - const searchInput = document.getElementById('map-search-input'); - const searchResults = document.getElementById('map-search-results'); - - searchInput?.addEventListener('input', () => { - const q = searchInput.value.trim(); - clearTimeout(_searchTimer); - if (q.length < 2) { searchResults.style.display = 'none'; return; } - _searchTimer = setTimeout(() => _runSearch(q), 400); - }); - - searchInput?.addEventListener('keydown', e => { - if (e.key === 'Escape') _clearSearch(); - }); - - document.getElementById('map-search-clear')?.addEventListener('click', _clearSearch); - - // Klick auf Karte schließt Ergebnisse (aber behĂ€lt Marker) - document.getElementById('central-map')?.addEventListener('pointerdown', () => { - searchResults.style.display = 'none'; - searchInput?.blur(); - }); + document.getElementById('map-pin-btn').addEventListener('click', _togglePlacementMode); + document.getElementById('map-radar-btn')?.addEventListener('click', _toggleRadar); + document.getElementById('map-temp-btn')?.addEventListener('click', _toggleTemp); } // ---------------------------------------------------------- @@ -1006,7 +907,7 @@ window.Page_map = (() => { const params = new URLSearchParams({ type: osmType, ...bbox }); try { const pois = await fetch(`/api/osm/pois?${params}`).then(r => r.json()); - const osmCount = (_layers[layerKey] || []).filter(m => !m._ownPlace).length; + const osmCount = _layers[layerKey].filter(m => !m._ownPlace).length; if (pois.length !== osmCount) _replaceOsmMarkers(layerKey, pois); _done++; const pct = Math.round(20 + _done / _total * 80); @@ -1018,14 +919,11 @@ window.Page_map = (() => { const pct = Math.round(20 + _done / _total * 80); const total = Object.values(_layers).flat().filter(m => !m._ownPlace).length; _setOsmStatus(pct < 100 ? `Scanne
` : `${total} Marker`, pct); - return (_layers[layerKey] || []).filter(m => !m._ownPlace).length; + return _layers[layerKey].filter(m => !m._ownPlace).length; } }); - try { - await Promise.all(freshTasks); - } finally { - _overpassActive = false; - } + await Promise.all(freshTasks); + _overpassActive = false; const totalLoaded = Object.values(_layers).flat().filter(m => !m._ownPlace).length; const allHidden = Object.keys(OSM_LAYER_MAP).every(k => _visible[k] === false); @@ -1033,13 +931,10 @@ window.Page_map = (() => { _setOsmStatus('Layer deaktiviert — Liste antippen', 100); } - // Wenn 0 OSM-Marker: Hintergrund-Overpass-Fetch lĂ€uft noch — bis zu 8× nachfragen - // Overpass fĂŒr alle Layer sequential: bis zu ~4min → Retries mĂŒssen das abdecken - if (totalLoaded === 0 && zoom >= 14 && _autoRetryCount < 8) { + // Wenn 0 OSM-Marker: Hintergrund-Fetch lĂ€uft noch — max 3× automatisch nachfragen + if (totalLoaded === 0 && zoom >= 14 && _autoRetryCount < 3) { _autoRetryCount++; - // 10s, 20s, 35s, 50s, 70s, 90s, 120s, 150s - const delays = [10000, 20000, 35000, 50000, 70000, 90000, 120000, 150000]; - const delay = delays[_autoRetryCount - 1] || 120000; + const delay = _autoRetryCount * 30000; // 30s, 60s, 90s _setOsmStatus(`Neue Umgebung – Daten werden geladen
`); setTimeout(() => { if (!_overpassActive) _scheduleOsmLoad(); }, delay); } @@ -2049,92 +1944,6 @@ window.Page_map = (() => { } catch { /* still */ } } - // ---------------------------------------------------------- - // Orts-Suche (Nominatim-Proxy) - // ---------------------------------------------------------- - async function _runSearch(q) { - const resultsEl = document.getElementById('map-search-results'); - if (!resultsEl) return; - resultsEl.innerHTML = '
Suche

'; - resultsEl.style.display = ''; - try { - const data = await API.get(`/osm/geocode?q=${encodeURIComponent(q)}`); - if (!data.length) { - resultsEl.innerHTML = '
Keine Ergebnisse
'; - return; - } - resultsEl.innerHTML = data.map((r, i) => - `
-
${UI.escape(r.name)}
- ${r.subtitle ? `
${UI.escape(r.subtitle)}
` : ''} -
` - ).join(''); - resultsEl.querySelectorAll('.map-search-item').forEach(el => { - el.addEventListener('pointerdown', e => { - e.stopPropagation(); - const r = data[+el.dataset.i]; - _flyToResult(r); - document.getElementById('map-search-input').value = r.name; - document.getElementById('map-search-clear').style.display = ''; - resultsEl.style.display = 'none'; - }); - }); - } catch { - resultsEl.innerHTML = '
Suche nicht verfĂŒgbar
'; - } - } - - function _flyToResult(r) { - if (!_map || !window.L) return; - _searchMarker?.remove(); - _map.flyTo([r.lat, r.lon], 15, { duration: 1.0 }); - _searchMarker = L.marker([r.lat, r.lon], { - icon: L.divIcon({ - className: '', - html: `
- - - - -
`, - iconSize: [32, 32], - iconAnchor: [16, 32], - }), - zIndexOffset: 1000, - }) - .addTo(_map) - .bindPopup(`
${UI.escape(r.name)}
- ${r.subtitle ? `
${UI.escape(r.subtitle)}
` : ''} - `, { maxWidth: 240 }) - .openPopup(); - - setTimeout(() => { - document.getElementById('search-marker-close')?.addEventListener('click', () => { - _clearSearch(); - _searchMarker?.closePopup(); - }); - }, 50); - } - - function _clearSearch() { - const input = document.getElementById('map-search-input'); - const results = document.getElementById('map-search-results'); - const wrap = document.getElementById('map-search-wrap'); - const btn = document.getElementById('map-search-btn'); - if (input) { input.value = ''; input.blur(); } - if (results) results.style.display = 'none'; - wrap?.classList.remove('active'); - btn?.classList.remove('active'); - _searchMarker?.remove(); - _searchMarker = null; - clearTimeout(_searchTimer); - } - return { init, refresh, onDogChange, startRecording: _startRecording, stopRecording: _stopRecording, isRecording: () => _recActive }; })(); diff --git a/backend/static/js/pages/routes.js b/backend/static/js/pages/routes.js index bf3ccaa..0f4e1af 100644 --- a/backend/static/js/pages/routes.js +++ b/backend/static/js/pages/routes.js @@ -1698,10 +1698,6 @@ window.Page_routes = (() => { center: [mid.lat, mid.lon], zoom: 15, zoomControl: false, attributionControl: false, }); - // Container hat im frisch eingefĂŒgten Fixed-Overlay erst jetzt seine - // finale Flex-Höhe — Leaflet muss sie neu vermessen, sonst lĂ€dt es nur - // oben Tiles und der Rest bleibt grau. - _navMap.invalidateSize(); // Route-Polylines: erledigt (grĂŒn) + ausstehend (orange) const doneLine = L.polyline([], { color: '#22c55e', weight: 5, opacity: 0.85 }).addTo(_navMap); @@ -1709,14 +1705,6 @@ window.Page_routes = (() => { _navMap.fitBounds(remainLine.getBounds(), { padding: [20, 20] }); _addRouteArrows(_navMap, track, '#3b82f6'); - // iOS rendert das Flex-Layout teils verzögert — nochmal neu vermessen - // und Ausschnitt erneut anpassen. - setTimeout(() => { - if (!_navMap) return; - _navMap.invalidateSize(); - _navMap.fitBounds(remainLine.getBounds(), { padding: [20, 20] }); - }, 250); - // Start/End-Marker (als Variable damit Reverse sie neu setzen kann) const mkPin = (p, color) => L.circleMarker([p.lat, p.lon], { radius: 8, color: '#fff', weight: 2, fillColor: color, fillOpacity: 1 diff --git a/backend/static/js/pages/settings.js b/backend/static/js/pages/settings.js index e87d6e0..1cfbe70 100644 --- a/backend/static/js/pages/settings.js +++ b/backend/static/js/pages/settings.js @@ -672,13 +672,6 @@ window.Page_settings = (() => {
-
-
OpenStreetMap – die Karte mitverbessern
-
-
LĂ€dt

-
-
-