📖 add a polished standalone user manual (viz/manual.html) + generator

The public site only had the in-app grimoire (behind the ✦ overlay); there was no
shareable, linkable user manual. build_manual.py extracts the grimoire (the canon,
per CLAUDE.md) + its styles from viz/index.html into a self-contained, always-
visible page with a generated 27-section table of contents and a 'play the
instrument →' back-link. Single source → no drift; re-run after editing the grimoire.

Served at godstrument.pro/manual.html (docroot is viz/). Verified: renders as a
normal page (overlay/close-button overridden away), TOC anchors resolve, all
feature sections present (zero, OMNI panel, vibes, dimensions), console clean.

Also refine the deploy convention: exclude dev docs/scripts (CLAUDE.md, README,
ZERO_OMNI_BRIEF.md, GODSTRUMENT_MANUAL_SOURCE.md, godstrument.txt, build_manual.py,
tests) so working notes never land on the public box.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-07-11 06:49:25 +10:00
parent fb32d10991
commit 297ca8457f
3 changed files with 1278 additions and 1 deletions

View File

@ -1,6 +1,6 @@
# Godstrument — Claude context # Godstrument — Claude context
Music-instrument web app; hosted at godstrument.pro on the **botchat** VPS (`humanjing@100.71.119.27`, exposed via Cloudflare Tunnel — NOT dealgod, despite earlier notes). App dir `/home/humanjing/godstrument` (not a git checkout). Deploy = rsync git-tracked files only (`--files-from=<(git ls-files)`, never `--delete`, never push `godstrument_users.db`/`auth_secret`/`patches/`) then `sudo -n systemctl restart godstrument`. Origin is Gitea `ssh://git@100.71.119.27:222/monster/Godstrument.git`. See [[godstrument-pro-deploy]]. Music-instrument web app; hosted at godstrument.pro on the **botchat** VPS (`humanjing@100.71.119.27`, exposed via Cloudflare Tunnel — NOT dealgod, despite earlier notes). App dir `/home/humanjing/godstrument` (not a git checkout). Deploy = rsync git-tracked files only, **minus dev docs/scripts** (`--files-from=<(git ls-files | grep -vE '^(CLAUDE\.md|README\.md|ZERO_OMNI_BRIEF\.md|GODSTRUMENT_MANUAL_SOURCE\.md|godstrument\.txt|build_manual\.py|test_.*\.py)$')`, never `--delete`, never push `godstrument_users.db`/`auth_secret`/`patches/`) then `sudo -n systemctl restart godstrument`. The **user manual** ships as `viz/manual.html` (served at godstrument.pro/manual.html), generated from the in-app grimoire by `python3 build_manual.py` — re-run it after editing the grimoire. Working notes/planning docs stay OUT of the deploy. Origin is Gitea `ssh://git@100.71.119.27:222/monster/Godstrument.git`. See [[godstrument-pro-deploy]].
- **Canon gotcha:** the real spec prose lives in `viz/index.html` (grimoire text around line ~950); `GODSTRUMENT_MANUAL_SOURCE.md` claims to be exhaustive but has drifted before (Earth Echo / Schumann layer was missing). When they disagree, **viz/index.html is truth** — update the manual to match, not the other way. - **Canon gotcha:** the real spec prose lives in `viz/index.html` (grimoire text around line ~950); `GODSTRUMENT_MANUAL_SOURCE.md` claims to be exhaustive but has drifted before (Earth Echo / Schumann layer was missing). When they disagree, **viz/index.html is truth** — update the manual to match, not the other way.
- Secrets (Cloudflare token etc.): read from `.env`/local files, never from chat. A CF token was pasted in chat once and rolled — don't repeat that. - Secrets (Cloudflare token etc.): read from `.env`/local files, never from chat. A CF token was pasted in chat once and rolled — don't repeat that.

105
build_manual.py Normal file
View File

