patchwarden 1.5.0 → 1.5.1
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/README.en.md +43 -2
- package/README.md +43 -2
- package/dist/assessments/agentAssessor.d.ts +1 -1
- package/dist/assessments/agentAssessor.js +3 -3
- package/dist/assessments/assessmentStore.d.ts +1 -1
- package/dist/assessments/assessmentStore.js +2 -2
- package/dist/assessments/confirmCli.js +5 -4
- package/dist/config.d.ts +7 -0
- package/dist/config.js +34 -0
- package/dist/control/middleware/auth.d.ts +10 -0
- package/dist/control/middleware/auth.js +8 -0
- package/dist/control/middleware/static.d.ts +3 -0
- package/dist/control/middleware/static.js +65 -0
- package/dist/control/routes/audit.d.ts +15 -0
- package/dist/control/routes/audit.js +277 -0
- package/dist/control/routes/evidence.d.ts +4 -0
- package/dist/control/routes/evidence.js +96 -0
- package/dist/control/routes/lineage.d.ts +3 -0
- package/dist/control/routes/lineage.js +71 -0
- package/dist/control/routes/policy.d.ts +3 -0
- package/dist/control/routes/policy.js +81 -0
- package/dist/control/routes/process.d.ts +5 -0
- package/dist/control/routes/process.js +200 -0
- package/dist/control/routes/sessions.d.ts +22 -0
- package/dist/control/routes/sessions.js +224 -0
- package/dist/control/routes/status.d.ts +6 -0
- package/dist/control/routes/status.js +250 -0
- package/dist/control/routes/taskActions.d.ts +21 -0
- package/dist/control/routes/taskActions.js +233 -0
- package/dist/control/routes/tasks.d.ts +20 -0
- package/dist/control/routes/tasks.js +310 -0
- package/dist/control/routes/workspace.d.ts +15 -0
- package/dist/control/routes/workspace.js +193 -0
- package/dist/control/runtime.d.ts +91 -0
- package/dist/control/runtime.js +392 -0
- package/dist/control/server.d.ts +13 -0
- package/dist/control/server.js +479 -0
- package/dist/control/shared.d.ts +35 -0
- package/dist/control/shared.js +288 -0
- package/dist/controlCenter.d.ts +6 -0
- package/dist/controlCenter.js +7 -2197
- package/dist/direct/directGuards.js +30 -8
- package/dist/doctor.d.ts +18 -1
- package/dist/doctor.js +579 -348
- package/dist/goal/goalReport.d.ts +54 -0
- package/dist/goal/goalReport.js +204 -0
- package/dist/goal/goalStatus.d.ts +6 -0
- package/dist/goal/specKitImport.d.ts +63 -0
- package/dist/goal/specKitImport.js +220 -0
- package/dist/goal/subgoalSync.js +2 -1
- package/dist/httpServer.js +15 -12
- package/dist/index.js +7 -4
- package/dist/logging.d.ts +7 -1
- package/dist/logging.js +8 -0
- package/dist/runner/changeCapture.d.ts +3 -3
- package/dist/runner/changeCapture.js +63 -39
- package/dist/runner/cli.js +7 -6
- package/dist/runner/postTaskCleanup.js +26 -2
- package/dist/runner/runTask.js +245 -221
- package/dist/runner/simpleProcess.js +4 -4
- package/dist/runner/watch.js +17 -14
- package/dist/security/contentRedaction.d.ts +6 -0
- package/dist/security/contentRedaction.js +22 -0
- package/dist/smoke-test.js +257 -251
- package/dist/test/unit/apply-patch.test.d.ts +1 -0
- package/dist/test/unit/apply-patch.test.js +225 -0
- package/dist/test/unit/create-task.test.d.ts +1 -0
- package/dist/test/unit/create-task.test.js +197 -0
- package/dist/test/unit/direct-guards.test.js +124 -9
- package/dist/test/unit/evidence-pack.test.js +95 -1
- package/dist/test/unit/get-task-status.test.d.ts +1 -0
- package/dist/test/unit/get-task-status.test.js +174 -0
- package/dist/test/unit/get-task-summary.test.d.ts +1 -0
- package/dist/test/unit/get-task-summary.test.js +146 -0
- package/dist/test/unit/goal-report.test.d.ts +1 -0
- package/dist/test/unit/goal-report.test.js +159 -0
- package/dist/test/unit/goal-subgoal-task.test.js +6 -6
- package/dist/test/unit/goal-tools-registry.test.js +6 -4
- package/dist/test/unit/path-guard.test.js +24 -0
- package/dist/test/unit/spec-kit-import.test.d.ts +1 -0
- package/dist/test/unit/spec-kit-import.test.js +341 -0
- package/dist/test/unit/wait-for-task.test.d.ts +1 -0
- package/dist/test/unit/wait-for-task.test.js +144 -0
- package/dist/tools/auditTask.d.ts +8 -63
- package/dist/tools/auditTask.js +12 -8
- package/dist/tools/createDirectSession.d.ts +1 -1
- package/dist/tools/createDirectSession.js +2 -2
- package/dist/tools/createTask.d.ts +2 -2
- package/dist/tools/createTask.js +4 -4
- package/dist/tools/dispatch/coreDispatch.d.ts +9 -0
- package/dist/tools/dispatch/coreDispatch.js +282 -0
- package/dist/tools/dispatch/diagnosticDispatch.d.ts +14 -0
- package/dist/tools/dispatch/diagnosticDispatch.js +78 -0
- package/dist/tools/dispatch/directDispatch.d.ts +8 -0
- package/dist/tools/dispatch/directDispatch.js +115 -0
- package/dist/tools/dispatch/goalDispatch.d.ts +8 -0
- package/dist/tools/dispatch/goalDispatch.js +91 -0
- package/dist/tools/dispatch/releaseDispatch.d.ts +8 -0
- package/dist/tools/dispatch/releaseDispatch.js +45 -0
- package/dist/tools/dispatch/types.d.ts +23 -0
- package/dist/tools/dispatch/types.js +15 -0
- package/dist/tools/evidencePack.d.ts +6 -0
- package/dist/tools/evidencePack.js +213 -6
- package/dist/tools/finalizeDirectSession.d.ts +1 -1
- package/dist/tools/finalizeDirectSession.js +3 -3
- package/dist/tools/goalSubgoalTask.d.ts +1 -1
- package/dist/tools/goalSubgoalTask.js +2 -2
- package/dist/tools/healthCheck.js +3 -3
- package/dist/tools/registry.d.ts +3 -3
- package/dist/tools/registry.js +60 -503
- package/dist/tools/retryTask.d.ts +2 -2
- package/dist/tools/retryTask.js +2 -2
- package/dist/tools/runTaskLoop.js +4 -4
- package/dist/tools/safeViews.d.ts +2 -2
- package/dist/tools/safeViews.js +2 -2
- package/dist/tools/toolRegistry.js +22 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/CODE_WIKI.md +983 -0
- package/docs/agentseal-integration.md +150 -0
- package/docs/architecture.md +63 -0
- package/docs/assets/patchwarden-oss-demo.gif +0 -0
- package/docs/dashboard-overview.md +86 -0
- package/docs/demo.md +8 -0
- package/docs/direct-session-workflow.md +98 -0
- package/docs/evidence-pack-schema.md +215 -0
- package/docs/execution-plan-2026-07-09.md +315 -0
- package/docs/lineage-evidence-pack-workflow.md +127 -0
- package/docs/mcp-inspector-testing.md +200 -0
- package/docs/open-source-application.md +162 -0
- package/docs/opencode-worker.md +151 -0
- package/docs/openhands-worker.md +181 -0
- package/docs/release-evidence.md +72 -0
- package/docs/roadmap-execution-and-acceptance.md +365 -0
- package/docs/spec-kit-integration.md +131 -0
- package/docs/task-safe-review-workflow.md +98 -0
- package/docs/threat-model.md +79 -0
- package/docs/user-feedback.md +40 -0
- package/docs/why-patchwarden.md +110 -0
- package/package.json +2 -2
- package/scripts/checks/control-center-smoke.js +356 -0
- package/scripts/checks/lifecycle-smoke.js +29 -23
- package/scripts/checks/mcp-smoke.js +3 -1
- package/scripts/generate-demo-gif.py +320 -0
- package/src/assessments/agentAssessor.ts +3 -3
- package/src/assessments/assessmentStore.ts +2 -2
- package/src/assessments/confirmCli.ts +5 -4
- package/src/config.ts +37 -0
- package/src/control/middleware/auth.ts +17 -0
- package/src/control/middleware/static.ts +71 -0
- package/src/control/routes/audit.ts +321 -0
- package/src/control/routes/evidence.ts +107 -0
- package/src/control/routes/lineage.ts +92 -0
- package/src/control/routes/policy.ts +81 -0
- package/src/control/routes/process.ts +204 -0
- package/src/control/routes/sessions.ts +251 -0
- package/src/control/routes/status.ts +325 -0
- package/src/control/routes/taskActions.ts +248 -0
- package/src/control/routes/tasks.ts +323 -0
- package/src/control/routes/workspace.ts +203 -0
- package/src/control/runtime.ts +472 -0
- package/src/control/server.ts +471 -0
- package/src/control/shared.ts +294 -0
- package/src/controlCenter.ts +7 -2347
- package/src/direct/directGuards.ts +28 -7
- package/src/doctor.ts +741 -481
- package/src/goal/goalReport.ts +271 -0
- package/src/goal/goalStatus.ts +6 -0
- package/src/goal/specKitImport.ts +355 -0
- package/src/goal/subgoalSync.ts +4 -2
- package/src/httpServer.ts +17 -14
- package/src/index.ts +7 -4
- package/src/logging.ts +10 -1
- package/src/runner/changeCapture.ts +70 -42
- package/src/runner/cli.ts +7 -6
- package/src/runner/postTaskCleanup.ts +26 -2
- package/src/runner/runTask.ts +325 -223
- package/src/runner/simpleProcess.ts +4 -4
- package/src/runner/watch.ts +17 -14
- package/src/security/contentRedaction.ts +29 -0
- package/src/smoke-test.ts +252 -250
- package/src/test/unit/apply-patch.test.ts +293 -0
- package/src/test/unit/create-task.test.ts +255 -0
- package/src/test/unit/direct-guards.test.ts +178 -8
- package/src/test/unit/evidence-pack.test.ts +110 -1
- package/src/test/unit/get-task-status.test.ts +203 -0
- package/src/test/unit/get-task-summary.test.ts +173 -0
- package/src/test/unit/goal-report.test.ts +189 -0
- package/src/test/unit/goal-subgoal-task.test.ts +6 -6
- package/src/test/unit/goal-tools-registry.test.ts +7 -5
- package/src/test/unit/path-guard.test.ts +30 -0
- package/src/test/unit/spec-kit-import.test.ts +429 -0
- package/src/test/unit/wait-for-task.test.ts +176 -0
- package/src/tools/auditTask.ts +99 -59
- package/src/tools/createDirectSession.ts +3 -3
- package/src/tools/createTask.ts +7 -7
- package/src/tools/dispatch/coreDispatch.ts +374 -0
- package/src/tools/dispatch/diagnosticDispatch.ts +101 -0
- package/src/tools/dispatch/directDispatch.ts +167 -0
- package/src/tools/dispatch/goalDispatch.ts +127 -0
- package/src/tools/dispatch/releaseDispatch.ts +65 -0
- package/src/tools/dispatch/types.ts +24 -0
- package/src/tools/evidencePack.ts +291 -6
- package/src/tools/finalizeDirectSession.ts +4 -4
- package/src/tools/goalSubgoalTask.ts +2 -2
- package/src/tools/healthCheck.ts +3 -3
- package/src/tools/registry.ts +68 -628
- package/src/tools/retryTask.ts +2 -2
- package/src/tools/runTaskLoop.ts +4 -4
- package/src/tools/safeViews.ts +2 -2
- package/src/tools/toolRegistry.ts +22 -0
- package/src/version.ts +1 -1
- package/ui/pages/audit.html +192 -3
- package/ui/pages/dashboard.html +959 -44
- package/ui/pages/direct-sessions.html +505 -53
- package/ui/pages/task-detail.html +456 -438
- package/ui/pages/tasks.html +598 -61
package/ui/pages/dashboard.html
CHANGED
|
@@ -255,6 +255,12 @@ html.dark {
|
|
|
255
255
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/><path d="M3 21v-5h5"/></svg>
|
|
256
256
|
<span>Refresh</span>
|
|
257
257
|
</button>
|
|
258
|
+
<div class="flex items-center gap-2 px-3 py-1.5 rounded-lg" style="background-color: var(--pw-bg-surface); border: 1px solid var(--pw-border); font-family: var(--pw-font-sans);">
|
|
259
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color: var(--pw-text-tertiary);"><path d="M4 20h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.93a2 2 0 0 1-1.66-.9l-.82-1.2A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13c0 1.1.9 2 2 2z"/></svg>
|
|
260
|
+
<select id="repo-selector" class="bg-transparent text-sm outline-none cursor-pointer" style="color: var(--pw-text-primary); font-family: var(--pw-font-sans); min-width: 180px;">
|
|
261
|
+
<option value=".">workspace root</option>
|
|
262
|
+
</select>
|
|
263
|
+
</div>
|
|
258
264
|
<div class="flex-1 min-w-0"></div>
|
|
259
265
|
<button data-action="open-core-tunnel" class="flex items-center gap-1.5 px-3.5 py-2 rounded-lg text-sm font-medium transition-colors whitespace-nowrap" style="background-color: transparent; color: var(--pw-accent); border: 1px solid var(--pw-accent); font-family: var(--pw-font-sans);">
|
|
260
266
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>
|
|
@@ -273,6 +279,17 @@ html.dark {
|
|
|
273
279
|
<!-- ===== ERROR BANNER ===== -->
|
|
274
280
|
<div id="error-banner" class="hidden mb-4 rounded-lg px-4 py-3 text-sm" style="background-color: var(--pw-state-error-bg); border: 1px solid var(--pw-state-error); color: var(--pw-state-error); font-family: var(--pw-font-sans);"></div>
|
|
275
281
|
|
|
282
|
+
<!-- ===== HEALTH SCORE CARD ===== -->
|
|
283
|
+
<div id="health-score-card" class="rounded-lg mb-4" style="background-color: var(--pw-bg-surface); border: 1px solid var(--pw-border);">
|
|
284
|
+
<div class="flex items-center justify-between px-4 py-3" style="border-bottom: 1px solid var(--pw-border);">
|
|
285
|
+
<h2 class="text-sm font-semibold truncate" data-zh="健康评分" data-en="Health Score" style="color: var(--pw-text-primary); font-family: var(--pw-font-sans);">健康评分</h2>
|
|
286
|
+
<span id="health-score-badge" class="text-xs font-semibold px-2.5 py-1 rounded-full whitespace-nowrap" style="color: var(--pw-text-tertiary); background-color: var(--pw-bg-hover);">—</span>
|
|
287
|
+
</div>
|
|
288
|
+
<div id="health-score-body" class="px-4 py-3 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-2">
|
|
289
|
+
<div class="text-xs" style="color: var(--pw-text-tertiary);">loading…</div>
|
|
290
|
+
</div>
|
|
291
|
+
</div>
|
|
292
|
+
|
|
276
293
|
<!-- ===== SETUP CHECKLIST ===== -->
|
|
277
294
|
<div id="setup-checklist-card" class="rounded-lg mb-4" style="background-color: var(--pw-bg-surface); border: 1px solid var(--pw-border);">
|
|
278
295
|
<div class="flex items-center justify-between px-4 py-3" style="border-bottom: 1px solid var(--pw-border);">
|
|
@@ -336,6 +353,15 @@ html.dark {
|
|
|
336
353
|
</div>
|
|
337
354
|
</div>
|
|
338
355
|
|
|
356
|
+
<!-- ===== STALE TASKS CARD ===== -->
|
|
357
|
+
<div id="stale-tasks-card" class="hidden rounded-lg mb-6" style="background-color: var(--pw-bg-surface); border: 1px solid var(--pw-border); border-left: 3px solid var(--pw-state-warning);">
|
|
358
|
+
<div class="flex items-center justify-between px-4 py-3" style="border-bottom: 1px solid var(--pw-border);">
|
|
359
|
+
<h2 class="text-sm font-semibold truncate" data-zh="Stale 任务" data-en="Stale Tasks" style="color: var(--pw-text-primary);">Stale 任务</h2>
|
|
360
|
+
<span id="stale-tasks-count" class="text-xs whitespace-nowrap" style="color: var(--pw-text-tertiary);">—</span>
|
|
361
|
+
</div>
|
|
362
|
+
<div id="stale-tasks-body" class="px-4 py-3 text-sm" style="color: var(--pw-text-tertiary);">Loading</div>
|
|
363
|
+
</div>
|
|
364
|
+
|
|
339
365
|
<!-- ===== TWO-COLUMN LAYOUT ===== -->
|
|
340
366
|
<div class="grid grid-cols-1 lg:grid-cols-5 gap-4 mb-6">
|
|
341
367
|
|
|
@@ -352,8 +378,12 @@ html.dark {
|
|
|
352
378
|
|
|
353
379
|
<!-- RIGHT COLUMN: 系统状态 (2/5 width) -->
|
|
354
380
|
<div class="lg:col-span-2 rounded-lg" style="background-color: var(--pw-bg-surface); border: 1px solid var(--pw-border);">
|
|
355
|
-
<div class="px-4 py-3" style="border-bottom: 1px solid var(--pw-border);">
|
|
381
|
+
<div class="flex items-center justify-between gap-2 px-4 py-3" style="border-bottom: 1px solid var(--pw-border);">
|
|
356
382
|
<h2 class="text-sm font-semibold truncate" data-zh="系统状态" data-en="System Status" style="color: var(--pw-text-primary);">系统状态</h2>
|
|
383
|
+
<button id="btn-copy-diagnostics" class="flex items-center gap-1.5 px-2.5 py-1 rounded-md text-xs font-medium transition-colors whitespace-nowrap" style="background-color: var(--pw-bg-hover); color: var(--pw-text-secondary); border: 1px solid var(--pw-border-subtle); font-family: var(--pw-font-sans);" type="button" title="Copy diagnostics to clipboard">
|
|
384
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
|
385
|
+
<span data-zh="复制诊断" data-en="Copy diagnostics">复制诊断</span>
|
|
386
|
+
</button>
|
|
357
387
|
</div>
|
|
358
388
|
<div id="system-status-body" class="px-4 py-2">
|
|
359
389
|
<div class="py-3 text-center text-sm" style="color: var(--pw-text-tertiary);">加载中…</div>
|
|
@@ -384,6 +414,17 @@ html.dark {
|
|
|
384
414
|
</div>
|
|
385
415
|
</div>
|
|
386
416
|
|
|
417
|
+
<!-- ===== SAFE MODAL ===== -->
|
|
418
|
+
<div id="safe-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center p-4" style="background-color: rgba(0,0,0,0.6);">
|
|
419
|
+
<div class="rounded-lg w-full max-w-2xl max-h-[80vh] flex flex-col" style="background-color: var(--pw-bg-surface); border: 1px solid var(--pw-border); box-shadow: var(--pw-shadow-modal);">
|
|
420
|
+
<div class="flex items-center justify-between px-4 py-3" style="border-bottom: 1px solid var(--pw-border);">
|
|
421
|
+
<h3 id="safe-modal-title" class="text-sm font-semibold truncate" style="color: var(--pw-text-primary);">—</h3>
|
|
422
|
+
<button id="safe-modal-close" class="text-lg leading-none" style="color: var(--pw-text-tertiary);" type="button">×</button>
|
|
423
|
+
</div>
|
|
424
|
+
<div id="safe-modal-body" class="flex-1 overflow-auto px-4 py-3 text-xs" style="color: var(--pw-text-primary); font-family: var(--pw-font-mono); white-space: pre-wrap; word-break: break-all;"></div>
|
|
425
|
+
</div>
|
|
426
|
+
</div>
|
|
427
|
+
|
|
387
428
|
</main>
|
|
388
429
|
|
|
389
430
|
</div>
|
|
@@ -477,6 +518,44 @@ html.dark {
|
|
|
477
518
|
@keyframes pw-toast-out {
|
|
478
519
|
to { opacity: 0; transform: translateX(20px); }
|
|
479
520
|
}
|
|
521
|
+
/* Quick action icon buttons */
|
|
522
|
+
.pw-quick-btn {
|
|
523
|
+
display: inline-flex;
|
|
524
|
+
align-items: center;
|
|
525
|
+
justify-content: center;
|
|
526
|
+
width: 24px;
|
|
527
|
+
height: 24px;
|
|
528
|
+
border-radius: 4px;
|
|
529
|
+
border: 1px solid var(--pw-border-subtle);
|
|
530
|
+
background-color: var(--pw-bg-hover);
|
|
531
|
+
color: var(--pw-text-secondary);
|
|
532
|
+
cursor: pointer;
|
|
533
|
+
transition: background-color var(--pw-transition-fast), color var(--pw-transition-fast);
|
|
534
|
+
text-decoration: none;
|
|
535
|
+
}
|
|
536
|
+
.pw-quick-btn:hover {
|
|
537
|
+
background-color: var(--pw-bg-active);
|
|
538
|
+
color: var(--pw-text-primary);
|
|
539
|
+
opacity: 1;
|
|
540
|
+
}
|
|
541
|
+
/* Acceptance status badge - more prominent than status */
|
|
542
|
+
.pw-acceptance-badge {
|
|
543
|
+
font-size: var(--pw-text-sm);
|
|
544
|
+
font-weight: var(--pw-weight-bold);
|
|
545
|
+
padding: 2px 8px;
|
|
546
|
+
border-width: 1px;
|
|
547
|
+
border-style: solid;
|
|
548
|
+
}
|
|
549
|
+
/* Stale task explanation - prominent */
|
|
550
|
+
.pw-stale-explanation {
|
|
551
|
+
font-size: var(--pw-text-sm);
|
|
552
|
+
font-weight: var(--pw-weight-medium);
|
|
553
|
+
color: var(--pw-text-primary);
|
|
554
|
+
padding: 6px 10px;
|
|
555
|
+
border-radius: var(--pw-radius-sm);
|
|
556
|
+
background-color: var(--pw-state-warning-bg);
|
|
557
|
+
border-left: 2px solid var(--pw-state-warning);
|
|
558
|
+
}
|
|
480
559
|
</style>
|
|
481
560
|
|
|
482
561
|
<script>
|
|
@@ -485,6 +564,7 @@ html.dark {
|
|
|
485
564
|
|
|
486
565
|
let controlToken = null;
|
|
487
566
|
let statusTimer = null;
|
|
567
|
+
let currentRepoPath = ".";
|
|
488
568
|
|
|
489
569
|
// ---------- Utilities ----------
|
|
490
570
|
function $(id) { return document.getElementById(id); }
|
|
@@ -878,6 +958,155 @@ html.dark {
|
|
|
878
958
|
}
|
|
879
959
|
}
|
|
880
960
|
|
|
961
|
+
// ---------- Clipboard + Safe Modal ----------
|
|
962
|
+
function copyToClipboard(text, label) {
|
|
963
|
+
if (!text) return;
|
|
964
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
965
|
+
navigator.clipboard.writeText(text).then(function () {
|
|
966
|
+
showToast('success', '已复制', (label || '内容') + '已复制到剪贴板');
|
|
967
|
+
}).catch(function () {
|
|
968
|
+
showToast('error', '复制失败', '请手动复制');
|
|
969
|
+
});
|
|
970
|
+
} else {
|
|
971
|
+
showToast('warning', '复制不支持', '浏览器不支持剪贴板 API');
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
// Format diagnostics response as plain text for clipboard.
|
|
976
|
+
function formatDiagnosticsText(d) {
|
|
977
|
+
if (!d || typeof d !== 'object') return '';
|
|
978
|
+
const lines = [];
|
|
979
|
+
lines.push('PatchWarden Diagnostics');
|
|
980
|
+
lines.push('======================');
|
|
981
|
+
lines.push('server_version: ' + (d.server_version != null ? d.server_version : '—'));
|
|
982
|
+
lines.push('schema_epoch: ' + (d.schema_epoch != null ? d.schema_epoch : '—'));
|
|
983
|
+
lines.push('tool_manifest_sha256: ' + (d.tool_manifest_sha256 != null ? d.tool_manifest_sha256 : '—'));
|
|
984
|
+
lines.push('watcher_status: ' + (d.watcher_status != null ? d.watcher_status : '—'));
|
|
985
|
+
lines.push('tunnel_core_ready: ' + (d.tunnel_core_ready === true ? 'true' : 'false'));
|
|
986
|
+
lines.push('tunnel_direct_ready: ' + (d.tunnel_direct_ready === true ? 'true' : 'false'));
|
|
987
|
+
lines.push('core_tool_count: ' + (d.core_tool_count != null ? d.core_tool_count : '—'));
|
|
988
|
+
lines.push('direct_tool_count: ' + (d.direct_tool_count != null ? d.direct_tool_count : '—'));
|
|
989
|
+
lines.push('direct_profile_enabled: ' + (d.direct_profile_enabled === true ? 'true' : 'false'));
|
|
990
|
+
lines.push('workspace_root: ' + (d.workspace_root != null ? d.workspace_root : '—'));
|
|
991
|
+
lines.push('');
|
|
992
|
+
const agents = Array.isArray(d.agent_status) ? d.agent_status : [];
|
|
993
|
+
lines.push('Agents:');
|
|
994
|
+
if (agents.length === 0) {
|
|
995
|
+
lines.push(' (none)');
|
|
996
|
+
} else {
|
|
997
|
+
agents.forEach(function (a) {
|
|
998
|
+
lines.push(' - ' + (a.name || '?') + ': ' + (a.available ? 'available' : 'unavailable'));
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
lines.push('');
|
|
1002
|
+
const failures = Array.isArray(d.recent_failures) ? d.recent_failures : [];
|
|
1003
|
+
lines.push('Recent failures:');
|
|
1004
|
+
if (failures.length === 0) {
|
|
1005
|
+
lines.push(' (none)');
|
|
1006
|
+
} else {
|
|
1007
|
+
failures.forEach(function (f) {
|
|
1008
|
+
lines.push(' - task_id: ' + (f.task_id || '?') + ', status: ' + (f.status || '?'));
|
|
1009
|
+
});
|
|
1010
|
+
}
|
|
1011
|
+
return lines.join('\n');
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
async function copyDiagnostics(btnEl) {
|
|
1015
|
+
const orig = btnEl ? btnEl.innerHTML : null;
|
|
1016
|
+
if (btnEl) {
|
|
1017
|
+
btnEl.disabled = true;
|
|
1018
|
+
btnEl.innerHTML =
|
|
1019
|
+
'<span class="inline-block w-3 h-3 align-middle border-2 border-current border-t-transparent rounded-full pw-spin"></span>'
|
|
1020
|
+
+ '<span class="ml-1.5">处理中…</span>';
|
|
1021
|
+
}
|
|
1022
|
+
try {
|
|
1023
|
+
const data = await fetchJson('/api/diagnostics');
|
|
1024
|
+
const text = formatDiagnosticsText(data);
|
|
1025
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
1026
|
+
await navigator.clipboard.writeText(text);
|
|
1027
|
+
showToast('success', '诊断信息已复制到剪贴板', 'PatchWarden Diagnostics 已复制到剪贴板');
|
|
1028
|
+
} else {
|
|
1029
|
+
showToast('warning', '复制不支持', '浏览器不支持剪贴板 API');
|
|
1030
|
+
}
|
|
1031
|
+
} catch (err) {
|
|
1032
|
+
showToast('error', '诊断复制失败', err.message);
|
|
1033
|
+
} finally {
|
|
1034
|
+
if (btnEl) {
|
|
1035
|
+
btnEl.disabled = false;
|
|
1036
|
+
btnEl.innerHTML = orig;
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
function showSafeModal(title, data) {
|
|
1042
|
+
const modal = $('safe-modal');
|
|
1043
|
+
const titleEl = $('safe-modal-title');
|
|
1044
|
+
const bodyEl = $('safe-modal-body');
|
|
1045
|
+
if (!modal || !titleEl || !bodyEl) return;
|
|
1046
|
+
titleEl.textContent = title || '—';
|
|
1047
|
+
let content;
|
|
1048
|
+
if (data && typeof data === 'object') {
|
|
1049
|
+
content = JSON.stringify(data, null, 2);
|
|
1050
|
+
} else {
|
|
1051
|
+
content = String(data || '');
|
|
1052
|
+
}
|
|
1053
|
+
bodyEl.textContent = content;
|
|
1054
|
+
modal.classList.remove('hidden');
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
function closeSafeModal() {
|
|
1058
|
+
const modal = $('safe-modal');
|
|
1059
|
+
if (modal) modal.classList.add('hidden');
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
async function fetchSafeSummary(taskId, kind, title) {
|
|
1063
|
+
if (!taskId) return;
|
|
1064
|
+
try {
|
|
1065
|
+
const data = await fetchJson('/api/tasks/' + encodeURIComponent(taskId) + '/' + kind);
|
|
1066
|
+
showSafeModal(title, data);
|
|
1067
|
+
} catch (err) {
|
|
1068
|
+
showError('加载 ' + title + ' 失败: ' + err.message);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
// ---------- Task status priority ----------
|
|
1073
|
+
function primaryStatus(task) {
|
|
1074
|
+
if (!task) return { key: '', label: '—', kind: 'neutral' };
|
|
1075
|
+
const priority = [
|
|
1076
|
+
'failed_policy_violation',
|
|
1077
|
+
'failed_scope_violation',
|
|
1078
|
+
'failed_verification',
|
|
1079
|
+
'needs_fix',
|
|
1080
|
+
'ready_for_review',
|
|
1081
|
+
'done_by_agent',
|
|
1082
|
+
'running',
|
|
1083
|
+
'pending'
|
|
1084
|
+
];
|
|
1085
|
+
const status = task.status || '';
|
|
1086
|
+
const acceptance = task.acceptance_status || '';
|
|
1087
|
+
const statusIdx = priority.indexOf(status);
|
|
1088
|
+
const acceptanceIdx = priority.indexOf(acceptance);
|
|
1089
|
+
let chosen = '';
|
|
1090
|
+
if (statusIdx >= 0 && acceptanceIdx >= 0) {
|
|
1091
|
+
chosen = statusIdx <= acceptanceIdx ? status : acceptance;
|
|
1092
|
+
} else if (statusIdx >= 0) {
|
|
1093
|
+
chosen = status;
|
|
1094
|
+
} else if (acceptanceIdx >= 0) {
|
|
1095
|
+
chosen = acceptance;
|
|
1096
|
+
} else {
|
|
1097
|
+
chosen = acceptance || status || '';
|
|
1098
|
+
}
|
|
1099
|
+
return { key: chosen, label: chosen || '—', kind: statusKind(chosen || status) };
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
function verificationHeadline(t) {
|
|
1103
|
+
if (!t) return '—';
|
|
1104
|
+
if (t.verification && t.verification.headline) return t.verification.headline;
|
|
1105
|
+
if (t.safe_result && t.safe_result.verification && t.safe_result.verification.headline) return t.safe_result.verification.headline;
|
|
1106
|
+
if (t.verification_headline) return t.verification_headline;
|
|
1107
|
+
return '—';
|
|
1108
|
+
}
|
|
1109
|
+
|
|
881
1110
|
// ---------- Render: Service Card ----------
|
|
882
1111
|
function renderServiceCard(prefix, label, status) {
|
|
883
1112
|
const cardEl = $('card-' + prefix);
|
|
@@ -1000,21 +1229,77 @@ html.dark {
|
|
|
1000
1229
|
bodyEl.innerHTML = '<div class="px-4 py-6 text-center text-sm" style="color: var(--pw-text-tertiary);">暂无任务</div>';
|
|
1001
1230
|
return;
|
|
1002
1231
|
}
|
|
1232
|
+
const titleMap = {
|
|
1233
|
+
'safe-result': 'Safe Result',
|
|
1234
|
+
'safe-audit': 'Safe Audit',
|
|
1235
|
+
'safe-test-summary': 'Safe Test Summary',
|
|
1236
|
+
'safe-diff-summary': 'Safe Diff Summary'
|
|
1237
|
+
};
|
|
1003
1238
|
let html = '';
|
|
1004
1239
|
top.forEach(function (t) {
|
|
1005
|
-
const
|
|
1006
|
-
const
|
|
1007
|
-
const
|
|
1240
|
+
const taskId = t.task_id || '';
|
|
1241
|
+
const href = '/pages/task-detail.html?id=' + encodeURIComponent(taskId);
|
|
1242
|
+
const sKind = statusKind(t.status);
|
|
1243
|
+
const sColor = statusColors(sKind);
|
|
1244
|
+
const aKind = statusKind(t.acceptance_status);
|
|
1245
|
+
const aColor = statusColors(aKind);
|
|
1246
|
+
// Skip status badge when redundant with acceptance (done + done_by_agent)
|
|
1247
|
+
const skipStatusBadge = (t.status === 'done' && t.acceptance_status === 'done_by_agent');
|
|
1248
|
+
const headline = verificationHeadline(t);
|
|
1249
|
+
const updated = t.updated_at || t.created_at || '';
|
|
1250
|
+
|
|
1008
1251
|
html +=
|
|
1009
|
-
'<
|
|
1010
|
-
+ '<
|
|
1011
|
-
|
|
1012
|
-
+ '<span class="shrink-0 text-xs whitespace-nowrap" style="color: var(--pw-text-
|
|
1013
|
-
+ '<
|
|
1014
|
-
+ '<
|
|
1015
|
-
+ '</
|
|
1252
|
+
'<div data-task-row class="px-4 py-3 transition-colors" style="border-bottom: 1px solid var(--pw-border-subtle);">'
|
|
1253
|
+
+ '<div class="flex items-center gap-2 flex-wrap">'
|
|
1254
|
+
// short task_id + copy button
|
|
1255
|
+
+ '<span class="shrink-0 text-xs font-medium px-2 py-0.5 rounded whitespace-nowrap" style="color: var(--pw-text-secondary); background-color: var(--pw-bg-hover); font-family: var(--pw-font-mono);" title="' + escapeHtml(taskId) + '">' + escapeHtml(shortId(taskId)) + '</span>'
|
|
1256
|
+
+ '<button class="pw-quick-btn" data-copy-id="' + escapeHtml(taskId) + '" title="复制 task_id" type="button">'
|
|
1257
|
+
+ '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>'
|
|
1258
|
+
+ '</button>'
|
|
1259
|
+
// title (truncated, link to detail)
|
|
1260
|
+
+ '<a href="' + href + '" class="flex-1 min-w-0 text-sm truncate hover:underline" style="color: var(--pw-text-primary);" title="' + escapeHtml(t.title || '') + '">' + escapeHtml(t.title || '—') + '</a>'
|
|
1261
|
+
// agent badge
|
|
1262
|
+
+ (t.agent ? badgeHtml(t.agent, 'info') : '')
|
|
1263
|
+
// status badge (skip if redundant)
|
|
1264
|
+
+ (skipStatusBadge ? '' : '<span class="shrink-0 text-xs font-medium px-2 py-0.5 rounded-full whitespace-nowrap" style="color: ' + sColor.color + '; background-color: ' + sColor.bg + ';">' + escapeHtml(t.status || '—') + '</span>')
|
|
1265
|
+
// acceptance badge (more prominent)
|
|
1266
|
+
+ (t.acceptance_status ? '<span class="shrink-0 pw-acceptance-badge rounded-full whitespace-nowrap" style="color: ' + aColor.color + '; background-color: ' + aColor.bg + '; border-color: ' + aColor.color + ';">' + escapeHtml(t.acceptance_status) + '</span>' : '')
|
|
1267
|
+
+ '</div>'
|
|
1268
|
+
// second line: repo_path, verification headline, updated_at, next_action
|
|
1269
|
+
+ '<div class="flex items-center gap-3 mt-1.5 flex-wrap">'
|
|
1270
|
+
+ '<span class="text-xs truncate" style="color: var(--pw-text-tertiary); font-family: var(--pw-font-mono); max-width: 200px;" title="' + escapeHtml(t.repo_path || '') + '">' + escapeHtml(t.repo_path || '—') + '</span>'
|
|
1271
|
+
+ '<span class="text-xs truncate" style="color: var(--pw-text-secondary); max-width: 280px;" title="' + escapeHtml(headline) + '">verify: ' + escapeHtml(headline) + '</span>'
|
|
1272
|
+
+ '<span class="text-xs whitespace-nowrap" style="color: var(--pw-text-tertiary);">' + escapeHtml(fmtTime(updated)) + '</span>'
|
|
1273
|
+
+ (t.next_action ? '<span class="text-xs truncate" style="color: var(--pw-text-tertiary); max-width: 200px;" title="' + escapeHtml(t.next_action) + '">next: ' + escapeHtml(t.next_action) + '</span>' : '')
|
|
1274
|
+
+ '</div>'
|
|
1275
|
+
// quick action buttons
|
|
1276
|
+
+ '<div class="flex items-center gap-1 mt-1.5">'
|
|
1277
|
+
+ '<button class="pw-quick-btn" data-safe-action="safe-result" data-task-id="' + escapeHtml(taskId) + '" title="safe result" type="button"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg></button>'
|
|
1278
|
+
+ '<button class="pw-quick-btn" data-safe-action="safe-audit" data-task-id="' + escapeHtml(taskId) + '" title="safe audit" type="button"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg></button>'
|
|
1279
|
+
+ '<button class="pw-quick-btn" data-safe-action="safe-test-summary" data-task-id="' + escapeHtml(taskId) + '" title="safe test summary" type="button"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg></button>'
|
|
1280
|
+
+ '<button class="pw-quick-btn" data-safe-action="safe-diff-summary" data-task-id="' + escapeHtml(taskId) + '" title="safe diff summary" type="button"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="M12 5l7 7-7 7"/></svg></button>'
|
|
1281
|
+
+ '<a href="' + href + '" class="pw-quick-btn" title="打开详情"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg></a>'
|
|
1282
|
+
+ '</div>'
|
|
1283
|
+
+ '</div>';
|
|
1016
1284
|
});
|
|
1017
1285
|
bodyEl.innerHTML = html;
|
|
1286
|
+
// Wire quick action buttons
|
|
1287
|
+
bodyEl.querySelectorAll('[data-safe-action]').forEach(function (btn) {
|
|
1288
|
+
btn.addEventListener('click', function (e) {
|
|
1289
|
+
e.preventDefault();
|
|
1290
|
+
e.stopPropagation();
|
|
1291
|
+
const action = btn.getAttribute('data-safe-action');
|
|
1292
|
+
const tid = btn.getAttribute('data-task-id');
|
|
1293
|
+
fetchSafeSummary(tid, action, titleMap[action] || action);
|
|
1294
|
+
});
|
|
1295
|
+
});
|
|
1296
|
+
bodyEl.querySelectorAll('[data-copy-id]').forEach(function (btn) {
|
|
1297
|
+
btn.addEventListener('click', function (e) {
|
|
1298
|
+
e.preventDefault();
|
|
1299
|
+
e.stopPropagation();
|
|
1300
|
+
copyToClipboard(btn.getAttribute('data-copy-id'), 'task_id');
|
|
1301
|
+
});
|
|
1302
|
+
});
|
|
1018
1303
|
}
|
|
1019
1304
|
|
|
1020
1305
|
// ---------- Render: Activity Log ----------
|
|
@@ -1069,49 +1354,341 @@ html.dark {
|
|
|
1069
1354
|
const lineages = Array.isArray(data.lineages) ? data.lineages : [];
|
|
1070
1355
|
if (count) count.textContent = String(data.total || lineages.length);
|
|
1071
1356
|
if (!lineages.length) {
|
|
1072
|
-
body.innerHTML =
|
|
1357
|
+
body.innerHTML =
|
|
1358
|
+
'<div class="text-xs mb-2" style="color: var(--pw-text-tertiary);">No loop lineage yet</div>'
|
|
1359
|
+
+ '<div class="flex items-center gap-2">'
|
|
1360
|
+
+ '<a href="/tasks.html" class="text-xs px-2.5 py-1 rounded whitespace-nowrap" style="background-color: var(--pw-accent-muted); color: var(--pw-accent); border: 1px solid var(--pw-accent);">Start guarded loop</a>'
|
|
1361
|
+
+ '<a href="/tasks.html" class="text-xs px-2.5 py-1 rounded whitespace-nowrap" style="background-color: var(--pw-bg-hover); color: var(--pw-text-secondary); border: 1px solid var(--pw-border-subtle);">View recent loop runs</a>'
|
|
1362
|
+
+ '</div>';
|
|
1073
1363
|
return;
|
|
1074
1364
|
}
|
|
1075
1365
|
const top = lineages[0];
|
|
1076
|
-
const
|
|
1077
|
-
const
|
|
1078
|
-
|
|
1079
|
-
const directStatus = direct
|
|
1080
|
-
? ((direct.status || 'unknown') + ' / audit ' + (direct.audit_decision || 'not_run'))
|
|
1366
|
+
const dv = top.direct_verification || null;
|
|
1367
|
+
const directStatus = dv
|
|
1368
|
+
? (escapeHtml(dv.status || 'unknown') + ' / audit ' + escapeHtml(dv.audit_decision || 'not_run'))
|
|
1081
1369
|
: 'not run';
|
|
1370
|
+
const mainCount = top.main_task_count != null ? top.main_task_count : (top.tasks && top.tasks.main ? 1 : 0);
|
|
1371
|
+
const fixCount = top.fix_task_count != null ? top.fix_task_count : (top.tasks && Array.isArray(top.tasks.fix) ? top.tasks.fix.length : 0);
|
|
1372
|
+
const cleanupCount = top.cleanup_task_count != null ? top.cleanup_task_count : (top.tasks && Array.isArray(top.tasks.cleanup) ? top.tasks.cleanup.length : 0);
|
|
1373
|
+
const iterCount = top.iterations != null ? top.iterations : (Array.isArray(top.rounds) ? top.rounds.length : 0);
|
|
1374
|
+
const warnCount = top.warnings_count != null ? top.warnings_count : (Array.isArray(top.warnings) ? top.warnings.length : 0);
|
|
1082
1375
|
body.innerHTML =
|
|
1083
1376
|
'<div class="flex items-center justify-between gap-2 mb-2">'
|
|
1084
1377
|
+ '<span class="text-xs truncate" style="color: var(--pw-text-primary); font-family: var(--pw-font-mono);" title="' + escapeHtml(top.lineage_id || '') + '">' + escapeHtml(shortId(top.lineage_id)) + '</span>'
|
|
1085
|
-
+ badgeHtml(top.
|
|
1378
|
+
+ badgeHtml(top.final_status || 'unknown', statusKind(top.final_status))
|
|
1086
1379
|
+ '</div>'
|
|
1087
1380
|
+ '<div class="text-xs truncate mb-1" style="color: var(--pw-text-secondary);" title="' + escapeHtml(top.goal || '') + '">' + escapeHtml(top.goal || 'No goal') + '</div>'
|
|
1088
|
-
+ '<div class="text-xs truncate mb-1" style="color: var(--pw-text-tertiary);">
|
|
1089
|
-
+ '<div class="text-xs truncate mb-1" style="color: var(--pw-text-tertiary);"
|
|
1090
|
-
+ '<div class="text-xs" style="color: var(--pw-text-tertiary);"
|
|
1381
|
+
+ '<div class="text-xs truncate mb-1" style="color: var(--pw-text-tertiary);">stop_reason: ' + escapeHtml(top.stop_reason || 'unknown') + '</div>'
|
|
1382
|
+
+ '<div class="text-xs truncate mb-1" style="color: var(--pw-text-tertiary);">iterations: ' + escapeHtml(String(iterCount)) + ' · main/fix/cleanup: ' + escapeHtml(String(mainCount)) + '/' + escapeHtml(String(fixCount)) + '/' + escapeHtml(String(cleanupCount)) + '</div>'
|
|
1383
|
+
+ '<div class="text-xs truncate mb-1" style="color: var(--pw-text-tertiary);" title="' + escapeHtml(dv && dv.session_id ? dv.session_id : '') + '">direct: ' + directStatus + '</div>'
|
|
1384
|
+
+ '<div class="text-xs mb-2" style="color: var(--pw-text-tertiary);">warnings: ' + escapeHtml(String(warnCount)) + '</div>'
|
|
1385
|
+
+ '<div class="flex items-center gap-2">'
|
|
1386
|
+
+ '<button class="text-xs px-2.5 py-1 rounded whitespace-nowrap" data-lineage-detail="' + escapeHtml(top.lineage_id || '') + '" type="button" style="background-color: var(--pw-bg-hover); color: var(--pw-text-secondary); border: 1px solid var(--pw-border-subtle);">View detail</button>'
|
|
1387
|
+
+ '<button class="pw-quick-btn" data-copy-id="' + escapeHtml(top.lineage_id || '') + '" title="复制 lineage_id" type="button"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button>'
|
|
1388
|
+
+ '</div>';
|
|
1389
|
+
// Wire lineage detail button
|
|
1390
|
+
const detailBtn = body.querySelector('[data-lineage-detail]');
|
|
1391
|
+
if (detailBtn) {
|
|
1392
|
+
detailBtn.addEventListener('click', function () {
|
|
1393
|
+
const lid = detailBtn.getAttribute('data-lineage-detail');
|
|
1394
|
+
if (lid) showLineageDetail(lid);
|
|
1395
|
+
});
|
|
1396
|
+
}
|
|
1397
|
+
// Wire copy button
|
|
1398
|
+
const copyBtn = body.querySelector('[data-copy-id]');
|
|
1399
|
+
if (copyBtn) {
|
|
1400
|
+
copyBtn.addEventListener('click', function (e) {
|
|
1401
|
+
e.preventDefault();
|
|
1402
|
+
e.stopPropagation();
|
|
1403
|
+
copyToClipboard(copyBtn.getAttribute('data-copy-id'), 'lineage_id');
|
|
1404
|
+
});
|
|
1405
|
+
}
|
|
1091
1406
|
} catch (err) {
|
|
1092
1407
|
if (count) count.textContent = 'error';
|
|
1093
1408
|
body.innerHTML = '<div class="text-xs" style="color: var(--pw-state-error);">' + escapeHtml(err.message) + '</div>';
|
|
1094
1409
|
}
|
|
1095
1410
|
}
|
|
1096
1411
|
|
|
1412
|
+
async function showLineageDetail(lineageId) {
|
|
1413
|
+
if (!lineageId) return;
|
|
1414
|
+
const modal = $('safe-modal');
|
|
1415
|
+
const titleEl = $('safe-modal-title');
|
|
1416
|
+
const bodyEl = $('safe-modal-body');
|
|
1417
|
+
if (!modal || !titleEl || !bodyEl) return;
|
|
1418
|
+
titleEl.textContent = 'Lineage Detail · ' + shortId(lineageId);
|
|
1419
|
+
bodyEl.innerHTML = '<div class="text-xs" style="color: var(--pw-text-tertiary);">Loading…</div>';
|
|
1420
|
+
modal.classList.remove('hidden');
|
|
1421
|
+
try {
|
|
1422
|
+
const data = await fetchJson('/api/lineages/' + encodeURIComponent(lineageId));
|
|
1423
|
+
bodyEl.innerHTML = renderLineageDetail(data);
|
|
1424
|
+
} catch (err) {
|
|
1425
|
+
bodyEl.innerHTML = '<div class="text-xs" style="color: var(--pw-state-error);">' + escapeHtml(err.message) + '</div>';
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
function renderLineageDetail(d) {
|
|
1430
|
+
if (!d) return '<div class="text-xs" style="color: var(--pw-text-tertiary);">No data</div>';
|
|
1431
|
+
const rounds = Array.isArray(d.rounds) ? d.rounds : [];
|
|
1432
|
+
const mainRounds = rounds.filter(function (r) { return r.role === 'main'; });
|
|
1433
|
+
const fixRounds = rounds.filter(function (r) { return r.role === 'fix_tests'; });
|
|
1434
|
+
const cleanupRounds = rounds.filter(function (r) { return r.role === 'cleanup'; });
|
|
1435
|
+
const directSessions = (d.tasks && Array.isArray(d.tasks.direct_sessions)) ? d.tasks.direct_sessions : [];
|
|
1436
|
+
|
|
1437
|
+
function roundRow(r) {
|
|
1438
|
+
return '<div class="py-1.5" style="border-bottom: 1px solid var(--pw-border-subtle);">'
|
|
1439
|
+
+ '<div class="flex items-center gap-2 mb-1 flex-wrap">'
|
|
1440
|
+
+ '<span class="text-xs" style="color: var(--pw-text-primary); font-family: var(--pw-font-mono);" title="' + escapeHtml(r.task_id || '') + '">' + escapeHtml(shortId(r.task_id)) + '</span>'
|
|
1441
|
+
+ badgeHtml(r.status || '—', statusKind(r.status))
|
|
1442
|
+
+ '<span class="text-xs" style="color: var(--pw-text-tertiary);">iter ' + escapeHtml(String(r.iteration || 0)) + '</span>'
|
|
1443
|
+
+ '</div>'
|
|
1444
|
+
+ '<div class="text-xs mb-0.5" style="color: var(--pw-text-secondary);">verification: ' + escapeHtml(r.verification_status || '—') + '</div>'
|
|
1445
|
+
+ '<div class="text-xs mb-0.5" style="color: var(--pw-text-secondary);">audit: ' + escapeHtml(r.audit_verdict || '—') + '</div>'
|
|
1446
|
+
+ (r.next_action ? '<div class="text-xs" style="color: var(--pw-text-tertiary);">next: ' + escapeHtml(r.next_action) + '</div>' : '')
|
|
1447
|
+
+ '</div>';
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
function directRow(s) {
|
|
1451
|
+
return '<div class="py-1.5" style="border-bottom: 1px solid var(--pw-border-subtle);">'
|
|
1452
|
+
+ '<div class="flex items-center gap-2 mb-1 flex-wrap">'
|
|
1453
|
+
+ '<span class="text-xs" style="color: var(--pw-text-primary); font-family: var(--pw-font-mono);" title="' + escapeHtml(s.session_id || '') + '">' + escapeHtml(shortId(s.session_id)) + '</span>'
|
|
1454
|
+
+ badgeHtml(s.status || 'unknown', statusKind(s.status))
|
|
1455
|
+
+ '</div>'
|
|
1456
|
+
+ '<div class="text-xs mb-0.5" style="color: var(--pw-text-secondary);">audit: ' + escapeHtml(s.audit_decision || 'not_run') + '</div>'
|
|
1457
|
+
+ '<div class="text-xs mb-0.5" style="color: var(--pw-text-secondary);">commands: ' + escapeHtml(String(s.command_count || 0)) + ' (passed ' + escapeHtml(String(s.passed_commands || 0)) + ' / failed ' + escapeHtml(String(s.failed_commands || 0)) + ')</div>'
|
|
1458
|
+
+ (s.next_action ? '<div class="text-xs" style="color: var(--pw-text-tertiary);">next: ' + escapeHtml(s.next_action) + '</div>' : '')
|
|
1459
|
+
+ '</div>';
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
function section(title, items, renderFn) {
|
|
1463
|
+
if (!items || items.length === 0) return '';
|
|
1464
|
+
return '<div class="mt-3">'
|
|
1465
|
+
+ '<div class="text-xs font-semibold mb-1" style="color: var(--pw-text-primary);">' + escapeHtml(title) + ' (' + items.length + ')</div>'
|
|
1466
|
+
+ items.map(renderFn).join('')
|
|
1467
|
+
+ '</div>';
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
const header =
|
|
1471
|
+
'<div class="mb-2">'
|
|
1472
|
+
+ '<div class="text-xs mb-1" style="color: var(--pw-text-secondary);"><span style="color: var(--pw-text-tertiary);">goal:</span> ' + escapeHtml(d.goal || '—') + '</div>'
|
|
1473
|
+
+ '<div class="flex items-center gap-2 mb-1 flex-wrap">'
|
|
1474
|
+
+ '<span class="text-xs" style="color: var(--pw-text-tertiary);">final_status:</span>' + badgeHtml(d.final_status || 'unknown', statusKind(d.final_status))
|
|
1475
|
+
+ '<span class="text-xs" style="color: var(--pw-text-tertiary);">stop_reason:</span><span class="text-xs" style="color: var(--pw-text-secondary);">' + escapeHtml(d.stop_reason || '—') + '</span>'
|
|
1476
|
+
+ '</div>'
|
|
1477
|
+
+ '<div class="text-xs mb-1" style="color: var(--pw-text-tertiary);">iterations: ' + escapeHtml(String(d.iterations != null ? d.iterations : rounds.length)) + ' · warnings: ' + escapeHtml(String(d.warnings_count != null ? d.warnings_count : (Array.isArray(d.warnings) ? d.warnings.length : 0))) + '</div>'
|
|
1478
|
+
+ '<div class="text-xs mb-1" style="color: var(--pw-text-tertiary);">main: ' + escapeHtml(String(d.main_task_count != null ? d.main_task_count : 0)) + ' · fix: ' + escapeHtml(String(d.fix_task_count != null ? d.fix_task_count : 0)) + ' · cleanup: ' + escapeHtml(String(d.cleanup_task_count != null ? d.cleanup_task_count : 0)) + '</div>'
|
|
1479
|
+
+ '<div class="text-xs" style="color: var(--pw-text-tertiary);">final_recommended_next_action: ' + escapeHtml(d.next_action || '—') + '</div>'
|
|
1480
|
+
+ '</div>';
|
|
1481
|
+
|
|
1482
|
+
return header
|
|
1483
|
+
+ section('Main Tasks', mainRounds, roundRow)
|
|
1484
|
+
+ section('Fix Tasks', fixRounds, roundRow)
|
|
1485
|
+
+ section('Cleanup Tasks', cleanupRounds, roundRow)
|
|
1486
|
+
+ section('Direct Verification', directSessions, directRow);
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
// ---------- Health Score ----------
|
|
1490
|
+
// Computes a health score from the /api/status payload and supplementary data.
|
|
1491
|
+
// Returns: { level: 'healthy'|'warning'|'degraded'|'blocked', dimensions: [{key,label,ok,detail}] }
|
|
1492
|
+
function calculateHealthScore(status, opts) {
|
|
1493
|
+
opts = opts || {};
|
|
1494
|
+
const staleCount = typeof opts.staleTaskCount === 'number' ? opts.staleTaskCount : 0;
|
|
1495
|
+
const failedCount = typeof opts.failedTaskCount === 'number' ? opts.failedTaskCount : 0;
|
|
1496
|
+
const policyValid = opts.policyValid === true;
|
|
1497
|
+
const releaseReady = opts.releaseReady === true;
|
|
1498
|
+
const directProfileEnabled = opts.directProfileEnabled === true;
|
|
1499
|
+
|
|
1500
|
+
const watcher = (status && status.watcher) || {};
|
|
1501
|
+
const tunnel = (status && status.tunnel) || {};
|
|
1502
|
+
const agents = (status && status.agents) || [];
|
|
1503
|
+
|
|
1504
|
+
const watcherHealthy = watcher.status === 'healthy';
|
|
1505
|
+
const coreReady = !!(tunnel.core && tunnel.core.ready);
|
|
1506
|
+
const directReady = !!(tunnel.direct && tunnel.direct.ready);
|
|
1507
|
+
const tunnelReady = coreReady && directReady;
|
|
1508
|
+
const agentsAvailable = Array.isArray(agents) && agents.some(function (a) { return a && a.available; });
|
|
1509
|
+
|
|
1510
|
+
const dimensions = [
|
|
1511
|
+
{ key: 'watcher_healthy', label: 'Watcher healthy', ok: watcherHealthy, detail: watcher.status || 'unknown' },
|
|
1512
|
+
{ key: 'tunnel_ready', label: 'Tunnel ready', ok: tunnelReady, detail: 'core ' + (coreReady ? '✓' : '✗') + ' / direct ' + (directReady ? '✓' : '✗') },
|
|
1513
|
+
{ key: 'agents_available', label: 'Agents available', ok: agentsAvailable, detail: Array.isArray(agents) ? agents.length + ' agent(s)' : '0' },
|
|
1514
|
+
{ key: 'stale_tasks', label: 'Stale tasks', ok: staleCount === 0, detail: String(staleCount) },
|
|
1515
|
+
{ key: 'failed_tasks', label: 'Failed tasks', ok: failedCount === 0, detail: String(failedCount) },
|
|
1516
|
+
{ key: 'policy_valid', label: 'Policy valid', ok: policyValid, detail: policyValid ? 'valid' : 'invalid' },
|
|
1517
|
+
{ key: 'release_ready', label: 'Release ready', ok: releaseReady, detail: opts.releaseReadyState || 'unknown' },
|
|
1518
|
+
{ key: 'direct_profile', label: 'Direct profile', ok: directProfileEnabled, detail: directProfileEnabled ? 'enabled' : 'disabled' }
|
|
1519
|
+
];
|
|
1520
|
+
|
|
1521
|
+
// Scoring logic (order: blocked > degraded > warning > healthy)
|
|
1522
|
+
let level = 'healthy';
|
|
1523
|
+
const watcherDownAndFailed = (!watcherHealthy && failedCount > 0);
|
|
1524
|
+
const tunnelDownNoAgents = (!tunnelReady && !agentsAvailable);
|
|
1525
|
+
if (watcherDownAndFailed || tunnelDownNoAgents) {
|
|
1526
|
+
level = 'blocked';
|
|
1527
|
+
} else if (failedCount > 2 || staleCount > 3 || !watcherHealthy) {
|
|
1528
|
+
level = 'degraded';
|
|
1529
|
+
} else if (staleCount > 0 || failedCount > 0 || !directProfileEnabled || !releaseReady) {
|
|
1530
|
+
level = 'warning';
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
return { level: level, dimensions: dimensions };
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
function healthScoreColor(level) {
|
|
1537
|
+
if (level === 'healthy') return { color: 'var(--pw-state-success)', bg: 'var(--pw-state-success-bg)' };
|
|
1538
|
+
if (level === 'warning') return { color: 'var(--pw-state-warning)', bg: 'var(--pw-state-warning-bg)' };
|
|
1539
|
+
if (level === 'degraded') return { color: '#f97316', bg: 'rgba(249, 115, 22, 0.1)' };
|
|
1540
|
+
if (level === 'blocked') return { color: 'var(--pw-state-error)', bg: 'var(--pw-state-error-bg)' };
|
|
1541
|
+
return { color: 'var(--pw-text-secondary)', bg: 'var(--pw-bg-hover)' };
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
function renderHealthScore(score) {
|
|
1545
|
+
const card = $('health-score-card');
|
|
1546
|
+
const badge = $('health-score-badge');
|
|
1547
|
+
const body = $('health-score-body');
|
|
1548
|
+
if (!card || !badge || !body || !score) return;
|
|
1549
|
+
|
|
1550
|
+
const c = healthScoreColor(score.level);
|
|
1551
|
+
badge.textContent = score.level;
|
|
1552
|
+
badge.style.color = c.color;
|
|
1553
|
+
badge.style.backgroundColor = c.bg;
|
|
1554
|
+
|
|
1555
|
+
let html = '';
|
|
1556
|
+
score.dimensions.forEach(function (d) {
|
|
1557
|
+
const okColor = d.ok ? 'var(--pw-state-success)' : 'var(--pw-state-error)';
|
|
1558
|
+
const icon = d.ok ? '✓' : '✗';
|
|
1559
|
+
html +=
|
|
1560
|
+
'<div class="flex items-center gap-2 px-2 py-1.5 rounded" style="background-color: var(--pw-bg-hover); border: 1px solid var(--pw-border-subtle);">'
|
|
1561
|
+
+ '<span class="shrink-0 text-xs font-semibold" style="color: ' + okColor + '; font-family: var(--pw-font-mono);">' + icon + '</span>'
|
|
1562
|
+
+ '<div class="min-w-0 flex-1">'
|
|
1563
|
+
+ '<div class="text-xs truncate" style="color: var(--pw-text-primary); font-family: var(--pw-font-sans);">' + escapeHtml(d.label) + '</div>'
|
|
1564
|
+
+ '<div class="text-xs truncate" style="color: var(--pw-text-tertiary); font-family: var(--pw-font-mono);" title="' + escapeHtml(d.detail) + '">' + escapeHtml(d.detail) + '</div>'
|
|
1565
|
+
+ '</div>'
|
|
1566
|
+
+ '</div>';
|
|
1567
|
+
});
|
|
1568
|
+
body.innerHTML = html;
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
async function refreshHealthScore() {
|
|
1572
|
+
const card = $('health-score-card');
|
|
1573
|
+
if (!card) return;
|
|
1574
|
+
try {
|
|
1575
|
+
// Fetch all required data in parallel
|
|
1576
|
+
const [statusRes, staleRes, policyRes, releaseRes, diagRes] = await Promise.allSettled([
|
|
1577
|
+
fetchJson('/api/status'),
|
|
1578
|
+
fetchJson('/api/tasks/stale'),
|
|
1579
|
+
fetchJson('/api/project-policy?repo_path=' + encodeURIComponent(currentRepoPath)),
|
|
1580
|
+
fetchJson('/api/release/status?repo_path=' + encodeURIComponent(currentRepoPath)),
|
|
1581
|
+
fetchJson('/api/diagnostics')
|
|
1582
|
+
]);
|
|
1583
|
+
|
|
1584
|
+
const status = statusRes.status === 'fulfilled' ? statusRes.value : null;
|
|
1585
|
+
const staleData = staleRes.status === 'fulfilled' ? staleRes.value : null;
|
|
1586
|
+
const policyData = policyRes.status === 'fulfilled' ? policyRes.value : null;
|
|
1587
|
+
const releaseData = releaseRes.status === 'fulfilled' ? releaseRes.value : null;
|
|
1588
|
+
const diagData = diagRes.status === 'fulfilled' ? diagRes.value : null;
|
|
1589
|
+
|
|
1590
|
+
// stale task count
|
|
1591
|
+
let staleTaskCount = 0;
|
|
1592
|
+
if (staleData) {
|
|
1593
|
+
const arr = Array.isArray(staleData.stale_tasks) ? staleData.stale_tasks
|
|
1594
|
+
: (Array.isArray(staleData.tasks) ? staleData.tasks : []);
|
|
1595
|
+
staleTaskCount = arr.length;
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
// failed task count - derive from status.tasks
|
|
1599
|
+
let failedTaskCount = 0;
|
|
1600
|
+
if (status && status.tasks && Array.isArray(status.tasks.tasks)) {
|
|
1601
|
+
failedTaskCount = status.tasks.tasks.filter(function (t) {
|
|
1602
|
+
const s = String(t && t.status || '').toLowerCase();
|
|
1603
|
+
return s.indexOf('fail') >= 0;
|
|
1604
|
+
}).length;
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
// policy valid
|
|
1608
|
+
const policyValid = !!(policyData && policyData.valid !== false);
|
|
1609
|
+
|
|
1610
|
+
// release ready
|
|
1611
|
+
const readyState = releaseData ? (releaseData.ready_state || 'unknown') : 'unknown';
|
|
1612
|
+
const releaseReady = readyState === 'ready';
|
|
1613
|
+
|
|
1614
|
+
// direct profile enabled - from diagnostics or status
|
|
1615
|
+
let directProfileEnabled = false;
|
|
1616
|
+
if (diagData && typeof diagData.direct_profile_enabled === 'boolean') {
|
|
1617
|
+
directProfileEnabled = diagData.direct_profile_enabled;
|
|
1618
|
+
} else if (status && status.config && typeof status.config.enableDirectProfile === 'boolean') {
|
|
1619
|
+
directProfileEnabled = status.config.enableDirectProfile;
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
const score = calculateHealthScore(status, {
|
|
1623
|
+
staleTaskCount: staleTaskCount,
|
|
1624
|
+
failedTaskCount: failedTaskCount,
|
|
1625
|
+
policyValid: policyValid,
|
|
1626
|
+
releaseReady: releaseReady,
|
|
1627
|
+
releaseReadyState: readyState,
|
|
1628
|
+
directProfileEnabled: directProfileEnabled
|
|
1629
|
+
});
|
|
1630
|
+
renderHealthScore(score);
|
|
1631
|
+
} catch (err) {
|
|
1632
|
+
const body = $('health-score-body');
|
|
1633
|
+
const badge = $('health-score-badge');
|
|
1634
|
+
if (body) body.innerHTML = '<div class="text-xs" style="color: var(--pw-state-error);">健康评分加载失败: ' + escapeHtml(err.message) + '</div>';
|
|
1635
|
+
if (badge) { badge.textContent = 'error'; badge.style.color = 'var(--pw-state-error)'; badge.style.backgroundColor = 'var(--pw-state-error-bg)'; }
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1097
1639
|
async function refreshProjectPolicy() {
|
|
1098
1640
|
const body = $('policy-overview-body');
|
|
1099
1641
|
const status = $('policy-status');
|
|
1100
1642
|
if (!body) return;
|
|
1101
1643
|
try {
|
|
1102
|
-
const data = await fetchJson('/api/project-policy?repo_path
|
|
1644
|
+
const data = await fetchJson('/api/project-policy?repo_path=' + encodeURIComponent(currentRepoPath));
|
|
1103
1645
|
const valid = data && data.valid !== false;
|
|
1104
|
-
if (status)
|
|
1646
|
+
if (status) {
|
|
1647
|
+
status.textContent = valid ? 'valid' : 'issues';
|
|
1648
|
+
status.style.color = valid ? 'var(--pw-state-success)' : 'var(--pw-state-error)';
|
|
1649
|
+
}
|
|
1105
1650
|
const issueCount = Array.isArray(data.issues) ? data.issues.length : 0;
|
|
1106
1651
|
const policy = data.effective_policy || {};
|
|
1107
1652
|
const release = (policy.release_mode || {});
|
|
1653
|
+
|
|
1654
|
+
// auto_cleanup: boolean or object with enabled field
|
|
1655
|
+
const autoCleanupRaw = policy.auto_cleanup;
|
|
1656
|
+
let autoCleanupEnabled = false;
|
|
1657
|
+
if (autoCleanupRaw === true) autoCleanupEnabled = true;
|
|
1658
|
+
else if (autoCleanupRaw && autoCleanupRaw.enabled === true) autoCleanupEnabled = true;
|
|
1659
|
+
|
|
1660
|
+
// protected_paths / high_risk_commands counts
|
|
1661
|
+
const protectedPathsCount = Array.isArray(policy.protected_paths) ? policy.protected_paths.length : 0;
|
|
1662
|
+
const highRiskCmdCount = Array.isArray(policy.high_risk_commands) ? policy.high_risk_commands.length : 0;
|
|
1663
|
+
|
|
1664
|
+
// release_mode summary
|
|
1665
|
+
const versionSource = release.version_source || 'package.json';
|
|
1666
|
+
const readyState = release.ready_state || '';
|
|
1667
|
+
|
|
1668
|
+
function row(label, valueHtml) {
|
|
1669
|
+
return '<div class="flex items-center justify-between gap-2 py-1" style="border-bottom: 1px solid var(--pw-border-subtle);">'
|
|
1670
|
+
+ '<span class="text-xs whitespace-nowrap" style="color: var(--pw-text-secondary);">' + label + '</span>'
|
|
1671
|
+
+ '<span class="text-xs text-right min-w-0 truncate" style="color: var(--pw-text-primary); font-family: var(--pw-font-mono);">' + valueHtml + '</span>'
|
|
1672
|
+
+ '</div>';
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1108
1675
|
body.innerHTML =
|
|
1109
|
-
'<div class="flex items-center justify-between gap-2 mb-
|
|
1676
|
+
'<div class="flex items-center justify-between gap-2 mb-1">'
|
|
1110
1677
|
+ '<span class="text-xs" style="color: var(--pw-text-secondary);">policy file</span>'
|
|
1111
1678
|
+ badgeHtml(data.exists ? 'present' : 'defaults', data.exists ? 'success' : 'warning')
|
|
1112
1679
|
+ '</div>'
|
|
1113
|
-
+ '<div class="
|
|
1114
|
-
+ '<
|
|
1680
|
+
+ '<div class="flex items-center justify-between gap-2 mb-1">'
|
|
1681
|
+
+ '<span class="text-xs" style="color: var(--pw-text-secondary);">validity</span>'
|
|
1682
|
+
+ badgeHtml(valid ? 'valid' : 'invalid', valid ? 'success' : 'error')
|
|
1683
|
+
+ '</div>'
|
|
1684
|
+
+ row('issues', String(issueCount))
|
|
1685
|
+
+ row('auto_cleanup', autoCleanupEnabled
|
|
1686
|
+
? '<span style="color: var(--pw-state-success);">enabled</span>'
|
|
1687
|
+
: '<span style="color: var(--pw-text-tertiary);">disabled</span>')
|
|
1688
|
+
+ row('protected_paths', String(protectedPathsCount))
|
|
1689
|
+
+ row('high_risk_commands', String(highRiskCmdCount))
|
|
1690
|
+
+ row('version_source', escapeHtml(versionSource))
|
|
1691
|
+
+ (readyState ? row('ready_state', escapeHtml(readyState)) : '');
|
|
1115
1692
|
} catch (err) {
|
|
1116
1693
|
if (status) status.textContent = 'error';
|
|
1117
1694
|
body.innerHTML = '<div class="text-xs" style="color: var(--pw-state-error);">' + escapeHtml(err.message) + '</div>';
|
|
@@ -1123,19 +1700,89 @@ html.dark {
|
|
|
1123
1700
|
const status = $('release-status');
|
|
1124
1701
|
if (!body) return;
|
|
1125
1702
|
try {
|
|
1126
|
-
const data = await fetchJson('/api/release/status?repo_path
|
|
1127
|
-
const
|
|
1128
|
-
|
|
1129
|
-
const
|
|
1130
|
-
const commands = Array.isArray(
|
|
1131
|
-
const
|
|
1703
|
+
const data = await fetchJson('/api/release/status?repo_path=' + encodeURIComponent(currentRepoPath));
|
|
1704
|
+
const vs = data.version_source || '';
|
|
1705
|
+
const ver = data.version || '';
|
|
1706
|
+
const consistent = data.version_consistent;
|
|
1707
|
+
const commands = Array.isArray(data.required_commands) ? data.required_commands : [];
|
|
1708
|
+
const blockedCount = data.commands_blocked_count != null ? data.commands_blocked_count : commands.filter(function (e) { return !e.allowed; }).length;
|
|
1709
|
+
const readyState = data.ready_state || 'unknown';
|
|
1710
|
+
const nextAction = data.next_action || '';
|
|
1711
|
+
|
|
1712
|
+
// Version display logic
|
|
1713
|
+
let versionDisplay;
|
|
1714
|
+
if (vs === 'workspace_root' || !ver) {
|
|
1715
|
+
versionDisplay = '选择具体 repo 以读取 version';
|
|
1716
|
+
} else {
|
|
1717
|
+
versionDisplay = ver;
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
// ready_state badge
|
|
1721
|
+
let readyKind = 'warning';
|
|
1722
|
+
if (readyState === 'ready') readyKind = 'success';
|
|
1723
|
+
else if (readyState === 'blocked') readyKind = 'error';
|
|
1724
|
+
|
|
1725
|
+
// version_consistent badge
|
|
1726
|
+
const consistentKind = consistent === true ? 'success' : (consistent === false ? 'error' : 'neutral');
|
|
1727
|
+
const consistentText = consistent === true ? '✓' : (consistent === false ? '✗' : '—');
|
|
1728
|
+
|
|
1729
|
+
// commands list
|
|
1730
|
+
let cmdHtml = '';
|
|
1731
|
+
if (commands.length > 0) {
|
|
1732
|
+
cmdHtml = '<div class="mt-2 space-y-1">';
|
|
1733
|
+
commands.forEach(function (cmd) {
|
|
1734
|
+
const allowedIcon = cmd.allowed ? '✓' : '✗';
|
|
1735
|
+
const cmdKind = cmd.allowed ? 'success' : 'error';
|
|
1736
|
+
const c = statusColors(cmdKind);
|
|
1737
|
+
cmdHtml += '<div class="flex items-center gap-2 text-xs">'
|
|
1738
|
+
+ '<span style="color: ' + c.color + ';">' + allowedIcon + '</span>'
|
|
1739
|
+
+ '<span style="color: var(--pw-text-primary); font-family: var(--pw-font-mono);">' + escapeHtml(cmd.command) + '</span>'
|
|
1740
|
+
+ (cmd.blocked_reason ? '<span style="color: var(--pw-state-warning); font-family: var(--pw-font-sans);">(' + escapeHtml(cmd.blocked_reason) + ')</span>' : '')
|
|
1741
|
+
+ '</div>';
|
|
1742
|
+
});
|
|
1743
|
+
cmdHtml += '</div>';
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
// blocked reason text
|
|
1747
|
+
let blockedReasonHtml = '';
|
|
1748
|
+
if (readyState === 'blocked' && data.blocked_reason) {
|
|
1749
|
+
blockedReasonHtml = '<div class="text-xs mt-2" style="color: var(--pw-state-error);">' + escapeHtml(data.blocked_reason) + '</div>';
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
if (status) {
|
|
1753
|
+
status.textContent = readyState;
|
|
1754
|
+
const sc = statusColors(readyKind);
|
|
1755
|
+
status.style.color = sc.color;
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1132
1758
|
body.innerHTML =
|
|
1133
|
-
'<div class="flex items-center justify-between gap-2 mb-
|
|
1759
|
+
'<div class="flex items-center justify-between gap-2 mb-1">'
|
|
1760
|
+
+ '<span class="text-xs" style="color: var(--pw-text-secondary);">package</span>'
|
|
1761
|
+
+ '<span class="text-xs" style="color: var(--pw-text-primary); font-family: var(--pw-font-mono);">' + escapeHtml(data.package_name || '—') + '</span>'
|
|
1762
|
+
+ '</div>'
|
|
1763
|
+
+ '<div class="flex items-center justify-between gap-2 mb-1">'
|
|
1764
|
+
+ '<span class="text-xs" style="color: var(--pw-text-secondary);">version_source</span>'
|
|
1765
|
+
+ '<span class="text-xs" style="color: var(--pw-text-primary); font-family: var(--pw-font-mono);">' + escapeHtml(vs || 'unknown') + '</span>'
|
|
1766
|
+
+ '</div>'
|
|
1767
|
+
+ '<div class="flex items-center justify-between gap-2 mb-1">'
|
|
1134
1768
|
+ '<span class="text-xs" style="color: var(--pw-text-secondary);">version</span>'
|
|
1135
|
-
+ '<span class="text-xs" style="color: var(--pw-text-primary); font-family: var(--pw-font-mono);">' + escapeHtml(
|
|
1769
|
+
+ '<span class="text-xs" style="color: var(--pw-text-primary); font-family: var(--pw-font-mono);">' + escapeHtml(versionDisplay) + '</span>'
|
|
1770
|
+
+ '</div>'
|
|
1771
|
+
+ '<div class="flex items-center justify-between gap-2 mb-1">'
|
|
1772
|
+
+ '<span class="text-xs" style="color: var(--pw-text-secondary);">consistent</span>'
|
|
1773
|
+
+ '<span class="text-xs" style="color: ' + statusColors(consistentKind).color + ';">' + consistentText + '</span>'
|
|
1136
1774
|
+ '</div>'
|
|
1137
|
-
+ '<div class="
|
|
1138
|
-
+ '<
|
|
1775
|
+
+ '<div class="flex items-center justify-between gap-2 mb-1">'
|
|
1776
|
+
+ '<span class="text-xs" style="color: var(--pw-text-secondary);">commands</span>'
|
|
1777
|
+
+ '<span class="text-xs" style="color: var(--pw-text-primary); font-family: var(--pw-font-mono);">' + blockedCount + ' / ' + commands.length + ' blocked</span>'
|
|
1778
|
+
+ '</div>'
|
|
1779
|
+
+ '<div class="flex items-center justify-between gap-2 mb-1">'
|
|
1780
|
+
+ '<span class="text-xs" style="color: var(--pw-text-secondary);">ready</span>'
|
|
1781
|
+
+ badgeHtml(readyState, readyKind)
|
|
1782
|
+
+ '</div>'
|
|
1783
|
+
+ cmdHtml
|
|
1784
|
+
+ blockedReasonHtml
|
|
1785
|
+
+ (nextAction ? '<div class="text-xs mt-1" style="color: var(--pw-text-tertiary);">next: ' + escapeHtml(nextAction) + '</div>' : '');
|
|
1139
1786
|
} catch (err) {
|
|
1140
1787
|
if (status) status.textContent = 'error';
|
|
1141
1788
|
body.innerHTML = '<div class="text-xs" style="color: var(--pw-state-error);">' + escapeHtml(err.message) + '</div>';
|
|
@@ -1150,24 +1797,261 @@ html.dark {
|
|
|
1150
1797
|
const data = await fetchJson('/api/evidence-packs');
|
|
1151
1798
|
const packs = Array.isArray(data.evidence_packs) ? data.evidence_packs : [];
|
|
1152
1799
|
if (status) status.textContent = String(data.total || packs.length);
|
|
1800
|
+
const hasLineages = data.has_lineages === true;
|
|
1801
|
+
const pendingIds = Array.isArray(data.pending_lineage_ids) ? data.pending_lineage_ids : [];
|
|
1802
|
+
|
|
1153
1803
|
if (!packs.length) {
|
|
1154
|
-
|
|
1804
|
+
if (!hasLineages) {
|
|
1805
|
+
body.innerHTML = '<div class="text-xs" style="color: var(--pw-text-tertiary);">Evidence pack is available after run_task_loop.</div>';
|
|
1806
|
+
return;
|
|
1807
|
+
}
|
|
1808
|
+
// Lineage exists but no evidence pack yet — offer export for the most recent pending lineage.
|
|
1809
|
+
const exportId = pendingIds.length > 0 ? pendingIds[0] : null;
|
|
1810
|
+
if (exportId) {
|
|
1811
|
+
body.innerHTML =
|
|
1812
|
+
'<div class="text-xs mb-2" style="color: var(--pw-text-tertiary);">Lineage exists but no evidence pack yet.</div>'
|
|
1813
|
+
+ '<div class="flex items-center gap-2">'
|
|
1814
|
+
+ '<button class="text-xs px-2.5 py-1 rounded whitespace-nowrap" data-export-pack="' + escapeHtml(exportId) + '" type="button" style="background-color: var(--pw-accent-muted); color: var(--pw-accent); border: 1px solid var(--pw-accent);">Export evidence pack</button>'
|
|
1815
|
+
+ '<button class="pw-quick-btn" data-copy-id="' + escapeHtml(exportId) + '" title="复制 lineage_id" type="button"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button>'
|
|
1816
|
+
+ '</div>';
|
|
1817
|
+
const exportBtn = body.querySelector('[data-export-pack]');
|
|
1818
|
+
if (exportBtn) {
|
|
1819
|
+
exportBtn.addEventListener('click', function () {
|
|
1820
|
+
const lid = exportBtn.getAttribute('data-export-pack');
|
|
1821
|
+
if (!lid) return;
|
|
1822
|
+
postAction('/api/evidence-packs/' + encodeURIComponent(lid) + '/export', exportBtn).then(function () {
|
|
1823
|
+
showToast('success', '导出成功', 'Evidence pack 已导出');
|
|
1824
|
+
refreshEvidencePacks();
|
|
1825
|
+
}).catch(function (err) {
|
|
1826
|
+
showToast('error', '导出失败', err.message);
|
|
1827
|
+
});
|
|
1828
|
+
});
|
|
1829
|
+
}
|
|
1830
|
+
const copyBtn = body.querySelector('[data-copy-id]');
|
|
1831
|
+
if (copyBtn) {
|
|
1832
|
+
copyBtn.addEventListener('click', function (e) {
|
|
1833
|
+
e.preventDefault();
|
|
1834
|
+
e.stopPropagation();
|
|
1835
|
+
copyToClipboard(copyBtn.getAttribute('data-copy-id'), 'lineage_id');
|
|
1836
|
+
});
|
|
1837
|
+
}
|
|
1838
|
+
} else {
|
|
1839
|
+
body.innerHTML = '<div class="text-xs" style="color: var(--pw-text-tertiary);">No exported evidence pack yet</div>';
|
|
1840
|
+
}
|
|
1155
1841
|
return;
|
|
1156
1842
|
}
|
|
1843
|
+
|
|
1157
1844
|
const top = packs[0];
|
|
1845
|
+
const exportStatus = top.export_status || 'exported';
|
|
1846
|
+
const jsonExists = top.evidence_json_exists === true;
|
|
1847
|
+
const mdExists = top.evidence_md_exists === true;
|
|
1848
|
+
const exportedAt = top.exported_at || (top.generated_at || '—');
|
|
1849
|
+
const lineageId = top.lineage_id || '';
|
|
1158
1850
|
body.innerHTML =
|
|
1159
1851
|
'<div class="flex items-center justify-between gap-2 mb-2">'
|
|
1160
|
-
+ '<span class="text-xs truncate" style="color: var(--pw-text-primary); font-family: var(--pw-font-mono);" title="' + escapeHtml(
|
|
1161
|
-
+ badgeHtml(
|
|
1852
|
+
+ '<span class="text-xs truncate" style="color: var(--pw-text-primary); font-family: var(--pw-font-mono);" title="' + escapeHtml(lineageId) + '">' + escapeHtml(shortId(lineageId)) + '</span>'
|
|
1853
|
+
+ badgeHtml(exportStatus, exportStatus === 'exported' ? 'success' : 'warning')
|
|
1162
1854
|
+ '</div>'
|
|
1163
|
-
+ '<div class="text-xs mb-1" style="color: var(--pw-text-tertiary);">
|
|
1164
|
-
+ '<div class="text-xs
|
|
1855
|
+
+ '<div class="text-xs mb-1" style="color: var(--pw-text-tertiary);">evidence.json: ' + (jsonExists ? '✓' : '✗') + ' · EVIDENCE.md: ' + (mdExists ? '✓' : '✗') + '</div>'
|
|
1856
|
+
+ '<div class="text-xs mb-1" style="color: var(--pw-text-tertiary);" title="' + escapeHtml(exportedAt) + '">exported_at: ' + escapeHtml(fmtTime(exportedAt)) + '</div>'
|
|
1857
|
+
+ '<div class="text-xs mb-2" style="color: var(--pw-text-tertiary);">policy issues: ' + escapeHtml(String(top.policy ? top.policy.issue_count : 0)) + '</div>'
|
|
1858
|
+
+ '<div class="flex items-center gap-2 flex-wrap">'
|
|
1859
|
+
+ (exportStatus !== 'exported' ? '<button class="text-xs px-2.5 py-1 rounded whitespace-nowrap" data-export-pack="' + escapeHtml(lineageId) + '" type="button" style="background-color: var(--pw-accent-muted); color: var(--pw-accent); border: 1px solid var(--pw-accent);">Export evidence pack</button>' : '')
|
|
1860
|
+
+ '<button class="text-xs px-2.5 py-1 rounded whitespace-nowrap" data-view-evidence="' + escapeHtml(lineageId) + '" type="button" style="background-color: var(--pw-bg-hover); color: var(--pw-text-secondary); border: 1px solid var(--pw-border-subtle);">View detail</button>'
|
|
1861
|
+
+ '<button class="pw-quick-btn" data-copy-id="' + escapeHtml(lineageId) + '" title="复制 lineage_id" type="button"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button>'
|
|
1862
|
+
+ '</div>';
|
|
1863
|
+
// Wire export button (if present)
|
|
1864
|
+
const exportBtn2 = body.querySelector('[data-export-pack]');
|
|
1865
|
+
if (exportBtn2) {
|
|
1866
|
+
exportBtn2.addEventListener('click', function () {
|
|
1867
|
+
const lid = exportBtn2.getAttribute('data-export-pack');
|
|
1868
|
+
if (!lid) return;
|
|
1869
|
+
postAction('/api/evidence-packs/' + encodeURIComponent(lid) + '/export', exportBtn2).then(function () {
|
|
1870
|
+
showToast('success', '导出成功', 'Evidence pack 已导出');
|
|
1871
|
+
refreshEvidencePacks();
|
|
1872
|
+
}).catch(function (err) {
|
|
1873
|
+
showToast('error', '导出失败', err.message);
|
|
1874
|
+
});
|
|
1875
|
+
});
|
|
1876
|
+
}
|
|
1877
|
+
// Wire view detail button
|
|
1878
|
+
const viewBtn = body.querySelector('[data-view-evidence]');
|
|
1879
|
+
if (viewBtn) {
|
|
1880
|
+
viewBtn.addEventListener('click', function () {
|
|
1881
|
+
const lid = viewBtn.getAttribute('data-view-evidence');
|
|
1882
|
+
if (lid) showEvidencePackDetail(lid);
|
|
1883
|
+
});
|
|
1884
|
+
}
|
|
1885
|
+
// Wire copy button
|
|
1886
|
+
const copyBtn2 = body.querySelector('[data-copy-id]');
|
|
1887
|
+
if (copyBtn2) {
|
|
1888
|
+
copyBtn2.addEventListener('click', function (e) {
|
|
1889
|
+
e.preventDefault();
|
|
1890
|
+
e.stopPropagation();
|
|
1891
|
+
copyToClipboard(copyBtn2.getAttribute('data-copy-id'), 'lineage_id');
|
|
1892
|
+
});
|
|
1893
|
+
}
|
|
1165
1894
|
} catch (err) {
|
|
1166
1895
|
if (status) status.textContent = 'error';
|
|
1167
1896
|
body.innerHTML = '<div class="text-xs" style="color: var(--pw-state-error);">' + escapeHtml(err.message) + '</div>';
|
|
1168
1897
|
}
|
|
1169
1898
|
}
|
|
1170
1899
|
|
|
1900
|
+
async function showEvidencePackDetail(lineageId) {
|
|
1901
|
+
if (!lineageId) return;
|
|
1902
|
+
const modal = $('safe-modal');
|
|
1903
|
+
const titleEl = $('safe-modal-title');
|
|
1904
|
+
const bodyEl = $('safe-modal-body');
|
|
1905
|
+
if (!modal || !titleEl || !bodyEl) return;
|
|
1906
|
+
titleEl.textContent = 'Evidence Pack · ' + shortId(lineageId);
|
|
1907
|
+
bodyEl.innerHTML = '<div class="text-xs" style="color: var(--pw-text-tertiary);">Loading…</div>';
|
|
1908
|
+
modal.classList.remove('hidden');
|
|
1909
|
+
try {
|
|
1910
|
+
const data = await fetchJson('/api/evidence-packs/' + encodeURIComponent(lineageId));
|
|
1911
|
+
const jsonPath = data.files && data.files.json ? data.files.json : '';
|
|
1912
|
+
const mdPath = data.files && data.files.markdown ? data.files.markdown : '';
|
|
1913
|
+
const lineage = data.lineage || {};
|
|
1914
|
+
bodyEl.innerHTML =
|
|
1915
|
+
'<div class="mb-3">'
|
|
1916
|
+
+ '<div class="text-xs mb-1" style="color: var(--pw-text-secondary);"><span style="color: var(--pw-text-tertiary);">lineage_id:</span> ' + escapeHtml(data.lineage_id || lineageId) + '</div>'
|
|
1917
|
+
+ '<div class="text-xs mb-1" style="color: var(--pw-text-secondary);"><span style="color: var(--pw-text-tertiary);">generated_at:</span> ' + escapeHtml(fmtTime(data.generated_at || '—')) + '</div>'
|
|
1918
|
+
+ '<div class="flex items-center gap-2 mb-1 flex-wrap">'
|
|
1919
|
+
+ '<span class="text-xs" style="color: var(--pw-text-tertiary);">final_status:</span>' + badgeHtml(lineage.final_status || 'unknown', statusKind(lineage.final_status))
|
|
1920
|
+
+ '<span class="text-xs" style="color: var(--pw-text-tertiary);">stop_reason:</span><span class="text-xs" style="color: var(--pw-text-secondary);">' + escapeHtml(lineage.stop_reason || '—') + '</span>'
|
|
1921
|
+
+ '</div>'
|
|
1922
|
+
+ '<div class="text-xs mb-1" style="color: var(--pw-text-tertiary);">policy valid: ' + (data.policy && data.policy.valid ? '✓' : '✗') + ' · issues: ' + escapeHtml(String(data.policy ? data.policy.issue_count : 0)) + '</div>'
|
|
1923
|
+
+ (data.next_action ? '<div class="text-xs mb-1" style="color: var(--pw-text-tertiary);">next: ' + escapeHtml(data.next_action) + '</div>' : '')
|
|
1924
|
+
+ '</div>'
|
|
1925
|
+
+ '<div class="mt-2">'
|
|
1926
|
+
+ '<div class="text-xs font-semibold mb-1" style="color: var(--pw-text-primary);">Files</div>'
|
|
1927
|
+
+ '<div class="text-xs mb-1" style="color: var(--pw-text-secondary); font-family: var(--pw-font-mono); word-break: break-all;" title="' + escapeHtml(jsonPath) + '">evidence.json: ' + (jsonPath ? escapeHtml(jsonPath) : '—') + '</div>'
|
|
1928
|
+
+ '<div class="text-xs mb-1" style="color: var(--pw-text-secondary); font-family: var(--pw-font-mono); word-break: break-all;" title="' + escapeHtml(mdPath) + '">EVIDENCE.md: ' + (mdPath ? escapeHtml(mdPath) : '—') + '</div>'
|
|
1929
|
+
+ '<div class="flex items-center gap-2 mt-2">'
|
|
1930
|
+
+ (jsonPath ? '<button class="text-xs px-2.5 py-1 rounded whitespace-nowrap" data-copy-path="' + escapeHtml(jsonPath) + '" type="button" style="background-color: var(--pw-bg-hover); color: var(--pw-text-secondary); border: 1px solid var(--pw-border-subtle);">Copy evidence.json path</button>' : '')
|
|
1931
|
+
+ (mdPath ? '<button class="text-xs px-2.5 py-1 rounded whitespace-nowrap" data-copy-path="' + escapeHtml(mdPath) + '" type="button" style="background-color: var(--pw-bg-hover); color: var(--pw-text-secondary); border: 1px solid var(--pw-border-subtle);">Copy EVIDENCE.md path</button>' : '')
|
|
1932
|
+
+ '</div>'
|
|
1933
|
+
+ '</div>';
|
|
1934
|
+
// Wire copy path buttons
|
|
1935
|
+
bodyEl.querySelectorAll('[data-copy-path]').forEach(function (btn) {
|
|
1936
|
+
btn.addEventListener('click', function () {
|
|
1937
|
+
copyToClipboard(btn.getAttribute('data-copy-path'), 'file path');
|
|
1938
|
+
});
|
|
1939
|
+
});
|
|
1940
|
+
} catch (err) {
|
|
1941
|
+
bodyEl.innerHTML = '<div class="text-xs" style="color: var(--pw-state-error);">' + escapeHtml(err.message) + '</div>';
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
// ---------- Repo selector ----------
|
|
1946
|
+
async function loadRepos() {
|
|
1947
|
+
const select = $('repo-selector');
|
|
1948
|
+
if (!select) return;
|
|
1949
|
+
try {
|
|
1950
|
+
const data = await fetchJson('/api/workspace/repos');
|
|
1951
|
+
const repos = Array.isArray(data.repos) ? data.repos : [];
|
|
1952
|
+
let html = '<option value=".">workspace root</option>';
|
|
1953
|
+
repos.forEach(function (r) {
|
|
1954
|
+
const label = r.name + (r.has_package_json && r.version ? ' (v' + r.version + ')' : '');
|
|
1955
|
+
const val = r.path || r.name;
|
|
1956
|
+
html += '<option value="' + escapeHtml(val) + '">' + escapeHtml(label) + '</option>';
|
|
1957
|
+
});
|
|
1958
|
+
select.innerHTML = html;
|
|
1959
|
+
} catch (err) {
|
|
1960
|
+
// keep default option
|
|
1961
|
+
console.error('loadRepos failed:', err);
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
function onRepoChange(selectedRepoPath) {
|
|
1966
|
+
currentRepoPath = selectedRepoPath || '.';
|
|
1967
|
+
Promise.all([
|
|
1968
|
+
refreshProjectPolicy(),
|
|
1969
|
+
refreshReleaseStatus(),
|
|
1970
|
+
refreshTasks(),
|
|
1971
|
+
refreshLineages(),
|
|
1972
|
+
refreshEvidencePacks(),
|
|
1973
|
+
refreshStaleTasks(),
|
|
1974
|
+
refreshHealthScore()
|
|
1975
|
+
]).catch(function (err) {
|
|
1976
|
+
showError('切换 repo 刷新失败: ' + err.message);
|
|
1977
|
+
});
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
// ---------- Stale tasks ----------
|
|
1981
|
+
async function refreshStaleTasks() {
|
|
1982
|
+
const body = $('stale-tasks-body');
|
|
1983
|
+
const countEl = $('stale-tasks-count');
|
|
1984
|
+
const card = $('stale-tasks-card');
|
|
1985
|
+
if (!body) return;
|
|
1986
|
+
try {
|
|
1987
|
+
const data = await fetchJson('/api/tasks/stale');
|
|
1988
|
+
const staleTasks = Array.isArray(data.stale_tasks) ? data.stale_tasks : [];
|
|
1989
|
+
const total = (typeof data.total === 'number') ? data.total : staleTasks.length;
|
|
1990
|
+
if (countEl) countEl.textContent = String(total);
|
|
1991
|
+
if (!staleTasks.length) {
|
|
1992
|
+
if (card) card.classList.add('hidden');
|
|
1993
|
+
return;
|
|
1994
|
+
}
|
|
1995
|
+
if (card) card.classList.remove('hidden');
|
|
1996
|
+
let html = '';
|
|
1997
|
+
staleTasks.forEach(function (st) {
|
|
1998
|
+
const taskId = st.task_id || '';
|
|
1999
|
+
const href = '/pages/task-detail.html?id=' + encodeURIComponent(taskId);
|
|
2000
|
+
const statusBadge = badgeHtml(st.status || '—', statusKind(st.status));
|
|
2001
|
+
const errorText = st.error ? escapeHtml(st.error.length > 100 ? st.error.slice(0, 100) + '…' : st.error) : '';
|
|
2002
|
+
const explanation = st.explanation || '';
|
|
2003
|
+
const nextAction = st.next_action || '';
|
|
2004
|
+
html +=
|
|
2005
|
+
'<div class="py-3" style="border-bottom: 1px solid var(--pw-border-subtle);" data-stale-task="' + escapeHtml(taskId) + '">'
|
|
2006
|
+
+ '<div class="flex items-center gap-2 mb-1.5 flex-wrap">'
|
|
2007
|
+
+ '<span class="text-xs font-medium px-2 py-0.5 rounded whitespace-nowrap" style="color: var(--pw-text-secondary); background-color: var(--pw-bg-hover); font-family: var(--pw-font-mono);" title="' + escapeHtml(taskId) + '">' + escapeHtml(shortId(taskId)) + '</span>'
|
|
2008
|
+
+ statusBadge
|
|
2009
|
+
+ '<span class="text-xs truncate" style="color: var(--pw-text-tertiary); font-family: var(--pw-font-mono); max-width: 200px;" title="' + escapeHtml(st.repo_path || '') + '">' + escapeHtml(st.repo_path || '—') + '</span>'
|
|
2010
|
+
+ '<div class="flex-1"></div>'
|
|
2011
|
+
+ '<button class="pw-quick-btn" data-copy-id="' + escapeHtml(taskId) + '" title="复制 task_id" type="button"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button>'
|
|
2012
|
+
+ '</div>'
|
|
2013
|
+
+ (explanation ? '<div class="pw-stale-explanation mb-1.5">' + escapeHtml(explanation) + '</div>' : '')
|
|
2014
|
+
+ (errorText ? '<div class="text-xs mb-1" style="color: var(--pw-state-error); font-family: var(--pw-font-mono);" title="' + escapeHtml(st.error || '') + '">' + errorText + '</div>' : '')
|
|
2015
|
+
+ (nextAction ? '<div class="text-xs mb-1.5" style="color: var(--pw-text-secondary);">next: ' + escapeHtml(nextAction) + '</div>' : '')
|
|
2016
|
+
+ '<div class="flex items-center gap-2">'
|
|
2017
|
+
+ '<a href="' + href + '" class="text-xs px-2.5 py-1 rounded whitespace-nowrap" style="background-color: var(--pw-bg-hover); color: var(--pw-text-secondary); border: 1px solid var(--pw-border-subtle);">查看详情</a>'
|
|
2018
|
+
+ '<button class="text-xs px-2.5 py-1 rounded whitespace-nowrap" data-hide-stale="' + escapeHtml(taskId) + '" type="button" style="background-color: var(--pw-state-error-bg); color: var(--pw-state-error); border: 1px solid var(--pw-state-error);">隐藏</button>'
|
|
2019
|
+
+ '<button class="text-xs px-2.5 py-1 rounded whitespace-nowrap" disabled style="background-color: var(--pw-bg-hover); color: var(--pw-text-tertiary); border: 1px solid var(--pw-border-subtle);">重建任务</button>'
|
|
2020
|
+
+ '</div>'
|
|
2021
|
+
+ '</div>';
|
|
2022
|
+
});
|
|
2023
|
+
body.innerHTML = html;
|
|
2024
|
+
// Wire copy buttons
|
|
2025
|
+
body.querySelectorAll('[data-copy-id]').forEach(function (btn) {
|
|
2026
|
+
btn.addEventListener('click', function (e) {
|
|
2027
|
+
e.preventDefault();
|
|
2028
|
+
e.stopPropagation();
|
|
2029
|
+
copyToClipboard(btn.getAttribute('data-copy-id'), 'task_id');
|
|
2030
|
+
});
|
|
2031
|
+
});
|
|
2032
|
+
// Wire hide-stale buttons
|
|
2033
|
+
body.querySelectorAll('[data-hide-stale]').forEach(function (btn) {
|
|
2034
|
+
btn.addEventListener('click', function (e) {
|
|
2035
|
+
e.preventDefault();
|
|
2036
|
+
e.stopPropagation();
|
|
2037
|
+
const tid = btn.getAttribute('data-hide-stale');
|
|
2038
|
+
postAction('/api/tasks/' + encodeURIComponent(tid) + '/hide-stale', btn).then(function () {
|
|
2039
|
+
showToast('success', '已隐藏', 'Stale 任务已隐藏');
|
|
2040
|
+
const row = btn.closest('[data-stale-task]');
|
|
2041
|
+
if (row && row.parentNode) row.parentNode.removeChild(row);
|
|
2042
|
+
const remaining = body.querySelectorAll('[data-stale-task]').length;
|
|
2043
|
+
if (countEl) countEl.textContent = String(remaining);
|
|
2044
|
+
if (remaining === 0 && card) card.classList.add('hidden');
|
|
2045
|
+
}).catch(function (err) {
|
|
2046
|
+
showToast('error', '隐藏失败', err.message);
|
|
2047
|
+
});
|
|
2048
|
+
});
|
|
2049
|
+
});
|
|
2050
|
+
} catch (err) {
|
|
2051
|
+
if (card) card.classList.add('hidden');
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
|
|
1171
2055
|
// ---------- Refresh orchestrators ----------
|
|
1172
2056
|
async function refreshStatus() {
|
|
1173
2057
|
try {
|
|
@@ -1197,7 +2081,7 @@ html.dark {
|
|
|
1197
2081
|
|
|
1198
2082
|
async function refreshTasks() {
|
|
1199
2083
|
try {
|
|
1200
|
-
const data = await fetchJson('/api/tasks');
|
|
2084
|
+
const data = await fetchJson('/api/tasks?repo_path=' + encodeURIComponent(currentRepoPath));
|
|
1201
2085
|
renderTasks(data);
|
|
1202
2086
|
} catch (err) {
|
|
1203
2087
|
const bodyEl = $('recent-tasks-body');
|
|
@@ -1208,14 +2092,14 @@ html.dark {
|
|
|
1208
2092
|
}
|
|
1209
2093
|
|
|
1210
2094
|
async function refreshAll() {
|
|
1211
|
-
await Promise.all([refreshStatus(), refreshTasks(), refreshLogs(), refreshEvents(), refreshLineages(), refreshProjectPolicy(), refreshReleaseStatus(), refreshEvidencePacks()]);
|
|
2095
|
+
await Promise.all([refreshStatus(), refreshTasks(), refreshLogs(), refreshEvents(), refreshLineages(), refreshProjectPolicy(), refreshReleaseStatus(), refreshEvidencePacks(), refreshStaleTasks(), refreshHealthScore()]);
|
|
1212
2096
|
}
|
|
1213
2097
|
|
|
1214
2098
|
// ---------- Actions ----------
|
|
1215
2099
|
async function handleAction(action, btnEl) {
|
|
1216
2100
|
try {
|
|
1217
2101
|
if (action === 'refresh') {
|
|
1218
|
-
await Promise.all([refreshStatus(), refreshTasks(), refreshLineages(), refreshProjectPolicy(), refreshReleaseStatus(), refreshEvidencePacks()]);
|
|
2102
|
+
await Promise.all([refreshStatus(), refreshTasks(), refreshLineages(), refreshProjectPolicy(), refreshReleaseStatus(), refreshEvidencePacks(), refreshStaleTasks(), refreshHealthScore()]);
|
|
1219
2103
|
return;
|
|
1220
2104
|
}
|
|
1221
2105
|
|
|
@@ -1269,6 +2153,35 @@ html.dark {
|
|
|
1269
2153
|
});
|
|
1270
2154
|
});
|
|
1271
2155
|
|
|
2156
|
+
// Copy diagnostics button (system status card header)
|
|
2157
|
+
const copyDiagBtn = $('btn-copy-diagnostics');
|
|
2158
|
+
if (copyDiagBtn) {
|
|
2159
|
+
copyDiagBtn.addEventListener('click', function () {
|
|
2160
|
+
copyDiagnostics(copyDiagBtn);
|
|
2161
|
+
});
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2164
|
+
// Repo selector change handler
|
|
2165
|
+
const repoSelector = $('repo-selector');
|
|
2166
|
+
if (repoSelector) {
|
|
2167
|
+
repoSelector.addEventListener('change', function () {
|
|
2168
|
+
onRepoChange(repoSelector.value);
|
|
2169
|
+
});
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
// Safe modal close handlers
|
|
2173
|
+
const modalClose = $('safe-modal-close');
|
|
2174
|
+
if (modalClose) {
|
|
2175
|
+
modalClose.addEventListener('click', function () { closeSafeModal(); });
|
|
2176
|
+
}
|
|
2177
|
+
const safeModal = $('safe-modal');
|
|
2178
|
+
if (safeModal) {
|
|
2179
|
+
safeModal.addEventListener('click', function (e) {
|
|
2180
|
+
if (e.target === safeModal) closeSafeModal();
|
|
2181
|
+
});
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
loadRepos();
|
|
1272
2185
|
loadToken().then(function () {
|
|
1273
2186
|
auditSeeded = false;
|
|
1274
2187
|
seenAuditKeys = new Set();
|
|
@@ -1282,6 +2195,8 @@ html.dark {
|
|
|
1282
2195
|
|
|
1283
2196
|
// Auto-refresh /api/status every 30s (no logs to avoid performance issues)
|
|
1284
2197
|
statusTimer = setInterval(function () { refreshStatus(); }, 30000);
|
|
2198
|
+
// Health score refresh (lightweight, reuses some endpoints) every 60s
|
|
2199
|
+
setInterval(function () { refreshHealthScore(); }, 60000);
|
|
1285
2200
|
// Refresh activity timeline every 60s
|
|
1286
2201
|
setInterval(function () { refreshEvents(); }, 60000);
|
|
1287
2202
|
// Audit toast polling: every 90s, best-effort
|