@deksden-com/dd-flow-cli 0.3.0 → 0.4.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/CHANGELOG.md +55 -0
- package/README.md +74 -5
- package/dist/build-info.json +5 -5
- package/dist/cli/help.js +116 -18
- package/dist/cli/run-cli.js +361 -29
- package/dist/schemas/compatibility.schema.json +81 -2
- package/dist/schemas/engine-manifest.schema.json +61 -0
- package/dist/schemas/flow-guidance.schema.json +17 -0
- package/dist/schemas/global-dashboard-data.schema.json +60 -2
- package/dist/schemas/mb-upgrade-migration-report.schema.json +93 -0
- package/dist/schemas/project-dashboard-data.schema.json +26 -2
- package/dist/schemas/project-summary.schema.json +73 -0
- package/dist/schemas/protocol-dashboard-data.schema.json +25 -2
- package/dist/schemas/status-report.schema.json +4 -2
- package/dist/services/branch-context.js +254 -0
- package/dist/services/cleanup.js +31 -0
- package/dist/services/cli-operation-classifier.js +104 -0
- package/dist/services/compatibility-preflight.js +127 -0
- package/dist/services/config.js +6 -0
- package/dist/services/dashboard-targets.js +95 -0
- package/dist/services/dashboard.js +376 -59
- package/dist/services/engines.js +532 -0
- package/dist/services/flow-guidance.js +8 -1
- package/dist/services/hooks.js +1 -1
- package/dist/services/lanes.js +333 -1
- package/dist/services/merge-queue.js +310 -15
- package/dist/services/merge-worker.js +44 -3
- package/dist/services/migrations.js +231 -0
- package/dist/services/project-summary.js +122 -0
- package/dist/services/projects.js +41 -6
- package/dist/services/protocol-lifecycle.js +144 -0
- package/dist/services/protocols.js +34 -7
- package/dist/services/sessions.js +21 -4
- package/dist/services/status.js +10 -0
- package/dist/services/version-status.js +39 -15
- package/dist/storage/database.js +25 -0
- package/dist/storage/paths.js +12 -0
- package/package.json +3 -2
|
@@ -4,11 +4,13 @@ import { spawnSync } from "node:child_process";
|
|
|
4
4
|
import { isActiveProtocolStatus, isActiveQueueStatus, isQueueHistoryStatus } from "../domain/contracts.js";
|
|
5
5
|
import { AppError } from "../shared/errors.js";
|
|
6
6
|
import { ensureDir, resolveProjectRoot } from "../storage/paths.js";
|
|
7
|
-
import { dashboardMarkdownPath, globalDashboardHtmlPath, globalDashboardJsonPath, globalDashboardMarkdownPath, projectDashboardHtmlPath, projectDashboardJsonPath, protocolDashboardHtmlPath, protocolDashboardJsonPath, readProjectConfig } from "./config.js";
|
|
7
|
+
import { dashboardMarkdownPath, globalDashboardHtmlPath, globalDashboardJsonPath, globalDashboardMarkdownPath, projectDashboardHtmlPath, projectDashboardJsonPath, projectSummaryJsonPath, protocolDashboardHtmlPath, protocolDashboardJsonPath, readProjectConfig } from "./config.js";
|
|
8
8
|
import { queueForProject } from "./merge-queue.js";
|
|
9
|
+
import { publishProjectSummary } from "./project-summary.js";
|
|
9
10
|
import { requireProjectByRoot } from "./projects.js";
|
|
10
11
|
import { protocolRunDiagnostics, protocolSetBoardForProtocol, protocolSetBoardsForProject, readProtocolRuntimeState, requireProtocol } from "./protocols.js";
|
|
11
12
|
import { activeFlowSessionsForProject } from "./sessions.js";
|
|
13
|
+
import { normalizeProtocolLifecycle } from "./protocol-lifecycle.js";
|
|
12
14
|
export function getCmuxStatus(context, input) {
|
|
13
15
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
14
16
|
return { ok: true, project_root: project.root, cmux: detectCmux(context) };
|
|
@@ -45,12 +47,49 @@ export function renderGlobalDashboard(context, input = {}) {
|
|
|
45
47
|
export function openDashboard(context, input) {
|
|
46
48
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
47
49
|
const config = readProjectConfig(context, project.id);
|
|
48
|
-
const format = parseDashboardFormat(input.format);
|
|
50
|
+
const format = parseDashboardFormat(input.format, "html");
|
|
49
51
|
if (input.viewer && input.viewer !== "cmux") {
|
|
50
52
|
throw new AppError("validation", "--viewer must be cmux", 2);
|
|
51
53
|
}
|
|
52
54
|
const dashboardPath = format === "html" ? projectDashboardHtmlPath(context, project.id) : dashboardMarkdownPath(project.root, config);
|
|
53
|
-
return
|
|
55
|
+
return {
|
|
56
|
+
ok: true,
|
|
57
|
+
project_root: project.root,
|
|
58
|
+
dashboard: { path: dashboardPath },
|
|
59
|
+
open: openCmuxDashboard(context, project.root, dashboardPath, format)
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
export function openGlobalDashboard(context, input = {}) {
|
|
63
|
+
if (input.viewer && input.viewer !== "cmux") {
|
|
64
|
+
throw new AppError("validation", "--viewer must be cmux", 2);
|
|
65
|
+
}
|
|
66
|
+
const format = parseDashboardFormat(input.format, "html");
|
|
67
|
+
const rendered = renderGlobalDashboard(context, { format });
|
|
68
|
+
return {
|
|
69
|
+
ok: true,
|
|
70
|
+
dashboard: rendered.dashboard,
|
|
71
|
+
open: openDashboardPath(context, rendered.dashboard.path, format)
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
export function openProtocolDashboard(context, input) {
|
|
75
|
+
if (input.viewer && input.viewer !== "cmux") {
|
|
76
|
+
throw new AppError("validation", "--viewer must be cmux", 2);
|
|
77
|
+
}
|
|
78
|
+
const format = parseDashboardFormat(input.format, "html");
|
|
79
|
+
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
80
|
+
if (format !== "html") {
|
|
81
|
+
throw new AppError("validation", "Protocol dashboard open supports --format html", 2, {
|
|
82
|
+
protocol: input.protocol
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
const rendered = renderProtocolDashboardHtml(context, project, input.protocol);
|
|
86
|
+
return {
|
|
87
|
+
ok: true,
|
|
88
|
+
project_root: project.root,
|
|
89
|
+
protocol_id: input.protocol,
|
|
90
|
+
dashboard: rendered.dashboard,
|
|
91
|
+
open: openCmuxDashboard(context, project.root, rendered.dashboard.path, format)
|
|
92
|
+
};
|
|
54
93
|
}
|
|
55
94
|
export function refreshDashboard(context, input) {
|
|
56
95
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
@@ -69,6 +108,7 @@ export function refreshDashboard(context, input) {
|
|
|
69
108
|
};
|
|
70
109
|
}
|
|
71
110
|
const rendered = renderDashboard(context, { projectRoot: project.root, format, ...(input.protocol ? { protocol: input.protocol } : {}) });
|
|
111
|
+
const projectSummary = input.protocol ? null : publishProjectSummary(context, { project });
|
|
72
112
|
const global = config.dashboard.global
|
|
73
113
|
? renderGlobalDashboard(context, {
|
|
74
114
|
output: format === "html" ? globalDashboardHtmlPath(context) : globalDashboardMarkdownPath(context, config),
|
|
@@ -80,6 +120,7 @@ export function refreshDashboard(context, input) {
|
|
|
80
120
|
ok: true,
|
|
81
121
|
project_root: project.root,
|
|
82
122
|
dashboard: rendered.dashboard,
|
|
123
|
+
project_summary: projectSummary,
|
|
83
124
|
global_dashboard: global.dashboard,
|
|
84
125
|
open: shouldOpen ? openCmuxDashboard(context, project.root, rendered.dashboard.path, format) : { ok: true, skipped: true, reason: "open_disabled" }
|
|
85
126
|
};
|
|
@@ -87,6 +128,44 @@ export function refreshDashboard(context, input) {
|
|
|
87
128
|
export function refreshGlobalDashboard(context, input = {}) {
|
|
88
129
|
return renderGlobalDashboard(context, input);
|
|
89
130
|
}
|
|
131
|
+
export function refreshAllDashboards(context, input = {}) {
|
|
132
|
+
const format = parseDashboardFormat(input.format, "html");
|
|
133
|
+
const global = renderGlobalDashboard(context, { format });
|
|
134
|
+
const projects = context.db.all("SELECT * FROM projects WHERE status = 'active' ORDER BY id ASC");
|
|
135
|
+
const results = projects.map((project) => {
|
|
136
|
+
if (!fs.existsSync(project.root)) {
|
|
137
|
+
return { ok: true, project_id: project.id, project_root: project.root, skipped: true, reason: "project_root_missing" };
|
|
138
|
+
}
|
|
139
|
+
const config = readProjectConfig(context, project.id);
|
|
140
|
+
if (!config.dashboard.project) {
|
|
141
|
+
return { ok: true, project_id: project.id, project_root: project.root, skipped: true, reason: "dashboard_project_disabled" };
|
|
142
|
+
}
|
|
143
|
+
try {
|
|
144
|
+
const projectSummary = publishProjectSummary(context, { project });
|
|
145
|
+
const rendered = renderDashboard(context, { projectRoot: project.root, format });
|
|
146
|
+
return { ok: true, project_id: project.id, project_root: project.root, dashboard: rendered.dashboard, project_summary: projectSummary };
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
return {
|
|
150
|
+
ok: false,
|
|
151
|
+
project_id: project.id,
|
|
152
|
+
project_root: project.root,
|
|
153
|
+
error: error instanceof Error ? error.message : String(error)
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
return {
|
|
158
|
+
ok: true,
|
|
159
|
+
dashboard: global.dashboard,
|
|
160
|
+
projects: results,
|
|
161
|
+
summary: {
|
|
162
|
+
total: results.length,
|
|
163
|
+
refreshed: results.filter((item) => item.ok && !("skipped" in item)).length,
|
|
164
|
+
skipped: results.filter((item) => "skipped" in item).length,
|
|
165
|
+
failed: results.filter((item) => !item.ok).length
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
}
|
|
90
169
|
export function autoRefreshDashboards(context, input) {
|
|
91
170
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
92
171
|
const config = readProjectConfig(context, project.id);
|
|
@@ -95,6 +174,7 @@ export function autoRefreshDashboards(context, input) {
|
|
|
95
174
|
}
|
|
96
175
|
const result = { ok: true, project_root: project.root };
|
|
97
176
|
if (config.dashboard.project) {
|
|
177
|
+
publishProjectSummary(context, { project });
|
|
98
178
|
const output = dashboardMarkdownPath(project.root, config);
|
|
99
179
|
const markdown = renderProjectDashboardMarkdown(context, project, output);
|
|
100
180
|
writeMarkdown(output, markdown);
|
|
@@ -168,6 +248,21 @@ function openCmuxDashboard(context, projectRoot, dashboardPath, format = "markdo
|
|
|
168
248
|
}
|
|
169
249
|
return { ok: true, opened: true, command };
|
|
170
250
|
}
|
|
251
|
+
function openDashboardPath(context, dashboardPath, format) {
|
|
252
|
+
const cmux = detectCmux(context);
|
|
253
|
+
if (!cmux.available) {
|
|
254
|
+
return { ok: true, skipped: true, reason: "cmux_unavailable", cmux };
|
|
255
|
+
}
|
|
256
|
+
if (context.env.DD_FLOW_CMUX_DRY_RUN === "true") {
|
|
257
|
+
return { ok: true, opened: false, dry_run: true, command: cmuxOpenCommand(format, dashboardPath) };
|
|
258
|
+
}
|
|
259
|
+
const command = cmuxOpenCommand(format, dashboardPath);
|
|
260
|
+
const result = spawnSync(command[0], command.slice(1), { encoding: "utf8" });
|
|
261
|
+
if (result.status !== 0) {
|
|
262
|
+
return { ok: true, skipped: true, reason: "cmux_open_failed", status: result.status, stderr: result.stderr.trim() };
|
|
263
|
+
}
|
|
264
|
+
return { ok: true, opened: true, command };
|
|
265
|
+
}
|
|
171
266
|
function cmuxOpenCommand(format, dashboardPath) {
|
|
172
267
|
return format === "html"
|
|
173
268
|
? ["cmux", "browser", "open", pathToFileUrl(dashboardPath)]
|
|
@@ -185,8 +280,8 @@ function detectCmux(context) {
|
|
|
185
280
|
? { available: true, source: "path", version: result.stdout.trim() }
|
|
186
281
|
: { available: false, source: "path", reason: result.error?.message ?? result.stderr.trim() ?? "cmux not found" };
|
|
187
282
|
}
|
|
188
|
-
function parseDashboardFormat(value) {
|
|
189
|
-
const format = value ??
|
|
283
|
+
function parseDashboardFormat(value, defaultFormat = "markdown") {
|
|
284
|
+
const format = value ?? defaultFormat;
|
|
190
285
|
if (format !== "markdown" && format !== "html") {
|
|
191
286
|
throw new AppError("validation", "--format must be markdown or html", 2, { format });
|
|
192
287
|
}
|
|
@@ -264,35 +359,11 @@ function renderGlobalDashboardHtml(context, output) {
|
|
|
264
359
|
function buildGlobalDashboardData(context, htmlPath) {
|
|
265
360
|
const projects = context.db.all("SELECT * FROM projects ORDER BY status ASC, updated_at DESC, root ASC");
|
|
266
361
|
const activeProjects = projects.filter((project) => project.status === "active");
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
const sessions = activeFlowSessionsForProject(context, project.id);
|
|
273
|
-
const openDefs = protocols.reduce((count, protocol) => count + jsonArray(protocol.active_def_json).length + jsonArray(protocol.blockers_json).length, 0);
|
|
274
|
-
return {
|
|
275
|
-
id: project.id,
|
|
276
|
-
title: path.basename(project.root) || project.id,
|
|
277
|
-
root: project.root,
|
|
278
|
-
short_root: compactPath(project.root),
|
|
279
|
-
status: project.status,
|
|
280
|
-
root_exists: fs.existsSync(project.root),
|
|
281
|
-
display_status: fs.existsSync(project.root) ? normalizeDisplayStatus(project.status) : "stale",
|
|
282
|
-
action_level: openDefs > 0 || locks.length > 0 ? "watch" : "none",
|
|
283
|
-
updated_at: project.updated_at,
|
|
284
|
-
href: projectDashboardHtmlPath(context, project.id),
|
|
285
|
-
active_protocols: activeProtocols.slice(0, 6).map(protocolCardMini),
|
|
286
|
-
metrics: {
|
|
287
|
-
active_protocols: activeProtocols.length,
|
|
288
|
-
queue: queue.filter((job) => isActiveQueueStatus(job.status)).length,
|
|
289
|
-
locks: locks.length,
|
|
290
|
-
sessions: sessions.length,
|
|
291
|
-
open_defs: openDefs
|
|
292
|
-
},
|
|
293
|
-
warnings: fs.existsSync(project.root) ? [] : ["project_root_missing"]
|
|
294
|
-
};
|
|
295
|
-
});
|
|
362
|
+
const summaryPolicy = globalDashboardSummaryPolicy();
|
|
363
|
+
const classified = projects.map((project) => classifyGlobalProjectSummary(context, project, summaryPolicy));
|
|
364
|
+
const projectCards = classified.flatMap((entry) => (entry.card ? [entry.card] : []));
|
|
365
|
+
const unsupportedProjects = classified.flatMap((entry) => (entry.unsupported ? [entry.unsupported] : []));
|
|
366
|
+
const summaryVersionGroups = buildSummaryVersionGroups(projectCards, unsupportedProjects);
|
|
296
367
|
return {
|
|
297
368
|
schema_id: "dd-flow/global-dashboard-data@1",
|
|
298
369
|
generated_at: context.now(),
|
|
@@ -303,14 +374,170 @@ function buildGlobalDashboardData(context, htmlPath) {
|
|
|
303
374
|
markdown_fallback: link("Markdown fallback", globalDashboardMarkdownPath(context), "available")
|
|
304
375
|
},
|
|
305
376
|
metrics: [
|
|
306
|
-
metric("projects",
|
|
377
|
+
metric("projects", projectCards.filter((card) => card.status === "active").length, "known", "compatible project summaries"),
|
|
378
|
+
metric("total_projects", activeProjects.length, "known", "projects.status=active"),
|
|
379
|
+
metric("unsupported_projects", unsupportedProjects.filter((project) => project.status === "active").length, "known", "project summaries"),
|
|
307
380
|
metric("active_protocols", projectCards.reduce((sum, card) => sum + Number(card.metrics.active_protocols), 0), "known", "protocols"),
|
|
308
381
|
metric("running_sessions", projectCards.reduce((sum, card) => sum + Number(card.metrics.sessions), 0), "known", "flow_sessions"),
|
|
309
382
|
metric("active_locks", projectCards.reduce((sum, card) => sum + Number(card.metrics.locks), 0), "known", "lane_locks"),
|
|
310
383
|
metric("open_defs", projectCards.reduce((sum, card) => sum + Number(card.metrics.open_defs), 0), "known", "protocols.active_def/blockers")
|
|
311
384
|
],
|
|
385
|
+
supported_summary_versions: summaryPolicy.supported.map((contract) => ({
|
|
386
|
+
schema_id: contract.schema_id,
|
|
387
|
+
schema_version: contract.schema_version,
|
|
388
|
+
label: contract.label,
|
|
389
|
+
status: "supported"
|
|
390
|
+
})),
|
|
391
|
+
summary_version_groups: summaryVersionGroups,
|
|
392
|
+
unsupported_projects: unsupportedProjects,
|
|
312
393
|
project_cards: projectCards,
|
|
313
|
-
warnings:
|
|
394
|
+
warnings: [
|
|
395
|
+
...projectCards.filter((card) => !card.root_exists).map((card) => ({ code: "project_root_missing", project_id: card.id })),
|
|
396
|
+
...unsupportedProjects.map((project) => ({ code: project.reason, project_id: project.id, summary_path: project.summary_path }))
|
|
397
|
+
]
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
function globalDashboardSummaryPolicy() {
|
|
401
|
+
return {
|
|
402
|
+
supported: [{ schema_id: "dd-flow/project-summary@1", schema_version: null, label: "project-summary@1" }],
|
|
403
|
+
install_hint: "Run the project-compatible dd-flow engine and refresh the project summary."
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
function classifyGlobalProjectSummary(context, project, policy) {
|
|
407
|
+
const summaryPath = projectSummaryJsonPath(context, project.id);
|
|
408
|
+
const parsed = readSummaryFile(summaryPath);
|
|
409
|
+
if (!parsed.ok) {
|
|
410
|
+
return {
|
|
411
|
+
card: null,
|
|
412
|
+
unsupported: unsupportedProject(project, {
|
|
413
|
+
reason: parsed.reason,
|
|
414
|
+
detail: parsed.detail,
|
|
415
|
+
summary_path: summaryPath,
|
|
416
|
+
install_hint: policy.install_hint
|
|
417
|
+
})
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
const summary = parsed.summary;
|
|
421
|
+
const schemaId = stringValue(summary.schema_id);
|
|
422
|
+
const schemaVersion = stringValue(summary.schema_version);
|
|
423
|
+
const supported = policy.supported.some((contract) => contract.schema_id === schemaId && (!contract.schema_version || contract.schema_version === schemaVersion));
|
|
424
|
+
if (!supported) {
|
|
425
|
+
return {
|
|
426
|
+
card: null,
|
|
427
|
+
unsupported: unsupportedProject(project, {
|
|
428
|
+
reason: "unsupported_summary_version",
|
|
429
|
+
detail: `Unsupported project summary contract: ${schemaId ?? "unknown"} ${schemaVersion ?? ""}`.trim(),
|
|
430
|
+
summary_path: summaryPath,
|
|
431
|
+
summary
|
|
432
|
+
})
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
return { card: projectCardFromSummary(context, project, summary), unsupported: null };
|
|
436
|
+
}
|
|
437
|
+
function readSummaryFile(file) {
|
|
438
|
+
if (!fs.existsSync(file)) {
|
|
439
|
+
return { ok: false, reason: "missing_summary", detail: "Published project summary is missing." };
|
|
440
|
+
}
|
|
441
|
+
try {
|
|
442
|
+
const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
443
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
444
|
+
return { ok: false, reason: "invalid_summary", detail: "Published project summary is not a JSON object." };
|
|
445
|
+
}
|
|
446
|
+
return { ok: true, summary: parsed };
|
|
447
|
+
}
|
|
448
|
+
catch (error) {
|
|
449
|
+
return { ok: false, reason: "unreadable_summary", detail: error instanceof Error ? error.message : "Failed to read project summary." };
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
function unsupportedProject(project, input) {
|
|
453
|
+
const summary = input.summary;
|
|
454
|
+
return {
|
|
455
|
+
id: project.id,
|
|
456
|
+
title: stringValue(summary?.name) ?? (path.basename(project.root) || project.id),
|
|
457
|
+
root: project.root,
|
|
458
|
+
short_root: compactPath(project.root),
|
|
459
|
+
status: project.status,
|
|
460
|
+
root_exists: fs.existsSync(project.root),
|
|
461
|
+
display_status: "unsupported",
|
|
462
|
+
action_level: "watch",
|
|
463
|
+
reason: input.reason,
|
|
464
|
+
detail: input.detail,
|
|
465
|
+
memorybank_version: summary?.memorybank_version ?? null,
|
|
466
|
+
summary_schema_id: stringValue(summary?.schema_id),
|
|
467
|
+
summary_schema_version: stringValue(summary?.schema_version),
|
|
468
|
+
summary_path: input.summary_path,
|
|
469
|
+
required_engine_range: summary?.required_engine_range ?? null,
|
|
470
|
+
install_hint: input.install_hint ?? summary?.install_hint ?? null,
|
|
471
|
+
updated_at: stringValue(summary?.last_activity_at) ?? project.updated_at
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
function buildSummaryVersionGroups(projectCards, unsupportedProjects) {
|
|
475
|
+
const groups = new Map();
|
|
476
|
+
const add = (entry, compatible) => {
|
|
477
|
+
const schemaId = stringValue(entry.summary_schema_id) ?? "missing";
|
|
478
|
+
const schemaVersion = stringValue(entry.summary_schema_version) ?? "unknown";
|
|
479
|
+
const key = `${schemaId}:${schemaVersion}:${compatible ? "compatible" : "unsupported"}`;
|
|
480
|
+
const existing = groups.get(key) ??
|
|
481
|
+
{
|
|
482
|
+
schema_id: schemaId,
|
|
483
|
+
schema_version: schemaVersion,
|
|
484
|
+
compatible,
|
|
485
|
+
count: 0,
|
|
486
|
+
project_ids: [],
|
|
487
|
+
project_names: []
|
|
488
|
+
};
|
|
489
|
+
existing.count = numberValue(existing.count) + 1;
|
|
490
|
+
existing.project_ids.push(String(entry.id));
|
|
491
|
+
existing.project_names.push(String(entry.title));
|
|
492
|
+
groups.set(key, existing);
|
|
493
|
+
};
|
|
494
|
+
projectCards.forEach((card) => add(card, true));
|
|
495
|
+
unsupportedProjects.forEach((project) => add(project, false));
|
|
496
|
+
return [...groups.values()].sort((a, b) => String(a.schema_id).localeCompare(String(b.schema_id)) || String(a.schema_version).localeCompare(String(b.schema_version)));
|
|
497
|
+
}
|
|
498
|
+
function projectCardFromSummary(context, project, summary) {
|
|
499
|
+
const metrics = recordValue(summary.resource_summary);
|
|
500
|
+
const protocolCounts = recordValue(summary.protocol_counts);
|
|
501
|
+
const rootExists = Boolean(summary.root_exists);
|
|
502
|
+
const openDefs = numberValue(metrics?.open_defs);
|
|
503
|
+
const locks = numberValue(metrics?.locks);
|
|
504
|
+
return {
|
|
505
|
+
id: project.id,
|
|
506
|
+
title: stringValue(summary.name) ?? (path.basename(project.root) || project.id),
|
|
507
|
+
root: project.root,
|
|
508
|
+
short_root: compactPath(project.root),
|
|
509
|
+
status: project.status,
|
|
510
|
+
root_exists: rootExists,
|
|
511
|
+
display_status: rootExists ? normalizeDisplayStatus(project.status) : "stale",
|
|
512
|
+
action_level: openDefs > 0 || locks > 0 ? "watch" : "none",
|
|
513
|
+
updated_at: stringValue(summary.last_activity_at) ?? project.updated_at,
|
|
514
|
+
href: projectDashboardHtmlPath(context, project.id),
|
|
515
|
+
summary_source: "project_summary",
|
|
516
|
+
summary_schema_id: stringValue(summary.schema_id),
|
|
517
|
+
summary_schema_version: stringValue(summary.schema_version),
|
|
518
|
+
summary_path: stringValue(summary.summary_path),
|
|
519
|
+
memorybank_version: summary.memorybank_version ?? null,
|
|
520
|
+
cli_version: summary.cli_version ?? null,
|
|
521
|
+
engine_version: summary.engine_version ?? null,
|
|
522
|
+
active_protocols: Array.isArray(summary.active_protocols) ? summary.active_protocols : [],
|
|
523
|
+
lifecycle_summary: {
|
|
524
|
+
total: numberValue(protocolCounts?.active),
|
|
525
|
+
by_lifecycle: []
|
|
526
|
+
},
|
|
527
|
+
resource_summary: {
|
|
528
|
+
queued_protocols: numberValue(metrics?.queue),
|
|
529
|
+
active_locks: locks,
|
|
530
|
+
queued_waiters: numberValue(metrics?.waiters)
|
|
531
|
+
},
|
|
532
|
+
metrics: {
|
|
533
|
+
active_protocols: numberValue(protocolCounts?.active),
|
|
534
|
+
queue: numberValue(metrics?.queue),
|
|
535
|
+
locks,
|
|
536
|
+
waiters: numberValue(metrics?.waiters),
|
|
537
|
+
sessions: numberValue(metrics?.sessions),
|
|
538
|
+
open_defs: openDefs
|
|
539
|
+
},
|
|
540
|
+
warnings: Array.isArray(summary.warnings) ? summary.warnings : []
|
|
314
541
|
};
|
|
315
542
|
}
|
|
316
543
|
function buildProjectDashboardData(context, project, htmlPath) {
|
|
@@ -318,11 +545,13 @@ function buildProjectDashboardData(context, project, htmlPath) {
|
|
|
318
545
|
const queue = queueForProject(context, project.id);
|
|
319
546
|
const sessions = activeFlowSessionsForProject(context, project.id);
|
|
320
547
|
const locks = context.db.all("SELECT lane, worker_id, status, expires_at FROM lane_locks WHERE project_id = ? ORDER BY updated_at DESC, id DESC LIMIT 20", [project.id]);
|
|
548
|
+
const waiters = context.db.all("SELECT id, lane, worker_id, status, queued_at, expires_at, reason, updated_at FROM lane_waiters WHERE project_id = ? ORDER BY updated_at DESC, id DESC LIMIT 20", [project.id]);
|
|
321
549
|
const activeProtocols = protocols.filter((protocol) => isActiveProtocolStatus(protocol.status));
|
|
322
550
|
const protocolCards = protocols.slice(0, 48).map((protocol, index) => buildProtocolCard(context, project, protocol, index < activeProtocols.length + 12));
|
|
323
551
|
const openDefs = protocols.reduce((count, protocol) => count + jsonArray(protocol.active_def_json).length + jsonArray(protocol.blockers_json).length, 0);
|
|
324
552
|
const protocolSetBoards = protocolSetBoardsForProject(context, project.id, project.root);
|
|
325
553
|
const reviewRuns = recentReviewRunsForProject(context, project.id, 8);
|
|
554
|
+
const queuedProtocols = queue.slice(0, 12).map(queueItemDashboard);
|
|
326
555
|
return {
|
|
327
556
|
schema_id: "dd-flow/project-dashboard-data@1",
|
|
328
557
|
generated_at: context.now(),
|
|
@@ -345,22 +574,18 @@ function buildProjectDashboardData(context, project, htmlPath) {
|
|
|
345
574
|
metric("completed_protocols", protocols.filter((protocol) => !isActiveProtocolStatus(protocol.status)).length, "known", "protocols"),
|
|
346
575
|
metric("merge_queue", queue.filter((job) => isActiveQueueStatus(job.status)).length, "known", "merge_queue"),
|
|
347
576
|
metric("active_locks", locks.filter((lock) => lock.status === "active").length, "known", "lane_locks"),
|
|
577
|
+
metric("queued_waiters", waiters.filter((waiter) => waiter.status === "queued").length, "known", "lane_waiters"),
|
|
348
578
|
metric("open_defs", openDefs, "known", "protocols.active_def/blockers"),
|
|
349
579
|
metric("running_sessions", sessions.length, "known", "flow_sessions"),
|
|
350
580
|
metric("review_runs", reviewRuns.length, "known", "flow_runs.flow_kind=mb-sdlc-review|review")
|
|
351
581
|
],
|
|
582
|
+
lifecycle_summary: lifecycleSummary(activeProtocols),
|
|
583
|
+
resource_summary: resourceSummary(queue, locks, waiters),
|
|
352
584
|
protocol_cards: protocolCards,
|
|
353
585
|
protocol_set_boards: protocolSetBoards,
|
|
354
586
|
review_runs: reviewRuns,
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
status: job.status,
|
|
358
|
-
display_status: normalizeDisplayStatus(job.status),
|
|
359
|
-
action_level: ["ready", "requeued"].includes(job.status) ? "actionable" : job.status === "claimed" ? "watch" : "none",
|
|
360
|
-
owner: job.claimed_by_session_id,
|
|
361
|
-
note: job.last_reason,
|
|
362
|
-
updated_at: job.updated_at
|
|
363
|
-
})),
|
|
587
|
+
queued_protocols: queuedProtocols,
|
|
588
|
+
merge_queue: queuedProtocols,
|
|
364
589
|
sessions: sessions.slice(0, 12).map((session) => ({
|
|
365
590
|
id: session.session_id,
|
|
366
591
|
flow_kind: session.flow_kind,
|
|
@@ -374,6 +599,16 @@ function buildProjectDashboardData(context, project, htmlPath) {
|
|
|
374
599
|
updated_at: session.updated_at
|
|
375
600
|
})),
|
|
376
601
|
locks: locks.slice(0, 12).map((lock) => ({ lane: lock.lane, worker_id: lock.worker_id, status: lock.status, expires_at: lock.expires_at })),
|
|
602
|
+
waiters: waiters.slice(0, 12).map((waiter) => ({
|
|
603
|
+
id: waiter.id,
|
|
604
|
+
lane: waiter.lane,
|
|
605
|
+
worker_id: waiter.worker_id,
|
|
606
|
+
status: waiter.status,
|
|
607
|
+
queued_at: waiter.queued_at,
|
|
608
|
+
expires_at: waiter.expires_at,
|
|
609
|
+
reason: waiter.reason,
|
|
610
|
+
updated_at: waiter.updated_at
|
|
611
|
+
})),
|
|
377
612
|
warnings: fs.existsSync(project.root) ? [] : [{ code: "project_root_missing", path: project.root }]
|
|
378
613
|
};
|
|
379
614
|
}
|
|
@@ -384,6 +619,8 @@ function buildProtocolDashboardData(context, project, protocolId, htmlPath) {
|
|
|
384
619
|
}
|
|
385
620
|
const runtimeProtocol = requireProtocol(context, protocol.id);
|
|
386
621
|
const runtimeState = readProtocolRuntimeState(context, runtimeProtocol).state;
|
|
622
|
+
const lifecycle = normalizeProtocolLifecycle({ state: runtimeState });
|
|
623
|
+
const queueItem = queueForProject(context, project.id).find((item) => item.protocol_id === protocolId) ?? null;
|
|
387
624
|
const runDiagnostics = protocolRunDiagnostics(context, runtimeProtocol, runtimeState);
|
|
388
625
|
const runs = context.db.all(`SELECT id, short_id, flow_kind, subject_type, subject_id, status, verdict, next_action, run_index_path, index_json, created_at, updated_at, completed_at
|
|
389
626
|
FROM flow_runs WHERE project_id = ? AND subject_type = 'protocol' AND subject_id = ?
|
|
@@ -425,6 +662,9 @@ function buildProtocolDashboardData(context, project, protocolId, htmlPath) {
|
|
|
425
662
|
title: protocol.id,
|
|
426
663
|
raw_status: protocol.status,
|
|
427
664
|
raw_stage: protocol.stage,
|
|
665
|
+
lifecycle,
|
|
666
|
+
lifecycle_stage: lifecycle.stage,
|
|
667
|
+
lifecycle_status: lifecycle.status,
|
|
428
668
|
display_status: normalizeDisplayStatus(protocol.status),
|
|
429
669
|
action_level: actionLevel(protocol.status, jsonArray(protocol.blockers_json).length + jsonArray(protocol.active_def_json).length),
|
|
430
670
|
next_action: protocol.next_action,
|
|
@@ -435,6 +675,18 @@ function buildProtocolDashboardData(context, project, protocolId, htmlPath) {
|
|
|
435
675
|
source_path: path.join(project.root, ".memory-bank", "protocol", `${protocol.id}.md`),
|
|
436
676
|
summary_path: path.join(project.root, ".memory-bank", "protocol", protocol.id, "summary.md")
|
|
437
677
|
},
|
|
678
|
+
resource: {
|
|
679
|
+
kind: "protocol",
|
|
680
|
+
lane: queueItem ? "merge" : null,
|
|
681
|
+
queue_status: queueItem?.status ?? null,
|
|
682
|
+
claim: queueItem?.claimed_by_session_id
|
|
683
|
+
? { protocol_id: protocol.id, worker_id: queueItem.claimed_by_session_id, claimed_at: queueItem.claimed_at, status: queueItem.status }
|
|
684
|
+
: null
|
|
685
|
+
},
|
|
686
|
+
queue_item: queueItem ? queueItemDashboard(queueItem) : null,
|
|
687
|
+
claim: queueItem?.claimed_by_session_id
|
|
688
|
+
? { protocol_id: protocol.id, worker_id: queueItem.claimed_by_session_id, claimed_at: queueItem.claimed_at, status: queueItem.status }
|
|
689
|
+
: null,
|
|
438
690
|
links: {
|
|
439
691
|
self: link("Protocol page", htmlPath, "available"),
|
|
440
692
|
project_dashboard: link("Project dashboard", projectDashboardHtmlPath(context, project.id), "available"),
|
|
@@ -680,6 +932,45 @@ function queueStatusSummary(queue) {
|
|
|
680
932
|
}
|
|
681
933
|
return [...counts.entries()].map(([status, count]) => `${status}:${count}`).join(", ");
|
|
682
934
|
}
|
|
935
|
+
function lifecycleSummary(protocols) {
|
|
936
|
+
const counts = new Map();
|
|
937
|
+
for (const protocol of protocols) {
|
|
938
|
+
const lifecycle = normalizeProtocolLifecycle({ rawStage: protocol.stage, rawStatus: protocol.status });
|
|
939
|
+
const key = [lifecycle.stage, lifecycle.substage, lifecycle.status].filter(Boolean).join("/");
|
|
940
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
941
|
+
}
|
|
942
|
+
return {
|
|
943
|
+
total: protocols.length,
|
|
944
|
+
by_lifecycle: [...counts.entries()].map(([lifecycle, count]) => ({ lifecycle, count }))
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
function resourceSummary(queue, locks, waiters) {
|
|
948
|
+
return {
|
|
949
|
+
queued_protocols: queue.filter((item) => isActiveQueueStatus(item.status)).length,
|
|
950
|
+
active_locks: locks.filter((lock) => lock.status === "active").length,
|
|
951
|
+
queued_waiters: waiters.filter((waiter) => waiter.status === "queued").length
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
function queueItemDashboard(queueItem) {
|
|
955
|
+
return {
|
|
956
|
+
protocol_id: queueItem.protocol_id,
|
|
957
|
+
queue_item_id: queueItem.id,
|
|
958
|
+
status: queueItem.status,
|
|
959
|
+
display_status: normalizeDisplayStatus(queueItem.status),
|
|
960
|
+
action_level: ["ready", "requeued"].includes(queueItem.status) ? "actionable" : queueItem.status === "claimed" ? "watch" : "none",
|
|
961
|
+
claim: queueItem.claimed_by_session_id
|
|
962
|
+
? {
|
|
963
|
+
protocol_id: queueItem.protocol_id,
|
|
964
|
+
worker_id: queueItem.claimed_by_session_id,
|
|
965
|
+
claimed_at: queueItem.claimed_at,
|
|
966
|
+
status: queueItem.status
|
|
967
|
+
}
|
|
968
|
+
: null,
|
|
969
|
+
owner: queueItem.claimed_by_session_id,
|
|
970
|
+
note: queueItem.last_reason,
|
|
971
|
+
updated_at: queueItem.updated_at
|
|
972
|
+
};
|
|
973
|
+
}
|
|
683
974
|
function workspaceForProtocol(worktrees, protocolId) {
|
|
684
975
|
return worktrees.find((worktree) => worktree.protocol_id === protocolId)?.worktree_path;
|
|
685
976
|
}
|
|
@@ -699,6 +990,15 @@ function compactPath(value) {
|
|
|
699
990
|
function truncate(value, maxLength) {
|
|
700
991
|
return value.length > maxLength ? `${value.slice(0, Math.max(0, maxLength - 3))}...` : value;
|
|
701
992
|
}
|
|
993
|
+
function recordValue(value) {
|
|
994
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
995
|
+
}
|
|
996
|
+
function stringValue(value) {
|
|
997
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
998
|
+
}
|
|
999
|
+
function numberValue(value) {
|
|
1000
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
1001
|
+
}
|
|
702
1002
|
function statusIcon(status) {
|
|
703
1003
|
if (["active", "running", "ready", "ready_for_merge", "claimed", "in_progress"].includes(status))
|
|
704
1004
|
return "🟢";
|
|
@@ -730,15 +1030,6 @@ function link(label, href, status, reason) {
|
|
|
730
1030
|
...(reason ? { degraded_reason: reason } : {})
|
|
731
1031
|
};
|
|
732
1032
|
}
|
|
733
|
-
function protocolCardMini(protocol) {
|
|
734
|
-
return {
|
|
735
|
-
id: protocol.id,
|
|
736
|
-
short_id: shortId(protocol.id),
|
|
737
|
-
stage: protocol.stage,
|
|
738
|
-
status: protocol.status,
|
|
739
|
-
display_status: normalizeDisplayStatus(protocol.status)
|
|
740
|
-
};
|
|
741
|
-
}
|
|
742
1033
|
function buildProtocolCard(context, project, protocol, generatePage) {
|
|
743
1034
|
const summary = planSummaryForProtocol(context, protocol.id);
|
|
744
1035
|
const activeDefCount = jsonArray(protocol.active_def_json).length;
|
|
@@ -747,10 +1038,12 @@ function buildProtocolCard(context, project, protocol, generatePage) {
|
|
|
747
1038
|
FROM flow_runs WHERE project_id = ? AND subject_type = 'protocol' AND subject_id = ?
|
|
748
1039
|
ORDER BY updated_at DESC, id DESC LIMIT 1`, [project.id, protocol.id]);
|
|
749
1040
|
let diagnostics = [];
|
|
1041
|
+
let lifecycle = normalizeProtocolLifecycle({ rawStage: protocol.stage, rawStatus: protocol.status });
|
|
750
1042
|
try {
|
|
751
1043
|
const runtimeProtocol = requireProtocol(context, protocol.id);
|
|
752
1044
|
const runtimeState = readProtocolRuntimeState(context, runtimeProtocol).state;
|
|
753
|
-
|
|
1045
|
+
lifecycle = normalizeProtocolLifecycle({ state: runtimeState });
|
|
1046
|
+
diagnostics = [...protocolRunDiagnostics(context, runtimeProtocol, runtimeState).diagnostics, ...lifecycle.diagnostics];
|
|
754
1047
|
}
|
|
755
1048
|
catch {
|
|
756
1049
|
diagnostics = [];
|
|
@@ -762,6 +1055,15 @@ function buildProtocolCard(context, project, protocol, generatePage) {
|
|
|
762
1055
|
href: protocolDashboardHtmlPath(context, project.id, protocol.id),
|
|
763
1056
|
raw_status: protocol.status,
|
|
764
1057
|
raw_stage: protocol.stage,
|
|
1058
|
+
lifecycle,
|
|
1059
|
+
lifecycle_stage: lifecycle.stage,
|
|
1060
|
+
lifecycle_status: lifecycle.status,
|
|
1061
|
+
resource: {
|
|
1062
|
+
kind: "protocol",
|
|
1063
|
+
lane: null,
|
|
1064
|
+
queue_status: null,
|
|
1065
|
+
claim: null
|
|
1066
|
+
},
|
|
765
1067
|
display_status: normalizeDisplayStatus(protocol.status),
|
|
766
1068
|
action_level: actionLevel(protocol.status, activeDefCount + blockerCount),
|
|
767
1069
|
flow_kind: latestRun?.flow_kind ?? "unknown",
|
|
@@ -978,7 +1280,9 @@ function renderDashboardHtmlPage(data) {
|
|
|
978
1280
|
<aside class="card verdict"><div class="verdictLabel">dd-flow dashboard</div><div class="verdictText" id="verdictText"></div><div class="sub" id="verdictNote"></div></aside>
|
|
979
1281
|
</section>
|
|
980
1282
|
<section class="metricGrid" id="metrics" data-testid="metric-strip"></section>
|
|
1283
|
+
<section class="card panel hidden" id="summaryVersionsPanel"><div class="sectionHead"><h2>Summary versions</h2><span class="pill" id="summaryVersionCount"></span></div><div class="list" id="summaryVersions"></div></section>
|
|
981
1284
|
<section class="card panel"><div class="sectionHead"><h2 id="cardsTitle"></h2><span class="pill" id="focusCount"></span></div><div class="grid" id="cards" data-testid="card-grid"></div></section>
|
|
1285
|
+
<section class="card panel hidden" id="unsupportedProjectsPanel"><div class="sectionHead"><h2>Unsupported projects</h2><span class="pill" id="unsupportedProjectCount"></span></div><div class="list" id="unsupportedProjects"></div></section>
|
|
982
1286
|
<section class="card panel hidden" id="setBoardPanel"><div class="sectionHead"><h2>Protocol set</h2><span class="pill" id="setBoardCount"></span></div><div class="list" id="setBoard"></div></section>
|
|
983
1287
|
<section class="card panel hidden" id="reviewRunsPanel"><div class="sectionHead"><h2>Review runs</h2><span class="pill" id="reviewRunCount"></span></div><div class="list" id="reviewRuns"></div></section>
|
|
984
1288
|
<section class="card panel"><div class="sectionHead"><h2>Навигация и артефакты</h2></div><div class="list" id="links"></div></section>
|
|
@@ -998,7 +1302,8 @@ function renderDashboardHtmlPage(data) {
|
|
|
998
1302
|
function metricValue(metric){ return metric && metric.value !== null && metric.value !== undefined ? metric.value : "N/A"; }
|
|
999
1303
|
function pageKind(){ return page.kind || "dashboard"; }
|
|
1000
1304
|
function cards(){ if(pageKind()==="global") return data.project_cards || []; if(pageKind()==="project") return data.protocol_cards || []; return data.run_history || []; }
|
|
1001
|
-
function
|
|
1305
|
+
function lifecycleText(card){ const lc = card.lifecycle || {}; return [lc.stage || card.lifecycle_stage || card.current_stage, lc.substage, lc.status || card.lifecycle_status || card.status].filter(Boolean).join(" / "); }
|
|
1306
|
+
function focusable(card){ const lc = card.lifecycle || {}; return ["actionable","blocked","watch"].includes(card.action_level) || ["blocked","failed","stale","unknown","waiting_for_user","ready_for_merge","queued"].includes(card.display_status) || ["blocked","waiting_for_user"].includes(lc.status) || Number(card.active_def_count || card.metrics?.open_defs || 0) > 0; }
|
|
1002
1307
|
function setMode(kind, value){ if(kind==="theme"){ root.dataset.theme = value; if(value === "system") document.documentElement.removeAttribute("data-theme"); else document.documentElement.dataset.theme = value; localStorage.setItem(storagePrefix+"theme", value); document.querySelectorAll("[data-theme-button]").forEach(b => b.setAttribute("aria-pressed", String(b.dataset.themeButton === value))); } else { root.dataset.view = value; localStorage.setItem(storagePrefix+"view", value); document.querySelectorAll("[data-view-button]").forEach(b => b.setAttribute("aria-pressed", String(b.dataset.viewButton === value))); renderCards(); } }
|
|
1003
1308
|
document.querySelectorAll("[data-theme-button]").forEach(b => b.addEventListener("click", () => setMode("theme", b.dataset.themeButton)));
|
|
1004
1309
|
document.querySelectorAll("[data-view-button]").forEach(b => b.addEventListener("click", () => setMode("view", b.dataset.viewButton)));
|
|
@@ -1012,21 +1317,33 @@ function renderDashboardHtmlPage(data) {
|
|
|
1012
1317
|
[["generated", data.generated_at],["schema", data.schema_id],["language", data.target_language]].forEach(([k,v]) => { const p=el("span","pill"); const b=el("b","",k); p.append(b, document.createTextNode(" "+String(v||""))); meta.append(p); });
|
|
1013
1318
|
}
|
|
1014
1319
|
function renderMetrics(){ const box=document.getElementById("metrics"); box.replaceChildren(); (data.metrics || []).forEach(m => { const item=el("article","metric"); item.title = [m.source,m.reason].filter(Boolean).join(" / "); item.append(el("strong","",metricValue(m)), el("span","",m.label || m.id)); box.append(item); }); }
|
|
1320
|
+
function renderSummaryVersions(){
|
|
1321
|
+
const panel=document.getElementById("summaryVersionsPanel"); const box=document.getElementById("summaryVersions"); box.replaceChildren();
|
|
1322
|
+
const groups = data.summary_version_groups || []; if(pageKind()!=="global" || !groups.length){ panel.classList.add("hidden"); return; }
|
|
1323
|
+
panel.classList.remove("hidden"); text("summaryVersionCount", groups.length + " groups");
|
|
1324
|
+
groups.forEach(group => { const row=el("div","row"); const names = (group.project_names || []).join(", "); const label = [group.schema_id || "missing", group.schema_version || "unknown", group.compatible ? "compatible" : "unsupported"].join(" / "); const body=el("div",""); body.append(el("div","",label), el("div","sub",names)); row.append(body, statusBadge(group.compatible ? "done" : "unsupported")); box.append(row); });
|
|
1325
|
+
}
|
|
1015
1326
|
function renderCards(){
|
|
1016
1327
|
const mode = root.dataset.view || "compact"; const all = cards(); const visible = mode === "focus" ? all.filter(focusable) : all; text("focusCount", mode === "focus" ? visible.length + " focus" : all.length + " total");
|
|
1017
1328
|
text("cardsTitle", pageKind()==="global" ? "Проекты" : pageKind()==="project" ? "Протоколы" : "Run history");
|
|
1018
1329
|
const box=document.getElementById("cards"); box.replaceChildren();
|
|
1019
1330
|
visible.forEach(card => { const c=el("article","card entity"); c.dataset.testid = pageKind()==="global" ? "project-card" : pageKind()==="project" ? "protocol-card" : "run-card"; c.dataset.status = card.display_status || card.status || "unknown";
|
|
1020
1331
|
const href = card.href || card.run_index_path || card.report?.href || ""; const primary = linkNode(card.short_id || card.title || card.id, href, "primary"); primary.setAttribute("aria-label", card.title || card.id || "card"); c.append(primary);
|
|
1021
|
-
const sub = el("div","sub", card.root || card
|
|
1022
|
-
const badges=el("div","badges"); [card.display_status || card.status, card.display_flow_kind || card.flow_kind, card.action_level].filter(Boolean).forEach(v => badges.append(statusBadge(v))); c.append(badges);
|
|
1332
|
+
const sub = el("div","sub", card.root || lifecycleText(card) || card.next_action || card.run_index_path || ""); sub.title = sub.textContent; c.append(sub);
|
|
1333
|
+
const badges=el("div","badges"); [card.display_status || card.status, lifecycleText(card), card.display_flow_kind || card.flow_kind, card.action_level].filter(Boolean).forEach(v => badges.append(statusBadge(v))); c.append(badges);
|
|
1023
1334
|
const mini=el("div","miniMetrics expandedOnly"); const metrics = card.metrics || {}; [["queue",metrics.queue],["locks",metrics.locks],["defs",metrics.open_defs ?? card.active_def_count]].forEach(([k,v]) => { const m=el("div",""); m.append(el("b","",v ?? "0"), document.createTextNode(k)); mini.append(m); }); c.append(mini); box.append(c); });
|
|
1024
1335
|
}
|
|
1025
1336
|
function renderLinks(){ const box=document.getElementById("links"); box.replaceChildren(); Object.entries(data.links || {}).forEach(([key,value]) => { const row=el("div","row"); row.append(linkNode(value.label || key, value.href, value.status === "available" ? "" : "disabled"), statusBadge(value.status || "unknown")); box.append(row); }); if(pageKind()==="protocol"){ (data.stage_pipeline || []).forEach(stage => { const row=el("div","row"); row.append(linkNode(stage.stage + " / " + stage.status, stage.report?.href, stage.report?.status === "available" ? "" : "disabled"), statusBadge(stage.display_status)); box.append(row); }); } }
|
|
1026
1337
|
function renderSetBoard(){ const panel=document.getElementById("setBoardPanel"); const box=document.getElementById("setBoard"); box.replaceChildren(); const boards = pageKind()==="protocol" ? [data.protocol_set_board].filter(Boolean) : (data.protocol_set_boards || []); if(!boards.length){ panel.classList.add("hidden"); return; } panel.classList.remove("hidden"); const total = boards.reduce((sum,b)=>sum + ((b.members || []).length),0); text("setBoardCount", total + " members"); boards.forEach(board => { const summary = board.summary || {}; const head=el("div","row"); head.append(el("div","", board.protocol_set || "protocol set"), el("span","badge", ["ready "+(summary.ready||0),"blocked "+(summary.blocked||0),"running "+(summary.running||0),"done "+(summary.done||0)].join(" / "))); box.append(head); (board.members || []).forEach(member => { const row=el("div","row"); const href = data.project?.root ? "file://" + String(member.path || "").split("/").map(encodeURIComponent).join("/") : ""; row.append(linkNode(member.id || "protocol", href, ""), statusBadge(member.set_status || "unknown")); box.append(row); }); }); }
|
|
1027
1338
|
function renderReviewRuns(){ const panel=document.getElementById("reviewRunsPanel"); const box=document.getElementById("reviewRuns"); box.replaceChildren(); const runs = data.review_runs || []; if(!runs.length){ panel.classList.add("hidden"); return; } panel.classList.remove("hidden"); text("reviewRunCount", runs.length + " recent"); runs.forEach(run => { const row=el("div","row"); const href = run.report?.href || run.run_index_path || ""; const label = [run.short_id || run.id, run.subject_id].filter(Boolean).join(" / "); row.append(linkNode(label, href, ""), statusBadge(run.display_status || run.status)); box.append(row); }); }
|
|
1339
|
+
function renderUnsupportedProjects(){
|
|
1340
|
+
const panel=document.getElementById("unsupportedProjectsPanel"); const box=document.getElementById("unsupportedProjects"); box.replaceChildren();
|
|
1341
|
+
const projects = data.unsupported_projects || []; if(pageKind()!=="global" || !projects.length){ panel.classList.add("hidden"); return; }
|
|
1342
|
+
panel.classList.remove("hidden"); text("unsupportedProjectCount", projects.length + " projects");
|
|
1343
|
+
projects.forEach(project => { const row=el("div","row"); const body=el("div",""); body.append(el("div","",project.title || project.id), el("div","sub",[project.reason, project.detail, project.root].filter(Boolean).join(" / "))); row.append(body, statusBadge(project.reason || "unsupported")); box.append(row); });
|
|
1344
|
+
}
|
|
1028
1345
|
function renderWarnings(){ const box=document.getElementById("warnings"); box.replaceChildren(); const warnings = data.warnings || []; if(!warnings.length){ box.append(el("div","sub","No degraded dashboard data.")); return; } warnings.forEach(w => box.append(el("div","warn", typeof w === "string" ? w : JSON.stringify(w)))); }
|
|
1029
|
-
renderHeader(); renderMetrics(); renderLinks(); renderSetBoard(); renderReviewRuns(); renderWarnings();
|
|
1346
|
+
renderHeader(); renderMetrics(); renderSummaryVersions(); renderLinks(); renderSetBoard(); renderReviewRuns(); renderUnsupportedProjects(); renderWarnings();
|
|
1030
1347
|
const savedTheme = ["system","light","dark"].includes(localStorage.getItem(storagePrefix+"theme")) ? localStorage.getItem(storagePrefix+"theme") : "system";
|
|
1031
1348
|
const savedView = ["compact","expanded","focus"].includes(localStorage.getItem(storagePrefix+"view")) ? localStorage.getItem(storagePrefix+"view") : "compact";
|
|
1032
1349
|
setMode("theme", savedTheme); setMode("view", savedView);
|