#!/usr/bin/env python3 """Revisa que el sitio cumpla «si no está en el sitio, no pasó». Cuatro preguntas, todas las veces: 1. ¿Cada enlace de cada pendiente abre algo? (si no, alguien se queda esperando un documento) 2. ¿Cada enlace DENTRO de los documentos publicados abre algo? 3. ¿Hay documentos fechados en las carpetas NN- que nunca se publicaron? 4. ¿Hay destinos del MAP cuyo origen ya no existe, o publicados fuera del nav? Uso: python3 scripts/revisar-enlaces.py [ruta-a-hbox-docs] Sale con 1 si algo está roto, para poder colgarlo del despliegue o del agente diario. Las rutas /api/… son del backend, no archivos: no cuentan como rotas. """ import json import os import re import sys from urllib.parse import unquote, urlparse REPO = os.path.abspath(sys.argv[1] if len(sys.argv) > 1 else os.path.join(os.path.dirname(__file__), "..")) PADRE = os.path.dirname(REPO) DOCS = os.path.join(REPO, "docs") SITE = os.path.join(REPO, "site") LINK = re.compile(r"\[([^\]]{0,120})\]\(\s*]+)>?(?:\s+\"[^\"]*\")?\s*\)") EXTERNO = ("http://", "https://", "mailto:", "tel:", "#", "data:") BACKEND = ("/api/",) def mapa(): txt = open(os.path.join(REPO, "scripts", "sync-docs.sh"), encoding="utf-8").read() return [tuple(m.group(1).split("|", 1)) for m in re.finditer(r'^\s*"([^"]+\|[^"]+)"\s*$', txt, re.M)] def docs_fechados(): out = [] for nn in sorted(d for d in os.listdir(PADRE) if re.match(r"^\d\d-", d)): for raiz, dirs, files in os.walk(os.path.join(PADRE, nn)): dirs[:] = [d for d in dirs if d not in (".git", "_to_delete", "img", "node_modules", "kicad")] out += [os.path.relpath(os.path.join(raiz, fn), PADRE) for fn in files if fn.endswith(".md") and re.match(r"^\d{4}-\d{2}-\d{2}_", fn)] return sorted(out) def resuelve(url, origen=None): """True / False / None (externo o del backend).""" if url.startswith(BACKEND): return None p = urlparse(url) if p.scheme in ("http", "https"): if p.netloc not in ("drogheda.helgen.online", ""): return None elif p.scheme or url.startswith(EXTERNO): return None ruta = unquote(p.path or "") if not ruta: return None if ruta.startswith("/"): rel = ruta.lstrip("/") cand = [os.path.join(SITE, rel, "index.html"), os.path.join(SITE, rel), os.path.join(DOCS, rel), os.path.join(DOCS, rel.rstrip("/") + ".md"), os.path.join(DOCS, rel, "index.md")] elif origen: base = os.path.dirname(origen) cand = [os.path.join(base, ruta), os.path.join(base, ruta.rstrip("/") + ".md"), os.path.join(base, ruta, "index.md")] else: return None return any(os.path.exists(c) for c in cand) def main(): malo = 0 print("Revisión de enlaces del sitio —", REPO) f = os.path.join(REPO, "_estado", "tablero.json") if os.path.exists(f): d = json.load(open(f, encoding="utf-8")) ts = d.get("tareas") or d.get("pendientes") or (d if isinstance(d, list) else []) rotos = [(t.get("id"), t.get("responsable"), e.get("titulo"), e.get("url")) for t in ts for e in (t.get("enlaces") or []) if (e.get("url") or "").strip() and resuelve(e["url"]) is False] print(f"1. enlaces de {len(ts)} pendientes: {len(rotos)} rotos") for tid, resp, et, url in rotos: print(f" ✕ {tid} ({resp}) «{et}» → {url}") malo += len(rotos) else: print("1. (sin _estado/tablero.json: corre ./scripts/tablero.sh foto)") rotos = [] for raiz, dirs, files in os.walk(DOCS): dirs[:] = [x for x in dirs if x not in ("img", "fonts", "stylesheets", "js", "descargas")] for fn in sorted(files): if not fn.endswith(".md"): continue ruta = os.path.join(raiz, fn) for m in LINK.finditer(open(ruta, encoding="utf-8", errors="replace").read()): if resuelve(m.group(2), ruta) is False: rotos.append((os.path.relpath(ruta, REPO), m.group(1)[:50], m.group(2))) print(f"2. enlaces dentro de los documentos: {len(rotos)} rotos") for d_, t_, u_ in rotos: print(f" ✕ {d_}: «{t_}» → {u_}") malo += len(rotos) m = mapa() origenes = {o for o, _ in m} faltan = [d for d in docs_fechados() if d not in origenes] print(f"3. documentos fechados sin publicar: {len(faltan)}") for d_ in faltan: print(" ✕", d_) malo += len(faltan) nav = open(os.path.join(REPO, "mkdocs.yml"), encoding="utf-8").read() perdidos = [o for o, _ in m if not os.path.exists(os.path.join(PADRE, o))] huerfanos = [d for o, d in m if d.endswith(".md") and d[len("docs/"):] not in nav] print(f"4. orígenes perdidos: {len(perdidos)} · publicados fuera del nav: {len(huerfanos)}") for x in perdidos + huerfanos: print(" ✕", x) malo += len(perdidos) + len(huerfanos) print("TODO BIEN" if not malo else f"{malo} COSAS ROTAS") return 1 if malo else 0 sys.exit(main())