diff --git a/VERSION b/VERSION
index f845c1c..1080e3f 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-1267
\ No newline at end of file
+1268
\ No newline at end of file
diff --git a/backend/static/index.html b/backend/static/index.html
index 207852f..e5a2150 100644
--- a/backend/static/index.html
+++ b/backend/static/index.html
@@ -86,14 +86,14 @@
Ban Yaro
-
+
-
-
-
-
-
+
+
+
+
+
@@ -620,11 +620,11 @@
-
-
-
-
-
+
+
+
+
+
@@ -634,7 +634,7 @@
-
+
diff --git a/backend/static/js/api.js b/backend/static/js/api.js
index b5022e2..7b0e5d8 100644
--- a/backend/static/js/api.js
+++ b/backend/static/js/api.js
@@ -492,7 +492,7 @@ const API = (() => {
// WETTER
// ----------------------------------------------------------
const weather = {
- alerts(lat, lon) { return get(`/weather/alerts?lat=${lat}&lon=${lon}`); },
+ // alerts() entfernt — /weather/alerts existierte im Backend nie (459cd42), kein Aufrufer
get(lat, lon) { return get(`/weather?lat=${lat}&lon=${lon}`); },
forecast(lat, lon) { return get(`/weather/forecast?lat=${lat}&lon=${lon}`); },
};
diff --git a/backend/static/js/app.js b/backend/static/js/app.js
index aa3b099..a8bf102 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 = '1267'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
+const APP_VER = '1268'; // ← 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/worlds.js b/backend/static/js/worlds.js
index 84830ec..da1e422 100644
--- a/backend/static/js/worlds.js
+++ b/backend/static/js/worlds.js
@@ -1857,9 +1857,12 @@ window.Worlds = (() => {
const out = [];
try {
const pos = await API.getLocation({ timeout: 4000, maximumAge: 600_000 });
+ // /poison + /lost filtern per lat/lon serverseitig (poison: Radius in METERN,
+ // lost: radius_km). Die früheren /nearby-Pfade existierten nie (459cd42-Verlust)
+ // — das doppelte catch hat den 404 jahrelang verschluckt.
const [p, l] = await Promise.allSettled([
- API.get(`/poison/nearby?lat=${pos.lat}&lon=${pos.lon}&radius=5`).catch(() => []),
- API.get(`/lost/nearby?lat=${pos.lat}&lon=${pos.lon}&radius=20`).catch(() => []),
+ API.get(`/poison?lat=${pos.lat}&lon=${pos.lon}&radius=5000`).catch(() => []),
+ API.get(`/lost?lat=${pos.lat}&lon=${pos.lon}&radius_km=20`).catch(() => []),
]);
if (p.value?.length) out.push({ icon:'skull', color:'#EF4444', title:'Giftköder in der Nähe', sub:`${p.value.length} Meldung${p.value.length>1?'en':''}`, page:'poison' });
if (l.value?.length) out.push({ icon:'dog', color:'#3B82F6', title:'Verlorener Hund', sub:`${l.value.length} Meldung${l.value.length>1?'en':''}`, page:'lost' });
diff --git a/backend/static/landing.html b/backend/static/landing.html
index 654b373..fc24dda 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 24b1305..e299457 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 = '1267';
+const VER = '1268';
const CACHE_VERSION = `by-v${VER}`;
const CACHE_STATIC = `${CACHE_VERSION}-static`;
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
diff --git a/tests/test_api_surface.py b/tests/test_api_surface.py
new file mode 100644
index 0000000..d69fc5f
--- /dev/null
+++ b/tests/test_api_surface.py
@@ -0,0 +1,114 @@
+"""API-Oberflaechen-Abgleich: Jeder Frontend-API-Aufruf muss eine Backend-Route haben.
+
+Hintergrund: Der Worktree-Merge-Verlust um 459cd42 (v1102) hinterliess Frontend-Code,
+der nie existierende Endpoints aufrief (/partner/my-profile, /breeder/my-editor,
+/poison/nearby, /lost/nearby) — teils jahrelang unbemerkt, weil catch() die Fehler
+verschluckte. Dieser Test macht solche Geister-Aufrufe zum Build-Fehler.
+"""
+
+import re
+import glob
+import os
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parent.parent
+STATIC = ROOT / "backend" / "static"
+
+METHOD = {"get": "GET", "post": "POST", "put": "PUT", "patch": "PATCH",
+ "del": "DELETE", "delete": "DELETE", "upload": "POST"}
+
+
+def _backend_routes():
+ main = (ROOT / "backend" / "main.py").read_text()
+
+ mod_of_var = {}
+ for m in re.finditer(r"from routes\.(\w+)\s+import\s+([^\n]+)", main):
+ mod, rest = m.group(1), m.group(2)
+ for part in rest.split(","):
+ part = part.strip()
+ am = re.match(r"(\w+)\s+as\s+(\w+)", part)
+ if am:
+ mod_of_var[am.group(2)] = (mod, am.group(1))
+ elif re.match(r"^\w+$", part):
+ mod_of_var[part] = (mod, part)
+
+ routes = set()
+
+ def norm(p):
+ return re.sub(r"\{[^}]+\}", "*", p).rstrip("/") or "/"
+
+ for m in re.finditer(r"app\.include_router\((\w+)(?:,\s*prefix=\"([^\"]*)\")?", main):
+ var, prefix = m.group(1), m.group(2) or ""
+ info = mod_of_var.get(var)
+ if not info:
+ continue
+ mod, routername = info
+ fn = ROOT / "backend" / "routes" / f"{mod}.py"
+ if not fn.exists():
+ continue
+ src = fn.read_text()
+ # Router kann eigenen Prefix mitbringen (z.B. invoices)
+ pm = re.search(rf"{routername}\s*=\s*APIRouter\(\s*prefix=\"([^\"]*)\"", src) or \
+ re.search(r"router\s*=\s*APIRouter\(\s*prefix=\"([^\"]*)\"", src)
+ own_prefix = pm.group(1) if pm and routername == "router" or pm and f"{routername} =" in src else (pm.group(1) if pm else "")
+ base = prefix + (own_prefix if not prefix else "")
+ if not base and pm:
+ base = pm.group(1)
+ for rm in re.finditer(rf"@{routername}\.(get|post|put|patch|delete)\(\s*[\"']([^\"']*)[\"']", src):
+ routes.add((rm.group(1).upper(), norm(base + rm.group(2))))
+
+ for m in re.finditer(r"@app\.(get|post|put|patch|delete)\(\s*[\"']([^\"']*)[\"']", main):
+ routes.add((m.group(1).upper(), norm(m.group(2))))
+ for m in re.finditer(r"@app\.api_route\(\s*[\"']([^\"']*)[\"'][^)]*methods=\[([^\]]*)\]", main):
+ for meth in re.findall(r"\"(\w+)\"", m.group(2)):
+ routes.add((meth.upper(), norm(m.group(1))))
+ return routes
+
+
+def _frontend_calls():
+ calls = []
+
+ def norm(p):
+ p = p.split("?")[0]
+ p = re.sub(r"\$\{[^}]+\}", "*", p)
+ return (p if p.startswith("/api") else "/api" + p).rstrip("/")
+
+ for fn in glob.glob(str(STATIC / "js" / "**" / "*.js"), recursive=True):
+ if any(s in fn for s in ("vendor", "leaflet", "qrcode.min", "maplibre")):
+ continue
+ src = open(fn, encoding="utf-8", errors="replace").read()
+ for m in re.finditer(
+ r"\b(?:API\.)?(get|post|put|patch|del|delete|upload)\(\s*[`'\"](/[^`'\"\s]*)[`'\"]",
+ src, re.S,
+ ):
+ raw = m.group(2)
+ if raw.startswith(("/js/", "/css/", "/icons/", "/img/", "/media/", "/data/", "/q/")):
+ continue
+ line = src[: m.start()].count("\n") + 1
+ calls.append((METHOD[m.group(1)], norm(raw), f"{os.path.relpath(fn, ROOT)}:{line}"))
+ return calls
+
+
+def _matches(call_path, route_path):
+ c, r = call_path.split("/"), route_path.split("/")
+ return len(c) == len(r) and all(a == b or a == "*" or b == "*" for a, b in zip(c, r))
+
+
+def test_no_ghost_api_calls():
+ routes = _backend_routes()
+ assert len(routes) > 400, f"Routen-Parser kaputt? Nur {len(routes)} Routen gefunden."
+ calls = _frontend_calls()
+ assert len(calls) > 300, f"Frontend-Parser kaputt? Nur {len(calls)} Aufrufe gefunden."
+
+ ghosts = []
+ for meth, path, loc in calls:
+ if not path.startswith("/api"):
+ continue
+ if any(bm == meth and _matches(path, bp) for bm, bp in routes):
+ continue
+ ghosts.append(f"{meth} {path} ({loc})")
+
+ assert not ghosts, (
+ "Frontend ruft Endpoints auf, die im Backend nicht existieren "
+ "(Worktree-Verlust-Muster!):\n " + "\n ".join(sorted(set(ghosts)))
+ )