app.js javascript
270 lines 8.4 KB
Raw
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