patchwarden 0.6.1 → 0.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/PatchWarden-Control-Tray.cmd +11 -0
  2. package/PatchWarden-Control.cmd +6 -0
  3. package/PatchWarden-Desktop.cmd +5 -0
  4. package/PatchWarden.cmd +1 -1
  5. package/README.en.md +106 -18
  6. package/README.md +30 -19
  7. package/Restart-PatchWarden-Control.cmd +6 -0
  8. package/Stop-PatchWarden.cmd +11 -0
  9. package/dist/controlCenter.d.ts +14 -0
  10. package/dist/controlCenter.js +2002 -0
  11. package/dist/doctor.js +3 -3
  12. package/dist/version.d.ts +1 -1
  13. package/dist/version.js +1 -1
  14. package/docs/control-center/README.md +33 -0
  15. package/docs/control-center/control-center-daily-driver.md +211 -0
  16. package/docs/control-center/control-center-mvp.md +205 -0
  17. package/docs/control-center/control-center-phase2.md +159 -0
  18. package/docs/demo.md +3 -0
  19. package/docs/release-v0.6.4.md +45 -0
  20. package/examples/openai-tunnel/README.md +5 -5
  21. package/examples/openai-tunnel/tunnel-client.example.yaml +3 -3
  22. package/package.json +23 -15
  23. package/scripts/README.md +47 -0
  24. package/scripts/{brand-check.js → checks/brand-check.js} +2 -2
  25. package/scripts/checks/control-center-smoke.js +1098 -0
  26. package/scripts/{control-smoke.js → checks/control-smoke.js} +17 -3
  27. package/scripts/{doctor-smoke.js → checks/doctor-smoke.js} +1 -1
  28. package/scripts/{http-mcp-smoke.js → checks/http-mcp-smoke.js} +2 -2
  29. package/scripts/{lifecycle-smoke.js → checks/lifecycle-smoke.js} +12 -12
  30. package/scripts/{mcp-manifest-check.js → checks/mcp-manifest-check.js} +2 -2
  31. package/scripts/{mcp-smoke.js → checks/mcp-smoke.js} +4 -2
  32. package/scripts/{package-manifest-check.js → checks/package-manifest-check.js} +2 -1
  33. package/scripts/{tunnel-supervisor-smoke.js → checks/tunnel-supervisor-smoke.js} +2 -2
  34. package/scripts/{unit-tests.js → checks/unit-tests.js} +1 -1
  35. package/scripts/{watcher-supervisor-smoke.js → checks/watcher-supervisor-smoke.js} +11 -5
  36. package/scripts/control/control-center-tray.ps1 +281 -0
  37. package/scripts/{get-patchwarden-health.ps1 → control/get-patchwarden-health.ps1} +3 -3
  38. package/scripts/{manage-patchwarden.ps1 → control/manage-patchwarden.ps1} +30 -4
  39. package/scripts/control/restart-control-center.ps1 +173 -0
  40. package/scripts/{restart-patchwarden.ps1 → control/restart-patchwarden.ps1} +1 -1
  41. package/scripts/control/start-control-center.ps1 +263 -0
  42. package/scripts/{start-patchwarden-tunnel.ps1 → control/start-patchwarden-tunnel.ps1} +48 -6
  43. package/scripts/control/stop-patchwarden.ps1 +114 -0
  44. package/scripts/launchers/Check-PatchWarden-Health.cmd +1 -1
  45. package/scripts/launchers/Reset-PatchWarden-Tunnel-Key.cmd +1 -1
  46. package/scripts/launchers/Restart-PatchWarden.cmd +1 -1
  47. package/scripts/launchers/Start-PatchWarden-Direct-Tunnel.cmd +1 -1
  48. package/scripts/launchers/Start-PatchWarden-Tunnel.cmd +1 -1
  49. package/scripts/{patchwarden-mcp-direct.cmd → mcp/patchwarden-mcp-direct.cmd} +1 -1
  50. package/scripts/{patchwarden-mcp-stdio.cmd → mcp/patchwarden-mcp-stdio.cmd} +1 -1
  51. package/scripts/{pack-clean.js → release/pack-clean.js} +9 -1
  52. package/src/controlCenter.ts +2152 -0
  53. package/src/doctor.ts +4 -4
  54. package/src/version.ts +1 -1
  55. package/ui/colors_and_type.css +141 -0
  56. package/ui/pages/audit.html +743 -0
  57. package/ui/pages/dashboard.html +1154 -0
  58. package/ui/pages/direct-sessions.html +652 -0
  59. package/ui/pages/logs.html +502 -0
  60. package/ui/pages/task-detail.html +1229 -0
  61. package/ui/pages/tasks.html +702 -0
  62. package/ui/pages/workspace.html +947 -0
  63. package/ui/partials/project-shell.html +362 -0
  64. package/ui/vendor/lucide.js +12 -0
  65. package/ui/vendor/tailwindcss-browser.js +947 -0
