📖 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:
parent
fb32d10991
commit
297ca8457f
@ -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
105
build_manual.py
Normal 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
1172
viz/manual.html
Normal file
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user