From 9afbf24535d0d035ca5e95d3e0adb21fb532e6ba Mon Sep 17 00:00:00 2001 From: rene Date: Wed, 3 Jun 2026 21:49:44 +0200 Subject: [PATCH] =?UTF-8?q?OSM-Beitr=C3=A4ge:=20"Hund=20willkommen=3F"=20?= =?UTF-8?q?=F0=9F=91=8D/=F0=9F=91=8E=20(dog=3Dyes/no)=20+=20Umdrehen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - dog=no zusätzlich zu dog=yes (Pächterwechsel → Ort nicht mehr hundefreundlich). - Map-Popup: ein "Hund willkommen?"-Block mit Daumen hoch/runter statt zwei Buttons. Beide rufen /dog-friendly mit welcome=true|false. - Backend generisch: tag_value yes|no; vorhandene Markierung mit anderem Wert wird umgedreht (Update statt 409); submit_dog_tag(value); Confirm/Revert prüft gegen den jeweiligen tag_value; Changeset-Kommentar wertabhängig. --- VERSION | 2 +- backend/routes/osm_contrib.py | 94 ++++++++++++++++++++-------------- backend/static/index.html | 24 ++++----- backend/static/js/app.js | 2 +- backend/static/js/pages/map.js | 41 ++++++++++----- backend/static/landing.html | 2 +- backend/static/sw.js | 2 +- 7 files changed, 100 insertions(+), 67 deletions(-) diff --git a/VERSION b/VERSION index 646782c..5a9264f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1157 \ No newline at end of file +1158 \ No newline at end of file diff --git a/backend/routes/osm_contrib.py b/backend/routes/osm_contrib.py index 0d032ec..672be37 100644 --- a/backend/routes/osm_contrib.py +++ b/backend/routes/osm_contrib.py @@ -48,6 +48,7 @@ class DogFriendlyIn(BaseModel): poi_type: Optional[str] = None lat: float lon: float + welcome: bool = True # True → dog=yes, False → dog=no (Pächterwechsel) def _verified_count(conn, uid: int) -> int: @@ -60,13 +61,13 @@ def _verified_count(conn, uid: int) -> int: # ------------------------------------------------------------------ # OSM-Changeset-Upload (write_api): Element holen → dog=yes → Changeset. # ------------------------------------------------------------------ -_CHANGESET_XML = ( - '' - '' - '' - '' - '' -) +def _changeset_xml(value: str) -> str: + note = "Hund willkommen" if value == "yes" else "Hund nicht willkommen" + return ('' + '' + f'' + '' + '') def _mark_submitted(contrib_id: int, etype: str, changeset_id): @@ -78,9 +79,9 @@ def _mark_submitted(contrib_id: int, etype: str, changeset_id): ) -async def submit_dog_yes(contrib_id: int, osm_id: int, osm_type: str, token: str) -> bool: - """Setzt dog=yes am OSM-Element des Nutzers (eigener OAuth-Token). Idempotent. - Wirft bei Fehler → Beitrag bleibt 'pending' (Retry über den Job).""" +async def submit_dog_tag(contrib_id: int, osm_id: int, osm_type: str, token: str, value: str) -> bool: + """Setzt dog= (yes|no) am OSM-Element des Nutzers (eigener OAuth-Token). + Idempotent. Wirft bei Fehler → Beitrag bleibt 'pending' (Retry über den Job).""" headers = {"Authorization": f"Bearer {token}"} order = [osm_type, "way" if osm_type == "node" else "node"] async with httpx.AsyncClient(timeout=20) as client: @@ -96,21 +97,21 @@ async def submit_dog_yes(contrib_id: int, osm_id: int, osm_type: str, token: str root = ET.fromstring(elem_xml) el = root.find(etype) existing = el.find("./tag[@k='dog']") - if existing is not None and existing.get("v") == "yes": + if existing is not None and existing.get("v") == value: _mark_submitted(contrib_id, etype, None) # schon gesetzt → fertig return True # 2) Changeset öffnen cs = await client.put(f"{OSM_API_BASE}/api/0.6/changeset/create", - headers=headers, content=_CHANGESET_XML) + headers=headers, content=_changeset_xml(value)) cs.raise_for_status() changeset_id = cs.text.strip() - # 3) dog=yes setzen + Element hochladen (Geometrie/andere Tags bleiben) + # 3) dog= setzen + Element hochladen (Geometrie/andere Tags bleiben) if existing is not None: - existing.set("v", "yes") + existing.set("v", value) else: - ET.SubElement(el, "tag", {"k": "dog", "v": "yes"}) + ET.SubElement(el, "tag", {"k": "dog", "v": value}) el.set("changeset", changeset_id) up = await client.put(f"{OSM_API_BASE}/api/0.6/{etype}/{osm_id}", headers=headers, content=ET.tostring(root, encoding="unicode")) @@ -132,12 +133,17 @@ async def mark_dog_friendly(body: DogFriendlyIn, user=Depends(get_current_user)) if not conn.execute("SELECT 1 FROM user_osm WHERE user_id=?", (uid,)).fetchone(): raise HTTPException(409, "Bitte zuerst dein OSM-Konto verknüpfen.") - # 1) Dedup — 1× pro POI - if conn.execute( - "SELECT 1 FROM osm_contributions WHERE user_id=? AND osm_id=? AND tag_key='dog'", + value = 'yes' if body.welcome else 'no' + + # 1) Vorhandene Markierung? Gleicher Wert → fertig. Anderer Wert → + # umdrehen erlaubt (Pächter wechseln → aus willkommen wird nicht mehr). + existing = conn.execute( + "SELECT id, tag_value FROM osm_contributions " + "WHERE user_id=? AND osm_id=? AND tag_key='dog'", (uid, body.osm_id) - ).fetchone(): - raise HTTPException(409, "Diesen Ort hast du schon als hundefreundlich markiert.") + ).fetchone() + if existing and existing['tag_value'] == value: + raise HTTPException(409, "Diesen Ort hast du schon so markiert.") # 2) Zeitkomponente: Tages-Rate-Limit today_n = conn.execute( @@ -184,16 +190,27 @@ async def mark_dog_friendly(body: DogFriendlyIn, user=Depends(get_current_user)) if poi and haversine_m(body.lat, body.lon, poi['lat'], poi['lon']) > POI_NEAR_M: raise HTTPException(422, "Position passt nicht zum gewählten Ort.") - # 5) verifiziert erfassen (pending; OSM-Upload gleich best-effort) - cur = conn.execute( - """INSERT INTO osm_contributions - (user_id, osm_id, osm_type, poi_type, tag_key, tag_value, lat, lon, - route_id, gps_distance_m, gps_points_near, status) - VALUES (?,?,?,?, 'dog','yes', ?,?, ?,?,?, 'pending')""", - (uid, body.osm_id, body.osm_type, body.poi_type, body.lat, body.lon, - best[0], round(best[1], 1), best[2]) - ) - contrib_id = cur.lastrowid + # 5) verifiziert erfassen oder umdrehen (pending; OSM-Upload gleich best-effort) + if existing: + conn.execute( + "UPDATE osm_contributions SET tag_value=?, osm_type=?, poi_type=?, " + "lat=?, lon=?, route_id=?, gps_distance_m=?, gps_points_near=?, " + "status='pending', changeset_id=NULL, submitted_at=NULL, " + "created_at=datetime('now') WHERE id=?", + (value, body.osm_type, body.poi_type, body.lat, body.lon, + best[0], round(best[1], 1), best[2], existing['id']) + ) + contrib_id = existing['id'] + else: + cur = conn.execute( + """INSERT INTO osm_contributions + (user_id, osm_id, osm_type, poi_type, tag_key, tag_value, lat, lon, + route_id, gps_distance_m, gps_points_near, status) + VALUES (?,?,?,?, 'dog',?, ?,?, ?,?,?, 'pending')""", + (uid, body.osm_id, body.osm_type, body.poi_type, value, body.lat, body.lon, + best[0], round(best[1], 1), best[2]) + ) + contrib_id = cur.lastrowid total = _verified_count(conn, uid) token_enc = conn.execute( "SELECT token_enc FROM user_osm WHERE user_id=?", (uid,) @@ -202,14 +219,14 @@ async def mark_dog_friendly(body: DogFriendlyIn, user=Depends(get_current_user)) # 6) OSM-Upload best-effort — Fehler → bleibt 'pending', Job versucht erneut submitted = False try: - submitted = await submit_dog_yes(contrib_id, body.osm_id, body.osm_type, _decrypt(token_enc)) + submitted = await submit_dog_tag(contrib_id, body.osm_id, body.osm_type, _decrypt(token_enc), value) except Exception as e: logger.warning("OSM-Upload später erneut (contrib %s): %s", contrib_id, e) - logger.info("dog=yes erfasst: user %s, osm %s, Tour %s (%.0fm, %d Pkt), submitted=%s", - uid, body.osm_id, best[0], best[1], best[2], submitted) + logger.info("dog=%s erfasst: user %s, osm %s, Tour %s (%.0fm, %d Pkt), submitted=%s", + value, uid, body.osm_id, best[0], best[1], best[2], submitted) return { - "status": "erfasst", "verified": True, "submitted": submitted, + "status": "erfasst", "value": value, "verified": True, "submitted": submitted, "verified_count": total, "badge": total >= BADGE_AT, "pro_progress": min(total, PRO_AT), "pro_at": PRO_AT, } @@ -264,19 +281,20 @@ async def run_confirmation_round(): # (1) Pending-Retry with db() as conn: pend = conn.execute( - "SELECT c.id, c.osm_id, c.osm_type, o.token_enc FROM osm_contributions c " + "SELECT c.id, c.osm_id, c.osm_type, c.tag_value, o.token_enc FROM osm_contributions c " "JOIN user_osm o ON o.user_id=c.user_id WHERE c.status='pending' LIMIT 50" ).fetchall() for r in pend: try: - await submit_dog_yes(r["id"], r["osm_id"], r["osm_type"] or "node", _decrypt(r["token_enc"])) + await submit_dog_tag(r["id"], r["osm_id"], r["osm_type"] or "node", + _decrypt(r["token_enc"]), r["tag_value"]) except Exception: pass # (2) Confirm/Revert with db() as conn: subs = conn.execute( - "SELECT id, user_id, osm_id, osm_type FROM osm_contributions " + "SELECT id, user_id, osm_id, osm_type, tag_value FROM osm_contributions " "WHERE status='submitted' AND submitted_at < datetime('now', ?)", (f"-{CONFIRM_AFTER_DAYS} days",) ).fetchall() @@ -290,7 +308,7 @@ async def run_confirmation_round(): if resp.status_code == 200: el = ET.fromstring(resp.text).find(etype) tag = el.find("./tag[@k='dog']") if el is not None else None - ok = tag is not None and tag.get("v") == "yes" + ok = tag is not None and tag.get("v") == r["tag_value"] new_status = "confirmed" if ok else "rejected" except Exception: continue # nächste Runde erneut diff --git a/backend/static/index.html b/backend/static/index.html index 0c4bd72..bb489c0 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/app.js b/backend/static/js/app.js index 7a79dd0..bd12889 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 = '1157'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '1158'; // ← 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/pages/map.js b/backend/static/js/pages/map.js index 656060f..2d9dac0 100644 --- a/backend/static/js/pages/map.js +++ b/backend/static/js/pages/map.js @@ -1103,13 +1103,21 @@ window.Page_map = (() => { ? `` : ``; - // "Hund war willkommen" (dog=yes) — nur bei OSM-POIs, wo's Sinn ergibt + // "Hund willkommen?" — 👍/👎 (dog=yes/no) bei OSM-POIs, wo's Sinn ergibt. + // dog=no nötig, weil Pächter wechseln und ein Ort nicht mehr hundefreundlich wird. const DOG_TYPES = ['restaurant', 'hotel', 'shop', 'tierarzt', 'hundesalon']; const dogBtn = (poi.source === 'osm' && DOG_TYPES.includes(layerKey)) - ? `` + ? `
+
Hund willkommen?
+
+ + +
+
` : ''; const openHours = poi.opening_hours @@ -1139,20 +1147,27 @@ window.Page_map = (() => { if (isOwn) _deleteUserPoi(poi.user_poi_id, marker, layerKey); else _showReportDialog(poi); }); - document.getElementById('mp-dogyes')?.addEventListener('click', async () => { - const b = document.getElementById('mp-dogyes'); - b.disabled = true; + const _sendDog = async (welcome) => { + const yes = document.getElementById('mp-dogyes'); + const no = document.getElementById('mp-dogno'); + if (yes) yes.disabled = true; + if (no) no.disabled = true; try { const r = await API.post('/osm-contrib/dog-friendly', { - osm_id: poi.id, osm_type: 'node', poi_type: layerKey, lat: poi.lat, lon: poi.lon, + osm_id: poi.id, osm_type: 'node', poi_type: layerKey, + lat: poi.lat, lon: poi.lon, welcome, }); - UI.toast.success(r.submitted ? 'Eingetragen — danke! 🐾' : 'Erfasst — wird übertragen 🐾'); - b.innerHTML = '✓ Eingetragen'; + UI.toast.success((welcome ? 'Hund willkommen' : 'Hund nicht willkommen') + + (r.submitted ? ' — eingetragen 🐾' : ' — wird übertragen 🐾')); + marker.closePopup(); } catch (e) { UI.toast.error(e?.message || 'Konnte nicht eintragen.'); - b.disabled = false; + if (yes) yes.disabled = false; + if (no) no.disabled = false; } - }); + }; + document.getElementById('mp-dogyes')?.addEventListener('click', () => _sendDog(true)); + document.getElementById('mp-dogno')?.addEventListener('click', () => _sendDog(false)); }, 50); } diff --git a/backend/static/landing.html b/backend/static/landing.html index 1873553..4023d8f 100644 --- a/backend/static/landing.html +++ b/backend/static/landing.html @@ -4,7 +4,7 @@ - + Ban Yaro — Die Hunde-App für Deutschland, Österreich & Schweiz diff --git a/backend/static/sw.js b/backend/static/sw.js index 0e33dea..8f27062 100644 --- a/backend/static/sw.js +++ b/backend/static/sw.js @@ -4,7 +4,7 @@ ============================================================ */ // ← EINZIGE Stelle für die Version — STATIC_ASSETS und CACHE_VERSION leiten sich ab -const VER = '1157'; +const VER = '1158'; const CACHE_VERSION = `by-v${VER}`; const CACHE_STATIC = `${CACHE_VERSION}-static`; const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten