#!/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 os import shutil import subprocess from concurrent.futures import ThreadPoolExecutor, as_completed from datetime import datetime, timezone from pathlib import Path from urllib.parse import unquote ROOT = Path(__file__).resolve().parents[1] SITE = ROOT / "site" CSS = ROOT / "scripts" / "docs-print.css" # Display label only. Catalog file links are relative so the tree can move hosts. DOCS_PUBLIC_HOST = os.environ.get("DOCS_PUBLIC_HOST", "zapier.georgelambert.org") # If set (e.g. https://docs.verae-time.net), PDF annotations become absolute. # Empty (default) writes path-relative URIs that work on any hostname at site root. DOCS_PUBLIC_URL = os.environ.get("DOCS_PUBLIC_URL", "").rstrip("/") 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"), ("packages/overview/10-docs-sync.md", "Docs sync: portal, IAM, keep, bootstrap"), ("packages/overview/11-hostnames.md", "Service → machine and DNS host names"), ], ), ( "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"), ("packages/verae-staff-iam/README.md", "verae-staff-iam"), ("packages/verae-staff-session/README.md", "verae-staff-session"), ("packages/verae-staff-ui/README.md", "verae-staff-ui"), ("packages/verae-keep/README.md", "verae-keep"), ("packages/verae-bootstrap/README.md", "verae-bootstrap (clone + deps per server type)"), ("packages/verae-uptime/README.md", "Off-box HTTPS door watch"), ("packages/verae-backup/README.md", "Backup SQLite / JetStream / worm+tree"), ("packages/verae-deploy/README.md", "Tagged deploy on the target OS"), ("packages/verae-proxmox/README.md", "Proxmox LXC worker on NS1 vmbr1"), ], ), ( "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 a–m"), ("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"\n", encoding="utf-8") banner.write_text( f'
' f'' f'
Verae Time × Zapier
' f"

{title}

