From 8c76263ea09a248fc41c0958ec2c9d153db0e92f Mon Sep 17 00:00:00 2001 From: rene Date: Sun, 7 Jun 2026 20:22:20 +0200 Subject: [PATCH] Worktree-Verlust-Audit: alle Geister-Endpoints gefunden + Regressions-Test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Systematischer Abgleich aller 528 Frontend-API-Aufrufe gegen 576 Backend- Routen (methodengenau, Wildcard-Matching für Parameter): Echte Opfer (zusätzlich zu partner/* und breeder/my-editor): - worlds.js Nearby-Alerts riefen /poison/nearby + /lost/nearby auf — beide existierten NIE; doppeltes catch verschluckte die 404s → die Welten zeigten seit jeher keine Giftköder-/Vermisst-Warnungen. Fix: bestehende Listen- Endpoints mit Geo-Filter nutzen (/poison?radius=Meter, /lost?radius_km). - API.weather.alerts → /weather/alerts existierte nie, hatte aber auch keinen Aufrufer — toter Wrapper entfernt. False Positives geprüft: invoices (Router bringt eigenen Prefix mit, alle 9 Routen ok), Seiten↔Dateien↔window.Page_*↔index.html-Sections alle konsistent. Neu: tests/test_api_surface.py — statischer API-Oberflächen-Abgleich als Dauertest; Geister-Aufrufe sind ab jetzt Build-Fehler. Suite: 59 passed. --- VERSION | 2 +- backend/static/index.html | 24 ++++---- backend/static/js/api.js | 2 +- backend/static/js/app.js | 2 +- backend/static/js/worlds.js | 7 ++- backend/static/landing.html | 2 +- backend/static/sw.js | 2 +- tests/test_api_surface.py | 114 ++++++++++++++++++++++++++++++++++++ 8 files changed, 136 insertions(+), 19 deletions(-) create mode 100644 tests/test_api_surface.py 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))) + )