master-zapier-plan-draft/scripts/build-docs-site.py
George Lambert cb07f5b321
Some checks are pending
offline / test (push) Waiting to run
Restyle staff and console UIs; add UI-Docs walkthrough and review PDF.
Match CS/sales/accounting/access-staff to the portal indigo system with
dollar amounts, skip links, and empty states. Fleet replica actions move
into overflow menus, roles become chips, Docs become cards, and the
header copy reflects the 0.0.0.0 bind. Simulator uses the same shell
(orange only for faults). Portal API keys are masked; admin customers
edit in a drawer. Catalog uses system-ui. New UI-Docs repo holds
screenshots, usage notes, and UI-REVIEW.pdf.
2026-09-11 18:01:33 -04:00

645 lines
28 KiB
Python
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""Build static documentation site into ./site for zapier.georgelambert.org.
Markdown is rendered to colored HTML *and* PDF. The home page (index.html)
defaults to PDF links; index-md.html is the Markdown/HTML index.
"""
from __future__ import annotations
import shutil
import subprocess
from concurrent.futures import ThreadPoolExecutor, as_completed
from datetime import datetime, timezone
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SITE = ROOT / "site"
CSS = ROOT / "scripts" / "docs-print.css"
SECTIONS = [
(
"Start here",
[
("packages/verae-ops/GETTING-STARTED.md", "Bring the system online (install, first-up, monitor)"),
("packages/overview/README.md", "What the system is (plain language)"),
("packages/verae-fleet/docs/CONSOLE.md", "Operator console (Fleet · Trace · Docs on :3850)"),
],
),
(
"Architecture",
[
("docs/02-architecture/composition.md", "Zappier + middleware composition"),
("docs/02-architecture/overview.md", "Middleware overview"),
("docs/02-architecture/nats-gateway.md", "NATS on NS1"),
("docs/02-architecture/nats-subjects.md", "NATS subjects"),
("docs/02-architecture/archive-nats.md", "Archive NATS, bloom, multi-receipt"),
("docs/02-architecture/modules-and-nats.md", "Module catalog and NATS addresses"),
("docs/02-architecture/tree-nodes.md", "Tree nodes and bulk Merkle summaries"),
("docs/02-architecture/fleet.md", "Fleet replica floors and monitor"),
("packages/overview/03-nats-cluster.md", "NATS.IO 3-server cluster"),
("packages/overview/04-uptime.md", "Uptime / replica floors"),
("packages/overview/05-network-failures.md", "Local network failures"),
("packages/overview/06-address-routing.md", "Address routing"),
("packages/overview/08-diagrams.md", "Architectural diagrams"),
],
),
(
"Per-module README (public PDFs)",
[
("packages/zappier/README.md", "zappier-edge (billing + users)"),
("packages/verae-zapier-middleware/README.md", "verae-middleware"),
("packages/verae-zapier/README.md", "verae-zapier-app"),
("packages/verae-activate/README.md", "verae-activate"),
("packages/verae-request-splitter/README.md", "verae-request-splitter"),
("packages/verae-archive-worm/README.md", "verae-archive-worm"),
("packages/verae-archive-aggregator/README.md", "verae-archive-aggregator"),
("packages/verae-tree-node/README.md", "verae-tree-node"),
("packages/verae-fleet/README.md", "verae-fleet"),
("packages/verae-zapier-simulator/README.md", "verae-zapier-simulator"),
("packages/zapier-user-docs/README.md", "zapier-user-docs"),
("packages/docs-master/README.md", "zapier-docs-master"),
("packages/verae-nats-process/README.md", "verae-nats-process"),
],
),
(
"Billing, users, portal (zappier-edge)",
[
("packages/zappier/README.md", "zappier-edge README (the billing/user platform)"),
("packages/zappier/docs/USER-MANUAL.md", "Operations & usage"),
("packages/zappier/docs/USER-MANAGEMENT.md", "Admin users, pricing, customer types"),
("packages/zappier/docs/CUSTOMER-PORTAL.md", "Signup, 2FA, API keys, reloads"),
("packages/zappier/docs/ACCOUNTING.md", "Invoices, PO billing, reports"),
("packages/zappier/docs/DEVELOPER.md", "Developer reference"),
("packages/zappier-account-balance/README.md", "Account balance (NATS SoT)"),
("packages/zappier-customer-service/README.md", "Customer-service credits API"),
("packages/zappier-sales-pricing/README.md", "Sales per-customer pricing API"),
("packages/zappier-accounting-export/README.md", "QuickBooks / accounting export"),
("packages/verae-access-authz/README.md", "Access-plane authorization"),
("packages/verae-access-web/README.md", "Direct web access plane"),
("packages/verae-access-api/README.md", "Direct customer API plane"),
("packages/verae-access-leaf/README.md", "S2S NATS leaf plane"),
("packages/verae-access-zapier/README.md", "Zapier-only access plane"),
("packages/verae-access-staff/README.md", "Staff access plane"),
("packages/zappier-identity/README.md", "veraeUserId identity mailbox"),
("packages/verae-jobs-events/README.md", "jobs.events mailbox"),
("packages/verae-nats-accounts/README.md", "NATS leaf/internal policy"),
("packages/zapier-decisions/README.md", "Decisions log"),
("packages/ui-docs/README.md", "UI walkthrough, screenshots, review PDF"),
],
),
(
"Install (detail)",
[
("packages/verae-ops/GETTING-STARTED.md", "Bring online (start here)"),
("packages/verae-ops/02-docker.md", "Docker Compose"),
("packages/verae-ops/03-proxmox.md", "Proxmox"),
("packages/verae-ops/05-dedicated-hardware.md", "Dedicated hardware"),
("packages/verae-ops/06-linking-services.md", "Linking services"),
],
),
(
"Samples",
[
("packages/zappier/zapier-app/README.md", "Sample Zapier app (metered zappier API)"),
("packages/verae-activate/README.md", "Activate-now Add Numbers"),
("packages/verae-zapier-simulator/README.md", "Hop-trace simulator"),
],
),
(
"User guide (signup → lookup)",
[
("packages/zapier-user-docs/README.md", "User guide index"),
("packages/zapier-user-docs/02-signup-zappier-portal.md", "Sign up"),
("packages/zapier-user-docs/04-register-a-hash.md", "Register a SHA-256"),
("packages/zapier-user-docs/06-lookup-central-chain.md", "Central chain lookup"),
("packages/zapier-user-docs/09-lookup-tree-nodes.md", "Tree-node lookup"),
],
),
(
"Product",
[
("docs/01-product/features-a-m.md", "Features am"),
("docs/01-product/api-gap-analysis.md", "Verae OpenAPI gaps"),
("docs/01-product/billing-and-keys.md", "Billing and API keys"),
("docs/00-sources/workspace-brief.md", "Original brief"),
("docs/00-sources/provenance.md", "Provenance"),
],
),
(
"API specs",
[
("docs/api/middleware-openapi.yaml", "Middleware OpenAPI"),
("packages/zappier/openapi.yaml", "Zappier OpenAPI"),
("docs/00-sources/veraetime-openapi.yaml", "Verae Timestamping OpenAPI snapshot"),
],
),
]
def rel(p: Path) -> str:
return p.relative_to(ROOT).as_posix()
def copy_tree(src: Path, dest: Path, ignore=None) -> None:
if not src.exists():
return
dest.parent.mkdir(parents=True, exist_ok=True)
if src.is_file():
shutil.copy2(src, dest)
return
shutil.copytree(src, dest, dirs_exist_ok=True, ignore=ignore)
def site_href_for(path: str, src: Path) -> tuple[str, str]:
"""Return (pdf_or_native_href, md_or_html_href) relative to SITE."""
suffix = src.suffix.lower()
if suffix == ".md":
pdf = Path(path).with_suffix(".pdf").as_posix()
html = Path(path).with_suffix(".html").as_posix()
return pdf, html
return path, path
def render_md(src: Path, html_dest: Path, pdf_dest: Path, title: str) -> str:
"""Pandoc markdown → colored HTML + WeasyPrint PDF. Returns '' on success."""
html_dest.parent.mkdir(parents=True, exist_ok=True)
pdf_dest.parent.mkdir(parents=True, exist_ok=True)
header = html_dest.with_suffix(".hdr.html")
banner = html_dest.with_suffix(".ban.html")
rel_src = src.relative_to(ROOT).as_posix() if src.is_relative_to(ROOT) else src.name
css_text = CSS.read_text(encoding="utf-8")
header.write_text(f"<style>{css_text}</style>\n", encoding="utf-8")
banner.write_text(
f'<div class="doc-banner">'
f'<nav class="site"><a href="/">zapier.georgelambert.org</a>'
f' · <a href="/index-md.html">Markdown indexes</a></nav>'
f'<div class="kicker">Verae Time × Zapier</div>'
f"<h1>{title}</h1>"
f'<div class="source-path">{rel_src}</div>'
f"</div>\n",
encoding="utf-8",
)
resource = str(src.parent)
r = subprocess.run(
[
"pandoc",
str(src),
"-o",
str(html_dest),
"--standalone",
f"--resource-path={resource}",
"--syntax-highlighting=breezedark",
f"--metadata=title={title}",
f"--include-in-header={header}",
f"--include-before-body={banner}",
f"--lua-filter={ROOT / 'scripts' / 'pdf-links.lua'}",
],
capture_output=True,
text=True,
)
header.unlink(missing_ok=True)
banner.unlink(missing_ok=True)
if r.returncode != 0:
return f"pandoc {src}: {r.stderr[-400:]}"
w = subprocess.run(
["weasyprint", str(html_dest), str(pdf_dest)],
capture_output=True,
text=True,
)
if w.returncode != 0:
return f"weasyprint {src}: {w.stderr[-400:]}"
return ""
def convert_all_markdown(copied: list[tuple[Path, Path, str]]) -> list[str]:
errors = []
n = len(copied)
print(f"rendering {n} markdown files to HTML + PDF…")
with ThreadPoolExecutor(max_workers=6) as pool:
futs = {
pool.submit(render_md, src, html, pdf, title): src
for src, html, pdf, title in (
(
src,
dest.with_suffix(".html"),
dest.with_suffix(".pdf"),
title,
)
for src, dest, title in copied
)
}
done = 0
for fut in as_completed(futs):
err = fut.result()
done += 1
if err:
errors.append(err)
print(f" [{done}/{n}] FAIL {futs[fut].name}")
elif done % 25 == 0 or done == n:
print(f" [{done}/{n}]")
return errors
def format_switch(to_md: bool) -> str:
if to_md:
href, label = "/index-md.html", "Markdown indexes"
else:
href, label = "/", "PDF catalog"
return (
f'<a class="format-switch" href="{href}">{label}</a>'
)
def page_shell(now: str, body: str, switch_to_md: bool, title: str) -> str:
switch = format_switch(switch_to_md)
return f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>{title}</title>
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='8' fill='%234f46e5'/%3E%3C/svg%3E"/>
<style>
:root {{ --ink:#171a26; --muted:#5b6178; --line:#d9dce8; --bg:#f4f5fb; --accent:#4f46e5; --accent-deep:#312e81; --soft:#eef0fe; }}
body {{ margin:0; font:16px/1.5 -apple-system, "SF Pro Text", "Segoe UI", "PingFang SC", sans-serif; color:var(--ink); background:var(--bg); }}
.skip {{ position:absolute; left:-999px; }}
.skip:focus {{ left:1rem; top:1rem; z-index:20; background:#fff; color:var(--accent); padding:.5rem .9rem; border-radius:8px; }}
a:focus-visible, input:focus-visible {{ outline:2px solid #fff; outline-offset:2px; }}
header {{ background:linear-gradient(160deg,#312e81 0%,#4f46e5 60%,#7c74f0 100%); color:#eef0fe; padding:2rem 1.5rem 1.4rem; position:relative; }}
header p {{ max-width:44rem; color:#e4e7ff; }}
.kicker {{ letter-spacing:.12em; text-transform:uppercase; font:700 11px system-ui; opacity:.75; }}
.format-switch {{
position:absolute; top:1.25rem; right:1.5rem;
font:650 13px system-ui, sans-serif; color:#fff; text-decoration:none;
background:rgba(255,255,255,.16); border:1px solid rgba(255,255,255,.35);
padding:.4rem .75rem; border-radius:999px;
}}
.format-switch:hover {{ background:rgba(255,255,255,.28); }}
main {{ max-width:52rem; margin:0 auto; padding:1.5rem 1.25rem 3rem; }}
h1 {{ margin:.4rem 0 .6rem; font-size:1.85rem; }}
h2 {{ border-top:1px solid var(--line); padding-top:.8rem; margin-top:1.8rem; color:var(--accent-deep); }}
ul {{ padding-left:1.2rem; }}
li {{ margin:.35rem 0; }}
a {{ color:var(--accent); }}
.path {{ color:var(--muted); font:12px ui-monospace, Menlo, monospace; margin-left:.4rem; }}
.badge {{ font:700 10px system-ui; letter-spacing:.04em; text-transform:uppercase;
background:var(--soft); color:var(--accent); padding:.1rem .35rem; border-radius:4px; margin-left:.25rem; }}
input {{ width:100%; padding:.55rem .7rem; font:16px system-ui; border:1px solid var(--line); border-radius:6px; }}
</style>
</head>
<body>
<a class="skip" href="#main">Skip to content</a>
<header>
{switch}
<div class="kicker">zapier.georgelambert.org</div>
<h1>Verae Time × Zapier documentation</h1>
<p>Project reference: setup, architecture, NATS, module APIs, user guide, and operator console.
Catalog defaults to <strong>colored PDFs</strong>. Generated {now}.</p>
</header>
<main id="main">
<p><input id="q" type="search" placeholder="Filter links…" /></p>
{body}
</main>
<script>
const q = document.getElementById('q');
q.addEventListener('input', () => {{
const v = q.value.toLowerCase();
document.querySelectorAll('li').forEach(li => {{
li.style.display = li.textContent.toLowerCase().includes(v) ? '' : 'none';
}});
}});
</script>
</body>
</html>
"""
def main() -> None:
if SITE.exists():
shutil.rmtree(SITE)
SITE.mkdir()
copy_tree(ROOT / "docs", SITE / "docs", ignore=shutil.ignore_patterns("_build", ".DS_Store"))
sphinx = ROOT / "docs" / "sphinx" / "_build" / "html"
if sphinx.exists():
copy_tree(sphinx, SITE / "sphinx")
latex_pdf = ROOT / "docs" / "sphinx" / "_build" / "latex" / "verae-zapier-modules.pdf"
if latex_pdf.exists():
copy_tree(latex_pdf, SITE / "sphinx" / "verae-zapier-modules.pdf")
for pkg in (
"verae-activate",
"verae-zapier",
"verae-zapier-middleware",
"zappier",
"verae-request-splitter",
"verae-archive-worm",
"verae-archive-aggregator",
"verae-tree-node",
"verae-zapier-simulator",
"zapier-user-docs",
"verae-fleet",
"overview",
"verae-nats-process",
"docs-master",
"verae-ops",
"zappier-account-balance",
"zappier-customer-service",
"zappier-sales-pricing",
"zappier-accounting-export",
"verae-access-authz",
"verae-access-web",
"verae-access-api",
"verae-access-leaf",
"verae-access-zapier",
"verae-access-staff",
"zappier-identity",
"verae-jobs-events",
"verae-nats-accounts",
"zapier-decisions",
):
pkg_root = ROOT / "packages" / pkg
if pkg in {"zapier-user-docs", "overview", "docs-master", "verae-ops"}:
copy_tree(
pkg_root,
SITE / "packages" / pkg,
ignore=shutil.ignore_patterns("node_modules", ".git", "src", "test", "data"),
)
elif pkg == "verae-fleet":
for name in ("README.md", "SUMMARY.md", "NATS.md", "SERVICES.md"):
p = pkg_root / name
if p.exists():
copy_tree(p, SITE / "packages" / pkg / name)
copy_tree(
pkg_root / "docs",
SITE / "packages" / pkg / "docs",
ignore=shutil.ignore_patterns("node_modules"),
)
elif pkg == "verae-nats-process":
for p in pkg_root.glob("*.md"):
copy_tree(p, SITE / "packages" / pkg / p.name)
else:
for name in ("README.md", "SUMMARY.md", "NATS.md"):
p = pkg_root / name
if p.exists():
copy_tree(p, SITE / "packages" / pkg / name)
zap_app = ROOT / "packages" / "zappier" / "zapier-app" / "README.md"
if zap_app.exists():
copy_tree(zap_app, SITE / "packages" / "zappier" / "zapier-app" / "README.md")
zdocs = ROOT / "packages" / "zappier" / "docs"
if zdocs.exists():
copy_tree(
zdocs,
SITE / "packages" / "zappier" / "docs",
ignore=shutil.ignore_patterns("screenshots", "walkthrough", "superpowers"),
)
for name in ("README.md", "TODO.md", "OPEN.md"):
src = ROOT / name
if not src.exists() and name == "OPEN.md":
src = ROOT / "docs" / "OPEN.md"
if src.exists():
copy_tree(src, SITE / src.name if src.parent == ROOT else SITE / "docs" / "OPEN.md")
copy_tree(ROOT / "docs" / "WORK-LOG.md", SITE / "docs" / "WORK-LOG.md")
copy_tree(ROOT / "packages" / "docs-master", SITE / "docs-master")
copy_tree(ROOT / "packages" / "overview", SITE / "overview")
copy_tree(ROOT / "packages" / "zapier-user-docs", SITE / "user-docs", ignore=shutil.ignore_patterns("node_modules"))
copy_tree(
ROOT / "packages" / "verae-fleet" / "docs",
SITE / "packages" / "verae-fleet" / "docs",
ignore=shutil.ignore_patterns("node_modules"),
)
sim_pub = ROOT / "packages" / "verae-zapier-simulator" / "public" / "index.html"
if sim_pub.exists():
copy_tree(sim_pub, SITE / "simulator" / "index.html")
r = ROOT / "research" / "zapier"
if r.exists():
for fname in (
"README.md",
"getting-started.md",
"PLATFORM-REFERENCE.md",
"FUNCTIONS-REFERENCE.md",
"MCP-REFERENCE.md",
"LOGIN.md",
"docs/zapier-billing.md",
):
p = r / fname
if p.exists():
copy_tree(p, SITE / "research" / fname)
diagrams = r / "docs" / "diagrams"
if diagrams.exists():
copy_tree(diagrams, SITE / "research" / "docs" / "diagrams")
# Render every published .md from the site copy so relative images resolve.
to_render: list[tuple[Path, Path, str]] = []
seen: set[Path] = set()
for md in SITE.rglob("*.md"):
if not md.is_file():
continue
key = md.resolve()
if key in seen:
continue
seen.add(key)
to_render.append((md, md, md.stem.replace("-", " ")))
# Prefer curated titles for catalog entries
title_map = {}
for _sec, links in SECTIONS:
for path, label in links:
title_map[path] = label
labeled = []
for src, dest, title in to_render:
try:
key = dest.relative_to(SITE).as_posix()
except ValueError:
key = dest.name
# packages/overview/... also listed as packages/overview
labeled.append((src, dest, title_map.get(key, title_map.get(src.relative_to(ROOT).as_posix() if src.is_relative_to(ROOT) else key, title))))
errors = convert_all_markdown(labeled)
# Also write CONSOLE.pdf next to the markdown in the fleet package (local console)
console_md = ROOT / "packages" / "verae-fleet" / "docs" / "CONSOLE.md"
if console_md.exists():
err = render_md(
console_md,
ROOT / "packages" / "verae-fleet" / "docs" / "CONSOLE.html",
ROOT / "packages" / "verae-fleet" / "docs" / "CONSOLE.pdf",
"Operator console",
)
if err:
errors.append(err)
now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
def cards(mode: str) -> str:
out = []
for title, links in SECTIONS:
lis = []
for path, label in links:
src = ROOT / path
if not src.exists() and path == "OPEN.md":
src = ROOT / "docs" / "OPEN.md"
path = "docs/OPEN.md"
if not src.exists():
continue
pdf_href, html_href = site_href_for(path, src)
if mode == "pdf":
href = pdf_href if (SITE / pdf_href).exists() or src.suffix.lower() in {".pdf", ".yaml", ".yml"} else html_href
badge = "PDF" if href.endswith(".pdf") else src.suffix.lstrip(".").upper() or "FILE"
else:
href = html_href if (SITE / html_href).exists() else path
badge = "MD" if src.suffix.lower() == ".md" else src.suffix.lstrip(".").upper() or "FILE"
lis.append(
f'<li><a href="{href}">{label}</a>'
f'<span class="badge">{badge}</span>'
f'<span class="path">{path}</span></li>'
)
out.append(f"<section><h2>{title}</h2><ul>{''.join(lis)}</ul></section>")
return "".join(out)
git_repos = [
("master-zapier-plan-draft", "Monorepo (this workspace)"),
("zappier-edge", "Metered HTTPS edge"),
("verae-middleware", "Zapier-facing HTTP + NATS workers"),
("verae-zapier-app", "Full Zapier Platform app"),
("verae-activate", "Activate-now Zapier app"),
("verae-request-splitter", "Hash vs attachment splitter"),
("verae-archive-worm", "Bloom-filtered WORM archive node"),
("verae-archive-aggregator", "Archive reply aggregator"),
("zapier-docs-master", "Master summaries and NATS contracts"),
("verae-tree-node", "Merkle leaf proofs (bulk summaries)"),
("verae-zapier-simulator", "Zapier interface + trace console"),
("zapier-user-docs", "Signup-to-usage user guide"),
("verae-fleet", "Service catalog, replica floors, monitor"),
("overview", "High-level system overview"),
("verae-nats-process", "Template for a new NATS address"),
("verae-ops", "Install, Docker, Proxmox, VMs, dedicated hardware"),
("zappier-account-balance", "NATS account balances"),
("zappier-customer-service", "CS credits"),
("zappier-sales-pricing", "Sales per-customer pricing"),
("zappier-accounting-export", "QuickBooks export"),
("verae-access-authz", "Access-plane authorization"),
("verae-access-web", "Direct web access"),
("verae-access-api", "Direct customer API"),
("verae-access-leaf", "S2S NATS leaf"),
("verae-access-zapier", "Zapier access plane"),
("verae-access-staff", "Staff access plane"),
("zappier-identity", "veraeUserId bind/lookup"),
("verae-jobs-events", "jobs.events mailbox"),
("verae-nats-accounts", "NATS leaf/internal policy"),
("zapier-decisions", "Decisions and action log"),
]
git_lis = "".join(
f'<li><a href="https://git.georgelambert.org/marchon/{name}">{name}</a> '
f'<span class="path">{desc}</span></li>'
for name, desc in git_repos
)
extra_pdf = [
("docs-master/README.pdf", "Master summaries (docs-master)"),
("docs-master/MESSAGE-FLOWS.pdf", "Numbered message flows"),
("docs-master/modules-and-nats.pdf", "NATS address table"),
("user-docs/README.pdf", "User guide index"),
("packages/verae-zapier-simulator/README.pdf", "Simulator README"),
("packages/verae-fleet/docs/CONSOLE.pdf", "Operator console"),
("overview/README.pdf", "System overview"),
("overview/INDEX.pdf", "Documentation index"),
("docs/modules-pdf/", "Module PDFs (book)"),
("docs/models-pdf/", "Model PDFs (book)"),
("sphinx/index.html", "Sphinx HTML"),
("sphinx/verae-zapier-modules.pdf", "Sphinx LaTeX PDF"),
]
extra_md = [
("docs-master/README.html", "Master summaries (docs-master)"),
("docs-master/MESSAGE-FLOWS.html", "Numbered message flows"),
("docs-master/modules-and-nats.html", "NATS address table"),
("user-docs/README.html", "User guide index"),
("packages/verae-zapier-simulator/README.html", "Simulator README"),
("packages/verae-fleet/docs/CONSOLE.html", "Operator console"),
("overview/README.html", "System overview"),
("overview/INDEX.html", "Documentation index"),
("docs/modules/", "All module markdown"),
("docs/models/", "All model markdown"),
("sphinx/index.html", "Sphinx HTML"),
("sphinx/verae-zapier-modules.pdf", "Sphinx LaTeX PDF"),
]
def extra_section(items: list[tuple[str, str]]) -> str:
lis = "".join(
f'<li><a href="{h}">{lab}</a></li>'
for h, lab in items
if (SITE / h).exists() or h.endswith("/") or h.startswith("sphinx")
)
return f"<section><h2>Catalogs</h2><ul>{lis}</ul></section>"
rest_pdf = f"""
{cards("pdf")}
<section>
<h2>Git repositories (Forgejo)</h2>
<p>Clone: <code>ssh://git@git.georgelambert.org:2223/marchon/&lt;name&gt;.git</code> (SSH port 2223). Branches <code>main</code> and <code>master</code>.</p>
<ul>{git_lis}</ul>
</section>
<section>
<h2>Master module docs</h2>
<ul>
<li><a href="docs-master/README.pdf">docs-master README</a><span class="badge">PDF</span></li>
<li><a href="docs-master/MESSAGE-FLOWS.pdf">MESSAGE-FLOWS</a><span class="badge">PDF</span></li>
<li><a href="docs-master/modules-and-nats.pdf">modules-and-nats</a><span class="badge">PDF</span></li>
<li><a href="docs-master/archive-nats.pdf">archive-nats</a><span class="badge">PDF</span></li>
<li><a href="docs-master/composition.pdf">composition</a><span class="badge">PDF</span></li>
<li><a href="user-docs/README.pdf">User guide (signup → tree-node lookup)</a><span class="badge">PDF</span></li>
<li><a href="packages/verae-zapier-simulator/README.pdf">Simulator README</a><span class="badge">PDF</span></li>
<li><a href="overview/README.pdf">System overview</a><span class="badge">PDF</span></li>
<li><a href="overview/INDEX.pdf">Documentation index</a><span class="badge">PDF</span></li>
<li><a href="packages/verae-fleet/docs/CONSOLE.pdf">Operator console</a><span class="badge">PDF</span></li>
<li><a href="overview/diagrams/system.svg">System diagram (SVG)</a></li>
</ul>
</section>
{extra_section(extra_pdf)}
"""
rest_md = f"""
{cards("md")}
<section>
<h2>Git repositories (Forgejo)</h2>
<p>Clone: <code>ssh://git@git.georgelambert.org:2223/marchon/&lt;name&gt;.git</code> (SSH port 2223).</p>
<ul>{git_lis}</ul>
</section>
<section>
<h2>Master module docs (HTML / Markdown)</h2>
<ul>
<li><a href="docs-master/README.html">docs-master README</a><span class="badge">MD</span></li>
<li><a href="docs-master/MESSAGE-FLOWS.html">MESSAGE-FLOWS</a><span class="badge">MD</span></li>
<li><a href="docs-master/modules-and-nats.html">modules-and-nats</a><span class="badge">MD</span></li>
<li><a href="docs-master/archive-nats.md">archive-nats.md</a><span class="badge">MD</span></li>
<li><a href="docs-master/composition.md">composition.md</a><span class="badge">MD</span></li>
<li><a href="user-docs/README.html">User guide</a><span class="badge">MD</span></li>
<li><a href="packages/verae-zapier-simulator/README.html">Simulator README</a><span class="badge">MD</span></li>
<li><a href="overview/README.html">System overview</a><span class="badge">MD</span></li>
<li><a href="overview/INDEX.html">Documentation index</a><span class="badge">MD</span></li>
<li><a href="packages/verae-fleet/docs/CONSOLE.html">Operator console</a><span class="badge">MD</span></li>
<li><a href="overview/diagrams/system.svg">System diagram (SVG)</a></li>
</ul>
</section>
{extra_section(extra_md)}
"""
(SITE / "index.html").write_text(
page_shell(now, rest_pdf, switch_to_md=True, title="Verae Time × Zapier documentation"),
encoding="utf-8",
)
(SITE / "index-md.html").write_text(
page_shell(now, rest_md, switch_to_md=False, title="Verae Time × Zapier — Markdown indexes"),
encoding="utf-8",
)
nfiles = sum(1 for _ in SITE.rglob("*") if _.is_file())
npdf = sum(1 for _ in SITE.rglob("*.pdf") if _.is_file())
print(f"site built at {SITE} ({nfiles} files, {npdf} PDFs)")
if errors:
print(f"{len(errors)} render errors:")
for e in errors[:20]:
print(" ", e[:300])
if __name__ == "__main__":
main()