" f'
{rel_src}
' f"
\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 _file_index(site: Path) -> dict[str, list[str]]: idx: dict[str, list[str]] = {} for p in site.rglob("*"): if p.is_file(): rel = p.relative_to(site).as_posix() idx.setdefault(p.name, []).append(rel) return idx _PREFERRED_PREFIXES = ( "overview/", "packages/overview/", "packages/verae-ops/", "packages/verae-fleet/docs/", "user-docs/", "packages/zapier-user-docs/", "docs/02-architecture/", "docs/modules/", "docs-master/", "packages/zapier-decisions/", "docs/modules-pdf/", "docs/models-pdf/", "sphinx/", ) def _pick_hit(hits: list[str]) -> str: for prefix in _PREFERRED_PREFIXES: for h in hits: if h.startswith(prefix): return h return hits[0] def _web_target(site: Path, rel: str, index: dict[str, list[str]] | None = None) -> str: """Map a site-relative path to a file that actually exists.""" rel = unquote(rel).split("#", 1)[0].lstrip("/") if rel.endswith(".html.pdf"): rel = rel[:-4] aliases = { "index-md.pdf": "index-md.html", "index-md.md": "index-md.html", "docs/architecture/overview.pdf": "docs/02-architecture/overview.pdf", "docs/architecture/nats-subjects.pdf": "docs/02-architecture/nats-subjects.pdf", "docs/architecture/nats-gateway.pdf": "docs/02-architecture/nats-gateway.pdf", "docs/architecture/composition.pdf": "docs/02-architecture/composition.pdf", "docs/architecture/fleet.pdf": "docs/02-architecture/fleet.pdf", "docs/sphinx/_build/html/index.pdf": "sphinx/index.html", "docs/sphinx/_build/html/index.html": "sphinx/index.html", } rel = aliases.get(rel, rel) candidates = [rel] if rel.endswith(".pdf"): candidates.append(rel[:-4] + ".html") candidates.append(rel[:-4] + ".md") if rel.endswith(".md"): candidates.append(rel[:-3] + ".pdf") candidates.append(rel[:-3] + ".html") stem = Path(rel).name if not stem or stem in {".", "/"}: return rel or "index.html" stem_pdf = Path(stem).with_suffix(".pdf").as_posix() stem_html = Path(stem).with_suffix(".html").as_posix() for folder in ( "overview", "packages/overview", "packages/verae-ops", "packages/verae-fleet/docs", "user-docs", "packages/zapier-user-docs", "docs/02-architecture", "docs/modules-pdf", "docs/models-pdf", "packages/zapier-decisions", "docs-master", "sphinx", ): candidates.append(f"{folder}/{stem_pdf}") candidates.append(f"{folder}/{stem_html}") for c in candidates: if (site / c).is_file(): return c if (site / c).is_dir(): for name in ("index.html", "index.pdf", "README.pdf", "README.html"): if (site / c / name).is_file(): return f"{c.rstrip('/')}/{name}" if index: names = [stem, stem_pdf, stem_html] if stem.endswith(".md"): names.append(Path(stem).with_suffix(".pdf").as_posix()) names.append(Path(stem).with_suffix(".html").as_posix()) for name in names: hits = index.get(name) or [] if hits: return _pick_hit(hits) return rel _DOCS_ORIGINS = ( "https://zapier.georgelambert.org/", "http://zapier.georgelambert.org/", ) def _href_for(pdf: Path, site: Path, target: str, frag: str = "") -> str: target = (target or "index.html").lstrip("/") or "index.html" dest = site / target if DOCS_PUBLIC_URL: return DOCS_PUBLIC_URL + "/" + target + frag rel = os.path.relpath(dest.resolve(), pdf.parent.resolve()) return Path(rel).as_posix() + frag def rewrite_pdf_uris(site: Path) -> None: """Turn WeasyPrint file:///… annotations into host-relative catalog paths. Default is a path relative to the PDF (works on any hostname at the site root). Set DOCS_PUBLIC_URL to emit absolute URLs instead. """ try: from pypdf import PdfReader, PdfWriter from pypdf.generic import NameObject, create_string_object except ImportError: print("pypdf missing; PDF URI rewrite skipped") return site = site.resolve() prefix = site.as_uri().rstrip("/") + "/" origins = list(_DOCS_ORIGINS) if DOCS_PUBLIC_URL: origins.append(DOCS_PUBLIC_URL.rstrip("/") + "/") index = _file_index(site) def resolve(pdf: Path, raw: str) -> str: frag = "" if "#" in raw: raw, frag = raw.split("#", 1) frag = "#" + frag target = None if raw.startswith(prefix): target = _web_target(site, raw[len(prefix) :], index) elif raw.startswith("file:"): name = unquote(raw.rsplit("/", 1)[-1] if "/" in raw else raw) if not name or name in {".", "/", "file:"}: target = "index.html" else: hit = _web_target(site, name, index) if (site / hit).exists(): target = hit elif "index-md" in raw: target = "index-md.html" else: target = "index.html" else: for origin in origins: if raw.startswith(origin): target = _web_target(site, raw[len(origin) :], index) break if target is None: return raw + frag return _href_for(pdf, site, target, frag) n_pdf = 0 n_fix = 0 for pdf in site.rglob("*.pdf"): try: reader = PdfReader(str(pdf)) except Exception: continue changed = False for page in reader.pages: annots = page.get("/Annots") if not annots: continue for annot in annots: obj = annot.get_object() action = obj.get("/A") if not action: continue uri = action.get("/URI") if not uri: continue raw = str(uri) new = resolve(pdf, raw) if new != raw: action[NameObject("/URI")] = create_string_object(new) changed = True n_fix += 1 if changed: writer = PdfWriter(clone_from=reader) tmp = pdf.with_suffix(".pdf.tmp") with tmp.open("wb") as fh: writer.write(fh) tmp.replace(pdf) n_pdf += 1 mode = DOCS_PUBLIC_URL + "/" if DOCS_PUBLIC_URL else "relative paths" print(f"rewrote {n_fix} PDF URIs in {n_pdf} files → {mode}") 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'{label}' ) def page_shell(now: str, body: str, switch_to_md: bool, title: str) -> str: switch = format_switch(switch_to_md) return f""" {title}
{switch}
{DOCS_PUBLIC_HOST}

Verae Time × Zapier documentation

Project reference: setup, architecture, NATS, module APIs, user guide, and operator console. Catalog defaults to colored PDFs. Generated {now}.

{body}
""" 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", "ui-docs", "verae-staff-session", "verae-staff-ui", "verae-staff-iam", "verae-keep", "verae-bootstrap", "verae-uptime", "verae-backup", "verae-deploy", "verae-proxmox", ): 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"), ) copy_tree(pkg_root / "services", SITE / "packages" / pkg / "services") elif pkg == "verae-nats-process": for p in pkg_root.glob("*.md"): copy_tree(p, SITE / "packages" / pkg / p.name) else: extras = ("README.md", "SUMMARY.md", "NATS.md") if pkg == "zapier-decisions": extras = extras + ("LOG.md", "TODO.md") if pkg == "verae-bootstrap": extras = extras + ("HOST-DEPS.md",) for name in extras: p = pkg_root / name if p.exists(): copy_tree(p, SITE / "packages" / pkg / name) if pkg == "zapier-decisions": copy_tree(pkg_root / "decisions", SITE / "packages" / pkg / "decisions") ui_docs = ROOT / "packages" / "ui-docs" for name in ("WALKTHROUGH.md", "REPORT.md", "UI-REVIEW.pdf"): p = ui_docs / name if p.exists(): copy_tree(p, SITE / "packages" / "ui-docs" / name) copy_tree( ui_docs / "screenshots", SITE / "packages" / "ui-docs" / "screenshots", ignore=shutil.ignore_patterns(".DS_Store"), ) 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"), ) copy_tree(ROOT / "packages" / "zappier" / "openapi.yaml", SITE / "packages" / "zappier" / "openapi.yaml") 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") def write_book_index(folder: Path, title: str, patterns: tuple[str, ...]) -> None: if not folder.is_dir(): return files: list[Path] = [] for pat in patterns: files.extend(p for p in folder.rglob(pat) if p.is_file()) skip = {"index.md", "index.html", "index.pdf"} files = sorted({p for p in files if p.name not in skip}) lines = [f"# {title}", "", f"{len(files)} files in this book.", ""] for p in files: rel = p.relative_to(folder).as_posix() lines.append(f"- [{rel}]({rel})") (folder / "index.md").write_text("\n".join(lines) + "\n", encoding="utf-8") write_book_index(SITE / "docs" / "modules-pdf", "Module PDFs (book)", ("*.pdf",)) write_book_index(SITE / "docs" / "models-pdf", "Model PDFs (book)", ("*.pdf",)) write_book_index(SITE / "packages" / "ui-docs" / "screenshots", "UI screenshots", ("*.png", "*.jpg", "*.webp")) write_book_index(SITE / "packages" / "overview" / "diagrams", "Overview diagrams", ("*.svg", "*.png")) write_book_index(SITE / "overview" / "diagrams", "Overview diagrams", ("*.svg", "*.png")) write_book_index(SITE / "packages" / "zapier-decisions" / "decisions", "Decisions", ("*.md", "*.pdf")) write_book_index(SITE / "packages" / "verae-fleet" / "services", "Fleet service JSON", ("*.json",)) write_book_index(SITE / "docs" / "00-sources" / "docs" / "diagrams", "Research diagrams", ("*.svg", "*.png")) # 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 if "research" in md.parts: continue # Sphinx HTML/PDF are copied from _build; don't WeasyPrint the rst sources. if "sphinx" in md.parts: 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) rewrite_pdf_uris(SITE) # 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'

