mustflow 1.30.0 → 2.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +35 -11
  2. package/dist/cli/commands/classify.js +61 -6
  3. package/dist/cli/commands/contract-lint.js +13 -4
  4. package/dist/cli/commands/dashboard.js +6 -0
  5. package/dist/cli/commands/index.js +5 -0
  6. package/dist/cli/commands/run.js +224 -48
  7. package/dist/cli/commands/upgrade.js +65 -0
  8. package/dist/cli/commands/verify.js +550 -33
  9. package/dist/cli/i18n/en.js +73 -10
  10. package/dist/cli/i18n/es.js +73 -10
  11. package/dist/cli/i18n/fr.js +73 -10
  12. package/dist/cli/i18n/hi.js +73 -10
  13. package/dist/cli/i18n/ko.js +73 -10
  14. package/dist/cli/i18n/zh.js +73 -10
  15. package/dist/cli/index.js +27 -46
  16. package/dist/cli/lib/command-registry.js +5 -0
  17. package/dist/cli/lib/dashboard-export.js +62 -12
  18. package/dist/cli/lib/dashboard-html/client-script.js +1936 -0
  19. package/dist/cli/lib/dashboard-html/locale-bootstrap.js +8 -0
  20. package/dist/cli/lib/dashboard-html/styles.js +572 -0
  21. package/dist/cli/lib/dashboard-html/template.js +134 -0
  22. package/dist/cli/lib/dashboard-html/types.js +1 -0
  23. package/dist/cli/lib/dashboard-html.js +1 -1907
  24. package/dist/cli/lib/dashboard-locale.js +37 -0
  25. package/dist/cli/lib/local-index/constants.js +48 -0
  26. package/dist/cli/lib/local-index/index.js +2256 -0
  27. package/dist/cli/lib/local-index/sql.js +15 -0
  28. package/dist/cli/lib/local-index/types.js +1 -0
  29. package/dist/cli/lib/local-index.js +1 -1908
  30. package/dist/cli/lib/reporter.js +6 -0
  31. package/dist/cli/lib/run-plan.js +96 -4
  32. package/dist/cli/lib/templates.js +18 -1
  33. package/dist/cli/lib/validation/command-intents.js +11 -0
  34. package/dist/cli/lib/validation/constants.js +238 -0
  35. package/dist/cli/lib/validation/index.js +1384 -0
  36. package/dist/cli/lib/validation/primitives.js +198 -0
  37. package/dist/cli/lib/validation/test-selection.js +95 -0
  38. package/dist/cli/lib/validation/types.js +1 -0
  39. package/dist/cli/lib/validation.js +1 -1661
  40. package/dist/core/bounded-output.js +38 -0
  41. package/dist/core/change-classification.js +6 -2
  42. package/dist/core/change-verification.js +240 -6
  43. package/dist/core/check-issues.js +12 -0
  44. package/dist/core/command-contract-validation.js +20 -0
  45. package/dist/core/command-effects.js +13 -0
  46. package/dist/core/completion-verdict.js +209 -0
  47. package/dist/core/contract-lint.js +316 -7
  48. package/dist/core/dashboard-verification.js +8 -0
  49. package/dist/core/external-evidence.js +9 -0
  50. package/dist/core/public-json-contracts.js +28 -0
  51. package/dist/core/repeated-failure.js +17 -0
  52. package/dist/core/repro-evidence.js +53 -0
  53. package/dist/core/run-performance-history.js +307 -0
  54. package/dist/core/run-profile.js +87 -0
  55. package/dist/core/run-receipt.js +171 -4
  56. package/dist/core/run-write-drift.js +18 -2
  57. package/dist/core/scope-risk.js +64 -0
  58. package/dist/core/skill-route-alignment.js +110 -0
  59. package/dist/core/source-anchor-status.js +4 -1
  60. package/dist/core/test-selection.js +227 -0
  61. package/dist/core/validation-ratchet.js +52 -0
  62. package/dist/core/verification-decision-graph.js +67 -0
  63. package/dist/core/verification-evidence.js +249 -0
  64. package/dist/core/verification-scheduler.js +96 -2
  65. package/examples/README.md +12 -4
  66. package/package.json +1 -1
  67. package/schemas/README.md +18 -4
  68. package/schemas/change-verification-report.schema.json +169 -5
  69. package/schemas/commands.schema.json +51 -1
  70. package/schemas/contract-lint-report.schema.json +80 -0
  71. package/schemas/dashboard-export.schema.json +500 -0
  72. package/schemas/explain-report.schema.json +2 -0
  73. package/schemas/latest-run-pointer.schema.json +384 -0
  74. package/schemas/run-receipt.schema.json +113 -0
  75. package/schemas/test-selection.schema.json +81 -0
  76. package/schemas/verify-report.schema.json +361 -1
  77. package/schemas/verify-run-manifest.schema.json +410 -0
  78. package/templates/default/common/.mustflow/config/commands.toml +1 -1
  79. package/templates/default/i18n.toml +1 -1
  80. package/templates/default/locales/en/.mustflow/skills/INDEX.md +124 -29
  81. package/templates/default/locales/en/.mustflow/skills/routes.toml +289 -0
  82. package/templates/default/manifest.toml +29 -2
@@ -0,0 +1,1936 @@
1
+ export function renderDashboardClientScript(options) {
2
+ const { serializedSnapshot, serializedToken, serializedLocaleBundle, serializedAvailableLocales, serializedStatusSnapshot, serializedDocReviewSnapshot, } = options;
3
+ return `const initialSnapshot = ${serializedSnapshot};
4
+ const dashboardToken = ${serializedToken};
5
+ const dashboardLocales = ${serializedLocaleBundle};
6
+ const availableLocales = ${serializedAvailableLocales};
7
+ const initialStatusSnapshot = ${serializedStatusSnapshot};
8
+ const initialDocReview = ${serializedDocReviewSnapshot};
9
+ let snapshot = initialSnapshot;
10
+ let pending = new Map();
11
+ let currentLocale = resolveInitialLocale();
12
+ let statusState = { key: "dashboard.ui.noChanges", text: "", type: "" };
13
+ let currentTab = "status";
14
+ let dashboardStatus = initialStatusSnapshot;
15
+ let docReview = initialDocReview;
16
+ let lastUpdatedAt = new Date();
17
+ let loadingCount = 0;
18
+ const listFilters = {
19
+ verification: { query: "", state: "all" },
20
+ commands: { query: "", state: "all" },
21
+ skills: { query: "", state: "all" }
22
+ };
23
+
24
+ const groups = [
25
+ ["dashboard.group.git", ["git.auto_stage", "git.auto_commit", "git.auto_push"]],
26
+ ["dashboard.group.commitMessage", "git.commit_message."],
27
+ ["dashboard.group.reporting", "reporting."],
28
+ ["dashboard.group.verification", "verification.selection."],
29
+ ["dashboard.group.testAuthoring", "testing.authoring."],
30
+ ["dashboard.group.codeStyle", "code_style."],
31
+ ["dashboard.group.refactoring", "refactoring.hotspots."],
32
+ ["dashboard.group.versioning", "release.versioning."]
33
+ ];
34
+ const copyFeedbackMs = 1500;
35
+ const docStatusFilters = ["active", "pending", "in_review", "changes_made", "needs_human", "approved", "ignored", "all"];
36
+ const reviewerKinds = ["human", "llm", "tool", "external"];
37
+
38
+ function resolveInitialLocale() {
39
+ const stored = window.localStorage.getItem("mustflow.dashboard.language");
40
+ if (availableLocales.includes(stored)) return stored;
41
+ const browserLocale = (window.navigator.language || "").slice(0, 2).toLowerCase();
42
+ return availableLocales.includes(browserLocale) ? browserLocale : "en";
43
+ }
44
+
45
+ function message(key) {
46
+ return dashboardLocales.messages[currentLocale]?.[key] ?? dashboardLocales.messages.en[key] ?? key;
47
+ }
48
+
49
+ function messageWithTime(key, time) {
50
+ return messageFormat(key, { time });
51
+ }
52
+
53
+ function messageWithCount(key, count) {
54
+ return messageFormat(key, { count });
55
+ }
56
+
57
+ function messageFormat(key, values) {
58
+ let text = message(key);
59
+ for (const [name, value] of Object.entries(values)) {
60
+ text = text.replaceAll("{" + name + "}", String(value));
61
+ }
62
+ return text;
63
+ }
64
+
65
+ function messageExists(key) {
66
+ return Boolean(dashboardLocales.messages[currentLocale]?.[key] ?? dashboardLocales.messages.en[key]);
67
+ }
68
+
69
+ function statusText(text, type = "") {
70
+ statusState = { key: "", text, type };
71
+ renderStatus();
72
+ }
73
+
74
+ function statusKey(key, type = "") {
75
+ statusState = { key, text: "", type };
76
+ renderStatus();
77
+ }
78
+
79
+ function renderStatus() {
80
+ const element = document.getElementById("status");
81
+ const text = statusState.key ? message(statusState.key) : statusState.text;
82
+ element.textContent = text;
83
+ element.className = statusState.type ? "status " + statusState.type : "status";
84
+ }
85
+
86
+ function renderLastUpdated() {
87
+ const element = document.getElementById("last-updated");
88
+ const formatted = new Intl.DateTimeFormat(currentLocale, {
89
+ hour: "2-digit",
90
+ minute: "2-digit",
91
+ second: "2-digit"
92
+ }).format(lastUpdatedAt);
93
+ element.textContent = messageWithTime("dashboard.ui.lastUpdated", formatted);
94
+ }
95
+
96
+ function setLoading(loading) {
97
+ loadingCount = Math.max(0, loadingCount + (loading ? 1 : -1));
98
+ const isLoading = loadingCount > 0;
99
+ const reload = document.getElementById("reload");
100
+ document.body.setAttribute("aria-busy", isLoading ? "true" : "false");
101
+ reload.disabled = isLoading;
102
+ reload.setAttribute("aria-disabled", isLoading ? "true" : "false");
103
+ if (isLoading) statusKey("dashboard.ui.loading");
104
+ }
105
+
106
+ function markDataUpdated() {
107
+ lastUpdatedAt = new Date();
108
+ renderLastUpdated();
109
+ }
110
+
111
+ function settingValue(id) {
112
+ return pending.has(id) ? pending.get(id) : snapshot.settings.find((setting) => setting.id === id)?.value;
113
+ }
114
+
115
+ function settingDescriptionKey(setting) {
116
+ const valueSpecificKey = "dashboard.setting." + setting.id + ".description." + String(settingValue(setting.id));
117
+ if (messageExists(valueSpecificKey)) return valueSpecificKey;
118
+ const key = "dashboard.setting." + setting.id + ".description";
119
+ return messageExists(key) ? key : "";
120
+ }
121
+
122
+ function settingDescription(setting) {
123
+ const key = settingDescriptionKey(setting);
124
+ return key ? message(key) : "";
125
+ }
126
+
127
+ function updateSettingDescription(id) {
128
+ const setting = snapshot.settings.find((item) => item.id === id);
129
+ if (!setting) return;
130
+ const element = document.getElementById(controlId(setting) + "-description");
131
+ if (element) element.textContent = settingDescription(setting);
132
+ }
133
+
134
+ function formatSettingValue(value) {
135
+ if (typeof value === "boolean") {
136
+ return message(value ? "dashboard.status.yes" : "dashboard.status.no");
137
+ }
138
+ return String(value);
139
+ }
140
+
141
+ function settingDisplayName(setting) {
142
+ return message("dashboard.setting." + setting.id) || setting.label;
143
+ }
144
+
145
+ function updateSaveState() {
146
+ document.getElementById("save").disabled = pending.size === 0;
147
+ }
148
+
149
+ function setPending(id, value) {
150
+ const original = snapshot.settings.find((setting) => setting.id === id)?.value;
151
+ if (Object.is(original, value)) {
152
+ pending.delete(id);
153
+ } else {
154
+ pending.set(id, value);
155
+ }
156
+ updateSaveState();
157
+ statusKey(pending.size === 0 ? "dashboard.ui.noChanges" : "dashboard.ui.unsavedChanges");
158
+ updateSettingDescription(id);
159
+ renderSettingsPendingSummary();
160
+ }
161
+
162
+ function controlId(setting) {
163
+ return "setting-" + setting.id.replace(/[^a-zA-Z0-9_-]/g, "-");
164
+ }
165
+
166
+ function renderInput(setting) {
167
+ if (setting.kind === "boolean") {
168
+ const input = document.createElement("input");
169
+ const inputId = controlId(setting);
170
+ input.id = inputId;
171
+ input.name = setting.id;
172
+ input.type = "checkbox";
173
+ input.checked = Boolean(settingValue(setting.id));
174
+ input.disabled = !setting.editable;
175
+ input.addEventListener("change", () => setPending(setting.id, input.checked));
176
+ return input;
177
+ }
178
+
179
+ if (setting.kind === "number") {
180
+ const input = document.createElement("input");
181
+ const inputId = controlId(setting);
182
+ input.id = inputId;
183
+ input.name = setting.id;
184
+ input.type = "number";
185
+ input.value = String(settingValue(setting.id));
186
+ if (setting.min !== undefined) input.min = String(setting.min);
187
+ if (setting.max !== undefined) input.max = String(setting.max);
188
+ input.disabled = !setting.editable;
189
+ input.addEventListener("input", () => setPending(setting.id, Number(input.value)));
190
+ return input;
191
+ }
192
+
193
+ if (setting.acceptsLocaleTag) {
194
+ const wrapper = document.createElement("div");
195
+ const select = document.createElement("select");
196
+ const customInput = document.createElement("input");
197
+ const inputId = controlId(setting);
198
+ const customInputId = inputId + "-custom";
199
+ const optionValues = setting.options || [];
200
+ const currentValue = String(settingValue(setting.id));
201
+ const customLocaleOptionValue = "__mustflow_custom_locale__";
202
+ const isCustomValue = !optionValues.includes(currentValue);
203
+ wrapper.className = "locale-tag-control";
204
+ select.id = inputId;
205
+ select.name = setting.id;
206
+ for (const option of optionValues) {
207
+ const child = document.createElement("option");
208
+ child.value = option;
209
+ child.textContent = option;
210
+ child.selected = option === currentValue;
211
+ select.appendChild(child);
212
+ }
213
+ const customChild = document.createElement("option");
214
+ customChild.value = customLocaleOptionValue;
215
+ customChild.textContent = message("dashboard.ui.customLocale");
216
+ customChild.selected = isCustomValue;
217
+ select.appendChild(customChild);
218
+ select.disabled = !setting.editable;
219
+
220
+ customInput.id = customInputId;
221
+ customInput.name = setting.id + ".custom";
222
+ customInput.type = "text";
223
+ customInput.autocomplete = "off";
224
+ customInput.spellcheck = false;
225
+ customInput.placeholder = "pt-BR";
226
+ customInput.value = isCustomValue ? currentValue : "";
227
+ customInput.hidden = !isCustomValue;
228
+ customInput.disabled = !setting.editable || !isCustomValue;
229
+ customInput.setAttribute("aria-label", message("dashboard.ui.customLocale"));
230
+
231
+ function updateCustomPending() {
232
+ const value = customInput.value.trim();
233
+ if (value.length > 0) {
234
+ setPending(setting.id, value);
235
+ } else {
236
+ const original = snapshot.settings.find((item) => item.id === setting.id)?.value;
237
+ if (original !== undefined) {
238
+ setPending(setting.id, original);
239
+ }
240
+ }
241
+ }
242
+
243
+ select.addEventListener("change", () => {
244
+ const customSelected = select.value === customLocaleOptionValue;
245
+ customInput.hidden = !customSelected;
246
+ customInput.disabled = !setting.editable || !customSelected;
247
+ if (customSelected) {
248
+ customInput.focus();
249
+ updateCustomPending();
250
+ } else {
251
+ setPending(setting.id, select.value);
252
+ }
253
+ });
254
+ customInput.addEventListener("input", updateCustomPending);
255
+ wrapper.appendChild(select);
256
+ wrapper.appendChild(customInput);
257
+ return wrapper;
258
+ }
259
+
260
+ const select = document.createElement("select");
261
+ const inputId = controlId(setting);
262
+ select.id = inputId;
263
+ select.name = setting.id;
264
+ for (const option of setting.options || []) {
265
+ const child = document.createElement("option");
266
+ child.value = option;
267
+ child.textContent = option;
268
+ child.selected = option === settingValue(setting.id);
269
+ select.appendChild(child);
270
+ }
271
+ select.disabled = !setting.editable;
272
+ select.addEventListener("change", () => setPending(setting.id, select.value));
273
+ return select;
274
+ }
275
+
276
+ function render() {
277
+ const root = document.getElementById("settings");
278
+ root.textContent = "";
279
+ renderSettingsPendingSummary();
280
+ for (const [titleKey, matcher] of groups) {
281
+ const settings = Array.isArray(matcher)
282
+ ? snapshot.settings.filter((setting) => matcher.includes(setting.id))
283
+ : snapshot.settings.filter((setting) => setting.id.startsWith(matcher));
284
+ if (settings.length === 0) continue;
285
+ const section = document.createElement("section");
286
+ const heading = document.createElement("h2");
287
+ heading.textContent = message(titleKey);
288
+ section.appendChild(heading);
289
+ for (const setting of settings) {
290
+ const row = document.createElement("div");
291
+ row.className = "setting";
292
+ const label = document.createElement("label");
293
+ label.htmlFor = controlId(setting);
294
+ const labelText = document.createElement("div");
295
+ labelText.className = "label";
296
+ const labelName = document.createElement("span");
297
+ labelName.textContent = settingDisplayName(setting);
298
+ labelText.appendChild(labelName);
299
+ const descriptionText = settingDescription(setting);
300
+ if (descriptionText) {
301
+ const description = document.createElement("span");
302
+ description.id = controlId(setting) + "-description";
303
+ description.className = "value-description";
304
+ description.textContent = descriptionText;
305
+ labelText.appendChild(description);
306
+ }
307
+ label.appendChild(labelText);
308
+ if (!setting.editable) {
309
+ const meta = document.createElement("div");
310
+ meta.className = "meta";
311
+ meta.textContent = setting.lockedReason
312
+ ? message("dashboard.ui.locked") + ": " + message(setting.lockedReason)
313
+ : message("dashboard.ui.locked");
314
+ label.appendChild(meta);
315
+ }
316
+ row.appendChild(label);
317
+ row.appendChild(renderInput(setting));
318
+ section.appendChild(row);
319
+ }
320
+ root.appendChild(section);
321
+ }
322
+ }
323
+
324
+ function renderSettingsPendingSummary() {
325
+ const root = document.getElementById("settings-pending-summary");
326
+ if (!root) return;
327
+ root.textContent = "";
328
+ if (pending.size === 0) {
329
+ root.hidden = true;
330
+ return;
331
+ }
332
+ root.hidden = false;
333
+
334
+ const header = document.createElement("div");
335
+ header.className = "settings-pending-header";
336
+ const title = document.createElement("div");
337
+ title.className = "settings-pending-title";
338
+ title.textContent = messageWithCount("dashboard.settings.pendingHeading", String(pending.size));
339
+ const reset = document.createElement("button");
340
+ reset.type = "button";
341
+ reset.textContent = message("dashboard.settings.resetChanges");
342
+ reset.addEventListener("click", resetPendingSettings);
343
+ header.appendChild(title);
344
+ header.appendChild(reset);
345
+ root.appendChild(header);
346
+
347
+ const list = document.createElement("ul");
348
+ list.className = "settings-pending-list";
349
+ for (const [id, value] of pending) {
350
+ const setting = snapshot.settings.find((item) => item.id === id);
351
+ if (!setting) continue;
352
+ const item = document.createElement("li");
353
+ item.textContent = messageFormat("dashboard.settings.pendingItem", {
354
+ name: settingDisplayName(setting),
355
+ from: formatSettingValue(setting.value),
356
+ to: formatSettingValue(value),
357
+ });
358
+ list.appendChild(item);
359
+ }
360
+ root.appendChild(list);
361
+ }
362
+
363
+ function resetPendingSettings() {
364
+ pending = new Map();
365
+ updateSaveState();
366
+ statusKey("dashboard.ui.noChanges");
367
+ render();
368
+ }
369
+
370
+ function renderLocaleSelector() {
371
+ const select = document.getElementById("dashboard-language");
372
+ select.textContent = "";
373
+ for (const locale of availableLocales) {
374
+ const option = document.createElement("option");
375
+ option.value = locale;
376
+ option.textContent = dashboardLocales.names[locale] ?? locale;
377
+ option.selected = locale === currentLocale;
378
+ select.appendChild(option);
379
+ }
380
+ }
381
+
382
+ function renderChrome() {
383
+ document.documentElement.lang = currentLocale;
384
+ document.getElementById("dashboard-title").textContent = message("dashboard.ui.title");
385
+ document.getElementById("tab-status").textContent = message("dashboard.tab.status");
386
+ document.getElementById("tab-verification").textContent = message("dashboard.tab.verification") + " (" + dashboardStatus.verification.recommendations.length + ")";
387
+ document.getElementById("tab-commands").textContent = message("dashboard.tab.commands");
388
+ document.getElementById("tab-release").textContent = message("dashboard.tab.release");
389
+ document.getElementById("tab-update").textContent = message("dashboard.tab.update") + " (" + (dashboardStatus.update.blockers.length + dashboardStatus.update.changes.length) + ")";
390
+ document.getElementById("tab-runs").textContent = message("dashboard.tab.runs");
391
+ document.getElementById("tab-skills").textContent = message("dashboard.tab.skills") + " (" + dashboardStatus.skills.count + ")";
392
+ document.getElementById("tab-settings").textContent = message("dashboard.tab.settings");
393
+ document.getElementById("tab-documents").textContent = message("dashboard.tab.documents") + " (" + docReview.count + ")";
394
+ const openMustflow = document.getElementById("open-mustflow");
395
+ openMustflow.title = message("dashboard.ui.openMustflow");
396
+ openMustflow.setAttribute("aria-label", message("dashboard.ui.openMustflow"));
397
+ document.getElementById("dashboard-language-label").textContent = message("dashboard.ui.language");
398
+ document.getElementById("reload").textContent = message("dashboard.ui.reload");
399
+ document.getElementById("save").textContent = message("dashboard.ui.save");
400
+ document.getElementById("save").hidden = currentTab !== "settings";
401
+ document.getElementById("doc-status-filter-label").textContent = message("dashboard.docs.statusFilter");
402
+ document.getElementById("doc-path-filter-label").textContent = message("dashboard.docs.pathFilter");
403
+ document.getElementById("doc-review-fields-label").textContent = message("dashboard.docs.reviewFields");
404
+ document.getElementById("doc-reviewer-kind-label").textContent = message("dashboard.docs.reviewerKind");
405
+ document.getElementById("doc-reviewer-id-label").textContent = message("dashboard.docs.reviewerId");
406
+ document.getElementById("doc-review-summary-label").textContent = message("dashboard.docs.summary");
407
+ document.getElementById("doc-path-filter").placeholder = message("dashboard.docs.pathFilterPlaceholder");
408
+ document.getElementById("doc-reviewer-id").placeholder = message("dashboard.docs.reviewerIdPlaceholder");
409
+ document.getElementById("doc-review-summary").placeholder = message("dashboard.docs.summaryPlaceholder");
410
+ renderStatus();
411
+ renderLastUpdated();
412
+ renderDocumentReviewerState();
413
+ }
414
+
415
+ function renderTabState() {
416
+ for (const tab of document.querySelectorAll(".tab")) {
417
+ const selected = tab.dataset.tab === currentTab;
418
+ tab.setAttribute("aria-selected", selected ? "true" : "false");
419
+ tab.setAttribute("tabindex", selected ? "0" : "-1");
420
+ }
421
+ document.getElementById("panel-status").hidden = currentTab !== "status";
422
+ document.getElementById("panel-verification").hidden = currentTab !== "verification";
423
+ document.getElementById("panel-commands").hidden = currentTab !== "commands";
424
+ document.getElementById("panel-release").hidden = currentTab !== "release";
425
+ document.getElementById("panel-update").hidden = currentTab !== "update";
426
+ document.getElementById("panel-runs").hidden = currentTab !== "runs";
427
+ document.getElementById("panel-skills").hidden = currentTab !== "skills";
428
+ document.getElementById("panel-settings").hidden = currentTab !== "settings";
429
+ document.getElementById("panel-documents").hidden = currentTab !== "documents";
430
+ renderChrome();
431
+ }
432
+
433
+ function loadCurrentTabData() {
434
+ if (currentTab === "documents") return loadDocuments();
435
+ if (currentTab === "settings") return Promise.resolve();
436
+ return loadStatus();
437
+ }
438
+
439
+ function reloadCurrentTabData() {
440
+ if (currentTab === "settings") return loadSnapshot();
441
+ return loadCurrentTabData();
442
+ }
443
+
444
+ function activateTab(tabName, options = {}) {
445
+ currentTab = tabName;
446
+ renderTabState();
447
+ if (options.focus) {
448
+ const selectedTab = Array.from(document.querySelectorAll(".tab")).find((tab) => tab.dataset.tab === currentTab);
449
+ if (selectedTab) selectedTab.focus();
450
+ }
451
+ loadCurrentTabData().catch((error) => statusText(error.message, "error"));
452
+ }
453
+
454
+ function handleTabKeydown(event) {
455
+ const tabs = Array.from(document.querySelectorAll(".tab"));
456
+ const currentIndex = tabs.findIndex((tab) => tab === event.currentTarget);
457
+ if (currentIndex < 0) return;
458
+
459
+ let nextIndex = null;
460
+ if (event.key === "ArrowRight" || event.key === "ArrowDown") {
461
+ nextIndex = (currentIndex + 1) % tabs.length;
462
+ } else if (event.key === "ArrowLeft" || event.key === "ArrowUp") {
463
+ nextIndex = (currentIndex - 1 + tabs.length) % tabs.length;
464
+ } else if (event.key === "Home") {
465
+ nextIndex = 0;
466
+ } else if (event.key === "End") {
467
+ nextIndex = tabs.length - 1;
468
+ }
469
+
470
+ if (nextIndex === null) return;
471
+ event.preventDefault();
472
+ activateTab(tabs[nextIndex].dataset.tab, { focus: true });
473
+ }
474
+
475
+ async function openMustflowFolder() {
476
+ const response = await fetch("/api/open-mustflow", {
477
+ method: "POST",
478
+ headers: { "x-mustflow-dashboard-token": dashboardToken }
479
+ });
480
+ if (!response.ok) throw new Error(await response.text());
481
+ statusKey("dashboard.ui.openedMustflow", "ok");
482
+ }
483
+
484
+ async function loadSnapshot() {
485
+ setLoading(true);
486
+ try {
487
+ const response = await fetch("/api/preferences", {
488
+ headers: { "x-mustflow-dashboard-token": dashboardToken }
489
+ });
490
+ if (!response.ok) throw new Error(await response.text());
491
+ snapshot = await response.json();
492
+ pending = new Map();
493
+ updateSaveState();
494
+ markDataUpdated();
495
+ statusKey("dashboard.ui.reloaded", "ok");
496
+ render();
497
+ } finally {
498
+ setLoading(false);
499
+ }
500
+ }
501
+
502
+ async function loadStatus() {
503
+ setLoading(true);
504
+ try {
505
+ const response = await fetch("/api/status", {
506
+ headers: { "x-mustflow-dashboard-token": dashboardToken }
507
+ });
508
+ if (!response.ok) throw new Error(await response.text());
509
+ dashboardStatus = await response.json();
510
+ markDataUpdated();
511
+ statusKey(
512
+ currentTab === "commands"
513
+ ? "dashboard.commands.reloaded"
514
+ : currentTab === "verification"
515
+ ? "dashboard.verification.reloaded"
516
+ : currentTab === "release"
517
+ ? "dashboard.release.reloaded"
518
+ : currentTab === "update"
519
+ ? "dashboard.update.reloaded"
520
+ : currentTab === "runs"
521
+ ? "dashboard.runs.reloaded"
522
+ : currentTab === "skills"
523
+ ? "dashboard.skills.reloaded"
524
+ : "dashboard.status.reloaded",
525
+ "ok"
526
+ );
527
+ renderStatusPanel();
528
+ renderVerificationPanel();
529
+ renderCommandPanel();
530
+ renderReleasePanel();
531
+ renderUpdatePanel();
532
+ renderRunsPanel();
533
+ renderSkillsPanel();
534
+ } finally {
535
+ setLoading(false);
536
+ }
537
+ }
538
+
539
+ function docStatusQuery() {
540
+ const value = document.getElementById("doc-status-filter").value;
541
+ if (value === "active") return "";
542
+ if (value === "all") return "?all=1";
543
+ return "?status=" + encodeURIComponent(value);
544
+ }
545
+
546
+ function formatBoolean(value) {
547
+ return message(value ? "dashboard.status.yes" : "dashboard.status.no");
548
+ }
549
+
550
+ function formatLatestRun(latestRun) {
551
+ if (!latestRun.exists) return message("dashboard.status.latestRunMissing");
552
+ if (!latestRun.valid) return message("dashboard.status.latestRunInvalid") + ": " + latestRun.error;
553
+ const parts = [latestRun.intent, latestRun.status];
554
+ if (latestRun.exit_code !== null) parts.push("exit " + latestRun.exit_code);
555
+ if (latestRun.finished_at) parts.push(latestRun.finished_at);
556
+ return parts.join(" / ");
557
+ }
558
+
559
+ function statusStateLabel(tone) {
560
+ if (tone === "ok") return message("dashboard.a11y.state.ok");
561
+ if (tone === "warn") return message("dashboard.a11y.state.warn");
562
+ return message("dashboard.a11y.state.neutral");
563
+ }
564
+
565
+ function appendStatusItem(root, labelKey, value, tone = "") {
566
+ const item = document.createElement("div");
567
+ item.className = "status-item";
568
+ const label = document.createElement("div");
569
+ label.className = "status-label";
570
+ label.textContent = message(labelKey);
571
+ const content = document.createElement("div");
572
+ content.className = tone ? "status-value " + tone : "status-value";
573
+ if (tone) {
574
+ const badge = document.createElement("span");
575
+ badge.className = "status-badge " + tone;
576
+ badge.textContent = statusStateLabel(tone);
577
+ content.appendChild(badge);
578
+ content.appendChild(document.createTextNode(value));
579
+ } else {
580
+ content.textContent = value;
581
+ }
582
+ content.setAttribute(
583
+ "aria-label",
584
+ message(labelKey) + ": " + value + " (" + statusStateLabel(tone) + ")"
585
+ );
586
+ item.appendChild(label);
587
+ item.appendChild(content);
588
+ root.appendChild(item);
589
+ }
590
+
591
+ function latestRunNeedsAttention(latestRun) {
592
+ if (!latestRun.exists) return false;
593
+ if (!latestRun.valid) return true;
594
+ return latestRun.status !== "passed" || latestRun.timed_out;
595
+ }
596
+
597
+ function deriveDashboardActions() {
598
+ const actions = [];
599
+ if (dashboardStatus.missing_files.length > 0) {
600
+ actions.push({
601
+ title: messageWithCount("dashboard.actions.missingFiles", dashboardStatus.missing_files.length),
602
+ meta: dashboardStatus.missing_files.slice(0, 2).join(", "),
603
+ tab: "status",
604
+ buttonKey: "dashboard.actions.openStatus"
605
+ });
606
+ }
607
+ if (dashboardStatus.issues.length > 0) {
608
+ actions.push({
609
+ title: messageWithCount("dashboard.actions.manifestIssues", dashboardStatus.issues.length),
610
+ meta: dashboardStatus.issues[0],
611
+ tab: "status",
612
+ buttonKey: "dashboard.actions.openStatus"
613
+ });
614
+ }
615
+ if (latestRunNeedsAttention(dashboardStatus.latest_run)) {
616
+ actions.push({
617
+ title: message("dashboard.actions.latestRun"),
618
+ meta: formatLatestRun(dashboardStatus.latest_run),
619
+ tab: "runs",
620
+ buttonKey: "dashboard.actions.openRuns"
621
+ });
622
+ }
623
+ if (dashboardStatus.update.blockers.length > 0) {
624
+ actions.push({
625
+ title: messageWithCount("dashboard.actions.updateBlockers", dashboardStatus.update.blockers.length),
626
+ meta: dashboardStatus.update.blockers[0].relativePath,
627
+ tab: "update",
628
+ buttonKey: "dashboard.actions.openUpdate"
629
+ });
630
+ }
631
+ if (dashboardStatus.verification.recommendations.length > 0) {
632
+ actions.push({
633
+ title: messageWithCount("dashboard.actions.verification", dashboardStatus.verification.recommendations.length),
634
+ meta: dashboardStatus.verification.recommendations.map((recommendation) => recommendation.intent).slice(0, 3).join(", "),
635
+ tab: "verification",
636
+ buttonKey: "dashboard.actions.openVerification"
637
+ });
638
+ }
639
+ if (dashboardStatus.active_review_documents > 0 || docReview.count > 0) {
640
+ const count = Math.max(dashboardStatus.active_review_documents, docReview.count);
641
+ actions.push({
642
+ title: messageWithCount("dashboard.actions.documents", count),
643
+ meta: docReview.items?.[0]?.path || "",
644
+ tab: "documents",
645
+ buttonKey: "dashboard.actions.openDocuments"
646
+ });
647
+ }
648
+ return actions.slice(0, 5);
649
+ }
650
+
651
+ function renderNextActions(root) {
652
+ const section = document.createElement("section");
653
+ const heading = document.createElement("h2");
654
+ heading.textContent = message("dashboard.actions.heading");
655
+ section.appendChild(heading);
656
+ const actions = deriveDashboardActions();
657
+ if (actions.length === 0) {
658
+ const empty = document.createElement("div");
659
+ empty.className = "empty";
660
+ empty.textContent = message("dashboard.actions.empty");
661
+ section.appendChild(empty);
662
+ root.appendChild(section);
663
+ return;
664
+ }
665
+ const list = document.createElement("div");
666
+ list.className = "next-actions";
667
+ for (const action of actions) {
668
+ const row = document.createElement("div");
669
+ row.className = "next-action-row";
670
+ const body = document.createElement("div");
671
+ const title = document.createElement("div");
672
+ title.className = "next-action-title";
673
+ title.textContent = action.title;
674
+ body.appendChild(title);
675
+ if (action.meta) {
676
+ const meta = document.createElement("div");
677
+ meta.className = "next-action-meta";
678
+ meta.textContent = action.meta;
679
+ body.appendChild(meta);
680
+ }
681
+ const button = document.createElement("button");
682
+ button.type = "button";
683
+ button.textContent = message(action.buttonKey);
684
+ button.addEventListener("click", () => activateTab(action.tab, { focus: true }));
685
+ row.appendChild(body);
686
+ row.appendChild(button);
687
+ list.appendChild(row);
688
+ }
689
+ section.appendChild(list);
690
+ root.appendChild(section);
691
+ }
692
+
693
+ function renderStatusPanel() {
694
+ const root = document.getElementById("dashboard-status");
695
+ root.textContent = "";
696
+ renderNextActions(root);
697
+ const section = document.createElement("section");
698
+ const heading = document.createElement("h2");
699
+ heading.textContent = message("dashboard.status.overview");
700
+ const grid = document.createElement("div");
701
+ grid.className = "status-grid";
702
+ const hasIssues = dashboardStatus.issues.length > 0 || dashboardStatus.changed_files.length > 0 || dashboardStatus.missing_files.length > 0;
703
+ appendStatusItem(grid, "dashboard.status.installed", formatBoolean(dashboardStatus.installed), dashboardStatus.installed ? "ok" : "warn");
704
+ appendStatusItem(grid, "dashboard.status.manifestLock", dashboardStatus.manifest_lock, dashboardStatus.manifest_lock === "present" ? "ok" : "warn");
705
+ appendStatusItem(grid, "dashboard.status.template", dashboardStatus.template ? dashboardStatus.template.id + " " + dashboardStatus.template.version : message("value.none"));
706
+ appendStatusItem(grid, "dashboard.status.trackedFiles", String(dashboardStatus.tracked_files));
707
+ appendStatusItem(grid, "dashboard.status.changedFiles", String(dashboardStatus.changed_files.length), dashboardStatus.changed_files.length === 0 ? "ok" : "warn");
708
+ appendStatusItem(grid, "dashboard.status.missingFiles", String(dashboardStatus.missing_files.length), dashboardStatus.missing_files.length === 0 ? "ok" : "warn");
709
+ appendStatusItem(grid, "dashboard.status.runnableIntents", String(dashboardStatus.runnable_intents.length));
710
+ appendStatusItem(grid, "dashboard.status.activeReviewDocuments", String(dashboardStatus.active_review_documents));
711
+ appendStatusItem(grid, "dashboard.status.latestRun", formatLatestRun(dashboardStatus.latest_run), dashboardStatus.latest_run.exists && dashboardStatus.latest_run.valid ? "ok" : "");
712
+ section.appendChild(heading);
713
+ section.appendChild(grid);
714
+ root.appendChild(section);
715
+
716
+ const issuesSection = document.createElement("section");
717
+ const issuesHeading = document.createElement("h2");
718
+ issuesHeading.textContent = message("dashboard.status.issues");
719
+ issuesSection.appendChild(issuesHeading);
720
+ if (!hasIssues) {
721
+ const empty = document.createElement("div");
722
+ empty.className = "empty";
723
+ empty.textContent = message("dashboard.status.noIssues");
724
+ issuesSection.appendChild(empty);
725
+ } else {
726
+ const list = document.createElement("ul");
727
+ list.className = "issue-list";
728
+ for (const issue of dashboardStatus.issues) {
729
+ const item = document.createElement("li");
730
+ item.textContent = issue;
731
+ list.appendChild(item);
732
+ }
733
+ for (const changed of dashboardStatus.changed_files) {
734
+ const item = document.createElement("li");
735
+ item.textContent = message("dashboard.status.changedFile") + ": " + changed;
736
+ list.appendChild(item);
737
+ }
738
+ for (const missing of dashboardStatus.missing_files) {
739
+ const item = document.createElement("li");
740
+ item.textContent = message("dashboard.status.missingFile") + ": " + missing;
741
+ list.appendChild(item);
742
+ }
743
+ issuesSection.appendChild(list);
744
+ }
745
+ root.appendChild(issuesSection);
746
+ }
747
+
748
+ async function copyVerificationCommand(command) {
749
+ await navigator.clipboard.writeText(command);
750
+ statusKey("dashboard.verification.copied", "ok");
751
+ }
752
+
753
+ async function copyVerificationPlan(commands) {
754
+ await navigator.clipboard.writeText(commands.join("\\n"));
755
+ statusKey("dashboard.verification.planCopied", "ok");
756
+ }
757
+
758
+ async function copyReleaseCommand(command) {
759
+ await navigator.clipboard.writeText(command);
760
+ statusKey("dashboard.release.copied", "ok");
761
+ }
762
+
763
+ async function copyUpdateCommand(command) {
764
+ await navigator.clipboard.writeText(command);
765
+ statusKey("dashboard.update.copied", "ok");
766
+ }
767
+
768
+ function setButtonAccessibleLabel(button, label) {
769
+ button.textContent = label;
770
+ button.title = label;
771
+ button.setAttribute("aria-label", label);
772
+ }
773
+
774
+ function copyCommandLabel(command) {
775
+ return messageFormat("dashboard.a11y.copyCommand", { command });
776
+ }
777
+
778
+ function showCopyButtonFeedback(button, restoreLabel) {
779
+ if (button.copyFeedbackTimeout) window.clearTimeout(button.copyFeedbackTimeout);
780
+ const originalDisabled = button.disabled;
781
+ setButtonAccessibleLabel(button, message("dashboard.ui.copied"));
782
+ button.disabled = true;
783
+ button.copyFeedbackTimeout = window.setTimeout(() => {
784
+ setButtonAccessibleLabel(button, restoreLabel);
785
+ button.disabled = originalDisabled;
786
+ button.copyFeedbackTimeout = null;
787
+ }, copyFeedbackMs);
788
+ }
789
+
790
+ function appendVerificationFiles(root, files) {
791
+ if (files.length === 0) return;
792
+ const details = document.createElement("div");
793
+ details.className = "verification-files";
794
+ details.textContent = message("dashboard.verification.files") + ": " + files.join(", ");
795
+ root.appendChild(details);
796
+ }
797
+
798
+ function normalizeFilterText(value) {
799
+ return String(value || "").toLowerCase();
800
+ }
801
+
802
+ function filterTextMatches(query, values) {
803
+ const normalizedQuery = normalizeFilterText(query).trim();
804
+ if (!normalizedQuery) return true;
805
+ return values.some((value) => normalizeFilterText(value).includes(normalizedQuery));
806
+ }
807
+
808
+ function renderListFilters(kind, stateOptions, rerender) {
809
+ const filter = listFilters[kind];
810
+ const wrapper = document.createElement("div");
811
+ wrapper.className = "list-filters";
812
+
813
+ const searchLabel = document.createElement("label");
814
+ const searchText = document.createElement("span");
815
+ searchText.textContent = message("dashboard.filter.search");
816
+ const search = document.createElement("input");
817
+ const searchId = "dashboard-" + kind + "-filter-search";
818
+ search.id = searchId;
819
+ search.type = "text";
820
+ search.autocomplete = "off";
821
+ search.spellcheck = false;
822
+ search.value = filter.query;
823
+ search.placeholder = message("dashboard.filter.searchPlaceholder");
824
+ search.addEventListener("input", () => {
825
+ const cursor = search.selectionStart;
826
+ filter.query = search.value;
827
+ rerender();
828
+ const nextSearch = document.getElementById(searchId);
829
+ if (nextSearch) {
830
+ nextSearch.focus();
831
+ if (cursor !== null) nextSearch.setSelectionRange(cursor, cursor);
832
+ }
833
+ });
834
+ searchLabel.appendChild(searchText);
835
+ searchLabel.appendChild(search);
836
+ wrapper.appendChild(searchLabel);
837
+
838
+ const stateLabel = document.createElement("label");
839
+ const stateText = document.createElement("span");
840
+ stateText.textContent = message("dashboard.filter.state");
841
+ const state = document.createElement("select");
842
+ const stateId = "dashboard-" + kind + "-filter-state";
843
+ state.id = stateId;
844
+ for (const option of stateOptions) {
845
+ const child = document.createElement("option");
846
+ child.value = option;
847
+ child.textContent = message("dashboard.filter." + option);
848
+ child.selected = option === filter.state;
849
+ state.appendChild(child);
850
+ }
851
+ state.addEventListener("change", () => {
852
+ filter.state = state.value;
853
+ rerender();
854
+ const nextState = document.getElementById(stateId);
855
+ if (nextState) nextState.focus();
856
+ });
857
+ stateLabel.appendChild(stateText);
858
+ stateLabel.appendChild(state);
859
+ wrapper.appendChild(stateLabel);
860
+ return wrapper;
861
+ }
862
+
863
+ function verificationStateMatches(recommendation) {
864
+ const state = listFilters.verification.state;
865
+ return state === "all" || (state === "runnable" && recommendation.runnable) || (state === "unavailable" && !recommendation.runnable);
866
+ }
867
+
868
+ function commandStateMatches(intent) {
869
+ const state = listFilters.commands.state;
870
+ return state === "all" || (state === "runnable" && intent.runnable) || (state === "unavailable" && !intent.runnable);
871
+ }
872
+
873
+ function skillRouteState(route) {
874
+ if (!route.exists) return "missing";
875
+ return route.aligned ? "aligned" : "mismatch";
876
+ }
877
+
878
+ function skillStateMatches(route) {
879
+ const state = listFilters.skills.state;
880
+ return state === "all" || skillRouteState(route) === state;
881
+ }
882
+
883
+ function createCollapsibleDetails(titleKey) {
884
+ const details = document.createElement("details");
885
+ details.className = "collapsible-details";
886
+ const summary = document.createElement("summary");
887
+ summary.textContent = message(titleKey);
888
+ details.appendChild(summary);
889
+ return details;
890
+ }
891
+
892
+ function renderVerificationPanel() {
893
+ const root = document.getElementById("dashboard-verification");
894
+ root.textContent = "";
895
+ const verification = dashboardStatus.verification;
896
+
897
+ const section = document.createElement("section");
898
+ const heading = document.createElement("h2");
899
+ heading.textContent = message("dashboard.verification.recommendations");
900
+ section.appendChild(heading);
901
+
902
+ if (verification.changed_files.length === 0) {
903
+ const empty = document.createElement("div");
904
+ empty.className = "empty";
905
+ empty.textContent = message("dashboard.verification.empty");
906
+ section.appendChild(empty);
907
+ root.appendChild(section);
908
+ return;
909
+ }
910
+
911
+ if (verification.recommendations.length === 0) {
912
+ const empty = document.createElement("div");
913
+ empty.className = "empty";
914
+ empty.textContent = message("dashboard.verification.none");
915
+ section.appendChild(empty);
916
+ root.appendChild(section);
917
+ return;
918
+ }
919
+
920
+ section.appendChild(renderListFilters("verification", ["all", "runnable", "unavailable"], renderVerificationPanel));
921
+ const recommendations = verification.recommendations.filter((recommendation) =>
922
+ verificationStateMatches(recommendation) &&
923
+ filterTextMatches(listFilters.verification.query, [
924
+ recommendation.intent,
925
+ recommendation.command,
926
+ message(recommendation.reason_key),
927
+ recommendation.files.join(" "),
928
+ ]),
929
+ );
930
+
931
+ if (recommendations.length === 0) {
932
+ const empty = document.createElement("div");
933
+ empty.className = "empty";
934
+ empty.textContent = message("dashboard.filter.noMatches");
935
+ section.appendChild(empty);
936
+ root.appendChild(section);
937
+ return;
938
+ }
939
+
940
+ for (const recommendation of recommendations) {
941
+ const row = document.createElement("div");
942
+ row.className = "verification-row";
943
+ const summary = document.createElement("div");
944
+ const name = document.createElement("div");
945
+ name.className = "command-name";
946
+ name.textContent = recommendation.intent;
947
+ const state = document.createElement("div");
948
+ state.className = recommendation.runnable ? "command-state ok" : "command-state warn";
949
+ state.textContent = recommendation.runnable ? message("dashboard.commands.runnable") : message("dashboard.verification.unavailable");
950
+ summary.appendChild(name);
951
+ summary.appendChild(state);
952
+
953
+ const details = document.createElement("div");
954
+ const command = document.createElement("div");
955
+ command.className = "verification-command";
956
+ command.textContent = recommendation.command;
957
+ const reason = document.createElement("div");
958
+ reason.className = "command-note";
959
+ reason.textContent = message(recommendation.reason_key);
960
+ details.appendChild(command);
961
+ details.appendChild(reason);
962
+ appendVerificationFiles(details, recommendation.files);
963
+
964
+ const copy = document.createElement("button");
965
+ copy.type = "button";
966
+ copy.className = "verification-copy";
967
+ const copyLabel = copyCommandLabel(recommendation.command);
968
+ setButtonAccessibleLabel(copy, copyLabel);
969
+ copy.disabled = !recommendation.runnable;
970
+ copy.setAttribute("aria-disabled", copy.disabled ? "true" : "false");
971
+ copy.addEventListener("click", () => {
972
+ copyVerificationCommand(recommendation.command)
973
+ .then(() => showCopyButtonFeedback(copy, copyLabel))
974
+ .catch((error) => statusText(error.message, "error"));
975
+ });
976
+
977
+ row.appendChild(summary);
978
+ row.appendChild(details);
979
+ row.appendChild(copy);
980
+ section.appendChild(row);
981
+ }
982
+
983
+ root.appendChild(section);
984
+
985
+ if (verification.schedule.batches.length > 0) {
986
+ const scheduleSection = document.createElement("section");
987
+ const scheduleHeading = document.createElement("h2");
988
+ scheduleHeading.textContent = message("dashboard.verification.schedule");
989
+ scheduleSection.appendChild(scheduleHeading);
990
+ const entriesByIntent = new Map(verification.schedule.entries.map((entry) => [entry.intent, entry]));
991
+ const recommendedIntents = new Set(recommendations.map((recommendation) => recommendation.intent));
992
+ const scheduleBatches = verification.schedule.batches
993
+ .map((batch) => ({
994
+ ...batch,
995
+ intents: batch.intents.filter((intent) => recommendedIntents.has(intent)),
996
+ commands: batch.commands.filter((command) => recommendations.some((recommendation) => recommendation.command === command)),
997
+ }))
998
+ .filter((batch) => batch.intents.length > 0 || batch.commands.length > 0);
999
+ const planCommands = scheduleBatches.flatMap((batch) => batch.commands);
1000
+ for (const batch of scheduleBatches) {
1001
+ const row = document.createElement("div");
1002
+ row.className = "verification-row";
1003
+ const summary = document.createElement("div");
1004
+ const name = document.createElement("div");
1005
+ name.className = "command-name";
1006
+ name.textContent = message("dashboard.verification.batch") + " " + batch.index;
1007
+ const state = document.createElement("div");
1008
+ state.className = "command-state ok";
1009
+ state.textContent = batch.locks.length > 0 ? message("dashboard.verification.locks") + ": " + batch.locks.join(", ") : message("dashboard.verification.noLocks");
1010
+ summary.appendChild(name);
1011
+ summary.appendChild(state);
1012
+
1013
+ const details = document.createElement("div");
1014
+ const commands = document.createElement("div");
1015
+ commands.className = "verification-command";
1016
+ commands.textContent = batch.commands.join(" -> ");
1017
+ details.appendChild(commands);
1018
+ for (const intent of batch.intents) {
1019
+ const entry = entriesByIntent.get(intent);
1020
+ if (!entry) continue;
1021
+ const effects = document.createElement("div");
1022
+ effects.className = "verification-files";
1023
+ effects.textContent = message("dashboard.verification.effects") + ": " + entry.effects.map((effect) => effect.mode + " " + (effect.path || effect.lock) + " [" + effect.lock + "]").join(", ");
1024
+ details.appendChild(effects);
1025
+ if (entry.conflicts.length > 0) {
1026
+ const conflicts = document.createElement("div");
1027
+ conflicts.className = "command-note";
1028
+ conflicts.textContent = message("dashboard.verification.conflicts") + ": " + entry.conflicts.map((conflict) => conflict.intent + " (" + conflict.lock + ")").join(", ");
1029
+ details.appendChild(conflicts);
1030
+ }
1031
+ }
1032
+
1033
+ const copy = document.createElement("button");
1034
+ copy.type = "button";
1035
+ copy.className = "verification-copy";
1036
+ const copyLabel = message("dashboard.a11y.copyVerificationPlan");
1037
+ setButtonAccessibleLabel(copy, copyLabel);
1038
+ copy.disabled = planCommands.length === 0;
1039
+ copy.setAttribute("aria-disabled", copy.disabled ? "true" : "false");
1040
+ copy.addEventListener("click", () => {
1041
+ copyVerificationPlan(planCommands)
1042
+ .then(() => showCopyButtonFeedback(copy, copyLabel))
1043
+ .catch((error) => statusText(error.message, "error"));
1044
+ });
1045
+
1046
+ row.appendChild(summary);
1047
+ row.appendChild(details);
1048
+ row.appendChild(copy);
1049
+ scheduleSection.appendChild(row);
1050
+ }
1051
+ root.appendChild(scheduleSection);
1052
+ }
1053
+
1054
+ if (verification.skipped.length > 0) {
1055
+ const skippedSection = createCollapsibleDetails("dashboard.verification.skipped");
1056
+ for (const skipped of verification.skipped) {
1057
+ const row = document.createElement("div");
1058
+ row.className = "command-note";
1059
+ row.textContent = skipped.intent + ": " + message(skipped.reason_key);
1060
+ skippedSection.appendChild(row);
1061
+ }
1062
+ root.appendChild(skippedSection);
1063
+ }
1064
+ }
1065
+
1066
+ function appendCommandMeta(root, labelKey, value) {
1067
+ if (value === null || value === undefined || value === "") return;
1068
+ const item = document.createElement("span");
1069
+ item.textContent = message(labelKey) + ": " + value;
1070
+ root.appendChild(item);
1071
+ }
1072
+
1073
+ function commandStateKey(intent) {
1074
+ if (intent.runnable) return "dashboard.commands.runnable";
1075
+ if (intent.status === "manual_only") return "dashboard.commands.manualOnly";
1076
+ if (intent.status === "unknown") return "dashboard.commands.unavailable";
1077
+ return "dashboard.commands.blocked";
1078
+ }
1079
+
1080
+ function formatList(values) {
1081
+ return values.length === 0 ? message("value.none") : values.join(", ");
1082
+ }
1083
+
1084
+ function formatCommandWriteLock(writeLock) {
1085
+ const paths = writeLock.paths.length === 0 ? message("value.none") : writeLock.paths.join(", ");
1086
+ return writeLock.lock + ": " + paths;
1087
+ }
1088
+
1089
+ function formatCommandLockConflict(conflict) {
1090
+ const paths = conflict.conflicting_paths.length === 0 ? "" : " / " + conflict.conflicting_paths.join(", ");
1091
+ return conflict.intent + " (" + conflict.lock + ")" + paths;
1092
+ }
1093
+
1094
+ function appendCommandEffectGraph(root, intent) {
1095
+ const graph = intent.effect_graph;
1096
+ if (!graph || graph.status !== "fresh") return;
1097
+ if (graph.write_locks.length === 0 && graph.lock_conflicts.length === 0) return;
1098
+
1099
+ const details = createCollapsibleDetails("dashboard.commands.effectGraph");
1100
+
1101
+ if (graph.write_locks.length > 0) {
1102
+ const locks = document.createElement("div");
1103
+ locks.className = "verification-files";
1104
+ locks.textContent = message("dashboard.commands.effectGraph") + ": " + graph.write_locks.map(formatCommandWriteLock).join(", ");
1105
+ details.appendChild(locks);
1106
+ }
1107
+
1108
+ if (graph.lock_conflicts.length > 0) {
1109
+ const conflicts = document.createElement("div");
1110
+ conflicts.className = "command-note";
1111
+ conflicts.textContent = message("dashboard.verification.conflicts") + ": " + graph.lock_conflicts.map(formatCommandLockConflict).join(", ");
1112
+ details.appendChild(conflicts);
1113
+ }
1114
+
1115
+ root.appendChild(details);
1116
+ }
1117
+
1118
+ function renderCommandPanel() {
1119
+ const root = document.getElementById("dashboard-commands");
1120
+ root.textContent = "";
1121
+ const section = document.createElement("section");
1122
+ const heading = document.createElement("h2");
1123
+ heading.textContent = message("dashboard.commands.heading");
1124
+ section.appendChild(heading);
1125
+
1126
+ if (!dashboardStatus.command_contract.exists || dashboardStatus.command_contract.intents.length === 0) {
1127
+ const empty = document.createElement("div");
1128
+ empty.className = "empty";
1129
+ empty.textContent = message("dashboard.commands.empty");
1130
+ section.appendChild(empty);
1131
+ root.appendChild(section);
1132
+ return;
1133
+ }
1134
+
1135
+ const graphStatus = dashboardStatus.command_contract.effect_graph_status;
1136
+ if (graphStatus && graphStatus.status !== "fresh") {
1137
+ const note = document.createElement("div");
1138
+ note.className = "command-note";
1139
+ note.textContent =
1140
+ message("dashboard.commands.effectGraphUnavailable") +
1141
+ ": " +
1142
+ (graphStatus.refresh_hint || graphStatus.status);
1143
+ section.appendChild(note);
1144
+ }
1145
+
1146
+ section.appendChild(renderListFilters("commands", ["all", "runnable", "unavailable"], renderCommandPanel));
1147
+ const intents = dashboardStatus.command_contract.intents.filter((intent) =>
1148
+ commandStateMatches(intent) &&
1149
+ filterTextMatches(listFilters.commands.query, [
1150
+ intent.name,
1151
+ intent.description,
1152
+ intent.status,
1153
+ intent.lifecycle,
1154
+ intent.run_policy,
1155
+ intent.stdin,
1156
+ intent.cwd,
1157
+ intent.reason,
1158
+ intent.agent_action,
1159
+ intent.writes.join(" "),
1160
+ intent.required_after.join(" "),
1161
+ ]),
1162
+ );
1163
+
1164
+ if (intents.length === 0) {
1165
+ const empty = document.createElement("div");
1166
+ empty.className = "empty";
1167
+ empty.textContent = message("dashboard.filter.noMatches");
1168
+ section.appendChild(empty);
1169
+ root.appendChild(section);
1170
+ return;
1171
+ }
1172
+
1173
+ for (const intent of intents) {
1174
+ const row = document.createElement("div");
1175
+ row.className = "command-row";
1176
+ const summary = document.createElement("div");
1177
+ const name = document.createElement("div");
1178
+ name.className = "command-name";
1179
+ name.textContent = intent.name;
1180
+ const state = document.createElement("div");
1181
+ state.className = intent.runnable ? "command-state ok" : "command-state warn";
1182
+ state.textContent = message(commandStateKey(intent));
1183
+ summary.appendChild(name);
1184
+ summary.appendChild(state);
1185
+
1186
+ const details = document.createElement("div");
1187
+ const description = document.createElement("div");
1188
+ description.className = "command-description";
1189
+ description.textContent = intent.description || message("value.none");
1190
+ const meta = document.createElement("div");
1191
+ meta.className = "command-meta";
1192
+ appendCommandMeta(meta, "dashboard.commands.status", intent.status);
1193
+ appendCommandMeta(meta, "dashboard.commands.lifecycle", intent.lifecycle);
1194
+ appendCommandMeta(meta, "dashboard.commands.runPolicy", intent.run_policy);
1195
+ appendCommandMeta(meta, "dashboard.commands.stdin", intent.stdin);
1196
+ appendCommandMeta(meta, "dashboard.commands.timeout", intent.timeout_seconds);
1197
+ appendCommandMeta(meta, "dashboard.commands.cwd", intent.cwd);
1198
+ appendCommandMeta(meta, "dashboard.commands.writes", formatList(intent.writes));
1199
+ details.appendChild(description);
1200
+ details.appendChild(meta);
1201
+ if (intent.reason) {
1202
+ const reason = document.createElement("div");
1203
+ reason.className = "command-note";
1204
+ reason.textContent = message("dashboard.commands.reason") + ": " + intent.reason;
1205
+ details.appendChild(reason);
1206
+ }
1207
+ if (intent.agent_action) {
1208
+ const action = document.createElement("div");
1209
+ action.className = "command-note";
1210
+ action.textContent = message("dashboard.commands.agentAction") + ": " + intent.agent_action;
1211
+ details.appendChild(action);
1212
+ }
1213
+ appendCommandEffectGraph(details, intent);
1214
+
1215
+ row.appendChild(summary);
1216
+ row.appendChild(details);
1217
+ section.appendChild(row);
1218
+ }
1219
+
1220
+ root.appendChild(section);
1221
+ }
1222
+
1223
+ function findIntent(name) {
1224
+ return dashboardStatus.command_contract.intents.find((intent) => intent.name === name);
1225
+ }
1226
+
1227
+ function renderReleaseCommand(root, intentName, fallbackCommand, reasonKey) {
1228
+ const intent = findIntent(intentName);
1229
+ const row = document.createElement("div");
1230
+ row.className = "verification-row";
1231
+ const summary = document.createElement("div");
1232
+ const name = document.createElement("div");
1233
+ name.className = "command-name";
1234
+ name.textContent = intentName;
1235
+ const state = document.createElement("div");
1236
+ const runnable = intentName === "version_check" ? true : intent ? intent.runnable : false;
1237
+ state.className = runnable ? "command-state ok" : "command-state warn";
1238
+ state.textContent = runnable ? message("dashboard.commands.runnable") : message("dashboard.verification.unavailable");
1239
+ summary.appendChild(name);
1240
+ summary.appendChild(state);
1241
+
1242
+ const details = document.createElement("div");
1243
+ const command = document.createElement("div");
1244
+ command.className = "verification-command";
1245
+ command.textContent = fallbackCommand;
1246
+ const reason = document.createElement("div");
1247
+ reason.className = "command-note";
1248
+ reason.textContent = message(reasonKey);
1249
+ details.appendChild(command);
1250
+ details.appendChild(reason);
1251
+
1252
+ const copy = document.createElement("button");
1253
+ copy.type = "button";
1254
+ copy.className = "verification-copy";
1255
+ const copyLabel = copyCommandLabel(fallbackCommand);
1256
+ setButtonAccessibleLabel(copy, copyLabel);
1257
+ copy.disabled = !runnable;
1258
+ copy.setAttribute("aria-disabled", copy.disabled ? "true" : "false");
1259
+ copy.addEventListener("click", () => {
1260
+ copyReleaseCommand(fallbackCommand)
1261
+ .then(() => showCopyButtonFeedback(copy, copyLabel))
1262
+ .catch((error) => statusText(error.message, "error"));
1263
+ });
1264
+
1265
+ row.appendChild(summary);
1266
+ row.appendChild(details);
1267
+ row.appendChild(copy);
1268
+ root.appendChild(row);
1269
+ }
1270
+
1271
+ function renderReleasePanel() {
1272
+ const root = document.getElementById("dashboard-release");
1273
+ root.textContent = "";
1274
+ const overview = document.createElement("section");
1275
+ const overviewHeading = document.createElement("h2");
1276
+ overviewHeading.textContent = message("dashboard.release.overview");
1277
+ const grid = document.createElement("div");
1278
+ grid.className = "status-grid";
1279
+ appendStatusItem(grid, "dashboard.release.packageVersion", dashboardStatus.release.package_name + " " + dashboardStatus.release.package_version);
1280
+ appendStatusItem(grid, "dashboard.release.templateVersion", dashboardStatus.template ? dashboardStatus.template.id + " " + dashboardStatus.template.version : message("value.none"));
1281
+ appendStatusItem(grid, "dashboard.release.autoBump", formatBoolean(Boolean(settingValue("release.versioning.auto_bump"))), settingValue("release.versioning.auto_bump") ? "ok" : "");
1282
+ appendStatusItem(grid, "dashboard.release.requireConfirmation", formatBoolean(Boolean(settingValue("release.versioning.require_user_confirmation"))));
1283
+ appendStatusItem(grid, "dashboard.release.changedFiles", String(dashboardStatus.release.release_sensitive_changed_files.length), dashboardStatus.release.release_sensitive_changed_files.length === 0 ? "ok" : "warn");
1284
+ overview.appendChild(overviewHeading);
1285
+ overview.appendChild(grid);
1286
+ root.appendChild(overview);
1287
+
1288
+ const sources = document.createElement("section");
1289
+ const sourcesHeading = document.createElement("h2");
1290
+ sourcesHeading.textContent = message("dashboard.release.versionSources");
1291
+ sources.appendChild(sourcesHeading);
1292
+ if (dashboardStatus.release.version_sources.length === 0) {
1293
+ const empty = document.createElement("div");
1294
+ empty.className = "empty";
1295
+ empty.textContent = message("dashboard.release.noVersionSources");
1296
+ sources.appendChild(empty);
1297
+ } else {
1298
+ for (const source of dashboardStatus.release.version_sources) {
1299
+ const row = document.createElement("div");
1300
+ row.className = "command-row";
1301
+ const summary = document.createElement("div");
1302
+ const name = document.createElement("div");
1303
+ name.className = "command-name";
1304
+ name.textContent = source.path;
1305
+ const state = document.createElement("div");
1306
+ state.className = "command-state";
1307
+ state.textContent = source.kind;
1308
+ summary.appendChild(name);
1309
+ summary.appendChild(state);
1310
+ const details = document.createElement("div");
1311
+ const meta = document.createElement("div");
1312
+ meta.className = "command-meta";
1313
+ appendCommandMeta(meta, "dashboard.release.declared", source.declared ? message("dashboard.status.yes") : message("dashboard.status.no"));
1314
+ appendCommandMeta(meta, "dashboard.release.authority", source.authority || message("value.none"));
1315
+ details.appendChild(meta);
1316
+ row.appendChild(summary);
1317
+ row.appendChild(details);
1318
+ sources.appendChild(row);
1319
+ }
1320
+ }
1321
+ root.appendChild(sources);
1322
+
1323
+ const changed = document.createElement("section");
1324
+ const changedHeading = document.createElement("h2");
1325
+ changedHeading.textContent = message("dashboard.release.changedFiles");
1326
+ changed.appendChild(changedHeading);
1327
+ if (dashboardStatus.release.release_sensitive_changed_files.length === 0) {
1328
+ const empty = document.createElement("div");
1329
+ empty.className = "empty";
1330
+ empty.textContent = message("dashboard.release.noChangedFiles");
1331
+ changed.appendChild(empty);
1332
+ } else {
1333
+ const list = document.createElement("ul");
1334
+ list.className = "issue-list";
1335
+ for (const file of dashboardStatus.release.release_sensitive_changed_files) {
1336
+ const item = document.createElement("li");
1337
+ item.textContent = file;
1338
+ list.appendChild(item);
1339
+ }
1340
+ changed.appendChild(list);
1341
+ }
1342
+ root.appendChild(changed);
1343
+
1344
+ const commands = document.createElement("section");
1345
+ const commandsHeading = document.createElement("h2");
1346
+ commandsHeading.textContent = message("dashboard.release.commands");
1347
+ commands.appendChild(commandsHeading);
1348
+ renderReleaseCommand(commands, "version_check", "mf version --check", "dashboard.release.reason.versionCheck");
1349
+ renderReleaseCommand(commands, "test_release", "mf run test_release", "dashboard.release.reason.testRelease");
1350
+ renderReleaseCommand(commands, "docs_validate", "mf run docs_validate", "dashboard.release.reason.docsValidate");
1351
+ root.appendChild(commands);
1352
+ }
1353
+
1354
+ function renderUpdateCommand(root, command, labelKey, reasonKey, enabled = true) {
1355
+ const row = document.createElement("div");
1356
+ row.className = "verification-row";
1357
+ const summary = document.createElement("div");
1358
+ const name = document.createElement("div");
1359
+ name.className = "command-name";
1360
+ name.textContent = message(labelKey);
1361
+ const state = document.createElement("div");
1362
+ state.className = enabled ? "command-state ok" : "command-state warn";
1363
+ state.textContent = enabled ? message("dashboard.commands.runnable") : message("dashboard.update.blocked");
1364
+ summary.appendChild(name);
1365
+ summary.appendChild(state);
1366
+
1367
+ const details = document.createElement("div");
1368
+ const commandText = document.createElement("div");
1369
+ commandText.className = "verification-command";
1370
+ commandText.textContent = command;
1371
+ const reason = document.createElement("div");
1372
+ reason.className = "command-note";
1373
+ reason.textContent = message(reasonKey);
1374
+ details.appendChild(commandText);
1375
+ details.appendChild(reason);
1376
+
1377
+ const copy = document.createElement("button");
1378
+ copy.type = "button";
1379
+ copy.className = "verification-copy";
1380
+ const copyLabel = copyCommandLabel(command);
1381
+ setButtonAccessibleLabel(copy, copyLabel);
1382
+ copy.disabled = !enabled;
1383
+ copy.setAttribute("aria-disabled", copy.disabled ? "true" : "false");
1384
+ copy.addEventListener("click", () => {
1385
+ copyUpdateCommand(command)
1386
+ .then(() => showCopyButtonFeedback(copy, copyLabel))
1387
+ .catch((error) => statusText(error.message, "error"));
1388
+ });
1389
+
1390
+ row.appendChild(summary);
1391
+ row.appendChild(details);
1392
+ row.appendChild(copy);
1393
+ root.appendChild(row);
1394
+ }
1395
+
1396
+ function renderUpdateItem(root, item) {
1397
+ const row = document.createElement("div");
1398
+ row.className = "command-row";
1399
+ const summary = document.createElement("div");
1400
+ const name = document.createElement("div");
1401
+ name.className = "command-name";
1402
+ name.textContent = item.relativePath;
1403
+ const state = document.createElement("div");
1404
+ state.className = item.action === "create" || item.action === "update" ? "command-state ok" : "command-state warn";
1405
+ state.textContent = message("dashboard.update.action." + item.action);
1406
+ summary.appendChild(name);
1407
+ summary.appendChild(state);
1408
+
1409
+ const details = document.createElement("div");
1410
+ const meta = document.createElement("div");
1411
+ meta.className = "command-meta";
1412
+ appendCommandMeta(meta, "dashboard.update.source", item.sourceKind);
1413
+ const reason = document.createElement("div");
1414
+ reason.className = "command-note";
1415
+ reason.textContent = message("dashboard.update.reason") + ": " + item.reason;
1416
+ details.appendChild(meta);
1417
+ details.appendChild(reason);
1418
+
1419
+ row.appendChild(summary);
1420
+ row.appendChild(details);
1421
+ root.appendChild(row);
1422
+ }
1423
+
1424
+ function renderUpdateItemList(root, titleKey, emptyKey, items) {
1425
+ const section = document.createElement("section");
1426
+ const heading = document.createElement("h2");
1427
+ heading.textContent = message(titleKey);
1428
+ section.appendChild(heading);
1429
+ if (items.length === 0) {
1430
+ const empty = document.createElement("div");
1431
+ empty.className = "empty";
1432
+ empty.textContent = message(emptyKey);
1433
+ section.appendChild(empty);
1434
+ } else {
1435
+ for (const item of items) renderUpdateItem(section, item);
1436
+ }
1437
+ root.appendChild(section);
1438
+ }
1439
+
1440
+ function renderUpdatePanel() {
1441
+ const root = document.getElementById("dashboard-update");
1442
+ root.textContent = "";
1443
+ const update = dashboardStatus.update;
1444
+
1445
+ const overview = document.createElement("section");
1446
+ const heading = document.createElement("h2");
1447
+ heading.textContent = message("dashboard.update.overview");
1448
+ const grid = document.createElement("div");
1449
+ grid.className = "status-grid";
1450
+ appendStatusItem(grid, "dashboard.update.dryRun", update.ok ? message("dashboard.status.yes") : message("dashboard.status.no"), update.ok ? "ok" : "warn");
1451
+ appendStatusItem(grid, "dashboard.update.applyReady", update.apply_ready ? message("dashboard.status.yes") : message("dashboard.status.no"), update.apply_ready ? "ok" : "warn");
1452
+ appendStatusItem(grid, "dashboard.update.wouldUpdate", String(update.summary.wouldUpdate));
1453
+ appendStatusItem(grid, "dashboard.update.wouldCreate", String(update.summary.wouldCreate));
1454
+ appendStatusItem(grid, "dashboard.update.blockedLocalChanges", String(update.summary.blockedLocalChanges), update.summary.blockedLocalChanges === 0 ? "ok" : "warn");
1455
+ appendStatusItem(grid, "dashboard.update.manualReview", String(update.summary.manualReview), update.summary.manualReview === 0 ? "ok" : "warn");
1456
+ appendStatusItem(grid, "dashboard.update.unchanged", String(update.summary.unchanged));
1457
+ overview.appendChild(heading);
1458
+ overview.appendChild(grid);
1459
+ if (update.error) {
1460
+ const error = document.createElement("div");
1461
+ error.className = "command-note";
1462
+ error.textContent = message("dashboard.update.error") + ": " + update.error;
1463
+ overview.appendChild(error);
1464
+ }
1465
+ root.appendChild(overview);
1466
+
1467
+ const commands = document.createElement("section");
1468
+ const commandsHeading = document.createElement("h2");
1469
+ commandsHeading.textContent = message("dashboard.update.commands");
1470
+ commands.appendChild(commandsHeading);
1471
+ renderUpdateCommand(commands, update.dry_run_command, "dashboard.update.command.dryRun", "dashboard.update.reason.dryRun", update.ok);
1472
+ renderUpdateCommand(commands, update.apply_command, "dashboard.update.command.apply", "dashboard.update.reason.apply", update.ok && update.apply_ready);
1473
+ root.appendChild(commands);
1474
+
1475
+ renderUpdateItemList(root, "dashboard.update.blockers", "dashboard.update.noBlockers", update.blockers);
1476
+ renderUpdateItemList(root, "dashboard.update.changes", "dashboard.update.noChanges", update.changes);
1477
+ }
1478
+
1479
+ function formatDuration(value) {
1480
+ if (typeof value !== "number") return message("value.none");
1481
+ if (value < 1000) return String(value) + " ms";
1482
+ return (value / 1000).toFixed(2) + " s";
1483
+ }
1484
+
1485
+ function renderRunOutput(root, titleKey, output) {
1486
+ const section = createCollapsibleDetails(titleKey);
1487
+ const meta = document.createElement("div");
1488
+ meta.className = "command-meta";
1489
+ appendCommandMeta(meta, "dashboard.runs.bytes", output.bytes);
1490
+ appendCommandMeta(meta, "dashboard.runs.truncated", formatBoolean(output.truncated));
1491
+ section.appendChild(meta);
1492
+ if (output.tail) {
1493
+ const pre = document.createElement("pre");
1494
+ pre.className = "doc-comment";
1495
+ pre.textContent = output.tail;
1496
+ section.appendChild(pre);
1497
+ } else {
1498
+ const empty = document.createElement("div");
1499
+ empty.className = "empty";
1500
+ empty.textContent = message("dashboard.runs.emptyOutput");
1501
+ section.appendChild(empty);
1502
+ }
1503
+ root.appendChild(section);
1504
+ }
1505
+
1506
+ function renderRunsPanel() {
1507
+ const root = document.getElementById("dashboard-runs");
1508
+ root.textContent = "";
1509
+ const run = dashboardStatus.run_history;
1510
+
1511
+ const overview = document.createElement("section");
1512
+ const heading = document.createElement("h2");
1513
+ heading.textContent = message("dashboard.runs.heading");
1514
+ overview.appendChild(heading);
1515
+
1516
+ if (!run.exists) {
1517
+ const empty = document.createElement("div");
1518
+ empty.className = "empty";
1519
+ empty.textContent = message("dashboard.runs.empty");
1520
+ overview.appendChild(empty);
1521
+ root.appendChild(overview);
1522
+ return;
1523
+ }
1524
+
1525
+ if (!run.valid) {
1526
+ const error = document.createElement("div");
1527
+ error.className = "command-note";
1528
+ error.textContent = message("dashboard.runs.invalid") + ": " + run.error;
1529
+ overview.appendChild(error);
1530
+ root.appendChild(overview);
1531
+ return;
1532
+ }
1533
+
1534
+ const grid = document.createElement("div");
1535
+ grid.className = "status-grid";
1536
+ appendStatusItem(grid, "dashboard.runs.intent", run.intent);
1537
+ appendStatusItem(grid, "dashboard.runs.status", run.status, run.status === "passed" ? "ok" : "warn");
1538
+ appendStatusItem(grid, "dashboard.runs.exitCode", run.exit_code === null ? message("value.none") : String(run.exit_code));
1539
+ appendStatusItem(grid, "dashboard.runs.timedOut", formatBoolean(run.timed_out), run.timed_out ? "warn" : "ok");
1540
+ appendStatusItem(grid, "dashboard.runs.startedAt", run.started_at || message("value.none"));
1541
+ appendStatusItem(grid, "dashboard.runs.finishedAt", run.finished_at || message("value.none"));
1542
+ appendStatusItem(grid, "dashboard.runs.duration", formatDuration(run.duration_ms));
1543
+ appendStatusItem(grid, "dashboard.runs.cwd", run.cwd || message("value.none"));
1544
+ appendStatusItem(grid, "dashboard.runs.mode", run.mode || message("value.none"));
1545
+ appendStatusItem(grid, "dashboard.runs.timeout", String(run.timeout_seconds));
1546
+ appendStatusItem(grid, "dashboard.runs.receiptPath", run.receipt_path || run.path);
1547
+ overview.appendChild(grid);
1548
+
1549
+ const meta = document.createElement("div");
1550
+ meta.className = "command-meta";
1551
+ appendCommandMeta(meta, "dashboard.runs.lifecycle", run.lifecycle);
1552
+ appendCommandMeta(meta, "dashboard.runs.runPolicy", run.run_policy);
1553
+ appendCommandMeta(meta, "dashboard.runs.successExitCodes", formatList(run.success_exit_codes.map(String)));
1554
+ appendCommandMeta(meta, "dashboard.runs.signal", run.signal || message("value.none"));
1555
+ appendCommandMeta(meta, "dashboard.runs.killMethod", run.kill_method || message("value.none"));
1556
+ overview.appendChild(meta);
1557
+
1558
+ if (run.command_line.length > 0) {
1559
+ const command = document.createElement("div");
1560
+ command.className = "verification-command";
1561
+ command.textContent = run.command_line.join(" ");
1562
+ overview.appendChild(command);
1563
+ }
1564
+
1565
+ if (run.error) {
1566
+ const error = document.createElement("div");
1567
+ error.className = "command-note";
1568
+ error.textContent = message("dashboard.runs.error") + ": " + run.error;
1569
+ overview.appendChild(error);
1570
+ }
1571
+
1572
+ root.appendChild(overview);
1573
+ renderRunOutput(root, "dashboard.runs.stdout", run.stdout);
1574
+ renderRunOutput(root, "dashboard.runs.stderr", run.stderr);
1575
+ }
1576
+
1577
+ function skillAlignmentKey(route) {
1578
+ if (!route.exists) return "dashboard.skills.missing";
1579
+ return route.aligned ? "dashboard.skills.aligned" : "dashboard.skills.mismatch";
1580
+ }
1581
+
1582
+ function renderSkillsPanel() {
1583
+ const root = document.getElementById("dashboard-skills");
1584
+ root.textContent = "";
1585
+ const overview = document.createElement("section");
1586
+ const heading = document.createElement("h2");
1587
+ heading.textContent = message("dashboard.skills.heading");
1588
+ const grid = document.createElement("div");
1589
+ grid.className = "status-grid";
1590
+ appendStatusItem(grid, "dashboard.skills.indexPath", dashboardStatus.skills.index_path);
1591
+ appendStatusItem(grid, "dashboard.skills.routes", String(dashboardStatus.skills.count));
1592
+ overview.appendChild(heading);
1593
+ overview.appendChild(grid);
1594
+ root.appendChild(overview);
1595
+
1596
+ const section = document.createElement("section");
1597
+ const routesHeading = document.createElement("h2");
1598
+ routesHeading.textContent = message("dashboard.skills.routes");
1599
+ section.appendChild(routesHeading);
1600
+
1601
+ if (!dashboardStatus.skills.exists || dashboardStatus.skills.routes.length === 0) {
1602
+ const empty = document.createElement("div");
1603
+ empty.className = "empty";
1604
+ empty.textContent = message("dashboard.skills.empty");
1605
+ section.appendChild(empty);
1606
+ root.appendChild(section);
1607
+ return;
1608
+ }
1609
+
1610
+ section.appendChild(renderListFilters("skills", ["all", "aligned", "mismatch", "missing"], renderSkillsPanel));
1611
+ const routes = dashboardStatus.skills.routes.filter((route) =>
1612
+ skillStateMatches(route) &&
1613
+ filterTextMatches(listFilters.skills.query, [
1614
+ route.skill,
1615
+ route.trigger,
1616
+ route.skill_path,
1617
+ route.required_input,
1618
+ route.edit_scope,
1619
+ route.risk,
1620
+ route.expected_output,
1621
+ route.verification_intents.join(" "),
1622
+ route.declared_command_intents.join(" "),
1623
+ ]),
1624
+ );
1625
+
1626
+ if (routes.length === 0) {
1627
+ const empty = document.createElement("div");
1628
+ empty.className = "empty";
1629
+ empty.textContent = message("dashboard.filter.noMatches");
1630
+ section.appendChild(empty);
1631
+ root.appendChild(section);
1632
+ return;
1633
+ }
1634
+
1635
+ for (const route of routes) {
1636
+ const row = document.createElement("div");
1637
+ row.className = "command-row";
1638
+ const summary = document.createElement("div");
1639
+ const name = document.createElement("div");
1640
+ name.className = "command-name";
1641
+ name.textContent = route.skill;
1642
+ const state = document.createElement("div");
1643
+ state.className = route.exists && route.aligned ? "command-state ok" : "command-state warn";
1644
+ state.textContent = message(skillAlignmentKey(route));
1645
+ summary.appendChild(name);
1646
+ summary.appendChild(state);
1647
+
1648
+ const details = document.createElement("div");
1649
+ const trigger = document.createElement("div");
1650
+ trigger.className = "command-description";
1651
+ trigger.textContent = route.trigger;
1652
+ const meta = document.createElement("div");
1653
+ meta.className = "command-meta";
1654
+ appendCommandMeta(meta, "dashboard.skills.path", route.skill_path);
1655
+ appendCommandMeta(meta, "dashboard.skills.requiredInput", route.required_input);
1656
+ appendCommandMeta(meta, "dashboard.skills.editScope", route.edit_scope);
1657
+ appendCommandMeta(meta, "dashboard.skills.risk", route.risk);
1658
+ appendCommandMeta(meta, "dashboard.skills.verificationIntents", formatList(route.verification_intents));
1659
+ appendCommandMeta(meta, "dashboard.skills.declaredCommandIntents", formatList(route.declared_command_intents));
1660
+ details.appendChild(trigger);
1661
+ details.appendChild(meta);
1662
+ if (route.expected_output) {
1663
+ const output = document.createElement("div");
1664
+ output.className = "command-note";
1665
+ output.textContent = message("dashboard.skills.expectedOutput") + ": " + route.expected_output;
1666
+ details.appendChild(output);
1667
+ }
1668
+
1669
+ row.appendChild(summary);
1670
+ row.appendChild(details);
1671
+ section.appendChild(row);
1672
+ }
1673
+
1674
+ root.appendChild(section);
1675
+ }
1676
+
1677
+ async function loadDocuments() {
1678
+ setLoading(true);
1679
+ try {
1680
+ const response = await fetch("/api/docs/review" + docStatusQuery(), {
1681
+ headers: { "x-mustflow-dashboard-token": dashboardToken }
1682
+ });
1683
+ if (!response.ok) throw new Error(await response.text());
1684
+ docReview = await response.json();
1685
+ markDataUpdated();
1686
+ statusKey("dashboard.docs.reloaded", "ok");
1687
+ renderChrome();
1688
+ renderDocuments();
1689
+ } finally {
1690
+ setLoading(false);
1691
+ }
1692
+ }
1693
+
1694
+ async function save() {
1695
+ const updates = Array.from(pending, ([id, value]) => ({ id, value }));
1696
+ const response = await fetch("/api/preferences", {
1697
+ method: "POST",
1698
+ headers: {
1699
+ "content-type": "application/json",
1700
+ "x-mustflow-dashboard-token": dashboardToken
1701
+ },
1702
+ body: JSON.stringify({ updates })
1703
+ });
1704
+ if (!response.ok) throw new Error(await response.text());
1705
+ snapshot = await response.json();
1706
+ pending = new Map();
1707
+ updateSaveState();
1708
+ markDataUpdated();
1709
+ statusKey("dashboard.ui.saved", "ok");
1710
+ render();
1711
+ }
1712
+
1713
+ async function markDocument(path, status) {
1714
+ const reviewerId = document.getElementById("doc-reviewer-id").value.trim();
1715
+ if (!reviewerId) {
1716
+ statusKey("dashboard.docs.missingReviewerId", "error");
1717
+ return;
1718
+ }
1719
+
1720
+ const response = await fetch("/api/docs/review" + docStatusQuery(), {
1721
+ method: "POST",
1722
+ headers: {
1723
+ "content-type": "application/json",
1724
+ "x-mustflow-dashboard-token": dashboardToken
1725
+ },
1726
+ body: JSON.stringify({
1727
+ path,
1728
+ status,
1729
+ reviewerKind: document.getElementById("doc-reviewer-kind").value,
1730
+ reviewerId,
1731
+ summary: document.getElementById("doc-review-summary").value.trim()
1732
+ })
1733
+ });
1734
+ if (!response.ok) throw new Error(await response.text());
1735
+ docReview = await response.json();
1736
+ markDataUpdated();
1737
+ statusKey("dashboard.docs.updated", "ok");
1738
+ renderChrome();
1739
+ renderDocuments();
1740
+ }
1741
+
1742
+ function renderDocFilters() {
1743
+ const statusSelect = document.getElementById("doc-status-filter");
1744
+ const currentStatus = statusSelect.value || "active";
1745
+ statusSelect.textContent = "";
1746
+ for (const value of docStatusFilters) {
1747
+ const option = document.createElement("option");
1748
+ option.value = value;
1749
+ option.textContent = message("dashboard.docs.filter." + value);
1750
+ option.selected = value === currentStatus;
1751
+ statusSelect.appendChild(option);
1752
+ }
1753
+
1754
+ const kindSelect = document.getElementById("doc-reviewer-kind");
1755
+ const currentKind = kindSelect.value || "human";
1756
+ kindSelect.textContent = "";
1757
+ for (const value of reviewerKinds) {
1758
+ const option = document.createElement("option");
1759
+ option.value = value;
1760
+ option.textContent = message("dashboard.docs.reviewerKind." + value);
1761
+ option.selected = value === currentKind;
1762
+ kindSelect.appendChild(option);
1763
+ }
1764
+ }
1765
+
1766
+ function documentMatchesPathFilter(entry, query) {
1767
+ const normalizedQuery = query.trim().toLowerCase();
1768
+ if (!normalizedQuery) return true;
1769
+ const path = String(entry.path || "");
1770
+ const fileName = path.split(/[\\\\/]/u).pop() || path;
1771
+ return path.toLowerCase().includes(normalizedQuery) || fileName.toLowerCase().includes(normalizedQuery);
1772
+ }
1773
+
1774
+ function currentReviewerId() {
1775
+ return document.getElementById("doc-reviewer-id").value.trim();
1776
+ }
1777
+
1778
+ function currentReviewerKind() {
1779
+ const value = document.getElementById("doc-reviewer-kind").value;
1780
+ return value || "human";
1781
+ }
1782
+
1783
+ function renderDocumentReviewerState() {
1784
+ const element = document.getElementById("doc-reviewer-state");
1785
+ if (!element) return;
1786
+ const reviewerId = currentReviewerId();
1787
+ if (!reviewerId) {
1788
+ element.className = "doc-reviewer-state warn";
1789
+ element.textContent = message("dashboard.docs.reviewerStateMissing");
1790
+ return;
1791
+ }
1792
+ element.className = "doc-reviewer-state";
1793
+ element.textContent = messageFormat("dashboard.docs.reviewerState", {
1794
+ kind: message("dashboard.docs.reviewerKind." + currentReviewerKind()),
1795
+ id: reviewerId,
1796
+ });
1797
+ }
1798
+
1799
+ function renderDocuments() {
1800
+ const root = document.getElementById("docs-review-list");
1801
+ root.textContent = "";
1802
+ renderDocumentReviewerState();
1803
+
1804
+ if (docReview.documents.length === 0) {
1805
+ const empty = document.createElement("div");
1806
+ empty.className = "empty";
1807
+ empty.textContent = message("dashboard.docs.empty");
1808
+ root.appendChild(empty);
1809
+ return;
1810
+ }
1811
+
1812
+ const pathFilter = document.getElementById("doc-path-filter").value;
1813
+ const documents = docReview.documents.filter((entry) => documentMatchesPathFilter(entry, pathFilter));
1814
+ if (documents.length === 0) {
1815
+ const empty = document.createElement("div");
1816
+ empty.className = "empty";
1817
+ empty.textContent = message("dashboard.docs.noSearchMatches");
1818
+ root.appendChild(empty);
1819
+ return;
1820
+ }
1821
+
1822
+ for (const entry of documents) {
1823
+ const row = document.createElement("div");
1824
+ row.className = "doc-row";
1825
+ const details = document.createElement("div");
1826
+ const docPath = document.createElement("div");
1827
+ docPath.className = "doc-path";
1828
+ docPath.textContent = entry.path;
1829
+ const meta = document.createElement("div");
1830
+ meta.className = "doc-meta";
1831
+ meta.textContent = entry.reason;
1832
+ details.appendChild(docPath);
1833
+ details.appendChild(meta);
1834
+ if (entry.review_comment) {
1835
+ const comment = document.createElement("pre");
1836
+ comment.className = "doc-comment";
1837
+ comment.textContent = message("dashboard.docs.comment") + ":\\n" + entry.review_comment;
1838
+ details.appendChild(comment);
1839
+ }
1840
+
1841
+ const status = document.createElement("div");
1842
+ status.className = "doc-status " + entry.status;
1843
+ status.textContent = message("dashboard.docs.status." + entry.status);
1844
+
1845
+ const actions = document.createElement("div");
1846
+ actions.className = "doc-actions";
1847
+ const reviewerIdMissing = currentReviewerId().length === 0;
1848
+ for (const [nextStatus, labelKey, tooltipKey] of [
1849
+ ["approved", "dashboard.docs.action.approve", "dashboard.docs.action.approve.tooltip"],
1850
+ ["needs_human", "dashboard.docs.action.needsReview", "dashboard.docs.action.needsReview.tooltip"],
1851
+ ["ignored", "dashboard.docs.action.ignore", "dashboard.docs.action.ignore.tooltip"]
1852
+ ]) {
1853
+ const button = document.createElement("button");
1854
+ button.type = "button";
1855
+ button.textContent = message(labelKey);
1856
+ const alreadySelected = entry.status === nextStatus;
1857
+ const actionLabel = reviewerIdMissing
1858
+ ? message("dashboard.docs.missingReviewerId")
1859
+ : alreadySelected
1860
+ ? messageFormat("dashboard.docs.action.currentStatus", { status: message("dashboard.docs.status." + nextStatus) })
1861
+ : message(tooltipKey);
1862
+ button.title = actionLabel;
1863
+ button.setAttribute("aria-label", actionLabel);
1864
+ button.disabled = reviewerIdMissing || alreadySelected;
1865
+ button.setAttribute("aria-disabled", button.disabled ? "true" : "false");
1866
+ button.addEventListener("click", () => {
1867
+ markDocument(entry.path, nextStatus).catch((error) => statusText(error.message, "error"));
1868
+ });
1869
+ actions.appendChild(button);
1870
+ }
1871
+
1872
+ row.appendChild(details);
1873
+ row.appendChild(status);
1874
+ row.appendChild(actions);
1875
+ root.appendChild(row);
1876
+ }
1877
+ }
1878
+
1879
+ document.getElementById("dashboard-language").addEventListener("change", (event) => {
1880
+ currentLocale = event.target.value;
1881
+ window.localStorage.setItem("mustflow.dashboard.language", currentLocale);
1882
+ renderLocaleSelector();
1883
+ renderChrome();
1884
+ renderDocFilters();
1885
+ renderStatusPanel();
1886
+ renderVerificationPanel();
1887
+ renderCommandPanel();
1888
+ renderReleasePanel();
1889
+ renderUpdatePanel();
1890
+ renderRunsPanel();
1891
+ renderSkillsPanel();
1892
+ render();
1893
+ renderDocuments();
1894
+ });
1895
+
1896
+ document.getElementById("reload").addEventListener("click", () => {
1897
+ reloadCurrentTabData().catch((error) => statusText(error.message, "error"));
1898
+ });
1899
+ document.getElementById("save").addEventListener("click", () => {
1900
+ save().catch((error) => statusText(error.message, "error"));
1901
+ });
1902
+ document.getElementById("open-mustflow").addEventListener("click", () => {
1903
+ openMustflowFolder().catch((error) => statusText(error.message, "error"));
1904
+ });
1905
+ document.getElementById("doc-status-filter").addEventListener("change", () => {
1906
+ loadDocuments().catch((error) => statusText(error.message, "error"));
1907
+ });
1908
+ document.getElementById("doc-path-filter").addEventListener("input", () => {
1909
+ renderDocuments();
1910
+ });
1911
+ document.getElementById("doc-reviewer-id").addEventListener("input", () => {
1912
+ renderDocuments();
1913
+ });
1914
+ document.getElementById("doc-reviewer-kind").addEventListener("change", () => {
1915
+ renderDocuments();
1916
+ });
1917
+ for (const tab of document.querySelectorAll(".tab")) {
1918
+ tab.addEventListener("click", () => {
1919
+ activateTab(tab.dataset.tab);
1920
+ });
1921
+ tab.addEventListener("keydown", handleTabKeydown);
1922
+ }
1923
+ renderLocaleSelector();
1924
+ renderDocFilters();
1925
+ renderChrome();
1926
+ renderTabState();
1927
+ renderStatusPanel();
1928
+ renderVerificationPanel();
1929
+ renderCommandPanel();
1930
+ renderReleasePanel();
1931
+ renderUpdatePanel();
1932
+ renderRunsPanel();
1933
+ renderSkillsPanel();
1934
+ render();
1935
+ renderDocuments();`;
1936
+ }