diff --git a/scanner/__main__.py b/scanner/__main__.py index 3704aae..327f09a 100644 --- a/scanner/__main__.py +++ b/scanner/__main__.py @@ -299,8 +299,12 @@ def _render_markdown(r: dict) -> str: lines += ["", "## Kaputte Links", ""] for entry in new_broken: lines.append(f"- [NEU] HTTP {entry['status']} `{entry['url']}`") + for page in entry.get("pages", [])[:3]: + lines.append(f" - gefunden auf: {page}") for entry in known_broken: - lines.append(f"- [bekannt] HTTP {entry['status']} `{entry['url']}`") + lines.append(f"- [bekannt, noch nicht behoben] HTTP {entry['status']} `{entry['url']}`") + for page in entry.get("pages", [])[:3]: + lines.append(f" - gefunden auf: {page}") if d.get("unexpected_jsonld"): lines += ["", "## Unerwartetes JSON-LD", ""] diff --git a/scanner/differ.py b/scanner/differ.py index 8814d5b..1ac43e2 100644 --- a/scanner/differ.py +++ b/scanner/differ.py @@ -216,12 +216,27 @@ def compare_snapshots(baseline_pages: dict, current_pages: dict, cfg: dict) -> d if p.get("status", 0) >= 400 } current_broken = set(current_broken_map) + + # Reverse index: broken_url → pages that contain a link to it + link_sources: dict[str, list[str]] = {} + for page_url, page in current_pages.items(): + for link_list in page.get("links", {}).values(): + if not isinstance(link_list, list): + continue + for link in link_list: + if isinstance(link, dict): + href = link.get("url", "") + if href: + link_sources.setdefault(href, []).append(page_url) + new_broken = [ - {"url": url, "status": current_broken_map[url]} + {"url": url, "status": current_broken_map[url], + "pages": sorted(set(link_sources.get(url, [])))} for url in sorted(current_broken - baseline_broken) ] known_broken = [ - {"url": url, "status": current_broken_map[url]} + {"url": url, "status": current_broken_map[url], + "pages": sorted(set(link_sources.get(url, [])))} for url in sorted(current_broken & baseline_broken) ] diff --git a/scanner/plain.py b/scanner/plain.py index 698fae9..7c660af 100644 --- a/scanner/plain.py +++ b/scanner/plain.py @@ -144,11 +144,14 @@ def klartext_befunde( "Nach einem geplanten Update ist das normal — sonst bitte prüfen lassen." ) - # --- Kaputte Links (rein informativ) --- + # --- Kaputte Links (immer melden, bis behoben) --- neue_kaputt = diff.get("new_broken_urls", []) - if neue_kaputt: + bekannte_kaputt = diff.get("known_broken_urls", []) + alle_kaputt = len(neue_kaputt) + len(bekannte_kaputt) + if alle_kaputt: saetze.append( - f"{len(neue_kaputt)} interne Link(s) führen ins Leere (Seite nicht gefunden)." + f"{alle_kaputt} interne Link(s) führen ins Leere (Seite nicht gefunden). " + "Bitte prüfen und den Verweis auf der betroffenen Seite korrigieren." ) if ext_links and ext_links.get("broken"): saetze.append( diff --git a/tests/test_differ.py b/tests/test_differ.py index fc38e09..63a949f 100644 --- a/tests/test_differ.py +++ b/tests/test_differ.py @@ -234,6 +234,20 @@ class TestBrokenLinks: assert len(result["new_broken_urls"]) == 1 assert result["new_broken_urls"][0]["url"] == "https://b.info/missing/" assert result["new_broken_urls"][0]["status"] == 404 + assert "pages" in result["new_broken_urls"][0] + + def test_broken_url_includes_source_pages(self): + """broken entry lists the pages that link to the broken URL.""" + home = self._make_page("https://b.info/") + home["links"]["a"] = [{"url": "https://b.info/missing/", "class": "internal"}] + baseline = {"https://b.info/": self._make_page("https://b.info/")} + current = { + "https://b.info/": home, + "https://b.info/missing/": self._make_page("https://b.info/missing/", status=404), + } + result = compare_snapshots(baseline, current, BASE_CFG) + entry = result["new_broken_urls"][0] + assert "https://b.info/" in entry["pages"] def test_known_broken_not_in_new(self): broken_page = {"https://b.info/old/": self._make_page("https://b.info/old/", status=404)} @@ -242,6 +256,7 @@ class TestBrokenLinks: result = compare_snapshots(baseline, current, BASE_CFG) assert result["new_broken_urls"] == [] assert len(result["known_broken_urls"]) == 1 + assert "pages" in result["known_broken_urls"][0] def test_broken_url_scores(self): result = score_diff({ diff --git a/tests/test_plain.py b/tests/test_plain.py index 03e9d32..ecb09a0 100644 --- a/tests/test_plain.py +++ b/tests/test_plain.py @@ -70,6 +70,20 @@ class TestKlartextBefunde: assert any("funktionieren" in s.lower() or "website" in s.lower() for s in saetze) _no_jargon(saetze) + def test_new_broken_internal_link_plain(self): + diff = {"new_broken_urls": [{"url": "https://x.de/gone", "status": 404, "pages": []}]} + saetze = klartext_befunde(diff) + assert any("leere" in s.lower() or "gefunden" in s.lower() for s in saetze) + _no_jargon(saetze) + + def test_known_broken_link_still_reported(self): + """known_broken_urls must appear in Klartext even with score=0.""" + diff = {"known_broken_urls": [{"url": "https://x.de/old", "status": 404, "pages": []}]} + saetze = klartext_befunde(diff) + assert any("leere" in s.lower() or "gefunden" in s.lower() for s in saetze), \ + "known_broken_urls müssen auch bei Score=0 im Klartext erscheinen" + _no_jargon(saetze) + class TestWasTun: def test_red_has_urgent_steps(self):