@ -0,0 +1,105 @@
#!/usr/bin/env python3
"""Generate viz/manual.html — the polished standalone user manual — from the
in-app grimoire in viz/index.html (the single source of truth per CLAUDE.md).
Re-run whenever the grimoire changes: python3 build_manual.py
ponytail: string-slice extraction, no HTML parser dep. The grimoire is a flat,
well-formed block; if its shape ever changes, this fails loudly (asserts) rather
than emitting a broken page.
"""
import re
import sys
import pathlib
HERE = pathlib.Path(__file__).parent
SRC = HERE / "viz" / "index.html"
OUT = HERE / "viz" / "manual.html"
html = SRC.read_text(encoding="utf-8")
# --- 1. the whole <style> block (includes :root vars + all #grimoire rules) ---
m = re.search(r"<style>(.*?)</style>", html, re.S)
assert m, "no <style> block found"
css = m.group(1)
# --- 2. the #grimoire block, balance-matched on <div>/</div> ------------------
start = html.find('<div id="grimoire">')
assert start != -1, "no #grimoire div found"
i, depth = start, 0
for tok in re.finditer(r"<div\b|</div>", html[start:]):
depth += 1 if tok.group() == "<div" else -1
if depth == 0:
end = start + tok.end()
break
else:
sys.exit("unbalanced #grimoire div")
grim = html[start:end]
# strip the in-app chrome: the × close button (the TOC lives outside this div)
grim = grim.replace('<span class="x">×</span>', "")
# --- 3. a generated table of contents from the <h2> headings -----------------
def slug(t):
return re.sub(r"[^a-z0-9]+", "-", re.sub(r"<[^>]+>", "", t).lower()).strip("-")
heads = re.findall(r"<h2>(.*?)</h2>", grim, re.S)
seen, toc = set(), []
for h in heads:
s = slug(h) or "section"
while s in seen:
s += "-x"
seen.add(s)
grim = grim.replace(f"<h2>{h}</h2>", f'<h2 id="{s}">{h}</h2>', 1)
toc.append(f'<a href="#{s}">{re.sub(r"<[^>]+>", "", h)}</a>')
assert toc, "no <h2> sections to build a TOC from"
toc_html = '<nav class="manual-toc"><div class="toc-h">Contents</div>' + "".join(toc) + "</nav>"
# --- 4. wrap into a standalone, always-visible page --------------------------
page = f"""<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Godstrument User Manual</title>
<meta name="description" content="How to play Godstrument — the instrument played by the living world. Every feature, from the world's feeds to the OMNI synth, zero mode, vibes and MIDI/CV out.">
<style>{css}</style>
<style>
/* standalone-manual overrides render the grimoire as a normal scrolling page */
html, body {{ margin: 0; }}
body {{ background: radial-gradient(ellipse at 50% 0%, #0c1533 0%, #070c1c 60%, #03050e 100%);
min-height: 100vh; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; }}
#grimoire {{ position: static !important; display: block !important; inset: auto !important;
overflow: visible !important; background: none !important; z-index: auto !important; }}
#grimoire .x {{ display: none !important; }}
.manual-bar {{ position: sticky; top: 0; z-index: 5; display: flex; align-items: center; justify-content: space-between;
padding: 11px 22px; background: rgba(6,10,20,0.72); -webkit-backdrop-filter: blur(9px); backdrop-filter: blur(9px);
border-bottom: 1px solid rgba(120,150,200,0.16); }}
.manual-bar .brand {{ color: #d6e2f4; font-weight: 800; letter-spacing: 2px; font-size: 14px; }}
.manual-bar a {{ color: #9fd0ff; text-decoration: none; font-size: 13px; letter-spacing: 0.4px; }}
.manual-bar a:hover {{ color: #fff; }}
.manual-toc {{ max-width: 720px; margin: 26px auto 8px; padding: 16px 22px; border-radius: 14px;
background: rgba(20,28,46,0.5); border: 1px solid rgba(120,150,220,0.2);
display: grid; grid-template-columns: 1fr 1fr; gap: 4px 22px; }}
.manual-toc .toc-h {{ grid-column: 1 / -1; font-weight: 800; letter-spacing: 1px; color: #d3e2ff; font-size: 13px;
text-transform: uppercase; margin-bottom: 4px; }}
.manual-toc a {{ color: rgba(180,200,235,0.85); text-decoration: none; font-size: 12.5px; line-height: 1.9;
border-bottom: 1px solid transparent; }}
.manual-toc a:hover {{ color: #eaf1ff; border-bottom-color: rgba(160,185,225,0.4); }}
#grimoire h2 {{ scroll-margin-top: 60px; }}
@media (max-width: 560px) {{ .manual-toc {{ grid-template-columns: 1fr; }} }}
</style>
</head>
<body>
<div class="manual-bar"><span class="brand"> GODSTRUMENT the manual</span><a href="/"> play the instrument </a></div>
{grim.replace('<div class="wrap">', '<div class="wrap">' + toc_html, 1)}
</body>
</html>
"""
OUT.write_text(page, encoding="utf-8")
print(f"wrote {OUT} ({len(page):,} bytes, {len(toc)} sections)")
# self-check: the page must be self-contained and carry the real feature sections
assert "OMNI" in page and "Building from zero" in page and "dimensions" in page.lower(), "manual missing key feature sections"
assert "id=\"grimoire\"" in page and page.count("<h2") >= 8, "manual structure looks wrong"
print("ok: self-contained, has OMNI / zero / dimensions sections")

1172
viz/manual.html Normal file

File diff suppressed because one or more lines are too long