@winspan/claude-forge 8.11.0 → 8.12.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.
|
@@ -4,995 +4,897 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>Claude Forge Dashboard</title>
|
|
7
|
-
<script src="
|
|
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">Overview</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
|
+
Dashboard
|
|
340
|
+
</a>
|
|
341
|
+
<div class="nav-section-title">Activity</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
|
+
Sessions
|
|
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
|
+
Events
|
|
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
|
+
Injections
|
|
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
|
+
Live
|
|
357
|
+
</a>
|
|
358
|
+
<div class="nav-section-title">Configuration</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
|
+
Rules
|
|
362
|
+
</a>
|
|
363
|
+
</aside>
|
|
364
|
+
|
|
365
|
+
<!-- Main -->
|
|
366
|
+
<main class="main">
|
|
367
|
+
<div class="topbar">
|
|
368
|
+
<div class="page-title" id="topbar-title">Dashboard</div>
|
|
369
|
+
<div class="actions">
|
|
370
|
+
<span id="daemon-status" class="badge badge-allow" style="font-size:0.75rem">Checking...</span>
|
|
371
|
+
<button class="btn" onclick="refreshPage()">↻ Refresh</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-Day Activity</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">Tool Usage</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">Recent Sessions</span>
|
|
397
|
+
<a onclick="nav('sessions')" style="font-size:0.8rem;color:var(--primary);cursor:pointer">View all →</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">Recent Activity</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="Search sessions..." 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
419
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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="Search by type, tool, project..." oninput="filterEvents()">
|
|
424
|
+
<select class="btn" id="events-type" onchange="filterEvents()">
|
|
425
|
+
<option value="">All Types</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>Time</th><th>Type</th><th>Tool</th><th>Project</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="Search injections..." oninput="filterInjections()">
|
|
444
|
+
<select class="btn" id="inj-handler" onchange="filterInjections()">
|
|
445
|
+
<option value="">All Handlers</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">Disconnected</span>
|
|
460
|
+
<button class="btn btn-primary" id="live-btn" onclick="toggleLive()">Connect</button>
|
|
461
|
+
<button class="btn" onclick="document.getElementById('live-log').innerHTML=''">Clear</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="Search rules..." 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">Detail</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 (
|
|
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:'Dashboard', sessions:'Sessions', events:'Events', injections:'Injections', live:'Live', rules:'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();
|
|
213
518
|
}
|
|
214
519
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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>'; }
|
|
520
|
+
function refreshPage() {
|
|
521
|
+
const active = document.querySelector('.page.active');
|
|
522
|
+
if (active) nav(active.id.replace('page-', ''));
|
|
229
523
|
}
|
|
230
524
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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>'; }
|
|
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');
|
|
256
531
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
const res = await fetch(API + '/api/events?limit=50');
|
|
261
|
-
const events = await res.json();
|
|
262
|
-
if (events.length === 0) {
|
|
263
|
-
document.getElementById('events-body').innerHTML = '<tr><td colspan="4" class="empty">No events</td></tr>';
|
|
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>'; }
|
|
532
|
+
function closeDrawer() {
|
|
533
|
+
document.getElementById('drawer').classList.remove('active');
|
|
534
|
+
document.getElementById('drawer-overlay').classList.remove('active');
|
|
277
535
|
}
|
|
278
536
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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>'; }
|
|
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' });
|
|
298
542
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
const handler = document.getElementById('inj-handler').value;
|
|
304
|
-
let url = API + '/api/injections?limit=50';
|
|
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>'; }
|
|
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' });
|
|
347
547
|
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
-
}
|
|
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>`;
|
|
359
551
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
let currentView = 'timeline'; // 'timeline' or 'tasks'
|
|
363
|
-
let timelineOffset = 0;
|
|
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
|
-
}
|
|
552
|
+
function empty(msg) {
|
|
553
|
+
return `<div class="empty"><div class="empty-icon">📭</div><div class="empty-text">${msg}</div></div>`;
|
|
376
554
|
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
currentView = currentView === 'timeline' ? 'tasks' : 'timeline';
|
|
380
|
-
document.getElementById('current-view-label').textContent = currentView === 'timeline' ? '时间线视图' : '任务视图';
|
|
381
|
-
if (currentSessionId) loadSessionDetail(currentSessionId);
|
|
555
|
+
function loading() {
|
|
556
|
+
return `<div class="loading">Loading...</div>`;
|
|
382
557
|
}
|
|
383
558
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
document.getElementById('session-detail').innerHTML = '<div class="empty">Loading...</div>';
|
|
388
|
-
}
|
|
389
|
-
|
|
559
|
+
// === Dashboard ===
|
|
560
|
+
async function loadDashboard() {
|
|
561
|
+
document.getElementById('dash-stats').innerHTML = loading();
|
|
390
562
|
try {
|
|
391
|
-
const
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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 + 's' : Math.floor(uptime/60) + 'm';
|
|
573
|
+
const mem = Math.round(status.memory.heapUsed / 1024 / 1024);
|
|
574
|
+
document.getElementById('dash-stats').innerHTML = `
|
|
575
|
+
<div class="stat-card"><div class="label">PID</div><div class="value">${status.pid}</div></div>
|
|
576
|
+
<div class="stat-card"><div class="label">Uptime</div><div class="value">${uptimeStr}</div></div>
|
|
577
|
+
<div class="stat-card"><div class="label">Memory</div><div class="value">${mem}MB</div></div>
|
|
578
|
+
<div class="stat-card"><div class="label">Events</div><div class="value">${status.eventCount}</div></div>
|
|
579
|
+
`;
|
|
580
|
+
document.getElementById('daemon-status').textContent = 'Running';
|
|
581
|
+
document.getElementById('daemon-status').className = 'badge badge-allow';
|
|
401
582
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
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">Status</div><div class="value" style="font-size:1rem;color:var(--red)">Offline</div></div>`;
|
|
588
|
+
document.getElementById('daemon-status').textContent = 'Offline';
|
|
589
|
+
document.getElementById('daemon-status').className = 'badge badge-block';
|
|
405
590
|
}
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
async function loadMoreTimeline() {
|
|
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
591
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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>`;
|
|
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('No sessions yet')
|
|
598
|
+
: sessions.map(s => sessionListItem(s)).join('');
|
|
599
|
+
} catch {
|
|
600
|
+
document.getElementById('dash-sessions').innerHTML = empty('Failed to load');
|
|
464
601
|
}
|
|
465
602
|
|
|
466
|
-
//
|
|
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;
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
let sessionData = null; // Store session data for task detail navigation
|
|
488
|
-
|
|
489
|
-
async function loadSessionTasks(sessionId) {
|
|
490
|
-
showPage('session-detail');
|
|
491
|
-
document.getElementById('session-detail').innerHTML = '<div class="empty">Loading...</div>';
|
|
492
|
-
|
|
603
|
+
// Recent activity
|
|
493
604
|
try {
|
|
494
|
-
const res = await fetch(API + '/api/
|
|
495
|
-
const
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
<h2 style="margin-bottom:0.5rem">会话 ${shortId}</h2>
|
|
505
|
-
<div style="color:var(--text-muted)">${data.session.first_prompt || '(无提示词)'}</div>
|
|
506
|
-
<div style="margin-top:0.5rem;font-size:0.8rem;color:var(--text-dim)">${start} ~ ${end} · ${data.tasks.length} tasks · ${data.session.event_count} events</div>
|
|
507
|
-
</div>
|
|
508
|
-
`;
|
|
509
|
-
|
|
510
|
-
// Task list (clean, clickable)
|
|
511
|
-
data.tasks.forEach((task, idx) => {
|
|
512
|
-
const taskStart = parseTime(task.start_time).toLocaleTimeString();
|
|
513
|
-
const taskEnd = task.end_time ? parseTime(task.end_time).toLocaleTimeString() : 'ongoing';
|
|
514
|
-
const toolCount = task.events.filter(e => e.tool_name).length;
|
|
515
|
-
const commitCount = task.summary.commits.length;
|
|
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>
|
|
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('No activity yet')
|
|
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>
|
|
530
615
|
</div>
|
|
531
|
-
|
|
532
|
-
|
|
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
|
-
}
|
|
616
|
+
</div>`).join('');
|
|
617
|
+
} catch {}
|
|
540
618
|
}
|
|
541
619
|
|
|
542
|
-
function
|
|
543
|
-
|
|
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>`;
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
// Quality issues
|
|
622
|
-
if (task.qualityIssues.length > 0) {
|
|
623
|
-
html += `<div class="card fade-in" style="margin-bottom:1rem;border-left:3px solid var(--yellow)">
|
|
624
|
-
<h3 style="margin-bottom:1rem">⚠️ 质量问题 (${task.qualityIssues.length} 个)</h3>
|
|
625
|
-
${task.qualityIssues.map(q => `
|
|
626
|
-
<div style="margin-bottom:0.75rem;padding:0.5rem;background:var(--bg-secondary);border-radius:4px">
|
|
627
|
-
<div style="font-size:0.75rem;color:var(--red);font-weight:600">[${q.level}/${q.category}]</div>
|
|
628
|
-
<div style="font-size:0.85rem;margin-top:0.25rem">${q.message}</div>
|
|
629
|
-
</div>
|
|
630
|
-
`).join('')}
|
|
631
|
-
</div>`;
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
// Commits
|
|
635
|
-
if (task.summary.commits.length > 0) {
|
|
636
|
-
html += `<div class="card fade-in" style="border-left:3px solid var(--green)">
|
|
637
|
-
<h3 style="margin-bottom:1rem">📦 产出 (${task.summary.commits.length} commits)</h3>
|
|
638
|
-
${task.summary.commits.map(c => `
|
|
639
|
-
<div style="margin-bottom:0.75rem;padding:0.5rem;background:var(--bg-secondary);border-radius:4px">
|
|
640
|
-
<div style="font-size:0.75rem;color:var(--text-dim)">${parseTime(c.timestamp).toLocaleTimeString()}</div>
|
|
641
|
-
<div style="font-size:0.85rem;color:var(--green);margin-top:0.25rem">${c.message}</div>
|
|
642
|
-
</div>
|
|
643
|
-
`).join('')}
|
|
644
|
-
</div>`;
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
document.getElementById('task-detail').innerHTML = html;
|
|
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: 'Events', 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
655
|
document.getElementById('live-status').textContent = 'Disconnected';
|
|
665
656
|
document.getElementById('live-status').className = 'badge badge-warn';
|
|
666
|
-
document.getElementById('live-
|
|
657
|
+
document.getElementById('live-btn').textContent = 'Connect';
|
|
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 = 'Live';
|
|
663
|
+
document.getElementById('live-status').className = 'badge badge-live';
|
|
664
|
+
document.getElementById('live-btn').textContent = 'Disconnect';
|
|
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
681
|
document.getElementById('live-status').textContent = 'Error';
|
|
691
682
|
document.getElementById('live-status').className = 'badge badge-block';
|
|
692
683
|
};
|
|
693
684
|
}
|
|
694
|
-
function clearLive() { document.getElementById('live-events').innerHTML = ''; }
|
|
695
|
-
|
|
696
|
-
// Initialize dashboard on page load
|
|
697
|
-
loadStatus();
|
|
698
|
-
loadSessions();
|
|
699
|
-
|
|
700
|
-
// ========== Stats Page with Chart.js ==========
|
|
701
|
-
let statsCharts = {};
|
|
702
|
-
|
|
703
|
-
async function loadStats() {
|
|
704
|
-
try {
|
|
705
|
-
const res = await fetch(API + '/api/stats');
|
|
706
|
-
const stats = await res.json();
|
|
707
|
-
|
|
708
|
-
// Tool Usage Chart (Doughnut)
|
|
709
|
-
const toolCtx = document.getElementById('chart-tool-usage');
|
|
710
|
-
if (statsCharts.toolUsage) statsCharts.toolUsage.destroy();
|
|
711
|
-
statsCharts.toolUsage = new Chart(toolCtx, {
|
|
712
|
-
type: 'doughnut',
|
|
713
|
-
data: {
|
|
714
|
-
labels: Object.keys(stats.toolUsage),
|
|
715
|
-
datasets: [{
|
|
716
|
-
data: Object.values(stats.toolUsage),
|
|
717
|
-
backgroundColor: ['#818cf8', '#34d399', '#fbbf24', '#f87171', '#60a5fa', '#a78bfa']
|
|
718
|
-
}]
|
|
719
|
-
},
|
|
720
|
-
options: {
|
|
721
|
-
responsive: true,
|
|
722
|
-
plugins: {
|
|
723
|
-
legend: { labels: { color: '#e2e8f0' } }
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
});
|
|
727
|
-
|
|
728
|
-
// Decision Levels Chart (Bar)
|
|
729
|
-
const decisionCtx = document.getElementById('chart-decision-levels');
|
|
730
|
-
if (statsCharts.decisionLevels) statsCharts.decisionLevels.destroy();
|
|
731
|
-
statsCharts.decisionLevels = new Chart(decisionCtx, {
|
|
732
|
-
type: 'bar',
|
|
733
|
-
data: {
|
|
734
|
-
labels: Object.keys(stats.decisionLevels),
|
|
735
|
-
datasets: [{
|
|
736
|
-
label: 'Count',
|
|
737
|
-
data: Object.values(stats.decisionLevels),
|
|
738
|
-
backgroundColor: '#818cf8'
|
|
739
|
-
}]
|
|
740
|
-
},
|
|
741
|
-
options: {
|
|
742
|
-
responsive: true,
|
|
743
|
-
plugins: {
|
|
744
|
-
legend: { labels: { color: '#e2e8f0' } }
|
|
745
|
-
},
|
|
746
|
-
scales: {
|
|
747
|
-
y: { ticks: { color: '#94a3b8' }, grid: { color: '#1e293b' } },
|
|
748
|
-
x: { ticks: { color: '#94a3b8' }, grid: { color: '#1e293b' } }
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
});
|
|
752
|
-
|
|
753
|
-
// Daily Activity Chart (Line)
|
|
754
|
-
const activityCtx = document.getElementById('chart-daily-activity');
|
|
755
|
-
if (statsCharts.dailyActivity) statsCharts.dailyActivity.destroy();
|
|
756
|
-
statsCharts.dailyActivity = new Chart(activityCtx, {
|
|
757
|
-
type: 'line',
|
|
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
|
-
}
|
|
778
|
-
});
|
|
779
|
-
} catch (err) {
|
|
780
|
-
console.error('Failed to load stats:', err);
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
// ========== Search and Filter Functions ==========
|
|
785
|
-
let allEvents = [];
|
|
786
|
-
let allRules = [];
|
|
787
|
-
let allInjections = [];
|
|
788
|
-
|
|
789
|
-
async function loadEvents() {
|
|
790
|
-
try {
|
|
791
|
-
const res = await fetch(API + '/api/events?limit=50');
|
|
792
|
-
allEvents = await res.json();
|
|
793
|
-
renderEvents(allEvents);
|
|
794
|
-
} catch {
|
|
795
|
-
document.getElementById('events-body').innerHTML = '<tr><td colspan="4" class="empty">Failed to load</td></tr>';
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
function renderEvents(events) {
|
|
800
|
-
if (events.length === 0) {
|
|
801
|
-
document.getElementById('events-body').innerHTML = '<tr><td colspan="4" class="empty">No events</td></tr>';
|
|
802
|
-
return;
|
|
803
|
-
}
|
|
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
|
-
|
|
816
|
-
function filterEvents() {
|
|
817
|
-
const query = document.getElementById('events-search').value.toLowerCase();
|
|
818
|
-
const filtered = allEvents.filter(e =>
|
|
819
|
-
(e.hook_type || '').toLowerCase().includes(query) ||
|
|
820
|
-
(e.tool_name || '').toLowerCase().includes(query) ||
|
|
821
|
-
(e.project_path || '').toLowerCase().includes(query)
|
|
822
|
-
);
|
|
823
|
-
renderEvents(filtered);
|
|
824
|
-
}
|
|
825
685
|
|
|
686
|
+
// === Init ===
|
|
687
|
+
nav('dashboard');
|
|
688
|
+
</script>
|
|
689
|
+
</body>
|
|
690
|
+
</html>
|
|
826
691
|
async function loadRules() {
|
|
692
|
+
document.getElementById('rules-list').innerHTML = loading();
|
|
827
693
|
try {
|
|
828
694
|
const res = await fetch(API + '/api/rules');
|
|
829
|
-
const
|
|
695
|
+
const data = await res.json();
|
|
830
696
|
allRules = [];
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
(conv.rules || []).forEach(rule => {
|
|
834
|
-
allRules.push({ ...rule, conventionId: id, conventionName: conv.name });
|
|
835
|
-
});
|
|
697
|
+
Object.entries(data).forEach(([id, conv]) => {
|
|
698
|
+
(conv.rules || []).forEach(r => allRules.push({ ...r, _convId: id, _convName: conv.name }));
|
|
836
699
|
});
|
|
837
|
-
|
|
838
700
|
renderRules(allRules);
|
|
839
701
|
} catch {
|
|
840
|
-
document.getElementById('rules-list').innerHTML = '
|
|
702
|
+
document.getElementById('rules-list').innerHTML = empty('Failed to load rules');
|
|
841
703
|
}
|
|
842
704
|
}
|
|
843
705
|
|
|
844
|
-
function renderRules(
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
const tags = (rule.tags || []).map(t => `<span class="tag">${t}</span>`).join('');
|
|
852
|
-
return `<div class="rule-card fade-in" onclick="loadRuleDetail(${idx})">
|
|
853
|
-
<div style="display:flex;justify-content:space-between;align-items:start">
|
|
854
|
-
<div style="flex:1">
|
|
855
|
-
<div style="font-size:0.75rem;color:var(--text-dim);margin-bottom:0.25rem">${rule.conventionName || rule.conventionId}</div>
|
|
856
|
-
<h3 style="margin-bottom:0.5rem">${rule.name || 'Unnamed Rule'}</h3>
|
|
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>
|
|
706
|
+
function renderRules(list) {
|
|
707
|
+
document.getElementById('rules-list').innerHTML = list.length === 0
|
|
708
|
+
? empty('No rules found')
|
|
709
|
+
: list.map(r => `<div class="list-item fade-in" onclick="openRuleDrawer(${JSON.stringify(r).replace(/"/g,'"')})">
|
|
710
|
+
<div style="flex:1;min-width:0">
|
|
711
|
+
<div style="font-size:0.875rem;font-weight:500;margin-bottom:2px">${r.name || r.id || 'Unnamed'}</div>
|
|
712
|
+
<div style="font-size:0.75rem;color:var(--text-dim)">${r._convName || r._convId} ${r.tags ? '· ' + r.tags.join(', ') : ''}</div>
|
|
859
713
|
</div>
|
|
860
|
-
<
|
|
861
|
-
</div
|
|
862
|
-
</div>`;
|
|
863
|
-
}).join('');
|
|
714
|
+
<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>
|
|
715
|
+
</div>`).join('');
|
|
864
716
|
}
|
|
865
717
|
|
|
866
718
|
function filterRules() {
|
|
867
|
-
const
|
|
868
|
-
|
|
869
|
-
(r.name
|
|
870
|
-
(r.description
|
|
871
|
-
(r.
|
|
872
|
-
(r.tags
|
|
873
|
-
);
|
|
874
|
-
renderRules(filtered);
|
|
719
|
+
const q = document.getElementById('rules-search').value.toLowerCase();
|
|
720
|
+
renderRules(allRules.filter(r =>
|
|
721
|
+
(r.name||'').toLowerCase().includes(q) ||
|
|
722
|
+
(r.description||'').toLowerCase().includes(q) ||
|
|
723
|
+
(r._convName||'').toLowerCase().includes(q) ||
|
|
724
|
+
(r.tags||[]).some(t => t.toLowerCase().includes(q))
|
|
725
|
+
));
|
|
875
726
|
}
|
|
876
727
|
|
|
728
|
+
function openRuleDrawer(r) {
|
|
729
|
+
const tags = (r.tags||[]).map(t => `<span class="tag">${t}</span>`).join('');
|
|
730
|
+
const html = `
|
|
731
|
+
<div class="detail-section"><div class="detail-label">Convention</div><div class="detail-value">${r._convName || r._convId}</div></div>
|
|
732
|
+
<div class="detail-section"><div class="detail-label">Name</div><div class="detail-value" style="font-weight:600">${r.name || '—'}</div></div>
|
|
733
|
+
${tags ? `<div class="detail-section"><div class="detail-label">Tags</div><div class="detail-value">${tags}</div></div>` : ''}
|
|
734
|
+
<div class="detail-section"><div class="detail-label">Description</div><div class="detail-value">${r.description || '—'}</div></div>
|
|
735
|
+
${r.when ? `<div class="detail-section"><div class="detail-label">Condition</div><pre class="detail-code">${r.when}</pre></div>` : ''}
|
|
736
|
+
${r.message ? `<div class="detail-section"><div class="detail-label">Message</div><div class="detail-value">${r.message}</div></div>` : ''}
|
|
737
|
+
${r.operator_guidance ? `<div class="detail-section"><div class="detail-label">Guidance</div><div class="detail-value" style="white-space:pre-wrap">${r.operator_guidance}</div></div>` : ''}
|
|
738
|
+
${r.doc_ref ? `<div class="detail-section"><div class="detail-label">Docs</div><div class="detail-value"><a href="${r.doc_ref}" target="_blank" style="color:var(--primary)">${r.doc_ref}</a></div></div>` : ''}
|
|
739
|
+
`;
|
|
740
|
+
openDrawer(r.name || 'Rule', html);
|
|
741
|
+
}
|
|
877
742
|
async function loadInjections() {
|
|
743
|
+
document.getElementById('inj-list').innerHTML = loading();
|
|
878
744
|
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);
|
|
745
|
+
const res = await fetch(API + '/api/injections?limit=100');
|
|
885
746
|
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
747
|
renderInjections(allInjections);
|
|
900
748
|
} catch {
|
|
901
|
-
document.getElementById('
|
|
749
|
+
document.getElementById('inj-list').innerHTML = empty('Failed to load');
|
|
902
750
|
}
|
|
903
751
|
}
|
|
904
752
|
|
|
905
|
-
function renderInjections(
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
<
|
|
918
|
-
|
|
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('');
|
|
753
|
+
function renderInjections(list) {
|
|
754
|
+
document.getElementById('inj-list').innerHTML = list.length === 0
|
|
755
|
+
? empty('No injections found')
|
|
756
|
+
: list.map(inj => `<div class="list-item fade-in" onclick="openInjDrawer(${JSON.stringify(inj).replace(/"/g,'"')})">
|
|
757
|
+
<div style="flex:1;min-width:0">
|
|
758
|
+
<div style="display:flex;align-items:center;gap:0.5rem;margin-bottom:2px">
|
|
759
|
+
<span class="badge badge-info">${inj.injection_type}</span>
|
|
760
|
+
<span style="font-size:0.75rem;color:var(--text-dim)">${inj.source_handler}</span>
|
|
761
|
+
</div>
|
|
762
|
+
<div style="font-size:0.8rem;color:var(--text-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${inj.content.slice(0,80)}</div>
|
|
763
|
+
<div style="font-size:0.75rem;color:var(--text-dim);margin-top:2px">${fmt(inj.timestamp)} · ${inj.session_id.slice(0,8)}</div>
|
|
764
|
+
</div>
|
|
765
|
+
<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>
|
|
766
|
+
</div>`).join('');
|
|
929
767
|
}
|
|
930
768
|
|
|
931
769
|
function filterInjections() {
|
|
932
|
-
const
|
|
933
|
-
const
|
|
934
|
-
|
|
935
|
-
(inj.
|
|
936
|
-
(inj.
|
|
937
|
-
);
|
|
938
|
-
renderInjections(filtered);
|
|
770
|
+
const q = document.getElementById('inj-search').value.toLowerCase();
|
|
771
|
+
const h = document.getElementById('inj-handler').value;
|
|
772
|
+
renderInjections(allInjections.filter(inj =>
|
|
773
|
+
(!h || inj.source_handler === h) &&
|
|
774
|
+
(!q || inj.content.toLowerCase().includes(q) || inj.injection_type.toLowerCase().includes(q))
|
|
775
|
+
));
|
|
939
776
|
}
|
|
940
777
|
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
778
|
+
function openInjDrawer(inj) {
|
|
779
|
+
const html = `
|
|
780
|
+
<div class="detail-section"><div class="detail-label">Type</div><div class="detail-value"><span class="badge badge-info">${inj.injection_type}</span></div></div>
|
|
781
|
+
<div class="detail-section"><div class="detail-label">Handler</div><div class="detail-value">${inj.source_handler}</div></div>
|
|
782
|
+
<div class="detail-section"><div class="detail-label">Time</div><div class="detail-value">${fmt(inj.timestamp)}</div></div>
|
|
783
|
+
<div class="detail-section"><div class="detail-label">Session</div><div class="detail-value" style="font-family:monospace;font-size:0.8rem">${inj.session_id}</div></div>
|
|
784
|
+
<div class="detail-section"><div class="detail-label">Content</div><pre class="detail-code">${inj.content}</pre></div>
|
|
785
|
+
`;
|
|
786
|
+
openDrawer(inj.injection_type, html);
|
|
787
|
+
}
|
|
788
|
+
async function loadEvents() {
|
|
789
|
+
document.getElementById('events-body').innerHTML = '<tr><td colspan="4" class="loading">Loading...</td></tr>';
|
|
790
|
+
try {
|
|
791
|
+
const res = await fetch(API + '/api/events?limit=100');
|
|
792
|
+
allEvents = await res.json();
|
|
793
|
+
renderEvents(allEvents);
|
|
794
|
+
} catch {
|
|
795
|
+
document.getElementById('events-body').innerHTML = '<tr><td colspan="4">' + empty('Failed to load') + '</td></tr>';
|
|
796
|
+
}
|
|
797
|
+
}
|
|
947
798
|
|
|
948
|
-
|
|
799
|
+
function renderEvents(list) {
|
|
800
|
+
document.getElementById('events-body').innerHTML = list.length === 0
|
|
801
|
+
? '<tr><td colspan="4">' + empty('No events') + '</td></tr>'
|
|
802
|
+
: list.map(e => `<tr onclick="openEventDrawer(${JSON.stringify(e).replace(/"/g,'"')})">
|
|
803
|
+
<td style="color:var(--text-dim);font-size:0.8rem">${fmtTime(e.timestamp)}</td>
|
|
804
|
+
<td>${badgeHook(e.hook_type)}</td>
|
|
805
|
+
<td style="font-family:monospace;font-size:0.8rem">${e.tool_name || '—'}</td>
|
|
806
|
+
<td style="color:var(--text-dim);font-size:0.8rem">${(e.project_path||'').split('/').pop() || '—'}</td>
|
|
807
|
+
</tr>`).join('');
|
|
808
|
+
}
|
|
949
809
|
|
|
950
|
-
|
|
951
|
-
const
|
|
810
|
+
function filterEvents() {
|
|
811
|
+
const q = document.getElementById('events-search').value.toLowerCase();
|
|
812
|
+
const t = document.getElementById('events-type').value;
|
|
813
|
+
renderEvents(allEvents.filter(e =>
|
|
814
|
+
(!t || e.hook_type === t) &&
|
|
815
|
+
(!q || (e.hook_type||'').toLowerCase().includes(q) || (e.tool_name||'').toLowerCase().includes(q) || (e.project_path||'').toLowerCase().includes(q))
|
|
816
|
+
));
|
|
817
|
+
}
|
|
952
818
|
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
</div>
|
|
819
|
+
function openEventDrawer(e) {
|
|
820
|
+
const html = `
|
|
821
|
+
<div class="detail-section"><div class="detail-label">Type</div><div class="detail-value">${badgeHook(e.hook_type)}</div></div>
|
|
822
|
+
<div class="detail-section"><div class="detail-label">Time</div><div class="detail-value">${fmt(e.timestamp)}</div></div>
|
|
823
|
+
<div class="detail-section"><div class="detail-label">Tool</div><div class="detail-value" style="font-family:monospace">${e.tool_name || '—'}</div></div>
|
|
824
|
+
<div class="detail-section"><div class="detail-label">Project</div><div class="detail-value" style="font-family:monospace;font-size:0.8rem">${e.project_path || '—'}</div></div>
|
|
825
|
+
<div class="detail-section"><div class="detail-label">Session</div><div class="detail-value" style="font-family:monospace;font-size:0.8rem">${e.session_id || '—'}</div></div>
|
|
826
|
+
${e.tool_input ? `<div class="detail-section"><div class="detail-label">Input</div><pre class="detail-code">${JSON.stringify(e.tool_input, null, 2)}</pre></div>` : ''}
|
|
960
827
|
`;
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
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>
|
|
828
|
+
openDrawer(e.hook_type + ' · ' + (e.tool_name || 'event'), html);
|
|
829
|
+
}
|
|
830
|
+
function sessionListItem(s) {
|
|
831
|
+
const prompt = (s.first_prompt || '(no prompt)').slice(0, 60);
|
|
832
|
+
return `<div class="list-item fade-in" onclick="openSessionDrawer('${s.session_id}')">
|
|
833
|
+
<div style="flex:1;min-width:0">
|
|
834
|
+
<div style="font-size:0.875rem;font-weight:500;margin-bottom:2px">${prompt}</div>
|
|
835
|
+
<div style="font-size:0.75rem;color:var(--text-dim)">${fmt(s.start_time)} · ${s.event_count} events · ${s.session_id.slice(0,8)}</div>
|
|
983
836
|
</div>
|
|
837
|
+
<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>
|
|
984
838
|
</div>`;
|
|
839
|
+
}
|
|
985
840
|
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
841
|
+
async function loadSessions() {
|
|
842
|
+
document.getElementById('sessions-list').innerHTML = loading();
|
|
843
|
+
try {
|
|
844
|
+
const res = await fetch(API + '/api/sessions?limit=50');
|
|
845
|
+
allSessions = await res.json();
|
|
846
|
+
renderSessions(allSessions);
|
|
847
|
+
} catch {
|
|
848
|
+
document.getElementById('sessions-list').innerHTML = empty('Failed to load sessions');
|
|
991
849
|
}
|
|
850
|
+
}
|
|
992
851
|
|
|
993
|
-
|
|
852
|
+
function renderSessions(list) {
|
|
853
|
+
document.getElementById('sessions-list').innerHTML = list.length === 0
|
|
854
|
+
? empty('No sessions found')
|
|
855
|
+
: list.map(s => sessionListItem(s)).join('');
|
|
994
856
|
}
|
|
995
857
|
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
858
|
+
function filterSessions() {
|
|
859
|
+
const q = document.getElementById('sessions-search').value.toLowerCase();
|
|
860
|
+
renderSessions(allSessions.filter(s =>
|
|
861
|
+
(s.first_prompt || '').toLowerCase().includes(q) ||
|
|
862
|
+
s.session_id.toLowerCase().includes(q)
|
|
863
|
+
));
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
async function openSessionDrawer(sessionId) {
|
|
867
|
+
openDrawer('Session Detail', loading());
|
|
868
|
+
try {
|
|
869
|
+
const res = await fetch(API + '/api/sessions/' + sessionId + '/detail');
|
|
870
|
+
const data = await res.json();
|
|
871
|
+
const s = data.session;
|
|
872
|
+
let html = `
|
|
873
|
+
<div class="detail-section">
|
|
874
|
+
<div class="detail-label">Session ID</div>
|
|
875
|
+
<div class="detail-value" style="font-family:monospace;font-size:0.8rem">${s.session_id}</div>
|
|
876
|
+
</div>
|
|
877
|
+
<div class="detail-section">
|
|
878
|
+
<div class="detail-label">Time</div>
|
|
879
|
+
<div class="detail-value">${fmt(s.start_time)} → ${fmt(s.end_time)}</div>
|
|
880
|
+
</div>
|
|
881
|
+
<div class="detail-section">
|
|
882
|
+
<div class="detail-label">First Prompt</div>
|
|
883
|
+
<div class="detail-value">${s.first_prompt || '—'}</div>
|
|
884
|
+
</div>
|
|
885
|
+
<div class="detail-section">
|
|
886
|
+
<div class="detail-label">Tasks (${data.tasks.length})</div>`;
|
|
887
|
+
data.tasks.forEach((t, i) => {
|
|
888
|
+
const tools = t.events.filter(e => e.tool_name).length;
|
|
889
|
+
html += `<div style="padding:0.75rem;background:var(--bg-secondary);border-radius:var(--radius-sm);margin-bottom:0.5rem;border:1px solid var(--border)">
|
|
890
|
+
<div style="font-weight:500;font-size:0.875rem;margin-bottom:4px">Task ${i+1}: ${t.title}</div>
|
|
891
|
+
<div style="font-size:0.75rem;color:var(--text-dim)">${fmtTime(t.start_time)} · ${tools} tools · ${t.summary.filesChanged.length} files</div>
|
|
892
|
+
${t.summary.commits.length > 0 ? `<div style="font-size:0.75rem;color:var(--green);margin-top:4px">✓ ${t.summary.commits.length} commit(s)</div>` : ''}
|
|
893
|
+
</div>`;
|
|
894
|
+
});
|
|
895
|
+
html += `</div>`;
|
|
896
|
+
openDrawer('Session · ' + s.session_id.slice(0,8), html);
|
|
897
|
+
} catch(e) {
|
|
898
|
+
openDrawer('Session Detail', empty('Failed to load: ' + e.message));
|
|
899
|
+
}
|
|
900
|
+
}
|