patchwarden 0.6.0 → 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.
- package/PatchWarden-Control-Tray.cmd +11 -0
- package/PatchWarden-Control.cmd +6 -0
- package/PatchWarden-Desktop.cmd +5 -0
- package/PatchWarden.cmd +1 -1
- package/README.en.md +106 -18
- package/README.md +30 -19
- package/Restart-PatchWarden-Control.cmd +6 -0
- package/Stop-PatchWarden.cmd +11 -0
- package/dist/controlCenter.d.ts +14 -0
- package/dist/controlCenter.js +2002 -0
- package/dist/doctor.js +4 -4
- package/dist/logging.d.ts +52 -0
- package/dist/logging.js +123 -0
- package/dist/runner/changeCapture.d.ts +41 -0
- package/dist/runner/changeCapture.js +171 -1
- package/dist/runner/runTask.js +204 -24
- package/dist/smoke-test.js +8 -8
- package/dist/test/unit/android-doctor.test.d.ts +1 -0
- package/dist/test/unit/android-doctor.test.js +118 -0
- package/dist/test/unit/chinese-path.test.d.ts +1 -0
- package/dist/test/unit/chinese-path.test.js +91 -0
- package/dist/test/unit/command-guard.test.d.ts +1 -0
- package/dist/test/unit/command-guard.test.js +160 -0
- package/dist/test/unit/direct-guards.test.d.ts +1 -0
- package/dist/test/unit/direct-guards.test.js +213 -0
- package/dist/test/unit/logging.test.d.ts +1 -0
- package/dist/test/unit/logging.test.js +275 -0
- package/dist/test/unit/path-guard.test.d.ts +1 -0
- package/dist/test/unit/path-guard.test.js +109 -0
- package/dist/test/unit/safe-status.test.d.ts +1 -0
- package/dist/test/unit/safe-status.test.js +165 -0
- package/dist/test/unit/sensitive-guard.test.d.ts +1 -0
- package/dist/test/unit/sensitive-guard.test.js +104 -0
- package/dist/test/unit/sync-file.test.d.ts +1 -0
- package/dist/test/unit/sync-file.test.js +154 -0
- package/dist/test/unit/watcher-status.test.d.ts +1 -0
- package/dist/test/unit/watcher-status.test.js +169 -0
- package/dist/tools/androidDoctor.d.ts +38 -0
- package/dist/tools/androidDoctor.js +391 -0
- package/dist/tools/auditTask.js +11 -5
- package/dist/tools/getTaskSummary.d.ts +3 -0
- package/dist/tools/getTaskSummary.js +15 -1
- package/dist/tools/healthCheck.d.ts +5 -0
- package/dist/tools/healthCheck.js +21 -0
- package/dist/tools/registry.js +53 -0
- package/dist/tools/safeStatus.d.ts +19 -0
- package/dist/tools/safeStatus.js +72 -0
- package/dist/tools/syncFile.d.ts +18 -0
- package/dist/tools/syncFile.js +65 -0
- package/dist/tools/taskOutputs.d.ts +2 -2
- package/dist/tools/toolCatalog.d.ts +2 -2
- package/dist/tools/toolCatalog.js +2 -0
- package/dist/version.d.ts +2 -2
- package/dist/version.js +2 -2
- package/dist/watcherStatus.d.ts +1 -0
- package/dist/watcherStatus.js +96 -4
- package/docs/control-center/README.md +33 -0
- package/docs/control-center/control-center-daily-driver.md +211 -0
- package/docs/control-center/control-center-mvp.md +205 -0
- package/docs/control-center/control-center-phase2.md +159 -0
- package/docs/demo.md +3 -0
- package/docs/performance-notes.md +55 -0
- package/docs/release-v0.6.1.md +75 -0
- package/docs/release-v0.6.4.md +45 -0
- package/examples/openai-tunnel/README.md +5 -5
- package/examples/openai-tunnel/tunnel-client.example.yaml +3 -3
- package/package.json +23 -14
- package/scripts/README.md +47 -0
- package/scripts/{brand-check.js → checks/brand-check.js} +2 -2
- package/scripts/checks/control-center-smoke.js +1098 -0
- package/scripts/{control-smoke.js → checks/control-smoke.js} +17 -3
- package/scripts/{doctor-smoke.js → checks/doctor-smoke.js} +1 -1
- package/scripts/{http-mcp-smoke.js → checks/http-mcp-smoke.js} +12 -4
- package/scripts/{lifecycle-smoke.js → checks/lifecycle-smoke.js} +347 -13
- package/scripts/{mcp-manifest-check.js → checks/mcp-manifest-check.js} +32 -9
- package/scripts/{mcp-smoke.js → checks/mcp-smoke.js} +14 -9
- package/scripts/{package-manifest-check.js → checks/package-manifest-check.js} +2 -1
- package/scripts/{tunnel-supervisor-smoke.js → checks/tunnel-supervisor-smoke.js} +2 -2
- package/scripts/checks/unit-tests.js +36 -0
- package/scripts/{watcher-supervisor-smoke.js → checks/watcher-supervisor-smoke.js} +11 -5
- package/scripts/control/control-center-tray.ps1 +281 -0
- package/scripts/{get-patchwarden-health.ps1 → control/get-patchwarden-health.ps1} +3 -3
- package/scripts/{manage-patchwarden.ps1 → control/manage-patchwarden.ps1} +30 -4
- package/scripts/control/restart-control-center.ps1 +173 -0
- package/scripts/{restart-patchwarden.ps1 → control/restart-patchwarden.ps1} +1 -1
- package/scripts/control/start-control-center.ps1 +263 -0
- package/scripts/{start-patchwarden-tunnel.ps1 → control/start-patchwarden-tunnel.ps1} +48 -6
- package/scripts/control/stop-patchwarden.ps1 +114 -0
- package/scripts/launchers/Check-PatchWarden-Health.cmd +1 -1
- package/scripts/launchers/Reset-PatchWarden-Tunnel-Key.cmd +1 -1
- package/scripts/launchers/Restart-PatchWarden.cmd +1 -1
- package/scripts/launchers/Start-PatchWarden-Direct-Tunnel.cmd +1 -1
- package/scripts/launchers/Start-PatchWarden-Tunnel.cmd +1 -1
- package/scripts/{patchwarden-mcp-direct.cmd → mcp/patchwarden-mcp-direct.cmd} +1 -1
- package/scripts/{patchwarden-mcp-stdio.cmd → mcp/patchwarden-mcp-stdio.cmd} +1 -1
- package/scripts/release/pack-clean.js +305 -0
- package/src/controlCenter.ts +2152 -0
- package/src/doctor.ts +5 -5
- package/src/logging.ts +152 -0
- package/src/runner/changeCapture.ts +212 -1
- package/src/runner/runTask.ts +220 -22
- package/src/smoke-test.ts +5 -5
- package/src/test/unit/android-doctor.test.ts +158 -0
- package/src/test/unit/chinese-path.test.ts +106 -0
- package/src/test/unit/command-guard.test.ts +221 -0
- package/src/test/unit/direct-guards.test.ts +297 -0
- package/src/test/unit/logging.test.ts +325 -0
- package/src/test/unit/path-guard.test.ts +150 -0
- package/src/test/unit/safe-status.test.ts +187 -0
- package/src/test/unit/sensitive-guard.test.ts +124 -0
- package/src/test/unit/sync-file.test.ts +231 -0
- package/src/test/unit/watcher-status.test.ts +190 -0
- package/src/tools/androidDoctor.ts +424 -0
- package/src/tools/auditTask.ts +11 -5
- package/src/tools/getTaskSummary.ts +22 -1
- package/src/tools/healthCheck.ts +22 -0
- package/src/tools/registry.ts +63 -0
- package/src/tools/safeStatus.ts +96 -0
- package/src/tools/syncFile.ts +122 -0
- package/src/tools/toolCatalog.ts +2 -0
- package/src/version.ts +2 -2
- package/src/watcherStatus.ts +101 -4
- package/ui/colors_and_type.css +141 -0
- package/ui/pages/audit.html +743 -0
- package/ui/pages/dashboard.html +1154 -0
- package/ui/pages/direct-sessions.html +652 -0
- package/ui/pages/logs.html +502 -0
- package/ui/pages/task-detail.html +1229 -0
- package/ui/pages/tasks.html +702 -0
- package/ui/pages/workspace.html +947 -0
- package/ui/partials/project-shell.html +362 -0
- package/ui/vendor/lucide.js +12 -0
- package/ui/vendor/tailwindcss-browser.js +947 -0
- package/scripts/pack-clean.js +0 -141
|
@@ -0,0 +1,743 @@
|
|
|
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>审计日志 — PatchWarden</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 {
|
|
136
|
+
animation: pw-spin 0.9s linear infinite;
|
|
137
|
+
}
|
|
138
|
+
@keyframes pw-spin {
|
|
139
|
+
from { transform: rotate(0deg); }
|
|
140
|
+
to { transform: rotate(360deg); }
|
|
141
|
+
}
|
|
142
|
+
.pw-row-selected {
|
|
143
|
+
background-color: var(--pw-bg-active) !important;
|
|
144
|
+
}
|
|
145
|
+
</style>
|
|
146
|
+
</head>
|
|
147
|
+
|
|
148
|
+
<body class="flex h-screen overflow-hidden">
|
|
149
|
+
|
|
150
|
+
<!-- ========== SIDEBAR ========== -->
|
|
151
|
+
<aside class="flex flex-col w-60 shrink-0 h-screen" style="background-color: var(--pw-bg); border-right: 1px solid var(--pw-border);">
|
|
152
|
+
|
|
153
|
+
<!-- Logo Area -->
|
|
154
|
+
<div class="flex items-center gap-3 px-5 py-5" style="border-bottom: 1px solid var(--pw-border-subtle);">
|
|
155
|
+
<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">
|
|
156
|
+
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
|
|
157
|
+
<path d="M9 12l2 2 4-4"/>
|
|
158
|
+
</svg>
|
|
159
|
+
<span class="truncate text-lg font-semibold tracking-tight" style="color: var(--pw-text-primary); font-family: var(--pw-font-mono);">PatchWarden</span>
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
<!-- Navigation -->
|
|
163
|
+
<nav class="flex-1 flex flex-col gap-1 px-3 pt-4 overflow-y-auto">
|
|
164
|
+
|
|
165
|
+
<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);">
|
|
166
|
+
<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">
|
|
167
|
+
<rect x="3" y="3" width="7" height="7" rx="1"/>
|
|
168
|
+
<rect x="14" y="3" width="7" height="7" rx="1"/>
|
|
169
|
+
<rect x="3" y="14" width="7" height="7" rx="1"/>
|
|
170
|
+
<rect x="14" y="14" width="7" height="7" rx="1"/>
|
|
171
|
+
</svg>
|
|
172
|
+
<span class="truncate" data-zh="控制台" data-en="Dashboard">控制台</span>
|
|
173
|
+
</a>
|
|
174
|
+
|
|
175
|
+
<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);">
|
|
176
|
+
<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">
|
|
177
|
+
<path d="M9 11l3 3L22 4"/>
|
|
178
|
+
<path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/>
|
|
179
|
+
</svg>
|
|
180
|
+
<span class="truncate" data-zh="任务面板" data-en="Tasks">任务面板</span>
|
|
181
|
+
</a>
|
|
182
|
+
|
|
183
|
+
<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);">
|
|
184
|
+
<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">
|
|
185
|
+
<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"/>
|
|
186
|
+
</svg>
|
|
187
|
+
<span class="truncate" data-zh="工作区" data-en="Workspace">工作区</span>
|
|
188
|
+
</a>
|
|
189
|
+
|
|
190
|
+
<a href="/pages/audit.html" data-nav-key="audit" 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);">
|
|
191
|
+
<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">
|
|
192
|
+
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
|
193
|
+
<polyline points="14 2 14 8 20 8"/>
|
|
194
|
+
<line x1="16" y1="13" x2="8" y2="13"/>
|
|
195
|
+
<line x1="16" y1="17" x2="8" y2="17"/>
|
|
196
|
+
<polyline points="10 9 9 9 8 9"/>
|
|
197
|
+
</svg>
|
|
198
|
+
<span class="truncate" data-zh="审计日志" data-en="Audit Log">审计日志</span>
|
|
199
|
+
</a>
|
|
200
|
+
|
|
201
|
+
<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);">
|
|
202
|
+
<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">
|
|
203
|
+
<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"/>
|
|
204
|
+
<line x1="4" y1="22" x2="4" y2="15"/>
|
|
205
|
+
</svg>
|
|
206
|
+
<span class="truncate" data-zh="Direct 会话" data-en="Direct Sessions">Direct 会话</span>
|
|
207
|
+
</a>
|
|
208
|
+
|
|
209
|
+
<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);">
|
|
210
|
+
<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">
|
|
211
|
+
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
|
212
|
+
<polyline points="14 2 14 8 20 8"/>
|
|
213
|
+
<line x1="16" y1="13" x2="8" y2="13"/>
|
|
214
|
+
<line x1="16" y1="17" x2="8" y2="17"/>
|
|
215
|
+
</svg>
|
|
216
|
+
<span class="truncate" data-zh="日志" data-en="Logs">日志</span>
|
|
217
|
+
</a>
|
|
218
|
+
|
|
219
|
+
</nav>
|
|
220
|
+
|
|
221
|
+
<!-- Bottom Section -->
|
|
222
|
+
<div class="px-5 py-4" style="border-top: 1px solid var(--pw-border-subtle);">
|
|
223
|
+
<div class="flex items-center gap-2 mb-3">
|
|
224
|
+
<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>
|
|
225
|
+
</div>
|
|
226
|
+
<div class="flex items-center gap-2">
|
|
227
|
+
<span class="shrink-0 w-2 h-2 rounded-full" style="background-color: var(--pw-state-success);"></span>
|
|
228
|
+
<span class="text-xs truncate" style="color: var(--pw-text-tertiary);">Connected</span>
|
|
229
|
+
</div>
|
|
230
|
+
</div>
|
|
231
|
+
|
|
232
|
+
</aside>
|
|
233
|
+
|
|
234
|
+
<!-- ========== MAIN AREA ========== -->
|
|
235
|
+
<div class="flex flex-col flex-1 min-w-0 h-screen overflow-hidden">
|
|
236
|
+
|
|
237
|
+
<!-- Top Bar -->
|
|
238
|
+
<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);">
|
|
239
|
+
<h1 class="text-lg font-semibold truncate min-w-0" style="color: var(--pw-text-primary); font-family: var(--pw-font-sans); text-wrap: balance; word-break: keep-all; overflow-wrap: break-word;" data-zh="审计日志" data-en="Audit Log">
|
|
240
|
+
审计日志
|
|
241
|
+
</h1>
|
|
242
|
+
|
|
243
|
+
<div class="flex items-center gap-3 shrink-0">
|
|
244
|
+
<!-- Refresh Button -->
|
|
245
|
+
<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);">
|
|
246
|
+
<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">
|
|
247
|
+
<polyline points="23 4 23 10 17 10"/>
|
|
248
|
+
<polyline points="1 20 1 14 7 14"/>
|
|
249
|
+
<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"/>
|
|
250
|
+
</svg>
|
|
251
|
+
<span data-zh="刷新" data-en="Refresh">刷新</span>
|
|
252
|
+
</button>
|
|
253
|
+
<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);">
|
|
254
|
+
PW
|
|
255
|
+
</div>
|
|
256
|
+
</div>
|
|
257
|
+
</header>
|
|
258
|
+
|
|
259
|
+
<!-- Page Content Area -->
|
|
260
|
+
<main class="flex-1 overflow-y-auto p-6" style="background-color: var(--pw-bg-elevated);">
|
|
261
|
+
|
|
262
|
+
<!-- ===== 1. Summary Stats Row ===== -->
|
|
263
|
+
<section class="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-6">
|
|
264
|
+
<!-- 总审计 -->
|
|
265
|
+
<div class="flex items-center gap-3 px-4 py-3 rounded-lg" style="background-color: var(--pw-bg-surface); border: 1px solid var(--pw-border);">
|
|
266
|
+
<div class="shrink-0 w-9 h-9 rounded-lg flex items-center justify-center" style="background-color: var(--pw-accent-subtle);">
|
|
267
|
+
<i data-lucide="file-check" class="w-4 h-4" style="color: var(--pw-accent);"></i>
|
|
268
|
+
</div>
|
|
269
|
+
<div class="min-w-0">
|
|
270
|
+
<div class="text-xs truncate" style="color: var(--pw-text-tertiary);" data-zh="总审计" data-en="Total Audits">总审计</div>
|
|
271
|
+
<div id="statTotal" class="text-xl font-bold whitespace-nowrap" style="color: var(--pw-text-primary); font-variant-numeric: tabular-nums;">--</div>
|
|
272
|
+
</div>
|
|
273
|
+
</div>
|
|
274
|
+
<!-- 通过 -->
|
|
275
|
+
<div class="flex items-center gap-3 px-4 py-3 rounded-lg" style="background-color: var(--pw-bg-surface); border-left: 3px solid var(--pw-state-success);">
|
|
276
|
+
<div class="shrink-0 w-9 h-9 rounded-lg flex items-center justify-center" style="background-color: var(--pw-state-success-bg);">
|
|
277
|
+
<i data-lucide="check-circle" class="w-4 h-4" style="color: var(--pw-state-success);"></i>
|
|
278
|
+
</div>
|
|
279
|
+
<div class="min-w-0">
|
|
280
|
+
<div class="text-xs truncate" style="color: var(--pw-text-tertiary);" data-zh="通过" data-en="Passed">通过</div>
|
|
281
|
+
<div id="statPass" class="text-xl font-bold whitespace-nowrap" style="color: var(--pw-state-success); font-variant-numeric: tabular-nums;">--</div>
|
|
282
|
+
</div>
|
|
283
|
+
</div>
|
|
284
|
+
<!-- 失败 -->
|
|
285
|
+
<div class="flex items-center gap-3 px-4 py-3 rounded-lg" style="background-color: var(--pw-bg-surface); border-left: 3px solid var(--pw-state-error);">
|
|
286
|
+
<div class="shrink-0 w-9 h-9 rounded-lg flex items-center justify-center" style="background-color: var(--pw-state-error-bg);">
|
|
287
|
+
<i data-lucide="x-circle" class="w-4 h-4" style="color: var(--pw-state-error);"></i>
|
|
288
|
+
</div>
|
|
289
|
+
<div class="min-w-0">
|
|
290
|
+
<div class="text-xs truncate" style="color: var(--pw-text-tertiary);" data-zh="失败" data-en="Failed">失败</div>
|
|
291
|
+
<div id="statFail" class="text-xl font-bold whitespace-nowrap" style="color: var(--pw-state-error); font-variant-numeric: tabular-nums;">--</div>
|
|
292
|
+
</div>
|
|
293
|
+
</div>
|
|
294
|
+
</section>
|
|
295
|
+
|
|
296
|
+
<!-- ===== 2. Audit Log Table ===== -->
|
|
297
|
+
<section class="rounded-lg overflow-hidden mb-6" style="background-color: var(--pw-bg-surface); border: 1px solid var(--pw-border); border-left: 3px solid var(--pw-accent);">
|
|
298
|
+
|
|
299
|
+
<!-- Loading State -->
|
|
300
|
+
<div id="loadingState" class="py-16 text-center">
|
|
301
|
+
<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">
|
|
302
|
+
<line x1="12" y1="2" x2="12" y2="6"/>
|
|
303
|
+
<line x1="12" y1="18" x2="12" y2="22"/>
|
|
304
|
+
<line x1="4.93" y1="4.93" x2="7.76" y2="7.76"/>
|
|
305
|
+
<line x1="16.24" y1="16.24" x2="19.07" y2="19.07"/>
|
|
306
|
+
<line x1="2" y1="12" x2="6" y2="12"/>
|
|
307
|
+
<line x1="18" y1="12" x2="22" y2="12"/>
|
|
308
|
+
<line x1="4.93" y1="19.07" x2="7.76" y2="16.24"/>
|
|
309
|
+
<line x1="16.24" y1="7.76" x2="19.07" y2="4.93"/>
|
|
310
|
+
</svg>
|
|
311
|
+
<p class="text-sm" style="color: var(--pw-text-tertiary); font-family: var(--pw-font-sans);" data-zh="加载中..." data-en="Loading...">加载中...</p>
|
|
312
|
+
</div>
|
|
313
|
+
|
|
314
|
+
<!-- Error State -->
|
|
315
|
+
<div id="errorState" class="hidden py-16 text-center">
|
|
316
|
+
<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">
|
|
317
|
+
<circle cx="12" cy="12" r="10"/>
|
|
318
|
+
<line x1="12" y1="8" x2="12" y2="12"/>
|
|
319
|
+
<line x1="12" y1="16" x2="12.01" y2="16"/>
|
|
320
|
+
</svg>
|
|
321
|
+
<p class="text-sm font-medium" style="color: var(--pw-state-error); font-family: var(--pw-font-sans);" data-zh="加载失败" data-en="Load Failed">加载失败</p>
|
|
322
|
+
<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>
|
|
323
|
+
<button id="retryBtn" class="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="Retry">重试</button>
|
|
324
|
+
</div>
|
|
325
|
+
|
|
326
|
+
<!-- Empty State -->
|
|
327
|
+
<div id="emptyState" class="hidden py-16 text-center">
|
|
328
|
+
<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">
|
|
329
|
+
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
|
330
|
+
<polyline points="14 2 14 8 20 8"/>
|
|
331
|
+
<line x1="9" y1="13" x2="15" y2="13"/>
|
|
332
|
+
</svg>
|
|
333
|
+
<p class="text-sm" style="color: var(--pw-text-tertiary); font-family: var(--pw-font-sans);" data-zh="暂无审计记录" data-en="No Audit Records">暂无审计记录</p>
|
|
334
|
+
</div>
|
|
335
|
+
|
|
336
|
+
<!-- Table -->
|
|
337
|
+
<div id="tableWrapper" class="hidden overflow-x-auto">
|
|
338
|
+
<table class="w-full text-sm" style="min-width: 640px;">
|
|
339
|
+
<thead>
|
|
340
|
+
<tr style="border-bottom: 1px solid var(--pw-border);">
|
|
341
|
+
<th class="px-4 py-3 text-left text-xs font-semibold whitespace-nowrap" style="color: var(--pw-text-tertiary);" data-zh="任务ID" data-en="Task ID">任务ID</th>
|
|
342
|
+
<th class="px-4 py-3 text-left text-xs font-semibold whitespace-nowrap" style="color: var(--pw-text-tertiary);" data-zh="裁定" data-en="Verdict">裁定</th>
|
|
343
|
+
<th class="px-4 py-3 text-left text-xs font-semibold whitespace-nowrap" style="color: var(--pw-text-tertiary);" data-zh="范围变更" data-en="Scope Changes">范围变更</th>
|
|
344
|
+
<th class="px-4 py-3 text-right text-xs font-semibold whitespace-nowrap" style="color: var(--pw-text-tertiary);" data-zh="审计时间" data-en="Audit Time">审计时间</th>
|
|
345
|
+
</tr>
|
|
346
|
+
</thead>
|
|
347
|
+
<tbody id="auditTableBody">
|
|
348
|
+
<!-- Rendered by JavaScript -->
|
|
349
|
+
</tbody>
|
|
350
|
+
</table>
|
|
351
|
+
</div>
|
|
352
|
+
</section>
|
|
353
|
+
|
|
354
|
+
<!-- ===== 3. Evidence Panel ===== -->
|
|
355
|
+
<section id="evidencePanel" class="hidden rounded-lg overflow-hidden" style="background-color: var(--pw-bg-surface); border: 1px solid var(--pw-border); border-left: 3px solid var(--pw-accent);">
|
|
356
|
+
<div class="px-5 py-4" style="border-bottom: 1px solid var(--pw-border-subtle);">
|
|
357
|
+
<div class="flex items-center gap-2 flex-wrap">
|
|
358
|
+
<i data-lucide="search" class="w-4 h-4 shrink-0" style="color: var(--pw-accent);"></i>
|
|
359
|
+
<h2 class="text-sm font-semibold" style="color: var(--pw-text-primary);" data-zh="审计证据" data-en="Audit Evidence">审计证据</h2>
|
|
360
|
+
<span id="evidenceTaskId" class="text-xs px-2 py-0.5 rounded-md whitespace-nowrap" style="font-family: var(--pw-font-mono); color: var(--pw-text-accent); background-color: var(--pw-accent-subtle);"></span>
|
|
361
|
+
</div>
|
|
362
|
+
</div>
|
|
363
|
+
<div class="px-5 py-4">
|
|
364
|
+
<pre id="evidenceJson" class="text-xs overflow-auto" style="font-family: var(--pw-font-mono); color: var(--pw-text-secondary); line-height: var(--pw-leading-relaxed); max-height: 480px; white-space: pre-wrap; word-break: break-word;"></pre>
|
|
365
|
+
</div>
|
|
366
|
+
</section>
|
|
367
|
+
|
|
368
|
+
</main>
|
|
369
|
+
|
|
370
|
+
</div>
|
|
371
|
+
|
|
372
|
+
<!-- Active nav item styling + sidebar hover states -->
|
|
373
|
+
<style>
|
|
374
|
+
[data-nav-key] {
|
|
375
|
+
transition: background-color var(--pw-transition-fast), color var(--pw-transition-fast);
|
|
376
|
+
}
|
|
377
|
+
[data-nav-key]:hover {
|
|
378
|
+
background-color: var(--pw-bg-hover);
|
|
379
|
+
color: var(--pw-text-primary);
|
|
380
|
+
}
|
|
381
|
+
[data-nav-key][data-active="true"] {
|
|
382
|
+
background-color: var(--pw-accent-subtle);
|
|
383
|
+
color: var(--pw-accent);
|
|
384
|
+
border-left: 3px solid var(--pw-accent);
|
|
385
|
+
padding-left: 9px;
|
|
386
|
+
}
|
|
387
|
+
input[type="text"]:focus {
|
|
388
|
+
border-color: var(--pw-border-focus);
|
|
389
|
+
background-color: var(--pw-bg-hover);
|
|
390
|
+
}
|
|
391
|
+
input[type="text"]::placeholder {
|
|
392
|
+
color: var(--pw-text-tertiary);
|
|
393
|
+
}
|
|
394
|
+
/* Table row hover */
|
|
395
|
+
tbody tr {
|
|
396
|
+
transition: background-color var(--pw-transition-fast);
|
|
397
|
+
}
|
|
398
|
+
tbody tr:hover {
|
|
399
|
+
background-color: var(--pw-bg-hover);
|
|
400
|
+
}
|
|
401
|
+
/* Reduced motion */
|
|
402
|
+
@media (prefers-reduced-motion: reduce) {
|
|
403
|
+
*, *::before, *::after {
|
|
404
|
+
transition-duration: 0.01ms !important;
|
|
405
|
+
animation-duration: 0.01ms !important;
|
|
406
|
+
}
|
|
407
|
+
.pw-spin {
|
|
408
|
+
animation-duration: 1.5s;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
/* Button hover states */
|
|
412
|
+
button:hover {
|
|
413
|
+
filter: brightness(1.1);
|
|
414
|
+
}
|
|
415
|
+
button:active {
|
|
416
|
+
filter: brightness(0.95);
|
|
417
|
+
transform: scale(0.98);
|
|
418
|
+
}
|
|
419
|
+
button:focus-visible {
|
|
420
|
+
outline: 2px solid var(--pw-border-focus);
|
|
421
|
+
outline-offset: 1px;
|
|
422
|
+
}
|
|
423
|
+
/* Refresh button hover */
|
|
424
|
+
#refreshBtn:hover {
|
|
425
|
+
background-color: var(--pw-bg-hover) !important;
|
|
426
|
+
color: var(--pw-text-primary) !important;
|
|
427
|
+
border-color: var(--pw-border-focus) !important;
|
|
428
|
+
}
|
|
429
|
+
/* Retry button hover */
|
|
430
|
+
#retryBtn:hover {
|
|
431
|
+
background-color: var(--pw-bg-active) !important;
|
|
432
|
+
color: var(--pw-text-primary) !important;
|
|
433
|
+
}
|
|
434
|
+
</style>
|
|
435
|
+
|
|
436
|
+
<script>
|
|
437
|
+
(function () {
|
|
438
|
+
'use strict';
|
|
439
|
+
|
|
440
|
+
var API_URL = '/api/audit';
|
|
441
|
+
|
|
442
|
+
var allAudits = [];
|
|
443
|
+
var selectedTaskId = null;
|
|
444
|
+
|
|
445
|
+
// ---- DOM helpers ----
|
|
446
|
+
function $(id) { return document.getElementById(id); }
|
|
447
|
+
function show(el) { el && el.classList.remove('hidden'); }
|
|
448
|
+
function hide(el) { el && el.classList.add('hidden'); }
|
|
449
|
+
|
|
450
|
+
function escapeHtml(s) {
|
|
451
|
+
if (s === null || s === undefined) return '';
|
|
452
|
+
return String(s).replace(/[&<>"']/g, function (c) {
|
|
453
|
+
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c];
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function escapeAttr(s) { return escapeHtml(s); }
|
|
458
|
+
|
|
459
|
+
function truncate(s, n) {
|
|
460
|
+
if (!s) return '';
|
|
461
|
+
s = String(s);
|
|
462
|
+
return s.length > n ? s.slice(0, n) + '…' : s;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function pad2(n) { return String(n).padStart(2, '0'); }
|
|
466
|
+
|
|
467
|
+
function formatTime(iso) {
|
|
468
|
+
if (!iso) return '—';
|
|
469
|
+
var d = new Date(iso);
|
|
470
|
+
if (isNaN(d.getTime())) return String(iso);
|
|
471
|
+
return d.getFullYear() + '-' + pad2(d.getMonth() + 1) + '-' + pad2(d.getDate())
|
|
472
|
+
+ ' ' + pad2(d.getHours()) + ':' + pad2(d.getMinutes());
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Resolve verdict from record. Real API spreads audit.json at top level;
|
|
476
|
+
// direct-session audits use "decision" instead of "verdict".
|
|
477
|
+
function resolveVerdict(record) {
|
|
478
|
+
if (!record) return '';
|
|
479
|
+
var v = record.verdict || record.decision || record.audited_verdict;
|
|
480
|
+
return v ? String(v).toLowerCase() : '';
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Resolve scope_changes. May be absent in real audit.json → returns ''.
|
|
484
|
+
function resolveScopeChanges(record) {
|
|
485
|
+
if (!record) return '';
|
|
486
|
+
var v = record.scope_changes;
|
|
487
|
+
if (v === undefined || v === null) {
|
|
488
|
+
// Some audit records nest scope info under checks; not reliable. Leave blank.
|
|
489
|
+
return '';
|
|
490
|
+
}
|
|
491
|
+
return String(v).toLowerCase();
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Resolve checked_at timestamp. Falls back to other common timestamp fields.
|
|
495
|
+
function resolveCheckedAt(record) {
|
|
496
|
+
if (!record) return '';
|
|
497
|
+
return record.checked_at || record.audited_at || record.timestamp || record.created_at || '';
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Determine the "details" payload for the evidence panel.
|
|
501
|
+
// Spec: details field. Real API: audit.json spread at top level (no wrapper).
|
|
502
|
+
// Prefer record.details when present; otherwise show the whole record.
|
|
503
|
+
function resolveDetails(record) {
|
|
504
|
+
if (!record) return null;
|
|
505
|
+
if (record.details !== undefined && record.details !== null) {
|
|
506
|
+
return record.details;
|
|
507
|
+
}
|
|
508
|
+
// Shallow copy without task_id to reduce noise; keep everything else.
|
|
509
|
+
var copy = {};
|
|
510
|
+
for (var k in record) {
|
|
511
|
+
if (Object.prototype.hasOwnProperty.call(record, k) && k !== 'task_id') {
|
|
512
|
+
copy[k] = record[k];
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
return copy;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// ---- State rendering ----
|
|
519
|
+
function showLoading() {
|
|
520
|
+
show($('loadingState'));
|
|
521
|
+
hide($('errorState'));
|
|
522
|
+
hide($('emptyState'));
|
|
523
|
+
hide($('tableWrapper'));
|
|
524
|
+
hide($('evidencePanel'));
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function showError(msg) {
|
|
528
|
+
hide($('loadingState'));
|
|
529
|
+
show($('errorState'));
|
|
530
|
+
hide($('emptyState'));
|
|
531
|
+
hide($('tableWrapper'));
|
|
532
|
+
hide($('evidencePanel'));
|
|
533
|
+
$('errorMessage').textContent = msg || '未知错误';
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function showEmpty() {
|
|
537
|
+
hide($('loadingState'));
|
|
538
|
+
hide($('errorState'));
|
|
539
|
+
show($('emptyState'));
|
|
540
|
+
hide($('tableWrapper'));
|
|
541
|
+
hide($('evidencePanel'));
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function showTable() {
|
|
545
|
+
hide($('loadingState'));
|
|
546
|
+
hide($('errorState'));
|
|
547
|
+
hide($('emptyState'));
|
|
548
|
+
show($('tableWrapper'));
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// ---- Badge rendering ----
|
|
552
|
+
// kind: 'verdict' | 'scope'
|
|
553
|
+
function badgeFor(value, kind) {
|
|
554
|
+
var v = String(value || '').toLowerCase();
|
|
555
|
+
var bg, color, border;
|
|
556
|
+
if (v === 'pass') {
|
|
557
|
+
bg = 'var(--pw-state-success-bg)';
|
|
558
|
+
color = 'var(--pw-state-success)';
|
|
559
|
+
border = 'var(--pw-state-success)';
|
|
560
|
+
} else if (v === 'fail') {
|
|
561
|
+
bg = 'var(--pw-state-error-bg)';
|
|
562
|
+
color = 'var(--pw-state-error)';
|
|
563
|
+
border = 'var(--pw-state-error)';
|
|
564
|
+
} else if (v === 'warn' || v === 'warning') {
|
|
565
|
+
bg = 'var(--pw-state-warning-bg)';
|
|
566
|
+
color = 'var(--pw-state-warning)';
|
|
567
|
+
border = 'var(--pw-state-warning)';
|
|
568
|
+
} else {
|
|
569
|
+
// Unknown / missing
|
|
570
|
+
bg = 'var(--pw-bg-hover)';
|
|
571
|
+
color = 'var(--pw-text-tertiary)';
|
|
572
|
+
border = 'var(--pw-border-subtle)';
|
|
573
|
+
}
|
|
574
|
+
var label = v || (kind === 'scope' ? '—' : '—');
|
|
575
|
+
return '<span class="inline-flex items-center px-2.5 py-0.5 rounded-md text-xs font-medium whitespace-nowrap" style="background-color: ' + bg + '; color: ' + color + '; border-left: 3px solid ' + border + ';">' + escapeHtml(label) + '</span>';
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// ---- Row rendering ----
|
|
579
|
+
function renderRow(record, idx) {
|
|
580
|
+
var taskId = record.task_id || '';
|
|
581
|
+
var truncatedId = truncate(taskId, 18);
|
|
582
|
+
var verdict = resolveVerdict(record);
|
|
583
|
+
var scope = resolveScopeChanges(record);
|
|
584
|
+
var checkedAt = resolveCheckedAt(record);
|
|
585
|
+
var altBg = (idx % 2 === 1) ? ' background-color: var(--pw-bg-hover);' : '';
|
|
586
|
+
var selectedCls = (taskId && taskId === selectedTaskId) ? ' pw-row-selected' : '';
|
|
587
|
+
|
|
588
|
+
var detailHref = '/pages/task-detail.html?id=' + encodeURIComponent(taskId);
|
|
589
|
+
|
|
590
|
+
return ''
|
|
591
|
+
+ '<tr class="cursor-pointer transition-colors' + selectedCls + '" style="border-bottom: 1px solid var(--pw-border-subtle);' + altBg + '" data-task-id="' + escapeAttr(taskId) + '">'
|
|
592
|
+
+ '<td class="px-4 py-3 whitespace-nowrap">'
|
|
593
|
+
+ '<a href="' + detailHref + '" class="inline-block hover:underline" style="font-family: var(--pw-font-mono); font-size: var(--pw-text-xs); color: var(--pw-text-accent);" title="' + escapeAttr(taskId) + '">' + escapeHtml(truncatedId) + '</a>'
|
|
594
|
+
+ '</td>'
|
|
595
|
+
+ '<td class="px-4 py-3 whitespace-nowrap">' + badgeFor(verdict, 'verdict') + '</td>'
|
|
596
|
+
+ '<td class="px-4 py-3 whitespace-nowrap">' + badgeFor(scope, 'scope') + '</td>'
|
|
597
|
+
+ '<td class="px-4 py-3 text-right whitespace-nowrap" style="font-family: var(--pw-font-mono); font-size: var(--pw-text-xs); color: var(--pw-text-tertiary);" title="' + escapeAttr(checkedAt) + '">' + escapeHtml(formatTime(checkedAt)) + '</td>'
|
|
598
|
+
+ '</tr>';
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function renderTable() {
|
|
602
|
+
if (allAudits.length === 0) {
|
|
603
|
+
showEmpty();
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
var html = '';
|
|
607
|
+
for (var i = 0; i < allAudits.length; i++) {
|
|
608
|
+
html += renderRow(allAudits[i], i);
|
|
609
|
+
}
|
|
610
|
+
$('auditTableBody').innerHTML = html;
|
|
611
|
+
showTable();
|
|
612
|
+
|
|
613
|
+
// Wire row click → show evidence
|
|
614
|
+
var rows = $('auditTableBody').querySelectorAll('tr[data-task-id]');
|
|
615
|
+
for (var r = 0; r < rows.length; r++) {
|
|
616
|
+
rows[r].addEventListener('click', handleRowClick);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function renderStats() {
|
|
621
|
+
var total = allAudits.length;
|
|
622
|
+
var passCount = 0;
|
|
623
|
+
var failCount = 0;
|
|
624
|
+
for (var i = 0; i < allAudits.length; i++) {
|
|
625
|
+
var v = resolveVerdict(allAudits[i]);
|
|
626
|
+
if (v === 'pass') passCount++;
|
|
627
|
+
else if (v === 'fail') failCount++;
|
|
628
|
+
}
|
|
629
|
+
$('statTotal').textContent = String(total);
|
|
630
|
+
$('statPass').textContent = String(passCount);
|
|
631
|
+
$('statFail').textContent = String(failCount);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function renderEvidence(record) {
|
|
635
|
+
var panel = $('evidencePanel');
|
|
636
|
+
if (!record) {
|
|
637
|
+
hide(panel);
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
$('evidenceTaskId').textContent = record.task_id || '—';
|
|
641
|
+
var details = resolveDetails(record);
|
|
642
|
+
var jsonText;
|
|
643
|
+
try {
|
|
644
|
+
jsonText = JSON.stringify(details, null, 2);
|
|
645
|
+
} catch (e) {
|
|
646
|
+
jsonText = String(details);
|
|
647
|
+
}
|
|
648
|
+
$('evidenceJson').textContent = jsonText;
|
|
649
|
+
show(panel);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function handleRowClick(e) {
|
|
653
|
+
// If the click originated on the <a> link, let it navigate; don't toggle evidence.
|
|
654
|
+
if (e && e.target && (e.target.tagName === 'A' || e.target.closest('a'))) {
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
var tr = e.currentTarget;
|
|
658
|
+
var taskId = tr.getAttribute('data-task-id') || '';
|
|
659
|
+
if (selectedTaskId && selectedTaskId === taskId) {
|
|
660
|
+
// Toggle off
|
|
661
|
+
selectedTaskId = null;
|
|
662
|
+
tr.classList.remove('pw-row-selected');
|
|
663
|
+
hide($('evidencePanel'));
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
// Clear previous selection
|
|
667
|
+
var prev = $('auditTableBody').querySelector('tr.pw-row-selected');
|
|
668
|
+
if (prev) prev.classList.remove('pw-row-selected');
|
|
669
|
+
selectedTaskId = taskId;
|
|
670
|
+
tr.classList.add('pw-row-selected');
|
|
671
|
+
// Find record
|
|
672
|
+
var record = null;
|
|
673
|
+
for (var i = 0; i < allAudits.length; i++) {
|
|
674
|
+
if (allAudits[i].task_id === taskId) {
|
|
675
|
+
record = allAudits[i];
|
|
676
|
+
break;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
renderEvidence(record);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// ---- API call ----
|
|
683
|
+
function setRefreshing(isRefreshing) {
|
|
684
|
+
var icon = $('refreshIcon');
|
|
685
|
+
if (!icon) return;
|
|
686
|
+
if (isRefreshing) {
|
|
687
|
+
icon.classList.add('pw-spin');
|
|
688
|
+
} else {
|
|
689
|
+
icon.classList.remove('pw-spin');
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function fetchAudits() {
|
|
694
|
+
showLoading();
|
|
695
|
+
setRefreshing(true);
|
|
696
|
+
fetch(API_URL, {
|
|
697
|
+
method: 'GET',
|
|
698
|
+
headers: { 'Accept': 'application/json' },
|
|
699
|
+
cache: 'no-store'
|
|
700
|
+
})
|
|
701
|
+
.then(function (res) {
|
|
702
|
+
if (!res.ok) {
|
|
703
|
+
return res.text().then(function (body) {
|
|
704
|
+
throw new Error('HTTP ' + res.status + (body ? (': ' + body.slice(0, 200)) : ''));
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
return res.json();
|
|
708
|
+
})
|
|
709
|
+
.then(function (data) {
|
|
710
|
+
allAudits = (data && Array.isArray(data.audits)) ? data.audits : [];
|
|
711
|
+
selectedTaskId = null;
|
|
712
|
+
renderStats();
|
|
713
|
+
renderTable();
|
|
714
|
+
})
|
|
715
|
+
.catch(function (err) {
|
|
716
|
+
var msg = (err && err.message) ? err.message : '未知错误';
|
|
717
|
+
if (err && err.name === 'TypeError' && /fetch/i.test(msg)) {
|
|
718
|
+
msg = '无法连接到 ' + API_URL + '(请确认 PatchWarden 服务正在运行)';
|
|
719
|
+
}
|
|
720
|
+
showError(msg);
|
|
721
|
+
})
|
|
722
|
+
.finally(function () {
|
|
723
|
+
setRefreshing(false);
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// ---- Event wiring ----
|
|
728
|
+
$('refreshBtn').addEventListener('click', fetchAudits);
|
|
729
|
+
|
|
730
|
+
var retryBtn = $('retryBtn');
|
|
731
|
+
if (retryBtn) {
|
|
732
|
+
retryBtn.addEventListener('click', fetchAudits);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// Initial load
|
|
736
|
+
fetchAudits();
|
|
737
|
+
})();
|
|
738
|
+
</script>
|
|
739
|
+
|
|
740
|
+
<!-- Lucide init -->
|
|
741
|
+
<script>lucide.createIcons();</script>
|
|
742
|
+
</body>
|
|
743
|
+
</html>
|