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
-
-
-
â banyaro.app
@@ -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 = (() => {
${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 => `
+
+
+ ${UI.escape(s.name)}
+ ${s.distance_m < 1000 ? s.distance_m + ' m' : (s.distance_m / 1000).toFixed(1) + ' km'}
+ `).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: `
SchlieĂen `,
- });
- 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)}
` : ''}
-
- Marker entfernen
- `, { 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 = (() => {
-
-