From f5bfc0d9c26396236380fca5083d99e5df4d205b Mon Sep 17 00:00:00 2001 From: Sasha Malahov Date: Fri, 14 Aug 2026 12:17:06 -0400 Subject: [PATCH 1/5] feat: restyle dashboard with light terminal aesthetic and fix .venv crawl bug - Source styling from personal-index: monospace font, grid cards, section headers - Light theme with green accent (#2d7d46) on #fafbfc background - Module grid cards replace table rows for better readability - Fix dashboard crawler to skip .venv directories - All 26 tests pass --- alloc/lib/dashboard.py | 3 +- alloc/lib/publish_dashboard.py | 514 ++++++++++++++++++--------------- 2 files changed, 291 insertions(+), 226 deletions(-) diff --git a/alloc/lib/dashboard.py b/alloc/lib/dashboard.py index fb43577..f90e60e 100644 --- a/alloc/lib/dashboard.py +++ b/alloc/lib/dashboard.py @@ -251,7 +251,8 @@ def crawl_package( # Collect all .py files (skip __pycache__) py_files = sorted( - p for p in pkg.rglob("*.py") if "__pycache__" not in p.parts + p for p in pkg.rglob("*.py") + if "__pycache__" not in p.parts and ".venv" not in p.parts ) modules: list[ModuleStats] = [] diff --git a/alloc/lib/publish_dashboard.py b/alloc/lib/publish_dashboard.py index a8a51b7..7caf25f 100644 --- a/alloc/lib/publish_dashboard.py +++ b/alloc/lib/publish_dashboard.py @@ -104,8 +104,8 @@ def _signal_badge(signal: str) -> str: ) -def _module_row(mod: dict[str, Any]) -> str: - """Return an HTML table row for a single module. +def _module_card(mod: dict[str, Any]) -> str: + """Return an HTML module card div. Parameters ---------- @@ -115,46 +115,45 @@ def _module_row(mod: dict[str, Any]) -> str: Returns ------- str - HTML ```` element. + HTML ``
`` card element. """ path = _escape_html(mod.get("path", "?")) lines = mod.get("lines", 0) functions = mod.get("functions", 0) classes = mod.get("classes", 0) tests = mod.get("test_count", 0) - has_doc = mod.get("has_docstring", False) signals = mod.get("signals", []) - # Docstring indicator - doc_icon = "✅" if has_doc else "❌" - - # Test coverage bar - test_pct = min(tests * 10, 100) if tests > 0 else 0 - test_bar = ( - f'
' - f'
' - f"
" - ) - # Signal badges badges_html = " ".join(_signal_badge(s) for s in signals) - signals_cell = badges_html if badges_html else '✅ clear' + if badges_html: + status_class = "mod-signals" + status_html = badges_html + else: + status_class = "clear" + status_html = "✅ clear" + + # Border color based on signals + border_color = "#22c55e" if not signals else "#c0392b" return ( - f"" - f"{path}" - f"{lines}" - f"{functions}" - f"{classes}" - f"{doc_icon}" - f"{tests} {test_bar}" - f"{signals_cell}" - f"" + f'
' + f'
' + f'{path}' + f'{status_html}' + f'
' + f'
' + f'CLASSES: {classes}' + f'FUNCS: {functions}' + f'LINES: {lines}' + f'TESTS: {tests}' + f'
' + f'
' ) -def _summary_card(title: str, value: str, icon: str = "") -> str: - """Return an HTML summary card div. +def _summary_card(title: str, value: str, icon: str = "", card_class: str = "") -> str: + """Return an HTML stat card div. Parameters ---------- @@ -164,23 +163,25 @@ def _summary_card(title: str, value: str, icon: str = "") -> str: Card value (e.g. "18"). icon : str Optional emoji icon. + card_class : str + Additional CSS class (e.g. "warn", "err"). Returns ------- str HTML ``
`` card element. """ + cls = f"stat-card {card_class}" if card_class else "stat-card" return ( - f'
' - f"
{icon}
" - f"
{_escape_html(value)}
" - f"
{_escape_html(title)}
" + f'
' + f"
{_escape_html(value)}
" + f"
{_escape_html(title)}
" f"
" ) def _signal_summary_cards(signals_summary: dict[str, int]) -> str: - """Return HTML cards for each signal type count. + """Return HTML stat cards for each signal type count. Parameters ---------- @@ -193,22 +194,18 @@ def _signal_summary_cards(signals_summary: dict[str, int]) -> str: HTML fragment with signal summary cards. """ if not signals_summary: - return '
0
' \ - '
All Clear ✅
' + return '
0
' \ + '
All Clear
' cards = [] for key in sorted(signals_summary.keys()): count = signals_summary[key] - bg, fg = SIGNAL_COLORS.get(key, ("#e9ecef", "#495057")) - icon = SIGNAL_ICONS.get(key, "⚠️") label = SIGNAL_LABELS.get(key, key) cards.append( - f'
' - f"
{icon}
" - f"
{count}
" - f"
{label}
" - f"
" + f'
' + f'
{count}
' + f'
{label}
' + f'
' ) return "\n".join(cards) @@ -242,9 +239,6 @@ def generate_html(metadata: dict[str, Any]) -> str: signals_summary = metadata.get("signals_summary", {}) modules = metadata.get("modules", []) - # Module rows - module_rows = "\n".join(_module_row(m) for m in modules) - # Summary cards summary_cards = "\n".join([ _summary_card("Modules", str(total_modules), "📁"), @@ -260,218 +254,305 @@ def generate_html(metadata: dict[str, Any]) -> str: # Timestamp now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC") + # Module cards (grid layout, inspired by personal-index) + module_cards = "\n".join(_module_card(m) for m in modules) + html = f""" -{pkg} — Health Dashboard +{pkg} // HEALTH DASHBOARD
- -
-

📊 {pkg} Health Dashboard

-
Generated: {now}
-
- -
- {summary_cards} -
+ +
+

{pkg} // HEALTH DASHBOARD

+
codebase projection — {total_modules} modules scanned · {now}
+
- -
- {signal_cards} + +
+
Surface Stats
+
+ {summary_cards}
+
- -
-

Signal Legend

-
🧪 S1 — No Tests
-
📦 S2 — Oversized - (>200 lines & >15 funcs)
-
💀 S3 — Dead Code (0 internal imports)
-
🔧 S4 — Lint/Type Errors
+ +
+
Health Signals
+ {signal_cards} +
+
🧪 S1 — No Tests: {signals_summary.get("S1", 0)}
+
📦 S2 — Oversized: {signals_summary.get("S2", 0)}
+
💀 S3 — Dead Code: {signals_summary.get("S3", 0)}
+
🔧 S4 — Lint Errors: {signals_summary.get("S4", 0)}
+
- + +
+
Module Map
- - +
- - -
- - - - - - - - - - - - - - {module_rows} - -
ModuleLinesFuncsClassesDocTestsSignals
+
+ {module_cards}
+
- - + +
From 36d67fbaea4bcab691628b0a616cd6f13acbab5e Mon Sep 17 00:00:00 2001 From: Sasha Malahov Date: Fri, 14 Aug 2026 12:27:29 -0400 Subject: [PATCH 2/5] feat: add multi-package comparison dashboard and increase font sizes - Add --compare CLI flag to compare multiple codebases side-by-side - Shape Comparison table shows metrics across packages - Increased base font from 13px to 15px, stat values to 2rem - Module cards and controls sized up for readability - Comparison mode renders separate module grids per package - All 26 tests pass --- alloc/lib/publish_dashboard.py | 430 +++++++++++++++++---------------- 1 file changed, 227 insertions(+), 203 deletions(-) diff --git a/alloc/lib/publish_dashboard.py b/alloc/lib/publish_dashboard.py index 7caf25f..5d1dcca 100644 --- a/alloc/lib/publish_dashboard.py +++ b/alloc/lib/publish_dashboard.py @@ -5,6 +5,8 @@ colour-coded by severity. An optional ``--sync`` CLI flag pushes the generated HTML to a ``gh-pages`` branch for GitHub Pages hosting. +Supports single-package view and multi-package comparison mode. + Usage ----- from alloc.lib.publish_dashboard import generate_html, publish @@ -15,6 +17,7 @@ Or from CLI:: python -m alloc.lib.publish_dashboard [--json PATH] [--output PATH] [--sync] + python -m alloc.lib.publish_dashboard --compare alloc/docs_dashboard_metadata.json new-trader.json Signals ------- @@ -81,42 +84,18 @@ def _escape_html(text: str) -> str: def _signal_badge(signal: str) -> str: - """Return an HTML span for a single signal badge. - - Parameters - ---------- - signal : str - Signal string like ``"S1:no_tests"`` or ``"S4:lint_errors(3)"``. - - Returns - ------- - str - HTML ```` element with colour-coded styling. - """ + """Return an HTML span for a single signal badge.""" key = signal.split(":")[0] label = signal.split(":", 1)[1] if ":" in signal else key - bg, fg = SIGNAL_COLORS.get(key, ("#e9ecef", "#495057")) icon = SIGNAL_ICONS.get(key, "⚠️") return ( - f'' + f'' f"{icon} {_escape_html(label)}" ) def _module_card(mod: dict[str, Any]) -> str: - """Return an HTML module card div. - - Parameters - ---------- - mod : dict - One module entry from the dashboard metadata. - - Returns - ------- - str - HTML ``
`` card element. - """ + """Return an HTML module card div.""" path = _escape_html(mod.get("path", "?")) lines = mod.get("lines", 0) functions = mod.get("functions", 0) @@ -124,23 +103,19 @@ def _module_card(mod: dict[str, Any]) -> str: tests = mod.get("test_count", 0) signals = mod.get("signals", []) - # Signal badges badges_html = " ".join(_signal_badge(s) for s in signals) if badges_html: - status_class = "mod-signals" status_html = badges_html else: - status_class = "clear" - status_html = "✅ clear" + status_html = 'clear' - # Border color based on signals border_color = "#22c55e" if not signals else "#c0392b" return ( f'
' f'
' f'{path}' - f'{status_html}' + f'{status_html}' f'
' f'
' f'CLASSES: {classes}' @@ -152,47 +127,19 @@ def _module_card(mod: dict[str, Any]) -> str: ) -def _summary_card(title: str, value: str, icon: str = "", card_class: str = "") -> str: - """Return an HTML stat card div. - - Parameters - ---------- - title : str - Card title (e.g. "Modules"). - value : str - Card value (e.g. "18"). - icon : str - Optional emoji icon. - card_class : str - Additional CSS class (e.g. "warn", "err"). - - Returns - ------- - str - HTML ``
`` card element. - """ +def _summary_card(title: str, value: str, card_class: str = "") -> str: + """Return an HTML stat card div.""" cls = f"stat-card {card_class}" if card_class else "stat-card" return ( f'
' - f"
{_escape_html(value)}
" - f"
{_escape_html(title)}
" - f"
" + f'
{_escape_html(value)}
' + f'
{_escape_html(title)}
' + f'
' ) def _signal_summary_cards(signals_summary: dict[str, int]) -> str: - """Return HTML stat cards for each signal type count. - - Parameters - ---------- - signals_summary : dict - Mapping of signal key (S1-S4) to count. - - Returns - ------- - str - HTML fragment with signal summary cards. - """ + """Return HTML stat cards for each signal type count.""" if not signals_summary: return '
0
' \ '
All Clear
' @@ -210,25 +157,65 @@ def _signal_summary_cards(signals_summary: dict[str, int]) -> str: return "\n".join(cards) -def generate_html(metadata: dict[str, Any]) -> str: - """Generate a standalone HTML dashboard page from metadata. +def _comparison_section(packages: list[dict[str, Any]]) -> str: + """Generate a comparison table between multiple packages.""" + rows = [] + metrics = [ + ("Modules", "total_modules"), + ("Lines", "total_lines"), + ("Functions", "total_functions"), + ("Classes", "total_classes"), + ("Tests", "total_tests"), + ("S1: No Tests", "signals_summary.S1"), + ("S2: Oversized", "signals_summary.S2"), + ("S3: Dead Code", "signals_summary.S3"), + ("S4: Lint Errors", "signals_summary.S4"), + ] + + headers = "".join( + f'{_escape_html(pkg.get("package", "?"))}' + for pkg in packages + ) + + for label, key in metrics: + cells = [] + for pkg in packages: + if "." in key: + parent, child = key.split(".") + val = pkg.get(parent, {}).get(child, 0) + else: + val = pkg.get(key, 0) + formatted = f"{val:,}" if isinstance(val, int) and val > 100 else str(val) + cells.append(f'{formatted}') - The output is a self-contained HTML document with inline CSS and JS — - no external dependencies required. Works on mobile and desktop via - responsive CSS Grid/Flexbox layout. + cells.insert(0, f'{label}') + rows.append("" + "".join(cells) + "") + + return ( + f'
' + f'
Shape Comparison
' + f'
' + f'' + f'{headers}' + f'{"".join(rows)}' + f'
Metric
' + f'
' + f'
' + ) + + +def generate_html( + metadata: dict[str, Any], + compare_with: list[dict[str, Any]] | None = None, +) -> str: + """Generate a standalone HTML dashboard page from metadata. Parameters ---------- metadata : dict The deserialised JSON output of :func:`dashboard.generate_json`. - Must contain keys: ``package``, ``total_modules``, ``total_lines``, - ``total_functions``, ``total_classes``, ``total_tests``, - ``signals_summary``, ``modules``. - - Returns - ------- - str - Complete HTML document string. + compare_with : list of dict, optional + Additional package metadata dicts to compare against. """ pkg = _escape_html(metadata.get("package", "unknown")) total_modules = metadata.get("total_modules", 0) @@ -239,23 +226,42 @@ def generate_html(metadata: dict[str, Any]) -> str: signals_summary = metadata.get("signals_summary", {}) modules = metadata.get("modules", []) - # Summary cards + module_cards = "\n".join(_module_card(m) for m in modules) + summary_cards = "\n".join([ - _summary_card("Modules", str(total_modules), "📁"), - _summary_card("Lines", f"{total_lines:,}", "📝"), - _summary_card("Functions", str(total_functions), "⚙️"), - _summary_card("Classes", str(total_classes), "🏗️"), - _summary_card("Tests", str(total_tests), "🧪"), + _summary_card("Modules", str(total_modules)), + _summary_card("Lines", f"{total_lines:,}"), + _summary_card("Functions", str(total_functions)), + _summary_card("Classes", str(total_classes)), + _summary_card("Tests", str(total_tests)), ]) - # Signal summary signal_cards = _signal_summary_cards(signals_summary) - # Timestamp now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC") - # Module cards (grid layout, inspired by personal-index) - module_cards = "\n".join(_module_card(m) for m in modules) + # Build comparison section if multiple packages + comparison_html = "" + all_packages = [metadata] + (compare_with or []) + if len(all_packages) > 1: + comparison_html = _comparison_section(all_packages) + + # Build additional package sections + other_sections = "" + for other in (compare_with or []): + other_pkg = _escape_html(other.get("package", "unknown")) + other_modules = other.get("modules", []) + other_cards = "\n".join(_module_card(m) for m in other_modules) + other_total_m = other.get("total_modules", 0) + + other_sections += f""" +
+
{other_pkg} — Module Map ({other_total_m} modules)
+
+ {other_cards} +
+
+""" html = f""" @@ -280,26 +286,26 @@ def generate_html(metadata: dict[str, Any]) -> str: background: #fafbfc; color: #1a1a2e; font-family: 'SF Mono', 'Fira Code', 'Courier New', Courier, monospace; - font-size: 13px; - line-height: 1.5; + font-size: 15px; + line-height: 1.6; min-height: 100vh; }} .dashboard {{ - max-width: 1200px; + max-width: 1300px; margin: 0 auto; - padding: 2rem 1.5rem; + padding: 2rem 2rem; }} /* ---- HEADER ---- */ .header {{ border-bottom: 2px solid #2d7d46; - padding-bottom: 1rem; - margin-bottom: 2rem; + padding-bottom: 1.2rem; + margin-bottom: 2.5rem; }} .header h1 {{ - font-size: 1.4rem; + font-size: 1.8rem; font-weight: 400; letter-spacing: 2px; text-transform: uppercase; @@ -308,8 +314,8 @@ def generate_html(metadata: dict[str, Any]) -> str: .header .prompt {{ color: #5a7a6a; - font-size: 0.8rem; - margin-top: 0.4rem; + font-size: 0.95rem; + margin-top: 0.5rem; }} .header .prompt::before {{ @@ -319,18 +325,18 @@ def generate_html(metadata: dict[str, Any]) -> str: /* ---- SECTION HEADERS ---- */ .section {{ - margin-bottom: 2.5rem; + margin-bottom: 3rem; }} .section-title {{ - font-size: 0.9rem; + font-size: 1.1rem; font-weight: 400; letter-spacing: 1.5px; text-transform: uppercase; color: #2d7d46; border-bottom: 1px solid #d0e0d8; - padding-bottom: 0.35rem; - margin-bottom: 1rem; + padding-bottom: 0.4rem; + margin-bottom: 1.2rem; }} .section-title::before {{ @@ -341,13 +347,13 @@ def generate_html(metadata: dict[str, Any]) -> str: /* ---- SURFACE STATS ---- */ .stats-grid {{ display: grid; - grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); - gap: 0.65rem; + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + gap: 0.8rem; }} .stat-card {{ border: 1px solid #d0e0d8; - padding: 0.7rem; + padding: 0.9rem; text-align: center; background: #fff; transition: border-color 0.2s; @@ -358,17 +364,17 @@ def generate_html(metadata: dict[str, Any]) -> str: }} .stat-value {{ - font-size: 1.6rem; + font-size: 2rem; color: #2d7d46; font-weight: 400; }} .stat-label {{ - font-size: 0.65rem; + font-size: 0.8rem; color: #5a7a6a; text-transform: uppercase; letter-spacing: 1px; - margin-top: 0.2rem; + margin-top: 0.25rem; }} .stat-card.warn .stat-value {{ @@ -382,9 +388,9 @@ def generate_html(metadata: dict[str, Any]) -> str: /* ---- SIGNAL SUMMARY ROW ---- */ .signal-summary {{ display: flex; - gap: 1rem; - margin-bottom: 0.75rem; - font-size: 0.75rem; + gap: 1.2rem; + margin-bottom: 0.8rem; + font-size: 0.9rem; color: #5a7a6a; }} @@ -393,16 +399,64 @@ def generate_html(metadata: dict[str, Any]) -> str: font-weight: 600; }} +/* ---- COMPARISON TABLE ---- */ +.comp-table-wrapper {{ + overflow-x: auto; + border: 1px solid #d0e0d8; + background: #fff; +}} + +.comp-table {{ + width: 100%; + border-collapse: collapse; +}} + +.comp-table th {{ + background: #f4f9f6; + padding: 0.8rem 1rem; + text-align: center; + font-size: 0.95rem; + font-weight: 400; + letter-spacing: 1px; + text-transform: uppercase; + color: #2d7d46; + border-bottom: 2px solid #d0e0d8; +}} + +.comp-table th.comp-label {{ + text-align: left; +}} + +.comp-table td {{ + padding: 0.6rem 1rem; + border-bottom: 1px solid #f0f4f2; + font-size: 0.95rem; +}} + +.comp-table td.comp-label {{ + color: #5a7a6a; + font-weight: 400; +}} + +.comp-table td.comp-val {{ + text-align: center; + color: #1a1a2e; +}} + +.comp-table tr:hover {{ + background: #f9fbfa; +}} + /* ---- MODULE GRID ---- */ .module-grid {{ display: grid; - grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); - gap: 0.55rem; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 0.6rem; }} .mod-card {{ border: 1px solid #d0e0d8; - padding: 0.55rem 0.75rem; + padding: 0.65rem 0.85rem; background: #fff; transition: background 0.2s, border-color 0.2s; }} @@ -416,12 +470,12 @@ def generate_html(metadata: dict[str, Any]) -> str: display: flex; justify-content: space-between; align-items: center; - margin-bottom: 0.25rem; - gap: 0.5rem; + margin-bottom: 0.3rem; + gap: 0.6rem; }} .mod-name {{ - font-size: 0.82rem; + font-size: 0.95rem; color: #2d7d46; overflow: hidden; text-overflow: ellipsis; @@ -430,7 +484,7 @@ def generate_html(metadata: dict[str, Any]) -> str: }} .mod-status {{ - font-size: 0.65rem; + font-size: 0.75rem; font-weight: 400; letter-spacing: 1px; white-space: nowrap; @@ -438,8 +492,8 @@ def generate_html(metadata: dict[str, Any]) -> str: .mod-meta {{ display: flex; - gap: 0.7rem; - font-size: 0.65rem; + gap: 0.8rem; + font-size: 0.78rem; color: #5a7a6a; flex-wrap: wrap; }} @@ -451,9 +505,9 @@ def generate_html(metadata: dict[str, Any]) -> str: /* ---- SIGNAL BADGES ---- */ .badge {{ display: inline-block; - padding: 0.1em 0.45em; + padding: 0.15em 0.5em; border-radius: 2px; - font-size: 0.62rem; + font-size: 0.7rem; font-weight: 600; letter-spacing: 0.5px; margin: 0.1em 0.1em; @@ -465,22 +519,22 @@ def generate_html(metadata: dict[str, Any]) -> str: .badge-s3 {{ background: #e8e8e8; color: #383d41; }} .badge-s4 {{ background: #f8d7da; color: #721c24; }} -.clear {{ color: #2d7d46; font-weight: 600; font-size: 0.72rem; }} +.clear {{ color: #2d7d46; font-weight: 600; font-size: 0.82rem; }} /* ---- CONTROLS ---- */ .controls {{ display: flex; - gap: 0.65rem; - margin-bottom: 1rem; + gap: 0.75rem; + margin-bottom: 1.2rem; flex-wrap: wrap; }} .controls input, .controls select {{ - padding: 0.45rem 0.65rem; + padding: 0.55rem 0.75rem; border: 1px solid #c0d0c8; border-radius: 2px; font-family: inherit; - font-size: 0.82rem; + font-size: 0.95rem; outline: none; background: #fff; }} @@ -489,21 +543,21 @@ def generate_html(metadata: dict[str, Any]) -> str: border-color: #2d7d46; }} -.controls input {{ flex: 1; min-width: 200px; }} +.controls input {{ flex: 1; min-width: 220px; }} /* ---- FOOTER ---- */ .footer {{ - margin-top: 2rem; - padding-top: 1rem; + margin-top: 2.5rem; + padding-top: 1.2rem; border-top: 1px solid #d0e0d8; color: #8aab9a; - font-size: 0.7rem; + font-size: 0.82rem; text-align: center; letter-spacing: 1px; }} /* ---- SCROLLBAR ---- */ -::-webkit-scrollbar {{ width: 5px; }} +::-webkit-scrollbar {{ width: 6px; }} ::-webkit-scrollbar-track {{ background: #fafbfc; }} ::-webkit-scrollbar-thumb {{ background: #c0d0c8; }} ::-webkit-scrollbar-thumb:hover {{ background: #8aab9a; }} @@ -511,9 +565,10 @@ def generate_html(metadata: dict[str, Any]) -> str: /* ---- RESPONSIVE ---- */ @media (max-width: 768px) {{ .dashboard {{ padding: 1rem; }} - .header h1 {{ font-size: 1.1rem; }} - .stats-grid {{ grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); gap: 0.4rem; }} + .header h1 {{ font-size: 1.3rem; }} + .stats-grid {{ grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 0.5rem; }} .module-grid {{ grid-template-columns: 1fr; }} + .signal-summary {{ flex-wrap: wrap; gap: 0.8rem; }} }} @@ -537,18 +592,23 @@ def generate_html(metadata: dict[str, Any]) -> str:
Health Signals
- {signal_cards} +
+ {signal_cards} +
-
🧪 S1 — No Tests: {signals_summary.get("S1", 0)}
-
📦 S2 — Oversized: {signals_summary.get("S2", 0)}
-
💀 S3 — Dead Code: {signals_summary.get("S3", 0)}
-
🔧 S4 — Lint Errors: {signals_summary.get("S4", 0)}
+
S1 No Tests: {signals_summary.get("S1", 0)}
+
S2 Oversized: {signals_summary.get("S2", 0)}
+
S3 Dead Code: {signals_summary.get("S3", 0)}
+
S4 Lint Errors: {signals_summary.get("S4", 0)}
- + +{comparison_html} + +
-
Module Map
+
{pkg} — Module Map ({total_modules} modules)
@@ -565,6 +625,8 @@ def generate_html(metadata: dict[str, Any]) -> str:
+{other_sections} +