| 1 | // đŸ¤– Shared script for the planet-scale operations guide. |
| 2 | // |
| 3 | // This file is the single source of truth for the guide's structure. It: |
| 4 | // 1. renders the sidebar navigation on every page (from the PAGES list below), |
| 5 | // 2. renders the previous / next links at the bottom of every page, |
| 6 | // 3. renders the "you are here" system map at the top of every page, |
| 7 | // 4. adds a small anchor link to every heading that has an id. |
| 8 | // |
| 9 | // To add a page: create the HTML file (copy an existing page as a template), |
| 10 | // then add one entry to PAGES below, in reading order. Nothing else to update — |
| 11 | // every page's sidebar and pager follow this list automatically. |
| 12 | // |
| 13 | // All markup injected here is built from static, in-code strings only (the |
| 14 | // repo rule: never innerHTML untrusted content). |
| 15 | |
| 16 | "use strict"; |
| 17 | |
| 18 | const PAGES = [ |
| 19 | { file: "index.html", num: "01", short: "Overview", title: "Overview and orientation" }, |
| 20 | { file: "router.html", num: "02", short: "The router", title: "The router: the front door of every box" }, |
| 21 | { file: "processes.html", num: "03", short: "Processes on a box", title: "Everything that runs on a box" }, |
| 22 | { file: "deploys.html", num: "04", short: "Deploys and updates", title: "Installing and updating the software" }, |
| 23 | { file: "spaces.html", num: "05", short: "Creating spaces", title: "Creating and removing chat spaces" }, |
| 24 | { file: "hibernation.html", num: "06", short: "Hibernation", title: "Hibernation: how idle spaces sleep in object storage" }, |
| 25 | { file: "mesh.html", num: "07", short: "The mesh", title: "The mesh: running more than one box" }, |
| 26 | { file: "operations.html", num: "08", short: "Day-to-day operations", title: "Day-to-day operations and checks" }, |
| 27 | { file: "dev.html", num: "09", short: "The proving ground", title: "The proving ground: the dev mesh" }, |
| 28 | { file: "reference.html", num: "10", short: "Reference", title: "Complete reference: commands, files, variables" }, |
| 29 | ]; |
| 30 | |
| 31 | function currentPage() { |
| 32 | const explicit = document.body.dataset.page; |
| 33 | if (explicit) return explicit; |
| 34 | const leaf = location.pathname.split("/").pop(); |
| 35 | return leaf === "" ? "index.html" : leaf; |
| 36 | } |
| 37 | |
| 38 | // ---- sidebar ---- |
| 39 | function renderSidebar() { |
| 40 | const nav = document.querySelector("nav.side"); |
| 41 | if (!nav) return; |
| 42 | const here = currentPage(); |
| 43 | const items = PAGES.map(p => { |
| 44 | const current = p.file === here ? ' aria-current="page"' : ""; |
| 45 | return `<li><a href="${p.file}"${current}><span class="num">${p.num}</span><span>${p.short}</span></a></li>`; |
| 46 | }).join(""); |
| 47 | nav.innerHTML = |
| 48 | `<p class="brand"><a href="index.html">Running the fleet</a></p>` + |
| 49 | `<p class="brand-sub">An operator’s guide to the planet-scale system</p>` + |
| 50 | `<ol>${items}</ol>`; |
| 51 | nav.setAttribute("aria-label", "Guide contents"); |
| 52 | } |
| 53 | |
| 54 | // ---- previous / next pager + colophon ---- |
| 55 | function renderPager() { |
| 56 | const wrap = document.querySelector("main.content .wrap"); |
| 57 | if (!wrap) return; |
| 58 | const here = currentPage(); |
| 59 | const idx = PAGES.findIndex(p => p.file === here); |
| 60 | if (idx === -1) return; |
| 61 | const prev = PAGES[idx - 1]; |
| 62 | const next = PAGES[idx + 1]; |
| 63 | const pager = document.createElement("footer"); |
| 64 | pager.className = "pager"; |
| 65 | let html = ""; |
| 66 | if (prev) html += `<a class="prev" href="${prev.file}"><span class="dir">Previous</span><span class="dest">${prev.num} · ${prev.short}</span></a>`; |
| 67 | if (next) html += `<a class="next" href="${next.file}"><span class="dir">Next</span><span class="dest">${next.num} · ${next.short}</span></a>`; |
| 68 | pager.innerHTML = html; |
| 69 | wrap.appendChild(pager); |
| 70 | |
| 71 | const colophon = document.createElement("footer"); |
| 72 | colophon.className = "colophon"; |
| 73 | colophon.innerHTML = |
| 74 | `\u{1F916} This guide was written by an LLM from the code and design documents in this repository. ` + |
| 75 | `The code is the source of truth; if the guide and the code disagree, trust the code and please fix the guide. ` + |
| 76 | `How to update it: <a href="README.html">maintaining this guide</a>. ` + |
| 77 | `Deeper design rationale: <code>docs/planet-scale.md</code> and <code>docs/instance-router.md</code>.`; |
| 78 | wrap.appendChild(colophon); |
| 79 | } |
| 80 | |
| 81 | // ---- heading anchors ---- |
| 82 | function renderHeadingAnchors() { |
| 83 | document.querySelectorAll("h2[id], h3[id]").forEach(h => { |
| 84 | const a = document.createElement("a"); |
| 85 | a.className = "hlink"; |
| 86 | a.href = "#" + h.id; |
| 87 | a.textContent = "#"; |
| 88 | a.setAttribute("aria-label", "Link to this section"); |
| 89 | h.appendChild(a); |
| 90 | }); |
| 91 | } |
| 92 | |
| 93 | // ---- the system map (the "you are here" diagram) ---- |
| 94 | // |
| 95 | // One SVG, drawn identically on every page; the page highlights its own |
| 96 | // component(s) via <div class="sysmap" data-highlight="router bucket">. |
| 97 | // Node keys: members, router, instances, services, control, updater, |
| 98 | // bucket, backup, peer. |
| 99 | function sysmapSVG(highlights) { |
| 100 | const hl = key => highlights.includes(key) ? ' class="here"' : ""; |
| 101 | return ` |
| 102 | <svg viewBox="0 0 960 400" role="img" aria-label="Map of the whole system: members' devices reach a box over the internet; on the box a router forwards traffic to chat space instances and sibling services; a control daemon and provisioner create spaces; an updater installs new versions; object storage holds parked spaces, leases, heartbeats and backups; peer boxes form the mesh."> |
| 103 | <!-- zone: the internet --> |
| 104 | <rect class="zone" x="10" y="34" width="140" height="330" rx="10"></rect> |
| 105 | <text class="zonelabel" x="24" y="24">THE INTERNET</text> |
| 106 | <g${hl("members")}> |
| 107 | <rect class="node" x="26" y="150" width="108" height="72" rx="7"></rect> |
| 108 | <text class="boxlabel" x="80" y="180" text-anchor="middle">Members’</text> |
| 109 | <text class="boxlabel" x="80" y="196" text-anchor="middle">devices</text> |
| 110 | <text class="sublabel" x="80" y="211" text-anchor="middle">browsers, apps</text> |
| 111 | </g> |
| 112 | |
| 113 | <!-- zone: one box --> |
| 114 | <rect class="zone" x="180" y="34" width="440" height="330" rx="10"></rect> |
| 115 | <text class="zonelabel" x="194" y="24">ONE BOX (A LINUX SERVER)</text> |
| 116 | |
| 117 | <g${hl("router")}> |
| 118 | <rect class="node" x="200" y="60" width="150" height="86" rx="7"></rect> |
| 119 | <text class="boxlabel" x="275" y="88" text-anchor="middle">lchat router</text> |
| 120 | <text class="sublabel" x="275" y="104" text-anchor="middle">TLS on ports 80 / 443</text> |
| 121 | <text class="sublabel" x="275" y="119" text-anchor="middle">host → unix socket</text> |
| 122 | <text class="sublabel" x="275" y="134" text-anchor="middle">activator lives inside</text> |
| 123 | </g> |
| 124 | |
| 125 | <g${hl("instances")}> |
| 126 | <rect class="node" x="420" y="52" width="180" height="34" rx="7"></rect> |
| 127 | <text class="boxlabel" x="510" y="73" text-anchor="middle">instance: chat-a.example.com</text> |
| 128 | <rect class="node" x="420" y="94" width="180" height="34" rx="7"></rect> |
| 129 | <text class="boxlabel" x="510" y="115" text-anchor="middle">instance: chat-b.example.com</text> |
| 130 | <rect class="node" x="420" y="136" width="180" height="34" rx="7"></rect> |
| 131 | <text class="boxlabel" x="510" y="157" text-anchor="middle">bot sidecars (one per instance)</text> |
| 132 | </g> |
| 133 | |
| 134 | <g${hl("services")}> |
| 135 | <rect class="node" x="420" y="186" width="180" height="46" rx="7"></rect> |
| 136 | <text class="boxlabel" x="510" y="205" text-anchor="middle">sibling services</text> |
| 137 | <text class="sublabel" x="510" y="221" text-anchor="middle">home server, push relay</text> |
| 138 | </g> |
| 139 | |
| 140 | <g${hl("control")}> |
| 141 | <rect class="node" x="200" y="186" width="150" height="62" rx="7"></rect> |
| 142 | <text class="boxlabel" x="275" y="207" text-anchor="middle">control daemon +</text> |
| 143 | <text class="boxlabel" x="275" y="222" text-anchor="middle">cloud provisioner</text> |
| 144 | <text class="sublabel" x="275" y="238" text-anchor="middle">creates chat spaces</text> |
| 145 | </g> |
| 146 | |
| 147 | <g${hl("updater")}> |
| 148 | <rect class="node" x="200" y="272" width="150" height="62" rx="7"></rect> |
| 149 | <text class="boxlabel" x="275" y="295" text-anchor="middle">auto-updater</text> |
| 150 | <text class="sublabel" x="275" y="311" text-anchor="middle">hourly timer; installs</text> |
| 151 | <text class="sublabel" x="275" y="325" text-anchor="middle">signed releases</text> |
| 152 | </g> |
| 153 | |
| 154 | <!-- zone: object storage --> |
| 155 | <rect class="zone" x="650" y="34" width="300" height="190" rx="10"></rect> |
| 156 | <text class="zonelabel" x="664" y="24">OBJECT STORAGE (AMAZON S3)</text> |
| 157 | |
| 158 | <g${hl("bucket")}> |
| 159 | <rect class="node" x="668" y="52" width="264" height="86" rx="7"></rect> |
| 160 | <text class="boxlabel" x="800" y="76" text-anchor="middle">hibernation bucket</text> |
| 161 | <text class="sublabel" x="800" y="93" text-anchor="middle">parked space databases · per-space locks</text> |
| 162 | <text class="sublabel" x="800" y="108" text-anchor="middle">mesh membership heartbeats</text> |
| 163 | <text class="sublabel" x="800" y="123" text-anchor="middle">one prefix per chat space</text> |
| 164 | </g> |
| 165 | |
| 166 | <g${hl("backup")}> |
| 167 | <rect class="node" x="668" y="152" width="264" height="56" rx="7"></rect> |
| 168 | <text class="boxlabel" x="800" y="176" text-anchor="middle">fleet-backup bucket</text> |
| 169 | <text class="sublabel" x="800" y="192" text-anchor="middle">continuous copies of every database</text> |
| 170 | </g> |
| 171 | |
| 172 | <!-- zone: peer boxes --> |
| 173 | <rect class="zone" x="650" y="252" width="300" height="112" rx="10"></rect> |
| 174 | <text class="zonelabel" x="664" y="246">OTHER BOXES — THE MESH</text> |
| 175 | |
| 176 | <g${hl("peer")}> |
| 177 | <rect class="node" x="668" y="268" width="264" height="38" rx="7"></rect> |
| 178 | <text class="boxlabel" x="800" y="292" text-anchor="middle">peer box 2 (same software)</text> |
| 179 | <rect class="node" x="668" y="314" width="264" height="38" rx="7"></rect> |
| 180 | <text class="boxlabel" x="800" y="338" text-anchor="middle">peer box 3 (same software)</text> |
| 181 | </g> |
| 182 | |
| 183 | <!-- wires --> |
| 184 | <line class="wire" x1="134" y1="186" x2="200" y2="110" marker-end="url(#arr)"></line> |
| 185 | <line class="wire" x1="350" y1="90" x2="420" y2="70" marker-end="url(#arr)"></line> |
| 186 | <line class="wire" x1="350" y1="100" x2="420" y2="110" marker-end="url(#arr)"></line> |
| 187 | <line class="wire" x1="350" y1="120" x2="420" y2="205" marker-end="url(#arr)"></line> |
| 188 | <line class="wire" x1="350" y1="80" x2="668" y2="80" marker-end="url(#arr)"></line> |
| 189 | <line class="wire" x1="600" y1="180" x2="668" y2="180" marker-end="url(#arr)"></line> |
| 190 | <line class="wire" x1="800" y1="268" x2="800" y2="224" marker-end="url(#arr)"></line> |
| 191 | <defs> |
| 192 | <marker id="arr" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="7" markerHeight="7" orient="auto-start-reverse"> |
| 193 | <path d="M0,0 L8,4 L0,8 z" fill="currentColor" opacity="0.55"></path> |
| 194 | </marker> |
| 195 | </defs> |
| 196 | </svg>`; |
| 197 | } |
| 198 | |
| 199 | function renderSysmap() { |
| 200 | const holder = document.querySelector(".sysmap"); |
| 201 | if (!holder) return; |
| 202 | const highlights = (holder.dataset.highlight || "").split(/\s+/).filter(Boolean); |
| 203 | holder.innerHTML = sysmapSVG(highlights); |
| 204 | } |
| 205 | |
| 206 | document.addEventListener("DOMContentLoaded", () => { |
| 207 | renderSidebar(); |
| 208 | renderSysmap(); |
| 209 | renderHeadingAnchors(); |
| 210 | renderPager(); |
| 211 | }); |
| 212 | |