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,502 @@
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="Logs">日志</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" 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" 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);">
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="日志" data-en="Logs" style="color: var(--pw-text-primary); font-family: var(--pw-font-sans);">
198
+ 日志
199
+ </h1>
200
+ <div class="flex items-center gap-3 shrink-0 flex-wrap justify-end">
201
+ <!-- Category selector -->
202
+ <div class="flex items-center gap-1 rounded-lg p-0.5" style="background-color: var(--pw-bg-surface); border: 1px solid var(--pw-border-subtle);">
203
+ <button class="cat-btn px-2.5 py-1 rounded-md text-xs font-medium transition-colors" data-cat="core" style="font-family: var(--pw-font-sans);">Core</button>
204
+ <button class="cat-btn px-2.5 py-1 rounded-md text-xs font-medium transition-colors" data-cat="direct" style="font-family: var(--pw-font-sans);">Direct</button>
205
+ <button class="cat-btn px-2.5 py-1 rounded-md text-xs font-medium transition-colors" data-cat="watcher" style="font-family: var(--pw-font-sans);">Watcher</button>
206
+ <button class="cat-btn px-2.5 py-1 rounded-md text-xs font-medium transition-colors" data-cat="control-center" style="font-family: var(--pw-font-sans);">Control Center</button>
207
+ </div>
208
+ <!-- Tail selector -->
209
+ <select id="tailSelect" class="px-2 py-1 rounded-md text-xs font-medium" style="background-color: var(--pw-bg-surface); color: var(--pw-text-secondary); border: 1px solid var(--pw-border-subtle); font-family: var(--pw-font-mono);">
210
+ <option value="100">tail 100</option>
211
+ <option value="300">tail 300</option>
212
+ <option value="1000">tail 1000</option>
213
+ </select>
214
+ <!-- Auto-refresh toggle -->
215
+ <label class="flex items-center gap-1.5 text-xs cursor-pointer select-none" style="color: var(--pw-text-secondary); font-family: var(--pw-font-sans);">
216
+ <input id="autoRefresh" type="checkbox" style="accent-color: var(--pw-accent);" />
217
+ <span data-zh="自动刷新" data-en="Auto refresh">自动刷新</span>
218
+ </label>
219
+ <span id="lastUpdated" class="text-xs whitespace-nowrap hidden" style="color: var(--pw-text-tertiary); font-family: var(--pw-font-mono);"></span>
220
+ <button id="refreshBtn" class="flex items-center gap-2 px-3 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);">
221
+ <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">
222
+ <polyline points="23 4 23 10 17 10"/>
223
+ <polyline points="1 20 1 14 7 14"/>
224
+ <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"/>
225
+ </svg>
226
+ <span data-zh="刷新" data-en="Refresh">刷新</span>
227
+ </button>
228
+ <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);">
229
+ PW
230
+ </div>
231
+ </div>
232
+ </header>
233
+
234
+ <!-- Page Content Area -->
235
+ <main class="flex-1 overflow-y-auto p-6" style="background-color: var(--pw-bg-elevated);">
236
+
237
+ <!-- ===== Error Banner ===== -->
238
+ <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);">
239
+ <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">
240
+ <circle cx="12" cy="12" r="10"/>
241
+ <line x1="12" y1="8" x2="12" y2="12"/>
242
+ <line x1="12" y1="16" x2="12.01" y2="16"/>
243
+ </svg>
244
+ <div class="min-w-0 flex-1">
245
+ <p class="text-sm font-medium" style="color: var(--pw-state-error);" data-zh="加载失败" data-en="Load Failed">加载失败</p>
246
+ <p id="error-message" class="text-xs mt-1 break-words" style="color: var(--pw-text-secondary); font-family: var(--pw-font-mono);"></p>
247
+ </div>
248
+ </div>
249
+
250
+ <!-- ===== Log Cards ===== -->
251
+ <div class="flex flex-col gap-6">
252
+
253
+ <!-- stdout -->
254
+ <section class="rounded-lg overflow-hidden" style="background-color: var(--pw-bg-surface); border: 1px solid var(--pw-border);">
255
+ <div class="flex items-center justify-between px-4 py-3" style="border-bottom: 1px solid var(--pw-border);">
256
+ <div class="flex items-center gap-2">
257
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="var(--pw-state-success)" 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>
258
+ <h2 class="text-sm font-semibold" style="color: var(--pw-text-primary); font-family: var(--pw-font-sans);">stdout</h2>
259
+ </div>
260
+ <span id="stdoutMeta" class="text-xs whitespace-nowrap" style="color: var(--pw-text-tertiary); font-family: var(--pw-font-mono);">—</span>
261
+ </div>
262
+ <div id="stdoutEmpty" class="px-4 py-8 text-center text-sm" style="color: var(--pw-text-tertiary); font-family: var(--pw-font-sans);" data-zh="暂无 stdout 日志" data-en="No stdout log">暂无 stdout 日志</div>
263
+ <pre id="stdoutContent" 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>
264
+ </section>
265
+
266
+ <!-- stderr -->
267
+ <section class="rounded-lg overflow-hidden" style="background-color: var(--pw-bg-surface); border: 1px solid var(--pw-border);">
268
+ <div class="flex items-center justify-between px-4 py-3" style="border-bottom: 1px solid var(--pw-border);">
269
+ <div class="flex items-center gap-2">
270
+ <svg 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"><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"/></svg>
271
+ <h2 class="text-sm font-semibold" style="color: var(--pw-text-primary); font-family: var(--pw-font-sans);">stderr</h2>
272
+ </div>
273
+ <span id="stderrMeta" class="text-xs whitespace-nowrap" style="color: var(--pw-text-tertiary); font-family: var(--pw-font-mono);">—</span>
274
+ </div>
275
+ <div id="stderrEmpty" class="px-4 py-8 text-center text-sm" style="color: var(--pw-text-tertiary); font-family: var(--pw-font-sans);" data-zh="暂无 stderr 日志" data-en="No stderr log">暂无 stderr 日志</div>
276
+ <pre id="stderrContent" 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-state-error); font-family: var(--pw-font-mono); max-height: 480px; overflow-y: auto;"></pre>
277
+ </section>
278
+
279
+ </div>
280
+
281
+ </main>
282
+ </div>
283
+
284
+ <!-- Active nav item styling + sidebar hover states -->
285
+ <style>
286
+ [data-nav-key] {
287
+ transition: background-color var(--pw-transition-fast), color var(--pw-transition-fast);
288
+ }
289
+ [data-nav-key]:hover {
290
+ background-color: var(--pw-bg-hover);
291
+ color: var(--pw-text-primary);
292
+ }
293
+ [data-nav-key][data-active="true"] {
294
+ background-color: var(--pw-accent-subtle);
295
+ color: var(--pw-accent);
296
+ border-left: 3px solid var(--pw-accent);
297
+ padding-left: 9px;
298
+ }
299
+ .cat-btn {
300
+ color: var(--pw-text-secondary);
301
+ background-color: transparent;
302
+ border: none;
303
+ cursor: pointer;
304
+ }
305
+ .cat-btn:hover {
306
+ color: var(--pw-text-primary);
307
+ }
308
+ .cat-btn[data-active="true"] {
309
+ background-color: var(--pw-accent-muted);
310
+ color: var(--pw-accent);
311
+ }
312
+ </style>
313
+
314
+ <script>lucide.createIcons();</script>
315
+
316
+ <script>
317
+ (function () {
318
+ 'use strict';
319
+
320
+ var DEFAULT_CATEGORY = 'core';
321
+ var DEFAULT_TAIL = 100;
322
+ var AUTO_REFRESH_INTERVAL_MS = 5000;
323
+
324
+ var currentCategory = DEFAULT_CATEGORY;
325
+ var currentTail = DEFAULT_TAIL;
326
+ var autoRefreshTimer = null;
327
+
328
+ function $(id) { return document.getElementById(id); }
329
+ function hide(el) { if (el) el.classList.add('hidden'); }
330
+ function show(el) { if (el) el.classList.remove('hidden'); }
331
+ function text(el, value) { if (el) el.textContent = value; }
332
+
333
+ function escapeHtml(s) {
334
+ if (s === null || s === undefined) return '';
335
+ return String(s).replace(/[&<>"']/g, function (c) {
336
+ return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c];
337
+ });
338
+ }
339
+
340
+ function pad2(n) { return String(n).padStart(2, '0'); }
341
+ function fmtTime(iso) {
342
+ if (!iso) return '—';
343
+ var d = new Date(iso);
344
+ if (isNaN(d.getTime())) return String(iso);
345
+ return d.getFullYear() + '-' + pad2(d.getMonth() + 1) + '-' + pad2(d.getDate())
346
+ + ' ' + pad2(d.getHours()) + ':' + pad2(d.getMinutes()) + ':' + pad2(d.getSeconds());
347
+ }
348
+
349
+ function showError(msg) {
350
+ var banner = $('error-banner');
351
+ var message = $('error-message');
352
+ if (message) message.textContent = msg || '未知错误';
353
+ if (banner) show(banner);
354
+ }
355
+ function clearError() {
356
+ var banner = $('error-banner');
357
+ if (banner) hide(banner);
358
+ }
359
+ function setRefreshing(b) {
360
+ var icon = $('refreshIcon');
361
+ if (!icon) return;
362
+ if (b) icon.classList.add('pw-spin'); else icon.classList.remove('pw-spin');
363
+ }
364
+ function setLastUpdated() {
365
+ var el = $('lastUpdated');
366
+ if (!el) return;
367
+ el.textContent = '更新于 ' + fmtTime(new Date().toISOString());
368
+ show(el);
369
+ }
370
+
371
+ function setCategory(cat) {
372
+ currentCategory = cat;
373
+ var btns = document.querySelectorAll('.cat-btn');
374
+ for (var i = 0; i < btns.length; i++) {
375
+ var b = btns[i];
376
+ if (b.getAttribute('data-cat') === cat) {
377
+ b.setAttribute('data-active', 'true');
378
+ } else {
379
+ b.removeAttribute('data-active');
380
+ }
381
+ }
382
+ }
383
+
384
+ function renderLogs(data) {
385
+ var stdout = (data && data.stdout) || '';
386
+ var stderr = (data && data.stderr) || '';
387
+ var reason = (data && data.reason) || null;
388
+ var tail = (data && data.tail) || currentTail;
389
+
390
+ // stdout
391
+ var stdoutEl = $('stdoutContent');
392
+ var stdoutEmpty = $('stdoutEmpty');
393
+ if (stdout) {
394
+ text(stdoutEl, stdout);
395
+ show(stdoutEl);
396
+ hide(stdoutEmpty);
397
+ } else {
398
+ hide(stdoutEl);
399
+ show(stdoutEmpty);
400
+ }
401
+ text($('stdoutMeta'), 'tail=' + tail + (reason ? ' · ' + reason : ''));
402
+
403
+ // stderr
404
+ var stderrEl = $('stderrContent');
405
+ var stderrEmpty = $('stderrEmpty');
406
+ if (stderr) {
407
+ text(stderrEl, stderr);
408
+ show(stderrEl);
409
+ hide(stderrEmpty);
410
+ } else {
411
+ hide(stderrEl);
412
+ show(stderrEmpty);
413
+ }
414
+ text($('stderrMeta'), 'tail=' + tail + (reason ? ' · ' + reason : ''));
415
+ }
416
+
417
+ function loadLogs() {
418
+ clearError();
419
+ setRefreshing(true);
420
+ var url = '/api/logs/' + encodeURIComponent(currentCategory) + '?tail=' + encodeURIComponent(String(currentTail));
421
+ fetch(url, { method: 'GET', headers: { 'Accept': 'application/json' }, cache: 'no-store' })
422
+ .then(function (res) {
423
+ if (!res.ok) {
424
+ return res.text().then(function (body) {
425
+ throw new Error('HTTP ' + res.status + (body ? (': ' + body.slice(0, 200)) : ''));
426
+ });
427
+ }
428
+ return res.json();
429
+ })
430
+ .then(function (data) {
431
+ renderLogs(data);
432
+ setLastUpdated();
433
+ })
434
+ .catch(function (err) {
435
+ var msg = (err && err.message) ? err.message : '未知错误';
436
+ if (err && err.name === 'TypeError' && /fetch/i.test(msg)) {
437
+ msg = '无法连接到日志服务(请确认 PatchWarden 服务正在运行)';
438
+ }
439
+ showError(msg);
440
+ })
441
+ .finally(function () {
442
+ setRefreshing(false);
443
+ });
444
+ }
445
+
446
+ // ---------- Auto-refresh ----------
447
+ function startAutoRefresh() {
448
+ stopAutoRefresh();
449
+ autoRefreshTimer = setInterval(loadLogs, AUTO_REFRESH_INTERVAL_MS);
450
+ }
451
+ function stopAutoRefresh() {
452
+ if (autoRefreshTimer) {
453
+ clearInterval(autoRefreshTimer);
454
+ autoRefreshTimer = null;
455
+ }
456
+ }
457
+
458
+ // ---------- Event wiring ----------
459
+ var catBtns = document.querySelectorAll('.cat-btn');
460
+ for (var i = 0; i < catBtns.length; i++) {
461
+ catBtns[i].addEventListener('click', function (ev) {
462
+ var cat = ev.currentTarget.getAttribute('data-cat');
463
+ if (cat && cat !== currentCategory) {
464
+ setCategory(cat);
465
+ loadLogs();
466
+ }
467
+ });
468
+ }
469
+
470
+ var tailSelect = $('tailSelect');
471
+ if (tailSelect) {
472
+ tailSelect.value = String(DEFAULT_TAIL);
473
+ tailSelect.addEventListener('change', function (ev) {
474
+ var v = parseInt(ev.target.value, 10);
475
+ if (v === 100 || v === 300 || v === 1000) {
476
+ currentTail = v;
477
+ loadLogs();
478
+ }
479
+ });
480
+ }
481
+
482
+ var autoRefreshChk = $('autoRefresh');
483
+ if (autoRefreshChk) {
484
+ autoRefreshChk.addEventListener('change', function (ev) {
485
+ if (ev.target.checked) {
486
+ startAutoRefresh();
487
+ } else {
488
+ stopAutoRefresh();
489
+ }
490
+ });
491
+ }
492
+
493
+ var refreshBtn = $('refreshBtn');
494
+ if (refreshBtn) refreshBtn.addEventListener('click', loadLogs);
495
+
496
+ // ---------- Initial load ----------
497
+ setCategory(DEFAULT_CATEGORY);
498
+ loadLogs();
499
+ })();
500
+ </script>
501
+ </body>
502
+ </html>