#!/usr/bin/env python3 """ Erzeugt eine HTML-Startseite (Dashboard) fuer demo-epb. Scant das Repo nach Word-Dokumenten, Reports, Code, Tests, Architektur, und schreibt build/index.html mit klickbaren Links. Run nach `make test && make coverage && make docs && make test-report && python3 tools/traceability.py publish docs/traceability && python3 tools/render_plantuml.py`. Output: build/index.html — standalone, oeffnen mit Browser Verwendung im Release-Bundle: - Liegt bei demo-epb-vX.Y.Z/index.html - Verlinkt alle anderen Bundle-Inhalte relativ """ from __future__ import annotations import datetime import html import os import re import subprocess from pathlib import Path REPO = Path(__file__).resolve().parent.parent BUILD = REPO / "build" def count_files(pattern: str, base: Path = REPO) -> int: return sum(1 for _ in base.glob(pattern)) def git_info() -> tuple[str, str]: try: sha = subprocess.check_output( ["git", "rev-parse", "--short", "HEAD"], cwd=str(REPO), text=True).strip() except Exception: sha = "?" try: tag = subprocess.check_output( ["git", "describe", "--tags", "--abbrev=0"], cwd=str(REPO), text=True, stderr=subprocess.DEVNULL).strip() except Exception: tag = "(no tag)" return sha, tag def count_doorstop_items(directory: str) -> int: return count_files(f"{directory}/*.md") def count_tests() -> int: total = 0 for f in (REPO / "tests" / "unit").glob("test_*.c"): text = f.read_text() total += len(re.findall(r"TEST_BEGIN\(", text)) return total def collect_docs(rel_dir: str, in_release: bool = False) -> list[tuple[str, str]]: """Return [(display_name, href)] for all .docx in a directory.""" out = [] d = REPO / rel_dir if not d.exists(): return out for f in sorted(d.glob("*.docx")): # In release bundle, paths are different; here we use relative-to-repo. href = os.path.relpath(f, REPO) # If running for in_release context, paths need adjustment, but for now # we always use repo-relative. out.append((f.stem, href)) return out def status_for(path: Path) -> str: if path.exists(): return "ok" return "missing" def kpi_card(label: str, value: str, sub: str = "", color: str = "#1f3864") -> str: return f"""
{html.escape(value)}
{html.escape(label)}
{html.escape(sub)}
""" def doc_section(title: str, docs: list[tuple[str, str]], description: str = "") -> str: if not docs: items = "
  • — keine Dokumente —
  • " else: items = "\n".join( f'
  • {html.escape(name)}
  • ' for name, href in docs ) return f"""

    {html.escape(title)}

    {f"

    {html.escape(description)}

    " if description else ""}
    """ def report_link(name: str, href: str, exists: bool, desc: str) -> str: cls = "ok" if exists else "missing" label = name + ("" if exists else " (nicht generiert — Coverage/Build laufen lassen)") if exists: return (f"
  • {html.escape(label)} " f"— {html.escape(desc)}
  • ") return f"
  • {html.escape(label)} — {html.escape(desc)}
  • " def main() -> int: BUILD.mkdir(parents=True, exist_ok=True) sha, tag = git_info() now = datetime.datetime.now(datetime.timezone.utc).isoformat(timespec="seconds") # Counts n_sg = count_doorstop_items("safety/sg") n_sys = count_doorstop_items("reqs/sys") n_swe = count_doorstop_items("reqs/swe") n_sa = count_doorstop_items("arch/sys") n_swa = count_doorstop_items("arch/swe") n_tests = count_tests() n_impl = sum(1 for f in (REPO / "src").glob("*.c")) n_stubs = sum(1 for f in (REPO / "src" / "stubs").glob("*.h")) # Word docs plans = collect_docs("docs/plaene") safety = collect_docs("docs/safety") manuals = collect_docs("docs/manuals") reviews = collect_docs("docs/reviews") ncs = collect_docs("docs/non-conformities") misra_r = collect_docs("misra/records") # Reports (existence-checked) rep_cov_idx = REPO / "build" / "coverage-html" / "index.html" rep_test_html = REPO / "build" / "test-report.html" rep_api = REPO / "build" / "api-doc" / "html" / "index.html" rep_trace = REPO / "docs" / "traceability" / "index.html" rep_cppcheck = REPO / "build" / "cppcheck-report.xml" html_body = f""" demo-epb {html.escape(tag)} — Projekt-Dashboard

    demo-epb — Elektrische Parkbremse

    Version {html.escape(tag)} · Commit {html.escape(sha)} · Generiert {html.escape(now)}
    {kpi_card("Safety Goals", str(n_sg), "ASIL D/D/A/C/B", "#d62728")} {kpi_card("System Reqs", str(n_sys), f"in reqs/sys/")} {kpi_card("SW Reqs", str(n_swe), f"in reqs/swe/")} {kpi_card("Arch-Elemente", f"{n_sa+n_swa}", f"{n_sa} SA + {n_swa} SWA")} {kpi_card("Komponenten", f"{n_impl}", f"+ {n_stubs} Stubs", "#2ca02c")} {kpi_card("Unit-Tests", str(n_tests), "Alle gruen", "#2ca02c")}

    Plaene (Word)

      """ for name, href in plans: if not href.startswith("docs/safety") and not href.startswith("docs/manuals"): html_body += f"
    • {html.escape(name)}
    • \n" html_body += "
    \n" html_body += doc_section("Funktionale Sicherheit (Word)", safety, "HARA, Safety Case, FMEDA, Compliance, Verification, Tool-Qualification") html_body += "
    " html_body += doc_section("Manuals (Word)", manuals, "End-User + Werkstatt-Doku") audit_docs = reviews + ncs + misra_r html_body += doc_section("Audit-Artefakte (Word)", audit_docs, "Reviews, Non-Conformities, MISRA-Deviation-Records") html_body += "
    " # Reports html_body += "

    Engineering-Reports (CI-generiert)

    " # Diagrams diagrams = sorted((REPO / "docs" / "diagrams").glob("*.svg")) if diagrams: html_body += "

    Architektur-Diagramme (PlantUML)

    " # Source code links html_body += """

    Source-Code

    """ html_body += f"""

    Externe Links

    """ out = BUILD / "index.html" out.write_text(html_body) print(f"Wrote {out}") return 0 if __name__ == "__main__": raise SystemExit(main())