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