301 lines
12 KiB
Python
301 lines
12 KiB
Python
#!/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"""
|
|
<div class='kpi'>
|
|
<div class='kpi-value' style='color:{color}'>{html.escape(value)}</div>
|
|
<div class='kpi-label'>{html.escape(label)}</div>
|
|
<div class='kpi-sub'>{html.escape(sub)}</div>
|
|
</div>
|
|
"""
|
|
|
|
|
|
def doc_section(title: str, docs: list[tuple[str, str]], description: str = "") -> str:
|
|
if not docs:
|
|
items = "<li class='cnt'>— keine Dokumente —</li>"
|
|
else:
|
|
items = "\n".join(
|
|
f'<li><a href="{html.escape(href)}">{html.escape(name)}</a></li>'
|
|
for name, href in docs
|
|
)
|
|
return f"""
|
|
<section>
|
|
<h2>{html.escape(title)}</h2>
|
|
{f"<p class='cnt'>{html.escape(description)}</p>" if description else ""}
|
|
<ul>{items}</ul>
|
|
</section>
|
|
"""
|
|
|
|
|
|
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"<li><a href='{html.escape(href)}'>{html.escape(label)}</a> "
|
|
f"<span class='cnt'>— {html.escape(desc)}</span></li>")
|
|
return f"<li class='{cls}'>{html.escape(label)} <span class='cnt'>— {html.escape(desc)}</span></li>"
|
|
|
|
|
|
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 — Links zeigen auf BUNDLE-Pfade (relative zum index.html im Deploy).
|
|
# Die CI-Pipeline deployt die Artefakte in genau diese Pfade,
|
|
# darum ist deren Existenz hier irrelevant — Links werden immer emittiert.
|
|
rep_paths = {
|
|
"coverage": "coverage/index.html",
|
|
"test": "reports/test-report.html",
|
|
"api": "api-doc/index.html",
|
|
"trace": "traceability/index.html",
|
|
"cppcheck": "reports/cppcheck-report.xml",
|
|
}
|
|
# Existence-Check zum Anzeigen "Generated?" — gegen den CI/lokalen Build-Pfad.
|
|
rep_cov_built = (REPO / "build" / "coverage-html" / "index.html").exists()
|
|
rep_test_built = (REPO / "build" / "test-report.html").exists()
|
|
rep_api_built = (REPO / "build" / "api-doc" / "html" / "index.html").exists()
|
|
rep_trace_built = (REPO / "docs" / "traceability" / "index.html").exists()
|
|
rep_cpp_built = (REPO / "build" / "cppcheck-report.xml").exists()
|
|
|
|
html_body = f"""<!doctype html>
|
|
<html lang='de'><head>
|
|
<meta charset='utf-8'>
|
|
<title>demo-epb {html.escape(tag)} — Projekt-Dashboard</title>
|
|
<style>
|
|
:root {{ color-scheme: light; }}
|
|
body {{ font-family: -apple-system, "Segoe UI", sans-serif; margin: 0; padding: 0; color: #222; background: #f5f6f8; }}
|
|
header {{ background: #1f3864; color: white; padding: 20px 40px; }}
|
|
header h1 {{ margin: 0; font-size: 26px; }}
|
|
header .meta {{ color: #b9c2d6; font-size: 13px; margin-top: 6px; }}
|
|
main {{ max-width: 1100px; margin: 0 auto; padding: 20px 40px; }}
|
|
.kpis {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 12px; margin: 20px 0; }}
|
|
.kpi {{ background: white; padding: 16px; border-radius: 6px; box-shadow: 0 1px 3px rgba(0,0,0,0.07); }}
|
|
.kpi-value {{ font-size: 28px; font-weight: 700; }}
|
|
.kpi-label {{ color: #555; font-size: 13px; margin-top: 4px; text-transform: uppercase; letter-spacing: 0.5px; }}
|
|
.kpi-sub {{ color: #888; font-size: 12px; margin-top: 2px; }}
|
|
section {{ background: white; padding: 16px 24px; border-radius: 6px; margin-bottom: 16px; box-shadow: 0 1px 3px rgba(0,0,0,0.06); }}
|
|
h2 {{ color: #1f3864; margin-top: 0; font-size: 17px; }}
|
|
ul {{ list-style: none; padding-left: 0; }}
|
|
li {{ padding: 4px 0; }}
|
|
a {{ color: #1f3864; text-decoration: none; border-bottom: 1px dotted #1f3864; }}
|
|
a:hover {{ background: #ffea8a; }}
|
|
.cnt {{ color: #888; font-size: 13px; }}
|
|
.missing {{ color: #c00; font-style: italic; }}
|
|
.cols {{ display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }}
|
|
@media (max-width: 700px) {{ .cols {{ grid-template-columns: 1fr; }} }}
|
|
footer {{ text-align: center; color: #888; padding: 30px; font-size: 13px; }}
|
|
.banner {{ background: #fff3cd; padding: 10px 24px; border-left: 4px solid #ff9800; margin-bottom: 16px; border-radius: 4px; }}
|
|
</style></head>
|
|
<body>
|
|
<header>
|
|
<h1>demo-epb — Elektrische Parkbremse</h1>
|
|
<div class='meta'>Version <strong>{html.escape(tag)}</strong> · Commit <code>{html.escape(sha)}</code> · Generiert {html.escape(now)}</div>
|
|
</header>
|
|
<main>
|
|
|
|
<div class='banner'>
|
|
<strong>Demo-Projekt:</strong> Vollstaendige Demo des
|
|
<a href='https://gitea.slohmaier.com/slohmaier/dev-process'>slohmaier Dev Process</a>.
|
|
Diese Software ist bewusst kein Produktivcode, sondern Showcase der Engineering-Methodik.
|
|
</div>
|
|
|
|
<div class='kpis'>
|
|
{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")}
|
|
</div>
|
|
|
|
<div class='cols'>
|
|
|
|
<section>
|
|
<h2>Plaene (Word)</h2>
|
|
<ul>
|
|
"""
|
|
for name, href in plans:
|
|
if not href.startswith("docs/safety") and not href.startswith("docs/manuals"):
|
|
html_body += f" <li><a href='{html.escape(href)}'>{html.escape(name)}</a></li>\n"
|
|
html_body += " </ul></section>\n"
|
|
|
|
html_body += doc_section("Funktionale Sicherheit (Word)", safety,
|
|
"HARA, Safety Case, FMEDA, Compliance, Verification, Tool-Qualification")
|
|
|
|
html_body += "</div><div class='cols'>"
|
|
|
|
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 += "</div>"
|
|
|
|
# Reports — Links immer setzen, Bundle-Pfade.
|
|
html_body += "<section><h2>Engineering-Reports (CI-generiert)</h2><ul>\n"
|
|
html_body += report_link("Traceability-Matrix", rep_paths["trace"], True,
|
|
"SG -> SYS -> SA, SWE -> SWA -> Code+Test, bidirektional verifiziert")
|
|
html_body += report_link("Test-Summary", rep_paths["test"], True,
|
|
f"{n_tests} Unit-Tests mit Anforderungs-Mapping")
|
|
html_body += report_link("Coverage (gcov/lcov)", rep_paths["coverage"], True,
|
|
"Statement + Branch Coverage, klickbar bis Zeilen-Level")
|
|
html_body += report_link("API-Dokumentation (Doxygen)", rep_paths["api"], True,
|
|
"Alle Header + Funktionen, mit @arch/@reqs/@asil")
|
|
html_body += report_link("Cppcheck-Report (HTML)", "reports/cppcheck/index.html", True,
|
|
"Statische Analyse + MISRA-Findings, klickbar pro Datei")
|
|
html_body += report_link("Cppcheck-Report (XML, Roh)", rep_paths["cppcheck"], True,
|
|
"Maschinen-lesbares Format fuer eigene Tools")
|
|
html_body += "</ul></section>"
|
|
|
|
# Diagrams
|
|
diagrams = sorted((REPO / "docs" / "diagrams").glob("*.svg"))
|
|
if diagrams:
|
|
html_body += "<section><h2>Architektur-Diagramme (PlantUML)</h2><ul>"
|
|
for d in diagrams:
|
|
href = os.path.relpath(d, REPO)
|
|
html_body += f" <li><a href='{html.escape(href)}'>{html.escape(d.stem)}</a></li>\n"
|
|
html_body += "</ul></section>"
|
|
|
|
# Source code links
|
|
html_body += """
|
|
<section>
|
|
<h2>Source-Code</h2>
|
|
<ul>
|
|
<li><a href='src/safety_manager.c'>safety_manager.c</a> — Safety Manager (ASIL-D, Hill-Hold + Auto-Apply + Drive-Away)</li>
|
|
<li><a href='src/apply_controller.c'>apply_controller.c</a> — Apply Controller (ASIL-D, State Machine)</li>
|
|
<li><a href='src/actuator_driver.c'>actuator_driver.c</a> — Actuator Driver (ASIL-B, Overcurrent-Cutoff)</li>
|
|
<li><a href='src/switch_debouncer.c'>switch_debouncer.c</a> — Switch Debouncer (QM)</li>
|
|
<li class='cnt'>Plus 6 Stub-Header in <a href='src/stubs/'>src/stubs/</a></li>
|
|
</ul>
|
|
</section>
|
|
"""
|
|
|
|
html_body += f"""
|
|
<section>
|
|
<h2>Externe Links</h2>
|
|
<ul>
|
|
<li><a href='https://gitea.slohmaier.com/slohmaier/demo-epb'>Gitea-Repo</a></li>
|
|
<li><a href='https://gitea.slohmaier.com/slohmaier/demo-epb/releases'>Releases</a></li>
|
|
<li><a href='https://gitea.slohmaier.com/slohmaier/dev-process'>Methodik-Repo (dev-process)</a></li>
|
|
</ul>
|
|
</section>
|
|
|
|
</main>
|
|
|
|
<footer>
|
|
Build {html.escape(now)} · demo-epb {html.escape(tag)} ({html.escape(sha)}) · slohmaier.com
|
|
</footer>
|
|
|
|
</body></html>
|
|
"""
|
|
|
|
out = BUILD / "index.html"
|
|
out.write_text(html_body)
|
|
print(f"Wrote {out}")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|