master-zapier-plan-draft/scripts/build-docs-site.py
George Lambert 392fe7bb15
Some checks are pending
offline / test (push) Waiting to run
Refresh docs-master with Forgejo URLs and NATS.md for every module
HTTPS-only packages (zappier, activate, zapier-app) now state they do
not subscribe to NATS. Catalog build copies docs-master onto the site.
2026-09-11 12:38:55 -04:00

251 lines
9.2 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."""
from __future__ import annotations
import shutil
import subprocess
from datetime import datetime, timezone
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SITE = ROOT / "site"
SECTIONS = [
(
"Start here",
[
("README.md", "Workspace README"),
("docs/04-activate/SETUP-ZAPIER-DEVELOPER.html", "Zapier developer setup (HTML)"),
("docs/04-activate/SETUP-ZAPIER-DEVELOPER.pdf", "Zapier developer setup (PDF)"),
("docs/OPEN.md", "Still open"),
("TODO.md", "Implementation TODO / gates"),
],
),
(
"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"),
],
),
(
"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 pandoc_md(src: Path, dest: Path, title: str) -> None:
dest.parent.mkdir(parents=True, exist_ok=True)
css = """
body{font:16px/1.5 Georgia,serif;max-width:48rem;margin:2rem auto;padding:0 1rem;color:#122}
a{color:#0b4f8a} code,pre{font-family:ui-monospace,Menlo,monospace;font-size:0.88rem}
pre{background:#1b2833;color:#eef;padding:0.8rem;overflow:auto}
table{border-collapse:collapse} td,th{border:1px solid #ccc;padding:0.35rem 0.5rem}
nav{font:14px system-ui;margin-bottom:1.5rem}
"""
header = dest.with_suffix(".hdr.html")
header.write_text(
f"<nav><a href=\"/\">zapier.georgelambert.org</a></nav>\n<style>{css}</style>\n",
encoding="utf-8",
)
subprocess.run(
[
"pandoc",
str(src),
"-o",
str(dest),
"--standalone",
f"--metadata=title={title}",
f"--include-in-header={header}",
],
check=False,
capture_output=True,
)
header.unlink(missing_ok=True)
def walk_md_pdf(prefix: Path) -> list[tuple[str, str]]:
items = []
if not prefix.exists():
return items
for p in sorted(prefix.rglob("*")):
if p.suffix.lower() in {".md", ".pdf", ".html", ".yaml", ".yml", ".svg"} and p.is_file():
items.append((rel(p), p.name))
return items
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")
for pkg in ("verae-activate", "verae-zapier", "verae-zapier-middleware", "zappier"):
readme = ROOT / "packages" / pkg / "README.md"
if readme.exists():
copy_tree(readme, SITE / "packages" / pkg / "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")
# research markdown + diagrams only
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")
# HTML versions of markdown for the catalog entries
html_pairs = []
for _title, links in SECTIONS:
for path, label in links:
src = ROOT / path
if src.suffix == ".md" and src.exists():
dest = SITE / Path(path).with_suffix(".html")
pandoc_md(src, dest, label)
html_pairs.append((path, dest.relative_to(SITE).as_posix(), label))
now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
cards = []
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
href = path
html = Path(path).with_suffix(".html").as_posix()
if src.suffix == ".md" and (SITE / html).exists():
href = html
lis.append(f'<li><a href="{href}">{label}</a> <span class="path">{path}</span></li>')
cards.append(f"<section><h2>{title}</h2><ul>{''.join(lis)}</ul></section>")
extra = [
("docs/modules/README.md", "Module API sheets"),
("docs/models/README.md", "Data models"),
("sphinx/index.html", "Sphinx HTML"),
("docs/04-activate/SETUP-ZAPIER-DEVELOPER.html", "Setup HTML"),
]
extra_lis = "".join(
f'<li><a href="{h}">{lab}</a></li>' for h, lab in extra if (SITE / h).exists() or h.startswith("sphinx")
)
index = f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>Verae Time × Zapier documentation</title>
<style>
:root {{ --ink:#12202c; --muted:#52646f; --line:#d5dee4; --bg:#f6f3ee; --accent:#0f6e56; }}
body {{ margin:0; font:16px/1.5 Georgia, serif; color:var(--ink); background:var(--bg); }}
header {{ background:var(--ink); color:#f6f3ee; padding:2rem 1.5rem; }}
header p {{ max-width:44rem; color:#d9e2e8; }}
.kicker {{ letter-spacing:.12em; text-transform:uppercase; font:700 11px system-ui; opacity:.75; }}
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; }}
ul {{ padding-left:1.2rem; }}
li {{ margin:.35rem 0; }}
a {{ color:#0b4f8a; }}
.path {{ color:var(--muted); font:12px ui-monospace, Menlo, monospace; margin-left:.4rem; }}
input {{ width:100%; padding:.55rem .7rem; font:16px system-ui; border:1px solid var(--line); border-radius:6px; }}
</style>
</head>
<body>
<header>
<div class="kicker">zapier.georgelambert.org</div>
<h1>Verae Time × Zapier documentation</h1>
<p>Project reference: setup checklist, architecture (including NATS job wait and WORM archive fan-out), module API sheets, models, OpenAPI, and research notes. Generated {now}.</p>
</header>
<main>
<p><input id="q" type="search" placeholder="Filter links…" /></p>
{''.join(cards)}
<section>
<h2>Catalogs</h2>
<ul>{extra_lis}
<li><a href="docs/modules/">All module markdown</a></li>
<li><a href="docs/models/">All model markdown</a></li>
<li><a href="docs/modules-pdf/">Module PDFs</a></li>
<li><a href="docs/models-pdf/">Model PDFs</a></li>
</ul>
</section>
</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>
"""
(SITE / "index.html").write_text(index, encoding="utf-8")
print(f"site built at {SITE} ({sum(1 for _ in SITE.rglob('*') if _.is_file())} files)")
if __name__ == "__main__":
main()