diff --git a/article_server.py b/article_server.py
index f38f706..eef61e9 100644
--- a/article_server.py
+++ b/article_server.py
@@ -88,6 +88,28 @@ def save_image_map(mapping: dict[str, list[str]]) -> None:
json.dump(mapping, f, indent=2)
+def find_orphan_images() -> list[str]:
+ """Find images in workspace that are NOT in article_images.json."""
+ import glob as _glob
+ existing = set()
+ for paths in get_image_map().values():
+ for p in paths:
+ existing.add(os.path.abspath(p))
+
+ orphans = []
+ for ext in ("*.jpg", "*.jpeg", "*.png", "*.webp", "*.gif"):
+ for p in _glob.iglob(os.path.join(WORKSPACE, "**", ext), recursive=True):
+ # Skip bsn-chatbot, media dir (already managed), __pycache__, .git, venv
+ if "/bsn-chatbot/" in p or "/__pycache__/" in p or "/.git/" in p or "/venv/" in p:
+ continue
+ if "/image_cache/" in p:
+ continue
+ abspath = os.path.abspath(p)
+ if abspath not in existing:
+ orphans.append(abspath)
+ return sorted(orphans)
+
+
def is_article(filename: str) -> bool:
for pattern in SKIP_PATTERNS:
if re.search(pattern, filename, re.IGNORECASE):
@@ -175,6 +197,28 @@ def build_index(articles: list[dict]) -> str:
# Read image map for badge display
img_map = get_image_map()
+ # ── Orphaned images section ──
+ orphan_imgs = find_orphan_images()
+ orphan_section = ""
+ if orphan_imgs:
+ article_opts = "\\n".join(
+ f''
+ for a in articles
+ )
+ orphan_rows = "\\n".join(
+ f'''
+ 🖼️ {os.path.basename(p)}
+
+
'''
+ for p in orphan_imgs[:20] # max 20 to keep page fast
+ )
+ orphan_section = f'''
+ 🖼️ {len(orphan_imgs)} verwaiste Bilder — keinem Artikel zugeordnet
+