app.js
javascript
sha256:a78e7e5a8740e03315f325d19edeb3aa1b306b3337d04abbaa9a9e0f3bbeb7a1
docs: MuseHub-first before ISR #74 — staging solidify NEXT
Human
3 days ago
| 1 | /** Overseer Kit ok app static UI — in-memory auth only (§Q0.6.2 / §Q4A / §LAC.6.3). */ |
| 2 | |
| 3 | let sessionCredential = null; |
| 4 | let csrfToken = null; |
| 5 | let statusAutoFetched = false; |
| 6 | |
| 7 | const TAB_EXPLAINERS = { |
| 8 | overview: "What this console is; suite doors; how to open it; bound repo", |
| 9 | status: "Health of governance for the bound repo", |
| 10 | roadmap: "Living phase board", |
| 11 | handover: "Next-session relay / paste prompt", |
| 12 | gates: "Pending honesty/governance reminders", |
| 13 | actions: "Dry-run-first freeze review + governance-sync + honesty-status", |
| 14 | ledger: "Append-only honesty ledger view/verify", |
| 15 | structure: "Full flowchart gallery", |
| 16 | }; |
| 17 | |
| 18 | function setAuthStatus(message, ok) { |
| 19 | const el = document.getElementById("auth-status"); |
| 20 | el.textContent = message; |
| 21 | el.className = ok ? "status ok" : "status error"; |
| 22 | } |
| 23 | |
| 24 | function authHeaders(mutating) { |
| 25 | const headers = { |
| 26 | Authorization: `Bearer ${sessionCredential}`, |
| 27 | }; |
| 28 | if (mutating) { |
| 29 | headers["X-Overseer-CSRF"] = csrfToken; |
| 30 | headers["Content-Type"] = "application/json"; |
| 31 | } |
| 32 | return headers; |
| 33 | } |
| 34 | |
| 35 | async function apiFetch(path, { method = "GET", body = null, mutating = false } = {}) { |
| 36 | const response = await fetch(path, { |
| 37 | method, |
| 38 | headers: authHeaders(mutating), |
| 39 | body: body === null ? undefined : JSON.stringify(body), |
| 40 | }); |
| 41 | const payload = await response.json(); |
| 42 | return { response, payload }; |
| 43 | } |
| 44 | |
| 45 | function showJson(targetId, payload) { |
| 46 | document.getElementById(targetId).textContent = JSON.stringify(payload, null, 2); |
| 47 | } |
| 48 | |
| 49 | function requireConfirm(message) { |
| 50 | return window.confirm(message); |
| 51 | } |
| 52 | |
| 53 | function setBoundRepo(repoRoot) { |
| 54 | const wrap = document.getElementById("bound-repo"); |
| 55 | const pathEl = document.getElementById("bound-repo-path"); |
| 56 | if (!repoRoot) { |
| 57 | wrap.hidden = true; |
| 58 | pathEl.textContent = ""; |
| 59 | return; |
| 60 | } |
| 61 | pathEl.textContent = String(repoRoot); |
| 62 | wrap.hidden = false; |
| 63 | } |
| 64 | |
| 65 | function collapseAuthPanel() { |
| 66 | document.getElementById("auth-panel").hidden = true; |
| 67 | document.getElementById("session-collapsed").hidden = false; |
| 68 | } |
| 69 | |
| 70 | function expandAuthPanel() { |
| 71 | document.getElementById("auth-panel").hidden = false; |
| 72 | document.getElementById("session-collapsed").hidden = true; |
| 73 | statusAutoFetched = false; |
| 74 | } |
| 75 | |
| 76 | function setTabExplainer(tabId) { |
| 77 | const el = document.getElementById("tab-explainer"); |
| 78 | const text = TAB_EXPLAINERS[tabId]; |
| 79 | if (!text) { |
| 80 | el.hidden = true; |
| 81 | el.textContent = ""; |
| 82 | return; |
| 83 | } |
| 84 | el.textContent = text; |
| 85 | el.hidden = false; |
| 86 | } |
| 87 | |
| 88 | async function refreshStatusOnce() { |
| 89 | const { payload } = await apiFetch("/api/status"); |
| 90 | humanizeStatus(payload); |
| 91 | showJson("status-output", payload); |
| 92 | } |
| 93 | |
| 94 | function activateTab(tabId) { |
| 95 | document.querySelectorAll(".tabs button").forEach((b) => b.classList.remove("active")); |
| 96 | document.querySelectorAll(".tab-panel").forEach((p) => p.classList.remove("active")); |
| 97 | const button = document.querySelector(`.tabs button[data-tab="${tabId}"]`); |
| 98 | const panel = document.getElementById(`tab-${tabId}`); |
| 99 | if (button) button.classList.add("active"); |
| 100 | if (panel) panel.classList.add("active"); |
| 101 | setTabExplainer(tabId); |
| 102 | if (tabId === "status" && !statusAutoFetched) { |
| 103 | statusAutoFetched = true; |
| 104 | refreshStatusOnce().catch((err) => { |
| 105 | showJson("status-output", { ok: false, error: String(err) }); |
| 106 | }); |
| 107 | } |
| 108 | } |
| 109 | |
| 110 | function flagLabel(value) { |
| 111 | if (value === true) return "ok"; |
| 112 | if (value === false) return "fail"; |
| 113 | return "n/a"; |
| 114 | } |
| 115 | |
| 116 | function nested(obj, path) { |
| 117 | let cur = obj; |
| 118 | for (const key of path) { |
| 119 | if (cur === null || cur === undefined || typeof cur !== "object") return undefined; |
| 120 | cur = cur[key]; |
| 121 | } |
| 122 | return cur; |
| 123 | } |
| 124 | |
| 125 | function humanizeStatus(payload) { |
| 126 | const result = payload && typeof payload.result === "object" && payload.result !== null |
| 127 | ? payload.result |
| 128 | : {}; |
| 129 | const pending = nested(result, ["governance_gates", "pending"]); |
| 130 | const pendingCount = Array.isArray(pending) ? String(pending.length) : "n/a"; |
| 131 | const rows = [ |
| 132 | { label: "Regime", value: nested(result, ["vcs", "regime"]) ?? "n/a" }, |
| 133 | { label: "Substrate", value: flagLabel(nested(result, ["substrate", "ok"])) }, |
| 134 | { label: "Muse sync", value: flagLabel(nested(result, ["muse_sync", "ok"])) }, |
| 135 | { |
| 136 | label: "Footprint", |
| 137 | value: flagLabel(nested(result, ["footprint_self_integrity", "ok"])), |
| 138 | }, |
| 139 | { |
| 140 | label: "Exit code", |
| 141 | value: payload && payload.exit_code !== undefined && payload.exit_code !== null |
| 142 | ? String(payload.exit_code) |
| 143 | : "n/a", |
| 144 | }, |
| 145 | { label: "Pending gates", value: pendingCount }, |
| 146 | ]; |
| 147 | const root = document.getElementById("status-summary"); |
| 148 | root.replaceChildren(); |
| 149 | for (const row of rows) { |
| 150 | const card = document.createElement("div"); |
| 151 | card.className = "status-card"; |
| 152 | const label = document.createElement("span"); |
| 153 | label.className = "label"; |
| 154 | label.textContent = row.label; |
| 155 | const value = document.createElement("span"); |
| 156 | value.className = "value"; |
| 157 | value.textContent = String(row.value); |
| 158 | card.append(label, value); |
| 159 | root.append(card); |
| 160 | } |
| 161 | } |
| 162 | |
| 163 | document.getElementById("auth-save").addEventListener("click", async () => { |
| 164 | sessionCredential = document.getElementById("session-input").value.trim(); |
| 165 | csrfToken = document.getElementById("csrf-input").value.trim(); |
| 166 | if (!sessionCredential || !csrfToken) { |
| 167 | setAuthStatus("Both values are required.", false); |
| 168 | return; |
| 169 | } |
| 170 | try { |
| 171 | const { response, payload } = await apiFetch("/api/health"); |
| 172 | if (!response.ok || !payload.ok) { |
| 173 | setAuthStatus("Authentication failed.", false); |
| 174 | return; |
| 175 | } |
| 176 | const repoRoot = nested(payload, ["result", "repo_root"]); |
| 177 | setBoundRepo(repoRoot); |
| 178 | setAuthStatus("Connected.", true); |
| 179 | collapseAuthPanel(); |
| 180 | document.getElementById("tabs").hidden = false; |
| 181 | document.getElementById("content").hidden = false; |
| 182 | activateTab("overview"); |
| 183 | } catch (err) { |
| 184 | setAuthStatus(`Connection error: ${err}`, false); |
| 185 | } |
| 186 | }); |
| 187 | |
| 188 | document.getElementById("auth-reconnect").addEventListener("click", () => { |
| 189 | expandAuthPanel(); |
| 190 | setAuthStatus("Paste credentials for a new session.", false); |
| 191 | }); |
| 192 | |
| 193 | document.querySelectorAll(".tabs button").forEach((button) => { |
| 194 | button.addEventListener("click", () => { |
| 195 | activateTab(button.dataset.tab); |
| 196 | }); |
| 197 | }); |
| 198 | |
| 199 | document.getElementById("open-structure").addEventListener("click", () => { |
| 200 | activateTab("structure"); |
| 201 | }); |
| 202 | |
| 203 | document.getElementById("refresh-status").addEventListener("click", async () => { |
| 204 | await refreshStatusOnce(); |
| 205 | }); |
| 206 | |
| 207 | document.getElementById("load-roadmap").addEventListener("click", async () => { |
| 208 | const { payload } = await apiFetch("/api/docs/roadmap"); |
| 209 | showJson("roadmap-output", payload); |
| 210 | }); |
| 211 | |
| 212 | document.getElementById("load-handover").addEventListener("click", async () => { |
| 213 | const { payload } = await apiFetch("/api/docs/handover"); |
| 214 | showJson("handover-output", payload); |
| 215 | }); |
| 216 | |
| 217 | document.getElementById("load-gates").addEventListener("click", async () => { |
| 218 | const { payload } = await apiFetch("/api/gates"); |
| 219 | showJson("gates-output", payload); |
| 220 | }); |
| 221 | |
| 222 | document.getElementById("run-freeze").addEventListener("click", async () => { |
| 223 | const dryRun = document.getElementById("freeze-dry-run").checked; |
| 224 | if (!dryRun && !requireConfirm("Run freeze review with stamp write? This is irreversible on the feature branch.")) { |
| 225 | return; |
| 226 | } |
| 227 | const path = document.getElementById("freeze-path").value.trim(); |
| 228 | const { payload } = await apiFetch("/api/review/freeze", { |
| 229 | method: "POST", |
| 230 | mutating: true, |
| 231 | body: { path, dry_run: dryRun }, |
| 232 | }); |
| 233 | showJson("freeze-output", payload); |
| 234 | }); |
| 235 | |
| 236 | document.getElementById("run-sync").addEventListener("click", async () => { |
| 237 | const write = document.getElementById("sync-write").checked; |
| 238 | if (write && !requireConfirm("Apply governance-sync writes on the feature branch?")) { |
| 239 | return; |
| 240 | } |
| 241 | const { payload } = await apiFetch("/api/governance-sync", { |
| 242 | method: "POST", |
| 243 | mutating: true, |
| 244 | body: { write }, |
| 245 | }); |
| 246 | showJson("sync-output", payload); |
| 247 | }); |
| 248 | |
| 249 | document.getElementById("run-honesty").addEventListener("click", async () => { |
| 250 | const { payload } = await apiFetch("/api/honesty-status", { |
| 251 | method: "POST", |
| 252 | mutating: true, |
| 253 | body: {}, |
| 254 | }); |
| 255 | showJson("honesty-output", payload); |
| 256 | }); |
| 257 | |
| 258 | document.getElementById("ledger-show").addEventListener("click", async () => { |
| 259 | const { payload } = await apiFetch("/api/ledger/show"); |
| 260 | showJson("ledger-output", payload); |
| 261 | }); |
| 262 | |
| 263 | document.getElementById("ledger-verify").addEventListener("click", async () => { |
| 264 | const { payload } = await apiFetch("/api/ledger/verify", { |
| 265 | method: "POST", |
| 266 | mutating: true, |
| 267 | body: {}, |
| 268 | }); |
| 269 | showJson("ledger-output", payload); |
| 270 | }); |
File History
1 commit
sha256:a78e7e5a8740e03315f325d19edeb3aa1b306b3337d04abbaa9a9e0f3bbeb7a1
docs: MuseHub-first before ISR #74 — staging solidify NEXT
Human
3 days ago