{label}

' f'

{badge}{path}

' f'Open {badge}
' ) out.append(f'

{title}

{"".join(lis)}
') 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"), ("UI-Docs", "UI walkthrough and screenshot PDF"), ("verae-staff-session", "Staff cookie login"), ("verae-staff-ui", "Shared staff review HTML"), ("verae-staff-iam", "Staff users, roles, permissions"), ("verae-keep", "Host keep-alive"), ("verae-bootstrap", "Clone + deps per server type"), ("verae-uptime", "Off-box HTTPS door watch"), ("verae-backup", "Backup SQLite / JetStream / worm+tree"), ("verae-deploy", "Tagged deploy on the target OS"), ("verae-proxmox", "Proxmox LXC worker (NS1 vmbr1)"), ] git_lis = "".join( f'
  • {name} ' f'{desc}
  • ' for name, desc in git_repos ) extra_pdf = [ ("docs-master/README.pdf", "Master summaries (docs-master)"), ("docs-master/CATALOG-HOST.pdf", "Move the catalog to another hostname"), ("docs-master/HOST-DEPS.pdf", "Host OS / Node / NATS installer"), ("docs-master/MESSAGE-FLOWS.pdf", "Numbered message flows"), ("docs-master/modules-and-nats.pdf", "NATS address table"), ("packages/verae-bootstrap/HOST-DEPS.pdf", "Host OS / Node / NATS installer"), ("user-docs/README.pdf", "User guide index"), ("packages/verae-zapier-simulator/README.pdf", "Simulator README"), ("packages/verae-fleet/docs/CONSOLE.pdf", "Operator console"), ("packages/ui-docs/UI-REVIEW.pdf", "UI review (screenshots + live doors)"), ("overview/README.pdf", "System overview"), ("overview/INDEX.pdf", "Documentation index"), ("docs/modules-pdf/index.pdf", "Module PDFs (book)"), ("docs/models-pdf/index.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/CATALOG-HOST.html", "Move the catalog to another hostname"), ("docs-master/HOST-DEPS.html", "Host OS / Node / NATS installer"), ("docs-master/MESSAGE-FLOWS.html", "Numbered message flows"), ("docs-master/modules-and-nats.html", "NATS address table"), ("packages/verae-bootstrap/HOST-DEPS.html", "Host OS / Node / NATS installer"), ("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/README.html", "All module markdown"), ("docs/models/README.html", "All model markdown"), ("docs/modules-pdf/index.html", "Module PDFs (book)"), ("docs/models-pdf/index.html", "Model PDFs (book)"), ("sphinx/index.html", "Sphinx HTML"), ("sphinx/verae-zapier-modules.pdf", "Sphinx LaTeX PDF"), ] LIVE_DOORS = [ ("portal.png", "https://portal.zapier.georgelambert.org/portal/", "Customer portal", "Public signup, sign-in, API key, statement, invoices. Access-web door."), ("api.png", "https://api.zapier.georgelambert.org/health", "Customer API", "Direct customer API (x-api-key). Not Zapier."), ("leaf.png", "https://leaf.zapier.georgelambert.org/health", "S2S leaf", "Server-to-server NATS leaf plane. No billing subjects."), ("zap.png", "https://zap.zapier.georgelambert.org/health", "Zapier plane", "Only Zapier Platform HTTPS. Cloud never talks to NATS."), ("staff.png", "https://staff.zapier.georgelambert.org/", "Staff plane", "CS / sales / accounting door after IAM."), ("iam.png", "https://iam.zapier.georgelambert.org/", "Staff IAM", "Named staff users, roles, permissions, sessions."), ("login.png", "https://login.zapier.georgelambert.org/health", "Staff session", "Shared staff cookie; proxies IAM when STAFF_IAM_URL is set."), ("cs.png", "https://cs.zapier.georgelambert.org/", "Customer service", "Review prepaid balance and apply credits (USD)."), ("sales.png", "https://sales.zapier.georgelambert.org/", "Sales", "Per-customer quote, tier, and multiplier."), ("acct.png", "https://acct.zapier.georgelambert.org/", "Accounting", "QuickBooks IIF and accounting CSV export."), ("mw.png", "https://mw.zapier.georgelambert.org/health", "Middleware", "Zapier /zapier/v1, jobs, archive, chain (MOCK_VERAE in lab)."), ("fleet.png", "https://fleet.zapier.georgelambert.org/", "Operator console", "Fleet replicas, Trace, Docs. Pause/stop honored by keep."), ("docs.png", "/", "This catalog", "Colored PDF documentation site."), ("git.png", "https://git.georgelambert.org/", "Forgejo", "Independent module repos (SSH 2223)."), ] def live_section() -> str: live_dir = ROOT / "packages" / "ui-docs" / "screenshots" / "live" dest = SITE / "live" dest.mkdir(parents=True, exist_ok=True) cards_html = [] for fname, href, title, desc in LIVE_DOORS: src = live_dir / fname img = "" if src.exists(): copy_tree(src, dest / fname) img = f'{title}' cards_html.append( f'
    {img}

    {title}

    ' f'{href}' f"

    {desc}

    " f'Open
    ' ) review = "" pdf = ROOT / "packages" / "ui-docs" / "UI-REVIEW.pdf" if pdf.exists(): copy_tree(pdf, SITE / "packages" / "ui-docs" / "UI-REVIEW.pdf") review = ( '

    Screenshot atlas with usage notes: ' 'UI-REVIEW.pdf.

    ' ) return ( f'

    Live services (NS1 · HTTPS)

    ' f"

    Each name is on Let’s Encrypt via Caddy on " f"NS1.GEORGELAMBERT.ORG. NATS, keep, and archive workers stay on loopback.

    " f"{review}" f'
    {"".join(cards_html)}
    ' ) def extra_section(items: list[tuple[str, str]]) -> str: lis = [] for h, lab in items: dest = SITE / h if dest.exists() or (h.startswith("sphinx") and (SITE / "sphinx").exists()): lis.append(f'
  • {lab}
  • ') return f"

    Catalogs

    " rest_pdf = f""" {cards("pdf")}

    Git repositories (Forgejo)

    Clone: ssh://git@git.georgelambert.org:2223/marchon/<name>.git (SSH port 2223). Branches main and master.

    Master module docs

    {extra_section(extra_pdf)} {live_section()} """ rest_md = f""" {cards("md")}

    Git repositories (Forgejo)

    Clone: ssh://git@git.georgelambert.org:2223/marchon/<name>.git (SSH port 2223).

    Master module docs (HTML / Markdown)

    {extra_section(extra_md)} {live_section()} """ (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()