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,652 @@
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="Direct 会话" data-en="Direct Sessions">Direct 会话</title>
7
+ <style id="theme-vars">
8
+ /* PatchWarden Brand CSS — Dark Security Theme */
9
+ /* Prefix: pw */
10
+
11
+ :root {
12
+ --pw-bg: #0a0e14;
13
+ --pw-bg-elevated: #111820;
14
+ --pw-bg-surface: #161d27;
15
+ --pw-bg-hover: #1c2533;
16
+ --pw-bg-active: #222d3d;
17
+ --pw-border: #1e2a3a;
18
+ --pw-border-subtle: #172030;
19
+ --pw-border-focus: #2dd4a8;
20
+
21
+ --pw-text-primary: #e8edf4;
22
+ --pw-text-secondary: #8b99ad;
23
+ --pw-text-tertiary: #5a6a80;
24
+ --pw-text-inverse: #0a0e14;
25
+ --pw-text-accent: #2dd4a8;
26
+
27
+ --pw-accent: #2dd4a8;
28
+ --pw-accent-hover: #34eabc;
29
+ --pw-accent-muted: rgba(45, 212, 168, 0.12);
30
+ --pw-accent-subtle: rgba(45, 212, 168, 0.06);
31
+
32
+ --pw-state-success: #22c55e;
33
+ --pw-state-success-bg: rgba(34, 197, 94, 0.1);
34
+ --pw-state-warning: #f59e0b;
35
+ --pw-state-warning-bg: rgba(245, 158, 11, 0.1);
36
+ --pw-state-error: #ef4444;
37
+ --pw-state-error-bg: rgba(239, 68, 68, 0.1);
38
+ --pw-state-info: #3b82f6;
39
+ --pw-state-info-bg: rgba(59, 130, 246, 0.1);
40
+
41
+ --pw-radius-sm: 4px;
42
+ --pw-radius-md: 8px;
43
+ --pw-radius-lg: 12px;
44
+ --pw-radius-full: 9999px;
45
+
46
+ --pw-shadow-float: 0 8px 32px rgba(0, 0, 0, 0.4);
47
+ --pw-shadow-modal: 0 16px 48px rgba(0, 0, 0, 0.5);
48
+
49
+ --pw-font-sans: 'Inter', 'Segoe UI', system-ui, -apple-system, sans-serif;
50
+ --pw-font-mono: 'JetBrains Mono', 'Cascadia Code', 'Consolas', monospace;
51
+
52
+ --pw-text-xs: 0.75rem;
53
+ --pw-text-sm: 0.8125rem;
54
+ --pw-text-base: 0.875rem;
55
+ --pw-text-lg: 1rem;
56
+ --pw-text-xl: 1.25rem;
57
+ --pw-text-2xl: 1.5rem;
58
+ --pw-text-3xl: 2rem;
59
+
60
+ --pw-leading-tight: 1.25;
61
+ --pw-leading-normal: 1.5;
62
+ --pw-leading-relaxed: 1.625;
63
+
64
+ --pw-weight-normal: 400;
65
+ --pw-weight-medium: 500;
66
+ --pw-weight-semibold: 600;
67
+ --pw-weight-bold: 700;
68
+
69
+ --pw-space-1: 4px;
70
+ --pw-space-2: 8px;
71
+ --pw-space-3: 12px;
72
+ --pw-space-4: 16px;
73
+ --pw-space-5: 20px;
74
+ --pw-space-6: 24px;
75
+ --pw-space-8: 32px;
76
+ --pw-space-10: 40px;
77
+ --pw-space-12: 48px;
78
+
79
+ --pw-transition-fast: 150ms ease;
80
+ --pw-transition-base: 200ms ease;
81
+ --pw-transition-slow: 300ms ease;
82
+ }
83
+
84
+ html.dark {
85
+ --pw-bg: #0a0e14;
86
+ --pw-bg-elevated: #111820;
87
+ --pw-bg-surface: #161d27;
88
+ --pw-bg-hover: #1c2533;
89
+ --pw-bg-active: #222d3d;
90
+ --pw-border: #1e2a3a;
91
+ --pw-border-subtle: #172030;
92
+ --pw-text-primary: #e8edf4;
93
+ --pw-text-secondary: #8b99ad;
94
+ --pw-text-tertiary: #5a6a80;
95
+ }
96
+ </style>
97
+ <script src="/vendor/tailwindcss-browser.js"></script>
98
+ <script src="/vendor/lucide.js"></script>
99
+ <style>
100
+ .no-scrollbar::-webkit-scrollbar { display: none; }
101
+ .no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
102
+ .pw-spin { animation: pw-spin 1s linear infinite; }
103
+ @keyframes pw-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
104
+ </style>
105
+ </head>
106
+ <body class="flex h-screen overflow-hidden">
107
+
108
+ <!-- ========== SIDEBAR ========== -->
109
+ <aside class="flex flex-col w-60 shrink-0 h-screen" style="background-color: var(--pw-bg); border-right: 1px solid var(--pw-border);">
110
+
111
+ <!-- Logo Area -->
112
+ <div class="flex items-center gap-3 px-5 py-5" style="border-bottom: 1px solid var(--pw-border-subtle);">
113
+ <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">
114
+ <path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
115
+ <path d="M9 12l2 2 4-4"/>
116
+ </svg>
117
+ <span class="truncate text-lg font-semibold tracking-tight" style="color: var(--pw-text-primary); font-family: var(--pw-font-mono);">PatchWarden</span>
118
+ </div>
119
+
120
+ <!-- Navigation -->
121
+ <nav class="flex-1 flex flex-col gap-1 px-3 pt-4 overflow-y-auto">
122
+
123
+ <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);">
124
+ <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">
125
+ <rect x="3" y="3" width="7" height="7" rx="1"/>
126
+ <rect x="14" y="3" width="7" height="7" rx="1"/>
127
+ <rect x="3" y="14" width="7" height="7" rx="1"/>
128
+ <rect x="14" y="14" width="7" height="7" rx="1"/>
129
+ </svg>
130
+ <span class="truncate" data-zh="控制台" data-en="Dashboard">控制台</span>
131
+ </a>
132
+
133
+ <a href="/pages/tasks.html" data-nav-key="tasks" data-dom-id="nav-tasks" 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);">
134
+ <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">
135
+ <path d="M9 11l3 3L22 4"/>
136
+ <path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/>
137
+ </svg>
138
+ <span class="truncate" data-zh="任务面板" data-en="Tasks">任务面板</span>
139
+ </a>
140
+
141
+ <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);">
142
+ <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">
143
+ <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"/>
144
+ </svg>
145
+ <span class="truncate" data-zh="工作区" data-en="Workspace">工作区</span>
146
+ </a>
147
+
148
+ <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);">
149
+ <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">
150
+ <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
151
+ <polyline points="14 2 14 8 20 8"/>
152
+ <line x1="16" y1="13" x2="8" y2="13"/>
153
+ <line x1="16" y1="17" x2="8" y2="17"/>
154
+ <polyline points="10 9 9 9 8 9"/>
155
+ </svg>
156
+ <span class="truncate" data-zh="审计日志" data-en="Audit Log">审计日志</span>
157
+ </a>
158
+
159
+ <a href="/pages/direct-sessions.html" data-nav-key="direct-sessions" data-dom-id="nav-direct-sessions" 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);">
160
+ <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">
161
+ <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"/>
162
+ <line x1="4" y1="22" x2="4" y2="15"/>
163
+ </svg>
164
+ <span class="truncate" data-zh="Direct 会话" data-en="Direct Sessions">Direct 会话</span>
165
+ </a>
166
+
167
+ <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);">
168
+ <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">
169
+ <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
170
+ <polyline points="14 2 14 8 20 8"/>
171
+ <line x1="16" y1="13" x2="8" y2="13"/>
172
+ <line x1="16" y1="17" x2="8" y2="17"/>
173
+ </svg>
174
+ <span class="truncate" data-zh="日志" data-en="Logs">日志</span>
175
+ </a>
176
+
177
+ </nav>
178
+
179
+ <!-- Bottom Section -->
180
+ <div class="px-5 py-4" style="border-top: 1px solid var(--pw-border-subtle);">
181
+ <div class="flex items-center gap-2 mb-3">
182
+ <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>
183
+ </div>
184
+ <div class="flex items-center gap-2">
185
+ <span class="shrink-0 w-2 h-2 rounded-full" style="background-color: var(--pw-state-success);"></span>
186
+ <span class="text-xs truncate" style="color: var(--pw-text-tertiary);">Connected</span>
187
+ </div>
188
+ </div>
189
+
190
+ </aside>
191
+
192
+ <!-- ========== MAIN AREA ========== -->
193
+ <div class="flex flex-col flex-1 min-w-0 h-screen overflow-hidden">
194
+
195
+ <!-- Top Bar -->
196
+ <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);">
197
+ <h1 class="text-lg font-semibold truncate min-w-0" data-zh="Direct 会话" data-en="Direct Sessions" style="color: var(--pw-text-primary); font-family: var(--pw-font-sans);">
198
+ Direct 会话
199
+ </h1>
200
+ <div class="flex items-center gap-4 shrink-0">
201
+ <span id="lastUpdated" class="text-xs whitespace-nowrap hidden" style="color: var(--pw-text-tertiary); font-family: var(--pw-font-mono);"></span>
202
+ <button id="refreshBtn" class="flex items-center gap-2 px-4 py-1.5 rounded-lg text-xs font-semibold 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);">
203
+ <svg id="refreshIcon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
204
+ <polyline points="23 4 23 10 17 10"/>
205
+ <polyline points="1 20 1 14 7 14"/>
206
+ <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"/>
207
+ </svg>
208
+ <span data-zh="刷新" data-en="Refresh">刷新</span>
209
+ </button>
210
+ <div class="shrink-0 w-8 h-8 rounded-full flex items-center justify-center text-xs font-semibold" style="background-color: var(--pw-accent-muted); color: var(--pw-accent); font-family: var(--pw-font-mono);">
211
+ PW
212
+ </div>
213
+ </div>
214
+ </header>
215
+
216
+ <!-- Page Content Area -->
217
+ <main class="flex-1 overflow-y-auto p-6" style="background-color: var(--pw-bg-elevated);">
218
+
219
+ <!-- ===== Error Banner ===== -->
220
+ <div id="error-banner" class="hidden mb-5 rounded-lg px-4 py-3 flex items-start gap-3" style="background-color: var(--pw-state-error-bg); border: 1px solid var(--pw-state-error);">
221
+ <svg class="shrink-0 mt-0.5" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="var(--pw-state-error)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
222
+ <circle cx="12" cy="12" r="10"/>
223
+ <line x1="12" y1="8" x2="12" y2="12"/>
224
+ <line x1="12" y1="16" x2="12.01" y2="16"/>
225
+ </svg>
226
+ <div class="min-w-0 flex-1">
227
+ <p class="text-sm font-medium" style="color: var(--pw-state-error);" data-zh="加载失败" data-en="Load Failed">加载失败</p>
228
+ <p id="error-message" class="text-xs mt-1 break-words" style="color: var(--pw-text-secondary); font-family: var(--pw-font-mono);"></p>
229
+ </div>
230
+ <button id="retry-btn" 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-secondary); border: 1px solid var(--pw-border-subtle); font-family: var(--pw-font-sans);" data-zh="重试" data-en="Retry">重试</button>
231
+ </div>
232
+
233
+ <!-- ===== Loading ===== -->
234
+ <div id="loading-state" class="rounded-lg py-16 text-center" style="background-color: var(--pw-bg-surface); border: 1px solid var(--pw-border);">
235
+ <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">
236
+ <line x1="12" y1="2" x2="12" y2="6"/>
237
+ <line x1="12" y1="18" x2="12" y2="22"/>
238
+ <line x1="4.93" y1="4.93" x2="7.76" y2="7.76"/>
239
+ <line x1="16.24" y1="16.24" x2="19.07" y2="19.07"/>
240
+ <line x1="2" y1="12" x2="6" y2="12"/>
241
+ <line x1="18" y1="12" x2="22" y2="12"/>
242
+ <line x1="4.93" y1="19.07" x2="7.76" y2="16.24"/>
243
+ <line x1="16.24" y1="7.76" x2="19.07" y2="4.93"/>
244
+ </svg>
245
+ <p class="text-sm" style="color: var(--pw-text-tertiary); font-family: var(--pw-font-sans);" data-zh="加载中..." data-en="Loading...">加载中...</p>
246
+ </div>
247
+
248
+ <!-- ===== Empty ===== -->
249
+ <div id="empty-state" class="hidden rounded-lg py-16 text-center" style="background-color: var(--pw-bg-surface); border: 1px solid var(--pw-border);">
250
+ <svg class="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">
251
+ <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"/>
252
+ <line x1="4" y1="22" x2="4" y2="15"/>
253
+ </svg>
254
+ <p class="text-sm" style="color: var(--pw-text-tertiary); font-family: var(--pw-font-sans);" data-zh="暂无 Direct 会话" data-en="No Direct sessions">暂无 Direct 会话</p>
255
+ <p id="empty-reason" class="text-xs mt-2" style="color: var(--pw-text-tertiary); font-family: var(--pw-font-mono);"></p>
256
+ </div>
257
+
258
+ <!-- ===== Sessions List ===== -->
259
+ <div id="sessions-list" class="hidden flex flex-col gap-3"></div>
260
+
261
+ <!-- ===== Session Detail ===== -->
262
+ <div id="session-detail" class="hidden flex flex-col gap-6"></div>
263
+
264
+ </main>
265
+ </div>
266
+
267
+ <!-- Active nav item styling + sidebar hover states -->
268
+ <style>
269
+ [data-nav-key] {
270
+ transition: background-color var(--pw-transition-fast), color var(--pw-transition-fast);
271
+ }
272
+ [data-nav-key]:hover {
273
+ background-color: var(--pw-bg-hover);
274
+ color: var(--pw-text-primary);
275
+ }
276
+ [data-nav-key][data-active="true"] {
277
+ background-color: var(--pw-accent-subtle);
278
+ color: var(--pw-accent);
279
+ border-left: 3px solid var(--pw-accent);
280
+ padding-left: 9px;
281
+ }
282
+ </style>
283
+
284
+ <script>lucide.createIcons();</script>
285
+
286
+ <script>
287
+ (function () {
288
+ 'use strict';
289
+
290
+ var API_URL = '/api/direct-sessions';
291
+
292
+ function $(id) { return document.getElementById(id); }
293
+ function hide(el) { if (el) el.classList.add('hidden'); }
294
+ function show(el) { if (el) el.classList.remove('hidden'); }
295
+
296
+ function escapeHtml(s) {
297
+ if (s === null || s === undefined) return '';
298
+ return String(s).replace(/[&<>"']/g, function (c) {
299
+ return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c];
300
+ });
301
+ }
302
+
303
+ function fmtTime(s) {
304
+ if (!s) return '—';
305
+ var d = new Date(s);
306
+ if (isNaN(d.getTime())) return escapeHtml(s);
307
+ return d.toLocaleString();
308
+ }
309
+
310
+ function pad2(n) { return String(n).padStart(2, '0'); }
311
+ function fmtTimeShort(iso) {
312
+ if (!iso) return '—';
313
+ var d = new Date(iso);
314
+ if (isNaN(d.getTime())) return String(iso);
315
+ return d.getFullYear() + '-' + pad2(d.getMonth() + 1) + '-' + pad2(d.getDate())
316
+ + ' ' + pad2(d.getHours()) + ':' + pad2(d.getMinutes()) + ':' + pad2(d.getSeconds());
317
+ }
318
+
319
+ function showError(msg) {
320
+ var banner = $('error-banner');
321
+ var message = $('error-message');
322
+ if (message) message.textContent = msg || '未知错误';
323
+ if (banner) show(banner);
324
+ }
325
+ function clearError() {
326
+ var banner = $('error-banner');
327
+ if (banner) hide(banner);
328
+ }
329
+ function setRefreshing(b) {
330
+ var icon = $('refreshIcon');
331
+ if (!icon) return;
332
+ if (b) icon.classList.add('pw-spin'); else icon.classList.remove('pw-spin');
333
+ }
334
+ function setLastUpdated() {
335
+ var el = $('lastUpdated');
336
+ if (!el) return;
337
+ el.textContent = '更新于 ' + fmtTime(new Date().toISOString());
338
+ show(el);
339
+ }
340
+
341
+ function showLoading() {
342
+ show($('loading-state'));
343
+ hide($('empty-state'));
344
+ hide($('sessions-list'));
345
+ hide($('session-detail'));
346
+ }
347
+ function showEmpty(reason) {
348
+ hide($('loading-state'));
349
+ show($('empty-state'));
350
+ hide($('sessions-list'));
351
+ hide($('session-detail'));
352
+ var rEl = $('empty-reason');
353
+ if (rEl) rEl.textContent = reason || '';
354
+ }
355
+ function showList() {
356
+ hide($('loading-state'));
357
+ hide($('empty-state'));
358
+ show($('sessions-list'));
359
+ hide($('session-detail'));
360
+ }
361
+ function showDetail() {
362
+ hide($('loading-state'));
363
+ hide($('empty-state'));
364
+ hide($('sessions-list'));
365
+ show($('session-detail'));
366
+ }
367
+
368
+ function verdictColor(v) {
369
+ var s = String(v || '').toLowerCase();
370
+ if (s === 'pass' || s === 'passed' || s === 'accept' || s === 'accepted') return 'success';
371
+ if (s === 'warn' || s === 'warning') return 'warning';
372
+ if (s === 'fail' || s === 'failed' || s === 'reject' || s === 'rejected') return 'error';
373
+ return 'muted';
374
+ }
375
+
376
+ var COLOR_VARS = {
377
+ success: { fg: 'var(--pw-state-success)', bg: 'var(--pw-state-success-bg)' },
378
+ warning: { fg: 'var(--pw-state-warning)', bg: 'var(--pw-state-warning-bg)' },
379
+ error: { fg: 'var(--pw-state-error)', bg: 'var(--pw-state-error-bg)' },
380
+ muted: { fg: 'var(--pw-text-tertiary)', bg: 'var(--pw-bg-hover)' }
381
+ };
382
+
383
+ function badgeHtml(text, color) {
384
+ var c = COLOR_VARS[color] || COLOR_VARS.muted;
385
+ return '<span class="text-xs px-2 py-0.5 rounded-md whitespace-nowrap" style="color: ' + c.fg + '; background-color: ' + c.bg + ';">' + escapeHtml(text) + '</span>';
386
+ }
387
+
388
+ // ---------- Render: sessions list ----------
389
+ function renderSessions(sessions) {
390
+ var listEl = $('sessions-list');
391
+ listEl.innerHTML = '';
392
+ if (!sessions || sessions.length === 0) {
393
+ showEmpty();
394
+ return;
395
+ }
396
+
397
+ var html = '';
398
+ for (var i = 0; i < sessions.length; i++) {
399
+ var s = sessions[i] || {};
400
+ var sid = s.session_id || '—';
401
+ var finalized = !!s.finalized;
402
+ var audited = !!s.audited;
403
+ var changedTotal = s.changed_files_total != null ? s.changed_files_total : '—';
404
+ var decision = s.audit_decision || null;
405
+ var decisionBadge = decision ? badgeHtml(decision, verdictColor(decision)) : '<span class="text-xs px-2 py-0.5 rounded-md" style="color: var(--pw-text-tertiary); background-color: var(--pw-bg-hover);">not audited</span>';
406
+ var stateBadge = finalized
407
+ ? badgeHtml('finalized', 'success')
408
+ : badgeHtml('open', 'warning');
409
+
410
+ var titleText = s.title || s.repo_path || sid;
411
+
412
+ html +=
413
+ '<div class="rounded-lg p-4 cursor-pointer transition-colors session-card" data-session-id="' + escapeHtml(sid) + '" '
414
+ + 'onmouseover="this.style.backgroundColor=\'var(--pw-bg-hover)\'" '
415
+ + 'onmouseout="this.style.backgroundColor=\'var(--pw-bg-surface)\'" '
416
+ + 'style="background-color: var(--pw-bg-surface); border: 1px solid var(--pw-border);">'
417
+ + '<div class="flex items-start justify-between gap-3 mb-3">'
418
+ + '<div class="min-w-0 flex-1">'
419
+ + '<div class="flex items-center gap-2 mb-1">'
420
+ + '<h3 class="text-sm font-semibold truncate" style="color: var(--pw-text-primary); font-family: var(--pw-font-sans);">' + escapeHtml(titleText) + '</h3>'
421
+ + '</div>'
422
+ + '<code class="text-xs break-all" style="color: var(--pw-text-tertiary); font-family: var(--pw-font-mono);">' + escapeHtml(sid) + '</code>'
423
+ + '</div>'
424
+ + '<div class="flex items-center gap-1.5 shrink-0 flex-wrap justify-end">'
425
+ + stateBadge + decisionBadge
426
+ + '</div>'
427
+ + '</div>'
428
+ + '<div class="grid grid-cols-2 sm:grid-cols-4 gap-3 text-xs">'
429
+ + '<div class="flex flex-col gap-0.5 min-w-0">'
430
+ + '<span class="text-xs" style="color: var(--pw-text-tertiary);">repo_path</span>'
431
+ + '<span class="truncate" style="color: var(--pw-text-secondary); font-family: var(--pw-font-mono);">' + escapeHtml(s.repo_path || '—') + '</span>'
432
+ + '</div>'
433
+ + '<div class="flex flex-col gap-0.5 min-w-0">'
434
+ + '<span class="text-xs" style="color: var(--pw-text-tertiary);">changed_files_total</span>'
435
+ + '<span class="truncate" style="color: var(--pw-text-primary); font-family: var(--pw-font-mono);">' + escapeHtml(String(changedTotal)) + '</span>'
436
+ + '</div>'
437
+ + '<div class="flex flex-col gap-0.5 min-w-0">'
438
+ + '<span class="text-xs" style="color: var(--pw-text-tertiary);">created_at</span>'
439
+ + '<span class="truncate" style="color: var(--pw-text-secondary); font-family: var(--pw-font-mono);">' + escapeHtml(fmtTimeShort(s.created_at)) + '</span>'
440
+ + '</div>'
441
+ + '<div class="flex flex-col gap-0.5 min-w-0">'
442
+ + '<span class="text-xs" style="color: var(--pw-text-tertiary);">expires_at</span>'
443
+ + '<span class="truncate" style="color: var(--pw-text-secondary); font-family: var(--pw-font-mono);">' + escapeHtml(fmtTimeShort(s.expires_at)) + '</span>'
444
+ + '</div>'
445
+ + '</div>'
446
+ + '</div>';
447
+ }
448
+ listEl.innerHTML = html;
449
+ showList();
450
+
451
+ var cards = listEl.querySelectorAll('.session-card');
452
+ for (var j = 0; j < cards.length; j++) {
453
+ cards[j].addEventListener('click', function (ev) {
454
+ var sid2 = ev.currentTarget.getAttribute('data-session-id');
455
+ if (sid2) loadSessionDetail(sid2);
456
+ });
457
+ }
458
+ }
459
+
460
+ // ---------- Render: session detail ----------
461
+ function renderSessionDetail(data) {
462
+ var el = $('session-detail');
463
+ var s = data.summary || {};
464
+ var sid = data.session_id || s.session_id || '—';
465
+
466
+ var decision = s.audit_decision || (data.audit_json && (data.audit_json.decision || data.audit_json.verdict)) || null;
467
+ var decisionBadge = decision ? badgeHtml(decision, verdictColor(decision)) : '';
468
+ var stateBadge = s.finalized ? badgeHtml('finalized', 'success') : badgeHtml('open', 'warning');
469
+
470
+ var changedTotal = s.changed_files_total != null ? s.changed_files_total : '—';
471
+
472
+ // verification summary (last run)
473
+ var vs = s.verification_summary || null;
474
+ var vsHtml = '—';
475
+ if (vs && typeof vs === 'object') {
476
+ var vsStatus = vs.status || vs.verdict || '—';
477
+ var vsCmds = Array.isArray(vs.commands) ? vs.commands.map(function (c) { return JSON.stringify(c); }).join(' ') : '';
478
+ vsHtml = '<span style="color: var(--pw-text-primary); font-family: var(--pw-font-mono);">' + escapeHtml(String(vsStatus)) + '</span>'
479
+ + (vsCmds ? '<div class="mt-1 text-xs break-all" style="color: var(--pw-text-tertiary);">' + escapeHtml(vsCmds) + '</div>' : '');
480
+ }
481
+
482
+ // Build content sections.
483
+ var sectionsHtml = '';
484
+
485
+ // summary.md
486
+ if (data.summary_md) {
487
+ sectionsHtml += detailPreSection('summary.md', data.summary_md);
488
+ }
489
+ // audit.md
490
+ if (data.audit_md) {
491
+ sectionsHtml += detailPreSection('audit.md', data.audit_md);
492
+ }
493
+ // audit.json (rendered as pretty JSON)
494
+ if (data.audit_json) {
495
+ sectionsHtml += detailPreSection('audit.json', JSON.stringify(data.audit_json, null, 2));
496
+ }
497
+ // diff.patch
498
+ if (data.diff_patch) {
499
+ sectionsHtml += detailPreSection('diff.patch', data.diff_patch);
500
+ }
501
+ // changed-files.json
502
+ if (data.changed_files) {
503
+ sectionsHtml += detailPreSection('changed-files.json', JSON.stringify(data.changed_files, null, 2));
504
+ }
505
+
506
+ if (!sectionsHtml) {
507
+ sectionsHtml = '<div class="rounded-lg py-10 text-center text-sm" style="background-color: var(--pw-bg-surface); border: 1px solid var(--pw-border); color: var(--pw-text-tertiary);" data-zh="该会话暂无可用产物文件" data-en="No artifact files available for this session">该会话暂无可用产物文件</div>';
508
+ }
509
+
510
+ el.innerHTML =
511
+ // Back link
512
+ '<div class="flex items-center gap-2 mb-2">'
513
+ + '<a href="#" id="back-to-list" 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);">'
514
+ + '<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>'
515
+ + '<span data-zh="返回会话列表" data-en="Back to Sessions">返回会话列表</span>'
516
+ + '</a>'
517
+ + '</div>'
518
+
519
+ // Header card
520
+ + '<section class="rounded-lg p-5" style="background-color: var(--pw-bg-surface); border: 1px solid var(--pw-border);">'
521
+ + '<div class="flex flex-col gap-4">'
522
+ + '<div class="flex flex-wrap items-center gap-3">'
523
+ + '<h2 class="text-lg font-semibold truncate" style="color: var(--pw-text-primary); font-family: var(--pw-font-mono);">' + escapeHtml(sid) + '</h2>'
524
+ + '<div class="flex items-center gap-1.5 shrink-0">' + stateBadge + decisionBadge + '</div>'
525
+ + '</div>'
526
+ + '<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-x-6 gap-y-3 text-sm">'
527
+ + detailMetaRow('repo_path', s.repo_path)
528
+ + detailMetaRow('resolved_repo_path', s.resolved_repo_path)
529
+ + detailMetaRow('changed_files_total', changedTotal)
530
+ + detailMetaRow('verification_summary', null, vsHtml)
531
+ + detailMetaRow('audit_decision', decision || '—')
532
+ + detailMetaRow('audit_checked_at', fmtTimeShort(s.audit_checked_at))
533
+ + detailMetaRow('created_at', fmtTimeShort(s.created_at))
534
+ + detailMetaRow('expires_at', fmtTimeShort(s.expires_at))
535
+ + detailMetaRow('finalized', s.finalized ? 'true' : 'false')
536
+ + detailMetaRow('finalized_at', fmtTimeShort(s.finalized_at))
537
+ + '</div>'
538
+ + '</div>'
539
+ + '</section>'
540
+
541
+ // Content sections
542
+ + '<div class="flex flex-col gap-6">' + sectionsHtml + '</div>';
543
+
544
+ showDetail();
545
+
546
+ var backBtn = $('back-to-list');
547
+ if (backBtn) backBtn.addEventListener('click', function (ev) {
548
+ ev.preventDefault();
549
+ loadSessions();
550
+ });
551
+ }
552
+
553
+ function detailMetaRow(label, value, customHtml) {
554
+ var valHtml = customHtml != null ? customHtml : escapeHtml(value == null || value === '' ? '—' : String(value));
555
+ return '<div class="flex flex-col gap-0.5 min-w-0">'
556
+ + '<span class="text-xs whitespace-nowrap" style="color: var(--pw-text-tertiary);">' + escapeHtml(label) + '</span>'
557
+ + '<span class="text-sm truncate" style="color: var(--pw-text-primary); font-family: var(--pw-font-mono);">' + valHtml + '</span>'
558
+ + '</div>';
559
+ }
560
+
561
+ function detailPreSection(title, content) {
562
+ return '<section class="rounded-lg overflow-hidden" style="background-color: var(--pw-bg-surface); border: 1px solid var(--pw-border);">'
563
+ + '<div class="flex items-center gap-2 px-4 py-3" style="border-bottom: 1px solid var(--pw-border);">'
564
+ + '<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>'
565
+ + '<h3 class="text-sm font-semibold" style="color: var(--pw-text-primary); font-family: var(--pw-font-mono);">' + escapeHtml(title) + '</h3>'
566
+ + '</div>'
567
+ + '<pre class="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;">' + escapeHtml(content) + '</pre>'
568
+ + '</section>';
569
+ }
570
+
571
+ // ---------- Network ----------
572
+ function loadSessions() {
573
+ clearError();
574
+ showLoading();
575
+ setRefreshing(true);
576
+ fetch(API_URL, { method: 'GET', headers: { 'Accept': 'application/json' }, cache: 'no-store' })
577
+ .then(function (res) {
578
+ if (!res.ok) {
579
+ return res.text().then(function (body) {
580
+ throw new Error('HTTP ' + res.status + (body ? (': ' + body.slice(0, 200)) : ''));
581
+ });
582
+ }
583
+ return res.json();
584
+ })
585
+ .then(function (data) {
586
+ data = data || {};
587
+ renderSessions(data.sessions || []);
588
+ setLastUpdated();
589
+ })
590
+ .catch(function (err) {
591
+ var msg = (err && err.message) ? err.message : '未知错误';
592
+ if (err && err.name === 'TypeError' && /fetch/i.test(msg)) {
593
+ msg = '无法连接到 ' + API_URL + '(请确认 PatchWarden 服务正在运行)';
594
+ }
595
+ showError(msg);
596
+ hide($('loading-state'));
597
+ })
598
+ .finally(function () {
599
+ setRefreshing(false);
600
+ });
601
+ }
602
+
603
+ function loadSessionDetail(sessionId) {
604
+ clearError();
605
+ showLoading();
606
+ setRefreshing(true);
607
+ var url = '/api/direct-sessions/' + encodeURIComponent(sessionId);
608
+ fetch(url, { method: 'GET', headers: { 'Accept': 'application/json' }, cache: 'no-store' })
609
+ .then(function (res) {
610
+ if (res.status === 404) {
611
+ return res.text().then(function () { throw new Error('NOT_FOUND'); });
612
+ }
613
+ if (!res.ok) {
614
+ return res.text().then(function (body) {
615
+ throw new Error('HTTP ' + res.status + (body ? (': ' + body.slice(0, 200)) : ''));
616
+ });
617
+ }
618
+ return res.json();
619
+ })
620
+ .then(function (data) {
621
+ if (!data || data.error) {
622
+ throw new Error((data && data.error) ? data.error : '响应缺少会话数据');
623
+ }
624
+ renderSessionDetail(data);
625
+ })
626
+ .catch(function (err) {
627
+ var msg = (err && err.message) ? err.message : '未知错误';
628
+ if (msg === 'NOT_FOUND') {
629
+ showError('会话不存在: ' + sessionId);
630
+ } else {
631
+ showError(msg);
632
+ }
633
+ // Fall back to list.
634
+ loadSessions();
635
+ })
636
+ .finally(function () {
637
+ setRefreshing(false);
638
+ });
639
+ }
640
+
641
+ // ---------- Event wiring ----------
642
+ var refreshBtn = $('refreshBtn');
643
+ if (refreshBtn) refreshBtn.addEventListener('click', loadSessions);
644
+ var retryBtn = $('retry-btn');
645
+ if (retryBtn) retryBtn.addEventListener('click', loadSessions);
646
+
647
+ // Initial load
648
+ loadSessions();
649
+ })();
650
+ </script>
651
+ </body>
652
+ </html>