@winspan/claude-forge 8.11.1 → 8.13.0
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/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +78 -18
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/core/utils/error-handler.d.ts +45 -0
- package/dist/core/utils/error-handler.d.ts.map +1 -0
- package/dist/core/utils/error-handler.js +233 -0
- package/dist/core/utils/error-handler.js.map +1 -0
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +8 -1
- package/dist/web/server.js.map +1 -1
- package/dist/web/static/index.html +837 -869
- package/package.json +6 -2
|
@@ -3,996 +3,964 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
-
<title>Claude Forge
|
|
6
|
+
<title>Claude Forge 管理后台</title>
|
|
7
7
|
<script src="vendor/chart.umd.min.js"></script>
|
|
8
8
|
<style>
|
|
9
|
+
/* === Reset & Theme === */
|
|
9
10
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
10
11
|
:root {
|
|
11
|
-
--bg: #
|
|
12
|
-
--
|
|
13
|
-
--
|
|
14
|
-
--
|
|
15
|
-
--
|
|
16
|
-
--
|
|
17
|
-
--
|
|
12
|
+
--bg: #f7f8fa;
|
|
13
|
+
--bg-card: #ffffff;
|
|
14
|
+
--bg-hover: #f1f3f7;
|
|
15
|
+
--bg-secondary: #f7f8fa;
|
|
16
|
+
--border: #e4e7ec;
|
|
17
|
+
--border-hover: #cfd4dc;
|
|
18
|
+
--text: #111827;
|
|
19
|
+
--text-muted: #4b5563;
|
|
20
|
+
--text-dim: #9ca3af;
|
|
21
|
+
--primary: #4f46e5;
|
|
22
|
+
--primary-hover: #4338ca;
|
|
23
|
+
--primary-soft: #eef2ff;
|
|
24
|
+
--primary-border: #c7d2fe;
|
|
25
|
+
--green: #16a34a; --green-soft: #dcfce7;
|
|
26
|
+
--yellow: #d97706; --yellow-soft: #fef3c7;
|
|
27
|
+
--red: #dc2626; --red-soft: #fee2e2;
|
|
28
|
+
--blue: #2563eb; --blue-soft: #dbeafe;
|
|
29
|
+
--purple: #7c3aed; --purple-soft: #ede9fe;
|
|
30
|
+
--radius: 10px;
|
|
31
|
+
--radius-sm: 6px;
|
|
32
|
+
--shadow-sm: 0 1px 2px rgba(16,24,40,0.04);
|
|
33
|
+
--shadow: 0 1px 3px rgba(16,24,40,0.06), 0 1px 2px rgba(16,24,40,0.04);
|
|
34
|
+
--shadow-lg: 0 4px 12px rgba(16,24,40,0.08);
|
|
35
|
+
--sidebar-width: 240px;
|
|
36
|
+
--drawer-width: 520px;
|
|
18
37
|
}
|
|
19
|
-
body {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
.
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
.
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
.
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
.
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
.
|
|
73
|
-
.
|
|
74
|
-
|
|
75
|
-
|
|
38
|
+
html, body { height: 100%; }
|
|
39
|
+
body {
|
|
40
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', sans-serif;
|
|
41
|
+
background: var(--bg);
|
|
42
|
+
color: var(--text);
|
|
43
|
+
line-height: 1.55;
|
|
44
|
+
-webkit-font-smoothing: antialiased;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/* === Layout === */
|
|
48
|
+
.app { display: flex; min-height: 100vh; position: relative; }
|
|
49
|
+
|
|
50
|
+
/* Sidebar */
|
|
51
|
+
.sidebar {
|
|
52
|
+
width: var(--sidebar-width);
|
|
53
|
+
background: var(--bg-card);
|
|
54
|
+
border-right: 1px solid var(--border);
|
|
55
|
+
padding: 1.25rem 0.75rem;
|
|
56
|
+
position: fixed; top: 0; left: 0; bottom: 0;
|
|
57
|
+
display: flex; flex-direction: column;
|
|
58
|
+
overflow-y: auto;
|
|
59
|
+
z-index: 100;
|
|
60
|
+
}
|
|
61
|
+
.sidebar .brand {
|
|
62
|
+
display: flex; align-items: center; gap: 0.625rem;
|
|
63
|
+
padding: 0.5rem 0.75rem 1.25rem;
|
|
64
|
+
border-bottom: 1px solid var(--border);
|
|
65
|
+
margin-bottom: 1rem;
|
|
66
|
+
}
|
|
67
|
+
.sidebar .brand-icon {
|
|
68
|
+
width: 32px; height: 32px; border-radius: 8px;
|
|
69
|
+
background: linear-gradient(135deg, var(--primary), var(--purple));
|
|
70
|
+
display: flex; align-items: center; justify-content: center;
|
|
71
|
+
color: white; font-weight: 700; font-size: 0.95rem;
|
|
72
|
+
box-shadow: var(--shadow);
|
|
73
|
+
}
|
|
74
|
+
.sidebar .brand-text {
|
|
75
|
+
font-weight: 600; font-size: 0.95rem;
|
|
76
|
+
color: var(--text); letter-spacing: -0.01em;
|
|
77
|
+
}
|
|
78
|
+
.sidebar .nav-section-title {
|
|
79
|
+
font-size: 0.7rem; font-weight: 600; color: var(--text-dim);
|
|
80
|
+
text-transform: uppercase; letter-spacing: 0.06em;
|
|
81
|
+
padding: 0.75rem 0.875rem 0.375rem;
|
|
82
|
+
}
|
|
83
|
+
.sidebar a {
|
|
84
|
+
display: flex; align-items: center; gap: 0.625rem;
|
|
85
|
+
padding: 0.5rem 0.875rem; margin: 1px 0;
|
|
86
|
+
color: var(--text-muted); text-decoration: none;
|
|
87
|
+
border-radius: var(--radius-sm); cursor: pointer;
|
|
88
|
+
font-size: 0.875rem; font-weight: 500;
|
|
89
|
+
transition: background 0.15s, color 0.15s;
|
|
90
|
+
}
|
|
91
|
+
.sidebar a:hover { background: var(--bg-hover); color: var(--text); }
|
|
92
|
+
.sidebar a.active {
|
|
93
|
+
background: var(--primary-soft);
|
|
94
|
+
color: var(--primary);
|
|
95
|
+
}
|
|
96
|
+
.nav-icon { width: 18px; height: 18px; flex-shrink: 0; }
|
|
97
|
+
|
|
98
|
+
/* Main content */
|
|
99
|
+
.main {
|
|
100
|
+
flex: 1;
|
|
101
|
+
margin-left: var(--sidebar-width);
|
|
102
|
+
min-height: 100vh;
|
|
103
|
+
}
|
|
104
|
+
.topbar {
|
|
105
|
+
height: 60px;
|
|
106
|
+
background: var(--bg-card);
|
|
107
|
+
border-bottom: 1px solid var(--border);
|
|
108
|
+
display: flex;
|
|
109
|
+
align-items: center;
|
|
110
|
+
justify-content: space-between;
|
|
111
|
+
padding: 0 1.75rem;
|
|
112
|
+
position: sticky;
|
|
113
|
+
top: 0;
|
|
114
|
+
z-index: 50;
|
|
115
|
+
}
|
|
116
|
+
.topbar .page-title {
|
|
117
|
+
font-size: 1rem;
|
|
118
|
+
font-weight: 600;
|
|
119
|
+
color: var(--text);
|
|
120
|
+
}
|
|
121
|
+
.topbar .actions {
|
|
122
|
+
display: flex;
|
|
123
|
+
gap: 0.5rem;
|
|
124
|
+
align-items: center;
|
|
125
|
+
}
|
|
126
|
+
.container {
|
|
127
|
+
padding: 1.5rem 1.75rem;
|
|
128
|
+
max-width: 1400px;
|
|
129
|
+
}
|
|
130
|
+
.page {
|
|
131
|
+
display: none;
|
|
132
|
+
}
|
|
133
|
+
.page.active {
|
|
134
|
+
display: block;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/* Drawer (侧边抽屉) */
|
|
138
|
+
.drawer-overlay {
|
|
139
|
+
position: fixed;
|
|
140
|
+
top: 0;
|
|
141
|
+
left: 0;
|
|
142
|
+
right: 0;
|
|
143
|
+
bottom: 0;
|
|
144
|
+
background: rgba(0, 0, 0, 0.4);
|
|
145
|
+
z-index: 200;
|
|
146
|
+
opacity: 0;
|
|
147
|
+
visibility: hidden;
|
|
148
|
+
transition: opacity 0.2s, visibility 0.2s;
|
|
149
|
+
}
|
|
150
|
+
.drawer-overlay.active {
|
|
151
|
+
opacity: 1;
|
|
152
|
+
visibility: visible;
|
|
153
|
+
}
|
|
154
|
+
.drawer {
|
|
155
|
+
position: fixed;
|
|
156
|
+
top: 0;
|
|
157
|
+
right: 0;
|
|
158
|
+
bottom: 0;
|
|
159
|
+
width: var(--drawer-width);
|
|
160
|
+
background: var(--bg-card);
|
|
161
|
+
border-left: 1px solid var(--border);
|
|
162
|
+
box-shadow: var(--shadow-lg);
|
|
163
|
+
z-index: 201;
|
|
164
|
+
display: flex;
|
|
165
|
+
flex-direction: column;
|
|
166
|
+
transform: translateX(100%);
|
|
167
|
+
transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
|
168
|
+
}
|
|
169
|
+
.drawer.active {
|
|
170
|
+
transform: translateX(0);
|
|
171
|
+
}
|
|
172
|
+
.drawer-header {
|
|
173
|
+
display: flex;
|
|
174
|
+
align-items: center;
|
|
175
|
+
justify-content: space-between;
|
|
176
|
+
padding: 1.25rem 1.5rem;
|
|
177
|
+
border-bottom: 1px solid var(--border);
|
|
178
|
+
flex-shrink: 0;
|
|
179
|
+
}
|
|
180
|
+
.drawer-title {
|
|
181
|
+
font-size: 1rem;
|
|
182
|
+
font-weight: 600;
|
|
183
|
+
color: var(--text);
|
|
184
|
+
}
|
|
185
|
+
.drawer-close {
|
|
186
|
+
width: 32px; height: 32px;
|
|
187
|
+
border-radius: var(--radius-sm);
|
|
188
|
+
border: 1px solid var(--border);
|
|
189
|
+
background: var(--bg-card);
|
|
190
|
+
cursor: pointer;
|
|
191
|
+
display: flex; align-items: center; justify-content: center;
|
|
192
|
+
color: var(--text-muted);
|
|
193
|
+
transition: all 0.15s;
|
|
194
|
+
}
|
|
195
|
+
.drawer-close:hover {
|
|
196
|
+
background: var(--bg-hover);
|
|
197
|
+
color: var(--text);
|
|
198
|
+
}
|
|
199
|
+
.drawer-body {
|
|
200
|
+
flex: 1;
|
|
201
|
+
overflow-y: auto;
|
|
202
|
+
padding: 1.5rem;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/* === Typography === */
|
|
206
|
+
h1 { font-size: 1.375rem; font-weight: 600; margin-bottom: 1.25rem; letter-spacing: -0.02em; }
|
|
207
|
+
h2 { font-size: 1.05rem; font-weight: 600; letter-spacing: -0.01em; }
|
|
208
|
+
h3 { font-size: 0.9rem; font-weight: 600; }
|
|
209
|
+
|
|
210
|
+
/* === Stat cards === */
|
|
211
|
+
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 1rem; margin-bottom: 1.5rem; }
|
|
212
|
+
.stat-card {
|
|
213
|
+
background: var(--bg-card);
|
|
214
|
+
border: 1px solid var(--border);
|
|
215
|
+
border-radius: var(--radius);
|
|
216
|
+
padding: 1.125rem 1.25rem;
|
|
217
|
+
box-shadow: var(--shadow-sm);
|
|
218
|
+
}
|
|
219
|
+
.stat-card .label { font-size: 0.75rem; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.04em; font-weight: 500; }
|
|
220
|
+
.stat-card .value { font-size: 1.75rem; font-weight: 700; margin-top: 0.25rem; color: var(--text); letter-spacing: -0.02em; }
|
|
221
|
+
|
|
222
|
+
/* === Grid layouts === */
|
|
223
|
+
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 1.25rem; }
|
|
224
|
+
.grid-3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 1.25rem; }
|
|
225
|
+
@media (max-width: 900px) { .grid-2, .grid-3 { grid-template-columns: 1fr; } }
|
|
226
|
+
|
|
227
|
+
/* === Panel (white box) === */
|
|
228
|
+
.panel {
|
|
229
|
+
background: var(--bg-card);
|
|
230
|
+
border: 1px solid var(--border);
|
|
231
|
+
border-radius: var(--radius);
|
|
232
|
+
box-shadow: var(--shadow-sm);
|
|
233
|
+
overflow: hidden;
|
|
234
|
+
}
|
|
235
|
+
.panel-header {
|
|
236
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
237
|
+
padding: 1rem 1.25rem;
|
|
238
|
+
border-bottom: 1px solid var(--border);
|
|
239
|
+
}
|
|
240
|
+
.panel-title { font-size: 0.875rem; font-weight: 600; color: var(--text); }
|
|
241
|
+
.panel-body { padding: 1.25rem; }
|
|
242
|
+
|
|
243
|
+
/* === Table === */
|
|
244
|
+
table { width: 100%; border-collapse: collapse; }
|
|
245
|
+
th, td { padding: 0.75rem 1rem; text-align: left; border-bottom: 1px solid var(--border); }
|
|
246
|
+
th { background: var(--bg-secondary); color: var(--text-muted); font-weight: 600; font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.05em; }
|
|
247
|
+
td { font-size: 0.875rem; color: var(--text); }
|
|
248
|
+
tr:last-child td { border-bottom: none; }
|
|
249
|
+
tr:hover td { background: var(--bg-hover); cursor: pointer; }
|
|
250
|
+
|
|
251
|
+
/* === Badges === */
|
|
252
|
+
.badge { display: inline-flex; align-items: center; padding: 2px 8px; border-radius: 999px; font-size: 0.7rem; font-weight: 500; }
|
|
253
|
+
.badge-block { background: var(--red-soft); color: var(--red); }
|
|
254
|
+
.badge-warn { background: var(--yellow-soft); color: var(--yellow); }
|
|
255
|
+
.badge-allow { background: var(--green-soft); color: var(--green); }
|
|
256
|
+
.badge-info { background: var(--primary-soft); color: var(--primary); }
|
|
257
|
+
.badge-live { background: var(--red-soft); color: var(--red); animation: pulse 1.5s infinite; }
|
|
258
|
+
@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.5; } }
|
|
259
|
+
|
|
260
|
+
/* === Buttons === */
|
|
261
|
+
.btn { background: var(--bg-card); color: var(--text-muted); border: 1px solid var(--border); padding: 0.45rem 0.875rem; border-radius: var(--radius-sm); cursor: pointer; font-size: 0.8125rem; font-weight: 500; transition: all 0.15s; }
|
|
262
|
+
.btn:hover { border-color: var(--border-hover); color: var(--text); background: var(--bg-hover); }
|
|
263
|
+
.btn-primary { background: var(--primary); color: white; border-color: var(--primary); }
|
|
264
|
+
.btn-primary:hover { background: var(--primary-hover); border-color: var(--primary-hover); color: white; }
|
|
265
|
+
select.btn { appearance: none; padding-right: 1.75rem; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%234b5563' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 0.6rem center; }
|
|
266
|
+
|
|
267
|
+
/* === Search === */
|
|
268
|
+
.search-box { padding: 0.55rem 0.875rem; background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius-sm); color: var(--text); font-size: 0.875rem; transition: border-color 0.15s, box-shadow 0.15s; min-width: 240px; }
|
|
269
|
+
.search-box::placeholder { color: var(--text-dim); }
|
|
270
|
+
.search-box:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 3px rgba(79,70,229,0.12); }
|
|
271
|
+
|
|
272
|
+
/* === Toolbar (above tables/lists) === */
|
|
273
|
+
.toolbar { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 1rem; flex-wrap: wrap; }
|
|
274
|
+
|
|
275
|
+
/* === List items === */
|
|
276
|
+
.list-item {
|
|
277
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
278
|
+
padding: 0.875rem 1.25rem;
|
|
279
|
+
border-bottom: 1px solid var(--border);
|
|
280
|
+
cursor: pointer;
|
|
281
|
+
transition: background 0.12s;
|
|
282
|
+
}
|
|
283
|
+
.list-item:last-child { border-bottom: none; }
|
|
284
|
+
.list-item:hover { background: var(--bg-hover); }
|
|
285
|
+
.list-item.selected { background: var(--primary-soft); }
|
|
286
|
+
|
|
287
|
+
/* === Empty state === */
|
|
288
|
+
.empty { text-align: center; padding: 3rem 1rem; color: var(--text-dim); }
|
|
289
|
+
.empty-icon { font-size: 2rem; margin-bottom: 0.75rem; }
|
|
290
|
+
.empty-text { font-size: 0.875rem; }
|
|
291
|
+
|
|
292
|
+
/* === Loading === */
|
|
293
|
+
.loading { text-align: center; padding: 2rem; color: var(--text-dim); font-size: 0.875rem; }
|
|
294
|
+
|
|
295
|
+
/* === Tags === */
|
|
296
|
+
.tag { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 0.7rem; background: var(--bg-secondary); color: var(--text-muted); border: 1px solid var(--border); margin-right: 0.25rem; }
|
|
297
|
+
|
|
298
|
+
/* === Detail sections in drawer === */
|
|
299
|
+
.detail-section { margin-bottom: 1.5rem; }
|
|
300
|
+
.detail-label { font-size: 0.75rem; font-weight: 600; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.04em; margin-bottom: 0.375rem; }
|
|
301
|
+
.detail-value { font-size: 0.875rem; color: var(--text); }
|
|
302
|
+
pre.detail-code { font-family: ui-monospace, monospace; font-size: 0.8rem; background: var(--bg-secondary); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 0.75rem; overflow-x: auto; white-space: pre-wrap; }
|
|
303
|
+
|
|
304
|
+
/* === Live log === */
|
|
305
|
+
.live-log { font-family: ui-monospace, monospace; font-size: 0.8rem; max-height: 65vh; overflow-y: auto; background: #0f172a; color: #e2e8f0; border-radius: var(--radius); padding: 1rem; }
|
|
306
|
+
.live-log-line { padding: 2px 0; border-bottom: 1px solid #1e293b; }
|
|
307
|
+
|
|
308
|
+
/* === Chart container === */
|
|
309
|
+
.chart-wrap { position: relative; height: 220px; }
|
|
310
|
+
|
|
311
|
+
/* === Animations === */
|
|
312
|
+
@keyframes fadeIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } }
|
|
313
|
+
.fade-in { animation: fadeIn 0.2s ease-out; }
|
|
314
|
+
|
|
315
|
+
/* === Activity feed === */
|
|
316
|
+
.activity-item { display: flex; gap: 0.75rem; padding: 0.625rem 0; border-bottom: 1px solid var(--border); }
|
|
317
|
+
.activity-item:last-child { border-bottom: none; }
|
|
318
|
+
.activity-dot { width: 8px; height: 8px; border-radius: 50%; margin-top: 6px; flex-shrink: 0; }
|
|
319
|
+
.activity-dot-green { background: var(--green); }
|
|
320
|
+
.activity-dot-blue { background: var(--blue); }
|
|
321
|
+
.activity-dot-yellow { background: var(--yellow); }
|
|
322
|
+
.activity-dot-red { background: var(--red); }
|
|
323
|
+
.activity-content { flex: 1; min-width: 0; }
|
|
324
|
+
.activity-title { font-size: 0.8125rem; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
325
|
+
.activity-time { font-size: 0.75rem; color: var(--text-dim); margin-top: 1px; }
|
|
76
326
|
</style>
|
|
77
327
|
</head>
|
|
78
328
|
<body>
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
<
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
<a onclick="showPage('live')" id="nav-live">Live</a>
|
|
86
|
-
<a onclick="showPage('rules')" id="nav-rules">Rules</a>
|
|
87
|
-
</nav>
|
|
88
|
-
|
|
89
|
-
<div class="container">
|
|
90
|
-
<!-- Dashboard -->
|
|
91
|
-
<div id="page-dashboard" class="page active">
|
|
92
|
-
<h1>Dashboard</h1>
|
|
93
|
-
<div class="cards" id="status-cards"></div>
|
|
94
|
-
<h1 style="margin-top:2rem">Recent Sessions</h1>
|
|
95
|
-
<div id="sessions-list"></div>
|
|
329
|
+
<div class="app">
|
|
330
|
+
<!-- Sidebar -->
|
|
331
|
+
<aside class="sidebar">
|
|
332
|
+
<div class="brand">
|
|
333
|
+
<div class="brand-icon">CF</div>
|
|
334
|
+
<div class="brand-text">Claude Forge</div>
|
|
96
335
|
</div>
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
336
|
+
<div class="nav-section-title">总览</div>
|
|
337
|
+
<a onclick="nav('dashboard')" id="nav-dashboard" class="active">
|
|
338
|
+
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>
|
|
339
|
+
仪表盘
|
|
340
|
+
</a>
|
|
341
|
+
<div class="nav-section-title">活动</div>
|
|
342
|
+
<a onclick="nav('sessions')" id="nav-sessions">
|
|
343
|
+
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
|
|
344
|
+
会话
|
|
345
|
+
</a>
|
|
346
|
+
<a onclick="nav('events')" id="nav-events">
|
|
347
|
+
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>
|
|
348
|
+
事件
|
|
349
|
+
</a>
|
|
350
|
+
<a onclick="nav('injections')" id="nav-injections">
|
|
351
|
+
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>
|
|
352
|
+
注入
|
|
353
|
+
</a>
|
|
354
|
+
<a onclick="nav('live')" id="nav-live">
|
|
355
|
+
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="4"/></svg>
|
|
356
|
+
实时
|
|
357
|
+
</a>
|
|
358
|
+
<div class="nav-section-title">配置</div>
|
|
359
|
+
<a onclick="nav('rules')" id="nav-rules">
|
|
360
|
+
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg>
|
|
361
|
+
规则
|
|
362
|
+
</a>
|
|
363
|
+
</aside>
|
|
364
|
+
|
|
365
|
+
<!-- Main -->
|
|
366
|
+
<main class="main">
|
|
367
|
+
<div class="topbar">
|
|
368
|
+
<div class="page-title" id="topbar-title">仪表盘</div>
|
|
369
|
+
<div class="actions">
|
|
370
|
+
<span id="daemon-status" class="badge badge-allow" style="font-size:0.75rem">检查中...</span>
|
|
371
|
+
<button class="btn" onclick="refreshPage()">↻ 刷新</button>
|
|
105
372
|
</div>
|
|
106
|
-
<div id="session-detail"></div>
|
|
107
373
|
</div>
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
<div
|
|
112
|
-
<
|
|
113
|
-
<
|
|
374
|
+
<div class="container">
|
|
375
|
+
|
|
376
|
+
<!-- Dashboard -->
|
|
377
|
+
<div id="page-dashboard" class="page active">
|
|
378
|
+
<div class="cards" id="dash-stats"></div>
|
|
379
|
+
<div class="grid-2" style="margin-bottom:1.25rem">
|
|
380
|
+
<div class="panel">
|
|
381
|
+
<div class="panel-header">
|
|
382
|
+
<span class="panel-title">近 7 天活动</span>
|
|
383
|
+
</div>
|
|
384
|
+
<div class="panel-body"><div class="chart-wrap"><canvas id="chart-activity"></canvas></div></div>
|
|
385
|
+
</div>
|
|
386
|
+
<div class="panel">
|
|
387
|
+
<div class="panel-header">
|
|
388
|
+
<span class="panel-title">工具使用分布</span>
|
|
389
|
+
</div>
|
|
390
|
+
<div class="panel-body"><div class="chart-wrap"><canvas id="chart-tools"></canvas></div></div>
|
|
391
|
+
</div>
|
|
392
|
+
</div>
|
|
393
|
+
<div class="grid-2">
|
|
394
|
+
<div class="panel">
|
|
395
|
+
<div class="panel-header">
|
|
396
|
+
<span class="panel-title">最近会话</span>
|
|
397
|
+
<a onclick="nav('sessions')" style="font-size:0.8rem;color:var(--primary);cursor:pointer">查看全部 →</a>
|
|
398
|
+
</div>
|
|
399
|
+
<div id="dash-sessions"></div>
|
|
400
|
+
</div>
|
|
401
|
+
<div class="panel">
|
|
402
|
+
<div class="panel-header">
|
|
403
|
+
<span class="panel-title">最近活动</span>
|
|
404
|
+
</div>
|
|
405
|
+
<div class="panel-body" id="dash-activity"></div>
|
|
406
|
+
</div>
|
|
407
|
+
</div>
|
|
114
408
|
</div>
|
|
115
|
-
<div id="task-detail"></div>
|
|
116
|
-
</div>
|
|
117
409
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
<div class="chart-container">
|
|
123
|
-
<h3 style="margin-bottom:1rem">Tool Usage Distribution</h3>
|
|
124
|
-
<canvas id="chart-tool-usage"></canvas>
|
|
410
|
+
<!-- Sessions -->
|
|
411
|
+
<div id="page-sessions" class="page">
|
|
412
|
+
<div class="toolbar">
|
|
413
|
+
<input class="search-box" id="sessions-search" placeholder="搜索会话..." oninput="filterSessions()">
|
|
125
414
|
</div>
|
|
126
|
-
<div class="
|
|
127
|
-
<
|
|
128
|
-
<canvas id="chart-decision-levels"></canvas>
|
|
415
|
+
<div class="panel" id="sessions-panel">
|
|
416
|
+
<div id="sessions-list"></div>
|
|
129
417
|
</div>
|
|
130
418
|
</div>
|
|
131
|
-
<div class="chart-container">
|
|
132
|
-
<h3 style="margin-bottom:1rem">7-Day Activity Trend</h3>
|
|
133
|
-
<canvas id="chart-daily-activity"></canvas>
|
|
134
|
-
</div>
|
|
135
|
-
</div>
|
|
136
|
-
|
|
137
|
-
<!-- Events -->
|
|
138
|
-
<div id="page-events" class="page">
|
|
139
|
-
<h1>Events</h1>
|
|
140
|
-
<input type="text" class="search-box" id="events-search" placeholder="Search events by type, tool, or project..." onkeyup="filterEvents()">
|
|
141
|
-
<button class="refresh" onclick="loadEvents()">Refresh</button>
|
|
142
|
-
<table id="events-table">
|
|
143
|
-
<thead><tr><th>Time</th><th>Type</th><th>Tool</th><th>Project</th></tr></thead>
|
|
144
|
-
<tbody id="events-body"></tbody>
|
|
145
|
-
</table>
|
|
146
|
-
</div>
|
|
147
419
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
420
|
+
<!-- Events -->
|
|
421
|
+
<div id="page-events" class="page">
|
|
422
|
+
<div class="toolbar">
|
|
423
|
+
<input class="search-box" id="events-search" placeholder="按类型、工具、项目搜索..." oninput="filterEvents()">
|
|
424
|
+
<select class="btn" id="events-type" onchange="filterEvents()">
|
|
425
|
+
<option value="">全部类型</option>
|
|
426
|
+
<option value="PreToolUse">PreToolUse</option>
|
|
427
|
+
<option value="PostToolUse">PostToolUse</option>
|
|
428
|
+
<option value="UserPromptSubmit">UserPromptSubmit</option>
|
|
429
|
+
<option value="Stop">Stop</option>
|
|
430
|
+
</select>
|
|
431
|
+
</div>
|
|
432
|
+
<div class="panel">
|
|
433
|
+
<table>
|
|
434
|
+
<thead><tr><th>时间</th><th>类型</th><th>工具</th><th>项目</th></tr></thead>
|
|
435
|
+
<tbody id="events-body"></tbody>
|
|
436
|
+
</table>
|
|
437
|
+
</div>
|
|
438
|
+
</div>
|
|
154
439
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
440
|
+
<!-- Injections -->
|
|
441
|
+
<div id="page-injections" class="page">
|
|
442
|
+
<div class="toolbar">
|
|
443
|
+
<input class="search-box" id="inj-search" placeholder="搜索注入内容..." oninput="filterInjections()">
|
|
444
|
+
<select class="btn" id="inj-handler" onchange="filterInjections()">
|
|
445
|
+
<option value="">全部 Handler</option>
|
|
446
|
+
<option value="UserPromptSubmitHandler">UserPromptSubmit</option>
|
|
447
|
+
<option value="PreToolUseHandler">PreToolUse</option>
|
|
448
|
+
<option value="PostToolUseHandler">PostToolUse</option>
|
|
449
|
+
</select>
|
|
450
|
+
</div>
|
|
451
|
+
<div class="panel" id="inj-panel">
|
|
452
|
+
<div id="inj-list"></div>
|
|
453
|
+
</div>
|
|
160
454
|
</div>
|
|
161
|
-
<div id="rule-detail"></div>
|
|
162
|
-
</div>
|
|
163
455
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
<option value="UserPromptSubmitHandler">UserPromptSubmitHandler</option>
|
|
173
|
-
<option value="PreToolUseHandler">PreToolUseHandler</option>
|
|
174
|
-
<option value="PostToolUseHandler">PostToolUseHandler</option>
|
|
175
|
-
</select>
|
|
176
|
-
<button class="refresh" onclick="loadInjections()">Refresh</button>
|
|
456
|
+
<!-- Live -->
|
|
457
|
+
<div id="page-live" class="page">
|
|
458
|
+
<div class="toolbar">
|
|
459
|
+
<span id="live-status" class="badge badge-warn">未连接</span>
|
|
460
|
+
<button class="btn btn-primary" id="live-btn" onclick="toggleLive()">连接</button>
|
|
461
|
+
<button class="btn" onclick="document.getElementById('live-log').innerHTML=''">清空</button>
|
|
462
|
+
</div>
|
|
463
|
+
<div class="live-log" id="live-log"></div>
|
|
177
464
|
</div>
|
|
178
|
-
<div id="injections-list"></div>
|
|
179
|
-
</div>
|
|
180
465
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
<
|
|
187
|
-
|
|
466
|
+
<!-- Rules -->
|
|
467
|
+
<div id="page-rules" class="page">
|
|
468
|
+
<div class="toolbar">
|
|
469
|
+
<input class="search-box" id="rules-search" placeholder="搜索规则..." oninput="filterRules()">
|
|
470
|
+
</div>
|
|
471
|
+
<div class="panel" id="rules-panel">
|
|
472
|
+
<div id="rules-list"></div>
|
|
473
|
+
</div>
|
|
188
474
|
</div>
|
|
189
|
-
|
|
475
|
+
|
|
476
|
+
</div><!-- /container -->
|
|
477
|
+
</main>
|
|
478
|
+
|
|
479
|
+
<!-- Drawer overlay -->
|
|
480
|
+
<div class="drawer-overlay" id="drawer-overlay" onclick="closeDrawer()"></div>
|
|
481
|
+
|
|
482
|
+
<!-- Drawer -->
|
|
483
|
+
<div class="drawer" id="drawer">
|
|
484
|
+
<div class="drawer-header">
|
|
485
|
+
<div class="drawer-title" id="drawer-title">详情</div>
|
|
486
|
+
<button class="drawer-close" onclick="closeDrawer()">
|
|
487
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
|
488
|
+
</button>
|
|
190
489
|
</div>
|
|
490
|
+
<div class="drawer-body" id="drawer-body"></div>
|
|
191
491
|
</div>
|
|
192
492
|
|
|
493
|
+
</div><!-- /app -->
|
|
494
|
+
|
|
193
495
|
<script>
|
|
194
496
|
const API = '';
|
|
497
|
+
let allEvents = [], allSessions = [], allInjections = [], allRules = [];
|
|
498
|
+
let liveSource = null;
|
|
499
|
+
let charts = {};
|
|
195
500
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
function showPage(name) {
|
|
501
|
+
// === Navigation ===
|
|
502
|
+
function nav(page) {
|
|
503
|
+
document.querySelectorAll('.sidebar a').forEach(a => a.classList.remove('active'));
|
|
201
504
|
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
|
|
202
|
-
document.
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
if (
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
if (
|
|
210
|
-
if (
|
|
211
|
-
if (
|
|
212
|
-
if (
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
async function loadStatus() {
|
|
216
|
-
try {
|
|
217
|
-
const res = await fetch(API + '/api/status');
|
|
218
|
-
const data = await res.json();
|
|
219
|
-
const uptime = Math.floor(data.uptime);
|
|
220
|
-
const uptimeStr = uptime < 60 ? uptime + 's' : Math.floor(uptime/60) + 'm';
|
|
221
|
-
const memMB = Math.round(data.memory.heapUsed / 1024 / 1024);
|
|
222
|
-
document.getElementById('status-cards').innerHTML = `
|
|
223
|
-
<div class="card"><div class="label">PID</div><div class="value">${data.pid}</div></div>
|
|
224
|
-
<div class="card"><div class="label">Uptime</div><div class="value">${uptimeStr}</div></div>
|
|
225
|
-
<div class="card"><div class="label">Memory</div><div class="value">${memMB}MB</div></div>
|
|
226
|
-
<div class="card"><div class="label">Events</div><div class="value">${data.eventCount}</div></div>
|
|
227
|
-
`;
|
|
228
|
-
} catch { document.getElementById('status-cards').innerHTML = '<div class="empty">Daemon not running</div>'; }
|
|
505
|
+
const navEl = document.getElementById('nav-' + page);
|
|
506
|
+
const pageEl = document.getElementById('page-' + page);
|
|
507
|
+
if (navEl) navEl.classList.add('active');
|
|
508
|
+
if (pageEl) pageEl.classList.add('active');
|
|
509
|
+
const titles = { dashboard:'仪表盘', sessions:'会话', events:'事件', injections:'注入', live:'实时', rules:'规则' };
|
|
510
|
+
document.getElementById('topbar-title').textContent = titles[page] || page;
|
|
511
|
+
closeDrawer();
|
|
512
|
+
if (page === 'dashboard') loadDashboard();
|
|
513
|
+
else if (page === 'sessions') loadSessions();
|
|
514
|
+
else if (page === 'events') loadEvents();
|
|
515
|
+
else if (page === 'injections') loadInjections();
|
|
516
|
+
else if (page === 'rules') loadRules();
|
|
517
|
+
else if (page === 'live' && !liveSource) toggleLive();
|
|
229
518
|
}
|
|
230
519
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
const sessions = await res.json();
|
|
235
|
-
if (sessions.length === 0) {
|
|
236
|
-
document.getElementById('sessions-list').innerHTML = '<div class="empty">No sessions found</div>';
|
|
237
|
-
return;
|
|
238
|
-
}
|
|
239
|
-
document.getElementById('sessions-list').innerHTML = sessions.map(s => {
|
|
240
|
-
const shortId = s.session_id.slice(0, 8);
|
|
241
|
-
const start = parseTime(s.start_time).toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' });
|
|
242
|
-
const end = parseTime(s.end_time).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' });
|
|
243
|
-
const prompt = s.first_prompt ? s.first_prompt.slice(0, 60) : '(无提示词)';
|
|
244
|
-
return `<div class="session-card fade-in" onclick="loadSessionDetail('${s.session_id}')">
|
|
245
|
-
<div style="display:flex;justify-content:space-between;align-items:center">
|
|
246
|
-
<div>
|
|
247
|
-
<div style="font-size:0.75rem;color:var(--text-dim)">会话 ${shortId} · ${start} ~ ${end}</div>
|
|
248
|
-
<div style="margin-top:0.375rem;font-size:0.95rem;font-weight:500">${prompt}</div>
|
|
249
|
-
<div style="margin-top:0.25rem;font-size:0.75rem;color:var(--text-dim)">${s.event_count} events</div>
|
|
250
|
-
</div>
|
|
251
|
-
<span style="color:var(--accent);font-size:0.8rem">→</span>
|
|
252
|
-
</div>
|
|
253
|
-
</div>`;
|
|
254
|
-
}).join('');
|
|
255
|
-
} catch { document.getElementById('sessions-list').innerHTML = '<div class="empty">Failed to load</div>'; }
|
|
520
|
+
function refreshPage() {
|
|
521
|
+
const active = document.querySelector('.page.active');
|
|
522
|
+
if (active) nav(active.id.replace('page-', ''));
|
|
256
523
|
}
|
|
257
524
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
return;
|
|
265
|
-
}
|
|
266
|
-
document.getElementById('events-body').innerHTML = events.map(e => {
|
|
267
|
-
const time = parseTime(e.timestamp).toLocaleTimeString();
|
|
268
|
-
const badge = e.hook_type === 'PreToolUse' ? 'info' : e.hook_type === 'Stop' ? 'warn' : 'allow';
|
|
269
|
-
return `<tr>
|
|
270
|
-
<td>${time}</td>
|
|
271
|
-
<td><span class="badge badge-${badge}">${e.hook_type}</span></td>
|
|
272
|
-
<td>${e.tool_name || '—'}</td>
|
|
273
|
-
<td>${(e.project_path || '').split('/').pop()}</td>
|
|
274
|
-
</tr>`;
|
|
275
|
-
}).join('');
|
|
276
|
-
} catch { document.getElementById('events-body').innerHTML = '<tr><td colspan="4" class="empty">Failed to load</td></tr>'; }
|
|
525
|
+
// === Drawer ===
|
|
526
|
+
function openDrawer(title, html) {
|
|
527
|
+
document.getElementById('drawer-title').textContent = title;
|
|
528
|
+
document.getElementById('drawer-body').innerHTML = html;
|
|
529
|
+
document.getElementById('drawer').classList.add('active');
|
|
530
|
+
document.getElementById('drawer-overlay').classList.add('active');
|
|
277
531
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
const res = await fetch(API + '/api/rules');
|
|
282
|
-
const rules = await res.json();
|
|
283
|
-
const entries = Object.entries(rules);
|
|
284
|
-
if (entries.length === 0) {
|
|
285
|
-
document.getElementById('rules-list').innerHTML = '<div class="empty">No rules loaded</div>';
|
|
286
|
-
return;
|
|
287
|
-
}
|
|
288
|
-
document.getElementById('rules-list').innerHTML = entries.map(([id, conv]) => {
|
|
289
|
-
const c = conv;
|
|
290
|
-
const ruleCount = (c.rules || []).length;
|
|
291
|
-
return `<div class="card" style="margin-bottom:1rem">
|
|
292
|
-
<div class="label">${id} v${c.version || '?'}</div>
|
|
293
|
-
<div class="value" style="font-size:1rem">${c.name || id}</div>
|
|
294
|
-
<div style="margin-top:0.5rem;color:#94a3b8;font-size:0.875rem">${ruleCount} rules</div>
|
|
295
|
-
</div>`;
|
|
296
|
-
}).join('');
|
|
297
|
-
} catch { document.getElementById('rules-list').innerHTML = '<div class="empty">Failed to load</div>'; }
|
|
532
|
+
function closeDrawer() {
|
|
533
|
+
document.getElementById('drawer').classList.remove('active');
|
|
534
|
+
document.getElementById('drawer-overlay').classList.remove('active');
|
|
298
535
|
}
|
|
299
536
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
if (session) url += '&session=' + session;
|
|
306
|
-
if (handler) url += '&handler=' + handler;
|
|
307
|
-
const res = await fetch(url);
|
|
308
|
-
const injections = await res.json();
|
|
309
|
-
|
|
310
|
-
if (injections.length === 0) {
|
|
311
|
-
document.getElementById('injections-list').innerHTML = '<div class="empty">No injections found</div>';
|
|
312
|
-
return;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
const sessRes = await fetch(API + '/api/sessions?limit=10');
|
|
316
|
-
const sessions = await sessRes.json();
|
|
317
|
-
const sessSelect = document.getElementById('inj-session');
|
|
318
|
-
if (sessSelect.options.length === 1) {
|
|
319
|
-
sessions.forEach(s => {
|
|
320
|
-
const opt = document.createElement('option');
|
|
321
|
-
opt.value = s.session_id;
|
|
322
|
-
opt.textContent = s.session_id.slice(0,8) + ' - ' + (s.first_prompt || 'no prompt').slice(0,30);
|
|
323
|
-
sessSelect.appendChild(opt);
|
|
324
|
-
});
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
document.getElementById('injections-list').innerHTML = injections.map((inj, idx) => {
|
|
328
|
-
const time = parseTime(inj.timestamp).toLocaleString();
|
|
329
|
-
const icon = inj.injection_type === 'reason' ? '🚫' : '💉';
|
|
330
|
-
const typeColor = inj.injection_type === 'systemMessage' ? '#93c5fd' : inj.injection_type === 'reason' ? '#fca5a5' : '#fcd34d';
|
|
331
|
-
const preview = inj.content.slice(0, 200).replace(/\n/g, ' ');
|
|
332
|
-
const needExpand = inj.content.length > 200;
|
|
333
|
-
return `<div class="card" style="margin-bottom:0.5rem">
|
|
334
|
-
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.5rem">
|
|
335
|
-
<span style="font-size:0.75rem;color:#64748b">${time}</span>
|
|
336
|
-
<span class="badge badge-info">${inj.source_handler}</span>
|
|
337
|
-
</div>
|
|
338
|
-
<div style="margin-bottom:0.5rem">
|
|
339
|
-
<span style="font-size:0.875rem;color:${typeColor}">${icon} ${inj.injection_type}</span>
|
|
340
|
-
<span style="font-size:0.75rem;color:#64748b;margin-left:0.5rem">${inj.content.length} chars</span>
|
|
341
|
-
${needExpand ? `<button class="refresh" style="margin-left:0.5rem;padding:2px 8px;font-size:0.75rem" onclick="toggleInj(${idx})">展开</button>` : ''}
|
|
342
|
-
</div>
|
|
343
|
-
<div id="inj-${idx}" style="font-size:0.875rem;color:#94a3b8;font-family:monospace;white-space:pre-wrap;max-height:${needExpand ? '3rem' : 'none'};overflow:hidden">${inj.content}</div>
|
|
344
|
-
</div>`;
|
|
345
|
-
}).join('');
|
|
346
|
-
} catch { document.getElementById('injections-list').innerHTML = '<div class="empty">Failed to load</div>'; }
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
function toggleInj(idx) {
|
|
350
|
-
const el = document.getElementById('inj-' + idx);
|
|
351
|
-
const btn = event.target;
|
|
352
|
-
if (el.style.maxHeight === 'none') {
|
|
353
|
-
el.style.maxHeight = '3rem';
|
|
354
|
-
btn.textContent = '展开';
|
|
355
|
-
} else {
|
|
356
|
-
el.style.maxHeight = 'none';
|
|
357
|
-
btn.textContent = '收起';
|
|
358
|
-
}
|
|
537
|
+
// === Helpers ===
|
|
538
|
+
function fmt(ts) {
|
|
539
|
+
if (!ts) return '—';
|
|
540
|
+
const d = new Date(String(ts).endsWith('Z') ? ts : ts + 'Z');
|
|
541
|
+
return d.toLocaleString('zh-CN', { month:'2-digit', day:'2-digit', hour:'2-digit', minute:'2-digit' });
|
|
359
542
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
let timelineData = [];
|
|
365
|
-
let timelineHasMore = false;
|
|
366
|
-
|
|
367
|
-
async function loadSessionDetail(sessionId) {
|
|
368
|
-
currentSessionId = sessionId;
|
|
369
|
-
timelineOffset = 0;
|
|
370
|
-
timelineData = [];
|
|
371
|
-
if (currentView === 'timeline') {
|
|
372
|
-
await loadSessionTimeline(sessionId);
|
|
373
|
-
} else {
|
|
374
|
-
await loadSessionTasks(sessionId);
|
|
375
|
-
}
|
|
543
|
+
function fmtTime(ts) {
|
|
544
|
+
if (!ts) return '—';
|
|
545
|
+
const d = new Date(String(ts).endsWith('Z') ? ts : ts + 'Z');
|
|
546
|
+
return d.toLocaleTimeString('zh-CN', { hour:'2-digit', minute:'2-digit', second:'2-digit' });
|
|
376
547
|
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
document.getElementById('current-view-label').textContent = currentView === 'timeline' ? '时间线视图' : '任务视图';
|
|
381
|
-
if (currentSessionId) loadSessionDetail(currentSessionId);
|
|
548
|
+
function badgeHook(type) {
|
|
549
|
+
const m = { PreToolUse:'badge-info', PostToolUse:'badge-allow', UserPromptSubmit:'badge-warn', Stop:'badge-warn' };
|
|
550
|
+
return `<span class="badge ${m[type]||'badge-info'}">${type||'—'}</span>`;
|
|
382
551
|
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
showPage('session-detail');
|
|
386
|
-
if (!append) {
|
|
387
|
-
document.getElementById('session-detail').innerHTML = '<div class="empty">Loading...</div>';
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
try {
|
|
391
|
-
const res = await fetch(API + '/api/sessions/' + sessionId + '/timeline?limit=50&offset=' + timelineOffset);
|
|
392
|
-
const data = await res.json();
|
|
393
|
-
|
|
394
|
-
if (!append) {
|
|
395
|
-
timelineData = data.timeline;
|
|
396
|
-
} else {
|
|
397
|
-
timelineData = timelineData.concat(data.timeline);
|
|
398
|
-
}
|
|
399
|
-
timelineHasMore = data.hasMore;
|
|
400
|
-
timelineOffset = data.offset + data.limit;
|
|
401
|
-
|
|
402
|
-
renderTimeline(data.session, timelineData, data.summary, timelineHasMore);
|
|
403
|
-
} catch (err) {
|
|
404
|
-
document.getElementById('session-detail').innerHTML = '<div class="empty">Failed to load: ' + err.message + '</div>';
|
|
405
|
-
}
|
|
552
|
+
function empty(msg) {
|
|
553
|
+
return `<div class="empty"><div class="empty-icon">📭</div><div class="empty-text">${msg}</div></div>`;
|
|
406
554
|
}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
await loadSessionTimeline(currentSessionId, true);
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
function renderTimeline(session, timeline, summary, hasMore) {
|
|
413
|
-
const shortId = session.session_id.slice(0, 8);
|
|
414
|
-
const start = parseTime(session.start_time).toLocaleString();
|
|
415
|
-
const end = parseTime(session.end_time).toLocaleString();
|
|
416
|
-
|
|
417
|
-
let html = `
|
|
418
|
-
<div class="card fade-in" style="margin-bottom:1.5rem">
|
|
419
|
-
<h2 style="margin-bottom:0.5rem">会话 ${shortId}</h2>
|
|
420
|
-
<div style="color:var(--text-muted)">${session.first_prompt || '(无提示词)'}</div>
|
|
421
|
-
<div style="margin-top:0.5rem;font-size:0.8rem;color:var(--text-dim)">${start} ~ ${end} · ${timeline.length} events loaded</div>
|
|
422
|
-
</div>
|
|
423
|
-
`;
|
|
424
|
-
|
|
425
|
-
timeline.forEach((item, idx) => {
|
|
426
|
-
const time = parseTime(item.timestamp).toLocaleTimeString();
|
|
427
|
-
|
|
428
|
-
if (item.type === 'user_input') {
|
|
429
|
-
html += `<div class="card fade-in" style="margin-bottom:0.75rem;border-left:3px solid var(--accent)">
|
|
430
|
-
<div style="font-size:0.75rem;color:var(--text-dim);margin-bottom:0.5rem">${time} 👤 用户输入</div>
|
|
431
|
-
<div style="font-size:0.95rem">"${item.data.content}"</div>
|
|
432
|
-
</div>`;
|
|
433
|
-
} else if (item.type === 'injection') {
|
|
434
|
-
const preview = item.data.content.slice(0, 100);
|
|
435
|
-
html += `<div class="card fade-in" style="margin-bottom:0.75rem;border-left:3px solid var(--purple)">
|
|
436
|
-
<div style="font-size:0.75rem;color:var(--text-dim);margin-bottom:0.5rem">${time} 💉 ${item.data.source} (${item.data.injection_type}, ${item.data.length} chars)</div>
|
|
437
|
-
<div style="font-size:0.85rem;color:var(--text-muted)">${preview}${item.data.truncated ? '...' : ''}</div>
|
|
438
|
-
</div>`;
|
|
439
|
-
} else if (item.type === 'tool_call') {
|
|
440
|
-
const tool = item.data.tool;
|
|
441
|
-
const input = item.data.input || {};
|
|
442
|
-
const ok = item.data.success ? '✓' : '✗';
|
|
443
|
-
let desc = '';
|
|
444
|
-
if (tool === 'Bash') desc = (input.command || '').slice(0, 80);
|
|
445
|
-
else if (tool === 'Edit' || tool === 'Write' || tool === 'Read') desc = input.file_path || '';
|
|
446
|
-
else desc = JSON.stringify(input).slice(0, 80);
|
|
447
|
-
|
|
448
|
-
html += `<div class="card fade-in" style="margin-bottom:0.75rem;border-left:3px solid ${ok === '✓' ? 'var(--green)' : 'var(--red)'}">
|
|
449
|
-
<div style="font-size:0.75rem;color:var(--text-dim);margin-bottom:0.5rem">${time} 🔧 ${tool} ${ok}</div>
|
|
450
|
-
<div style="font-size:0.85rem;color:var(--text-muted);font-family:monospace">${desc}</div>
|
|
451
|
-
</div>`;
|
|
452
|
-
} else if (item.type === 'quality_check') {
|
|
453
|
-
html += `<div class="card fade-in" style="margin-bottom:0.75rem;border-left:3px solid var(--yellow)">
|
|
454
|
-
<div style="font-size:0.75rem;color:var(--text-dim);margin-bottom:0.5rem">${time} ⚠️ 质量检查</div>
|
|
455
|
-
<div style="font-size:0.85rem">${JSON.stringify(item.data).slice(0, 150)}</div>
|
|
456
|
-
</div>`;
|
|
457
|
-
}
|
|
458
|
-
});
|
|
459
|
-
|
|
460
|
-
if (hasMore) {
|
|
461
|
-
html += `<div style="text-align:center;margin:1.5rem 0">
|
|
462
|
-
<button class="btn btn-primary" onclick="loadMoreTimeline()">加载更多</button>
|
|
463
|
-
</div>`;
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
// Summary
|
|
467
|
-
html += `<div class="card fade-in" style="margin-top:1.5rem;border:2px solid var(--accent)">
|
|
468
|
-
<h3 style="margin-bottom:1rem">📊 任务总结</h3>
|
|
469
|
-
${summary.commits.length > 0 ? `<div style="margin-bottom:0.75rem">
|
|
470
|
-
<div style="font-weight:600;margin-bottom:0.5rem">✅ 产出 (${summary.commits.length} commits)</div>
|
|
471
|
-
${summary.commits.map(c => `<div style="margin-left:1rem;font-size:0.85rem;color:var(--green)">- ${c.message}</div>`).join('')}
|
|
472
|
-
</div>` : ''}
|
|
473
|
-
${summary.filesChanged.length > 0 ? `<div style="margin-bottom:0.75rem">
|
|
474
|
-
<div style="font-weight:600;margin-bottom:0.5rem">✅ 修改文件 (${summary.filesChanged.length} 个)</div>
|
|
475
|
-
${summary.filesChanged.slice(0, 10).map(f => `<div style="margin-left:1rem;font-size:0.85rem;color:var(--text-muted)">- ${f}</div>`).join('')}
|
|
476
|
-
${summary.filesChanged.length > 10 ? `<div style="margin-left:1rem;font-size:0.75rem;color:var(--text-dim)">... 还有 ${summary.filesChanged.length - 10} 个</div>` : ''}
|
|
477
|
-
</div>` : ''}
|
|
478
|
-
${summary.unresolvedIssues.length > 0 ? `<div>
|
|
479
|
-
<div style="font-weight:600;margin-bottom:0.5rem">⚠️ 未解决质量问题 (${summary.unresolvedIssues.length} 个)</div>
|
|
480
|
-
${summary.unresolvedIssues.map(q => `<div style="margin-left:1rem;font-size:0.85rem;color:var(--red)">- [${q.level}/${q.category}] ${q.message}</div>`).join('')}
|
|
481
|
-
</div>` : '<div style="color:var(--green)">✅ 无未解决问题</div>'}
|
|
482
|
-
</div>`;
|
|
483
|
-
|
|
484
|
-
document.getElementById('session-detail').innerHTML = html;
|
|
555
|
+
function loading() {
|
|
556
|
+
return `<div class="loading">加载中...</div>`;
|
|
485
557
|
}
|
|
486
558
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
showPage('session-detail');
|
|
491
|
-
document.getElementById('session-detail').innerHTML = '<div class="empty">Loading...</div>';
|
|
492
|
-
|
|
559
|
+
// === Dashboard ===
|
|
560
|
+
async function loadDashboard() {
|
|
561
|
+
document.getElementById('dash-stats').innerHTML = loading();
|
|
493
562
|
try {
|
|
494
|
-
const
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
const
|
|
499
|
-
const
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
</div>
|
|
563
|
+
const [statusRes, statsRes] = await Promise.all([
|
|
564
|
+
fetch(API + '/api/status'),
|
|
565
|
+
fetch(API + '/api/stats')
|
|
566
|
+
]);
|
|
567
|
+
const status = await statusRes.json();
|
|
568
|
+
const stats = await statsRes.json();
|
|
569
|
+
|
|
570
|
+
// Stat cards
|
|
571
|
+
const uptime = Math.floor(status.uptime);
|
|
572
|
+
const uptimeStr = uptime < 60 ? uptime + ' 秒' : Math.floor(uptime/60) + ' 分钟';
|
|
573
|
+
const mem = Math.round(status.memory.heapUsed / 1024 / 1024);
|
|
574
|
+
document.getElementById('dash-stats').innerHTML = `
|
|
575
|
+
<div class="stat-card"><div class="label">进程 ID</div><div class="value">${status.pid}</div></div>
|
|
576
|
+
<div class="stat-card"><div class="label">运行时长</div><div class="value">${uptimeStr}</div></div>
|
|
577
|
+
<div class="stat-card"><div class="label">内存占用</div><div class="value">${mem}MB</div></div>
|
|
578
|
+
<div class="stat-card"><div class="label">事件总数</div><div class="value">${status.eventCount}</div></div>
|
|
508
579
|
`;
|
|
580
|
+
document.getElementById('daemon-status').textContent = '运行中';
|
|
581
|
+
document.getElementById('daemon-status').className = 'badge badge-allow';
|
|
509
582
|
|
|
510
|
-
//
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
html += `<div class="session-card fade-in" onclick="loadTaskDetail(${idx})" style="cursor:pointer">
|
|
518
|
-
<div style="display:flex;justify-content:space-between;align-items:start">
|
|
519
|
-
<div style="flex:1">
|
|
520
|
-
<h3 style="margin-bottom:0.5rem">任务 ${idx + 1}: ${task.title}</h3>
|
|
521
|
-
<div style="font-size:0.8rem;color:var(--text-muted);margin-bottom:0.5rem">${taskStart} ~ ${taskEnd}</div>
|
|
522
|
-
<div style="font-size:0.75rem;color:var(--text-dim)">
|
|
523
|
-
📝 ${task.prompts.length} 次输入 ·
|
|
524
|
-
💉 ${task.injections.length} 次注入 ·
|
|
525
|
-
🔧 ${toolCount} 次工具调用 ·
|
|
526
|
-
📁 ${task.summary.filesChanged.length} 个文件
|
|
527
|
-
${commitCount > 0 ? ` · 📦 ${commitCount} commits` : ''}
|
|
528
|
-
${task.qualityIssues.length > 0 ? ` · ⚠️ ${task.qualityIssues.length} 个问题` : ''}
|
|
529
|
-
</div>
|
|
530
|
-
</div>
|
|
531
|
-
<div style="font-size:1.5rem">→</div>
|
|
532
|
-
</div>
|
|
533
|
-
</div>`;
|
|
534
|
-
});
|
|
535
|
-
|
|
536
|
-
document.getElementById('session-detail').innerHTML = html;
|
|
537
|
-
} catch (err) {
|
|
538
|
-
document.getElementById('session-detail').innerHTML = '<div class="empty">Failed to load: ' + err.message + '</div>';
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
function loadTaskDetail(taskIdx) {
|
|
543
|
-
if (!sessionData || !sessionData.tasks[taskIdx]) return;
|
|
544
|
-
|
|
545
|
-
showPage('task-detail');
|
|
546
|
-
const task = sessionData.tasks[taskIdx];
|
|
547
|
-
|
|
548
|
-
document.getElementById('task-detail-title').textContent = `任务 ${taskIdx + 1}: ${task.title}`;
|
|
549
|
-
|
|
550
|
-
const taskStart = parseTime(task.start_time).toLocaleString();
|
|
551
|
-
const taskEnd = task.end_time ? parseTime(task.end_time).toLocaleString() : 'ongoing';
|
|
552
|
-
const toolList = Object.entries(task.summary.toolUsage).map(([k, v]) => `${k}×${v}`).join(', ');
|
|
553
|
-
|
|
554
|
-
let html = `
|
|
555
|
-
<div class="card fade-in" style="margin-bottom:1.5rem">
|
|
556
|
-
<div style="font-size:0.85rem;color:var(--text-dim)">${taskStart} ~ ${taskEnd}</div>
|
|
557
|
-
<div style="margin-top:0.5rem;font-size:0.85rem">
|
|
558
|
-
${task.event_count} events · ${task.summary.filesChanged.length} files changed
|
|
559
|
-
</div>
|
|
560
|
-
</div>
|
|
561
|
-
`;
|
|
562
|
-
|
|
563
|
-
// User inputs
|
|
564
|
-
html += `<div class="card fade-in" style="margin-bottom:1rem">
|
|
565
|
-
<h3 style="margin-bottom:1rem">📝 用户输入 (${task.prompts.length} 次)</h3>
|
|
566
|
-
${task.prompts.map((p, pidx) => `
|
|
567
|
-
<div style="margin-bottom:1rem;padding:0.75rem;background:var(--bg-secondary);border-radius:6px">
|
|
568
|
-
<div style="font-size:0.75rem;color:var(--text-dim);margin-bottom:0.5rem">${parseTime(p.timestamp).toLocaleTimeString()}</div>
|
|
569
|
-
<div style="white-space:pre-wrap">${p.content}</div>
|
|
570
|
-
</div>
|
|
571
|
-
`).join('')}
|
|
572
|
-
</div>`;
|
|
573
|
-
|
|
574
|
-
// Injections
|
|
575
|
-
html += `<div class="card fade-in" style="margin-bottom:1rem">
|
|
576
|
-
<h3 style="margin-bottom:1rem">💉 forge 注入 (${task.injections.length} 次)</h3>
|
|
577
|
-
${task.injections.map((inj, iidx) => `
|
|
578
|
-
<div style="margin-bottom:1rem">
|
|
579
|
-
<div style="font-size:0.85rem;font-weight:600;margin-bottom:0.5rem">
|
|
580
|
-
${parseTime(inj.timestamp).toLocaleTimeString()} - [${inj.source_handler}] ${inj.injection_type}
|
|
581
|
-
</div>
|
|
582
|
-
<pre style="font-size:0.75rem;background:var(--bg-secondary);padding:0.75rem;border-radius:6px;white-space:pre-wrap;max-height:400px;overflow-y:auto">${inj.content}</pre>
|
|
583
|
-
</div>
|
|
584
|
-
`).join('')}
|
|
585
|
-
</div>`;
|
|
586
|
-
|
|
587
|
-
// Tool executions
|
|
588
|
-
const toolEvents = task.events.filter(e => e.tool_name);
|
|
589
|
-
html += `<div class="card fade-in" style="margin-bottom:1rem">
|
|
590
|
-
<h3 style="margin-bottom:0.5rem">⚙️ 工具执行 (${toolEvents.length} 次)</h3>
|
|
591
|
-
<div style="font-size:0.85rem;color:var(--text-muted);margin-bottom:1rem">统计: ${toolList || 'none'}</div>
|
|
592
|
-
${toolEvents.map((ev, eidx) => {
|
|
593
|
-
const input = ev.tool_input || {};
|
|
594
|
-
let summary = '';
|
|
595
|
-
if (ev.tool_name === 'Bash') summary = (input.command || '').slice(0, 100);
|
|
596
|
-
else if (ev.tool_name === 'Edit' || ev.tool_name === 'Write') summary = `file: ${input.file_path || ''}`;
|
|
597
|
-
else if (ev.tool_name === 'Read') summary = `file: ${input.file_path || ''}`;
|
|
598
|
-
else summary = JSON.stringify(input).slice(0, 100);
|
|
599
|
-
|
|
600
|
-
return `<div style="margin-bottom:1rem;padding:0.75rem;background:var(--bg-secondary);border-radius:6px">
|
|
601
|
-
<div style="font-size:0.75rem;color:var(--text-dim);margin-bottom:0.5rem">
|
|
602
|
-
${parseTime(ev.timestamp).toLocaleTimeString()} - 🔧 ${ev.tool_name}
|
|
603
|
-
</div>
|
|
604
|
-
<div style="font-size:0.85rem;margin-bottom:0.5rem;font-family:monospace">${summary}</div>
|
|
605
|
-
<details style="margin-top:0.5rem">
|
|
606
|
-
<summary style="cursor:pointer;font-size:0.75rem;color:var(--accent)">查看完整 input/output</summary>
|
|
607
|
-
<pre style="margin-top:0.5rem;font-size:0.7rem;background:var(--bg);padding:0.5rem;border-radius:4px;overflow-x:auto;max-height:300px;overflow-y:auto">${JSON.stringify({input: ev.tool_input, output: ev.tool_output}, null, 2)}</pre>
|
|
608
|
-
</details>
|
|
609
|
-
</div>`;
|
|
610
|
-
}).join('')}
|
|
611
|
-
</div>`;
|
|
612
|
-
|
|
613
|
-
// Files changed
|
|
614
|
-
if (task.summary.filesChanged.length > 0) {
|
|
615
|
-
html += `<div class="card fade-in" style="margin-bottom:1rem">
|
|
616
|
-
<h3 style="margin-bottom:1rem">📁 修改的文件 (${task.summary.filesChanged.length} 个)</h3>
|
|
617
|
-
${task.summary.filesChanged.map(f => `<div style="font-size:0.85rem;font-family:monospace;margin-bottom:0.25rem">- ${f}</div>`).join('')}
|
|
618
|
-
</div>`;
|
|
583
|
+
// Charts
|
|
584
|
+
renderActivityChart(stats.dailyActivity || []);
|
|
585
|
+
renderToolChart(stats.toolUsage || {});
|
|
586
|
+
} catch {
|
|
587
|
+
document.getElementById('dash-stats').innerHTML = `<div class="stat-card"><div class="label">状态</div><div class="value" style="font-size:1rem;color:var(--red)">离线</div></div>`;
|
|
588
|
+
document.getElementById('daemon-status').textContent = '离线';
|
|
589
|
+
document.getElementById('daemon-status').className = 'badge badge-block';
|
|
619
590
|
}
|
|
620
591
|
|
|
621
|
-
//
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
`).join('')}
|
|
631
|
-
</div>`;
|
|
592
|
+
// Recent sessions
|
|
593
|
+
try {
|
|
594
|
+
const res = await fetch(API + '/api/sessions?limit=5');
|
|
595
|
+
const sessions = await res.json();
|
|
596
|
+
document.getElementById('dash-sessions').innerHTML = sessions.length === 0
|
|
597
|
+
? empty('暂无会话')
|
|
598
|
+
: sessions.map(s => sessionListItem(s)).join('');
|
|
599
|
+
} catch {
|
|
600
|
+
document.getElementById('dash-sessions').innerHTML = empty('加载失败');
|
|
632
601
|
}
|
|
633
602
|
|
|
634
|
-
//
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
603
|
+
// Recent activity
|
|
604
|
+
try {
|
|
605
|
+
const res = await fetch(API + '/api/events?limit=10');
|
|
606
|
+
const events = await res.json();
|
|
607
|
+
document.getElementById('dash-activity').innerHTML = events.length === 0
|
|
608
|
+
? empty('暂无活动')
|
|
609
|
+
: events.map(e => `
|
|
610
|
+
<div class="activity-item">
|
|
611
|
+
<div class="activity-dot activity-dot-${hookColor(e.hook_type)}"></div>
|
|
612
|
+
<div class="activity-content">
|
|
613
|
+
<div class="activity-title">${e.hook_type} ${e.tool_name ? '· ' + e.tool_name : ''}</div>
|
|
614
|
+
<div class="activity-time">${fmtTime(e.timestamp)}</div>
|
|
615
|
+
</div>
|
|
616
|
+
</div>`).join('');
|
|
617
|
+
} catch {}
|
|
618
|
+
}
|
|
646
619
|
|
|
647
|
-
|
|
620
|
+
function hookColor(t) {
|
|
621
|
+
return { PreToolUse:'blue', PostToolUse:'green', UserPromptSubmit:'yellow', Stop:'red' }[t] || 'blue';
|
|
648
622
|
}
|
|
649
623
|
|
|
650
|
-
function
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
624
|
+
function renderActivityChart(data) {
|
|
625
|
+
const ctx = document.getElementById('chart-activity');
|
|
626
|
+
if (!ctx) return;
|
|
627
|
+
if (charts.activity) charts.activity.destroy();
|
|
628
|
+
charts.activity = new Chart(ctx, {
|
|
629
|
+
type: 'line',
|
|
630
|
+
data: {
|
|
631
|
+
labels: data.map(d => d.date),
|
|
632
|
+
datasets: [{ label: '事件数', data: data.map(d => d.count), borderColor: '#4f46e5', backgroundColor: 'rgba(79,70,229,0.08)', tension: 0.3, fill: true }]
|
|
633
|
+
},
|
|
634
|
+
options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { y: { ticks: { color: '#9ca3af' }, grid: { color: '#e4e7ec' } }, x: { ticks: { color: '#9ca3af' }, grid: { color: '#e4e7ec' } } } }
|
|
635
|
+
});
|
|
656
636
|
}
|
|
657
637
|
|
|
658
|
-
|
|
638
|
+
function renderToolChart(data) {
|
|
639
|
+
const ctx = document.getElementById('chart-tools');
|
|
640
|
+
if (!ctx) return;
|
|
641
|
+
if (charts.tools) charts.tools.destroy();
|
|
642
|
+
const labels = Object.keys(data).slice(0, 8);
|
|
643
|
+
const values = labels.map(k => data[k]);
|
|
644
|
+
charts.tools = new Chart(ctx, {
|
|
645
|
+
type: 'doughnut',
|
|
646
|
+
data: { labels, datasets: [{ data: values, backgroundColor: ['#4f46e5','#16a34a','#d97706','#dc2626','#2563eb','#7c3aed','#0891b2','#db2777'] }] },
|
|
647
|
+
options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'right', labels: { color: '#4b5563', boxWidth: 12, font: { size: 11 } } } } }
|
|
648
|
+
});
|
|
649
|
+
}
|
|
659
650
|
|
|
660
|
-
|
|
651
|
+
// === Live ===
|
|
661
652
|
function toggleLive() {
|
|
662
653
|
if (liveSource) {
|
|
663
654
|
liveSource.close(); liveSource = null;
|
|
664
|
-
document.getElementById('live-status').textContent = '
|
|
655
|
+
document.getElementById('live-status').textContent = '未连接';
|
|
665
656
|
document.getElementById('live-status').className = 'badge badge-warn';
|
|
666
|
-
document.getElementById('live-
|
|
657
|
+
document.getElementById('live-btn').textContent = '连接';
|
|
658
|
+
document.getElementById('live-btn').className = 'btn btn-primary';
|
|
667
659
|
return;
|
|
668
660
|
}
|
|
669
661
|
liveSource = new EventSource(API + '/api/events/stream');
|
|
670
|
-
document.getElementById('live-status').textContent = '
|
|
671
|
-
document.getElementById('live-status').className = 'badge badge-
|
|
672
|
-
document.getElementById('live-
|
|
662
|
+
document.getElementById('live-status').textContent = '实时中';
|
|
663
|
+
document.getElementById('live-status').className = 'badge badge-live';
|
|
664
|
+
document.getElementById('live-btn').textContent = '断开连接';
|
|
665
|
+
document.getElementById('live-btn').className = 'btn';
|
|
673
666
|
liveSource.onmessage = function(ev) {
|
|
674
667
|
try {
|
|
675
668
|
const e = JSON.parse(ev.data);
|
|
676
669
|
if (e.type === 'connected') return;
|
|
677
|
-
const
|
|
678
|
-
const time = parseTime(e.timestamp).toLocaleTimeString();
|
|
679
|
-
const tool = e.tool_name || '';
|
|
680
|
-
const detail = e.user_prompt ? e.user_prompt.slice(0,80) : (e.tool_input?.command || e.tool_input?.file_path || '');
|
|
670
|
+
const log = document.getElementById('live-log');
|
|
681
671
|
const line = document.createElement('div');
|
|
682
|
-
line.
|
|
683
|
-
const
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
672
|
+
line.className = 'live-log-line';
|
|
673
|
+
const tool = e.tool_name ? `<span style="color:#93c5fd">${e.tool_name}</span>` : '';
|
|
674
|
+
const detail = (e.user_prompt || e.tool_input?.command || e.tool_input?.file_path || '').toString().slice(0, 60);
|
|
675
|
+
line.innerHTML = `<span style="color:#64748b">${fmtTime(e.timestamp)}</span> ${badgeHook(e.hook_type)} ${tool} <span style="color:#cbd5e1">${detail}</span>`;
|
|
676
|
+
log.appendChild(line);
|
|
677
|
+
log.scrollTop = log.scrollHeight;
|
|
678
|
+
} catch {}
|
|
688
679
|
};
|
|
689
680
|
liveSource.onerror = function() {
|
|
690
|
-
document.getElementById('live-status').textContent = '
|
|
681
|
+
document.getElementById('live-status').textContent = '连接错误';
|
|
691
682
|
document.getElementById('live-status').className = 'badge badge-block';
|
|
692
683
|
};
|
|
693
684
|
}
|
|
694
|
-
function clearLive() { document.getElementById('live-events').innerHTML = ''; }
|
|
695
685
|
|
|
696
|
-
//
|
|
697
|
-
|
|
698
|
-
|
|
686
|
+
// === Sessions ===
|
|
687
|
+
function sessionListItem(s) {
|
|
688
|
+
const prompt = (s.first_prompt || '(无提示词)').slice(0, 60);
|
|
689
|
+
return `<div class="list-item fade-in" onclick="openSessionDrawer('${s.session_id}')">
|
|
690
|
+
<div style="flex:1;min-width:0">
|
|
691
|
+
<div style="font-size:0.875rem;font-weight:500;margin-bottom:2px">${prompt}</div>
|
|
692
|
+
<div style="font-size:0.75rem;color:var(--text-dim)">${fmt(s.start_time)} · ${s.event_count} 个事件 · ${s.session_id.slice(0,8)}</div>
|
|
693
|
+
</div>
|
|
694
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="color:var(--text-dim);flex-shrink:0"><polyline points="9 18 15 12 9 6"/></svg>
|
|
695
|
+
</div>`;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
async function loadSessions() {
|
|
699
|
+
document.getElementById('sessions-list').innerHTML = loading();
|
|
700
|
+
try {
|
|
701
|
+
const res = await fetch(API + '/api/sessions?limit=50');
|
|
702
|
+
allSessions = await res.json();
|
|
703
|
+
renderSessions(allSessions);
|
|
704
|
+
} catch {
|
|
705
|
+
document.getElementById('sessions-list').innerHTML = empty('加载会话失败');
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
function renderSessions(list) {
|
|
710
|
+
document.getElementById('sessions-list').innerHTML = list.length === 0
|
|
711
|
+
? empty('未找到会话')
|
|
712
|
+
: list.map(s => sessionListItem(s)).join('');
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function filterSessions() {
|
|
716
|
+
const q = document.getElementById('sessions-search').value.toLowerCase();
|
|
717
|
+
renderSessions(allSessions.filter(s =>
|
|
718
|
+
(s.first_prompt || '').toLowerCase().includes(q) ||
|
|
719
|
+
s.session_id.toLowerCase().includes(q)
|
|
720
|
+
));
|
|
721
|
+
}
|
|
699
722
|
|
|
700
|
-
|
|
701
|
-
let statsCharts = {};
|
|
723
|
+
let currentSessionData = null;
|
|
702
724
|
|
|
703
|
-
async function
|
|
725
|
+
async function openSessionDrawer(sessionId) {
|
|
726
|
+
openDrawer('会话详情', loading());
|
|
704
727
|
try {
|
|
705
|
-
const res = await fetch(API + '/api/
|
|
706
|
-
const
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
options: {
|
|
721
|
-
responsive: true,
|
|
722
|
-
plugins: {
|
|
723
|
-
legend: { labels: { color: '#e2e8f0' } }
|
|
724
|
-
}
|
|
725
|
-
}
|
|
728
|
+
const res = await fetch(API + '/api/sessions/' + sessionId + '/detail');
|
|
729
|
+
const data = await res.json();
|
|
730
|
+
currentSessionData = data;
|
|
731
|
+
const s = data.session;
|
|
732
|
+
let html = '<div class="detail-section"><div class="detail-label">会话 ID</div><div class="detail-value" style="font-family:monospace;font-size:0.8rem">' + s.session_id + '</div></div>';
|
|
733
|
+
html += '<div class="detail-section"><div class="detail-label">时间</div><div class="detail-value">' + fmt(s.start_time) + ' → ' + fmt(s.end_time) + '</div></div>';
|
|
734
|
+
html += '<div class="detail-section"><div class="detail-label">首条提示词</div><div class="detail-value">' + (s.first_prompt || '—') + '</div></div>';
|
|
735
|
+
html += '<div class="detail-section"><div class="detail-label">任务(' + data.tasks.length + ')</div>';
|
|
736
|
+
data.tasks.forEach((t, i) => {
|
|
737
|
+
const tools = t.events.filter(e => e.tool_name).length;
|
|
738
|
+
html += '<div onclick="openTaskDrawer(' + i + ')" style="padding:0.75rem;background:var(--bg-secondary);border-radius:var(--radius-sm);margin-bottom:0.5rem;border:1px solid var(--border);cursor:pointer;transition:all 0.15s" onmouseover="this.style.borderColor=\'var(--primary)\';this.style.background=\'var(--primary-soft)\'" onmouseout="this.style.borderColor=\'var(--border)\';this.style.background=\'var(--bg-secondary)\'">';
|
|
739
|
+
html += '<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:4px"><div style="font-weight:500;font-size:0.875rem;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding-right:0.5rem">任务 ' + (i+1) + ':' + t.title + '</div><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="color:var(--text-dim);flex-shrink:0"><polyline points="9 18 15 12 9 6"/></svg></div>';
|
|
740
|
+
html += '<div style="font-size:0.75rem;color:var(--text-dim)">' + fmtTime(t.start_time) + ' · ' + tools + ' 次工具调用 · ' + t.summary.filesChanged.length + ' 个文件</div>';
|
|
741
|
+
if (t.summary.commits.length > 0) html += '<div style="font-size:0.75rem;color:var(--green);margin-top:4px">✓ ' + t.summary.commits.length + ' 次提交</div>';
|
|
742
|
+
html += '</div>';
|
|
726
743
|
});
|
|
744
|
+
html += '</div>';
|
|
745
|
+
openDrawer('会话 · ' + s.session_id.slice(0,8), html);
|
|
746
|
+
} catch(e) {
|
|
747
|
+
openDrawer('会话详情', empty('加载失败:' + e.message));
|
|
748
|
+
}
|
|
749
|
+
}
|
|
727
750
|
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
},
|
|
746
|
-
scales: {
|
|
747
|
-
y: { ticks: { color: '#94a3b8' }, grid: { color: '#1e293b' } },
|
|
748
|
-
x: { ticks: { color: '#94a3b8' }, grid: { color: '#1e293b' } }
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
+
function openTaskDrawer(taskIndex) {
|
|
752
|
+
if (!currentSessionData || !currentSessionData.tasks[taskIndex]) return;
|
|
753
|
+
const t = currentSessionData.tasks[taskIndex];
|
|
754
|
+
const tools = t.events.filter(e => e.tool_name).length;
|
|
755
|
+
|
|
756
|
+
let html = '<div class="detail-section"><div class="detail-label">标题</div><div class="detail-value" style="font-weight:600">' + t.title + '</div></div>';
|
|
757
|
+
html += '<div class="detail-section"><div class="detail-label">时间</div><div class="detail-value">' + fmt(t.start_time) + (t.end_time ? ' → ' + fmt(t.end_time) : '') + '</div></div>';
|
|
758
|
+
html += '<div class="detail-section"><div class="detail-label">统计</div><div class="detail-value">' + tools + ' 次工具调用 · ' + t.summary.filesChanged.length + ' 个文件变更 · ' + t.summary.commits.length + ' 次提交</div></div>';
|
|
759
|
+
|
|
760
|
+
// User prompts
|
|
761
|
+
if (t.prompts && t.prompts.length > 0) {
|
|
762
|
+
html += '<div class="detail-section"><div class="detail-label">用户提示词(' + t.prompts.length + ')</div>';
|
|
763
|
+
t.prompts.forEach(p => {
|
|
764
|
+
html += '<div style="padding:0.625rem;background:var(--bg-secondary);border-radius:var(--radius-sm);margin-bottom:0.375rem;border:1px solid var(--border);font-size:0.8rem">';
|
|
765
|
+
html += '<div style="color:var(--text-dim);font-size:0.7rem;margin-bottom:2px">' + fmtTime(p.timestamp) + '</div>';
|
|
766
|
+
html += '<div style="white-space:pre-wrap;word-break:break-word">' + (p.content || '—') + '</div>';
|
|
767
|
+
html += '</div>';
|
|
751
768
|
});
|
|
769
|
+
html += '</div>';
|
|
770
|
+
}
|
|
752
771
|
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
data: {
|
|
759
|
-
labels: stats.dailyActivity.map(d => d.date),
|
|
760
|
-
datasets: [{
|
|
761
|
-
label: 'Events',
|
|
762
|
-
data: stats.dailyActivity.map(d => d.count),
|
|
763
|
-
borderColor: '#818cf8',
|
|
764
|
-
backgroundColor: 'rgba(129,140,248,0.1)',
|
|
765
|
-
tension: 0.3
|
|
766
|
-
}]
|
|
767
|
-
},
|
|
768
|
-
options: {
|
|
769
|
-
responsive: true,
|
|
770
|
-
plugins: {
|
|
771
|
-
legend: { labels: { color: '#e2e8f0' } }
|
|
772
|
-
},
|
|
773
|
-
scales: {
|
|
774
|
-
y: { ticks: { color: '#94a3b8' }, grid: { color: '#1e293b' } },
|
|
775
|
-
x: { ticks: { color: '#94a3b8' }, grid: { color: '#1e293b' } }
|
|
776
|
-
}
|
|
777
|
-
}
|
|
772
|
+
// Tool usage summary
|
|
773
|
+
if (t.summary.toolUsage && Object.keys(t.summary.toolUsage).length > 0) {
|
|
774
|
+
html += '<div class="detail-section"><div class="detail-label">工具使用分布</div><div class="detail-value">';
|
|
775
|
+
Object.entries(t.summary.toolUsage).sort((a,b) => b[1]-a[1]).forEach(([name, count]) => {
|
|
776
|
+
html += '<span class="tag" style="margin-bottom:4px">' + name + ' × ' + count + '</span>';
|
|
778
777
|
});
|
|
779
|
-
|
|
780
|
-
console.error('Failed to load stats:', err);
|
|
778
|
+
html += '</div></div>';
|
|
781
779
|
}
|
|
782
|
-
}
|
|
783
780
|
|
|
784
|
-
//
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
781
|
+
// Files changed
|
|
782
|
+
if (t.summary.filesChanged && t.summary.filesChanged.length > 0) {
|
|
783
|
+
html += '<div class="detail-section"><div class="detail-label">变更的文件(' + t.summary.filesChanged.length + ')</div>';
|
|
784
|
+
t.summary.filesChanged.forEach(f => {
|
|
785
|
+
const short = f.split('/').slice(-2).join('/');
|
|
786
|
+
html += '<div style="font-family:monospace;font-size:0.75rem;color:var(--text-muted);padding:2px 0" title="' + f + '">' + short + '</div>';
|
|
787
|
+
});
|
|
788
|
+
html += '</div>';
|
|
789
|
+
}
|
|
788
790
|
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
791
|
+
// Commits
|
|
792
|
+
if (t.summary.commits && t.summary.commits.length > 0) {
|
|
793
|
+
html += '<div class="detail-section"><div class="detail-label">提交记录(' + t.summary.commits.length + ')</div>';
|
|
794
|
+
t.summary.commits.forEach(c => {
|
|
795
|
+
html += '<div style="padding:0.5rem;background:var(--green-soft);border-radius:var(--radius-sm);margin-bottom:0.25rem;font-size:0.8rem;color:var(--text)"><span style="color:var(--green)">✓</span> ' + c.message + '</div>';
|
|
796
|
+
});
|
|
797
|
+
html += '</div>';
|
|
796
798
|
}
|
|
797
|
-
}
|
|
798
799
|
|
|
799
|
-
|
|
800
|
-
if (
|
|
801
|
-
|
|
802
|
-
|
|
800
|
+
// Injections
|
|
801
|
+
if (t.injections && t.injections.length > 0) {
|
|
802
|
+
html += '<div class="detail-section"><div class="detail-label">注入内容(' + t.injections.length + ')</div>';
|
|
803
|
+
t.injections.forEach(inj => {
|
|
804
|
+
const escaped = inj.content.replace(/</g, '<').replace(/>/g, '>');
|
|
805
|
+
html += '<div style="padding:0.625rem;background:var(--primary-soft);border-radius:var(--radius-sm);margin-bottom:0.375rem;border:1px solid var(--primary-border)">';
|
|
806
|
+
html += '<div style="display:flex;align-items:center;gap:0.5rem;margin-bottom:4px"><span class="badge badge-info">' + inj.injection_type + '</span><span style="font-size:0.7rem;color:var(--text-dim)">' + inj.source_handler + ' · ' + fmtTime(inj.timestamp) + '</span></div>';
|
|
807
|
+
html += '<div style="font-size:0.75rem;color:var(--text);white-space:pre-wrap;word-break:break-word;max-height:200px;overflow-y:auto;font-family:ui-monospace,monospace">' + escaped + '</div>';
|
|
808
|
+
html += '</div>';
|
|
809
|
+
});
|
|
810
|
+
html += '</div>';
|
|
803
811
|
}
|
|
804
|
-
document.getElementById('events-body').innerHTML = events.map(e => {
|
|
805
|
-
const time = parseTime(e.timestamp).toLocaleTimeString();
|
|
806
|
-
const badge = e.hook_type === 'PreToolUse' ? 'info' : e.hook_type === 'Stop' ? 'warn' : 'allow';
|
|
807
|
-
return `<tr>
|
|
808
|
-
<td>${time}</td>
|
|
809
|
-
<td><span class="badge badge-${badge}">${e.hook_type}</span></td>
|
|
810
|
-
<td>${e.tool_name || '—'}</td>
|
|
811
|
-
<td>${(e.project_path || '').split('/').pop()}</td>
|
|
812
|
-
</tr>`;
|
|
813
|
-
}).join('');
|
|
814
|
-
}
|
|
815
812
|
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
813
|
+
// Quality issues
|
|
814
|
+
if (t.qualityIssues && t.qualityIssues.length > 0) {
|
|
815
|
+
html += '<div class="detail-section"><div class="detail-label">质量问题(' + t.qualityIssues.length + ')</div>';
|
|
816
|
+
t.qualityIssues.forEach(q => {
|
|
817
|
+
const sev = q.severity || 'info';
|
|
818
|
+
const badgeClass = sev === 'must' ? 'badge-block' : sev === 'should' ? 'badge-warn' : 'badge-info';
|
|
819
|
+
const sevLabel = sev === 'must' ? '必须' : sev === 'should' ? '建议' : '提示';
|
|
820
|
+
html += '<div style="padding:0.5rem;background:var(--yellow-soft);border-radius:var(--radius-sm);margin-bottom:0.375rem;border:1px solid var(--border);font-size:0.8rem">';
|
|
821
|
+
html += '<div style="display:flex;gap:0.5rem;align-items:center;margin-bottom:2px"><span class="badge ' + badgeClass + '">' + sevLabel + '</span><span style="font-family:monospace;font-size:0.7rem;color:var(--text-dim)">' + (q.file_path || '').split('/').slice(-2).join('/') + '</span></div>';
|
|
822
|
+
html += '<div style="color:var(--text)">' + (q.description || q.message || '') + '</div>';
|
|
823
|
+
html += '</div>';
|
|
824
|
+
});
|
|
825
|
+
html += '</div>';
|
|
826
|
+
}
|
|
825
827
|
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
828
|
+
// Governance decisions
|
|
829
|
+
if (t.decisions && t.decisions.length > 0) {
|
|
830
|
+
html += '<div class="detail-section"><div class="detail-label">治理决策(' + t.decisions.length + ')</div>';
|
|
831
|
+
t.decisions.forEach(d => {
|
|
832
|
+
const lv = d.level || 'info';
|
|
833
|
+
const badgeClass = lv === 'block' ? 'badge-block' : lv === 'warn' ? 'badge-warn' : lv === 'allow' ? 'badge-allow' : 'badge-info';
|
|
834
|
+
const lvLabel = lv === 'block' ? '阻断' : lv === 'warn' ? '警告' : lv === 'allow' ? '放行' : lv === 'confirm' ? '确认' : lv;
|
|
835
|
+
html += '<div style="padding:0.5rem;background:var(--bg-secondary);border-radius:var(--radius-sm);margin-bottom:0.375rem;border:1px solid var(--border);font-size:0.8rem">';
|
|
836
|
+
html += '<div style="display:flex;gap:0.5rem;align-items:center;margin-bottom:2px"><span class="badge ' + badgeClass + '">' + lvLabel + '</span><span style="font-family:monospace;font-size:0.7rem;color:var(--text-dim)">' + (d.rule_id || '') + '</span></div>';
|
|
837
|
+
html += '<div style="color:var(--text-muted)">' + (d.reason || '—') + '</div>';
|
|
838
|
+
html += '</div>';
|
|
839
|
+
});
|
|
840
|
+
html += '</div>';
|
|
841
|
+
}
|
|
831
842
|
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
843
|
+
// Recent events
|
|
844
|
+
if (t.events && t.events.length > 0) {
|
|
845
|
+
html += '<div class="detail-section"><div class="detail-label">事件(展示 ' + Math.min(t.events.length, 30) + ' / ' + t.events.length + ')</div>';
|
|
846
|
+
t.events.slice(0, 30).forEach(e => {
|
|
847
|
+
const inputPreview = e.tool_input ? (JSON.stringify(e.tool_input).slice(0, 80) + (JSON.stringify(e.tool_input).length > 80 ? '...' : '')) : '';
|
|
848
|
+
html += '<div style="padding:0.5rem;border-bottom:1px solid var(--border);font-size:0.75rem">';
|
|
849
|
+
html += '<div style="display:flex;gap:0.5rem;align-items:center;margin-bottom:2px">';
|
|
850
|
+
html += '<span style="color:var(--text-dim)">' + fmtTime(e.timestamp) + '</span>';
|
|
851
|
+
html += badgeHook(e.hook_type);
|
|
852
|
+
if (e.tool_name) html += '<span style="font-family:monospace;color:var(--text-muted)">' + e.tool_name + '</span>';
|
|
853
|
+
html += '</div>';
|
|
854
|
+
if (inputPreview) html += '<div style="font-family:monospace;color:var(--text-dim);font-size:0.7rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + inputPreview + '</div>';
|
|
855
|
+
html += '</div>';
|
|
836
856
|
});
|
|
857
|
+
html += '</div>';
|
|
858
|
+
}
|
|
837
859
|
|
|
838
|
-
|
|
860
|
+
// Back button
|
|
861
|
+
html = '<div style="margin-bottom:1rem"><button class="btn" onclick="openSessionDrawer(\'' + currentSessionData.session.session_id + '\')">← 返回会话</button></div>' + html;
|
|
862
|
+
|
|
863
|
+
openDrawer('任务:' + t.title.slice(0, 40), html);
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// === Events ===
|
|
867
|
+
async function loadEvents() {
|
|
868
|
+
document.getElementById('events-body').innerHTML = '<tr><td colspan="4" class="loading">加载中...</td></tr>';
|
|
869
|
+
try {
|
|
870
|
+
const res = await fetch(API + '/api/events?limit=100');
|
|
871
|
+
allEvents = await res.json();
|
|
872
|
+
renderEvents(allEvents);
|
|
839
873
|
} catch {
|
|
840
|
-
document.getElementById('
|
|
874
|
+
document.getElementById('events-body').innerHTML = '<tr><td colspan="4">' + empty('加载失败') + '</td></tr>';
|
|
841
875
|
}
|
|
842
876
|
}
|
|
843
877
|
|
|
844
|
-
function
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
878
|
+
function renderEvents(list) {
|
|
879
|
+
document.getElementById('events-body').innerHTML = list.length === 0
|
|
880
|
+
? '<tr><td colspan="4">' + empty('暂无事件') + '</td></tr>'
|
|
881
|
+
: list.map(e => '<tr onclick="openEventDrawer(' + JSON.stringify(e).replace(/"/g,'"') + ')"><td style="color:var(--text-dim);font-size:0.8rem">' + fmtTime(e.timestamp) + '</td><td>' + badgeHook(e.hook_type) + '</td><td style="font-family:monospace;font-size:0.8rem">' + (e.tool_name || '—') + '</td><td style="color:var(--text-dim);font-size:0.8rem">' + ((e.project_path||'').split('/').pop() || '—') + '</td></tr>').join('');
|
|
882
|
+
}
|
|
849
883
|
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
<div style="font-size:0.85rem;color:var(--text-muted);margin-bottom:0.5rem">${(rule.description || '').slice(0, 100)}</div>
|
|
858
|
-
<div style="margin-top:0.5rem">${tags}</div>
|
|
859
|
-
</div>
|
|
860
|
-
<span style="color:var(--accent);font-size:1.5rem">→</span>
|
|
861
|
-
</div>
|
|
862
|
-
</div>`;
|
|
863
|
-
}).join('');
|
|
884
|
+
function filterEvents() {
|
|
885
|
+
const q = document.getElementById('events-search').value.toLowerCase();
|
|
886
|
+
const t = document.getElementById('events-type').value;
|
|
887
|
+
renderEvents(allEvents.filter(e =>
|
|
888
|
+
(!t || e.hook_type === t) &&
|
|
889
|
+
(!q || (e.hook_type||'').toLowerCase().includes(q) || (e.tool_name||'').toLowerCase().includes(q) || (e.project_path||'').toLowerCase().includes(q))
|
|
890
|
+
));
|
|
864
891
|
}
|
|
865
892
|
|
|
866
|
-
function
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
);
|
|
874
|
-
renderRules(filtered);
|
|
893
|
+
function openEventDrawer(e) {
|
|
894
|
+
let html = '<div class="detail-section"><div class="detail-label">类型</div><div class="detail-value">' + badgeHook(e.hook_type) + '</div></div>';
|
|
895
|
+
html += '<div class="detail-section"><div class="detail-label">时间</div><div class="detail-value">' + fmt(e.timestamp) + '</div></div>';
|
|
896
|
+
html += '<div class="detail-section"><div class="detail-label">工具</div><div class="detail-value" style="font-family:monospace">' + (e.tool_name || '—') + '</div></div>';
|
|
897
|
+
html += '<div class="detail-section"><div class="detail-label">项目</div><div class="detail-value" style="font-family:monospace;font-size:0.8rem">' + (e.project_path || '—') + '</div></div>';
|
|
898
|
+
html += '<div class="detail-section"><div class="detail-label">会话</div><div class="detail-value" style="font-family:monospace;font-size:0.8rem">' + (e.session_id || '—') + '</div></div>';
|
|
899
|
+
if (e.tool_input) html += '<div class="detail-section"><div class="detail-label">输入参数</div><pre class="detail-code">' + JSON.stringify(e.tool_input, null, 2) + '</pre></div>';
|
|
900
|
+
openDrawer(e.hook_type + ' · ' + (e.tool_name || '事件'), html);
|
|
875
901
|
}
|
|
876
902
|
|
|
903
|
+
// === Injections ===
|
|
877
904
|
async function loadInjections() {
|
|
905
|
+
document.getElementById('inj-list').innerHTML = loading();
|
|
878
906
|
try {
|
|
879
|
-
const
|
|
880
|
-
const handler = document.getElementById('inj-handler').value;
|
|
881
|
-
let url = API + '/api/injections?limit=50';
|
|
882
|
-
if (session) url += '&session=' + session;
|
|
883
|
-
if (handler) url += '&handler=' + handler;
|
|
884
|
-
const res = await fetch(url);
|
|
907
|
+
const res = await fetch(API + '/api/injections?limit=100');
|
|
885
908
|
allInjections = await res.json();
|
|
886
|
-
|
|
887
|
-
const sessRes = await fetch(API + '/api/sessions?limit=10');
|
|
888
|
-
const sessions = await sessRes.json();
|
|
889
|
-
const sessSelect = document.getElementById('inj-session');
|
|
890
|
-
if (sessSelect.options.length === 1) {
|
|
891
|
-
sessions.forEach(s => {
|
|
892
|
-
const opt = document.createElement('option');
|
|
893
|
-
opt.value = s.session_id;
|
|
894
|
-
opt.textContent = s.session_id.slice(0, 8);
|
|
895
|
-
sessSelect.appendChild(opt);
|
|
896
|
-
});
|
|
897
|
-
}
|
|
898
|
-
|
|
899
909
|
renderInjections(allInjections);
|
|
900
910
|
} catch {
|
|
901
|
-
document.getElementById('
|
|
911
|
+
document.getElementById('inj-list').innerHTML = empty('加载失败');
|
|
902
912
|
}
|
|
903
913
|
}
|
|
904
914
|
|
|
905
|
-
function renderInjections(
|
|
906
|
-
|
|
907
|
-
document.getElementById('injections-list').innerHTML = '<div class="empty">No injections found</div>';
|
|
908
|
-
return;
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
document.getElementById('injections-list').innerHTML = injections.map(inj => {
|
|
912
|
-
const time = parseTime(inj.timestamp).toLocaleString();
|
|
913
|
-
const shortSession = inj.session_id.slice(0, 8);
|
|
914
|
-
const preview = inj.content.slice(0, 150);
|
|
915
|
-
return `<div class="inj-card fade-in">
|
|
916
|
-
<div style="display:flex;justify-content:space-between;margin-bottom:0.5rem">
|
|
917
|
-
<span class="badge badge-info">${inj.injection_type}</span>
|
|
918
|
-
<span style="font-size:0.75rem;color:var(--text-dim)">${time}</span>
|
|
919
|
-
</div>
|
|
920
|
-
<div style="font-size:0.8rem;color:var(--text-muted);margin-bottom:0.5rem">
|
|
921
|
-
Session: ${shortSession} · Handler: ${inj.source_handler}
|
|
922
|
-
</div>
|
|
923
|
-
<details>
|
|
924
|
-
<summary style="cursor:pointer;font-size:0.85rem;color:var(--accent)">Preview: ${preview}...</summary>
|
|
925
|
-
<pre style="margin-top:0.5rem;font-size:0.75rem;background:var(--bg-secondary);padding:0.75rem;border-radius:6px;white-space:pre-wrap;max-height:400px;overflow-y:auto">${inj.content}</pre>
|
|
926
|
-
</details>
|
|
927
|
-
</div>`;
|
|
928
|
-
}).join('');
|
|
915
|
+
function renderInjections(list) {
|
|
916
|
+
document.getElementById('inj-list').innerHTML = list.length === 0 ? empty('暂无注入记录') : list.map(inj => '<div class="list-item fade-in" onclick="openInjDrawer(' + JSON.stringify(inj).replace(/"/g,'"') + ')"><div style="flex:1;min-width:0"><div style="display:flex;align-items:center;gap:0.5rem;margin-bottom:2px"><span class="badge badge-info">' + inj.injection_type + '</span><span style="font-size:0.75rem;color:var(--text-dim)">' + inj.source_handler + '</span></div><div style="font-size:0.8rem;color:var(--text-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis">' + inj.content.slice(0,80) + '</div><div style="font-size:0.75rem;color:var(--text-dim);margin-top:2px">' + fmt(inj.timestamp) + ' · ' + inj.session_id.slice(0,8) + '</div></div><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="color:var(--text-dim);flex-shrink:0"><polyline points="9 18 15 12 9 6"/></svg></div>').join('');
|
|
929
917
|
}
|
|
930
918
|
|
|
931
919
|
function filterInjections() {
|
|
932
|
-
const
|
|
933
|
-
const
|
|
934
|
-
|
|
935
|
-
(inj.content || '').toLowerCase().includes(query) ||
|
|
936
|
-
(inj.source_handler || '').toLowerCase().includes(query)
|
|
937
|
-
);
|
|
938
|
-
renderInjections(filtered);
|
|
920
|
+
const q = document.getElementById('inj-search').value.toLowerCase();
|
|
921
|
+
const h = document.getElementById('inj-handler').value;
|
|
922
|
+
renderInjections(allInjections.filter(inj => (!h || inj.source_handler === h) && (!q || inj.content.toLowerCase().includes(q) || inj.injection_type.toLowerCase().includes(q))));
|
|
939
923
|
}
|
|
940
924
|
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
const tags = (rule.tags || []).map(t => `<span class="tag">${t}</span>`).join('');
|
|
951
|
-
const docRef = rule.doc_ref ? `<a href="${rule.doc_ref}" target="_blank" style="color:var(--accent)">${rule.doc_ref}</a>` : 'N/A';
|
|
925
|
+
function openInjDrawer(inj) {
|
|
926
|
+
let html = '<div class="detail-section"><div class="detail-label">类型</div><div class="detail-value"><span class="badge badge-info">' + inj.injection_type + '</span></div></div>';
|
|
927
|
+
html += '<div class="detail-section"><div class="detail-label">Handler</div><div class="detail-value">' + inj.source_handler + '</div></div>';
|
|
928
|
+
html += '<div class="detail-section"><div class="detail-label">时间</div><div class="detail-value">' + fmt(inj.timestamp) + '</div></div>';
|
|
929
|
+
html += '<div class="detail-section"><div class="detail-label">会话</div><div class="detail-value" style="font-family:monospace;font-size:0.8rem">' + inj.session_id + '</div></div>';
|
|
930
|
+
html += '<div class="detail-section"><div class="detail-label">注入内容</div><pre class="detail-code">' + inj.content + '</pre></div>';
|
|
931
|
+
openDrawer(inj.injection_type, html);
|
|
932
|
+
}
|
|
952
933
|
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
934
|
+
// === Rules ===
|
|
935
|
+
async function loadRules() {
|
|
936
|
+
document.getElementById('rules-list').innerHTML = loading();
|
|
937
|
+
try {
|
|
938
|
+
const res = await fetch(API + '/api/rules');
|
|
939
|
+
allRules = await res.json();
|
|
940
|
+
renderRules(allRules);
|
|
941
|
+
} catch {
|
|
942
|
+
document.getElementById('rules-list').innerHTML = empty('加载规则失败');
|
|
943
|
+
}
|
|
944
|
+
}
|
|
961
945
|
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
<div style="font-size:0.85rem;font-weight:600;color:var(--text-muted)">Category</div>
|
|
966
|
-
<div style="font-size:0.95rem">${rule.category || 'N/A'}</div>
|
|
967
|
-
</div>
|
|
968
|
-
<div style="margin-bottom:0.75rem">
|
|
969
|
-
<div style="font-size:0.85rem;font-weight:600;color:var(--text-muted)">When Condition</div>
|
|
970
|
-
<pre style="font-size:0.85rem;background:var(--bg-secondary);padding:0.75rem;border-radius:6px;overflow-x:auto">${rule.when || 'N/A'}</pre>
|
|
971
|
-
</div>
|
|
972
|
-
<div style="margin-bottom:0.75rem">
|
|
973
|
-
<div style="font-size:0.85rem;font-weight:600;color:var(--text-muted)">Action</div>
|
|
974
|
-
<div style="font-size:0.95rem">${rule.action || 'N/A'}</div>
|
|
975
|
-
</div>
|
|
976
|
-
<div style="margin-bottom:0.75rem">
|
|
977
|
-
<div style="font-size:0.85rem;font-weight:600;color:var(--text-muted)">Message</div>
|
|
978
|
-
<div style="font-size:0.95rem">${rule.message || 'N/A'}</div>
|
|
979
|
-
</div>
|
|
980
|
-
<div>
|
|
981
|
-
<div style="font-size:0.85rem;font-weight:600;color:var(--text-muted)">Documentation</div>
|
|
982
|
-
<div style="font-size:0.95rem">${docRef}</div>
|
|
983
|
-
</div>
|
|
984
|
-
</div>`;
|
|
946
|
+
function renderRules(list) {
|
|
947
|
+
document.getElementById('rules-list').innerHTML = list.length === 0 ? empty('未找到规则') : list.map(r => '<div class="list-item fade-in" onclick="openRuleDrawer(' + JSON.stringify(r).replace(/"/g,'"') + ')"><div style="flex:1;min-width:0"><div style="font-size:0.875rem;font-weight:500;margin-bottom:2px">' + (r.name || r.id) + '</div><div style="font-size:0.75rem;color:var(--text-dim)">' + (r.description || '') + '</div></div><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="color:var(--text-dim);flex-shrink:0"><polyline points="9 18 15 12 9 6"/></svg></div>').join('');
|
|
948
|
+
}
|
|
985
949
|
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
</div>`;
|
|
991
|
-
}
|
|
950
|
+
function filterRules() {
|
|
951
|
+
const q = document.getElementById('rules-search').value.toLowerCase();
|
|
952
|
+
renderRules(allRules.filter(r => (r.name||'').toLowerCase().includes(q) || (r.description||'').toLowerCase().includes(q)));
|
|
953
|
+
}
|
|
992
954
|
|
|
993
|
-
|
|
955
|
+
function openRuleDrawer(r) {
|
|
956
|
+
let html = '<div class="detail-section"><div class="detail-label">名称</div><div class="detail-value" style="font-weight:600">' + (r.name || '—') + '</div></div>';
|
|
957
|
+
html += '<div class="detail-section"><div class="detail-label">描述</div><div class="detail-value">' + (r.description || '—') + '</div></div>';
|
|
958
|
+
if (r.stats) html += '<div class="detail-section"><div class="detail-label">统计</div><div class="detail-value">触发 ' + (r.stats.totalTriggers || 0) + ' 次 | 阻断 ' + (r.stats.blockCount || 0) + ' 次 | 警告 ' + (r.stats.warnCount || 0) + ' 次</div></div>';
|
|
959
|
+
openDrawer(r.name || '规则', html);
|
|
994
960
|
}
|
|
995
961
|
|
|
962
|
+
// === Init ===
|
|
963
|
+
nav('dashboard');
|
|
996
964
|
</script>
|
|
997
965
|
</body>
|
|
998
966
|
</html>
|