Worktree-Verlust-Audit: alle Geister-Endpoints gefunden + Regressions-Test
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.
This commit is contained in:
parent
487dacc7c7
commit
8c76263ea0
8 changed files with 136 additions and 19 deletions
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
1267
|
||||
1268
|
||||
|
|
@ -86,14 +86,14 @@
|
|||
<title>Ban Yaro</title>
|
||||
|
||||
<!-- Theme + theme-color Statusleiste vor CSS setzen -->
|
||||
<script src="/js/boot-early.js?v=1267"></script>
|
||||
<script src="/js/boot-early.js?v=1268"></script>
|
||||
|
||||
<!-- CSS: Reihenfolge ist wichtig — ?v= zwingt Browser zur Neuladung -->
|
||||
<link rel="stylesheet" href="/css/design-system.css?v=1267">
|
||||
<link rel="stylesheet" href="/css/layout.css?v=1267">
|
||||
<link rel="stylesheet" href="/css/components.css?v=1267">
|
||||
<link rel="stylesheet" href="/css/utilities.css?v=1267">
|
||||
<link rel="stylesheet" href="/css/lists.css?v=1267">
|
||||
<link rel="stylesheet" href="/css/design-system.css?v=1268">
|
||||
<link rel="stylesheet" href="/css/layout.css?v=1268">
|
||||
<link rel="stylesheet" href="/css/components.css?v=1268">
|
||||
<link rel="stylesheet" href="/css/utilities.css?v=1268">
|
||||
<link rel="stylesheet" href="/css/lists.css?v=1268">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
|
|
@ -620,11 +620,11 @@
|
|||
<div id="modal-container"></div>
|
||||
|
||||
<!-- JS: Reihenfolge ist wichtig — erst Basis, dann Features -->
|
||||
<script src="/js/api.js?v=1267"></script>
|
||||
<script src="/js/ui.js?v=1267"></script>
|
||||
<script src="/js/app.js?v=1267"></script>
|
||||
<script src="/js/worlds.js?v=1267"></script>
|
||||
<script src="/js/offline-indicator.js?v=1267"></script>
|
||||
<script src="/js/api.js?v=1268"></script>
|
||||
<script src="/js/ui.js?v=1268"></script>
|
||||
<script src="/js/app.js?v=1268"></script>
|
||||
<script src="/js/worlds.js?v=1268"></script>
|
||||
<script src="/js/offline-indicator.js?v=1268"></script>
|
||||
|
||||
<!-- Feature-Seiten werden lazy geladen -->
|
||||
|
||||
|
|
@ -634,7 +634,7 @@
|
|||
|
||||
|
||||
<!-- Boot: Offline-Banner + SW-Registration (extrahiert für CSP) -->
|
||||
<script src="/js/boot.js?v=1267"></script>
|
||||
<script src="/js/boot.js?v=1268"></script>
|
||||
|
||||
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -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}`); },
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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' });
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<script src="/js/landing-init.js?v=1267"></script>
|
||||
<script src="/js/landing-init.js?v=1268"></script>
|
||||
<title>Ban Yaro — Die Hunde-App für Deutschland, Österreich & Schweiz</title>
|
||||
<meta name="description" content="Ban Yaro: Die kostenlose All-in-One Hunde-App für DACH. Tagebuch, Giftköder-Alarm, Training mit KI, Forum, Wurfbörse, Stammbaum, Inzucht-Check — DSGVO-konform, offline-fähig, direkt im Browser.">
|
||||
<meta name="keywords" content="Hunde App, Hunde Community, Wurfbörse, Züchter, Welpen kaufen, Stammbaum Hund, Inzuchtkoeffizient, Hundezucht, Impfpass Hund, Giftköder Alarm, Gassi Community, Hundetraining App, Hunde Forum, Hunde KI, Hundefilm Datenbank, Welpen Marktplatz">
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
114
tests/test_api_surface.py
Normal file
114
tests/test_api_surface.py
Normal file
|
|
@ -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)))
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue