@grifhinz/logics-manager 2.3.3 → 2.5.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.
@@ -1,6 +1,5 @@
1
1
  (() => {
2
2
  const stateKey = "logics.localViewer.state";
3
- const autoRefreshIntervalMs = 60 * 1000;
4
3
  const meta = () => document.getElementById("viewer-meta");
5
4
  const documentPanel = () => document.getElementById("viewer-document");
6
5
  const documentTitle = () => document.getElementById("viewer-document-title");
@@ -10,6 +9,16 @@
10
9
  const updateCopy = () => document.getElementById("viewer-update-copy");
11
10
  const updateCommand = () => document.getElementById("viewer-update-command");
12
11
  const filterCount = () => document.getElementById("viewer-filter-count");
12
+ const repoPill = () => document.getElementById("viewer-repo-pill");
13
+ const autoRefreshControl = () => document.getElementById("viewer-auto-refresh");
14
+ const refreshIntervalControl = () => document.getElementById("viewer-refresh-interval");
15
+ const refreshMenuButton = () => document.getElementById("viewer-refresh-menu-button");
16
+ const refreshMenuPanel = () => document.getElementById("viewer-refresh-menu");
17
+ const activityClearControl = () => document.getElementById("activity-clear");
18
+ const activityStorageLimit = 80;
19
+ const minAutoRefreshIntervalSeconds = 5;
20
+ const maxAutoRefreshIntervalSeconds = 60;
21
+ const defaultAutoRefreshIntervalMs = 15 * 1000;
13
22
  const defaultFilterState = {
14
23
  focus: "active",
15
24
  type: "all",
@@ -19,12 +28,20 @@
19
28
  };
20
29
  let viewerFilterState = { ...defaultFilterState };
21
30
  let latestItems = [];
31
+ let latestRepoRoot = "";
32
+ let latestMetaText = "Read-only local viewer";
33
+ let autoRefreshIntervalMs = defaultAutoRefreshIntervalMs;
34
+ let nextAutoRefreshAt = 0;
35
+ let autoRefreshEnabled = true;
36
+ let autoRefreshTimeoutId = 0;
37
+ let autoRefreshIntervalTouched = false;
22
38
  let applyingLocalChrome = false;
23
39
  let autoRefreshStarted = false;
24
40
  let itemsLoadInFlight = false;
25
41
  let refreshAfterVisible = false;
26
42
  let mermaidInitialized = false;
27
43
  let focusApplied = false;
44
+ let latestGitBadgeCounts = { unpushedCommits: 0, uncommittedFiles: 0 };
28
45
 
29
46
  function readStoredState() {
30
47
  try {
@@ -62,6 +79,52 @@
62
79
  writeStoredState({ ...nextState, viewerFilterState: { ...viewerFilterState } });
63
80
  }
64
81
 
82
+ function updateStoredActivity(nextItems) {
83
+ const storedState = readStoredState();
84
+ const baseState = storedState && typeof storedState === "object" ? storedState : {};
85
+ const previousSnapshot = baseState.activitySnapshot && typeof baseState.activitySnapshot === "object"
86
+ ? baseState.activitySnapshot
87
+ : {};
88
+ const history = Array.isArray(baseState.activityHistory) ? [...baseState.activityHistory] : [];
89
+ const nextSnapshot = {};
90
+ const now = new Date().toISOString();
91
+ const decorated = nextItems.map((item) => {
92
+ const relPath = String(item.relPath || item.path || item.id || "");
93
+ const status = String(item?.indicators?.Status || "").trim();
94
+ if (relPath) {
95
+ nextSnapshot[relPath] = { status, updatedAt: item.updatedAt || "" };
96
+ }
97
+ const previous = relPath ? previousSnapshot[relPath] : null;
98
+ const previousStatus = String(previous?.status || "").trim();
99
+ const statusChanged = Boolean(previousStatus && status && previousStatus !== status);
100
+ if (relPath && (statusChanged || !previous)) {
101
+ history.unshift({ path: relPath, at: now, status, previousStatus, type: statusChanged ? "status-change" : "updated" });
102
+ }
103
+ return statusChanged ? { ...item, activityType: "status-change" } : item;
104
+ });
105
+ writeStoredState({
106
+ ...baseState,
107
+ viewerFilterState: { ...viewerFilterState },
108
+ activitySnapshot: nextSnapshot,
109
+ activityHistory: history.slice(0, activityStorageLimit)
110
+ });
111
+ return decorated;
112
+ }
113
+
114
+ function clearActivityHistory() {
115
+ const storedState = readStoredState();
116
+ const nextState = storedState && typeof storedState === "object" ? { ...storedState } : {};
117
+ delete nextState.activitySnapshot;
118
+ delete nextState.activityHistory;
119
+ writeStoredState(nextState);
120
+ latestItems = latestItems.map((item) => {
121
+ const clone = { ...item };
122
+ delete clone.activityType;
123
+ return clone;
124
+ });
125
+ setMeta("Local activity history cleared.");
126
+ }
127
+
65
128
  function markdownApi() {
66
129
  if (typeof window.createCdxLogicsMarkdownApi === "function") {
67
130
  return window.createCdxLogicsMarkdownApi();
@@ -90,9 +153,139 @@
90
153
  }
91
154
 
92
155
  function setMeta(text) {
156
+ latestMetaText = text;
157
+ renderMeta();
158
+ }
159
+
160
+ function renderMeta() {
93
161
  const node = meta();
94
162
  if (node) {
95
- node.textContent = text;
163
+ const parts = [latestMetaText];
164
+ if (autoRefreshEnabled && nextAutoRefreshAt > 0) {
165
+ const seconds = Math.max(0, Math.ceil((nextAutoRefreshAt - Date.now()) / 1000));
166
+ parts.push(`next auto refresh in ${seconds}s`);
167
+ }
168
+ node.textContent = parts.join(" · ");
169
+ }
170
+ }
171
+
172
+ function normalizeAutoRefreshIntervalSeconds(value) {
173
+ const seconds = Math.round(Number(value));
174
+ if (!Number.isFinite(seconds) || seconds <= 0) {
175
+ return defaultAutoRefreshIntervalMs / 1000;
176
+ }
177
+ return Math.min(maxAutoRefreshIntervalSeconds, Math.max(minAutoRefreshIntervalSeconds, seconds));
178
+ }
179
+
180
+ function updateRefreshIntervalControl() {
181
+ const control = refreshIntervalControl();
182
+ if (!(control instanceof HTMLSelectElement)) {
183
+ return;
184
+ }
185
+ const seconds = String(Math.round(autoRefreshIntervalMs / 1000));
186
+ if (![...control.options].some((option) => option.value === seconds)) {
187
+ const option = document.createElement("option");
188
+ option.value = seconds;
189
+ option.textContent = `${seconds} sec`;
190
+ control.appendChild(option);
191
+ }
192
+ control.value = seconds;
193
+ }
194
+
195
+ function setAutoRefreshIntervalSeconds(value, options = {}) {
196
+ autoRefreshIntervalMs = normalizeAutoRefreshIntervalSeconds(value) * 1000;
197
+ if (options.user) {
198
+ autoRefreshIntervalTouched = true;
199
+ }
200
+ updateRefreshIntervalControl();
201
+ scheduleNextAutoRefresh();
202
+ }
203
+
204
+ function scheduleNextAutoRefresh() {
205
+ if (autoRefreshTimeoutId) {
206
+ window.clearTimeout(autoRefreshTimeoutId);
207
+ autoRefreshTimeoutId = 0;
208
+ }
209
+ nextAutoRefreshAt = autoRefreshEnabled ? Date.now() + autoRefreshIntervalMs : 0;
210
+ if (autoRefreshEnabled) {
211
+ autoRefreshTimeoutId = window.setTimeout(autoRefreshItems, autoRefreshIntervalMs);
212
+ }
213
+ renderMeta();
214
+ }
215
+
216
+ function updateRepositoryIdentity(payload) {
217
+ latestRepoRoot = String(payload.root || latestRepoRoot || "");
218
+ const pill = repoPill();
219
+ if (!pill) {
220
+ return;
221
+ }
222
+ const repoName = String(payload.repoName || latestRepoRoot.split(/[\\/]/).filter(Boolean).pop() || "repository");
223
+ pill.textContent = repoName;
224
+ pill.title = latestRepoRoot || repoName;
225
+ }
226
+
227
+ function normalizeGitBadgeCounts(payload) {
228
+ const counts = payload && typeof payload === "object" ? payload.badgeCounts || {} : {};
229
+ return {
230
+ unpushedCommits: Math.max(0, Number(counts.unpushedCommits || payload?.ahead || 0)),
231
+ uncommittedFiles: Math.max(0, Number(counts.uncommittedFiles || 0))
232
+ };
233
+ }
234
+
235
+ function renderGitBadge(kind, count) {
236
+ const value = Number(count || 0);
237
+ if (value <= 0) {
238
+ return "";
239
+ }
240
+ const label = kind === "commits"
241
+ ? `${value} commits locaux non pushés`
242
+ : `${value} fichiers modifiés non commités`;
243
+ return `<span class="viewer-git-badge viewer-git-badge--${kind}" title="${escapeHtml(label)}" aria-label="${escapeHtml(label)}">${escapeHtml(value)}</span>`;
244
+ }
245
+
246
+ function gitBadgeHtml(scope) {
247
+ const commitsVisible = latestGitBadgeCounts.unpushedCommits > 0 && (
248
+ scope === "main" || scope === "history"
249
+ );
250
+ const filesVisible = latestGitBadgeCounts.uncommittedFiles > 0 && (
251
+ scope === "main" || scope === "changes"
252
+ );
253
+ const html = [
254
+ commitsVisible ? renderGitBadge("commits", latestGitBadgeCounts.unpushedCommits) : "",
255
+ filesVisible ? renderGitBadge("files", latestGitBadgeCounts.uncommittedFiles) : ""
256
+ ].filter(Boolean).join("");
257
+ return html ? `<span class="viewer-git-badges" data-viewer-git-badges="${escapeHtml(scope)}">${html}</span>` : "";
258
+ }
259
+
260
+ function updateMainGitBadges() {
261
+ const button = document.getElementById("viewer-git");
262
+ if (!(button instanceof HTMLElement)) {
263
+ return;
264
+ }
265
+ button.querySelector('[data-viewer-git-badges="main"]')?.remove();
266
+ const html = gitBadgeHtml("main");
267
+ if (html) {
268
+ button.insertAdjacentHTML("beforeend", html);
269
+ }
270
+ }
271
+
272
+ function setGitBadgeCountsFromPayload(payload, options = {}) {
273
+ latestGitBadgeCounts = normalizeGitBadgeCounts(payload);
274
+ if (options.updateMain !== false) {
275
+ updateMainGitBadges();
276
+ }
277
+ }
278
+
279
+ async function refreshGitBadgeCounters() {
280
+ try {
281
+ const response = await fetch("/api/git-status");
282
+ const data = await response.json();
283
+ if (response.ok && data.ok && data.payload?.state === "ok") {
284
+ setGitBadgeCountsFromPayload(data.payload);
285
+ }
286
+ } catch {
287
+ latestGitBadgeCounts = { unpushedCommits: 0, uncommittedFiles: 0 };
288
+ updateMainGitBadges();
96
289
  }
97
290
  }
98
291
 
@@ -333,16 +526,24 @@
333
526
  }
334
527
 
335
528
  function postToApp(payload, options = {}) {
336
- latestItems = Array.isArray(payload.items) ? payload.items : [];
337
- const nextPayload = options.silent ? payload : applyFocusRequest(payload);
529
+ latestItems = updateStoredActivity(Array.isArray(payload.items) ? payload.items : []);
530
+ if (!autoRefreshIntervalTouched) {
531
+ autoRefreshIntervalMs = normalizeAutoRefreshIntervalSeconds(payload.autoRefreshIntervalSeconds) * 1000;
532
+ updateRefreshIntervalControl();
533
+ }
534
+ updateRepositoryIdentity(payload);
535
+ const payloadWithActivity = { ...payload, items: latestItems };
536
+ const nextPayload = options.silent ? payloadWithActivity : applyFocusRequest(payloadWithActivity);
338
537
  window.dispatchEvent(new MessageEvent("message", { data: { type: "data", payload: nextPayload } }));
339
538
  const rootName = payload.root ? payload.root.split(/[\\/]/).filter(Boolean).pop() : "repository";
340
539
  if (!options.silent) {
341
540
  setMeta(`${rootName} · ${payload.items.length} docs · refreshed ${new Date().toLocaleTimeString()}`);
342
541
  }
542
+ scheduleNextAutoRefresh();
343
543
  renderUpdateNotice(payload.updateInfo);
344
544
  updateFilterSummary();
345
545
  applyLocalViewerChrome();
546
+ bindRefreshMenuControls();
346
547
  }
347
548
 
348
549
  function renderUpdateNotice(updateInfo) {
@@ -380,18 +581,47 @@
380
581
  throw new Error(data.error || "Unable to load viewer data.");
381
582
  }
382
583
  postToApp(data.payload, { silent: Boolean(options.silent) });
584
+ if (method !== "POST") {
585
+ await refreshGitBadgeCounters();
586
+ }
383
587
  return true;
384
588
  } finally {
385
589
  itemsLoadInFlight = false;
386
590
  }
387
591
  }
388
592
 
593
+ function isGitStatusOpen() {
594
+ const panel = documentPanel();
595
+ const title = documentTitle();
596
+ return Boolean(panel && !panel.hidden && title && title.textContent === "Git status");
597
+ }
598
+
599
+ function isCdxStatusOpen() {
600
+ const panel = documentPanel();
601
+ const title = documentTitle();
602
+ return Boolean(panel && !panel.hidden && title && title.textContent === "CDX status");
603
+ }
604
+
605
+ async function refreshViewer(method = "POST", options = {}) {
606
+ await loadItems(method, options);
607
+ if (isGitStatusOpen()) {
608
+ await showGitStatus({ preserve: true, silent: Boolean(options.silent) });
609
+ } else if (isCdxStatusOpen()) {
610
+ await showCdxStatus({ silent: Boolean(options.silent) });
611
+ } else if (method === "POST") {
612
+ await refreshGitBadgeCounters();
613
+ }
614
+ }
615
+
389
616
  function autoRefreshItems() {
617
+ if (!autoRefreshEnabled) {
618
+ return;
619
+ }
390
620
  if (document.hidden) {
391
621
  refreshAfterVisible = true;
392
622
  return;
393
623
  }
394
- loadItems("POST", { silent: true }).catch((error) => setMeta(error.message));
624
+ refreshViewer("POST", { silent: true }).catch((error) => setMeta(error.message));
395
625
  }
396
626
 
397
627
  function startAutoRefresh() {
@@ -399,7 +629,9 @@
399
629
  return;
400
630
  }
401
631
  autoRefreshStarted = true;
402
- window.setInterval(autoRefreshItems, autoRefreshIntervalMs);
632
+ window.setInterval(() => {
633
+ renderMeta();
634
+ }, 1000);
403
635
  document.addEventListener("visibilitychange", () => {
404
636
  if (!document.hidden && refreshAfterVisible) {
405
637
  refreshAfterVisible = false;
@@ -408,13 +640,57 @@
408
640
  });
409
641
  }
410
642
 
643
+ function setAutoRefreshEnabled(enabled) {
644
+ autoRefreshEnabled = Boolean(enabled);
645
+ const control = autoRefreshControl();
646
+ if (control instanceof HTMLInputElement) {
647
+ control.checked = autoRefreshEnabled;
648
+ }
649
+ scheduleNextAutoRefresh();
650
+ }
651
+
652
+ function setRefreshMenuOpen(open) {
653
+ const panel = refreshMenuPanel();
654
+ const button = refreshMenuButton();
655
+ if (!panel) {
656
+ return;
657
+ }
658
+ panel.hidden = !open;
659
+ if (button instanceof HTMLElement) {
660
+ button.setAttribute("aria-expanded", open ? "true" : "false");
661
+ }
662
+ }
663
+
664
+ function bindRefreshMenuControls() {
665
+ const button = refreshMenuButton();
666
+ if (button) {
667
+ button.onclick = (event) => {
668
+ event.stopPropagation();
669
+ const panel = refreshMenuPanel();
670
+ setRefreshMenuOpen(Boolean(panel?.hidden));
671
+ };
672
+ }
673
+ const panel = refreshMenuPanel();
674
+ if (panel) {
675
+ panel.onclick = (event) => {
676
+ event.stopPropagation();
677
+ };
678
+ }
679
+ }
680
+
411
681
  function statusValue(item) {
412
682
  return String(item?.indicators?.Status || "").toLowerCase();
413
683
  }
414
684
 
415
685
  function isClosed(item) {
416
686
  const status = statusValue(item);
417
- return status.includes("done") || status.includes("archived") || status.includes("obsolete");
687
+ return (
688
+ status.includes("done") ||
689
+ status.includes("archived") ||
690
+ status.includes("obsolete") ||
691
+ status.includes("superseded") ||
692
+ status.includes("settled")
693
+ );
418
694
  }
419
695
 
420
696
  function hasLinks(item) {
@@ -435,6 +711,52 @@
435
711
  return timestamp > 0 && timestamp < Date.now() - 30 * 24 * 60 * 60 * 1000 && !isClosed(item);
436
712
  }
437
713
 
714
+ function isRecent(item, days = 7) {
715
+ return updatedWithin(item, days);
716
+ }
717
+
718
+ function hasMissingOrAmbiguousStatus(item) {
719
+ const rawStatus = String(item?.indicators?.Status || "").trim();
720
+ if (!rawStatus) {
721
+ return true;
722
+ }
723
+ const normalized = rawStatus.toLowerCase();
724
+ return ![
725
+ "draft",
726
+ "ready",
727
+ "in progress",
728
+ "blocked",
729
+ "done",
730
+ "active",
731
+ "proposed",
732
+ "accepted",
733
+ "validated",
734
+ "rejected",
735
+ "superseded",
736
+ "settled",
737
+ "archived",
738
+ "obsolete"
739
+ ].includes(normalized);
740
+ }
741
+
742
+ function isSafeLogicsDocPath(value) {
743
+ const path = String(value || "").replace(/\\/g, "/").replace(/^\.?\//, "").trim();
744
+ if (!path || path.startsWith("/") || path.startsWith("~") || /^[A-Za-z]:/.test(path)) {
745
+ return false;
746
+ }
747
+ if (path.split("/").includes("..") || !path.endsWith(".md")) {
748
+ return false;
749
+ }
750
+ return [
751
+ "logics/request/",
752
+ "logics/backlog/",
753
+ "logics/tasks/",
754
+ "logics/product/",
755
+ "logics/architecture/",
756
+ "logics/specs/"
757
+ ].some((prefix) => path.startsWith(prefix));
758
+ }
759
+
438
760
  function matchesViewerFilter(item) {
439
761
  if (!item) {
440
762
  return false;
@@ -562,58 +884,233 @@
562
884
  count.textContent = `${visibleCount} of ${latestItems.length} docs shown${suffix}`;
563
885
  }
564
886
 
565
- function buildCorpusInsights() {
566
- const countsByStage = latestItems.reduce((acc, item) => {
567
- acc[item.stage] = (acc[item.stage] || 0) + 1;
887
+ function countBy(items, selector) {
888
+ return items.reduce((acc, item) => {
889
+ const key = selector(item) || "unknown";
890
+ acc[key] = (acc[key] || 0) + 1;
568
891
  return acc;
569
892
  }, {});
570
- const active = latestItems.filter((item) => !isClosed(item)).length;
571
- const blocked = latestItems.filter((item) => statusValue(item).includes("blocked")).length;
572
- const unlinked = latestItems.filter((item) => (item.references || []).length === 0 && (item.usedBy || []).length === 0).length;
573
- const incompleteChains = latestItems.filter((item) => ["request", "backlog"].includes(item.stage) && !item.isPromoted && !isClosed(item)).length;
574
- const cards = [
575
- ["Docs", latestItems.length],
576
- ["Active", active],
577
- ["Blocked", blocked],
578
- ["Unlinked", unlinked]
579
- ].map(([label, value]) => `
893
+ }
894
+
895
+ function renderMetricCards(entries) {
896
+ return entries.map(([label, value]) => `
580
897
  <div class="viewer-insights__card">
581
898
  <div class="viewer-insights__label">${escapeHtml(label)}</div>
582
899
  <div class="viewer-insights__value">${escapeHtml(value)}</div>
583
900
  </div>
584
901
  `).join("");
902
+ }
903
+
904
+ function renderInsightRows(items, emptyText = "No signals") {
905
+ if (!items.length) {
906
+ return `<li class="viewer-insights__item">${escapeHtml(emptyText)}</li>`;
907
+ }
908
+ return items.map(([label, value]) => `
909
+ <li class="viewer-insights__item"><span>${escapeHtml(label)}</span><strong>${escapeHtml(value)}</strong></li>
910
+ `).join("");
911
+ }
912
+
913
+ function renderDocRows(items, emptyText = "None", limit = 6) {
914
+ if (!items.length) {
915
+ return `<li class="viewer-insights__row viewer-insights__row--empty">${escapeHtml(emptyText)}</li>`;
916
+ }
917
+ const rows = items.map((item, index) => {
918
+ const path = item.relPath || item.path || "";
919
+ const control = path && isSafeLogicsDocPath(path)
920
+ ? `<button class="viewer-insights__doc" type="button" data-viewer-doc-path="${escapeHtml(path)}">${escapeHtml(item.id || path)}</button>`
921
+ : `<span class="viewer-insights__doc">${escapeHtml(item.id || path || item.title)}</span>`;
922
+ return `
923
+ <li class="viewer-insights__row" ${index >= limit ? "hidden data-viewer-hidden-row" : ""}>
924
+ ${control}
925
+ <span>${escapeHtml(item.indicators?.Status || item.stage || "No status")}</span>
926
+ </li>
927
+ `;
928
+ });
929
+ const hiddenCount = Math.max(0, items.length - limit);
930
+ if (hiddenCount > 0) {
931
+ rows.push(`<li class="viewer-insights__row"><button class="viewer-insights__reveal" type="button" data-viewer-reveal>Show ${hiddenCount} more</button></li>`);
932
+ }
933
+ return rows.join("");
934
+ }
935
+
936
+ function renderPathRows(paths, emptyText = "None", limit = 6) {
937
+ if (!paths.length) {
938
+ return `<li class="viewer-insights__row viewer-insights__row--empty">${escapeHtml(emptyText)}</li>`;
939
+ }
940
+ const rows = paths.map((path, index) => {
941
+ const control = isSafeLogicsDocPath(path)
942
+ ? `<button class="viewer-insights__doc" type="button" data-viewer-doc-path="${escapeHtml(path)}">${escapeHtml(path)}</button>`
943
+ : `<span class="viewer-insights__doc">${escapeHtml(path)}</span>`;
944
+ return `<li class="viewer-insights__row" ${index >= limit ? "hidden data-viewer-hidden-row" : ""}>${control}</li>`;
945
+ });
946
+ const hiddenCount = Math.max(0, paths.length - limit);
947
+ if (hiddenCount > 0) {
948
+ rows.push(`<li class="viewer-insights__row"><button class="viewer-insights__reveal" type="button" data-viewer-reveal>Show ${hiddenCount} more</button></li>`);
949
+ }
950
+ return rows.join("");
951
+ }
952
+
953
+ function renderActionRows(actions) {
954
+ return actions.map((action) => {
955
+ if (action.filter) {
956
+ return `
957
+ <li class="viewer-insights__row">
958
+ <button class="viewer-insights__action" type="button" data-viewer-filter-group="${escapeHtml(action.filter.group)}" data-viewer-filter-value="${escapeHtml(action.filter.value)}">${escapeHtml(action.label)}</button>
959
+ <strong>${escapeHtml(action.value)}</strong>
960
+ </li>
961
+ `;
962
+ }
963
+ if (action.health) {
964
+ return `
965
+ <li class="viewer-insights__row">
966
+ <button class="viewer-insights__action" type="button" data-viewer-open-health>${escapeHtml(action.label)}</button>
967
+ <strong>${escapeHtml(action.value)}</strong>
968
+ </li>
969
+ `;
970
+ }
971
+ if (action.path && isSafeLogicsDocPath(action.path)) {
972
+ return `
973
+ <li class="viewer-insights__row">
974
+ <button class="viewer-insights__action" type="button" data-viewer-doc-path="${escapeHtml(action.path)}">${escapeHtml(action.label)}</button>
975
+ <strong>${escapeHtml(action.value)}</strong>
976
+ </li>
977
+ `;
978
+ }
979
+ return `<li class="viewer-insights__row"><span>${escapeHtml(action.label)}</span><strong>${escapeHtml(action.value)}</strong></li>`;
980
+ }).join("");
981
+ }
982
+
983
+ function itemLabel(item) {
984
+ return `${item.id || item.relPath || "doc"} - ${item.indicators?.Status || "No status"}`;
985
+ }
986
+
987
+ function buildCorpusInsights(lintData = null, auditData = null) {
988
+ const docs = latestItems;
989
+ const itemPaths = new Set(docs.map((item) => item.relPath).filter(Boolean));
990
+ const countsByStage = countBy(docs, (item) => item.stage);
991
+ const closed = docs.filter(isClosed);
992
+ const open = docs.filter((item) => !isClosed(item));
993
+ const blocked = docs.filter((item) => statusValue(item).includes("blocked"));
994
+ const missingStatus = docs.filter(hasMissingOrAmbiguousStatus);
995
+ const recentlyModified = docs.filter((item) => isRecent(item, 7));
996
+ const incompleteChains = docs.filter((item) => ["request", "backlog"].includes(item.stage) && !item.isPromoted && !isClosed(item));
997
+ const unlinked = docs.filter((item) => (item.references || []).length === 0 && (item.usedBy || []).length === 0);
998
+ const brokenRefs = [];
999
+ const relationshipCounts = {};
1000
+ docs.forEach((item) => {
1001
+ relationshipCounts[item.stage] = (relationshipCounts[item.stage] || 0) + (item.references || []).length + (item.usedBy || []).length;
1002
+ (item.references || []).forEach((ref) => {
1003
+ if (ref.path && !itemPaths.has(ref.path)) {
1004
+ brokenRefs.push(`${item.id} -> ${ref.path}`);
1005
+ }
1006
+ });
1007
+ });
1008
+ const mostReferenced = [...docs]
1009
+ .sort((left, right) => (right.usedBy || []).length - (left.usedBy || []).length)
1010
+ .filter((item) => (item.usedBy || []).length > 0)
1011
+ .slice(0, 8);
1012
+ const recentRows = [...docs]
1013
+ .sort((left, right) => (Date.parse(right.updatedAt || "") || 0) - (Date.parse(left.updatedAt || "") || 0))
1014
+ .slice(0, 8);
1015
+ const staleActive = open.filter(isStale).slice(0, 8);
1016
+ const qualityFindings = lintData && auditData ? collectHealthFindings(lintData, auditData) : [];
1017
+ const qualityBySource = countBy(qualityFindings, (finding) => finding.source || finding.code || "finding");
1018
+ const qualityByDocType = countBy(qualityFindings, (finding) => {
1019
+ const path = String(finding.path || "");
1020
+ const matched = docs.find((item) => item.relPath === path);
1021
+ return matched?.stage || (path ? "unknown document" : "repository");
1022
+ });
1023
+ const concentratedIssues = Object.entries(countBy(qualityFindings, (finding) => finding.path || "repository"))
1024
+ .sort((left, right) => Number(right[1]) - Number(left[1]))
1025
+ .slice(0, 8);
1026
+ const actions = [];
1027
+ if (blocked.length) {
1028
+ actions.push({ label: "Review blocked workflow docs", value: blocked.length, filter: { group: "focus", value: "blocked" } });
1029
+ }
1030
+ if (incompleteChains.length) {
1031
+ actions.push({ label: "Promote or close incomplete workflow chains", value: incompleteChains.length, filter: { group: "focus", value: "needs-promotion" } });
1032
+ }
1033
+ if (brokenRefs.length) {
1034
+ actions.push({ label: "Repair broken references", value: brokenRefs.length, health: true });
1035
+ }
1036
+ if (qualityFindings.length) {
1037
+ actions.push({ label: "Open validation health", value: qualityFindings.length, health: true });
1038
+ }
1039
+ if (missingStatus.length) {
1040
+ actions.push({ label: "Normalize missing or ambiguous statuses", value: missingStatus.length, path: missingStatus[0]?.relPath || "" });
1041
+ }
1042
+ if (!actions.length) {
1043
+ actions.push({ label: "No immediate operator action detected", value: "OK" });
1044
+ }
1045
+
585
1046
  const stageRows = Object.entries(countsByStage)
586
1047
  .sort((left, right) => String(left[0]).localeCompare(String(right[0])))
587
- .map(([stage, count]) => `<li class="viewer-insights__item"><span>${escapeHtml(stage)}</span><strong>${escapeHtml(count)}</strong></li>`)
588
- .join("");
589
- const recentRows = [...latestItems]
590
- .sort((left, right) => (Date.parse(right.updatedAt || "") || 0) - (Date.parse(left.updatedAt || "") || 0))
591
- .slice(0, 8)
592
- .map((item) => `<li class="viewer-insights__item"><span>${escapeHtml(item.id)}</span><strong>${escapeHtml(item.indicators?.Status || "No status")}</strong></li>`)
593
- .join("");
1048
+ .map(([stage, count]) => [stage, count]);
594
1049
  return `
595
1050
  <div class="viewer-insights">
596
- <div class="viewer-insights__summary">${cards}</div>
597
- <div class="viewer-insights__grid">
598
- <div class="viewer-insights__card">
599
- <div class="viewer-insights__label">Incomplete chains</div>
600
- <div class="viewer-insights__value">${escapeHtml(incompleteChains)}</div>
601
- </div>
602
- </div>
1051
+ <div class="viewer-insights__summary">${renderMetricCards([
1052
+ ["Docs", docs.length],
1053
+ ["Open", open.length],
1054
+ ["Closed", closed.length],
1055
+ ["Blocked", blocked.length],
1056
+ ["Missing status", missingStatus.length],
1057
+ ["Modified 7d", recentlyModified.length]
1058
+ ])}</div>
1059
+ <section class="viewer-insights__section">
1060
+ <h2>Overview</h2>
1061
+ <ul class="viewer-insights__list">${renderInsightRows(stageRows, "No docs loaded")}</ul>
1062
+ </section>
1063
+ <section class="viewer-insights__section">
1064
+ <h2>Flow health</h2>
1065
+ <ul class="viewer-insights__list">${renderInsightRows([
1066
+ ["Incomplete workflow chains", incompleteChains.length],
1067
+ ["Promotion gaps", incompleteChains.filter((item) => item.stage === "request" || item.stage === "backlog").length],
1068
+ ["Orphan or unlinked docs", unlinked.length],
1069
+ ["Broken reference risks", brokenRefs.length]
1070
+ ])}</ul>
1071
+ <ul class="viewer-insights__rows">${renderDocRows(incompleteChains, "No incomplete chains")}</ul>
1072
+ </section>
603
1073
  <section class="viewer-insights__section">
604
- <h2>Corpus families</h2>
605
- <ul class="viewer-insights__list">${stageRows || '<li class="viewer-insights__item">No docs loaded</li>'}</ul>
1074
+ <h2>Activity</h2>
1075
+ <ul class="viewer-insights__list">${renderInsightRows([
1076
+ ["Latest changes", recentRows.map(itemLabel).join(", ") || "None"],
1077
+ ["Stale active docs", staleActive.map(itemLabel).join(", ") || "None"],
1078
+ ["Recently active docs", recentlyModified.slice(0, 8).map(itemLabel).join(", ") || "None"],
1079
+ ["Activity classification", `recent ${recentlyModified.length}, stale ${open.filter(isStale).length}, quiet ${Math.max(0, open.length - recentlyModified.length)}`]
1080
+ ])}</ul>
1081
+ <ul class="viewer-insights__rows">${renderDocRows(recentRows, "No recent documents")}</ul>
606
1082
  </section>
607
1083
  <section class="viewer-insights__section">
608
- <h2>Recent activity</h2>
609
- <ul class="viewer-insights__list">${recentRows || '<li class="viewer-insights__item">No recent docs</li>'}</ul>
1084
+ <h2>Traceability</h2>
1085
+ <ul class="viewer-insights__list">${renderInsightRows([
1086
+ ["Most referenced docs", mostReferenced.map((item) => `${item.id} (${(item.usedBy || []).length})`).join(", ") || "None"],
1087
+ ["Unlinked docs", unlinked.slice(0, 8).map((item) => item.id).join(", ") || "None"],
1088
+ ["Broken references", brokenRefs.slice(0, 8).join(", ") || "None"],
1089
+ ["Relationships by type", Object.entries(relationshipCounts).map(([stage, count]) => `${stage} ${count}`).join(", ") || "None"]
1090
+ ])}</ul>
1091
+ <ul class="viewer-insights__rows">${renderDocRows(unlinked, "No unlinked documents")}${renderPathRows(brokenRefs, "No broken references")}</ul>
1092
+ </section>
1093
+ <section class="viewer-insights__section">
1094
+ <h2>Quality signals</h2>
1095
+ <ul class="viewer-insights__list">${renderInsightRows([
1096
+ ["Lint/audit categories", Object.entries(qualityBySource).map(([key, count]) => `${key} ${count}`).join(", ") || "No findings loaded"],
1097
+ ["Findings by document type", Object.entries(qualityByDocType).map(([key, count]) => `${key} ${count}`).join(", ") || "No findings loaded"],
1098
+ ["Concentrated issues", concentratedIssues.map(([key, count]) => `${key} ${count}`).join(", ") || "None"]
1099
+ ])}</ul>
1100
+ <ul class="viewer-insights__rows">${renderPathRows(concentratedIssues.map(([key, count]) => `${key} (${count})`), "No concentrated issues")}</ul>
1101
+ </section>
1102
+ <section class="viewer-insights__section">
1103
+ <h2>Operator actions</h2>
1104
+ <ul class="viewer-insights__rows">${renderActionRows(actions)}</ul>
610
1105
  </section>
611
1106
  </div>
612
1107
  `;
613
1108
  }
614
1109
 
615
- function showCorpusInsights() {
616
- setDocument("Corpus insights", buildCorpusInsights());
1110
+ async function showCorpusInsights() {
1111
+ const [lintResponse, auditResponse] = await Promise.all([fetch("/api/lint"), fetch("/api/audit")]);
1112
+ const [lintData, auditData] = await Promise.all([lintResponse.json(), auditResponse.json()]);
1113
+ setDocument("Corpus insights", buildCorpusInsights(lintData, auditData));
617
1114
  setMeta("Corpus insights loaded.");
618
1115
  }
619
1116
 
@@ -711,9 +1208,9 @@
711
1208
  const list = findings.length
712
1209
  ? findings.slice(0, 50).map((finding) => {
713
1210
  const path = finding.path || "";
714
- const pathControl = path
1211
+ const pathControl = path && isSafeLogicsDocPath(path)
715
1212
  ? `<button class="viewer-health__path" type="button" data-viewer-doc-path="${escapeHtml(path)}">${escapeHtml(path)}</button>`
716
- : '<span class="viewer-health__meta">Repository-level finding</span>';
1213
+ : `<span class="viewer-health__meta">${escapeHtml(path ? `Repository-level or unsafe path: ${path}` : "Repository-level finding")}</span>`;
717
1214
  const severity = finding.severity || finding.code || finding.source || "finding";
718
1215
  return `
719
1216
  <li class="viewer-health__issue">
@@ -744,6 +1241,710 @@
744
1241
  setMeta("Health loaded.");
745
1242
  }
746
1243
 
1244
+ function objectEntries(value) {
1245
+ return value && typeof value === "object" && !Array.isArray(value) ? Object.entries(value) : [];
1246
+ }
1247
+
1248
+ function asArray(value) {
1249
+ if (Array.isArray(value)) {
1250
+ return value;
1251
+ }
1252
+ if (value && typeof value === "object") {
1253
+ return Object.entries(value).map(([key, entry]) => ({ name: key, ...(entry && typeof entry === "object" ? entry : { value: entry }) }));
1254
+ }
1255
+ return [];
1256
+ }
1257
+
1258
+ function pickFirstObject(status, keys) {
1259
+ for (const key of keys) {
1260
+ if (status?.[key] && typeof status[key] === "object" && !Array.isArray(status[key])) {
1261
+ return status[key];
1262
+ }
1263
+ }
1264
+ return {};
1265
+ }
1266
+
1267
+ function pickFirstArray(status, keys) {
1268
+ for (const key of keys) {
1269
+ const entries = asArray(status?.[key]);
1270
+ if (entries.length) {
1271
+ return entries;
1272
+ }
1273
+ }
1274
+ return [];
1275
+ }
1276
+
1277
+ function cdxRows(status) {
1278
+ return asArray(status?.rows);
1279
+ }
1280
+
1281
+ function cdxProviders(status) {
1282
+ const explicitProviders = pickFirstArray(status, ["providers", "providerStatus", "provider_status"]);
1283
+ if (explicitProviders.length) {
1284
+ return explicitProviders;
1285
+ }
1286
+ const grouped = new Map();
1287
+ cdxRows(status).forEach((row) => {
1288
+ const provider = String(row.provider || "unknown");
1289
+ const current = grouped.get(provider) || { name: provider, enabled: 0, active: 0, authenticated: 0, sessions: 0, lowest_available_pct: null };
1290
+ current.sessions += 1;
1291
+ if (row.enabled) {
1292
+ current.enabled += 1;
1293
+ }
1294
+ if (row.active) {
1295
+ current.active += 1;
1296
+ }
1297
+ if (String(row.auth_status || "").toLowerCase() === "authenticated") {
1298
+ current.authenticated += 1;
1299
+ }
1300
+ if (typeof row.available_pct === "number") {
1301
+ current.lowest_available_pct = current.lowest_available_pct === null
1302
+ ? row.available_pct
1303
+ : Math.min(current.lowest_available_pct, row.available_pct);
1304
+ }
1305
+ current.state = current.active > 0 ? "active" : current.enabled > 0 ? "enabled" : "disabled";
1306
+ grouped.set(provider, current);
1307
+ });
1308
+ return Array.from(grouped.values());
1309
+ }
1310
+
1311
+ function cdxSessions(status) {
1312
+ const explicitSessions = pickFirstArray(status, ["sessions", "activeSessions", "active_sessions"]);
1313
+ return sortCdxSessionsByRemaining(explicitSessions.length ? explicitSessions : cdxRows(status));
1314
+ }
1315
+
1316
+ function cdxReadiness(status) {
1317
+ const explicitReadiness = pickFirstObject(status, ["readiness", "quota", "quotas", "limits"]);
1318
+ if (objectEntries(explicitReadiness).length) {
1319
+ return explicitReadiness;
1320
+ }
1321
+ const rows = cdxRows(status);
1322
+ if (!rows.length) {
1323
+ return {};
1324
+ }
1325
+ const enabled = rows.filter((row) => row.enabled).length;
1326
+ const active = rows.filter((row) => row.active).length;
1327
+ const authenticated = rows.filter((row) => String(row.auth_status || "").toLowerCase() === "authenticated").length;
1328
+ const availableValues = rows.map((row) => row.available_pct).filter((value) => typeof value === "number");
1329
+ const lowestAvailable = availableValues.length ? Math.min(...availableValues) : null;
1330
+ return {
1331
+ enabled_sessions: enabled,
1332
+ active_sessions: active,
1333
+ authenticated_sessions: authenticated,
1334
+ lowest_remaining: lowestAvailable === null ? "not reported" : `${lowestAvailable}%`
1335
+ };
1336
+ }
1337
+
1338
+ function renderCdxObjectRows(value, emptyText) {
1339
+ const rows = objectEntries(value).slice(0, 12).map(([key, entry]) => `
1340
+ <li class="viewer-cdx__row">
1341
+ <span>${escapeHtml(cdxLabel(key))}</span>
1342
+ <strong>${escapeHtml(typeof entry === "object" ? JSON.stringify(entry) : entry)}</strong>
1343
+ </li>
1344
+ `).join("");
1345
+ return rows || `<li class="viewer-cdx__empty">${escapeHtml(emptyText)}</li>`;
1346
+ }
1347
+
1348
+ function cdxLabel(value) {
1349
+ return String(value || "")
1350
+ .replace(/[_-]+/g, " ")
1351
+ .replace(/\b\w/g, (letter) => letter.toUpperCase());
1352
+ }
1353
+
1354
+ function cdxStateClass(value) {
1355
+ const state = String(value || "").toLowerCase();
1356
+ if (["ready", "ok", "active", "enabled", "authenticated"].some((entry) => state.includes(entry))) {
1357
+ return "ok";
1358
+ }
1359
+ if (["starting", "pending", "warning", "low", "limited"].some((entry) => state.includes(entry))) {
1360
+ return "warn";
1361
+ }
1362
+ if (["error", "failed", "disabled", "unavailable", "unauthenticated"].some((entry) => state.includes(entry))) {
1363
+ return "bad";
1364
+ }
1365
+ return "neutral";
1366
+ }
1367
+
1368
+ function cdxRemainingPct(item) {
1369
+ const value = item?.remaining_pct ?? item?.remainingPct ?? item?.available_pct ?? item?.availablePct ?? item?.lowest_available_pct ?? item?.lowestAvailablePct;
1370
+ const percent = Number(value);
1371
+ return Number.isFinite(percent) ? Math.max(0, Math.min(100, Math.round(percent))) : null;
1372
+ }
1373
+
1374
+ function cdxPct(value) {
1375
+ const percent = Number(value);
1376
+ return Number.isFinite(percent) ? `${Math.max(0, Math.min(100, Math.round(percent)))}%` : "-";
1377
+ }
1378
+
1379
+ function cdxField(item, keys, fallback = "-") {
1380
+ for (const key of keys) {
1381
+ const value = item?.[key];
1382
+ if (value !== undefined && value !== null && value !== "") {
1383
+ return value;
1384
+ }
1385
+ }
1386
+ return fallback;
1387
+ }
1388
+
1389
+ function cdxRemainingClass(percent) {
1390
+ if (percent === null) {
1391
+ return "neutral";
1392
+ }
1393
+ if (percent <= 10) {
1394
+ return "bad";
1395
+ }
1396
+ if (percent <= 30) {
1397
+ return "warn";
1398
+ }
1399
+ return "ok";
1400
+ }
1401
+
1402
+ function sortCdxSessionsByRemaining(entries) {
1403
+ return [...entries].sort((left, right) => {
1404
+ const leftRemaining = cdxRemainingPct(left);
1405
+ const rightRemaining = cdxRemainingPct(right);
1406
+ if (leftRemaining === null && rightRemaining === null) {
1407
+ return 0;
1408
+ }
1409
+ if (leftRemaining === null) {
1410
+ return 1;
1411
+ }
1412
+ if (rightRemaining === null) {
1413
+ return -1;
1414
+ }
1415
+ return rightRemaining - leftRemaining;
1416
+ });
1417
+ }
1418
+
1419
+ function formatCdxValue(key, value) {
1420
+ if (["reset_at", "resetAt", "resets_at", "resetsAt", "reset_5h_at", "reset5hAt", "reset_week_at", "resetWeekAt", "updated_at", "updatedAt"].includes(key)) {
1421
+ return formatCdxResetAt(value);
1422
+ }
1423
+ if (typeof value === "object") {
1424
+ return JSON.stringify(value);
1425
+ }
1426
+ return value;
1427
+ }
1428
+
1429
+ function parseCdxDate(value) {
1430
+ const raw = String(value || "").trim();
1431
+ if (!raw) {
1432
+ return null;
1433
+ }
1434
+ const shortDate = raw.match(/^([A-Za-z]{3,})\s+(\d{1,2})\s+(\d{1,2}:\d{2})$/);
1435
+ if (shortDate) {
1436
+ const year = new Date().getFullYear();
1437
+ const timestamp = Date.parse(`${shortDate[1]} ${shortDate[2]} ${year} ${shortDate[3]}`);
1438
+ return Number.isFinite(timestamp) ? timestamp : null;
1439
+ }
1440
+ const timestamp = Date.parse(raw);
1441
+ if (Number.isFinite(timestamp)) {
1442
+ return timestamp;
1443
+ }
1444
+ return null;
1445
+ }
1446
+
1447
+ function formatRelativeTime(timestamp) {
1448
+ const diffMs = timestamp - Date.now();
1449
+ const absMs = Math.abs(diffMs);
1450
+ const minutes = Math.round(absMs / 60000);
1451
+ if (minutes < 1) {
1452
+ return diffMs >= 0 ? "now" : "just now";
1453
+ }
1454
+ const hours = Math.floor(minutes / 60);
1455
+ const days = Math.floor(hours / 24);
1456
+ const remainingHours = hours % 24;
1457
+ const remainingMinutes = minutes % 60;
1458
+ let body = "";
1459
+ if (days > 0) {
1460
+ body = `${days}d${remainingHours > 0 ? ` ${remainingHours}h` : ""}`;
1461
+ } else if (hours > 0) {
1462
+ body = `${hours}h${remainingMinutes > 0 ? ` ${remainingMinutes}m` : ""}`;
1463
+ } else {
1464
+ body = `${minutes}m`;
1465
+ }
1466
+ return diffMs >= 0 ? `in ${body}` : `${body} ago`;
1467
+ }
1468
+
1469
+ function formatCdxResetAt(value) {
1470
+ const raw = String(value || "").trim();
1471
+ if (!raw) {
1472
+ return "-";
1473
+ }
1474
+ const timestamp = parseCdxDate(raw);
1475
+ return timestamp === null ? raw : formatRelativeTime(timestamp);
1476
+ }
1477
+
1478
+ function formatCdxCredits(value) {
1479
+ const text = String(value ?? "").trim();
1480
+ if (!text || text === "-") {
1481
+ return "-";
1482
+ }
1483
+ const number = Number(text);
1484
+ return Number.isFinite(number) ? number.toFixed(2) : text;
1485
+ }
1486
+
1487
+ function renderCdxBadge(value, fallback = "reported") {
1488
+ const label = String(value || fallback || "reported");
1489
+ return `<span class="viewer-cdx__badge viewer-cdx__badge--${cdxStateClass(label)}">${escapeHtml(cdxLabel(label))}</span>`;
1490
+ }
1491
+
1492
+ function cdxDetailEntries(item, excludedKeys) {
1493
+ return objectEntries(item)
1494
+ .filter(([key, value]) => !excludedKeys.includes(key) && value !== undefined && value !== null && value !== "")
1495
+ .slice(0, 6);
1496
+ }
1497
+
1498
+ function renderCdxDetailPills(item, excludedKeys) {
1499
+ const details = cdxDetailEntries(item, excludedKeys).map(([key, value]) => `
1500
+ <span class="viewer-cdx__pill"><span>${escapeHtml(cdxLabel(key))}</span><strong>${escapeHtml(formatCdxValue(key, value))}</strong></span>
1501
+ `).join("");
1502
+ return details ? `<div class="viewer-cdx__pills">${details}</div>` : "";
1503
+ }
1504
+
1505
+ function renderCdxRemainingPill(item) {
1506
+ const percent = cdxRemainingPct(item);
1507
+ if (percent === null) {
1508
+ return "";
1509
+ }
1510
+ return `
1511
+ <span class="viewer-cdx__remaining viewer-cdx__remaining--${cdxRemainingClass(percent)}" title="${escapeHtml(percent)}% usage remaining">
1512
+ <span>Remaining</span>
1513
+ <strong>${escapeHtml(percent)}%</strong>
1514
+ </span>
1515
+ `;
1516
+ }
1517
+
1518
+ function cdxSessionBlock(item) {
1519
+ const explicit = cdxField(item, ["block", "blocked", "blocking"], "");
1520
+ if (explicit && explicit !== true) {
1521
+ return explicit;
1522
+ }
1523
+ const fiveHour = Number(cdxField(item, ["remaining_5h_pct", "remaining5hPct"], NaN));
1524
+ const week = Number(cdxField(item, ["remaining_week_pct", "remainingWeekPct"], NaN));
1525
+ if (Number.isFinite(fiveHour) && fiveHour <= 0) {
1526
+ return "5H";
1527
+ }
1528
+ if (Number.isFinite(week) && week <= 1) {
1529
+ return "WEEK";
1530
+ }
1531
+ return explicit === true ? "YES" : "-";
1532
+ }
1533
+
1534
+ function renderCdxSessionTable(sessions, emptyText) {
1535
+ if (!sessions.length) {
1536
+ return `<div class="viewer-cdx__empty">${escapeHtml(emptyText)}</div>`;
1537
+ }
1538
+ const rows = sessions.slice(0, 24).map((entry) => {
1539
+ const item = entry && typeof entry === "object" ? entry : { value: entry };
1540
+ const name = cdxField(item, ["session_name", "name", "id", "value"]);
1541
+ const sessionName = `${name}${item.active ? "*" : ""}`;
1542
+ const status = cdxField(item, ["status", "state"]);
1543
+ const auth = String(cdxField(item, ["auth_status", "authStatus"], "-")).replace("authenticated", "logged");
1544
+ const block = cdxSessionBlock(item);
1545
+ return `
1546
+ <tr>
1547
+ <td class="viewer-cdx__session-name">${escapeHtml(sessionName)}</td>
1548
+ <td>${escapeHtml(cdxField(item, ["provider"], "-"))}</td>
1549
+ <td>${renderCdxBadge(status)}</td>
1550
+ <td>${escapeHtml(auth)}</td>
1551
+ <td>${renderCdxRemainingPill(item) || escapeHtml(cdxPct(cdxField(item, ["available_pct", "availablePct"], NaN)))}</td>
1552
+ <td>${escapeHtml(cdxPct(cdxField(item, ["remaining_5h_pct", "remaining5hPct"], NaN)))}</td>
1553
+ <td>${escapeHtml(cdxPct(cdxField(item, ["remaining_week_pct", "remainingWeekPct"], NaN)))}</td>
1554
+ <td>${escapeHtml(block)}</td>
1555
+ <td>${escapeHtml(formatCdxCredits(cdxField(item, ["credits", "cr"], "-")))}</td>
1556
+ <td>${escapeHtml(formatCdxResetAt(cdxField(item, ["reset_5h_at", "reset5hAt", "reset_at", "resetAt"], "")))}</td>
1557
+ <td>${escapeHtml(formatCdxResetAt(cdxField(item, ["reset_week_at", "resetWeekAt", "reset_at", "resetAt"], "")))}</td>
1558
+ <td>${escapeHtml(formatCdxResetAt(cdxField(item, ["updated_at", "updatedAt"], "")))}</td>
1559
+ </tr>
1560
+ `;
1561
+ }).join("");
1562
+ return `
1563
+ <div class="viewer-cdx__table-wrap">
1564
+ <table class="viewer-cdx__table">
1565
+ <thead>
1566
+ <tr>
1567
+ <th>SESSION</th>
1568
+ <th>PROV.</th>
1569
+ <th>STATUS</th>
1570
+ <th>AUTH</th>
1571
+ <th>OK</th>
1572
+ <th>5H</th>
1573
+ <th>WEEK</th>
1574
+ <th>BLOCK</th>
1575
+ <th>CR</th>
1576
+ <th>RESET 5H</th>
1577
+ <th>RESET WEEK</th>
1578
+ <th>UPDATED</th>
1579
+ </tr>
1580
+ </thead>
1581
+ <tbody>${rows}</tbody>
1582
+ </table>
1583
+ </div>
1584
+ `;
1585
+ }
1586
+
1587
+ function renderCdxEntityRows(entries, emptyText, options = {}) {
1588
+ const titleKeys = options.titleKeys || ["name", "session_name", "id", "provider", "model", "value"];
1589
+ const stateKeys = options.stateKeys || ["state", "status", "readiness", "available", "auth_status"];
1590
+ const excludedKeys = [...titleKeys, ...stateKeys, "available_pct", "availablePct", "remaining_pct", "remainingPct", "lowest_available_pct", "lowestAvailablePct"];
1591
+ const rows = entries.slice(0, 16).map((entry) => {
1592
+ const item = entry && typeof entry === "object" ? entry : { value: entry };
1593
+ const name = titleKeys.map((key) => item[key]).find(Boolean) || "entry";
1594
+ const state = stateKeys.map((key) => item[key]).find((value) => value !== undefined && value !== null && value !== "") || "";
1595
+ const subtitle = options.subtitleKeys
1596
+ ? options.subtitleKeys.map((key) => item[key]).filter(Boolean).join(" · ")
1597
+ : "";
1598
+ return `
1599
+ <li class="viewer-cdx__entity">
1600
+ <div class="viewer-cdx__entity-main">
1601
+ <div>
1602
+ <strong>${escapeHtml(name)}</strong>
1603
+ ${subtitle ? `<div class="viewer-cdx__meta">${escapeHtml(subtitle)}</div>` : ""}
1604
+ </div>
1605
+ <div class="viewer-cdx__entity-status">
1606
+ ${renderCdxRemainingPill(item)}
1607
+ ${renderCdxBadge(state)}
1608
+ </div>
1609
+ </div>
1610
+ ${renderCdxDetailPills(item, excludedKeys)}
1611
+ </li>
1612
+ `;
1613
+ }).join("");
1614
+ return rows || `<li class="viewer-cdx__empty">${escapeHtml(emptyText)}</li>`;
1615
+ }
1616
+
1617
+ function renderCdxStatus(payload) {
1618
+ if (!payload || payload.state !== "ok") {
1619
+ return `
1620
+ <div class="viewer-cdx">
1621
+ <div class="viewer-cdx__state">${escapeHtml(payload?.message || "CDX status is unavailable.")}</div>
1622
+ </div>
1623
+ `;
1624
+ }
1625
+ const status = payload.status || {};
1626
+ const providers = cdxProviders(status);
1627
+ const sessions = cdxSessions(status);
1628
+ const readiness = cdxReadiness(status);
1629
+ const commands = pickFirstArray(status, ["nextCommands", "next_commands", "safeCommands", "safe_commands", "commands"])
1630
+ .map((entry) => typeof entry === "string" ? entry : (entry.command || entry.value || entry.name || ""))
1631
+ .filter(Boolean);
1632
+ if (!commands.length) {
1633
+ commands.push("cdx status --json");
1634
+ }
1635
+ const runtimeState = status.state || status.status || status.availability || "ok";
1636
+ const readinessCount = objectEntries(readiness).length;
1637
+ const cards = [
1638
+ ["Runtime", runtimeState],
1639
+ ["Providers", providers.length],
1640
+ ["Sessions", sessions.length],
1641
+ ["Readiness", readinessCount ? `${readinessCount} signals` : "Not reported"]
1642
+ ].map(([label, value]) => `
1643
+ <div class="viewer-cdx__card">
1644
+ <div class="viewer-cdx__label">${escapeHtml(label)}</div>
1645
+ <div class="viewer-cdx__value">${label === "Runtime" ? renderCdxBadge(value) : escapeHtml(value)}</div>
1646
+ </div>
1647
+ `).join("");
1648
+ const commandRows = commands.slice(0, 10).map((command, index) => `
1649
+ <li>
1650
+ <span>${escapeHtml(index + 1)}</span>
1651
+ <code>${escapeHtml(command)}</code>
1652
+ </li>
1653
+ `).join("");
1654
+ return `
1655
+ <div class="viewer-cdx">
1656
+ <div class="viewer-cdx__summary">${cards}</div>
1657
+ <div class="viewer-cdx__workspace">
1658
+ <div class="viewer-cdx__stack">
1659
+ <section class="viewer-cdx__section">
1660
+ <h2 class="viewer-cdx__heading">Sessions</h2>
1661
+ ${renderCdxSessionTable(sessions, "No sessions reported.")}
1662
+ </section>
1663
+ <section class="viewer-cdx__section">
1664
+ <h2 class="viewer-cdx__heading">Providers</h2>
1665
+ <ul class="viewer-cdx__list">${renderCdxEntityRows(providers, "No provider status reported.", { subtitleKeys: ["model"] })}</ul>
1666
+ </section>
1667
+ </div>
1668
+ <div class="viewer-cdx__stack">
1669
+ <section class="viewer-cdx__section">
1670
+ <h2 class="viewer-cdx__heading">Readiness and quota</h2>
1671
+ <ul class="viewer-cdx__list">${renderCdxObjectRows(readiness, "No readiness or quota details reported.")}</ul>
1672
+ </section>
1673
+ <section class="viewer-cdx__section">
1674
+ <h2 class="viewer-cdx__heading">Safe next commands</h2>
1675
+ <ul class="viewer-cdx__commands">${commandRows || '<li class="viewer-cdx__empty">No suggested commands reported.</li>'}</ul>
1676
+ </section>
1677
+ </div>
1678
+ </div>
1679
+ </div>
1680
+ `;
1681
+ }
1682
+
1683
+ async function showCdxStatus(options = {}) {
1684
+ if (!options.silent) {
1685
+ setMeta("Checking CDX status...");
1686
+ }
1687
+ const response = await fetch("/api/cdx-status");
1688
+ let data = {};
1689
+ try {
1690
+ data = await response.json();
1691
+ } catch {
1692
+ data = {};
1693
+ }
1694
+ if (response.status === 404) {
1695
+ setDocument("CDX status", renderCdxStatus({
1696
+ state: "unavailable",
1697
+ message: "CDX status endpoint unavailable. Restart the local viewer so it loads the current logics-manager backend."
1698
+ }));
1699
+ setMeta("Restart the local viewer to enable CDX status.");
1700
+ return;
1701
+ }
1702
+ if (!response.ok || !data.ok) {
1703
+ throw new Error(data.error || "Unable to load CDX status.");
1704
+ }
1705
+ setDocument("CDX status", renderCdxStatus(data.payload));
1706
+ setMeta(options.silent ? "CDX status refreshed." : "CDX status loaded.");
1707
+ }
1708
+
1709
+ function renderGitStatus(payload) {
1710
+ if (!payload || payload.state !== "ok") {
1711
+ return `
1712
+ <div class="viewer-git">
1713
+ <div class="viewer-git__state">${escapeHtml(payload?.message || "Git status is unavailable.")}</div>
1714
+ </div>
1715
+ `;
1716
+ }
1717
+ const counts = payload.counts || {};
1718
+ const stagedCount = Number(counts.staged || 0);
1719
+ const modifiedCount = Number(counts.modified || 0);
1720
+ const deletedCount = Number(counts.deleted || 0);
1721
+ const renamedCount = Number(counts.renamed || 0);
1722
+ const untrackedCount = Number(counts.untracked || 0);
1723
+ const summary = [
1724
+ ["Branch", payload.branch || "HEAD"],
1725
+ ["Tracking", payload.tracking || "None"],
1726
+ ["Ahead", payload.ahead || 0],
1727
+ ["Behind", payload.behind || 0],
1728
+ ["State", payload.clean ? "Clean" : "Dirty"],
1729
+ ["Staged", stagedCount],
1730
+ ["Worktree", modifiedCount + deletedCount + renamedCount],
1731
+ ["Untracked", untrackedCount]
1732
+ ];
1733
+ const cards = renderMetricCards(summary);
1734
+ const groupDefs = [
1735
+ ["staged", "Staged", "staged"],
1736
+ ["modified", "Modified", "worktree"],
1737
+ ["deleted", "Deleted", "worktree"],
1738
+ ["renamed", "Renamed", "worktree"],
1739
+ ["untracked", "Untracked", "untracked"]
1740
+ ];
1741
+ const domainDefs = [
1742
+ ["changes", "Changes", stagedCount + modifiedCount + deletedCount + renamedCount + untrackedCount],
1743
+ ["staged", "Staged", stagedCount],
1744
+ ["worktree", "Worktree", modifiedCount + deletedCount + renamedCount],
1745
+ ["untracked", "Untracked", untrackedCount],
1746
+ ["history", "History", Array.isArray(payload.recentCommits) ? payload.recentCommits.length : (payload.latestCommit ? 1 : 0)],
1747
+ ["remote", "Remote", payload.tracking ? 1 : 0]
1748
+ ];
1749
+ const domains = domainDefs.map(([key, label, count], index) => `
1750
+ <button class="viewer-git__domain${index === 0 ? " is-active" : ""}" type="button" data-viewer-git-domain="${escapeHtml(key)}" aria-pressed="${index === 0 ? "true" : "false"}">
1751
+ <span class="viewer-git__domain-label">${escapeHtml(label)}${key === "changes" ? gitBadgeHtml("changes") : ""}${key === "history" ? gitBadgeHtml("history") : ""}</span><strong>${escapeHtml(count)}</strong>
1752
+ </button>
1753
+ `).join("");
1754
+ const renderFileSections = (allowedKeys) => groupDefs.filter(([key]) => allowedKeys.includes(key)).map(([key, label]) => {
1755
+ const entries = Array.isArray(payload.groups?.[key]) ? payload.groups[key] : [];
1756
+ if (!entries.length) {
1757
+ return "";
1758
+ }
1759
+ return `
1760
+ <section class="viewer-git__section">
1761
+ <h2>${escapeHtml(label)}</h2>
1762
+ <ul class="viewer-git__files">${entries.map((entry) => `
1763
+ <li>
1764
+ <button class="viewer-git__file" type="button" data-viewer-git-file="${escapeHtml(entry.path)}" data-viewer-git-cached="${key === "staged" ? "1" : "0"}">
1765
+ <span class="viewer-git__file-path">${escapeHtml(entry.from ? `${entry.from} -> ${entry.path}` : entry.path)}</span>
1766
+ ${entry.logicsType ? `<span class="viewer-git__file-kind">${escapeHtml(entry.logicsType)}</span>` : ""}
1767
+ </button>
1768
+ </li>
1769
+ `).join("")}</ul>
1770
+ </section>
1771
+ `;
1772
+ }).join("");
1773
+ const changesSections = renderFileSections(["staged", "modified", "deleted", "renamed", "untracked"]);
1774
+ const stagedSections = renderFileSections(["staged"]);
1775
+ const worktreeSections = renderFileSections(["modified", "deleted", "renamed"]);
1776
+ const untrackedSections = renderFileSections(["untracked"]);
1777
+ const clean = payload.clean ? '<p class="viewer-git__state">Working tree clean.</p>' : "";
1778
+ const recentCommits = Array.isArray(payload.recentCommits) ? payload.recentCommits : [];
1779
+ const historyRows = recentCommits.length
1780
+ ? recentCommits.map((commit) => `
1781
+ <li class="viewer-git__commit-row">
1782
+ <div class="viewer-git__commit-main">
1783
+ <code>${escapeHtml(commit.hash || "")}</code>
1784
+ <strong>${escapeHtml(commit.subject || "Untitled commit")}</strong>
1785
+ </div>
1786
+ <div class="viewer-git__commit-meta">
1787
+ <span>${escapeHtml([commit.author, commit.date].filter(Boolean).join(" · ") || "Unknown")}</span>
1788
+ ${commit.refs ? `<span class="viewer-git__commit-refs">${escapeHtml(commit.refs)}</span>` : ""}
1789
+ </div>
1790
+ </li>
1791
+ `).join("")
1792
+ : `<li class="viewer-git__commit-row">${escapeHtml(payload.latestCommit || "No commit history available.")}</li>`;
1793
+ const history = `
1794
+ <section class="viewer-git__section">
1795
+ <h2>History</h2>
1796
+ <ul class="viewer-git__commits">${historyRows}</ul>
1797
+ </section>
1798
+ `;
1799
+ const remote = `
1800
+ <section class="viewer-git__section">
1801
+ <h2>Remote</h2>
1802
+ <p class="viewer-git__state">${escapeHtml(payload.tracking ? `Tracking ${payload.tracking}` : "No upstream branch detected.")}</p>
1803
+ <p class="viewer-git__state">${escapeHtml(`Ahead ${payload.ahead || 0}, behind ${payload.behind || 0}`)}</p>
1804
+ </section>
1805
+ `;
1806
+ return `
1807
+ <div class="viewer-git">
1808
+ <div class="viewer-git__summary">${cards}</div>
1809
+ <div class="viewer-git__workspace">
1810
+ <nav class="viewer-git__domains" aria-label="Git domains">${domains}</nav>
1811
+ <div class="viewer-git__content" aria-label="Git domain content">
1812
+ <section class="viewer-git__panel" data-viewer-git-panel="changes">
1813
+ <header class="viewer-git__panel-header"><span>Changes</span><strong>${escapeHtml(stagedCount + modifiedCount + deletedCount + renamedCount + untrackedCount)} files</strong></header>
1814
+ ${clean}
1815
+ ${changesSections || '<p class="viewer-git__state">No file changes detected.</p>'}
1816
+ </section>
1817
+ <section class="viewer-git__panel" data-viewer-git-panel="staged" hidden>
1818
+ <header class="viewer-git__panel-header"><span>Staged</span><strong>${escapeHtml(stagedCount)} files</strong></header>
1819
+ ${stagedSections || '<p class="viewer-git__state">No staged files.</p>'}
1820
+ </section>
1821
+ <section class="viewer-git__panel" data-viewer-git-panel="worktree" hidden>
1822
+ <header class="viewer-git__panel-header"><span>Worktree</span><strong>${escapeHtml(modifiedCount + deletedCount + renamedCount)} files</strong></header>
1823
+ ${worktreeSections || '<p class="viewer-git__state">No modified, deleted, or renamed files.</p>'}
1824
+ </section>
1825
+ <section class="viewer-git__panel" data-viewer-git-panel="untracked" hidden>
1826
+ <header class="viewer-git__panel-header"><span>Untracked</span><strong>${escapeHtml(untrackedCount)} files</strong></header>
1827
+ ${untrackedSections || '<p class="viewer-git__state">No untracked files.</p>'}
1828
+ </section>
1829
+ <section class="viewer-git__panel" data-viewer-git-panel="history" hidden>
1830
+ <header class="viewer-git__panel-header"><span>History</span><strong>${escapeHtml(recentCommits.length || (payload.latestCommit ? 1 : 0))} commits</strong></header>
1831
+ ${history}
1832
+ </section>
1833
+ <section class="viewer-git__panel" data-viewer-git-panel="remote" hidden>
1834
+ <header class="viewer-git__panel-header"><span>Remote</span><strong>${escapeHtml(payload.tracking || "none")}</strong></header>
1835
+ ${remote}
1836
+ </section>
1837
+ </div>
1838
+ <section class="viewer-git__detail" aria-label="Git diff">
1839
+ <div class="viewer-git__detail-title">Diff preview</div>
1840
+ <div class="viewer-git__diff" data-viewer-git-diff>Select a changed file to preview its diff.</div>
1841
+ </section>
1842
+ </div>
1843
+ </div>
1844
+ `;
1845
+ }
1846
+
1847
+ function setActiveGitFile(button) {
1848
+ document.querySelectorAll("[data-viewer-git-file]").forEach((node) => {
1849
+ if (node instanceof HTMLElement) {
1850
+ node.classList.toggle("is-active", node === button);
1851
+ }
1852
+ });
1853
+ }
1854
+
1855
+ async function loadGitDiff(path, cached, button = null) {
1856
+ const diffPanel = document.querySelector("[data-viewer-git-diff]");
1857
+ if (!(diffPanel instanceof HTMLElement) || !path) {
1858
+ return;
1859
+ }
1860
+ if (button instanceof HTMLElement) {
1861
+ setActiveGitFile(button);
1862
+ }
1863
+ diffPanel.textContent = "Loading diff...";
1864
+ const params = new URLSearchParams({ path });
1865
+ if (cached) {
1866
+ params.set("cached", "1");
1867
+ }
1868
+ const response = await fetch(`/api/git-diff?${params.toString()}`);
1869
+ const data = await response.json();
1870
+ const payload = data.payload || {};
1871
+ if (!response.ok || !data.ok || payload.state !== "ok") {
1872
+ diffPanel.textContent = payload.message || data.error || "Unable to load diff.";
1873
+ return;
1874
+ }
1875
+ const content = payload.diff || payload.message || "No diff is available for this file.";
1876
+ diffPanel.innerHTML = `<div class="viewer-git__diff-meta">${escapeHtml(payload.path || path)} · ${escapeHtml(payload.mode || "worktree")}${payload.truncated ? " · truncated" : ""}</div><pre><code>${escapeHtml(content)}</code></pre>`;
1877
+ }
1878
+
1879
+ function applyGitDomain(domain) {
1880
+ const selected = domain || "changes";
1881
+ document.querySelectorAll(".viewer-git__domain[data-viewer-git-domain]").forEach((node) => {
1882
+ if (node instanceof HTMLElement) {
1883
+ const active = node.getAttribute("data-viewer-git-domain") === selected;
1884
+ node.classList.toggle("is-active", active);
1885
+ node.setAttribute("aria-pressed", active ? "true" : "false");
1886
+ }
1887
+ });
1888
+ document.querySelectorAll("[data-viewer-git-panel]").forEach((node) => {
1889
+ if (node instanceof HTMLElement) {
1890
+ node.hidden = node.getAttribute("data-viewer-git-panel") !== selected;
1891
+ }
1892
+ });
1893
+ }
1894
+
1895
+ function currentGitViewState() {
1896
+ const activeDomain = document.querySelector(".viewer-git__domain.is-active[data-viewer-git-domain]");
1897
+ const activeFile = document.querySelector(".viewer-git__file.is-active[data-viewer-git-file]");
1898
+ return {
1899
+ domain: activeDomain instanceof HTMLElement ? activeDomain.getAttribute("data-viewer-git-domain") || "changes" : "changes",
1900
+ path: activeFile instanceof HTMLElement ? activeFile.getAttribute("data-viewer-git-file") || "" : "",
1901
+ cached: activeFile instanceof HTMLElement && activeFile.getAttribute("data-viewer-git-cached") === "1",
1902
+ };
1903
+ }
1904
+
1905
+ function findGitFileButton(path, cached) {
1906
+ return Array.from(document.querySelectorAll("[data-viewer-git-file]")).find((node) => (
1907
+ node instanceof HTMLElement &&
1908
+ node.getAttribute("data-viewer-git-file") === path &&
1909
+ (node.getAttribute("data-viewer-git-cached") === "1") === Boolean(cached)
1910
+ )) || null;
1911
+ }
1912
+
1913
+ async function showGitStatus(options = {}) {
1914
+ const previous = options.preserve ? currentGitViewState() : { domain: "changes", path: "", cached: false };
1915
+ if (!options.silent) {
1916
+ setMeta("Checking Git status...");
1917
+ }
1918
+ const response = await fetch("/api/git-status");
1919
+ let data = {};
1920
+ try {
1921
+ data = await response.json();
1922
+ } catch {
1923
+ data = {};
1924
+ }
1925
+ if (response.status === 404) {
1926
+ setDocument("Git status", renderGitStatus({
1927
+ state: "unavailable",
1928
+ message: "Git status endpoint unavailable. Restart the local viewer so it loads the current logics-manager backend."
1929
+ }));
1930
+ setMeta("Restart the local viewer to enable Git status.");
1931
+ return;
1932
+ }
1933
+ if (!response.ok || !data.ok) {
1934
+ throw new Error(data.error || "Unable to load Git status.");
1935
+ }
1936
+ setGitBadgeCountsFromPayload(data.payload, { updateMain: false });
1937
+ updateMainGitBadges();
1938
+ setDocument("Git status", renderGitStatus(data.payload));
1939
+ applyGitDomain(previous.domain || "changes");
1940
+ const restoredFile = previous.path ? findGitFileButton(previous.path, previous.cached) : null;
1941
+ const firstFile = restoredFile || document.querySelector("[data-viewer-git-file]");
1942
+ if (firstFile instanceof HTMLElement) {
1943
+ await loadGitDiff(firstFile.getAttribute("data-viewer-git-file") || "", firstFile.getAttribute("data-viewer-git-cached") === "1", firstFile);
1944
+ }
1945
+ setMeta(options.silent ? "Git status refreshed." : "Git status loaded.");
1946
+ }
1947
+
747
1948
  window.acquireVsCodeApi = function acquireVsCodeApi() {
748
1949
  return {
749
1950
  postMessage(message) {
@@ -755,7 +1956,7 @@
755
1956
  return;
756
1957
  }
757
1958
  if (message.type === "refresh") {
758
- loadItems("POST").catch((error) => setMeta(error.message));
1959
+ refreshViewer("POST").catch((error) => setMeta(error.message));
759
1960
  return;
760
1961
  }
761
1962
  if (message.type === "open" || message.type === "read") {
@@ -786,22 +1987,68 @@
786
1987
  setControlValue("show-companion-docs", true, "change");
787
1988
  setControlValue("hide-empty-columns", true, "change");
788
1989
  applyLocalViewerChrome();
789
- [document.getElementById("viewer-insights"), document.getElementById("header-logics-insights")].forEach((button) => {
1990
+ [document.getElementById("viewer-insights")].forEach((button) => {
790
1991
  button?.addEventListener("click", () => {
791
- showCorpusInsights();
1992
+ showCorpusInsights().catch((error) => setMeta(error.message));
1993
+ });
1994
+ });
1995
+ const autoControl = autoRefreshControl();
1996
+ if (autoControl instanceof HTMLInputElement) {
1997
+ autoControl.addEventListener("change", () => {
1998
+ setAutoRefreshEnabled(autoControl.checked);
1999
+ });
2000
+ setAutoRefreshEnabled(autoControl.checked);
2001
+ }
2002
+ const intervalControl = refreshIntervalControl();
2003
+ if (intervalControl instanceof HTMLSelectElement) {
2004
+ updateRefreshIntervalControl();
2005
+ intervalControl.addEventListener("change", () => {
2006
+ setAutoRefreshIntervalSeconds(intervalControl.value, { user: true });
792
2007
  });
2008
+ }
2009
+ bindRefreshMenuControls();
2010
+ document.addEventListener("click", (event) => {
2011
+ const target = event.target;
2012
+ const button = refreshMenuButton();
2013
+ const panel = refreshMenuPanel();
2014
+ try {
2015
+ if (target && (
2016
+ button?.contains(target) ||
2017
+ panel?.contains(target)
2018
+ )) {
2019
+ return;
2020
+ }
2021
+ } catch {
2022
+ // Ignore non-node event targets and close the menu below.
2023
+ }
2024
+ setRefreshMenuOpen(false);
2025
+ });
2026
+ document.addEventListener("keydown", (event) => {
2027
+ if (event.key === "Escape") {
2028
+ setRefreshMenuOpen(false);
2029
+ }
793
2030
  });
794
2031
  document.querySelectorAll('[data-action="refresh"]').forEach((element) => {
795
2032
  if (!(element instanceof HTMLElement)) {
796
2033
  return;
797
2034
  }
798
2035
  element.addEventListener("click", () => {
799
- loadItems("POST").catch((error) => setMeta(error.message));
2036
+ setRefreshMenuOpen(false);
2037
+ refreshViewer("POST").catch((error) => setMeta(error.message));
800
2038
  });
801
2039
  });
802
2040
  document.getElementById("viewer-health")?.addEventListener("click", () => {
803
2041
  showHealth().catch((error) => setMeta(error.message));
804
2042
  });
2043
+ document.getElementById("viewer-git")?.addEventListener("click", () => {
2044
+ showGitStatus().catch((error) => setMeta(error.message));
2045
+ });
2046
+ document.getElementById("viewer-cdx")?.addEventListener("click", () => {
2047
+ showCdxStatus().catch((error) => setMeta(error.message));
2048
+ });
2049
+ activityClearControl()?.addEventListener("click", () => {
2050
+ clearActivityHistory();
2051
+ });
805
2052
  document.querySelectorAll("[data-viewer-filter-group]").forEach((element) => {
806
2053
  if (element instanceof HTMLSelectElement) {
807
2054
  element.addEventListener("change", () => {
@@ -828,10 +2075,44 @@
828
2075
  document.addEventListener("click", (event) => {
829
2076
  window.setTimeout(() => applyLocalViewerChrome(), 0);
830
2077
  const target = event.target instanceof Element ? event.target.closest("[data-viewer-doc-path]") : null;
831
- if (!(target instanceof HTMLElement)) {
2078
+ const healthTarget = event.target instanceof Element ? event.target.closest("[data-viewer-open-health]") : null;
2079
+ const filterTarget = event.target instanceof Element ? event.target.closest("[data-viewer-filter-group][data-viewer-filter-value]") : null;
2080
+ const revealTarget = event.target instanceof Element ? event.target.closest("[data-viewer-reveal]") : null;
2081
+ const gitDomainTarget = event.target instanceof Element ? event.target.closest(".viewer-git__domain[data-viewer-git-domain]") : null;
2082
+ const gitFileTarget = event.target instanceof Element ? event.target.closest("[data-viewer-git-file]") : null;
2083
+ if (revealTarget instanceof HTMLElement) {
2084
+ const list = revealTarget.closest("ul");
2085
+ list?.querySelectorAll("[data-viewer-hidden-row]").forEach((row) => {
2086
+ if (row instanceof HTMLElement) {
2087
+ row.hidden = false;
2088
+ row.removeAttribute("data-viewer-hidden-row");
2089
+ }
2090
+ });
2091
+ revealTarget.closest("li")?.remove();
2092
+ return;
2093
+ }
2094
+ if (gitDomainTarget instanceof HTMLElement) {
2095
+ applyGitDomain(gitDomainTarget.getAttribute("data-viewer-git-domain") || "changes");
2096
+ return;
2097
+ }
2098
+ if (gitFileTarget instanceof HTMLElement) {
2099
+ loadGitDiff(
2100
+ gitFileTarget.getAttribute("data-viewer-git-file") || "",
2101
+ gitFileTarget.getAttribute("data-viewer-git-cached") === "1",
2102
+ gitFileTarget
2103
+ ).catch((error) => setMeta(error.message));
2104
+ return;
2105
+ }
2106
+ if (healthTarget instanceof HTMLElement) {
2107
+ showHealth().catch((error) => setMeta(error.message));
2108
+ return;
2109
+ }
2110
+ if (filterTarget instanceof HTMLElement) {
2111
+ applyViewerFilter(filterTarget.getAttribute("data-viewer-filter-group") || "", filterTarget.getAttribute("data-viewer-filter-value") || "");
2112
+ setMeta("Insight filter applied. Clear filters restores the normal viewer view.");
832
2113
  return;
833
2114
  }
834
- const path = target.getAttribute("data-viewer-doc-path");
2115
+ const path = target instanceof HTMLElement ? target.getAttribute("data-viewer-doc-path") : "";
835
2116
  if (path) {
836
2117
  showDocumentByPath(path).catch((error) => setMeta(error.message));
837
2118
  }