@grifhinz/logics-manager 2.5.2 → 2.7.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.
- package/VERSION +1 -1
- package/clients/shared-web/media/css/layout.css +6 -0
- package/clients/viewer/browser-host.js +898 -68
- package/clients/viewer/index.html +48 -22
- package/clients/viewer/viewer.css +536 -8
- package/logics_manager/assist.py +1 -0
- package/logics_manager/flow.py +6 -2
- package/logics_manager/lint.py +11 -7
- package/logics_manager/viewer.py +603 -14
- package/package.json +1 -1
- package/pyproject.toml +1 -1
|
@@ -8,14 +8,22 @@
|
|
|
8
8
|
const updateBanner = () => document.getElementById("viewer-update");
|
|
9
9
|
const updateCopy = () => document.getElementById("viewer-update-copy");
|
|
10
10
|
const updateCommand = () => document.getElementById("viewer-update-command");
|
|
11
|
+
const connectionBanner = () => document.getElementById("viewer-connection");
|
|
12
|
+
const connectionCopy = () => document.getElementById("viewer-connection-copy");
|
|
13
|
+
const connectionDetail = () => document.getElementById("viewer-connection-detail");
|
|
11
14
|
const filterCount = () => document.getElementById("viewer-filter-count");
|
|
12
15
|
const repoPill = () => document.getElementById("viewer-repo-pill");
|
|
16
|
+
const projectMenu = () => document.getElementById("viewer-project-menu");
|
|
17
|
+
const repoGithubLink = () => document.getElementById("viewer-repo-github");
|
|
18
|
+
const repoFolderButton = () => document.getElementById("viewer-repo-folder");
|
|
19
|
+
const ciButton = () => document.getElementById("viewer-ci");
|
|
13
20
|
const autoRefreshControl = () => document.getElementById("viewer-auto-refresh");
|
|
14
21
|
const refreshIntervalControl = () => document.getElementById("viewer-refresh-interval");
|
|
15
22
|
const refreshMenuButton = () => document.getElementById("viewer-refresh-menu-button");
|
|
16
23
|
const refreshMenuPanel = () => document.getElementById("viewer-refresh-menu");
|
|
17
24
|
const activityClearControl = () => document.getElementById("activity-clear");
|
|
18
25
|
const activityStorageLimit = 80;
|
|
26
|
+
const gitHistoryPageSize = 10;
|
|
19
27
|
const minAutoRefreshIntervalSeconds = 5;
|
|
20
28
|
const maxAutoRefreshIntervalSeconds = 60;
|
|
21
29
|
const defaultAutoRefreshIntervalMs = 15 * 1000;
|
|
@@ -29,6 +37,9 @@
|
|
|
29
37
|
let viewerFilterState = { ...defaultFilterState };
|
|
30
38
|
let latestItems = [];
|
|
31
39
|
let latestRepoRoot = "";
|
|
40
|
+
let latestRepository = { root: "", githubUrl: "" };
|
|
41
|
+
let latestCapabilities = {};
|
|
42
|
+
let latestProjects = [];
|
|
32
43
|
let latestMetaText = "Read-only local viewer";
|
|
33
44
|
let autoRefreshIntervalMs = defaultAutoRefreshIntervalMs;
|
|
34
45
|
let nextAutoRefreshAt = 0;
|
|
@@ -42,6 +53,9 @@
|
|
|
42
53
|
let mermaidInitialized = false;
|
|
43
54
|
let focusApplied = false;
|
|
44
55
|
let latestGitBadgeCounts = { unpushedCommits: 0, uncommittedFiles: 0 };
|
|
56
|
+
let latestCiStatus = { visible: false, badgeState: "unknown", message: "" };
|
|
57
|
+
let connectionState = "connected";
|
|
58
|
+
let lastSuccessfulSyncAt = 0;
|
|
45
59
|
|
|
46
60
|
function readStoredState() {
|
|
47
61
|
try {
|
|
@@ -157,6 +171,53 @@
|
|
|
157
171
|
renderMeta();
|
|
158
172
|
}
|
|
159
173
|
|
|
174
|
+
function formatConnectionTime(timestamp) {
|
|
175
|
+
if (!timestamp) {
|
|
176
|
+
return "No successful sync yet";
|
|
177
|
+
}
|
|
178
|
+
return `Last successful sync ${new Date(timestamp).toLocaleTimeString()}`;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function renderConnectionNotice() {
|
|
182
|
+
const banner = connectionBanner();
|
|
183
|
+
if (!(banner instanceof HTMLElement)) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (connectionState !== "disconnected") {
|
|
187
|
+
banner.hidden = true;
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const copy = connectionCopy();
|
|
191
|
+
const detail = connectionDetail();
|
|
192
|
+
if (copy) {
|
|
193
|
+
copy.textContent = "Local viewer server disconnected. Displayed data may be stale; waiting for reconnection.";
|
|
194
|
+
}
|
|
195
|
+
if (detail) {
|
|
196
|
+
detail.textContent = formatConnectionTime(lastSuccessfulSyncAt);
|
|
197
|
+
}
|
|
198
|
+
banner.hidden = false;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function markConnectionHealthy(options = {}) {
|
|
202
|
+
const wasDisconnected = connectionState === "disconnected";
|
|
203
|
+
connectionState = "connected";
|
|
204
|
+
lastSuccessfulSyncAt = Date.now();
|
|
205
|
+
renderConnectionNotice();
|
|
206
|
+
if (wasDisconnected && !options.silent) {
|
|
207
|
+
setMeta(`Reconnected · refreshed ${new Date(lastSuccessfulSyncAt).toLocaleTimeString()}`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function markConnectionDisconnected(error) {
|
|
212
|
+
connectionState = "disconnected";
|
|
213
|
+
renderConnectionNotice();
|
|
214
|
+
scheduleNextAutoRefresh();
|
|
215
|
+
const message = error instanceof Error && error.message
|
|
216
|
+
? error.message
|
|
217
|
+
: "Unable to reach local viewer server.";
|
|
218
|
+
setMeta(`Disconnected · ${message}`);
|
|
219
|
+
}
|
|
220
|
+
|
|
160
221
|
function renderMeta() {
|
|
161
222
|
const node = meta();
|
|
162
223
|
if (node) {
|
|
@@ -215,13 +276,221 @@
|
|
|
215
276
|
|
|
216
277
|
function updateRepositoryIdentity(payload) {
|
|
217
278
|
latestRepoRoot = String(payload.root || latestRepoRoot || "");
|
|
279
|
+
latestProjects = Array.isArray(payload.projects) ? payload.projects : latestProjects;
|
|
280
|
+
const repository = payload.repository && typeof payload.repository === "object" ? payload.repository : {};
|
|
281
|
+
latestRepository = {
|
|
282
|
+
root: String(repository.root || latestRepoRoot || ""),
|
|
283
|
+
githubUrl: String(repository.githubUrl || "")
|
|
284
|
+
};
|
|
218
285
|
const pill = repoPill();
|
|
219
|
-
if (
|
|
286
|
+
if (pill) {
|
|
287
|
+
const repoName = String(payload.repoName || latestRepoRoot.split(/[\\/]/).filter(Boolean).pop() || "repository");
|
|
288
|
+
const label = pill.querySelector("[data-viewer-project-label]");
|
|
289
|
+
if (label) {
|
|
290
|
+
label.textContent = repoName;
|
|
291
|
+
} else {
|
|
292
|
+
pill.textContent = repoName;
|
|
293
|
+
}
|
|
294
|
+
pill.title = latestRepoRoot || repoName;
|
|
295
|
+
if ("disabled" in pill) {
|
|
296
|
+
pill.disabled = latestProjects.length <= 1;
|
|
297
|
+
}
|
|
298
|
+
pill.onclick = () => {
|
|
299
|
+
const menu = projectMenu();
|
|
300
|
+
setProjectMenuOpen(Boolean(menu?.hidden));
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
updateRepositoryShortcuts();
|
|
304
|
+
renderProjectMenu();
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function projectStateLabel(project) {
|
|
308
|
+
if (project?.active) {
|
|
309
|
+
return "current";
|
|
310
|
+
}
|
|
311
|
+
if (project?.available === false) {
|
|
312
|
+
return "missing";
|
|
313
|
+
}
|
|
314
|
+
if (project?.hasLogics === false) {
|
|
315
|
+
return "no Logics";
|
|
316
|
+
}
|
|
317
|
+
return "available";
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function renderProjectMenu() {
|
|
321
|
+
const menu = projectMenu();
|
|
322
|
+
if (!(menu instanceof HTMLElement)) {
|
|
220
323
|
return;
|
|
221
324
|
}
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
325
|
+
const projects = latestProjects.filter((project) => project && typeof project === "object");
|
|
326
|
+
menu.innerHTML = projects.map((project) => `
|
|
327
|
+
<button class="viewer-project-switcher__item${project.active ? " is-active" : ""}" type="button" role="menuitem" data-viewer-project-id="${escapeHtml(project.id || "")}" title="${escapeHtml(project.root || project.name || "")}">
|
|
328
|
+
<span class="viewer-project-switcher__item-name">${escapeHtml(project.name || "project")}</span>
|
|
329
|
+
<span class="viewer-project-switcher__item-state">${escapeHtml(projectStateLabel(project))}</span>
|
|
330
|
+
<span class="viewer-project-switcher__item-path">${escapeHtml(project.root || "")}</span>
|
|
331
|
+
</button>
|
|
332
|
+
`).join("");
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function setProjectMenuOpen(open) {
|
|
336
|
+
const button = repoPill();
|
|
337
|
+
const menu = projectMenu();
|
|
338
|
+
if (!(button instanceof HTMLElement) || !(menu instanceof HTMLElement)) {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
const nextOpen = Boolean(open) && latestProjects.length > 1;
|
|
342
|
+
menu.hidden = !nextOpen;
|
|
343
|
+
button.setAttribute("aria-expanded", nextOpen ? "true" : "false");
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async function switchViewerProject(projectId) {
|
|
347
|
+
if (!projectId) {
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
const target = latestProjects.find((project) => project.id === projectId);
|
|
351
|
+
if (!target || target.active) {
|
|
352
|
+
setProjectMenuOpen(false);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
setProjectMenuOpen(false);
|
|
356
|
+
setMeta(`Switching to ${target.name || "project"}...`);
|
|
357
|
+
const response = await fetch("/api/switch-project", {
|
|
358
|
+
method: "POST",
|
|
359
|
+
headers: { "Content-Type": "application/json" },
|
|
360
|
+
body: JSON.stringify({ projectId })
|
|
361
|
+
});
|
|
362
|
+
const data = await response.json();
|
|
363
|
+
if (!response.ok || !data.ok) {
|
|
364
|
+
throw new Error(data.error || "Unable to switch project.");
|
|
365
|
+
}
|
|
366
|
+
latestGitBadgeCounts = { unpushedCommits: 0, uncommittedFiles: 0 };
|
|
367
|
+
latestCiStatus = { visible: false, badgeState: "unknown", message: "" };
|
|
368
|
+
updateMainGitBadges();
|
|
369
|
+
updateMainCiBadge(latestCiStatus);
|
|
370
|
+
updateMainCdxBadge(null);
|
|
371
|
+
const panel = documentPanel();
|
|
372
|
+
if (panel) {
|
|
373
|
+
panel.hidden = true;
|
|
374
|
+
}
|
|
375
|
+
postToApp(data.payload);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async function bootstrapLogicsProject() {
|
|
379
|
+
setMeta("Bootstrapping Logics...");
|
|
380
|
+
const response = await fetch("/api/bootstrap-logics", { method: "POST" });
|
|
381
|
+
const data = await response.json();
|
|
382
|
+
if (!response.ok || !data.ok) {
|
|
383
|
+
throw new Error(data.error || "Unable to bootstrap Logics.");
|
|
384
|
+
}
|
|
385
|
+
postToApp(data.payload);
|
|
386
|
+
const created = Array.isArray(data.bootstrap?.created_paths) ? data.bootstrap.created_paths.length : 0;
|
|
387
|
+
setMeta(created > 0 ? `Logics bootstrapped · ${created} paths created.` : "Logics bootstrap checked.");
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function normalizeCapabilities(payload) {
|
|
391
|
+
const capabilities = payload?.capabilities && typeof payload.capabilities === "object" ? payload.capabilities : {};
|
|
392
|
+
return {
|
|
393
|
+
logics: capabilities.logics || { state: "ready", available: true, message: "" },
|
|
394
|
+
git: capabilities.git || { state: "ready", available: true, message: "" },
|
|
395
|
+
ci: capabilities.ci || { state: "ready", available: true, message: "" },
|
|
396
|
+
cdx: capabilities.cdx || { state: "ready", available: true, message: "" },
|
|
397
|
+
cdxRuns: capabilities.cdxRuns || { state: "unsupported", available: false, message: "" }
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function capability(name) {
|
|
402
|
+
return latestCapabilities?.[name] || { state: "unknown", available: false, message: "" };
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function isCapabilityAvailable(name) {
|
|
406
|
+
return capability(name).available === true;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function capabilityMessage(name, fallback) {
|
|
410
|
+
return String(capability(name).message || fallback || "");
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function setButtonUnavailable(button, message) {
|
|
414
|
+
if (!(button instanceof HTMLElement) || !("disabled" in button)) {
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
button.disabled = true;
|
|
418
|
+
button.setAttribute("aria-disabled", "true");
|
|
419
|
+
button.title = message;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function setButtonAvailable(button, title) {
|
|
423
|
+
if (!(button instanceof HTMLElement) || !("disabled" in button)) {
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
button.disabled = false;
|
|
427
|
+
button.removeAttribute("aria-disabled");
|
|
428
|
+
button.title = title;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function updateCapabilityControls() {
|
|
432
|
+
const gitButton = document.getElementById("viewer-git");
|
|
433
|
+
if (gitButton instanceof HTMLElement) {
|
|
434
|
+
gitButton.hidden = !isCapabilityAvailable("git");
|
|
435
|
+
if (isCapabilityAvailable("git")) {
|
|
436
|
+
setButtonAvailable(gitButton, "Show Git status");
|
|
437
|
+
} else {
|
|
438
|
+
setButtonUnavailable(gitButton, capabilityMessage("git", "Git is not available for this project."));
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const ci = ciButton();
|
|
443
|
+
if (ci instanceof HTMLElement) {
|
|
444
|
+
ci.hidden = !isCapabilityAvailable("ci");
|
|
445
|
+
if (isCapabilityAvailable("ci")) {
|
|
446
|
+
setButtonAvailable(ci, "Show GitHub Actions CI status");
|
|
447
|
+
} else {
|
|
448
|
+
setButtonUnavailable(ci, capabilityMessage("ci", "CI is not available for this project."));
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const cdx = document.getElementById("viewer-cdx");
|
|
453
|
+
if (cdx instanceof HTMLElement) {
|
|
454
|
+
if (isCapabilityAvailable("cdx")) {
|
|
455
|
+
setButtonAvailable(cdx, "Show CDX status");
|
|
456
|
+
} else {
|
|
457
|
+
setButtonUnavailable(cdx, capabilityMessage("cdx", "CDX is not available for this project."));
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function updateRepositoryShortcuts() {
|
|
463
|
+
const github = repoGithubLink();
|
|
464
|
+
const folder = repoFolderButton();
|
|
465
|
+
if (github instanceof HTMLAnchorElement) {
|
|
466
|
+
if (latestRepository.githubUrl) {
|
|
467
|
+
github.hidden = false;
|
|
468
|
+
github.href = latestRepository.githubUrl;
|
|
469
|
+
} else {
|
|
470
|
+
github.hidden = true;
|
|
471
|
+
github.removeAttribute("href");
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
if (folder instanceof HTMLButtonElement) {
|
|
475
|
+
folder.hidden = !latestRepository.root;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
async function openRepositoryFolder() {
|
|
480
|
+
if (!latestRepository.root) {
|
|
481
|
+
setMeta("Repository folder is unavailable.");
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
try {
|
|
485
|
+
const response = await fetch("/api/open-repo-folder", { method: "POST" });
|
|
486
|
+
const data = await response.json();
|
|
487
|
+
if (!response.ok || !data.ok) {
|
|
488
|
+
throw new Error(data.error || "Unable to open repository folder.");
|
|
489
|
+
}
|
|
490
|
+
setMeta("Repository folder opened.");
|
|
491
|
+
} catch (error) {
|
|
492
|
+
setMeta(error instanceof Error ? error.message : "Unable to open repository folder.");
|
|
493
|
+
}
|
|
225
494
|
}
|
|
226
495
|
|
|
227
496
|
function normalizeGitBadgeCounts(payload) {
|
|
@@ -269,6 +538,151 @@
|
|
|
269
538
|
}
|
|
270
539
|
}
|
|
271
540
|
|
|
541
|
+
function ciBadgeTone(value) {
|
|
542
|
+
const state = String(value || "").toLowerCase();
|
|
543
|
+
if (state === "passing") {
|
|
544
|
+
return "passing";
|
|
545
|
+
}
|
|
546
|
+
if (state === "failing") {
|
|
547
|
+
return "failing";
|
|
548
|
+
}
|
|
549
|
+
if (state === "running" || state === "queued") {
|
|
550
|
+
return "running";
|
|
551
|
+
}
|
|
552
|
+
if (state === "cancelled") {
|
|
553
|
+
return "cancelled";
|
|
554
|
+
}
|
|
555
|
+
if (state === "unavailable") {
|
|
556
|
+
return "unavailable";
|
|
557
|
+
}
|
|
558
|
+
return "unknown";
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function ciBadgeLabel(value) {
|
|
562
|
+
const state = ciBadgeTone(value);
|
|
563
|
+
if (state === "passing") {
|
|
564
|
+
return "pass";
|
|
565
|
+
}
|
|
566
|
+
if (state === "failing") {
|
|
567
|
+
return "fail";
|
|
568
|
+
}
|
|
569
|
+
if (state === "running") {
|
|
570
|
+
return String(value || "").toLowerCase() === "queued" ? "queue" : "run";
|
|
571
|
+
}
|
|
572
|
+
if (state === "cancelled") {
|
|
573
|
+
return "cancel";
|
|
574
|
+
}
|
|
575
|
+
if (state === "unavailable") {
|
|
576
|
+
return "auth";
|
|
577
|
+
}
|
|
578
|
+
return "n/a";
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function renderCiButtonBadge(payload) {
|
|
582
|
+
const state = payload?.badgeState || payload?.state || "unknown";
|
|
583
|
+
const label = ciBadgeLabel(state);
|
|
584
|
+
const tone = ciBadgeTone(state);
|
|
585
|
+
return `<span class="viewer-ci-badge viewer-ci-badge--${escapeHtml(tone)}" data-viewer-ci-badge title="${escapeHtml(payload?.message || `CI ${label}`)}">${escapeHtml(label)}</span>`;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function updateMainCiBadge(payload = latestCiStatus) {
|
|
589
|
+
latestCiStatus = payload && typeof payload === "object" ? payload : { visible: false, badgeState: "unknown", message: "" };
|
|
590
|
+
const button = ciButton();
|
|
591
|
+
if (!(button instanceof HTMLElement)) {
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
button.querySelector("[data-viewer-ci-badge]")?.remove();
|
|
595
|
+
if (!latestCiStatus.visible) {
|
|
596
|
+
button.hidden = true;
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
button.hidden = false;
|
|
600
|
+
button.title = latestCiStatus.message || "Show GitHub Actions CI status";
|
|
601
|
+
button.insertAdjacentHTML("beforeend", renderCiButtonBadge(latestCiStatus));
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
async function refreshCiBadgeCounters() {
|
|
605
|
+
if (!isCapabilityAvailable("ci")) {
|
|
606
|
+
updateMainCiBadge({ visible: false, badgeState: "unknown", message: capabilityMessage("ci", "CI is not available for this project.") });
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
try {
|
|
610
|
+
const response = await fetch("/api/ci-status");
|
|
611
|
+
if (response.status === 404) {
|
|
612
|
+
updateMainCiBadge({ visible: false, badgeState: "unknown", message: "CI status endpoint unavailable." });
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
const data = await response.json();
|
|
616
|
+
if (response.ok && data.ok) {
|
|
617
|
+
updateMainCiBadge(data.payload);
|
|
618
|
+
}
|
|
619
|
+
} catch {
|
|
620
|
+
updateMainCiBadge({ visible: false, badgeState: "unknown", message: "CI status unavailable." });
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function activeCdxAssistantCountFromPayload(payload) {
|
|
625
|
+
if (!payload || payload.state !== "ok") {
|
|
626
|
+
return 0;
|
|
627
|
+
}
|
|
628
|
+
const status = payload.status || {};
|
|
629
|
+
const sessions = cdxSessions(status);
|
|
630
|
+
const sessionActive = sessions.filter((session) => {
|
|
631
|
+
const state = String(session.state || session.status || session.availability || "").toLowerCase();
|
|
632
|
+
return session.active === true ||
|
|
633
|
+
state.includes("active") ||
|
|
634
|
+
state.includes("running") ||
|
|
635
|
+
state.includes("busy");
|
|
636
|
+
}).length;
|
|
637
|
+
if (sessionActive > 0) {
|
|
638
|
+
return sessionActive;
|
|
639
|
+
}
|
|
640
|
+
const rowsActive = cdxRows(status).filter((row) => row.active === true).length;
|
|
641
|
+
if (rowsActive > 0) {
|
|
642
|
+
return rowsActive;
|
|
643
|
+
}
|
|
644
|
+
return cdxProviders(status).reduce((total, provider) => total + Math.max(0, Number(provider.active || 0)), 0);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function updateMainCdxBadge(payload) {
|
|
648
|
+
const button = document.getElementById("viewer-cdx");
|
|
649
|
+
if (!(button instanceof HTMLElement)) {
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
button.querySelector("[data-viewer-cdx-badge]")?.remove();
|
|
653
|
+
const activeCount = activeCdxAssistantCountFromPayload(payload);
|
|
654
|
+
if (activeCount <= 0) {
|
|
655
|
+
button.title = isCapabilityAvailable("cdx")
|
|
656
|
+
? "Show CDX status"
|
|
657
|
+
: capabilityMessage("cdx", "CDX is not available for this project.");
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
const label = activeCount > 9 ? "9+" : String(activeCount);
|
|
661
|
+
const title = activeCount === 1 ? "1 active assistant/session" : `${activeCount} active assistants/sessions`;
|
|
662
|
+
button.title = `Show CDX status · ${title}`;
|
|
663
|
+
button.insertAdjacentHTML("beforeend", `<span class="viewer-cdx-button-badge" data-viewer-cdx-badge title="${escapeHtml(title)}" aria-label="${escapeHtml(title)}">${escapeHtml(label)}</span>`);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
async function refreshCdxBadgeCounters() {
|
|
667
|
+
if (!isCapabilityAvailable("cdx")) {
|
|
668
|
+
updateMainCdxBadge(null);
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
try {
|
|
672
|
+
const response = await fetch("/api/cdx-status");
|
|
673
|
+
if (response.status === 404) {
|
|
674
|
+
updateMainCdxBadge(null);
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
const data = await response.json();
|
|
678
|
+
if (response.ok && data.ok) {
|
|
679
|
+
updateMainCdxBadge(data.payload);
|
|
680
|
+
}
|
|
681
|
+
} catch {
|
|
682
|
+
updateMainCdxBadge(null);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
272
686
|
function setGitBadgeCountsFromPayload(payload, options = {}) {
|
|
273
687
|
latestGitBadgeCounts = normalizeGitBadgeCounts(payload);
|
|
274
688
|
if (options.updateMain !== false) {
|
|
@@ -277,6 +691,11 @@
|
|
|
277
691
|
}
|
|
278
692
|
|
|
279
693
|
async function refreshGitBadgeCounters() {
|
|
694
|
+
if (!isCapabilityAvailable("git")) {
|
|
695
|
+
latestGitBadgeCounts = { unpushedCommits: 0, uncommittedFiles: 0 };
|
|
696
|
+
updateMainGitBadges();
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
280
699
|
try {
|
|
281
700
|
const response = await fetch("/api/git-status");
|
|
282
701
|
const data = await response.json();
|
|
@@ -354,13 +773,10 @@
|
|
|
354
773
|
}, 0);
|
|
355
774
|
}
|
|
356
775
|
|
|
357
|
-
function applyFocusRequest(payload) {
|
|
358
|
-
if (focusApplied) {
|
|
359
|
-
return payload;
|
|
360
|
-
}
|
|
776
|
+
function applyFocusRequest(payload, options = {}) {
|
|
361
777
|
const request = focusRequest();
|
|
362
778
|
if (!request.focus) {
|
|
363
|
-
if (window.location.search.includes("focus=")) {
|
|
779
|
+
if (!focusApplied && !options.silent && window.location.search.includes("focus=")) {
|
|
364
780
|
window.setTimeout(() => setMeta("Invalid focus target. Loaded corpus without changing selection."), 0);
|
|
365
781
|
}
|
|
366
782
|
focusApplied = true;
|
|
@@ -368,14 +784,20 @@
|
|
|
368
784
|
}
|
|
369
785
|
const item = findFocusItem(request.focus);
|
|
370
786
|
if (!item) {
|
|
371
|
-
|
|
787
|
+
if (!focusApplied && !options.silent) {
|
|
788
|
+
window.setTimeout(() => setMeta(`Focus target not found: ${request.focus}`), 0);
|
|
789
|
+
}
|
|
372
790
|
focusApplied = true;
|
|
373
791
|
return payload;
|
|
374
792
|
}
|
|
793
|
+
const nextPayload = { ...payload, selectedId: item.id };
|
|
794
|
+
if (focusApplied) {
|
|
795
|
+
persistSelectedItem(item.id);
|
|
796
|
+
return nextPayload;
|
|
797
|
+
}
|
|
375
798
|
viewerFilterState = { ...viewerFilterState, focus: "all", type: "all", status: "any", relation: "any", activity: "any" };
|
|
376
799
|
persistSelectedItem(item.id);
|
|
377
800
|
focusApplied = true;
|
|
378
|
-
const nextPayload = { ...payload, selectedId: item.id };
|
|
379
801
|
window.setTimeout(() => {
|
|
380
802
|
revealFocusedCard(item);
|
|
381
803
|
if (request.read) {
|
|
@@ -526,14 +948,17 @@
|
|
|
526
948
|
}
|
|
527
949
|
|
|
528
950
|
function postToApp(payload, options = {}) {
|
|
951
|
+
markConnectionHealthy({ silent: Boolean(options.silent) });
|
|
529
952
|
latestItems = updateStoredActivity(Array.isArray(payload.items) ? payload.items : []);
|
|
530
953
|
if (!autoRefreshIntervalTouched) {
|
|
531
954
|
autoRefreshIntervalMs = normalizeAutoRefreshIntervalSeconds(payload.autoRefreshIntervalSeconds) * 1000;
|
|
532
955
|
updateRefreshIntervalControl();
|
|
533
956
|
}
|
|
534
957
|
updateRepositoryIdentity(payload);
|
|
958
|
+
latestCapabilities = normalizeCapabilities(payload);
|
|
959
|
+
updateCapabilityControls();
|
|
535
960
|
const payloadWithActivity = { ...payload, items: latestItems };
|
|
536
|
-
const nextPayload =
|
|
961
|
+
const nextPayload = applyFocusRequest(payloadWithActivity, { silent: Boolean(options.silent) });
|
|
537
962
|
window.dispatchEvent(new MessageEvent("message", { data: { type: "data", payload: nextPayload } }));
|
|
538
963
|
const rootName = payload.root ? payload.root.split(/[\\/]/).filter(Boolean).pop() : "repository";
|
|
539
964
|
if (!options.silent) {
|
|
@@ -541,6 +966,8 @@
|
|
|
541
966
|
}
|
|
542
967
|
scheduleNextAutoRefresh();
|
|
543
968
|
renderUpdateNotice(payload.updateInfo);
|
|
969
|
+
refreshCiBadgeCounters();
|
|
970
|
+
refreshCdxBadgeCounters();
|
|
544
971
|
updateFilterSummary();
|
|
545
972
|
applyLocalViewerChrome();
|
|
546
973
|
bindRefreshMenuControls();
|
|
@@ -585,6 +1012,9 @@
|
|
|
585
1012
|
await refreshGitBadgeCounters();
|
|
586
1013
|
}
|
|
587
1014
|
return true;
|
|
1015
|
+
} catch (error) {
|
|
1016
|
+
markConnectionDisconnected(error);
|
|
1017
|
+
throw error;
|
|
588
1018
|
} finally {
|
|
589
1019
|
itemsLoadInFlight = false;
|
|
590
1020
|
}
|
|
@@ -602,12 +1032,28 @@
|
|
|
602
1032
|
return Boolean(panel && !panel.hidden && title && title.textContent === "CDX status");
|
|
603
1033
|
}
|
|
604
1034
|
|
|
1035
|
+
function isCdxRunsOpen() {
|
|
1036
|
+
const panel = documentPanel();
|
|
1037
|
+
const title = documentTitle();
|
|
1038
|
+
return Boolean(panel && !panel.hidden && title && title.textContent === "CDX runs");
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
function isCiStatusOpen() {
|
|
1042
|
+
const panel = documentPanel();
|
|
1043
|
+
const title = documentTitle();
|
|
1044
|
+
return Boolean(panel && !panel.hidden && title && title.textContent === "CI status");
|
|
1045
|
+
}
|
|
1046
|
+
|
|
605
1047
|
async function refreshViewer(method = "POST", options = {}) {
|
|
606
1048
|
await loadItems(method, options);
|
|
607
1049
|
if (isGitStatusOpen()) {
|
|
608
1050
|
await showGitStatus({ preserve: true, silent: Boolean(options.silent) });
|
|
1051
|
+
} else if (isCiStatusOpen()) {
|
|
1052
|
+
await showCiStatus({ silent: Boolean(options.silent) });
|
|
609
1053
|
} else if (isCdxStatusOpen()) {
|
|
610
1054
|
await showCdxStatus({ silent: Boolean(options.silent) });
|
|
1055
|
+
} else if (isCdxRunsOpen()) {
|
|
1056
|
+
await showCdxRuns({ silent: Boolean(options.silent) });
|
|
611
1057
|
} else if (method === "POST") {
|
|
612
1058
|
await refreshGitBadgeCounters();
|
|
613
1059
|
}
|
|
@@ -893,14 +1339,43 @@
|
|
|
893
1339
|
}
|
|
894
1340
|
|
|
895
1341
|
function renderMetricCards(entries) {
|
|
896
|
-
return entries.map(([label, value]) => `
|
|
897
|
-
<div class="viewer-insights__card">
|
|
1342
|
+
return entries.map(([label, value, tone]) => `
|
|
1343
|
+
<div class="viewer-insights__card${tone ? ` viewer-insights__card--${escapeHtml(tone)}` : ""}">
|
|
898
1344
|
<div class="viewer-insights__label">${escapeHtml(label)}</div>
|
|
899
1345
|
<div class="viewer-insights__value">${escapeHtml(value)}</div>
|
|
900
1346
|
</div>
|
|
901
1347
|
`).join("");
|
|
902
1348
|
}
|
|
903
1349
|
|
|
1350
|
+
function renderInsightBars(entries, total) {
|
|
1351
|
+
const denominator = Math.max(1, Number(total) || 0);
|
|
1352
|
+
if (!entries.length) {
|
|
1353
|
+
return '<li class="viewer-insights__bar-row">No corpus shape available</li>';
|
|
1354
|
+
}
|
|
1355
|
+
return entries.map(([label, value]) => {
|
|
1356
|
+
const count = Number(value) || 0;
|
|
1357
|
+
const width = Math.max(count > 0 ? 4 : 0, Math.min(100, Math.round((count / denominator) * 100)));
|
|
1358
|
+
return `
|
|
1359
|
+
<li class="viewer-insights__bar-row">
|
|
1360
|
+
<div class="viewer-insights__bar-meta"><span>${escapeHtml(label)}</span><strong>${escapeHtml(count)}</strong></div>
|
|
1361
|
+
<div class="viewer-insights__bar-track" aria-hidden="true"><span style="width: ${width}%"></span></div>
|
|
1362
|
+
</li>
|
|
1363
|
+
`;
|
|
1364
|
+
}).join("");
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
function renderSignalRows(items, emptyText = "No signals") {
|
|
1368
|
+
if (!items.length) {
|
|
1369
|
+
return `<li class="viewer-insights__signal viewer-insights__signal--empty">${escapeHtml(emptyText)}</li>`;
|
|
1370
|
+
}
|
|
1371
|
+
return items.map(([label, value, tone]) => `
|
|
1372
|
+
<li class="viewer-insights__signal${tone ? ` viewer-insights__signal--${escapeHtml(tone)}` : ""}">
|
|
1373
|
+
<span>${escapeHtml(label)}</span>
|
|
1374
|
+
<strong>${escapeHtml(value)}</strong>
|
|
1375
|
+
</li>
|
|
1376
|
+
`).join("");
|
|
1377
|
+
}
|
|
1378
|
+
|
|
904
1379
|
function renderInsightRows(items, emptyText = "No signals") {
|
|
905
1380
|
if (!items.length) {
|
|
906
1381
|
return `<li class="viewer-insights__item">${escapeHtml(emptyText)}</li>`;
|
|
@@ -1046,63 +1521,80 @@
|
|
|
1046
1521
|
const stageRows = Object.entries(countsByStage)
|
|
1047
1522
|
.sort((left, right) => String(left[0]).localeCompare(String(right[0])))
|
|
1048
1523
|
.map(([stage, count]) => [stage, count]);
|
|
1524
|
+
const qualityTotal = qualityFindings.length;
|
|
1525
|
+
const needsAttention = blocked.length + incompleteChains.length + brokenRefs.length + missingStatus.length + qualityTotal;
|
|
1526
|
+
const activeQuiet = Math.max(0, open.length - recentlyModified.length - staleActive.length);
|
|
1527
|
+
const primaryState = needsAttention > 0
|
|
1528
|
+
? `${needsAttention} signals need attention`
|
|
1529
|
+
: "No immediate workflow risk detected";
|
|
1049
1530
|
return `
|
|
1050
1531
|
<div class="viewer-insights">
|
|
1051
|
-
<
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
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>
|
|
1073
|
-
<section class="viewer-insights__section">
|
|
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>
|
|
1082
|
-
</section>
|
|
1083
|
-
<section class="viewer-insights__section">
|
|
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>
|
|
1532
|
+
<section class="viewer-insights__hero">
|
|
1533
|
+
<div>
|
|
1534
|
+
<h2>Overview</h2>
|
|
1535
|
+
<p>${escapeHtml(primaryState)} across ${escapeHtml(docs.length)} workflow docs.</p>
|
|
1536
|
+
</div>
|
|
1537
|
+
<div class="viewer-insights__summary">${renderMetricCards([
|
|
1538
|
+
["Docs", docs.length],
|
|
1539
|
+
["Needs attention", needsAttention, needsAttention ? "warning" : "ok"],
|
|
1540
|
+
["Recent 7d", recentlyModified.length],
|
|
1541
|
+
["Quality findings", qualityTotal, qualityTotal ? "warning" : "ok"]
|
|
1542
|
+
])}</div>
|
|
1101
1543
|
</section>
|
|
1102
1544
|
<section class="viewer-insights__section">
|
|
1103
1545
|
<h2>Operator actions</h2>
|
|
1104
|
-
<ul class="viewer-insights__rows">${renderActionRows(actions)}</ul>
|
|
1546
|
+
<ul class="viewer-insights__rows viewer-insights__rows--actions">${renderActionRows(actions)}</ul>
|
|
1105
1547
|
</section>
|
|
1548
|
+
<div class="viewer-insights__workspace">
|
|
1549
|
+
<section class="viewer-insights__section">
|
|
1550
|
+
<h2>Corpus shape</h2>
|
|
1551
|
+
<ul class="viewer-insights__bars">${renderInsightBars(stageRows, docs.length)}</ul>
|
|
1552
|
+
<ul class="viewer-insights__list">${renderInsightRows([
|
|
1553
|
+
["Open", open.length],
|
|
1554
|
+
["Closed", closed.length],
|
|
1555
|
+
["Blocked", blocked.length],
|
|
1556
|
+
["Missing status", missingStatus.length]
|
|
1557
|
+
])}</ul>
|
|
1558
|
+
</section>
|
|
1559
|
+
<section class="viewer-insights__section">
|
|
1560
|
+
<h2>Flow health</h2>
|
|
1561
|
+
<ul class="viewer-insights__signals">${renderSignalRows([
|
|
1562
|
+
["Incomplete workflow chains", incompleteChains.length, incompleteChains.length ? "warning" : "ok"],
|
|
1563
|
+
["Promotion gaps", incompleteChains.filter((item) => item.stage === "request" || item.stage === "backlog").length, incompleteChains.length ? "warning" : "ok"],
|
|
1564
|
+
["Orphan or unlinked docs", unlinked.length, unlinked.length ? "muted" : "ok"],
|
|
1565
|
+
["Broken reference risks", brokenRefs.length, brokenRefs.length ? "warning" : "ok"]
|
|
1566
|
+
])}</ul>
|
|
1567
|
+
<ul class="viewer-insights__rows">${renderDocRows(incompleteChains, "No incomplete chains")}</ul>
|
|
1568
|
+
</section>
|
|
1569
|
+
<section class="viewer-insights__section">
|
|
1570
|
+
<h2>Activity</h2>
|
|
1571
|
+
<ul class="viewer-insights__signals">${renderSignalRows([
|
|
1572
|
+
["Recently active docs", recentlyModified.length],
|
|
1573
|
+
["Stale active docs", staleActive.length, staleActive.length ? "warning" : "ok"],
|
|
1574
|
+
["Quiet active docs", activeQuiet]
|
|
1575
|
+
])}</ul>
|
|
1576
|
+
<ul class="viewer-insights__rows">${renderDocRows(recentRows, "No recent documents")}</ul>
|
|
1577
|
+
</section>
|
|
1578
|
+
<section class="viewer-insights__section">
|
|
1579
|
+
<h2>Traceability</h2>
|
|
1580
|
+
<ul class="viewer-insights__signals">${renderSignalRows([
|
|
1581
|
+
["Broken references", brokenRefs.length, brokenRefs.length ? "warning" : "ok"],
|
|
1582
|
+
["Unlinked docs", unlinked.length, unlinked.length ? "muted" : "ok"],
|
|
1583
|
+
["Most referenced docs", mostReferenced.map((item) => `${item.id} (${(item.usedBy || []).length})`).join(", ") || "None"],
|
|
1584
|
+
["Relationships by type", Object.entries(relationshipCounts).map(([stage, count]) => `${stage} ${count}`).join(", ") || "None"]
|
|
1585
|
+
])}</ul>
|
|
1586
|
+
<ul class="viewer-insights__rows">${renderPathRows(brokenRefs, "No broken references")}${renderDocRows(unlinked, "No unlinked documents")}</ul>
|
|
1587
|
+
</section>
|
|
1588
|
+
<section class="viewer-insights__section viewer-insights__section--wide">
|
|
1589
|
+
<h2>Quality signals</h2>
|
|
1590
|
+
<ul class="viewer-insights__signals">${renderSignalRows([
|
|
1591
|
+
["Lint/audit categories", Object.entries(qualityBySource).map(([key, count]) => `${key} ${count}`).join(", ") || "No findings loaded", qualityTotal ? "warning" : "ok"],
|
|
1592
|
+
["Findings by document type", Object.entries(qualityByDocType).map(([key, count]) => `${key} ${count}`).join(", ") || "No findings loaded"],
|
|
1593
|
+
["Concentrated issues", concentratedIssues.map(([key, count]) => `${key} ${count}`).join(", ") || "None"]
|
|
1594
|
+
])}</ul>
|
|
1595
|
+
<ul class="viewer-insights__rows">${renderPathRows(concentratedIssues.map(([key, count]) => `${key} (${count})`), "No concentrated issues")}</ul>
|
|
1596
|
+
</section>
|
|
1597
|
+
</div>
|
|
1106
1598
|
</div>
|
|
1107
1599
|
`;
|
|
1108
1600
|
}
|
|
@@ -1614,10 +2106,20 @@
|
|
|
1614
2106
|
return rows || `<li class="viewer-cdx__empty">${escapeHtml(emptyText)}</li>`;
|
|
1615
2107
|
}
|
|
1616
2108
|
|
|
2109
|
+
function renderCdxModeSwitcher(active) {
|
|
2110
|
+
return `
|
|
2111
|
+
<div class="viewer-cdx__modes" role="tablist" aria-label="CDX views">
|
|
2112
|
+
<button class="viewer-cdx__mode${active === "status" ? " is-active" : ""}" type="button" data-viewer-cdx-mode="status" aria-selected="${active === "status" ? "true" : "false"}">Status</button>
|
|
2113
|
+
<button class="viewer-cdx__mode${active === "runs" ? " is-active" : ""}" type="button" data-viewer-cdx-mode="runs" aria-selected="${active === "runs" ? "true" : "false"}">Runs</button>
|
|
2114
|
+
</div>
|
|
2115
|
+
`;
|
|
2116
|
+
}
|
|
2117
|
+
|
|
1617
2118
|
function renderCdxStatus(payload) {
|
|
1618
2119
|
if (!payload || payload.state !== "ok") {
|
|
1619
2120
|
return `
|
|
1620
2121
|
<div class="viewer-cdx">
|
|
2122
|
+
${renderCdxModeSwitcher("status")}
|
|
1621
2123
|
<div class="viewer-cdx__state">${escapeHtml(payload?.message || "CDX status is unavailable.")}</div>
|
|
1622
2124
|
</div>
|
|
1623
2125
|
`;
|
|
@@ -1653,6 +2155,7 @@
|
|
|
1653
2155
|
`).join("");
|
|
1654
2156
|
return `
|
|
1655
2157
|
<div class="viewer-cdx">
|
|
2158
|
+
${renderCdxModeSwitcher("status")}
|
|
1656
2159
|
<div class="viewer-cdx__summary">${cards}</div>
|
|
1657
2160
|
<div class="viewer-cdx__workspace">
|
|
1658
2161
|
<div class="viewer-cdx__stack">
|
|
@@ -1680,7 +2183,87 @@
|
|
|
1680
2183
|
`;
|
|
1681
2184
|
}
|
|
1682
2185
|
|
|
2186
|
+
function renderCdxRuns(payload) {
|
|
2187
|
+
if (!payload || payload.state !== "ok") {
|
|
2188
|
+
return `
|
|
2189
|
+
<div class="viewer-cdx">
|
|
2190
|
+
${renderCdxModeSwitcher("runs")}
|
|
2191
|
+
<div class="viewer-cdx__state">${escapeHtml(payload?.message || "CDX runs are unavailable.")}</div>
|
|
2192
|
+
</div>
|
|
2193
|
+
`;
|
|
2194
|
+
}
|
|
2195
|
+
const runs = Array.isArray(payload.runs) ? payload.runs : [];
|
|
2196
|
+
const rows = runs.map((run) => `
|
|
2197
|
+
<tr>
|
|
2198
|
+
<td><code>${escapeHtml(run.run_id || "-")}</code></td>
|
|
2199
|
+
<td>${renderCdxBadge(run.status || "unknown")}</td>
|
|
2200
|
+
<td>${escapeHtml(run.kind || "assistant")}</td>
|
|
2201
|
+
<td>${escapeHtml(run.session || "-")}</td>
|
|
2202
|
+
<td>${escapeHtml(run.cwd || "-")}</td>
|
|
2203
|
+
<td><button class="viewer-cdx__mode" type="button" data-viewer-cdx-report="${escapeHtml(run.run_id || "")}">Report</button></td>
|
|
2204
|
+
</tr>
|
|
2205
|
+
`).join("");
|
|
2206
|
+
return `
|
|
2207
|
+
<div class="viewer-cdx">
|
|
2208
|
+
${renderCdxModeSwitcher("runs")}
|
|
2209
|
+
<section class="viewer-cdx__section">
|
|
2210
|
+
<div class="viewer-ci__heading"><h2>Assistant runs</h2><span>${escapeHtml(runs.length)} reported</span></div>
|
|
2211
|
+
<div class="viewer-cdx__table-wrap">
|
|
2212
|
+
<table class="viewer-cdx__table">
|
|
2213
|
+
<thead><tr><th>RUN</th><th>STATUS</th><th>KIND</th><th>SESSION</th><th>CWD</th><th>REPORT</th></tr></thead>
|
|
2214
|
+
<tbody>${rows || '<tr><td colspan="6" class="viewer-cdx__empty">No assistant runs reported.</td></tr>'}</tbody>
|
|
2215
|
+
</table>
|
|
2216
|
+
</div>
|
|
2217
|
+
</section>
|
|
2218
|
+
</div>
|
|
2219
|
+
`;
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2222
|
+
function renderCdxReport(payload) {
|
|
2223
|
+
if (!payload || payload.state !== "ok" || !payload.report) {
|
|
2224
|
+
return `
|
|
2225
|
+
<div class="viewer-cdx">
|
|
2226
|
+
${renderCdxModeSwitcher("runs")}
|
|
2227
|
+
<div class="viewer-cdx__state">${escapeHtml(payload?.message || "CDX run report is unavailable.")}</div>
|
|
2228
|
+
</div>
|
|
2229
|
+
`;
|
|
2230
|
+
}
|
|
2231
|
+
const report = payload.report || {};
|
|
2232
|
+
const run = report.run || {};
|
|
2233
|
+
const taskReport = report.task_report || {};
|
|
2234
|
+
const findings = Array.isArray(taskReport.findings) ? taskReport.findings : [];
|
|
2235
|
+
const findingRows = findings.map((finding, index) => {
|
|
2236
|
+
const location = [finding.path || finding.file || "", finding.line || ""].filter(Boolean).join(":") || "-";
|
|
2237
|
+
return `<li class="viewer-cdx__entity"><div class="viewer-cdx__entity-main"><div><strong>${escapeHtml(finding.message || finding.title || `Finding ${index + 1}`)}</strong><div class="viewer-cdx__meta">${escapeHtml(location)}</div></div>${renderCdxBadge(finding.severity || "unknown")}</div></li>`;
|
|
2238
|
+
}).join("");
|
|
2239
|
+
const canCreate = taskReport.kind === "code-review";
|
|
2240
|
+
return `
|
|
2241
|
+
<div class="viewer-cdx">
|
|
2242
|
+
${renderCdxModeSwitcher("runs")}
|
|
2243
|
+
<section class="viewer-cdx__section">
|
|
2244
|
+
<div class="viewer-ci__heading"><h2>Run report</h2><span>${escapeHtml(run.status || "unknown")}</span></div>
|
|
2245
|
+
<ul class="viewer-cdx__list">
|
|
2246
|
+
<li class="viewer-cdx__row"><span>Run</span><strong>${escapeHtml(run.run_id || taskReport.run_id || "-")}</strong></li>
|
|
2247
|
+
<li class="viewer-cdx__row"><span>Kind</span><strong>${escapeHtml(taskReport.kind || run.kind || "assistant")}</strong></li>
|
|
2248
|
+
<li class="viewer-cdx__row"><span>Summary</span><strong>${escapeHtml(taskReport.summary || "No summary reported.")}</strong></li>
|
|
2249
|
+
</ul>
|
|
2250
|
+
${canCreate ? `<button class="btn" type="button" data-viewer-cdx-create-request="${escapeHtml(run.run_id || taskReport.run_id || "")}">Create Logics request</button>` : ""}
|
|
2251
|
+
</section>
|
|
2252
|
+
<section class="viewer-cdx__section">
|
|
2253
|
+
<div class="viewer-ci__heading"><h2>Findings</h2><span>${escapeHtml(findings.length)} reported</span></div>
|
|
2254
|
+
<ul class="viewer-cdx__list">${findingRows || '<li class="viewer-cdx__empty">No structured findings reported.</li>'}</ul>
|
|
2255
|
+
</section>
|
|
2256
|
+
</div>
|
|
2257
|
+
`;
|
|
2258
|
+
}
|
|
2259
|
+
|
|
1683
2260
|
async function showCdxStatus(options = {}) {
|
|
2261
|
+
if (!isCapabilityAvailable("cdx")) {
|
|
2262
|
+
const message = capabilityMessage("cdx", "CDX is not available for this project.");
|
|
2263
|
+
setDocument("CDX status", renderCdxStatus({ state: capability("cdx").state, message }));
|
|
2264
|
+
setMeta(message);
|
|
2265
|
+
return;
|
|
2266
|
+
}
|
|
1684
2267
|
if (!options.silent) {
|
|
1685
2268
|
setMeta("Checking CDX status...");
|
|
1686
2269
|
}
|
|
@@ -1702,10 +2285,170 @@
|
|
|
1702
2285
|
if (!response.ok || !data.ok) {
|
|
1703
2286
|
throw new Error(data.error || "Unable to load CDX status.");
|
|
1704
2287
|
}
|
|
2288
|
+
updateMainCdxBadge(data.payload);
|
|
1705
2289
|
setDocument("CDX status", renderCdxStatus(data.payload));
|
|
1706
2290
|
setMeta(options.silent ? "CDX status refreshed." : "CDX status loaded.");
|
|
1707
2291
|
}
|
|
1708
2292
|
|
|
2293
|
+
async function showCdxRuns(options = {}) {
|
|
2294
|
+
if (!isCapabilityAvailable("cdx")) {
|
|
2295
|
+
const message = capabilityMessage("cdx", "CDX is not available for this project.");
|
|
2296
|
+
setDocument("CDX runs", renderCdxRuns({ state: capability("cdx").state, message }));
|
|
2297
|
+
setMeta(message);
|
|
2298
|
+
return;
|
|
2299
|
+
}
|
|
2300
|
+
if (!options.silent) {
|
|
2301
|
+
setMeta("Checking CDX runs...");
|
|
2302
|
+
}
|
|
2303
|
+
const response = await fetch("/api/cdx-runs");
|
|
2304
|
+
let data = {};
|
|
2305
|
+
try {
|
|
2306
|
+
data = await response.json();
|
|
2307
|
+
} catch {
|
|
2308
|
+
data = {};
|
|
2309
|
+
}
|
|
2310
|
+
if (!response.ok || !data.ok) {
|
|
2311
|
+
throw new Error(data.error || "Unable to load CDX runs.");
|
|
2312
|
+
}
|
|
2313
|
+
setDocument("CDX runs", renderCdxRuns(data.payload));
|
|
2314
|
+
setMeta(options.silent ? "CDX runs refreshed." : "CDX runs loaded.");
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2317
|
+
async function showCdxReport(runId) {
|
|
2318
|
+
if (!runId) {
|
|
2319
|
+
return;
|
|
2320
|
+
}
|
|
2321
|
+
setMeta("Loading CDX report...");
|
|
2322
|
+
const response = await fetch(`/api/cdx-run-report?${new URLSearchParams({ runId }).toString()}`);
|
|
2323
|
+
const data = await response.json();
|
|
2324
|
+
if (!response.ok || !data.ok) {
|
|
2325
|
+
throw new Error(data.error || "Unable to load CDX report.");
|
|
2326
|
+
}
|
|
2327
|
+
setDocument("CDX run report", renderCdxReport(data.payload));
|
|
2328
|
+
setMeta("CDX report loaded.");
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2331
|
+
async function createRequestFromCdxReport(runId) {
|
|
2332
|
+
if (!runId) {
|
|
2333
|
+
return;
|
|
2334
|
+
}
|
|
2335
|
+
setMeta("Creating Logics request from CDX report...");
|
|
2336
|
+
const response = await fetch("/api/cdx-report-request", {
|
|
2337
|
+
method: "POST",
|
|
2338
|
+
headers: { "Content-Type": "application/json" },
|
|
2339
|
+
body: JSON.stringify({ runId })
|
|
2340
|
+
});
|
|
2341
|
+
const data = await response.json();
|
|
2342
|
+
if (!response.ok || !data.ok) {
|
|
2343
|
+
throw new Error(data.error || "Unable to create Logics request.");
|
|
2344
|
+
}
|
|
2345
|
+
postToApp(data.payload);
|
|
2346
|
+
setMeta(`Created ${data.created?.id || "Logics request"} from CDX report.`);
|
|
2347
|
+
}
|
|
2348
|
+
|
|
2349
|
+
function renderCiBadge(value) {
|
|
2350
|
+
const tone = ciBadgeTone(value);
|
|
2351
|
+
return `<span class="viewer-ci__badge viewer-ci__badge--${escapeHtml(tone)}">${escapeHtml(ciBadgeLabel(value))}</span>`;
|
|
2352
|
+
}
|
|
2353
|
+
|
|
2354
|
+
function formatCiDate(value) {
|
|
2355
|
+
const timestamp = Date.parse(String(value || ""));
|
|
2356
|
+
if (!Number.isFinite(timestamp)) {
|
|
2357
|
+
return "";
|
|
2358
|
+
}
|
|
2359
|
+
return new Date(timestamp).toLocaleString();
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2362
|
+
function renderCiStatus(payload) {
|
|
2363
|
+
if (!payload || !payload.visible) {
|
|
2364
|
+
return `
|
|
2365
|
+
<div class="viewer-ci">
|
|
2366
|
+
<div class="viewer-ci__state">${escapeHtml(payload?.message || "GitHub Actions CI is not configured for this repository.")}</div>
|
|
2367
|
+
</div>
|
|
2368
|
+
`;
|
|
2369
|
+
}
|
|
2370
|
+
const run = payload.run && typeof payload.run === "object" ? payload.run : null;
|
|
2371
|
+
const jobs = Array.isArray(payload.jobs) ? payload.jobs : [];
|
|
2372
|
+
const state = payload.badgeState || run?.badgeState || payload.state || "unknown";
|
|
2373
|
+
const cards = renderMetricCards([
|
|
2374
|
+
["State", ciBadgeLabel(state)],
|
|
2375
|
+
["Branch", run?.branch || payload.branch || "Unknown"],
|
|
2376
|
+
["Commit", (run?.headSha || payload.headSha || "").slice(0, 7) || "Unknown"],
|
|
2377
|
+
["Match", run?.matchSource === "head" ? "Current HEAD" : "Latest branch run"]
|
|
2378
|
+
]);
|
|
2379
|
+
const runUrl = run?.htmlUrl ? `<a class="viewer-ci__link" href="${escapeHtml(run.htmlUrl)}" target="_blank" rel="noreferrer">Open in GitHub</a>` : "";
|
|
2380
|
+
const runRows = run ? [
|
|
2381
|
+
["Workflow", run.workflowName || run.name || "GitHub Actions"],
|
|
2382
|
+
["Status", `${run.status || "unknown"}${run.conclusion ? ` / ${run.conclusion}` : ""}`],
|
|
2383
|
+
["Event", run.event || "Unknown"],
|
|
2384
|
+
["Commit", run.commitMessage || payload.subject || "Unknown"],
|
|
2385
|
+
["Author", run.author || payload.author || "Unknown"],
|
|
2386
|
+
["Started", formatCiDate(run.runStartedAt || run.createdAt) || "Unknown"],
|
|
2387
|
+
["Updated", formatCiDate(run.updatedAt) || "Unknown"]
|
|
2388
|
+
].map(([label, value]) => `
|
|
2389
|
+
<li class="viewer-ci__row"><span>${escapeHtml(label)}</span><strong>${escapeHtml(value)}</strong></li>
|
|
2390
|
+
`).join("") : `<li class="viewer-ci__empty">${escapeHtml(payload.message || "No GitHub Actions run found for this branch.")}</li>`;
|
|
2391
|
+
const jobRows = jobs.length ? jobs.map((job) => {
|
|
2392
|
+
const jobState = ciBadgeTone(job.conclusion || job.status);
|
|
2393
|
+
const content = `
|
|
2394
|
+
<span>${escapeHtml(job.name || "Job")}</span>
|
|
2395
|
+
<strong>${escapeHtml([job.status, job.conclusion].filter(Boolean).join(" / ") || "unknown")}</strong>
|
|
2396
|
+
`;
|
|
2397
|
+
return `<li class="viewer-ci__job viewer-ci__job--${escapeHtml(jobState)}">${job.htmlUrl ? `<a href="${escapeHtml(job.htmlUrl)}" target="_blank" rel="noreferrer">${content}</a>` : content}</li>`;
|
|
2398
|
+
}).join("") : `<li class="viewer-ci__empty">No job details reported.</li>`;
|
|
2399
|
+
return `
|
|
2400
|
+
<div class="viewer-ci">
|
|
2401
|
+
<div class="viewer-ci__summary">${cards}</div>
|
|
2402
|
+
<div class="viewer-ci__workspace">
|
|
2403
|
+
<section class="viewer-ci__section">
|
|
2404
|
+
<div class="viewer-ci__heading"><h2>Latest run</h2>${renderCiBadge(state)}</div>
|
|
2405
|
+
<ul class="viewer-ci__list">${runRows}</ul>
|
|
2406
|
+
${runUrl}
|
|
2407
|
+
</section>
|
|
2408
|
+
<section class="viewer-ci__section">
|
|
2409
|
+
<div class="viewer-ci__heading"><h2>Jobs</h2><span>${escapeHtml(jobs.length)} reported</span></div>
|
|
2410
|
+
<ul class="viewer-ci__jobs">${jobRows}</ul>
|
|
2411
|
+
</section>
|
|
2412
|
+
</div>
|
|
2413
|
+
</div>
|
|
2414
|
+
`;
|
|
2415
|
+
}
|
|
2416
|
+
|
|
2417
|
+
async function showCiStatus(options = {}) {
|
|
2418
|
+
if (!isCapabilityAvailable("ci")) {
|
|
2419
|
+
const message = capabilityMessage("ci", "CI is not available for this project.");
|
|
2420
|
+
setDocument("CI status", renderCiStatus({ visible: false, state: capability("ci").state, message }));
|
|
2421
|
+
setMeta(message);
|
|
2422
|
+
return;
|
|
2423
|
+
}
|
|
2424
|
+
if (!options.silent) {
|
|
2425
|
+
setMeta("Checking CI status...");
|
|
2426
|
+
}
|
|
2427
|
+
const response = await fetch("/api/ci-status");
|
|
2428
|
+
let data = {};
|
|
2429
|
+
try {
|
|
2430
|
+
data = await response.json();
|
|
2431
|
+
} catch {
|
|
2432
|
+
data = {};
|
|
2433
|
+
}
|
|
2434
|
+
if (response.status === 404) {
|
|
2435
|
+
setDocument("CI status", renderCiStatus({
|
|
2436
|
+
visible: true,
|
|
2437
|
+
state: "unavailable",
|
|
2438
|
+
badgeState: "unavailable",
|
|
2439
|
+
message: "CI status endpoint unavailable. Restart the local viewer so it loads the current logics-manager backend."
|
|
2440
|
+
}));
|
|
2441
|
+
setMeta("Restart the local viewer to enable CI status.");
|
|
2442
|
+
return;
|
|
2443
|
+
}
|
|
2444
|
+
if (!response.ok || !data.ok) {
|
|
2445
|
+
throw new Error(data.error || "Unable to load CI status.");
|
|
2446
|
+
}
|
|
2447
|
+
updateMainCiBadge(data.payload);
|
|
2448
|
+
setDocument("CI status", renderCiStatus(data.payload));
|
|
2449
|
+
setMeta(options.silent ? "CI status refreshed." : "CI status loaded.");
|
|
2450
|
+
}
|
|
2451
|
+
|
|
1709
2452
|
function renderGitStatus(payload) {
|
|
1710
2453
|
if (!payload || payload.state !== "ok") {
|
|
1711
2454
|
return `
|
|
@@ -1785,9 +2528,16 @@
|
|
|
1785
2528
|
const untrackedSections = renderFileSections(["untracked"]);
|
|
1786
2529
|
const clean = payload.clean ? '<p class="viewer-git__state">Working tree clean.</p>' : "";
|
|
1787
2530
|
const recentCommits = Array.isArray(payload.recentCommits) ? payload.recentCommits : [];
|
|
2531
|
+
const renderGitHistoryReveal = (hiddenCount) => {
|
|
2532
|
+
if (hiddenCount <= 0) {
|
|
2533
|
+
return "";
|
|
2534
|
+
}
|
|
2535
|
+
const nextCount = Math.min(gitHistoryPageSize, hiddenCount);
|
|
2536
|
+
return `<li class="viewer-git__commit-row viewer-git__commit-row--reveal"><button class="viewer-git__reveal" type="button" data-viewer-git-history-reveal>Show ${escapeHtml(nextCount)} more</button></li>`;
|
|
2537
|
+
};
|
|
1788
2538
|
const historyRows = recentCommits.length
|
|
1789
|
-
? recentCommits.map((commit) => `
|
|
1790
|
-
<li class="viewer-git__commit-row">
|
|
2539
|
+
? recentCommits.map((commit, index) => `
|
|
2540
|
+
<li class="viewer-git__commit-row" ${index >= gitHistoryPageSize ? "hidden data-viewer-git-history-hidden" : ""}>
|
|
1791
2541
|
<div class="viewer-git__commit-main">
|
|
1792
2542
|
<code>${escapeHtml(commit.hash || "")}</code>
|
|
1793
2543
|
<strong>${escapeHtml(commit.subject || "Untitled commit")}</strong>
|
|
@@ -1797,7 +2547,7 @@
|
|
|
1797
2547
|
${commit.refs ? `<span class="viewer-git__commit-refs">${escapeHtml(commit.refs)}</span>` : ""}
|
|
1798
2548
|
</div>
|
|
1799
2549
|
</li>
|
|
1800
|
-
`).join("")
|
|
2550
|
+
`).join("") + renderGitHistoryReveal(Math.max(0, recentCommits.length - gitHistoryPageSize))
|
|
1801
2551
|
: `<li class="viewer-git__commit-row">${escapeHtml(payload.latestCommit || "No commit history available.")}</li>`;
|
|
1802
2552
|
const history = `
|
|
1803
2553
|
<section class="viewer-git__section">
|
|
@@ -1944,6 +2694,12 @@
|
|
|
1944
2694
|
|
|
1945
2695
|
async function showGitStatus(options = {}) {
|
|
1946
2696
|
const previous = options.preserve ? currentGitViewState() : { domain: "changes", path: "", cached: false };
|
|
2697
|
+
if (!isCapabilityAvailable("git")) {
|
|
2698
|
+
const message = capabilityMessage("git", "Git is not available for this project.");
|
|
2699
|
+
setDocument("Git status", renderGitStatus({ state: capability("git").state, message }));
|
|
2700
|
+
setMeta(message);
|
|
2701
|
+
return;
|
|
2702
|
+
}
|
|
1947
2703
|
if (!options.silent) {
|
|
1948
2704
|
setMeta("Checking Git status...");
|
|
1949
2705
|
}
|
|
@@ -1991,6 +2747,10 @@
|
|
|
1991
2747
|
refreshViewer("POST").catch((error) => setMeta(error.message));
|
|
1992
2748
|
return;
|
|
1993
2749
|
}
|
|
2750
|
+
if (message.type === "bootstrap-logics") {
|
|
2751
|
+
bootstrapLogicsProject().catch((error) => setMeta(error.message));
|
|
2752
|
+
return;
|
|
2753
|
+
}
|
|
1994
2754
|
if (message.type === "open" || message.type === "read") {
|
|
1995
2755
|
const item = latestItems.find((entry) => entry.id === message.id);
|
|
1996
2756
|
showDocument(item).catch((error) => setMeta(error.message));
|
|
@@ -2021,6 +2781,7 @@
|
|
|
2021
2781
|
applyLocalViewerChrome();
|
|
2022
2782
|
[document.getElementById("viewer-insights")].forEach((button) => {
|
|
2023
2783
|
button?.addEventListener("click", () => {
|
|
2784
|
+
setRefreshMenuOpen(false);
|
|
2024
2785
|
showCorpusInsights().catch((error) => setMeta(error.message));
|
|
2025
2786
|
});
|
|
2026
2787
|
});
|
|
@@ -2070,14 +2831,25 @@
|
|
|
2070
2831
|
});
|
|
2071
2832
|
});
|
|
2072
2833
|
document.getElementById("viewer-health")?.addEventListener("click", () => {
|
|
2834
|
+
setRefreshMenuOpen(false);
|
|
2073
2835
|
showHealth().catch((error) => setMeta(error.message));
|
|
2074
2836
|
});
|
|
2075
2837
|
document.getElementById("viewer-git")?.addEventListener("click", () => {
|
|
2076
2838
|
showGitStatus().catch((error) => setMeta(error.message));
|
|
2077
2839
|
});
|
|
2840
|
+
ciButton()?.addEventListener("click", () => {
|
|
2841
|
+
showCiStatus().catch((error) => setMeta(error.message));
|
|
2842
|
+
});
|
|
2078
2843
|
document.getElementById("viewer-cdx")?.addEventListener("click", () => {
|
|
2079
2844
|
showCdxStatus().catch((error) => setMeta(error.message));
|
|
2080
2845
|
});
|
|
2846
|
+
repoPill()?.addEventListener("click", () => {
|
|
2847
|
+
const menu = projectMenu();
|
|
2848
|
+
setProjectMenuOpen(Boolean(menu?.hidden));
|
|
2849
|
+
});
|
|
2850
|
+
repoFolderButton()?.addEventListener("click", () => {
|
|
2851
|
+
openRepositoryFolder().catch((error) => setMeta(error.message));
|
|
2852
|
+
});
|
|
2081
2853
|
activityClearControl()?.addEventListener("click", () => {
|
|
2082
2854
|
clearActivityHistory();
|
|
2083
2855
|
});
|
|
@@ -2110,8 +2882,66 @@
|
|
|
2110
2882
|
const healthTarget = event.target instanceof Element ? event.target.closest("[data-viewer-open-health]") : null;
|
|
2111
2883
|
const filterTarget = event.target instanceof Element ? event.target.closest("[data-viewer-filter-group][data-viewer-filter-value]") : null;
|
|
2112
2884
|
const revealTarget = event.target instanceof Element ? event.target.closest("[data-viewer-reveal]") : null;
|
|
2885
|
+
const gitHistoryRevealTarget = event.target instanceof Element ? event.target.closest("[data-viewer-git-history-reveal]") : null;
|
|
2113
2886
|
const gitDomainTarget = event.target instanceof Element ? event.target.closest(".viewer-git__domain[data-viewer-git-domain]") : null;
|
|
2114
2887
|
const gitFileTarget = event.target instanceof Element ? event.target.closest("[data-viewer-git-file]") : null;
|
|
2888
|
+
const projectSwitcherTarget = event.target instanceof Element ? event.target.closest("#viewer-repo-pill") : null;
|
|
2889
|
+
const projectTarget = event.target instanceof Element ? event.target.closest("[data-viewer-project-id]") : null;
|
|
2890
|
+
const cdxModeTarget = event.target instanceof Element ? event.target.closest("[data-viewer-cdx-mode]") : null;
|
|
2891
|
+
const cdxReportTarget = event.target instanceof Element ? event.target.closest("[data-viewer-cdx-report]") : null;
|
|
2892
|
+
const cdxCreateRequestTarget = event.target instanceof Element ? event.target.closest("[data-viewer-cdx-create-request]") : null;
|
|
2893
|
+
if (cdxReportTarget instanceof HTMLElement) {
|
|
2894
|
+
showCdxReport(cdxReportTarget.getAttribute("data-viewer-cdx-report") || "").catch((error) => setMeta(error.message));
|
|
2895
|
+
return;
|
|
2896
|
+
}
|
|
2897
|
+
if (cdxCreateRequestTarget instanceof HTMLElement) {
|
|
2898
|
+
createRequestFromCdxReport(cdxCreateRequestTarget.getAttribute("data-viewer-cdx-create-request") || "").catch((error) => setMeta(error.message));
|
|
2899
|
+
return;
|
|
2900
|
+
}
|
|
2901
|
+
if (cdxModeTarget instanceof HTMLElement) {
|
|
2902
|
+
const mode = cdxModeTarget.getAttribute("data-viewer-cdx-mode") || "status";
|
|
2903
|
+
if (mode === "runs") {
|
|
2904
|
+
showCdxRuns().catch((error) => setMeta(error.message));
|
|
2905
|
+
} else {
|
|
2906
|
+
showCdxStatus().catch((error) => setMeta(error.message));
|
|
2907
|
+
}
|
|
2908
|
+
return;
|
|
2909
|
+
}
|
|
2910
|
+
if (projectSwitcherTarget instanceof HTMLElement) {
|
|
2911
|
+
const menu = projectMenu();
|
|
2912
|
+
setProjectMenuOpen(Boolean(menu?.hidden));
|
|
2913
|
+
return;
|
|
2914
|
+
}
|
|
2915
|
+
if (projectTarget instanceof HTMLElement) {
|
|
2916
|
+
event.preventDefault();
|
|
2917
|
+
switchViewerProject(projectTarget.getAttribute("data-viewer-project-id") || "").catch((error) => setMeta(error.message));
|
|
2918
|
+
return;
|
|
2919
|
+
}
|
|
2920
|
+
if (gitHistoryRevealTarget instanceof HTMLElement) {
|
|
2921
|
+
event.preventDefault();
|
|
2922
|
+
event.stopImmediatePropagation();
|
|
2923
|
+
if (gitHistoryRevealTarget.dataset.viewerGitHistoryBusy === "true") {
|
|
2924
|
+
return;
|
|
2925
|
+
}
|
|
2926
|
+
gitHistoryRevealTarget.dataset.viewerGitHistoryBusy = "true";
|
|
2927
|
+
const list = gitHistoryRevealTarget.closest("ul");
|
|
2928
|
+
const hiddenRows = Array.from(list?.querySelectorAll("[data-viewer-git-history-hidden]") || [])
|
|
2929
|
+
.filter((row) => row instanceof HTMLElement);
|
|
2930
|
+
hiddenRows.slice(0, gitHistoryPageSize).forEach((row) => {
|
|
2931
|
+
if (row instanceof HTMLElement) {
|
|
2932
|
+
row.hidden = false;
|
|
2933
|
+
row.removeAttribute("data-viewer-git-history-hidden");
|
|
2934
|
+
}
|
|
2935
|
+
});
|
|
2936
|
+
const remaining = Array.from(list?.querySelectorAll("[data-viewer-git-history-hidden]") || []).length;
|
|
2937
|
+
if (remaining > 0) {
|
|
2938
|
+
gitHistoryRevealTarget.textContent = `Show ${Math.min(gitHistoryPageSize, remaining)} more`;
|
|
2939
|
+
gitHistoryRevealTarget.dataset.viewerGitHistoryBusy = "false";
|
|
2940
|
+
} else {
|
|
2941
|
+
gitHistoryRevealTarget.closest("li")?.remove();
|
|
2942
|
+
}
|
|
2943
|
+
return;
|
|
2944
|
+
}
|
|
2115
2945
|
if (revealTarget instanceof HTMLElement) {
|
|
2116
2946
|
const list = revealTarget.closest("ul");
|
|
2117
2947
|
list?.querySelectorAll("[data-viewer-hidden-row]").forEach((row) => {
|