@@ -0,0 +1,1229 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN" class="dark">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title data-zh="任务详情" data-en="Task Detail">任务详情</title>
7
+ <style id="theme-vars">
8
+ /* PatchWarden Brand CSS — Dark Security Theme */
9
+ /* Prefix: pw */
10
+
11
+ :root {
12
+ /* ── Primary Palette ── */
13
+ --pw-bg: #0a0e14;
14
+ --pw-bg-elevated: #111820;
15
+ --pw-bg-surface: #161d27;
16
+ --pw-bg-hover: #1c2533;
17
+ --pw-bg-active: #222d3d;
18
+ --pw-border: #1e2a3a;
19
+ --pw-border-subtle: #172030;
20
+ --pw-border-focus: #2dd4a8;
21
+
22
+ /* ── Text ── */
23
+ --pw-text-primary: #e8edf4;
24
+ --pw-text-secondary: #8b99ad;
25
+ --pw-text-tertiary: #5a6a80;
26
+ --pw-text-inverse: #0a0e14;
27
+ --pw-text-accent: #2dd4a8;
28
+
29
+ /* ── Brand Accent — Security Green ── */
30
+ --pw-accent: #2dd4a8;
31
+ --pw-accent-hover: #34eabc;
32
+ --pw-accent-muted: rgba(45, 212, 168, 0.12);
33
+ --pw-accent-subtle: rgba(45, 212, 168, 0.06);
34
+
35
+ /* ── State Colors ── */
36
+ --pw-state-success: #22c55e;
37
+ --pw-state-success-bg: rgba(34, 197, 94, 0.1);
38
+ --pw-state-warning: #f59e0b;
39
+ --pw-state-warning-bg: rgba(245, 158, 11, 0.1);
40
+ --pw-state-error: #ef4444;
41
+ --pw-state-error-bg: rgba(239, 68, 68, 0.1);
42
+ --pw-state-info: #3b82f6;
43
+ --pw-state-info-bg: rgba(59, 130, 246, 0.1);
44
+
45
+ /* ── Radius ── */
46
+ --pw-radius-sm: 4px;
47
+ --pw-radius-md: 8px;
48
+ --pw-radius-lg: 12px;
49
+ --pw-radius-full: 9999px;
50
+
51
+ /* ── Shadows — static surfaces use border only; floating layers use subtle shadow ── */
52
+ --pw-shadow-float: 0 8px 32px rgba(0, 0, 0, 0.4);
53
+ --pw-shadow-modal: 0 16px 48px rgba(0, 0, 0, 0.5);
54
+
55
+ /* ── Typography ── */
56
+ --pw-font-sans: 'Inter', 'Segoe UI', system-ui, -apple-system, sans-serif;
57
+ --pw-font-mono: 'JetBrains Mono', 'Cascadia Code', 'Consolas', monospace;
58
+
59
+ --pw-text-xs: 0.75rem;
60
+ --pw-text-sm: 0.8125rem;
61
+ --pw-text-base: 0.875rem;
62
+ --pw-text-lg: 1rem;
63
+ --pw-text-xl: 1.25rem;
64
+ --pw-text-2xl: 1.5rem;
65
+ --pw-text-3xl: 2rem;
66
+
67
+ --pw-leading-tight: 1.25;
68
+ --pw-leading-normal: 1.5;
69
+ --pw-leading-relaxed: 1.625;
70
+
71
+ --pw-weight-normal: 400;
72
+ --pw-weight-medium: 500;
73
+ --pw-weight-semibold: 600;
74
+ --pw-weight-bold: 700;
75
+
76
+ /* ── Spacing ── */
77
+ --pw-space-1: 4px;
78
+ --pw-space-2: 8px;
79
+ --pw-space-3: 12px;
80
+ --pw-space-4: 16px;
81
+ --pw-space-5: 20px;
82
+ --pw-space-6: 24px;
83
+ --pw-space-8: 32px;
84
+ --pw-space-10: 40px;
85
+ --pw-space-12: 48px;
86
+
87
+ /* ── Transitions ── */
88
+ --pw-transition-fast: 150ms ease;
89
+ --pw-transition-base: 200ms ease;
90
+ --pw-transition-slow: 300ms ease;
91
+ }
92
+
93
+ html.dark {
94
+ --pw-bg: #0a0e14;
95
+ --pw-bg-elevated: #111820;
96
+ --pw-bg-surface: #161d27;
97
+ --pw-bg-hover: #1c2533;
98
+ --pw-bg-active: #222d3d;
99
+ --pw-border: #1e2a3a;
100
+ --pw-border-subtle: #172030;
101
+ --pw-text-primary: #e8edf4;
102
+ --pw-text-secondary: #8b99ad;
103
+ --pw-text-tertiary: #5a6a80;
104
+ }
105
+
106
+ </style>
107
+ <script src="/vendor/tailwindcss-browser.js"></script>
108
+ <script src="/vendor/lucide.js"></script>
109
+ <style type="text/tailwindcss">
110
+ @theme inline {
111
+ --color-accent: var(--pw-accent);
112
+ --color-border: var(--pw-border);
113
+ }
114
+ @layer base {
115
+ body { background: var(--pw-background); color: var(--pw-text-primary); }
116
+ td, th { @apply break-words; word-break: break-all; word-break: auto-phrase; }
117
+ th { @apply whitespace-nowrap; }
118
+ }
119
+ </style>
120
+ <style>
121
+ .no-scrollbar::-webkit-scrollbar { display: none; }
122
+ .no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
123
+ [data-icon] {
124
+ display: inline-flex;
125
+ align-items: center;
126
+ justify-content: center;
127
+ -webkit-mask-size: contain;
128
+ mask-size: contain;
129
+ -webkit-mask-repeat: no-repeat;
130
+ mask-repeat: no-repeat;
131
+ -webkit-mask-position: center;
132
+ mask-position: center;
133
+ background-color: currentColor;
134
+ }
135
+ .pw-spin { animation: pw-spin 1s linear infinite; }
136
+ @keyframes pw-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
137
+ </style>
138
+ </head>
139
+ <body class="flex h-screen overflow-hidden">
140
+
141
+ <!-- ========== SIDEBAR ========== -->
142
+ <aside class="flex flex-col w-60 shrink-0 h-screen" style="background-color: var(--pw-bg); border-right: 1px solid var(--pw-border);">
143
+
144
+ <!-- Logo Area -->
145
+ <div class="flex items-center gap-3 px-5 py-5" style="border-bottom: 1px solid var(--pw-border-subtle);">
146
+ <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="var(--pw-accent)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
147
+ <path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
148
+ <path d="M9 12l2 2 4-4"/>
149
+ </svg>
150
+ <span class="truncate text-lg font-semibold tracking-tight" style="color: var(--pw-text-primary); font-family: var(--pw-font-mono);">PatchWarden</span>
151
+ </div>
152
+
153
+ <!-- Navigation -->
154
+ <nav class="flex-1 flex flex-col gap-1 px-3 pt-4 overflow-y-auto">
155
+
156
+ <a href="/pages/dashboard.html" data-nav-key="dashboard" data-dom-id="nav-dashboard" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-colors truncate" style="color: var(--pw-text-secondary); font-family: var(--pw-font-sans);">
157
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="shrink-0">
158
+ <rect x="3" y="3" width="7" height="7" rx="1"/>
159
+ <rect x="14" y="3" width="7" height="7" rx="1"/>
160
+ <rect x="3" y="14" width="7" height="7" rx="1"/>
161
+ <rect x="14" y="14" width="7" height="7" rx="1"/>
162
+ </svg>
163
+ <span class="truncate" data-zh="控制台" data-en="Dashboard">控制台</span>
164
+ </a>
165
+
166
+ <a href="/pages/tasks.html" data-nav-key="tasks" data-dom-id="nav-tasks" data-active="true" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-colors truncate" style="color: var(--pw-text-secondary); font-family: var(--pw-font-sans);">
167
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="shrink-0">
168
+ <path d="M9 11l3 3L22 4"/>
169
+ <path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/>
170
+ </svg>
171
+ <span class="truncate" data-zh="任务面板" data-en="Tasks">任务面板</span>
172
+ </a>
173
+
174
+ <a href="/pages/workspace.html" data-nav-key="workspace" data-dom-id="nav-workspace" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-colors truncate" style="color: var(--pw-text-secondary); font-family: var(--pw-font-sans);">
175
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="shrink-0">
176
+ <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"/>
177
+ </svg>
178
+ <span class="truncate" data-zh="工作区" data-en="Workspace">工作区</span>
179
+ </a>
180
+
181
+ <a href="/pages/audit.html" data-nav-key="audit" data-dom-id="nav-audit" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-colors truncate" style="color: var(--pw-text-secondary); font-family: var(--pw-font-sans);">
182
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="shrink-0">
183
+ <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
184
+ <polyline points="14 2 14 8 20 8"/>
185
+ <line x1="16" y1="13" x2="8" y2="13"/>
186
+ <line x1="16" y1="17" x2="8" y2="17"/>
187
+ <polyline points="10 9 9 9 8 9"/>
188
+ </svg>
189
+ <span class="truncate" data-zh="审计日志" data-en="Audit Log">审计日志</span>
190
+ </a>
191
+
192
+ <a href="/pages/direct-sessions.html" data-nav-key="direct-sessions" data-dom-id="nav-direct-sessions" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-colors truncate" style="color: var(--pw-text-secondary); font-family: var(--pw-font-sans);">
193
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="shrink-0">
194
+ <path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"/>
195
+ <line x1="4" y1="22" x2="4" y2="15"/>
196
+ </svg>
197
+ <span class="truncate" data-zh="Direct 会话" data-en="Direct Sessions">Direct 会话</span>
198
+ </a>
199
+
200
+ <a href="/pages/logs.html" data-nav-key="logs" data-dom-id="nav-logs" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-colors truncate" style="color: var(--pw-text-secondary); font-family: var(--pw-font-sans);">
201
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="shrink-0">
202
+ <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
203
+ <polyline points="14 2 14 8 20 8"/>
204
+ <line x1="16" y1="13" x2="8" y2="13"/>
205
+ <line x1="16" y1="17" x2="8" y2="17"/>
206
+ </svg>
207
+ <span class="truncate" data-zh="日志" data-en="Logs">日志</span>
208
+ </a>
209
+
210
+ </nav>
211
+
212
+ <!-- Bottom Section -->
213
+ <div class="px-5 py-4" style="border-top: 1px solid var(--pw-border-subtle);">
214
+ <div class="flex items-center gap-2 mb-3">
215
+ <span class="text-xs font-medium px-2 py-0.5 rounded-md whitespace-nowrap" style="color: var(--pw-text-tertiary); background-color: var(--pw-bg-surface); border: 1px solid var(--pw-border-subtle); font-family: var(--pw-font-mono);">Core v0.6.0</span>
216
+ </div>
217
+ <div class="flex items-center gap-2">
218
+ <span class="shrink-0 w-2 h-2 rounded-full" style="background-color: var(--pw-state-success);"></span>
219
+ <span class="text-xs truncate" style="color: var(--pw-text-tertiary);">Connected</span>
220
+ </div>
221
+ </div>
222
+
223
+ </aside>
224
+
225
+ <!-- ========== MAIN AREA ========== -->
226
+ <div class="flex flex-col flex-1 min-w-0 h-screen overflow-hidden">
227
+
228
+ <!-- Top Bar -->
229
+ <header class="flex items-center justify-between shrink-0 px-6 py-3" style="border-bottom: 1px solid var(--pw-border); background-color: var(--pw-bg);">
230
+ <h1 class="text-lg font-semibold truncate min-w-0" data-zh="任务详情" data-en="Task Detail" style="color: var(--pw-text-primary); font-family: var(--pw-font-sans); text-wrap: balance; word-break: keep-all; overflow-wrap: break-word;">
231
+ 任务详情
232
+ </h1>
233
+ <div class="flex items-center gap-2 shrink-0">
234
+ <button id="btnRefresh" title="刷新" class="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium whitespace-nowrap transition-colors" style="background-color: var(--pw-bg-surface); color: var(--pw-text-secondary); border: 1px solid var(--pw-border-subtle); font-family: var(--pw-font-sans);">
235
+ <svg id="btnRefreshIcon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
236
+ <polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/>
237
+ <path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>
238
+ </svg>
239
+ <span data-zh="刷新" data-en="Refresh">刷新</span>
240
+ </button>
241
+ <button id="btnCopyId" title="复制 task_id" class="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium whitespace-nowrap transition-colors" style="background-color: var(--pw-bg-surface); color: var(--pw-text-secondary); border: 1px solid var(--pw-border-subtle); font-family: var(--pw-font-sans);">
242
+ <svg width="14" height="14" 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>
243
+ <span data-zh="复制 ID" data-en="Copy ID">复制 ID</span>
244
+ </button>
245
+ <button id="btnOpenFolder" title="打开任务目录" class="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium whitespace-nowrap transition-colors" style="background-color: var(--pw-bg-surface); color: var(--pw-text-secondary); border: 1px solid var(--pw-border-subtle); font-family: var(--pw-font-sans);">
246
+ <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="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>
247
+ <span data-zh="打开目录" data-en="Open Folder">打开目录</span>
248
+ </button>
249
+ <button id="btnAudit" title="运行 audit_task" class="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium whitespace-nowrap transition-colors" style="background-color: var(--pw-accent-muted); color: var(--pw-accent); border: 1px solid rgba(45, 212, 168, 0.3); font-family: var(--pw-font-sans);">
250
+ <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="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>
251
+ <span data-zh="运行审计" data-en="Run Audit">运行审计</span>
252
+ </button>
253
+ <div class="shrink-0 w-8 h-8 rounded-full flex items-center justify-center text-xs font-semibold ml-2" style="background-color: var(--pw-accent-muted); color: var(--pw-accent); font-family: var(--pw-font-mono);">PW</div>
254
+ </div>
255
+ </header>
256
+
257
+ <!-- Page Content Area -->
258
+ <main class="flex-1 overflow-y-auto p-6" style="background-color: var(--pw-bg-elevated);">
259
+
260
+ <!-- Breadcrumb + Back -->
261
+ <div class="flex items-center gap-2 mb-6">
262
+ <a href="/pages/tasks.html" data-dom-id="back-tasks" class="inline-flex items-center gap-1.5 text-sm font-medium transition-colors whitespace-nowrap" style="color: var(--pw-accent); font-family: var(--pw-font-sans);">
263
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></svg>
264
+ <span data-zh="返回任务列表" data-en="Back to Tasks">返回任务列表</span>
265
+ </a>
266
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="var(--pw-text-tertiary)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="shrink-0"><path d="m9 18 6-6-6-6"/></svg>
267
+ <span id="breadcrumbId" class="text-sm truncate" style="color: var(--pw-text-tertiary); font-family: var(--pw-font-mono);">—</span>
268
+ </div>
269
+
270
+ <!-- ===== Loading State ===== -->
271
+ <div id="loadingState" class="rounded-lg py-16 text-center" style="background-color: var(--pw-bg-surface); border: 1px solid var(--pw-border);">
272
+ <svg class="pw-spin inline-block mb-3" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="var(--pw-text-tertiary)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
273
+ <line x1="12" y1="2" x2="12" y2="6"/>
274
+ <line x1="12" y1="18" x2="12" y2="22"/>
275
+ <line x1="4.93" y1="4.93" x2="7.76" y2="7.76"/>
276
+ <line x1="16.24" y1="16.24" x2="19.07" y2="19.07"/>
277
+ <line x1="2" y1="12" x2="6" y2="12"/>
278
+ <line x1="18" y1="12" x2="22" y2="12"/>
279
+ <line x1="4.93" y1="19.07" x2="7.76" y2="16.24"/>
280
+ <line x1="16.24" y1="7.76" x2="19.07" y2="4.93"/>
281
+ </svg>
282
+ <p class="text-sm" style="color: var(--pw-text-tertiary); font-family: var(--pw-font-sans);" data-zh="加载中..." data-en="Loading...">加载中...</p>
283
+ </div>
284
+
285
+ <!-- ===== Error State ===== -->
286
+ <div id="errorState" class="hidden rounded-lg py-16 text-center" style="background-color: var(--pw-bg-surface); border: 1px solid var(--pw-border);">
287
+ <svg class="inline-block mb-3" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="var(--pw-state-error)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
288
+ <circle cx="12" cy="12" r="10"/>
289
+ <line x1="12" y1="8" x2="12" y2="12"/>
290
+ <line x1="12" y1="16" x2="12.01" y2="16"/>
291
+ </svg>
292
+ <p id="errorTitle" class="text-sm font-medium" style="color: var(--pw-state-error); font-family: var(--pw-font-sans);" data-zh="加载失败" data-en="Load Failed">加载失败</p>
293
+ <p id="errorMessage" class="text-xs mt-2 px-6 break-words" style="color: var(--pw-text-tertiary); font-family: var(--pw-font-mono);"></p>
294
+ <a href="/pages/tasks.html" class="inline-block mt-4 px-4 py-1.5 rounded-md text-xs font-medium transition-colors" 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);" data-zh="返回任务列表" data-en="Back to Tasks">返回任务列表</a>
295
+ </div>
296
+
297
+ <!-- ===== Detail Content (hidden until loaded) ===== -->
298
+ <div id="detailContent" class="hidden flex flex-col gap-6">
299
+
300
+ <!-- ===== Stale / Reconcile Banner ===== -->
301
+ <div id="staleBanner" class="hidden rounded-lg px-4 py-3 flex items-start gap-3" style="background-color: var(--pw-state-warning-bg); border: 1px solid var(--pw-state-warning);">
302
+ <svg class="shrink-0 mt-0.5" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="var(--pw-state-warning)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
303
+ <circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/>
304
+ </svg>
305
+ <div class="min-w-0 flex-1">
306
+ <p class="text-sm font-medium" style="color: var(--pw-state-warning);" data-zh="任务疑似僵死" data-en="Task appears stale">任务疑似僵死</p>
307
+ <p id="staleReasonsText" class="text-xs mt-1 break-words" style="color: var(--pw-text-secondary); font-family: var(--pw-font-mono);"></p>
308
+ </div>
309
+ <button id="btnReconcile" class="shrink-0 px-3 py-1 rounded-md text-xs font-medium transition-colors" style="background-color: var(--pw-bg-hover); color: var(--pw-text-primary); border: 1px solid var(--pw-border-subtle); font-family: var(--pw-font-sans);" data-zh="Reconcile" data-en="Reconcile">Reconcile</button>
310
+ </div>
311
+
312
+ <!-- Task Header Card -->
313
+ <section class="rounded-lg p-5" style="background-color: var(--pw-bg-surface); border: 1px solid var(--pw-border);">
314
+ <div class="flex flex-col gap-4">
315
+ <!-- Top row: ID + Status -->
316
+ <div class="flex flex-wrap items-center gap-3">
317
+ <span id="hdrTaskId" class="text-xl font-semibold truncate" style="color: var(--pw-text-primary); font-family: var(--pw-font-mono);">—</span>
318
+ <span id="hdrStatusBadge" class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md text-xs font-medium whitespace-nowrap" style="background-color: var(--pw-state-warning-bg); color: var(--pw-state-warning); border: 1px solid rgba(245, 158, 11, 0.2);">
319
+ <span class="shrink-0 w-1.5 h-1.5 rounded-full" style="background-color: var(--pw-state-warning);"></span>
320
+ <span id="hdrStatusText">—</span>
321
+ </span>
322
+ </div>
323
+ <!-- Metadata grid -->
324
+ <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-x-6 gap-y-3 text-sm">
325
+ <div class="flex flex-col gap-0.5 min-w-0">
326
+ <span class="text-xs whitespace-nowrap" style="color: var(--pw-text-tertiary);" data-zh="阶段" data-en="Phase">阶段</span>
327
+ <span id="hdrPhase" class="truncate" style="color: var(--pw-text-primary); font-family: var(--pw-font-mono);">—</span>
328
+ </div>
329
+ <div class="flex flex-col gap-0.5 min-w-0">
330
+ <span class="text-xs whitespace-nowrap" style="color: var(--pw-text-tertiary);" data-zh="Agent" data-en="Agent">Agent</span>
331
+ <span id="hdrAgent" class="truncate" style="color: var(--pw-text-primary); font-family: var(--pw-font-mono);">—</span>
332
+ </div>
333
+ <div class="flex flex-col gap-0.5 min-w-0">
334
+ <span class="text-xs whitespace-nowrap" style="color: var(--pw-text-tertiary);" data-zh="创建时间" data-en="Created">创建时间</span>
335
+ <span id="hdrCreatedAt" class="truncate" style="color: var(--pw-text-secondary); font-family: var(--pw-font-mono);">—</span>
336
+ </div>
337
+ <div class="flex flex-col gap-0.5 min-w-0">
338
+ <span class="text-xs whitespace-nowrap" style="color: var(--pw-text-tertiary);" data-zh="更新时间" data-en="Updated">更新时间</span>
339
+ <span id="hdrUpdatedAt" class="truncate" style="color: var(--pw-text-secondary); font-family: var(--pw-font-mono);">—</span>
340
+ </div>
341
+ <div class="flex flex-col gap-0.5 min-w-0">
342
+ <span class="text-xs whitespace-nowrap" style="color: var(--pw-text-tertiary);" data-zh="测试命令" data-en="Test Command">测试命令</span>
343
+ <span id="hdrTestCommand" class="truncate" style="color: var(--pw-text-secondary); font-family: var(--pw-font-mono);">—</span>
344
+ </div>
345
+ <div class="flex flex-col gap-0.5 min-w-0">
346
+ <span class="text-xs whitespace-nowrap" style="color: var(--pw-text-tertiary);" data-zh="超时(秒)" data-en="Timeout (s)">超时(秒)</span>
347
+ <span id="hdrTimeout" class="truncate" style="color: var(--pw-text-secondary); font-family: var(--pw-font-mono);">—</span>
348
+ </div>
349
+ </div>
350
+ <!-- Code metadata block -->
351
+ <div class="rounded-md p-3 text-xs leading-relaxed" style="background-color: var(--pw-bg); border: 1px solid var(--pw-border-subtle); font-family: var(--pw-font-mono); color: var(--pw-text-secondary);">
352
+ <div class="flex items-start gap-2 mb-1">
353
+ <span class="shrink-0" style="color: var(--pw-text-tertiary);">repo_path:</span>
354
+ <span id="hdrRepoPath" class="break-all" style="color: var(--pw-text-primary);">—</span>
355
+ </div>
356
+ <div class="flex items-start gap-2 mb-1">
357
+ <span class="shrink-0" style="color: var(--pw-text-tertiary);">verify_commands:</span>
358
+ <span id="hdrVerifyCommands" class="break-all" style="color: var(--pw-text-primary);">—</span>
359
+ </div>
360
+ <div class="flex items-start gap-2">
361
+ <span class="shrink-0" style="color: var(--pw-text-tertiary);">plan_id:</span>
362
+ <span id="hdrPlanId" class="break-all" style="color: var(--pw-text-primary);">—</span>
363
+ </div>
364
+ </div>
365
+ <!-- Error block (only shown when status.error present) -->
366
+ <div id="hdrErrorBlock" class="hidden rounded-md p-3 text-xs leading-relaxed" style="background-color: var(--pw-state-error-bg); color: var(--pw-state-error); font-family: var(--pw-font-mono);">
367
+ <div class="flex items-start gap-2">
368
+ <span class="shrink-0 font-semibold">error:</span>
369
+ <span id="hdrErrorText" class="break-all whitespace-pre-wrap"></span>
370
+ </div>
371
+ </div>
372
+ </div>
373
+ </section>
374
+
375
+ <!-- Two-Column Layout -->
376
+ <div class="grid grid-cols-1 lg:grid-cols-12 gap-6">
377
+
378
+ <!-- Left Column (~58%) -->
379
+ <div class="lg:col-span-7 flex flex-col gap-6">
380
+
381
+ <!-- Execution Status Card -->
382
+ <section class="rounded-lg p-5" style="background-color: var(--pw-bg-surface); border: 1px solid var(--pw-border);">
383
+ <h2 class="text-base font-semibold mb-4" data-zh="执行状态" data-en="Execution Status" style="color: var(--pw-text-primary); font-family: var(--pw-font-sans);">执行状态</h2>
384
+ <div class="grid grid-cols-1 sm:grid-cols-3 gap-x-6 gap-y-3 text-sm">
385
+ <div class="flex flex-col gap-0.5 min-w-0">
386
+ <span class="text-xs whitespace-nowrap" style="color: var(--pw-text-tertiary);" data-zh="运行阶段" data-en="Runtime Phase">运行阶段</span>
387
+ <span id="rtPhase" class="truncate" style="color: var(--pw-text-primary); font-family: var(--pw-font-mono);">—</span>
388
+ </div>
389
+ <div class="flex flex-col gap-0.5 min-w-0">
390
+ <span class="text-xs whitespace-nowrap" style="color: var(--pw-text-tertiary);" data-zh="最近心跳" data-en="Last Heartbeat">最近心跳</span>
391
+ <span id="rtHeartbeat" class="truncate" style="color: var(--pw-text-secondary); font-family: var(--pw-font-mono);">—</span>
392
+ </div>
393
+ <div class="flex flex-col gap-0.5 min-w-0">
394
+ <span class="text-xs whitespace-nowrap" style="color: var(--pw-text-tertiary);" data-zh="当前命令" data-en="Current Command">当前命令</span>
395
+ <span id="rtCurrentCmd" class="truncate" style="color: var(--pw-text-secondary); font-family: var(--pw-font-mono);">—</span>
396
+ </div>
397
+ </div>
398
+ </section>
399
+
400
+ <!-- Diff Preview Card -->
401
+ <section class="rounded-lg overflow-hidden" style="background-color: var(--pw-bg-surface); border: 1px solid var(--pw-border);">
402
+ <div class="flex items-center gap-2 px-4 py-3" style="border-bottom: 1px solid var(--pw-border);">
403
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="var(--pw-text-secondary)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4Z"/></svg>
404
+ <h2 class="text-base font-semibold" data-zh="变更预览" data-en="Diff Preview" style="color: var(--pw-text-primary); font-family: var(--pw-font-sans);">变更预览</h2>
405
+ </div>
406
+ <div id="diffEmpty" class="px-4 py-8 text-center text-sm" style="color: var(--pw-text-tertiary); font-family: var(--pw-font-sans);" data-zh="暂无 diff 内容" data-en="No diff available">暂无 diff 内容</div>
407
+ <pre id="diffContent" class="hidden px-4 py-3 text-xs leading-relaxed overflow-x-auto whitespace-pre-wrap break-all" style="background-color: #0d1117; color: var(--pw-text-secondary); font-family: var(--pw-font-mono); max-height: 480px; overflow-y: auto;"></pre>
408
+ </section>
409
+
410
+ <!-- Changed Files Card -->
411
+ <section class="rounded-lg overflow-hidden" style="background-color: var(--pw-bg-surface); border: 1px solid var(--pw-border);">
412
+ <div class="flex items-center justify-between px-4 py-3" style="border-bottom: 1px solid var(--pw-border);">
413
+ <div class="flex items-center gap-2">
414
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="var(--pw-text-secondary)" 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>
415
+ <h2 class="text-base font-semibold" data-zh="变更文件" data-en="Changed Files" style="color: var(--pw-text-primary); font-family: var(--pw-font-sans);">变更文件</h2>
416
+ </div>
417
+ <span id="cfCountBadge" class="hidden text-xs px-2 py-0.5 rounded-md whitespace-nowrap" style="color: var(--pw-text-tertiary); background-color: var(--pw-bg-hover); font-family: var(--pw-font-mono);">—</span>
418
+ </div>
419
+ <div id="cfEmpty" class="px-4 py-8 text-center text-sm" style="color: var(--pw-text-tertiary); font-family: var(--pw-font-sans);" data-zh="暂无变更文件记录" data-en="No changed-files record">暂无变更文件记录</div>
420
+ <div id="cfBody" class="hidden px-4 py-3 flex flex-col gap-3">
421
+ <!-- file_stats summary row -->
422
+ <div id="cfStatsRow" class="hidden grid grid-cols-2 sm:grid-cols-4 gap-2 text-xs"></div>
423
+ <!-- changed files list (truncated) -->
424
+ <div id="cfList" class="rounded-md p-2 text-xs leading-relaxed overflow-auto" style="background-color: #0d1117; color: var(--pw-text-secondary); font-family: var(--pw-font-mono); max-height: 320px;"></div>
425
+ </div>
426
+ </section>
427
+
428
+ <!-- Verify Log Card -->
429
+ <section class="rounded-lg overflow-hidden" style="background-color: var(--pw-bg-surface); border: 1px solid var(--pw-border);">
430
+ <div class="flex items-center gap-2 px-4 py-3" style="border-bottom: 1px solid var(--pw-border);">
431
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="var(--pw-text-secondary)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>
432
+ <h2 class="text-base font-semibold" data-zh="验证日志" data-en="Verify Log" style="color: var(--pw-text-primary); font-family: var(--pw-font-sans);">验证日志</h2>
433
+ </div>
434
+ <div id="verifyLogEmpty" class="px-4 py-8 text-center text-sm" style="color: var(--pw-text-tertiary); font-family: var(--pw-font-sans);" data-zh="暂无验证日志" data-en="No verify log">暂无验证日志</div>
435
+ <pre id="verifyLogContent" class="hidden px-4 py-3 text-xs leading-relaxed overflow-x-auto whitespace-pre-wrap break-all" style="background-color: #0d1117; color: var(--pw-text-secondary); font-family: var(--pw-font-mono); max-height: 320px; overflow-y: auto;"></pre>
436
+ </section>
437
+
438
+ </div>
439
+
440
+ <!-- Right Column (~42%) -->
441
+ <div class="lg:col-span-5 flex flex-col gap-6">
442
+
443
+ <!-- Result Summary Card -->
444
+ <section class="rounded-lg p-5" style="background-color: var(--pw-bg-surface); border: 1px solid var(--pw-border);">
445
+ <h2 class="text-base font-semibold mb-4" data-zh="结果摘要" data-en="Result Summary" style="color: var(--pw-text-primary); font-family: var(--pw-font-sans);">结果摘要</h2>
446
+ <div class="flex flex-col gap-3">
447
+ <div class="flex items-center justify-between text-sm">
448
+ <span style="color: var(--pw-text-secondary);" data-zh="任务状态" data-en="Status">任务状态</span>
449
+ <span id="rsStatus" class="font-semibold" style="color: var(--pw-text-primary); font-family: var(--pw-font-mono);">—</span>
450
+ </div>
451
+ <div class="flex items-center justify-between text-sm">
452
+ <span style="color: var(--pw-text-secondary);" data-zh="验证状态" data-en="Verify Status">验证状态</span>
453
+ <span id="rsVerifyStatus" class="font-semibold" style="color: var(--pw-text-primary); font-family: var(--pw-font-mono);">—</span>
454
+ </div>
455
+ <div id="rsSummaryBlock" class="hidden rounded-md p-3 text-xs leading-relaxed" style="background-color: var(--pw-bg); border: 1px solid var(--pw-border-subtle); color: var(--pw-text-secondary); font-family: var(--pw-font-sans);">
456
+ <span id="rsSummary" class="break-all whitespace-pre-wrap"></span>
457
+ </div>
458
+ </div>
459
+ </section>
460
+
461
+ <!-- Audit Results Card -->
462
+ <section class="rounded-lg p-5" style="background-color: var(--pw-bg-surface); border: 1px solid var(--pw-border);">
463
+ <div class="flex items-center justify-between mb-4">
464
+ <h2 class="text-base font-semibold" data-zh="审计结果" data-en="Audit Result" style="color: var(--pw-text-primary); font-family: var(--pw-font-sans);">审计结果</h2>
465
+ <span id="auditVerdictBadge" class="inline-flex items-center px-2.5 py-1 rounded-md text-xs font-medium whitespace-nowrap" style="background-color: var(--pw-bg-hover); color: var(--pw-text-tertiary); border: 1px solid var(--pw-border-subtle);">—</span>
466
+ </div>
467
+ <div class="flex flex-col gap-3">
468
+ <div class="flex items-center justify-between text-sm">
469
+ <span style="color: var(--pw-text-secondary);" data-zh="范围变更" data-en="Scope Changes">范围变更</span>
470
+ <span id="auditScope" class="font-semibold" style="color: var(--pw-text-primary); font-family: var(--pw-font-mono);">—</span>
471
+ </div>
472
+ <div class="flex items-center justify-between text-sm">
473
+ <span style="color: var(--pw-text-secondary);" data-zh="检查时间" data-en="Checked At">检查时间</span>
474
+ <span id="auditCheckedAt" class="truncate" style="color: var(--pw-text-secondary); font-family: var(--pw-font-mono);">—</span>
475
+ </div>
476
+ <div id="auditEmptyHint" class="text-xs" style="color: var(--pw-text-tertiary); font-family: var(--pw-font-sans);" data-zh="暂无审计记录" data-en="No audit record yet">暂无审计记录</div>
477
+ </div>
478
+ </section>
479
+
480
+ <!-- Independent Review Card (audit_task artifact) -->
481
+ <section class="rounded-lg overflow-hidden" style="background-color: var(--pw-bg-surface); border: 1px solid var(--pw-border);">
482
+ <div class="flex items-center justify-between px-4 py-3" style="border-bottom: 1px solid var(--pw-border);">
483
+ <div class="flex items-center gap-2">
484
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="var(--pw-text-secondary)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
485
+ <h2 class="text-base font-semibold" data-zh="独立审计报告" data-en="Independent Review" style="color: var(--pw-text-primary); font-family: var(--pw-font-sans);">独立审计报告</h2>
486
+ </div>
487
+ <span id="reviewVerdictBadge" class="text-xs px-2 py-0.5 rounded-md whitespace-nowrap" style="color: var(--pw-text-tertiary); background-color: var(--pw-bg-hover); font-family: var(--pw-font-mono);">—</span>
488
+ </div>
489
+ <div id="reviewEmpty" class="px-4 py-8 text-center text-sm" style="color: var(--pw-text-tertiary); font-family: var(--pw-font-sans);" data-zh="尚未运行 audit_task" data-en="audit_task not run yet">尚未运行 audit_task</div>
490
+ <pre id="reviewContent" class="hidden px-4 py-3 text-xs leading-relaxed overflow-x-auto whitespace-pre-wrap break-all" style="background-color: #0d1117; color: var(--pw-text-secondary); font-family: var(--pw-font-mono); max-height: 320px; overflow-y: auto;"></pre>
491
+ </section>
492
+
493
+ <!-- Verification Summary Card -->
494
+ <section class="rounded-lg p-5" style="background-color: var(--pw-bg-surface); border: 1px solid var(--pw-border);">
495
+ <h2 class="text-base font-semibold mb-4" data-zh="验证摘要" data-en="Verification Summary" style="color: var(--pw-text-primary); font-family: var(--pw-font-sans);">验证摘要</h2>
496
+ <div class="flex flex-col gap-3">
497
+ <div class="flex items-center justify-between text-sm">
498
+ <span style="color: var(--pw-text-secondary);" data-zh="验证状态" data-en="Status">验证状态</span>
499
+ <span id="vsStatus" class="font-semibold" style="color: var(--pw-text-primary); font-family: var(--pw-font-mono);">—</span>
500
+ </div>
501
+ <div class="flex items-center justify-between text-sm">
502
+ <span style="color: var(--pw-text-secondary);" data-zh="检查时间" data-en="Checked At">检查时间</span>
503
+ <span id="vsCheckedAt" class="truncate" style="color: var(--pw-text-secondary); font-family: var(--pw-font-mono);">—</span>
504
+ </div>
505
+ <div id="vsCommandsBlock" class="hidden rounded-md p-2 text-xs leading-relaxed" style="background-color: var(--pw-bg); border: 1px solid var(--pw-border-subtle); color: var(--pw-text-secondary); font-family: var(--pw-font-mono);">
506
+ <div class="text-xs mb-1" style="color: var(--pw-text-tertiary);" data-zh="验证命令" data-en="Commands">验证命令</div>
507
+ <pre id="vsCommands" class="whitespace-pre-wrap break-all"></pre>
508
+ </div>
509
+ <div id="vsEmptyHint" class="text-xs" style="color: var(--pw-text-tertiary); font-family: var(--pw-font-sans);" data-zh="暂无验证摘要" data-en="No verification summary">暂无验证摘要</div>
510
+ </div>
511
+ </section>
512
+
513
+ <!-- Warnings / Errors Card -->
514
+ <section class="rounded-lg overflow-hidden" style="background-color: var(--pw-bg-surface); border: 1px solid var(--pw-border);">
515
+ <div class="flex items-center gap-2 px-4 py-3" style="border-bottom: 1px solid var(--pw-border);">
516
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="var(--pw-text-secondary)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
517
+ <h2 class="text-base font-semibold" data-zh="警告 / 错误" data-en="Warnings / Errors" style="color: var(--pw-text-primary); font-family: var(--pw-font-sans);">警告 / 错误</h2>
518
+ </div>
519
+ <div id="weEmpty" class="px-4 py-6 text-center text-sm" style="color: var(--pw-text-tertiary); font-family: var(--pw-font-sans);" data-zh="无警告或错误" data-en="No warnings or errors">无警告或错误</div>
520
+ <div id="weBody" class="hidden px-4 py-3 flex flex-col gap-3">
521
+ <div id="weWarningsBlock" class="hidden">
522
+ <div class="text-xs mb-1.5 font-medium" style="color: var(--pw-state-warning);" data-zh="警告" data-en="Warnings">警告</div>
523
+ <div id="weWarnings" class="rounded-md p-2 text-xs leading-relaxed" style="background-color: var(--pw-state-warning-bg); color: var(--pw-state-warning); font-family: var(--pw-font-mono);"></div>
524
+ </div>
525
+ <div id="weErrorsBlock" class="hidden">
526
+ <div class="text-xs mb-1.5 font-medium" style="color: var(--pw-state-error);" data-zh="错误" data-en="Errors">错误</div>
527
+ <div id="weErrors" class="rounded-md p-2 text-xs leading-relaxed" style="background-color: var(--pw-state-error-bg); color: var(--pw-state-error); font-family: var(--pw-font-mono);"></div>
528
+ </div>
529
+ </div>
530
+ </section>
531
+
532
+ <!-- Test Log Card -->
533
+ <section class="rounded-lg overflow-hidden" style="background-color: var(--pw-bg-surface); border: 1px solid var(--pw-border);">
534
+ <div class="flex items-center gap-2 px-4 py-3" style="border-bottom: 1px solid var(--pw-border);">
535
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="var(--pw-text-secondary)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg>
536
+ <h2 class="text-base font-semibold" data-zh="测试日志" data-en="Test Log" style="color: var(--pw-text-primary); font-family: var(--pw-font-sans);">测试日志</h2>
537
+ </div>
538
+ <div id="logEmpty" class="px-4 py-8 text-center text-sm" style="color: var(--pw-text-tertiary); font-family: var(--pw-font-sans);" data-zh="暂无测试日志" data-en="No test log available">暂无测试日志</div>
539
+ <pre id="logContent" class="hidden px-4 py-3 text-xs leading-relaxed overflow-x-auto whitespace-pre-wrap break-all" style="background-color: #0d1117; color: var(--pw-text-secondary); font-family: var(--pw-font-mono); max-height: 480px; overflow-y: auto;"></pre>
540
+ </section>
541
+
542
+ </div>
543
+
544
+ </div>
545
+
546
+ </div>
547
+
548
+ </main>
549
+ </div>
550
+
551
+ <!-- Active nav item styling + sidebar hover states -->
552
+ <style>
553
+ [data-nav-key] {
554
+ transition: background-color var(--pw-transition-fast), color var(--pw-transition-fast);
555
+ }
556
+ [data-nav-key]:hover {
557
+ background-color: var(--pw-bg-hover);
558
+ color: var(--pw-text-primary);
559
+ }
560
+ [data-nav-key][data-active="true"] {
561
+ background-color: var(--pw-accent-subtle);
562
+ color: var(--pw-accent);
563
+ border-left: 3px solid var(--pw-accent);
564
+ padding-left: 9px;
565
+ }
566
+ </style>
567
+
568
+ <script>
569
+ (function () {
570
+ 'use strict';
571
+
572
+ // ---- DOM helpers ----
573
+ function $(id) { return document.getElementById(id); }
574
+ function show(el) { el && el.classList.remove('hidden'); }
575
+ function hide(el) { el && el.classList.add('hidden'); }
576
+ function text(el, value) { if (el) el.textContent = value; }
577
+
578
+ function escapeText(s) {
579
+ if (s === null || s === undefined) return '';
580
+ return String(s);
581
+ }
582
+
583
+ function pad2(n) { return String(n).padStart(2, '0'); }
584
+
585
+ function formatTime(iso) {
586
+ if (!iso) return '—';
587
+ var d = new Date(iso);
588
+ if (isNaN(d.getTime())) return String(iso);
589
+ return d.getFullYear() + '-' + pad2(d.getMonth() + 1) + '-' + pad2(d.getDate())
590
+ + ' ' + pad2(d.getHours()) + ':' + pad2(d.getMinutes()) + ':' + pad2(d.getSeconds());
591
+ }
592
+
593
+ // status -> { color, text, bg, border }
594
+ // color: success (green) | info (blue) | warning (yellow) | error (red) | muted (gray)
595
+ var STATUS_META = {
596
+ pending: { color: 'warning', text: '等待中' },
597
+ running: { color: 'info', text: '运行中' },
598
+ done: { color: 'success', text: '已完成' },
599
+ failed: { color: 'error', text: '失败' },
600
+ failed_scope_violation: { color: 'error', text: '范围违规' },
601
+ canceled: { color: 'muted', text: '已取消' },
602
+ timeout: { color: 'muted', text: '超时' }
603
+ };
604
+
605
+ var PHASE_LABELS = {
606
+ queued: '排队中',
607
+ preparing: '准备中',
608
+ running: '运行中',
609
+ verifying: '验证中',
610
+ collecting_artifacts: '收集产物',
611
+ completed: '已完成'
612
+ };
613
+
614
+ function getStatusMeta(status) {
615
+ return STATUS_META[status] || { color: 'muted', text: status || '—' };
616
+ }
617
+
618
+ function getPhaseLabel(phase) {
619
+ return PHASE_LABELS[phase] || phase || '—';
620
+ }
621
+
622
+ var COLOR_VARS = {
623
+ success: { fg: 'var(--pw-state-success)', bg: 'var(--pw-state-success-bg)', border: 'rgba(34, 197, 94, 0.2)' },
624
+ info: { fg: 'var(--pw-state-info)', bg: 'var(--pw-state-info-bg)', border: 'rgba(59, 130, 246, 0.2)' },
625
+ warning: { fg: 'var(--pw-state-warning)', bg: 'var(--pw-state-warning-bg)', border: 'rgba(245, 158, 11, 0.2)' },
626
+ error: { fg: 'var(--pw-state-error)', bg: 'var(--pw-state-error-bg)', border: 'rgba(239, 68, 68, 0.2)' },
627
+ muted: { fg: 'var(--pw-text-tertiary)', bg: 'var(--pw-bg-hover)', border: 'var(--pw-border-subtle)' }
628
+ };
629
+
630
+ function applyBadgeColors(el, color) {
631
+ if (!el) return;
632
+ var c = COLOR_VARS[color] || COLOR_VARS.muted;
633
+ el.style.backgroundColor = c.bg;
634
+ el.style.color = c.fg;
635
+ el.style.borderColor = c.border;
636
+ }
637
+
638
+ function applyVerdictBadge(el, verdict) {
639
+ if (!el) return;
640
+ var v = String(verdict || '').toLowerCase();
641
+ var color = 'muted';
642
+ if (v === 'pass' || v === 'passed') color = 'success';
643
+ else if (v === 'warn' || v === 'warning') color = 'warning';
644
+ else if (v === 'fail' || v === 'failed' || v === 'error') color = 'error';
645
+ applyBadgeColors(el, color);
646
+ text(el, verdict || '—');
647
+ }
648
+
649
+ function renderListOrJoin(value) {
650
+ if (value === null || value === undefined) return '—';
651
+ if (Array.isArray(value)) {
652
+ if (value.length === 0) return '—';
653
+ return value.map(function (v) { return JSON.stringify(v); }).join(', ');
654
+ }
655
+ return String(value);
656
+ }
657
+
658
+ function escapeHtml(s) {
659
+ if (s === null || s === undefined) return '';
660
+ return String(s).replace(/[&<>"']/g, function (c) {
661
+ return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c];
662
+ });
663
+ }
664
+
665
+ // ---- Control token (loaded lazily for POST actions) ----
666
+ var controlToken = null;
667
+ function loadToken() {
668
+ return fetch('/control-token.json', { cache: 'no-store' })
669
+ .then(function (res) { return res.json(); })
670
+ .then(function (data) { controlToken = (data && data.token) || null; })
671
+ .catch(function () { controlToken = null; });
672
+ }
673
+ function ensureToken() {
674
+ if (controlToken) return Promise.resolve();
675
+ return loadToken();
676
+ }
677
+ function postAction(url) {
678
+ return ensureToken().then(function () {
679
+ return fetch(url, {
680
+ method: 'POST',
681
+ headers: { 'X-PatchWarden-Control-Token': controlToken || '' },
682
+ cache: 'no-store'
683
+ });
684
+ });
685
+ }
686
+
687
+ // ---- State rendering ----
688
+ function showLoading() {
689
+ show($('loadingState'));
690
+ hide($('errorState'));
691
+ hide($('detailContent'));
692
+ }
693
+
694
+ function showError(title, msg) {
695
+ hide($('loadingState'));
696
+ show($('errorState'));
697
+ hide($('detailContent'));
698
+ text($('errorTitle'), title || '加载失败');
699
+ text($('errorMessage'), msg || '');
700
+ }
701
+
702
+ function showDetail() {
703
+ hide($('loadingState'));
704
+ hide($('errorState'));
705
+ show($('detailContent'));
706
+ }
707
+
708
+ // ---- Render task detail ----
709
+ function renderDetail(data) {
710
+ var taskId = data.task_id || '—';
711
+ text($('breadcrumbId'), taskId);
712
+ text($('hdrTaskId'), taskId);
713
+
714
+ // Status
715
+ var status = (data.status && data.status.status) || data.status_status || null;
716
+ var statusMeta = getStatusMeta(status);
717
+ text($('hdrStatusText'), statusMeta.text);
718
+ applyBadgeColors($('hdrStatusBadge'), statusMeta.color);
719
+
720
+ // Phase
721
+ var phase = (data.status && data.status.phase) || null;
722
+ text($('hdrPhase'), getPhaseLabel(phase));
723
+
724
+ // Agent
725
+ var agent = (data.status && data.status.agent) || null;
726
+ text($('hdrAgent'), agent || '—');
727
+
728
+ // Times
729
+ var createdAt = (data.status && data.status.created_at) || null;
730
+ var updatedAt = (data.status && data.status.updated_at) || null;
731
+ text($('hdrCreatedAt'), formatTime(createdAt));
732
+ text($('hdrUpdatedAt'), formatTime(updatedAt));
733
+
734
+ // Test command + timeout
735
+ var testCommand = (data.status && data.status.test_command) || null;
736
+ text($('hdrTestCommand'), testCommand || '—');
737
+ var timeoutSeconds = (data.status && data.status.timeout_seconds);
738
+ text($('hdrTimeout'), (timeoutSeconds === null || timeoutSeconds === undefined) ? '—' : String(timeoutSeconds));
739
+
740
+ // Code metadata
741
+ var repoPath = (data.status && data.status.repo_path) || null;
742
+ text($('hdrRepoPath'), repoPath || '—');
743
+
744
+ var verifyCommands = (data.status && data.status.verify_commands) || null;
745
+ text($('hdrVerifyCommands'), renderListOrJoin(verifyCommands));
746
+
747
+ var planId = (data.status && data.status.plan_id) || null;
748
+ text($('hdrPlanId'), planId || '—');
749
+
750
+ // Error block
751
+ var statusError = (data.status && data.status.error) || null;
752
+ if (statusError) {
753
+ text($('hdrErrorText'), escapeText(statusError));
754
+ show($('hdrErrorBlock'));
755
+ } else {
756
+ hide($('hdrErrorBlock'));
757
+ }
758
+
759
+ // ---- Execution status (runtime) ----
760
+ var runtime = data.runtime || null;
761
+ var rtPhase = runtime ? (runtime.phase || null) : null;
762
+ text($('rtPhase'), getPhaseLabel(rtPhase));
763
+ var rtHeartbeat = runtime ? (runtime.last_heartbeat_at || null) : null;
764
+ text($('rtHeartbeat'), formatTime(rtHeartbeat));
765
+ var rtCmd = runtime ? (runtime.current_command || null) : null;
766
+ text($('rtCurrentCmd'), rtCmd || '—');
767
+
768
+ // ---- Result summary ----
769
+ var result = data.result || null;
770
+ var rsStatus = result ? (result.status || null) : null;
771
+ text($('rsStatus'), rsStatus || '—');
772
+ var rsVerify = result ? (result.verify_status || null) : null;
773
+ text($('rsVerifyStatus'), rsVerify || '—');
774
+ var rsSummary = result ? (result.summary || null) : null;
775
+ if (rsSummary) {
776
+ text($('rsSummary'), escapeText(rsSummary));
777
+ show($('rsSummaryBlock'));
778
+ } else {
779
+ hide($('rsSummaryBlock'));
780
+ }
781
+
782
+ // ---- Audit ----
783
+ var audit = data.audit || null;
784
+ var verdict = audit ? (audit.verdict || audit.decision || null) : null;
785
+ applyVerdictBadge($('auditVerdictBadge'), verdict);
786
+ var scopeChanges = audit ? (audit.scope_changes || null) : null;
787
+ text($('auditScope'), scopeChanges || '—');
788
+ var checkedAt = audit ? (audit.checked_at || null) : null;
789
+ text($('auditCheckedAt'), formatTime(checkedAt));
790
+ if (audit) {
791
+ hide($('auditEmptyHint'));
792
+ } else {
793
+ show($('auditEmptyHint'));
794
+ }
795
+
796
+ // ---- Diff preview ----
797
+ // API returns diff_patch (snake_case); also accept diff/testLog/test_log defensively.
798
+ var diff = data.diff_patch || data.diff || data.diffPatch || null;
799
+ var diffEl = $('diffContent');
800
+ var diffEmpty = $('diffEmpty');
801
+ if (diff) {
802
+ text(diffEl, escapeText(diff));
803
+ show(diffEl);
804
+ hide(diffEmpty);
805
+ } else {
806
+ hide(diffEl);
807
+ show(diffEmpty);
808
+ }
809
+
810
+ // ---- Test log ----
811
+ var testLog = data.test_log || data.testLog || null;
812
+ var logEl = $('logContent');
813
+ var logEmpty = $('logEmpty');
814
+ if (testLog) {
815
+ text(logEl, escapeText(testLog));
816
+ show(logEl);
817
+ hide(logEmpty);
818
+ } else {
819
+ hide(logEl);
820
+ show(logEmpty);
821
+ }
822
+
823
+ // ---- Changed files ----
824
+ renderChangedFiles(data.changed_files, data.file_stats);
825
+
826
+ // ---- Verify log ----
827
+ var verifyLog = data.verify_log || null;
828
+ var vlEl = $('verifyLogContent');
829
+ var vlEmpty = $('verifyLogEmpty');
830
+ if (verifyLog) {
831
+ text(vlEl, escapeText(verifyLog));
832
+ show(vlEl);
833
+ hide(vlEmpty);
834
+ } else {
835
+ hide(vlEl);
836
+ show(vlEmpty);
837
+ }
838
+
839
+ // ---- Independent review (audit_task artifact) ----
840
+ var review = data.independent_review || null;
841
+ var rvContent = review ? (review.content || null) : null;
842
+ var rvVerdict = review ? (review.verdict || null) : null;
843
+ var rvEl = $('reviewContent');
844
+ var rvEmpty = $('reviewEmpty');
845
+ var rvBadge = $('reviewVerdictBadge');
846
+ if (rvContent) {
847
+ text(rvEl, escapeText(rvContent));
848
+ show(rvEl);
849
+ hide(rvEmpty);
850
+ } else {
851
+ hide(rvEl);
852
+ show(rvEmpty);
853
+ }
854
+ applyVerdictBadge(rvBadge, rvVerdict);
855
+
856
+ // ---- Verification summary ----
857
+ renderVerificationSummary(data.verification_summary);
858
+
859
+ // ---- Warnings / Errors ----
860
+ renderWarningsErrors(data.warnings, data.errors);
861
+
862
+ // ---- Stale / Reconcile banner ----
863
+ renderStaleBanner(data.stale, data.reconcile);
864
+
865
+ showDetail();
866
+ }
867
+
868
+ // ---- Render: changed files ----
869
+ function renderChangedFiles(changedFiles, fileStats) {
870
+ var emptyEl = $('cfEmpty');
871
+ var bodyEl = $('cfBody');
872
+ var listEl = $('cfList');
873
+ var statsEl = $('cfStatsRow');
874
+ var badgeEl = $('cfCountBadge');
875
+
876
+ if (!changedFiles) {
877
+ hide(bodyEl);
878
+ hide(badgeEl);
879
+ show(emptyEl);
880
+ return;
881
+ }
882
+
883
+ // Build a list of paths to display.
884
+ var paths = [];
885
+ if (Array.isArray(changedFiles.files)) {
886
+ for (var i = 0; i < changedFiles.files.length; i++) {
887
+ var f = changedFiles.files[i];
888
+ if (typeof f === 'string') paths.push(f);
889
+ else if (f && typeof f === 'object' && typeof f.path === 'string') paths.push(f.path);
890
+ }
891
+ }
892
+ if (paths.length === 0 && Array.isArray(changedFiles.changed_files)) {
893
+ for (var j = 0; j < changedFiles.changed_files.length; j++) {
894
+ var cf = changedFiles.changed_files[j];
895
+ if (typeof cf === 'string') paths.push(cf);
896
+ else if (cf && typeof cf === 'object' && typeof cf.path === 'string') paths.push(cf.path);
897
+ }
898
+ }
899
+
900
+ // file_stats summary
901
+ var statsHtml = '';
902
+ if (fileStats && typeof fileStats === 'object') {
903
+ var counts = fileStats.counts || fileStats;
904
+ var statItems = [
905
+ { label: 'modified', value: counts.modified != null ? counts.modified : null },
906
+ { label: 'added', value: counts.added != null ? counts.added : null },
907
+ { label: 'deleted', value: counts.deleted != null ? counts.deleted : null },
908
+ { label: 'untracked', value: counts.untracked != null ? counts.untracked : null }
909
+ ];
910
+ var hasAny = false;
911
+ statsHtml = statItems.map(function (it) {
912
+ if (it.value === null || it.value === undefined) return '';
913
+ hasAny = true;
914
+ return '<div class="rounded-md px-2 py-1.5" style="background-color: var(--pw-bg); border: 1px solid var(--pw-border-subtle);">'
915
+ + '<div class="text-xs" style="color: var(--pw-text-tertiary);">' + escapeHtml(it.label) + '</div>'
916
+ + '<div class="text-sm font-semibold" style="color: var(--pw-text-primary); font-family: var(--pw-font-mono);">' + escapeHtml(String(it.value)) + '</div>'
917
+ + '</div>';
918
+ }).join('');
919
+ if (hasAny) {
920
+ show(statsEl);
921
+ statsEl.innerHTML = statsHtml;
922
+ } else {
923
+ hide(statsEl);
924
+ }
925
+ } else {
926
+ hide(statsEl);
927
+ }
928
+
929
+ if (paths.length === 0) {
930
+ hide(bodyEl);
931
+ hide(badgeEl);
932
+ show(emptyEl);
933
+ return;
934
+ }
935
+
936
+ // Render path list (cap at 500 to avoid rendering huge DOM).
937
+ var displayCount = Math.min(paths.length, 500);
938
+ var listHtml = '';
939
+ for (var k = 0; k < displayCount; k++) {
940
+ listHtml += escapeHtml(paths[k]) + '\n';
941
+ }
942
+ if (paths.length > displayCount) {
943
+ listHtml += '\n... (' + (paths.length - displayCount) + ' more truncated)';
944
+ }
945
+ listEl.textContent = listHtml;
946
+
947
+ // Badge
948
+ text(badgeEl, paths.length + ' 个文件');
949
+ show(badgeEl);
950
+
951
+ hide(emptyEl);
952
+ show(bodyEl);
953
+ }
954
+
955
+ // ---- Render: verification summary ----
956
+ function renderVerificationSummary(vs) {
957
+ var statusEl = $('vsStatus');
958
+ var checkedEl = $('vsCheckedAt');
959
+ var cmdsBlock = $('vsCommandsBlock');
960
+ var cmdsEl = $('vsCommands');
961
+ var emptyHint = $('vsEmptyHint');
962
+
963
+ if (!vs) {
964
+ text(statusEl, '—');
965
+ text(checkedEl, '—');
966
+ hide(cmdsBlock);
967
+ show(emptyHint);
968
+ return;
969
+ }
970
+ hide(emptyHint);
971
+ text(statusEl, vs.status || '—');
972
+ text(checkedEl, formatTime(vs.checked_at));
973
+ if (Array.isArray(vs.commands) && vs.commands.length > 0) {
974
+ text(cmdsEl, vs.commands.map(function (c) { return JSON.stringify(c); }).join('\n'));
975
+ show(cmdsBlock);
976
+ } else {
977
+ hide(cmdsBlock);
978
+ }
979
+ }
980
+
981
+ // ---- Render: warnings / errors ----
982
+ function renderWarningsErrors(warnings, errors) {
983
+ var emptyEl = $('weEmpty');
984
+ var bodyEl = $('weBody');
985
+ var warnBlock = $('weWarningsBlock');
986
+ var errBlock = $('weErrorsBlock');
987
+ var warnEl = $('weWarnings');
988
+ var errEl = $('weErrors');
989
+
990
+ var wList = Array.isArray(warnings) ? warnings : [];
991
+ var eList = Array.isArray(errors) ? errors : [];
992
+
993
+ if (wList.length === 0 && eList.length === 0) {
994
+ show(emptyEl);
995
+ hide(bodyEl);
996
+ return;
997
+ }
998
+ hide(emptyEl);
999
+ show(bodyEl);
1000
+
1001
+ if (wList.length > 0) {
1002
+ warnEl.textContent = wList.map(function (w) { return String(w); }).join('\n---\n');
1003
+ show(warnBlock);
1004
+ } else {
1005
+ hide(warnBlock);
1006
+ }
1007
+
1008
+ if (eList.length > 0) {
1009
+ errEl.textContent = eList.map(function (e) { return String(e); }).join('\n---\n');
1010
+ show(errBlock);
1011
+ } else {
1012
+ hide(errBlock);
1013
+ }
1014
+ }
1015
+
1016
+ // ---- Render: stale / reconcile banner ----
1017
+ function renderStaleBanner(stale, reconcile) {
1018
+ var banner = $('staleBanner');
1019
+ var reasonsEl = $('staleReasonsText');
1020
+ var btnReconcile = $('btnReconcile');
1021
+
1022
+ var isStale = !!(stale && stale.is_stale);
1023
+ if (!isStale) {
1024
+ hide(banner);
1025
+ return;
1026
+ }
1027
+ var reasons = (stale && Array.isArray(stale.stale_reasons)) ? stale.stale_reasons : [];
1028
+ var reasonsText = reasons.length > 0 ? reasons.join(' · ') : 'task appears stale';
1029
+ // Append reconcile state if present.
1030
+ if (reconcile && reconcile.decision) {
1031
+ reasonsText += '\n[reconcile: ' + reconcile.decision;
1032
+ if (reconcile.reconciled_at) reasonsText += ' @ ' + reconcile.reconciled_at;
1033
+ reasonsText += ']';
1034
+ }
1035
+ text(reasonsEl, reasonsText);
1036
+ show(banner);
1037
+ }
1038
+
1039
+ // ---- Action: run audit_task ----
1040
+ function runAudit(taskId) {
1041
+ var btn = $('btnAudit');
1042
+ if (btn) {
1043
+ btn.disabled = true;
1044
+ btn.style.opacity = '0.6';
1045
+ }
1046
+ postAction('/api/tasks/' + encodeURIComponent(taskId) + '/audit')
1047
+ .then(function (res) {
1048
+ return res.json().then(function (data) {
1049
+ return { ok: res.ok, status: res.status, data: data };
1050
+ });
1051
+ })
1052
+ .then(function (r) {
1053
+ if (!r.ok) {
1054
+ var msg = (r.data && r.data.error) || ('HTTP ' + r.status);
1055
+ alert('运行审计失败: ' + msg);
1056
+ } else {
1057
+ alert('审计完成。verdict=' + ((r.data && r.data.audit && r.data.audit.verdict) || '—'));
1058
+ loadTask(taskId);
1059
+ }
1060
+ })
1061
+ .catch(function (err) {
1062
+ alert('运行审计失败: ' + ((err && err.message) || '未知错误'));
1063
+ })
1064
+ .finally(function () {
1065
+ if (btn) {
1066
+ btn.disabled = false;
1067
+ btn.style.opacity = '';
1068
+ }
1069
+ });
1070
+ }
1071
+
1072
+ // ---- Action: reconcile ----
1073
+ function runReconcile(taskId) {
1074
+ var btn = $('btnReconcile');
1075
+ if (btn) {
1076
+ btn.disabled = true;
1077
+ btn.style.opacity = '0.6';
1078
+ }
1079
+ postAction('/api/tasks/' + encodeURIComponent(taskId) + '/reconcile')
1080
+ .then(function (res) {
1081
+ return res.json().then(function (data) {
1082
+ return { ok: res.ok, status: res.status, data: data };
1083
+ });
1084
+ })
1085
+ .then(function (r) {
1086
+ if (!r.ok) {
1087
+ var msg = (r.data && r.data.error) || ('HTTP ' + r.status);
1088
+ alert('Reconcile 失败: ' + msg);
1089
+ } else {
1090
+ var decision = (r.data && r.data.decision) || '—';
1091
+ alert('Reconcile 完成: ' + decision);
1092
+ loadTask(taskId);
1093
+ }
1094
+ })
1095
+ .catch(function (err) {
1096
+ alert('Reconcile 失败: ' + ((err && err.message) || '未知错误'));
1097
+ })
1098
+ .finally(function () {
1099
+ if (btn) {
1100
+ btn.disabled = false;
1101
+ btn.style.opacity = '';
1102
+ }
1103
+ });
1104
+ }
1105
+
1106
+ // ---- Action: open task folder ----
1107
+ function openFolder(taskId) {
1108
+ postAction('/api/tasks/' + encodeURIComponent(taskId) + '/open-folder')
1109
+ .then(function (res) {
1110
+ return res.json().then(function (data) {
1111
+ return { ok: res.ok, status: res.status, data: data };
1112
+ });
1113
+ })
1114
+ .then(function (r) {
1115
+ if (!r.ok) {
1116
+ var msg = (r.data && r.data.error) || ('HTTP ' + r.status);
1117
+ alert('打开目录失败: ' + msg);
1118
+ }
1119
+ })
1120
+ .catch(function (err) {
1121
+ alert('打开目录失败: ' + ((err && err.message) || '未知错误'));
1122
+ });
1123
+ }
1124
+
1125
+ // ---- Action: copy task id ----
1126
+ function copyTaskId(taskId) {
1127
+ if (navigator.clipboard && navigator.clipboard.writeText) {
1128
+ navigator.clipboard.writeText(taskId).then(
1129
+ function () { /* silent success */ },
1130
+ function () { fallbackCopy(taskId); }
1131
+ );
1132
+ } else {
1133
+ fallbackCopy(taskId);
1134
+ }
1135
+ }
1136
+ function fallbackCopy(text) {
1137
+ try {
1138
+ var ta = document.createElement('textarea');
1139
+ ta.value = text;
1140
+ ta.style.position = 'fixed';
1141
+ ta.style.opacity = '0';
1142
+ document.body.appendChild(ta);
1143
+ ta.select();
1144
+ document.execCommand('copy');
1145
+ document.body.removeChild(ta);
1146
+ } catch (e) { /* ignore */ }
1147
+ }
1148
+
1149
+ // ---- API fetch ----
1150
+ function loadTask(taskId) {
1151
+ showLoading();
1152
+ var url = '/api/tasks/' + encodeURIComponent(taskId);
1153
+ fetch(url, {
1154
+ method: 'GET',
1155
+ headers: { 'Accept': 'application/json' },
1156
+ cache: 'no-store'
1157
+ })
1158
+ .then(function (res) {
1159
+ if (res.status === 404) {
1160
+ // Task not found
1161
+ return res.text().then(function () {
1162
+ throw new Error('NOT_FOUND');
1163
+ });
1164
+ }
1165
+ if (!res.ok) {
1166
+ return res.text().then(function (body) {
1167
+ throw new Error('HTTP ' + res.status + (body ? (': ' + body.slice(0, 200)) : ''));
1168
+ });
1169
+ }
1170
+ return res.json();
1171
+ })
1172
+ .then(function (data) {
1173
+ if (!data || data.error) {
1174
+ throw new Error((data && data.error) ? data.error : '响应缺少任务数据');
1175
+ }
1176
+ renderDetail(data);
1177
+ })
1178
+ .catch(function (err) {
1179
+ var msg = (err && err.message) ? err.message : '未知错误';
1180
+ if (msg === 'NOT_FOUND') {
1181
+ showError('任务不存在', '未找到任务 ID: ' + taskId);
1182
+ } else if (err && err.name === 'TypeError' && /fetch/i.test(msg)) {
1183
+ showError('无法连接服务', '请确认 PatchWarden Control Center 正在运行 (127.0.0.1:8090)');
1184
+ } else {
1185
+ showError('加载失败', msg);
1186
+ }
1187
+ });
1188
+ }
1189
+
1190
+ // ---- Entry point ----
1191
+ var params = new URLSearchParams(window.location.search);
1192
+ var taskId = params.get('id');
1193
+ if (!taskId) {
1194
+ showError('缺少 task id', '请在 URL 中提供 ?id=<task_id>');
1195
+ } else {
1196
+ loadTask(taskId);
1197
+ }
1198
+
1199
+ // ---- Action button wiring ----
1200
+ var btnRefresh = $('btnRefresh');
1201
+ if (btnRefresh) btnRefresh.addEventListener('click', function () {
1202
+ if (taskId) loadTask(taskId);
1203
+ });
1204
+ var btnCopyId = $('btnCopyId');
1205
+ if (btnCopyId) btnCopyId.addEventListener('click', function () {
1206
+ if (taskId) copyTaskId(taskId);
1207
+ });
1208
+ var btnOpenFolder = $('btnOpenFolder');
1209
+ if (btnOpenFolder) btnOpenFolder.addEventListener('click', function () {
1210
+ if (taskId) openFolder(taskId);
1211
+ });
1212
+ var btnAudit = $('btnAudit');
1213
+ if (btnAudit) btnAudit.addEventListener('click', function () {
1214
+ if (!taskId) return;
1215
+ if (!confirm('确认对该任务运行 audit_task?仅当任务处于终态时才会执行。')) return;
1216
+ runAudit(taskId);
1217
+ });
1218
+ var btnReconcile = $('btnReconcile');
1219
+ if (btnReconcile) btnReconcile.addEventListener('click', function () {
1220
+ if (!taskId) return;
1221
+ if (!confirm('确认 reconcile 该任务?将写入 reconcile 记录(不会删除任务)。')) return;
1222
+ runReconcile(taskId);
1223
+ });
1224
+ })();
1225
+ </script>
1226
+
1227
+ <script>lucide.createIcons();</script>
1228
+ </body>
1229
+ </html>