@nockdev/hsa 1.2.0 → 1.2.2
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/dashboard.html +34 -16
- package/dist/hsa-cli.bundle.js +1 -1
- package/dist/hsa-daemon.bundle.js +1 -1
- package/dist/hsa-http.bundle.js +1 -1
- package/dist/hsa-lib.bundle.js +1 -1
- package/dist/integrity.json +214 -208
- package/logs.html +1018 -480
- package/package.json +1 -1
package/logs.html
CHANGED
|
@@ -1,492 +1,1030 @@
|
|
|
1
|
-
<!
|
|
1
|
+
<!doctype html>
|
|
2
2
|
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8"
|
|
5
|
-
<meta name="viewport" content="width=device-width,initial-scale=1"
|
|
6
|
-
<title>HSA Logs</title>
|
|
7
|
-
<link rel="preconnect" href="https://fonts.googleapis.com"
|
|
8
|
-
<link
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
.
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
.
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
.
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
.
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
.
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
.
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
6
|
+
<title>HSA Logs</title>
|
|
7
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
8
|
+
<link
|
|
9
|
+
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap"
|
|
10
|
+
rel="stylesheet"
|
|
11
|
+
/>
|
|
12
|
+
<style>
|
|
13
|
+
* {
|
|
14
|
+
margin: 0;
|
|
15
|
+
padding: 0;
|
|
16
|
+
box-sizing: border-box;
|
|
17
|
+
}
|
|
18
|
+
:root {
|
|
19
|
+
--bg: #0d1117;
|
|
20
|
+
--surface: #161b22;
|
|
21
|
+
--surface2: #1c2129;
|
|
22
|
+
--border: rgba(240, 246, 252, 0.1);
|
|
23
|
+
--border-h: rgba(240, 246, 252, 0.2);
|
|
24
|
+
--text: #e6edf3;
|
|
25
|
+
--text2: #8b949e;
|
|
26
|
+
--text3: #6e7681;
|
|
27
|
+
--ok: #3fb950;
|
|
28
|
+
--err: #f85149;
|
|
29
|
+
--warn: #d29922;
|
|
30
|
+
--accent: #388bfd;
|
|
31
|
+
--accent-subtle: rgba(56, 139, 253, 0.1);
|
|
32
|
+
--font: Inter, system-ui, sans-serif;
|
|
33
|
+
--mono: "JetBrains Mono", monospace;
|
|
34
|
+
}
|
|
35
|
+
html,
|
|
36
|
+
body {
|
|
37
|
+
background: var(--bg);
|
|
38
|
+
color: var(--text);
|
|
39
|
+
font-family: var(--font);
|
|
40
|
+
font-size: 14px;
|
|
41
|
+
height: 100%;
|
|
42
|
+
}
|
|
43
|
+
a {
|
|
44
|
+
color: var(--accent);
|
|
45
|
+
text-decoration: none;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/* Layout */
|
|
49
|
+
.app {
|
|
50
|
+
display: flex;
|
|
51
|
+
flex-direction: column;
|
|
52
|
+
height: 100vh;
|
|
53
|
+
max-width: 1400px;
|
|
54
|
+
margin: 0 auto;
|
|
55
|
+
padding: 0 20px;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/* Header */
|
|
59
|
+
.header {
|
|
60
|
+
display: flex;
|
|
61
|
+
align-items: center;
|
|
62
|
+
gap: 12px;
|
|
63
|
+
padding: 14px 0;
|
|
64
|
+
border-bottom: 1px solid var(--border);
|
|
65
|
+
flex-shrink: 0;
|
|
66
|
+
}
|
|
67
|
+
.header h1 {
|
|
68
|
+
font-size: 15px;
|
|
69
|
+
font-weight: 600;
|
|
70
|
+
white-space: nowrap;
|
|
71
|
+
display: flex;
|
|
72
|
+
align-items: center;
|
|
73
|
+
gap: 8px;
|
|
74
|
+
}
|
|
75
|
+
.status-dot {
|
|
76
|
+
width: 8px;
|
|
77
|
+
height: 8px;
|
|
78
|
+
border-radius: 50%;
|
|
79
|
+
flex-shrink: 0;
|
|
80
|
+
}
|
|
81
|
+
.status-dot.ok {
|
|
82
|
+
background: var(--ok);
|
|
83
|
+
box-shadow: 0 0 6px var(--ok);
|
|
84
|
+
}
|
|
85
|
+
.status-dot.err {
|
|
86
|
+
background: var(--err);
|
|
87
|
+
box-shadow: 0 0 6px var(--err);
|
|
88
|
+
}
|
|
89
|
+
.status-dot.pending {
|
|
90
|
+
background: var(--warn);
|
|
91
|
+
animation: pulse 1.5s infinite;
|
|
92
|
+
}
|
|
93
|
+
@keyframes pulse {
|
|
94
|
+
0%,
|
|
95
|
+
100% {
|
|
96
|
+
opacity: 1;
|
|
97
|
+
}
|
|
98
|
+
50% {
|
|
99
|
+
opacity: 0.4;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
.header-controls {
|
|
103
|
+
display: flex;
|
|
104
|
+
gap: 8px;
|
|
105
|
+
align-items: center;
|
|
106
|
+
margin-left: auto;
|
|
107
|
+
}
|
|
108
|
+
.header-controls input {
|
|
109
|
+
background: var(--surface);
|
|
110
|
+
border: 1px solid var(--border);
|
|
111
|
+
color: var(--text);
|
|
112
|
+
padding: 5px 10px;
|
|
113
|
+
border-radius: 6px;
|
|
114
|
+
font: inherit;
|
|
115
|
+
font-size: 12px;
|
|
116
|
+
width: 200px;
|
|
117
|
+
}
|
|
118
|
+
.header-controls input:focus {
|
|
119
|
+
outline: none;
|
|
120
|
+
border-color: var(--accent);
|
|
121
|
+
}
|
|
122
|
+
.btn {
|
|
123
|
+
background: var(--surface);
|
|
124
|
+
border: 1px solid var(--border);
|
|
125
|
+
color: var(--text2);
|
|
126
|
+
padding: 5px 10px;
|
|
127
|
+
border-radius: 6px;
|
|
128
|
+
cursor: pointer;
|
|
129
|
+
font: inherit;
|
|
130
|
+
font-size: 12px;
|
|
131
|
+
font-weight: 500;
|
|
132
|
+
transition: all 0.15s;
|
|
133
|
+
display: inline-flex;
|
|
134
|
+
align-items: center;
|
|
135
|
+
gap: 4px;
|
|
136
|
+
}
|
|
137
|
+
.btn:hover {
|
|
138
|
+
border-color: var(--border-h);
|
|
139
|
+
color: var(--text);
|
|
140
|
+
}
|
|
141
|
+
.btn.active {
|
|
142
|
+
border-color: var(--accent);
|
|
143
|
+
color: var(--accent);
|
|
144
|
+
background: var(--accent-subtle);
|
|
145
|
+
}
|
|
146
|
+
.nav-link {
|
|
147
|
+
font-size: 12px;
|
|
148
|
+
color: var(--text2);
|
|
149
|
+
padding: 4px 8px;
|
|
150
|
+
border-radius: 4px;
|
|
151
|
+
transition: all 0.15s;
|
|
152
|
+
}
|
|
153
|
+
.nav-link:hover {
|
|
154
|
+
color: var(--text);
|
|
155
|
+
background: var(--surface);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/* Context Selector */
|
|
159
|
+
.context-bar {
|
|
160
|
+
display: flex;
|
|
161
|
+
gap: 12px;
|
|
162
|
+
padding: 8px 0;
|
|
163
|
+
border-bottom: 1px solid var(--border);
|
|
164
|
+
flex-shrink: 0;
|
|
165
|
+
flex-wrap: wrap;
|
|
166
|
+
align-items: center;
|
|
167
|
+
}
|
|
168
|
+
.context-bar label {
|
|
169
|
+
font-size: 10px;
|
|
170
|
+
color: var(--text3);
|
|
171
|
+
text-transform: uppercase;
|
|
172
|
+
letter-spacing: 0.5px;
|
|
173
|
+
font-weight: 600;
|
|
174
|
+
}
|
|
175
|
+
.ctx-select {
|
|
176
|
+
background: var(--surface);
|
|
177
|
+
border: 1px solid var(--border);
|
|
178
|
+
color: var(--text);
|
|
179
|
+
font-family: var(--font);
|
|
180
|
+
font-size: 12px;
|
|
181
|
+
padding: 3px 22px 3px 8px;
|
|
182
|
+
border-radius: 12px;
|
|
183
|
+
cursor: pointer;
|
|
184
|
+
transition: border 0.15s;
|
|
185
|
+
appearance: none;
|
|
186
|
+
-webkit-appearance: none;
|
|
187
|
+
background-image: url("data:image/svg+xml,%3Csvg width='10' height='6' viewBox='0 0 10 6' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1L5 5L9 1' stroke='%238b949e' stroke-width='1.5' stroke-linecap='round'/%3E%3C/svg%3E");
|
|
188
|
+
background-repeat: no-repeat;
|
|
189
|
+
background-position: right 6px center;
|
|
190
|
+
}
|
|
191
|
+
.ctx-select:focus {
|
|
192
|
+
outline: none;
|
|
193
|
+
border-color: var(--accent);
|
|
194
|
+
}
|
|
195
|
+
.ctx-group {
|
|
196
|
+
display: flex;
|
|
197
|
+
align-items: center;
|
|
198
|
+
gap: 5px;
|
|
199
|
+
}
|
|
200
|
+
.ctx-sep {
|
|
201
|
+
width: 1px;
|
|
202
|
+
height: 18px;
|
|
203
|
+
background: var(--border);
|
|
204
|
+
margin: 0 3px;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/* Stats Bar */
|
|
208
|
+
.stats-bar {
|
|
209
|
+
display: flex;
|
|
210
|
+
gap: 24px;
|
|
211
|
+
padding: 10px 0;
|
|
212
|
+
border-bottom: 1px solid var(--border);
|
|
213
|
+
flex-shrink: 0;
|
|
214
|
+
flex-wrap: wrap;
|
|
215
|
+
}
|
|
216
|
+
.stat {
|
|
217
|
+
display: flex;
|
|
218
|
+
align-items: center;
|
|
219
|
+
gap: 5px;
|
|
220
|
+
font-size: 12px;
|
|
221
|
+
color: var(--text2);
|
|
222
|
+
}
|
|
223
|
+
.stat-value {
|
|
224
|
+
font-weight: 600;
|
|
225
|
+
color: var(--text);
|
|
226
|
+
font-variant-numeric: tabular-nums;
|
|
227
|
+
}
|
|
228
|
+
.stat-value.err {
|
|
229
|
+
color: var(--err);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/* Filter Bar */
|
|
233
|
+
.filter-bar {
|
|
234
|
+
display: flex;
|
|
235
|
+
gap: 8px;
|
|
236
|
+
padding: 8px 0;
|
|
237
|
+
border-bottom: 1px solid var(--border);
|
|
238
|
+
flex-shrink: 0;
|
|
239
|
+
flex-wrap: wrap;
|
|
240
|
+
align-items: center;
|
|
241
|
+
}
|
|
242
|
+
.filter-bar select {
|
|
243
|
+
background: var(--surface);
|
|
244
|
+
border: 1px solid var(--border);
|
|
245
|
+
color: var(--text);
|
|
246
|
+
padding: 4px 8px;
|
|
247
|
+
border-radius: 6px;
|
|
248
|
+
font: inherit;
|
|
249
|
+
font-size: 12px;
|
|
250
|
+
}
|
|
251
|
+
.filter-bar select:focus {
|
|
252
|
+
outline: none;
|
|
253
|
+
border-color: var(--accent);
|
|
254
|
+
}
|
|
255
|
+
.filter-bar input {
|
|
256
|
+
background: var(--surface);
|
|
257
|
+
border: 1px solid var(--border);
|
|
258
|
+
color: var(--text);
|
|
259
|
+
padding: 4px 10px;
|
|
260
|
+
border-radius: 6px;
|
|
261
|
+
font: inherit;
|
|
262
|
+
font-size: 12px;
|
|
263
|
+
flex: 1;
|
|
264
|
+
min-width: 150px;
|
|
265
|
+
}
|
|
266
|
+
.filter-bar input:focus {
|
|
267
|
+
outline: none;
|
|
268
|
+
border-color: var(--accent);
|
|
269
|
+
}
|
|
270
|
+
.filter-count {
|
|
271
|
+
font-size: 11px;
|
|
272
|
+
color: var(--text3);
|
|
273
|
+
margin-left: auto;
|
|
274
|
+
font-family: var(--mono);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/* Log Table */
|
|
278
|
+
.log-container {
|
|
279
|
+
flex: 1;
|
|
280
|
+
overflow-y: auto;
|
|
281
|
+
min-height: 0;
|
|
282
|
+
}
|
|
283
|
+
.log-table {
|
|
284
|
+
width: 100%;
|
|
285
|
+
}
|
|
286
|
+
/* 5-column grid: Time | IDE | Tool | Status | Duration */
|
|
287
|
+
.log-row {
|
|
288
|
+
display: grid;
|
|
289
|
+
grid-template-columns: 95px 80px 1fr 65px 75px;
|
|
290
|
+
gap: 10px;
|
|
291
|
+
padding: 7px 8px;
|
|
292
|
+
border-bottom: 1px solid var(--border);
|
|
293
|
+
align-items: center;
|
|
294
|
+
cursor: pointer;
|
|
295
|
+
transition: background 0.1s;
|
|
296
|
+
}
|
|
297
|
+
.log-row:hover {
|
|
298
|
+
background: var(--surface);
|
|
299
|
+
}
|
|
300
|
+
.log-row.expanded {
|
|
301
|
+
background: var(--surface);
|
|
302
|
+
border-bottom: none;
|
|
303
|
+
}
|
|
304
|
+
.log-row.error {
|
|
305
|
+
border-left: 2px solid var(--err);
|
|
306
|
+
}
|
|
307
|
+
.log-row.slow {
|
|
308
|
+
border-left: 2px solid var(--warn);
|
|
309
|
+
}
|
|
310
|
+
.log-time {
|
|
311
|
+
font-family: var(--mono);
|
|
312
|
+
font-size: 11px;
|
|
313
|
+
color: var(--text2);
|
|
314
|
+
white-space: nowrap;
|
|
315
|
+
}
|
|
316
|
+
.log-ide {
|
|
317
|
+
font-size: 10px;
|
|
318
|
+
overflow: hidden;
|
|
319
|
+
text-overflow: ellipsis;
|
|
320
|
+
white-space: nowrap;
|
|
321
|
+
}
|
|
322
|
+
.log-tool {
|
|
323
|
+
font-family: var(--mono);
|
|
324
|
+
font-size: 12px;
|
|
325
|
+
font-weight: 500;
|
|
326
|
+
overflow: hidden;
|
|
327
|
+
text-overflow: ellipsis;
|
|
328
|
+
white-space: nowrap;
|
|
329
|
+
}
|
|
330
|
+
.log-status {
|
|
331
|
+
font-size: 11px;
|
|
332
|
+
font-weight: 500;
|
|
333
|
+
text-align: center;
|
|
334
|
+
padding: 2px 6px;
|
|
335
|
+
border-radius: 10px;
|
|
336
|
+
display: inline-block;
|
|
337
|
+
}
|
|
338
|
+
.log-status.ok {
|
|
339
|
+
color: var(--ok);
|
|
340
|
+
background: rgba(63, 185, 80, 0.1);
|
|
341
|
+
}
|
|
342
|
+
.log-status.error {
|
|
343
|
+
color: var(--err);
|
|
344
|
+
background: rgba(248, 81, 73, 0.1);
|
|
345
|
+
}
|
|
346
|
+
.log-duration {
|
|
347
|
+
font-family: var(--mono);
|
|
348
|
+
font-size: 11px;
|
|
349
|
+
color: var(--text2);
|
|
350
|
+
text-align: right;
|
|
351
|
+
}
|
|
352
|
+
.log-duration.slow {
|
|
353
|
+
color: var(--warn);
|
|
354
|
+
font-weight: 600;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/* IDE Badges */
|
|
358
|
+
.ide-badge {
|
|
359
|
+
display: inline-flex;
|
|
360
|
+
align-items: center;
|
|
361
|
+
gap: 3px;
|
|
362
|
+
font-size: 10px;
|
|
363
|
+
font-weight: 500;
|
|
364
|
+
padding: 1px 6px;
|
|
365
|
+
border-radius: 8px;
|
|
366
|
+
border: 1px solid;
|
|
367
|
+
white-space: nowrap;
|
|
368
|
+
}
|
|
369
|
+
.ide-badge[data-ide="cursor"] {
|
|
370
|
+
background: rgba(168, 85, 247, 0.12);
|
|
371
|
+
border-color: rgba(168, 85, 247, 0.3);
|
|
372
|
+
color: #a855f7;
|
|
373
|
+
}
|
|
374
|
+
.ide-badge[data-ide="vscode"] {
|
|
375
|
+
background: rgba(56, 139, 253, 0.12);
|
|
376
|
+
border-color: rgba(56, 139, 253, 0.3);
|
|
377
|
+
color: #388bfd;
|
|
378
|
+
}
|
|
379
|
+
.ide-badge[data-ide="antigravity"] {
|
|
380
|
+
background: rgba(63, 185, 80, 0.12);
|
|
381
|
+
border-color: rgba(63, 185, 80, 0.3);
|
|
382
|
+
color: #3fb950;
|
|
383
|
+
}
|
|
384
|
+
.ide-badge[data-ide="windsurf"] {
|
|
385
|
+
background: rgba(210, 153, 34, 0.12);
|
|
386
|
+
border-color: rgba(210, 153, 34, 0.3);
|
|
387
|
+
color: #d29922;
|
|
388
|
+
}
|
|
389
|
+
.ide-badge[data-ide="claude-desktop"] {
|
|
390
|
+
background: rgba(248, 81, 73, 0.12);
|
|
391
|
+
border-color: rgba(248, 81, 73, 0.3);
|
|
392
|
+
color: #f85149;
|
|
393
|
+
}
|
|
394
|
+
.ide-badge[data-ide="gemini-cli"] {
|
|
395
|
+
background: rgba(56, 139, 253, 0.12);
|
|
396
|
+
border-color: rgba(56, 139, 253, 0.3);
|
|
397
|
+
color: #388bfd;
|
|
398
|
+
}
|
|
399
|
+
.ide-badge[data-ide="codex"] {
|
|
400
|
+
background: rgba(63, 185, 80, 0.12);
|
|
401
|
+
border-color: rgba(63, 185, 80, 0.3);
|
|
402
|
+
color: #3fb950;
|
|
403
|
+
}
|
|
404
|
+
.ide-badge[data-ide="neovim"] {
|
|
405
|
+
background: rgba(63, 185, 80, 0.12);
|
|
406
|
+
border-color: rgba(63, 185, 80, 0.3);
|
|
407
|
+
color: #3fb950;
|
|
408
|
+
}
|
|
409
|
+
.ide-badge[data-ide="zed"] {
|
|
410
|
+
background: rgba(247, 120, 186, 0.12);
|
|
411
|
+
border-color: rgba(247, 120, 186, 0.3);
|
|
412
|
+
color: #f778ba;
|
|
413
|
+
}
|
|
414
|
+
.ide-badge[data-ide="unknown"] {
|
|
415
|
+
background: rgba(139, 148, 158, 0.12);
|
|
416
|
+
border-color: rgba(139, 148, 158, 0.3);
|
|
417
|
+
color: #8b949e;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/* Detail Panel */
|
|
421
|
+
.log-detail {
|
|
422
|
+
background: var(--surface2);
|
|
423
|
+
border-bottom: 1px solid var(--border);
|
|
424
|
+
padding: 12px 16px;
|
|
425
|
+
display: none;
|
|
426
|
+
}
|
|
427
|
+
.log-detail.show {
|
|
428
|
+
display: block;
|
|
429
|
+
}
|
|
430
|
+
.detail-section {
|
|
431
|
+
margin-bottom: 10px;
|
|
432
|
+
}
|
|
433
|
+
.detail-section:last-child {
|
|
434
|
+
margin-bottom: 0;
|
|
435
|
+
}
|
|
436
|
+
.detail-label {
|
|
437
|
+
font-size: 10px;
|
|
438
|
+
text-transform: uppercase;
|
|
439
|
+
letter-spacing: 0.5px;
|
|
440
|
+
color: var(--text3);
|
|
441
|
+
margin-bottom: 4px;
|
|
442
|
+
font-weight: 600;
|
|
443
|
+
}
|
|
444
|
+
.detail-content {
|
|
445
|
+
font-family: var(--mono);
|
|
446
|
+
font-size: 11px;
|
|
447
|
+
color: var(--text2);
|
|
448
|
+
background: var(--bg);
|
|
449
|
+
padding: 8px 10px;
|
|
450
|
+
border-radius: 6px;
|
|
451
|
+
border: 1px solid var(--border);
|
|
452
|
+
white-space: pre-wrap;
|
|
453
|
+
word-break: break-all;
|
|
454
|
+
max-height: 200px;
|
|
455
|
+
overflow-y: auto;
|
|
456
|
+
line-height: 1.5;
|
|
457
|
+
}
|
|
458
|
+
.detail-content.err {
|
|
459
|
+
border-color: var(--err);
|
|
460
|
+
color: var(--err);
|
|
461
|
+
}
|
|
462
|
+
.detail-meta {
|
|
463
|
+
display: flex;
|
|
464
|
+
gap: 14px;
|
|
465
|
+
flex-wrap: wrap;
|
|
466
|
+
}
|
|
467
|
+
.detail-meta span {
|
|
468
|
+
font-size: 11px;
|
|
469
|
+
color: var(--text3);
|
|
470
|
+
}
|
|
471
|
+
.detail-meta strong {
|
|
472
|
+
color: var(--text2);
|
|
473
|
+
font-weight: 500;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/* Empty State */
|
|
477
|
+
.empty-state {
|
|
478
|
+
display: flex;
|
|
479
|
+
flex-direction: column;
|
|
480
|
+
align-items: center;
|
|
481
|
+
justify-content: center;
|
|
482
|
+
padding: 60px 20px;
|
|
483
|
+
color: var(--text3);
|
|
484
|
+
}
|
|
485
|
+
.empty-state .icon {
|
|
486
|
+
font-size: 40px;
|
|
487
|
+
margin-bottom: 12px;
|
|
488
|
+
opacity: 0.5;
|
|
489
|
+
}
|
|
490
|
+
.empty-state p {
|
|
491
|
+
font-size: 14px;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/* Responsive */
|
|
495
|
+
@media (max-width: 768px) {
|
|
496
|
+
.header {
|
|
497
|
+
flex-wrap: wrap;
|
|
498
|
+
}
|
|
499
|
+
.header-controls {
|
|
500
|
+
width: 100%;
|
|
501
|
+
}
|
|
502
|
+
.header-controls input {
|
|
503
|
+
flex: 1;
|
|
504
|
+
}
|
|
505
|
+
.stats-bar {
|
|
506
|
+
gap: 12px;
|
|
507
|
+
}
|
|
508
|
+
/* Hide IDE column on mobile */
|
|
509
|
+
.log-row {
|
|
510
|
+
grid-template-columns: 80px 1fr 50px 60px;
|
|
511
|
+
gap: 6px;
|
|
512
|
+
font-size: 12px;
|
|
513
|
+
}
|
|
514
|
+
.log-ide {
|
|
515
|
+
display: none;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
</style>
|
|
519
|
+
</head>
|
|
520
|
+
<body>
|
|
521
|
+
<div class="app">
|
|
522
|
+
<!-- Header -->
|
|
523
|
+
<div class="header">
|
|
524
|
+
<div class="status-dot pending" id="statusDot"></div>
|
|
525
|
+
<h1>🔍 HSA Logs</h1>
|
|
526
|
+
<a href="/dashboard" class="nav-link">← Dashboard</a>
|
|
527
|
+
<div class="header-controls">
|
|
528
|
+
<input
|
|
529
|
+
type="text"
|
|
530
|
+
id="endpoint"
|
|
531
|
+
value="http://localhost:13100"
|
|
532
|
+
placeholder="HSA endpoint"
|
|
533
|
+
/>
|
|
534
|
+
<button class="btn" id="connectBtn" onclick="connect()">
|
|
535
|
+
Connect
|
|
536
|
+
</button>
|
|
537
|
+
<button class="btn" id="streamBtn" onclick="toggleStream()">
|
|
538
|
+
▶ Stream
|
|
539
|
+
</button>
|
|
540
|
+
<button class="btn" onclick="clearLogs()">Clear</button>
|
|
541
|
+
</div>
|
|
542
|
+
</div>
|
|
543
|
+
|
|
544
|
+
<!-- Context Selector Bar -->
|
|
545
|
+
<div class="context-bar">
|
|
546
|
+
<div class="ctx-group">
|
|
547
|
+
<label>IDE</label>
|
|
548
|
+
<select class="ctx-select" id="ctxIde" onchange="applyFilters()">
|
|
549
|
+
<option value="">All IDEs</option>
|
|
550
|
+
</select>
|
|
551
|
+
</div>
|
|
552
|
+
<span class="ctx-sep"></span>
|
|
553
|
+
<div class="ctx-group">
|
|
554
|
+
<label>Project</label>
|
|
555
|
+
<select class="ctx-select" id="ctxProject" onchange="applyFilters()">
|
|
556
|
+
<option value="">All Projects</option>
|
|
557
|
+
</select>
|
|
558
|
+
</div>
|
|
559
|
+
<span class="ctx-sep"></span>
|
|
560
|
+
<div class="ctx-group">
|
|
561
|
+
<label>Session</label>
|
|
562
|
+
<select class="ctx-select" id="ctxSession" onchange="applyFilters()">
|
|
563
|
+
<option value="">All Sessions</option>
|
|
564
|
+
</select>
|
|
565
|
+
</div>
|
|
566
|
+
</div>
|
|
567
|
+
|
|
568
|
+
<!-- Stats Bar -->
|
|
569
|
+
<div class="stats-bar">
|
|
570
|
+
<div class="stat">
|
|
571
|
+
📊 <span class="stat-value" id="statTotal">0</span> calls
|
|
572
|
+
</div>
|
|
573
|
+
<div class="stat">
|
|
574
|
+
❌ <span class="stat-value err" id="statErrors">0</span> errors
|
|
575
|
+
</div>
|
|
576
|
+
<div class="stat">
|
|
577
|
+
⚡ <span class="stat-value" id="statAvg">0</span>ms avg
|
|
578
|
+
</div>
|
|
579
|
+
<div class="stat">
|
|
580
|
+
📈 <span class="stat-value" id="statRate">0</span> calls/min
|
|
581
|
+
</div>
|
|
582
|
+
</div>
|
|
583
|
+
|
|
584
|
+
<!-- Filter Bar -->
|
|
585
|
+
<div class="filter-bar">
|
|
586
|
+
<select id="filterTool" onchange="applyFilters()">
|
|
587
|
+
<option value="">All Tools</option>
|
|
588
|
+
</select>
|
|
589
|
+
<select id="filterStatus" onchange="applyFilters()">
|
|
590
|
+
<option value="">All Status</option>
|
|
591
|
+
<option value="ok">✓ OK</option>
|
|
592
|
+
<option value="error">✗ Error</option>
|
|
593
|
+
</select>
|
|
594
|
+
<select id="filterTransport" onchange="applyFilters()">
|
|
595
|
+
<option value="">All Transports</option>
|
|
596
|
+
<option value="stdio">stdio</option>
|
|
597
|
+
<option value="http">http</option>
|
|
598
|
+
</select>
|
|
599
|
+
<input
|
|
600
|
+
type="text"
|
|
601
|
+
id="filterSearch"
|
|
602
|
+
placeholder="🔍 Search logs..."
|
|
603
|
+
oninput="debounceFilter()"
|
|
604
|
+
/>
|
|
605
|
+
<span class="filter-count" id="filterCount"></span>
|
|
606
|
+
</div>
|
|
607
|
+
|
|
608
|
+
<!-- Log Container -->
|
|
609
|
+
<div class="log-container" id="logContainer">
|
|
610
|
+
<div class="empty-state" id="emptyState">
|
|
611
|
+
<div class="icon">📋</div>
|
|
612
|
+
<p>
|
|
613
|
+
No log entries yet. Connect to an HSA endpoint to start streaming.
|
|
614
|
+
</p>
|
|
615
|
+
</div>
|
|
616
|
+
<div class="log-table" id="logTable"></div>
|
|
617
|
+
</div>
|
|
194
618
|
</div>
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
let
|
|
202
|
-
let
|
|
203
|
-
let
|
|
204
|
-
let
|
|
205
|
-
let
|
|
206
|
-
let
|
|
207
|
-
let
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
setTimeout(() => connect(), 300);
|
|
214
|
-
|
|
215
|
-
// ── Connection ────────────────────────────────────────────
|
|
216
|
-
async function connect() {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
if (refreshTimer) clearInterval(refreshTimer);
|
|
238
|
-
refreshTimer = setInterval(() => { fetchStats(); fetchSessions(); }, 5000);
|
|
239
|
-
} catch (e) {
|
|
240
|
-
setStatus('err');
|
|
241
|
-
console.error('Connection failed:', e);
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
function setStatus(s) {
|
|
246
|
-
document.getElementById('statusDot').className = 'status-dot ' + s;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// ── Sessions for Context ──────────────────────────────────
|
|
250
|
-
async function fetchSessions() {
|
|
251
|
-
if (!baseUrl) return;
|
|
252
|
-
try {
|
|
253
|
-
const res = await fetch(baseUrl + '/api/sessions', { signal: AbortSignal.timeout(5000) });
|
|
254
|
-
if (!res.ok) return;
|
|
255
|
-
const data = await res.json();
|
|
256
|
-
allSessions = data.sessions || [];
|
|
257
|
-
populateSessionDropdown(allSessions);
|
|
258
|
-
} catch {}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
function populateSessionDropdown(sessions) {
|
|
262
|
-
const sel = document.getElementById('ctxSession');
|
|
263
|
-
const cur = sel.value;
|
|
264
|
-
sel.innerHTML = '<option value="">All Sessions</option>';
|
|
265
|
-
sessions.forEach(s => {
|
|
266
|
-
const opt = document.createElement('option');
|
|
267
|
-
opt.value = s.sessionId;
|
|
268
|
-
opt.textContent = `${s.projectName} (${s.ideName})`;
|
|
269
|
-
sel.appendChild(opt);
|
|
270
|
-
});
|
|
271
|
-
sel.value = cur;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
function updateContextSelectors(ides, projects) {
|
|
275
|
-
const ideSelect = document.getElementById('ctxIde');
|
|
276
|
-
const projSelect = document.getElementById('ctxProject');
|
|
277
|
-
const curIde = ideSelect.value;
|
|
278
|
-
const curProj = projSelect.value;
|
|
279
|
-
|
|
280
|
-
ideSelect.innerHTML = '<option value="">All IDEs</option>';
|
|
281
|
-
ides.forEach(i => {
|
|
282
|
-
const opt = document.createElement('option');
|
|
283
|
-
opt.value = i; opt.textContent = i;
|
|
284
|
-
ideSelect.appendChild(opt);
|
|
285
|
-
});
|
|
286
|
-
ideSelect.value = curIde;
|
|
287
|
-
|
|
288
|
-
projSelect.innerHTML = '<option value="">All Projects</option>';
|
|
289
|
-
projects.forEach(p => {
|
|
290
|
-
const opt = document.createElement('option');
|
|
291
|
-
opt.value = p; opt.textContent = p;
|
|
292
|
-
projSelect.appendChild(opt);
|
|
293
|
-
});
|
|
294
|
-
projSelect.value = curProj;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
// ── SSE Streaming ─────────────────────────────────────────
|
|
298
|
-
function toggleStream() {
|
|
299
|
-
if (isStreaming) stopStream();
|
|
300
|
-
else startStream();
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
function startStream() {
|
|
304
|
-
if (!baseUrl) return;
|
|
305
|
-
if (eventSource) eventSource.close();
|
|
306
|
-
try {
|
|
307
|
-
eventSource = new EventSource(baseUrl + '/api/logs/stream');
|
|
308
|
-
eventSource.onmessage = (event) => {
|
|
309
|
-
try {
|
|
310
|
-
const data = JSON.parse(event.data);
|
|
311
|
-
if (data.type === 'entry') {
|
|
312
|
-
entries.unshift(data.entry);
|
|
313
|
-
if (entries.length > 500) entries.pop();
|
|
619
|
+
|
|
620
|
+
<script>
|
|
621
|
+
// ── State ─────────────────────────────────────────────────
|
|
622
|
+
let entries = [];
|
|
623
|
+
let filteredEntries = [];
|
|
624
|
+
let baseUrl = "";
|
|
625
|
+
let eventSource = null;
|
|
626
|
+
let isStreaming = false;
|
|
627
|
+
let refreshTimer = null;
|
|
628
|
+
let filterTimeout = null;
|
|
629
|
+
let allSessions = [];
|
|
630
|
+
let sseRetryMs = 1000;
|
|
631
|
+
let statsThrottleTimer = null;
|
|
632
|
+
|
|
633
|
+
// ── Init ──────────────────────────────────────────────────
|
|
634
|
+
const savedEndpoint = localStorage.getItem("hsa-log-endpoint");
|
|
635
|
+
if (savedEndpoint)
|
|
636
|
+
document.getElementById("endpoint").value = savedEndpoint;
|
|
637
|
+
setTimeout(() => connect(), 300);
|
|
638
|
+
|
|
639
|
+
// ── Connection ────────────────────────────────────────────
|
|
640
|
+
async function connect() {
|
|
641
|
+
let url = document.getElementById("endpoint").value.trim();
|
|
642
|
+
if (!url) return;
|
|
643
|
+
if (!url.startsWith("http")) url = "http://" + url;
|
|
644
|
+
if (url.match(/^https?:\/\/:\d+/))
|
|
645
|
+
url = url.replace("://", "://localhost");
|
|
646
|
+
baseUrl = url.replace(/\/$/, "");
|
|
647
|
+
localStorage.setItem(
|
|
648
|
+
"hsa-log-endpoint",
|
|
649
|
+
document.getElementById("endpoint").value.trim(),
|
|
650
|
+
);
|
|
651
|
+
|
|
652
|
+
setStatus("pending");
|
|
653
|
+
try {
|
|
654
|
+
const res = await fetch(baseUrl + "/api/logs?limit=200");
|
|
655
|
+
if (!res.ok) throw new Error(res.statusText);
|
|
656
|
+
const data = await res.json();
|
|
657
|
+
entries = data.entries || [];
|
|
658
|
+
updateToolFilter(data.tools || []);
|
|
659
|
+
updateContextSelectors(data.ides || [], data.projects || []);
|
|
660
|
+
setStatus("ok");
|
|
314
661
|
applyFilters();
|
|
315
662
|
fetchStats();
|
|
663
|
+
fetchSessions();
|
|
664
|
+
if (!isStreaming) toggleStream();
|
|
665
|
+
if (refreshTimer) clearInterval(refreshTimer);
|
|
666
|
+
refreshTimer = setInterval(() => {
|
|
667
|
+
fetchStats();
|
|
668
|
+
fetchSessions();
|
|
669
|
+
}, 5000);
|
|
670
|
+
} catch (e) {
|
|
671
|
+
setStatus("err");
|
|
672
|
+
console.error("Connection failed:", e);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function setStatus(s) {
|
|
677
|
+
document.getElementById("statusDot").className = "status-dot " + s;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// ── Sessions for Context ──────────────────────────────────
|
|
681
|
+
async function fetchSessions() {
|
|
682
|
+
if (!baseUrl) return;
|
|
683
|
+
try {
|
|
684
|
+
const res = await fetch(baseUrl + "/api/sessions", {
|
|
685
|
+
signal: AbortSignal.timeout(5000),
|
|
686
|
+
});
|
|
687
|
+
if (!res.ok) return;
|
|
688
|
+
const data = await res.json();
|
|
689
|
+
allSessions = data.sessions || [];
|
|
690
|
+
populateSessionDropdown(allSessions);
|
|
691
|
+
} catch {}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function populateSessionDropdown(sessions) {
|
|
695
|
+
const sel = document.getElementById("ctxSession");
|
|
696
|
+
const cur = sel.value;
|
|
697
|
+
sel.innerHTML = '<option value="">All Sessions</option>';
|
|
698
|
+
sessions.forEach((s) => {
|
|
699
|
+
const opt = document.createElement("option");
|
|
700
|
+
opt.value = s.sessionId;
|
|
701
|
+
opt.textContent = `${s.projectName} (${s.ideName})`;
|
|
702
|
+
sel.appendChild(opt);
|
|
703
|
+
});
|
|
704
|
+
sel.value = cur;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function updateContextSelectors(ides, projects) {
|
|
708
|
+
const ideSelect = document.getElementById("ctxIde");
|
|
709
|
+
const projSelect = document.getElementById("ctxProject");
|
|
710
|
+
const curIde = ideSelect.value;
|
|
711
|
+
const curProj = projSelect.value;
|
|
712
|
+
|
|
713
|
+
ideSelect.innerHTML = '<option value="">All IDEs</option>';
|
|
714
|
+
ides.forEach((i) => {
|
|
715
|
+
const opt = document.createElement("option");
|
|
716
|
+
opt.value = i;
|
|
717
|
+
opt.textContent = i;
|
|
718
|
+
ideSelect.appendChild(opt);
|
|
719
|
+
});
|
|
720
|
+
ideSelect.value = curIde;
|
|
721
|
+
|
|
722
|
+
projSelect.innerHTML = '<option value="">All Projects</option>';
|
|
723
|
+
projects.forEach((p) => {
|
|
724
|
+
const opt = document.createElement("option");
|
|
725
|
+
opt.value = p;
|
|
726
|
+
opt.textContent = p;
|
|
727
|
+
projSelect.appendChild(opt);
|
|
728
|
+
});
|
|
729
|
+
projSelect.value = curProj;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// ── SSE Streaming ─────────────────────────────────────────
|
|
733
|
+
function toggleStream() {
|
|
734
|
+
if (isStreaming) stopStream();
|
|
735
|
+
else startStream();
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function startStream() {
|
|
739
|
+
if (!baseUrl) return;
|
|
740
|
+
if (eventSource) eventSource.close();
|
|
741
|
+
try {
|
|
742
|
+
eventSource = new EventSource(baseUrl + "/api/logs/stream");
|
|
743
|
+
eventSource.onmessage = (event) => {
|
|
744
|
+
try {
|
|
745
|
+
const data = JSON.parse(event.data);
|
|
746
|
+
if (data.type === "entry") {
|
|
747
|
+
entries.unshift(data.entry);
|
|
748
|
+
if (entries.length > 500) entries.pop();
|
|
749
|
+
// FE-04: Incremental prepend instead of full rebuild
|
|
750
|
+
prependEntry(data.entry);
|
|
751
|
+
throttledFetchStats();
|
|
752
|
+
}
|
|
753
|
+
} catch {}
|
|
754
|
+
};
|
|
755
|
+
eventSource.onerror = () => {
|
|
756
|
+
setStatus("err");
|
|
757
|
+
if (eventSource) {
|
|
758
|
+
eventSource.close();
|
|
759
|
+
eventSource = null;
|
|
760
|
+
}
|
|
761
|
+
// FE-01: Auto-reconnect with exponential backoff
|
|
762
|
+
setTimeout(() => {
|
|
763
|
+
if (isStreaming) startStream();
|
|
764
|
+
sseRetryMs = Math.min(sseRetryMs * 2, 30000);
|
|
765
|
+
}, sseRetryMs);
|
|
766
|
+
};
|
|
767
|
+
eventSource.onopen = () => {
|
|
768
|
+
setStatus("ok");
|
|
769
|
+
sseRetryMs = 1000;
|
|
770
|
+
};
|
|
771
|
+
isStreaming = true;
|
|
772
|
+
document.getElementById("streamBtn").textContent = "⏸ Pause";
|
|
773
|
+
document.getElementById("streamBtn").classList.add("active");
|
|
774
|
+
} catch {}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// FE-05: Throttle stats fetching to max once per 2 seconds
|
|
778
|
+
function throttledFetchStats() {
|
|
779
|
+
if (statsThrottleTimer) return;
|
|
780
|
+
statsThrottleTimer = setTimeout(() => {
|
|
781
|
+
statsThrottleTimer = null;
|
|
782
|
+
fetchStats();
|
|
783
|
+
}, 2000);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
function stopStream() {
|
|
787
|
+
if (eventSource) {
|
|
788
|
+
eventSource.close();
|
|
789
|
+
eventSource = null;
|
|
790
|
+
}
|
|
791
|
+
isStreaming = false;
|
|
792
|
+
document.getElementById("streamBtn").textContent = "▶ Stream";
|
|
793
|
+
document.getElementById("streamBtn").classList.remove("active");
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// ── Stats ─────────────────────────────────────────────────
|
|
797
|
+
async function fetchStats() {
|
|
798
|
+
if (!baseUrl) return;
|
|
799
|
+
try {
|
|
800
|
+
const res = await fetch(baseUrl + "/api/logs/stats");
|
|
801
|
+
if (!res.ok) return;
|
|
802
|
+
const s = await res.json();
|
|
803
|
+
document.getElementById("statTotal").textContent = s.totalCalls;
|
|
804
|
+
document.getElementById("statErrors").textContent = s.errorCount;
|
|
805
|
+
document.getElementById("statAvg").textContent = s.avgDurationMs;
|
|
806
|
+
document.getElementById("statRate").textContent = s.callsPerMinute;
|
|
807
|
+
} catch {}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// ── Filters ───────────────────────────────────────────────
|
|
811
|
+
function updateToolFilter(tools) {
|
|
812
|
+
const sel = document.getElementById("filterTool");
|
|
813
|
+
const current = sel.value;
|
|
814
|
+
sel.innerHTML = '<option value="">All Tools</option>';
|
|
815
|
+
tools.forEach((t) => {
|
|
816
|
+
const opt = document.createElement("option");
|
|
817
|
+
opt.value = t;
|
|
818
|
+
opt.textContent = t.replace("hsa_", "");
|
|
819
|
+
sel.appendChild(opt);
|
|
820
|
+
});
|
|
821
|
+
sel.value = current;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
function debounceFilter() {
|
|
825
|
+
clearTimeout(filterTimeout);
|
|
826
|
+
filterTimeout = setTimeout(applyFilters, 200);
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
function updateFilterCount() {
|
|
830
|
+
document.getElementById("filterCount").textContent =
|
|
831
|
+
filteredEntries.length === entries.length
|
|
832
|
+
? `${entries.length} entries`
|
|
833
|
+
: `${filteredEntries.length} / ${entries.length}`;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// FE-04: Check if a single entry passes current filters
|
|
837
|
+
function matchesFilters(e) {
|
|
838
|
+
const tool = document.getElementById("filterTool").value;
|
|
839
|
+
const status = document.getElementById("filterStatus").value;
|
|
840
|
+
const transport = document.getElementById("filterTransport").value;
|
|
841
|
+
const search = document.getElementById("filterSearch").value.toLowerCase();
|
|
842
|
+
const ctxIde = document.getElementById("ctxIde").value;
|
|
843
|
+
const ctxProject = document.getElementById("ctxProject").value;
|
|
844
|
+
const ctxSession = document.getElementById("ctxSession").value;
|
|
845
|
+
|
|
846
|
+
if (tool && e.tool !== tool) return false;
|
|
847
|
+
if (status && e.status !== status) return false;
|
|
848
|
+
if (transport && e.transport !== transport) return false;
|
|
849
|
+
if (ctxIde && e.ideName !== ctxIde) return false;
|
|
850
|
+
if (ctxProject && e.projectName !== ctxProject) return false;
|
|
851
|
+
if (ctxSession && e.sessionId !== ctxSession) return false;
|
|
852
|
+
if (search) {
|
|
853
|
+
const haystack = `${e.tool} ${e.response?.preview || ""} ${e.error || ""} ${e.ideName || ""} ${e.projectName || ""}`.toLowerCase();
|
|
854
|
+
if (!haystack.includes(search)) return false;
|
|
855
|
+
}
|
|
856
|
+
return true;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// FE-04: Prepend a single new entry to the DOM without full rebuild
|
|
860
|
+
function prependEntry(entry) {
|
|
861
|
+
if (!matchesFilters(entry)) return;
|
|
862
|
+
filteredEntries.unshift(entry);
|
|
863
|
+
updateFilterCount();
|
|
864
|
+
|
|
865
|
+
const table = document.getElementById("logTable");
|
|
866
|
+
const empty = document.getElementById("emptyState");
|
|
867
|
+
empty.style.display = "none";
|
|
868
|
+
|
|
869
|
+
const tempDiv = document.createElement("div");
|
|
870
|
+
tempDiv.innerHTML = renderRow(entry);
|
|
871
|
+
const fragment = document.createDocumentFragment();
|
|
872
|
+
while (tempDiv.firstChild) fragment.appendChild(tempDiv.firstChild);
|
|
873
|
+
table.insertBefore(fragment, table.firstChild);
|
|
874
|
+
|
|
875
|
+
// Trim overflow (each entry = row + detail = 2 elements)
|
|
876
|
+
while (table.children.length > 1000) {
|
|
877
|
+
table.removeChild(table.lastChild);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
function applyFilters() {
|
|
882
|
+
const tool = document.getElementById("filterTool").value;
|
|
883
|
+
const status = document.getElementById("filterStatus").value;
|
|
884
|
+
const transport = document.getElementById("filterTransport").value;
|
|
885
|
+
const search = document
|
|
886
|
+
.getElementById("filterSearch")
|
|
887
|
+
.value.toLowerCase();
|
|
888
|
+
const ctxIde = document.getElementById("ctxIde").value;
|
|
889
|
+
const ctxProject = document.getElementById("ctxProject").value;
|
|
890
|
+
const ctxSession = document.getElementById("ctxSession").value;
|
|
891
|
+
|
|
892
|
+
filteredEntries = entries.filter((e) => {
|
|
893
|
+
if (tool && e.tool !== tool) return false;
|
|
894
|
+
if (status && e.status !== status) return false;
|
|
895
|
+
if (transport && e.transport !== transport) return false;
|
|
896
|
+
if (ctxIde && e.ideName !== ctxIde) return false;
|
|
897
|
+
if (ctxProject && e.projectName !== ctxProject) return false;
|
|
898
|
+
if (ctxSession && e.sessionId !== ctxSession) return false;
|
|
899
|
+
if (search) {
|
|
900
|
+
const haystack =
|
|
901
|
+
`${e.tool} ${e.response?.preview || ""} ${e.error || ""} ${e.ideName || ""} ${e.projectName || ""}`.toLowerCase();
|
|
902
|
+
if (!haystack.includes(search)) return false;
|
|
903
|
+
}
|
|
904
|
+
return true;
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
updateFilterCount();
|
|
908
|
+
renderEntries();
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// ── Rendering ─────────────────────────────────────────────
|
|
912
|
+
function renderEntries() {
|
|
913
|
+
const table = document.getElementById("logTable");
|
|
914
|
+
const empty = document.getElementById("emptyState");
|
|
915
|
+
|
|
916
|
+
if (filteredEntries.length === 0) {
|
|
917
|
+
empty.style.display = "flex";
|
|
918
|
+
table.innerHTML = "";
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
empty.style.display = "none";
|
|
922
|
+
|
|
923
|
+
let html = "";
|
|
924
|
+
for (let i = 0; i < filteredEntries.length; i++) {
|
|
925
|
+
html += renderRow(filteredEntries[i]);
|
|
926
|
+
}
|
|
927
|
+
table.innerHTML = html;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// FE-04: Use entry.id for detail panel IDs instead of array index
|
|
931
|
+
function renderRow(e) {
|
|
932
|
+
const time = formatTime(e.timestamp);
|
|
933
|
+
const isErr = e.status === "error";
|
|
934
|
+
const isSlow = e.durationMs > 500;
|
|
935
|
+
const rowClass = isErr ? "error" : isSlow ? "slow" : "";
|
|
936
|
+
const statusClass = isErr ? "error" : "ok";
|
|
937
|
+
const statusText = isErr ? "✗ ERR" : "✓ OK";
|
|
938
|
+
const durClass = isSlow ? "slow" : "";
|
|
939
|
+
const durText =
|
|
940
|
+
e.durationMs < 1
|
|
941
|
+
? "<1ms"
|
|
942
|
+
: e.durationMs > 1000
|
|
943
|
+
? (e.durationMs / 1000).toFixed(1) + "s"
|
|
944
|
+
: Math.round(e.durationMs) + "ms";
|
|
945
|
+
const toolDisplay = e.tool.replace("hsa_", "");
|
|
946
|
+
const ide = e.ideName || "unknown";
|
|
947
|
+
const detailId = e.id || Math.random().toString(36).slice(2);
|
|
948
|
+
|
|
949
|
+
let detail = `<div class="log-detail" id="detail-${escHtml(detailId)}">`;
|
|
950
|
+
|
|
951
|
+
// Request args
|
|
952
|
+
detail += `<div class="detail-section"><div class="detail-label">Request</div>`;
|
|
953
|
+
detail += `<div class="detail-content">${escHtml(JSON.stringify(e.request?.args || {}, null, 2))}</div></div>`;
|
|
954
|
+
|
|
955
|
+
// Response preview
|
|
956
|
+
if (e.response?.preview) {
|
|
957
|
+
detail += `<div class="detail-section"><div class="detail-label">Response (${fmtSize(e.response.size)})</div>`;
|
|
958
|
+
detail += `<div class="detail-content">${escHtml(e.response.preview)}</div></div>`;
|
|
316
959
|
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
if (!res.ok) return;
|
|
340
|
-
const s = await res.json();
|
|
341
|
-
document.getElementById('statTotal').textContent = s.totalCalls;
|
|
342
|
-
document.getElementById('statErrors').textContent = s.errorCount;
|
|
343
|
-
document.getElementById('statAvg').textContent = s.avgDurationMs;
|
|
344
|
-
document.getElementById('statRate').textContent = s.callsPerMinute;
|
|
345
|
-
} catch {}
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
// ── Filters ───────────────────────────────────────────────
|
|
349
|
-
function updateToolFilter(tools) {
|
|
350
|
-
const sel = document.getElementById('filterTool');
|
|
351
|
-
const current = sel.value;
|
|
352
|
-
sel.innerHTML = '<option value="">All Tools</option>';
|
|
353
|
-
tools.forEach(t => {
|
|
354
|
-
const opt = document.createElement('option');
|
|
355
|
-
opt.value = t; opt.textContent = t.replace('hsa_', '');
|
|
356
|
-
sel.appendChild(opt);
|
|
357
|
-
});
|
|
358
|
-
sel.value = current;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
function debounceFilter() {
|
|
362
|
-
clearTimeout(filterTimeout);
|
|
363
|
-
filterTimeout = setTimeout(applyFilters, 200);
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
function applyFilters() {
|
|
367
|
-
const tool = document.getElementById('filterTool').value;
|
|
368
|
-
const status = document.getElementById('filterStatus').value;
|
|
369
|
-
const transport = document.getElementById('filterTransport').value;
|
|
370
|
-
const search = document.getElementById('filterSearch').value.toLowerCase();
|
|
371
|
-
const ctxIde = document.getElementById('ctxIde').value;
|
|
372
|
-
const ctxProject = document.getElementById('ctxProject').value;
|
|
373
|
-
const ctxSession = document.getElementById('ctxSession').value;
|
|
374
|
-
|
|
375
|
-
filteredEntries = entries.filter(e => {
|
|
376
|
-
if (tool && e.tool !== tool) return false;
|
|
377
|
-
if (status && e.status !== status) return false;
|
|
378
|
-
if (transport && e.transport !== transport) return false;
|
|
379
|
-
if (ctxIde && e.ideName !== ctxIde) return false;
|
|
380
|
-
if (ctxProject && e.projectName !== ctxProject) return false;
|
|
381
|
-
if (ctxSession && e.sessionId !== ctxSession) return false;
|
|
382
|
-
if (search) {
|
|
383
|
-
const haystack = `${e.tool} ${e.response?.preview || ''} ${e.error || ''} ${e.ideName || ''} ${e.projectName || ''}`.toLowerCase();
|
|
384
|
-
if (!haystack.includes(search)) return false;
|
|
385
|
-
}
|
|
386
|
-
return true;
|
|
387
|
-
});
|
|
388
|
-
|
|
389
|
-
document.getElementById('filterCount').textContent =
|
|
390
|
-
filteredEntries.length === entries.length
|
|
391
|
-
? `${entries.length} entries`
|
|
392
|
-
: `${filteredEntries.length} / ${entries.length}`;
|
|
393
|
-
|
|
394
|
-
renderEntries();
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
// ── Rendering ─────────────────────────────────────────────
|
|
398
|
-
function renderEntries() {
|
|
399
|
-
const table = document.getElementById('logTable');
|
|
400
|
-
const empty = document.getElementById('emptyState');
|
|
401
|
-
|
|
402
|
-
if (filteredEntries.length === 0) {
|
|
403
|
-
empty.style.display = 'flex';
|
|
404
|
-
table.innerHTML = '';
|
|
405
|
-
return;
|
|
406
|
-
}
|
|
407
|
-
empty.style.display = 'none';
|
|
408
|
-
|
|
409
|
-
let html = '';
|
|
410
|
-
for (let i = 0; i < filteredEntries.length; i++) {
|
|
411
|
-
html += renderRow(filteredEntries[i], i);
|
|
412
|
-
}
|
|
413
|
-
table.innerHTML = html;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
function renderRow(e, idx) {
|
|
417
|
-
const time = formatTime(e.timestamp);
|
|
418
|
-
const isErr = e.status === 'error';
|
|
419
|
-
const isSlow = e.durationMs > 500;
|
|
420
|
-
const rowClass = isErr ? 'error' : (isSlow ? 'slow' : '');
|
|
421
|
-
const statusClass = isErr ? 'error' : 'ok';
|
|
422
|
-
const statusText = isErr ? '✗ ERR' : '✓ OK';
|
|
423
|
-
const durClass = isSlow ? 'slow' : '';
|
|
424
|
-
const durText = e.durationMs < 1 ? '<1ms' : (e.durationMs > 1000 ? (e.durationMs/1000).toFixed(1)+'s' : Math.round(e.durationMs)+'ms');
|
|
425
|
-
const toolDisplay = e.tool.replace('hsa_', '');
|
|
426
|
-
const ide = e.ideName || 'unknown';
|
|
427
|
-
|
|
428
|
-
let detail = `<div class="log-detail" id="detail-${idx}">`;
|
|
429
|
-
|
|
430
|
-
// Request args
|
|
431
|
-
detail += `<div class="detail-section"><div class="detail-label">Request</div>`;
|
|
432
|
-
detail += `<div class="detail-content">${escHtml(JSON.stringify(e.request?.args || {}, null, 2))}</div></div>`;
|
|
433
|
-
|
|
434
|
-
// Response preview
|
|
435
|
-
if (e.response?.preview) {
|
|
436
|
-
detail += `<div class="detail-section"><div class="detail-label">Response (${fmtSize(e.response.size)})</div>`;
|
|
437
|
-
detail += `<div class="detail-content">${escHtml(e.response.preview)}</div></div>`;
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
// Error
|
|
441
|
-
if (e.error) {
|
|
442
|
-
detail += `<div class="detail-section"><div class="detail-label">Error</div>`;
|
|
443
|
-
detail += `<div class="detail-content err">${escHtml(e.error)}</div></div>`;
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
// Meta — now includes Session & Project
|
|
447
|
-
detail += `<div class="detail-meta">`;
|
|
448
|
-
detail += `<span>ID: <strong>${e.id}</strong></span>`;
|
|
449
|
-
detail += `<span>Transport: <strong>${e.transport}</strong></span>`;
|
|
450
|
-
if (e.sessionId) detail += `<span>Session: <strong>${e.sessionId}</strong></span>`;
|
|
451
|
-
if (e.projectName) detail += `<span>Project: <strong>${e.projectName}</strong></span>`;
|
|
452
|
-
if (e.request?.requestId) detail += `<span>Request: <strong>${e.request.requestId}</strong></span>`;
|
|
453
|
-
if (e.meta?.ip) detail += `<span>IP: <strong>${e.meta.ip}</strong></span>`;
|
|
454
|
-
detail += `</div></div>`;
|
|
455
|
-
|
|
456
|
-
return `<div class="log-row ${rowClass}" onclick="toggleDetail(${idx})">
|
|
960
|
+
|
|
961
|
+
// Error
|
|
962
|
+
if (e.error) {
|
|
963
|
+
detail += `<div class="detail-section"><div class="detail-label">Error</div>`;
|
|
964
|
+
detail += `<div class="detail-content err">${escHtml(e.error)}</div></div>`;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// Meta — now includes Session & Project
|
|
968
|
+
detail += `<div class="detail-meta">`;
|
|
969
|
+
detail += `<span>ID: <strong>${e.id}</strong></span>`;
|
|
970
|
+
detail += `<span>Transport: <strong>${e.transport}</strong></span>`;
|
|
971
|
+
if (e.sessionId)
|
|
972
|
+
detail += `<span>Session: <strong>${e.sessionId}</strong></span>`;
|
|
973
|
+
if (e.projectName)
|
|
974
|
+
detail += `<span>Project: <strong>${e.projectName}</strong></span>`;
|
|
975
|
+
if (e.request?.requestId)
|
|
976
|
+
detail += `<span>Request: <strong>${e.request.requestId}</strong></span>`;
|
|
977
|
+
if (e.meta?.ip)
|
|
978
|
+
detail += `<span>IP: <strong>${e.meta.ip}</strong></span>`;
|
|
979
|
+
detail += `</div></div>`;
|
|
980
|
+
|
|
981
|
+
return `<div class="log-row ${rowClass}" onclick="toggleDetail('${escHtml(detailId)}')">
|
|
457
982
|
<span class="log-time">${time}</span>
|
|
458
983
|
<span class="log-ide"><span class="ide-badge" data-ide="${escHtml(ide)}">${escHtml(ide)}</span></span>
|
|
459
984
|
<span class="log-tool">${toolDisplay}</span>
|
|
460
985
|
<span class="log-status ${statusClass}">${statusText}</span>
|
|
461
986
|
<span class="log-duration ${durClass}">${durText}</span>
|
|
462
987
|
</div>${detail}`;
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
function toggleDetail(
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
// ── Helpers ───────────────────────────────────────────────
|
|
483
|
-
function formatTime(iso) {
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
}
|
|
487
|
-
function fmtSize(n) {
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
function toggleDetail(detailId) {
|
|
991
|
+
const el = document.getElementById("detail-" + detailId);
|
|
992
|
+
if (!el) return;
|
|
993
|
+
const row = el.previousElementSibling;
|
|
994
|
+
const isOpen = el.classList.contains("show");
|
|
995
|
+
|
|
996
|
+
document.querySelectorAll(".log-detail.show").forEach((d) => {
|
|
997
|
+
d.classList.remove("show");
|
|
998
|
+
d.previousElementSibling?.classList.remove("expanded");
|
|
999
|
+
});
|
|
1000
|
+
|
|
1001
|
+
if (!isOpen) {
|
|
1002
|
+
el.classList.add("show");
|
|
1003
|
+
row?.classList.add("expanded");
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// ── Helpers ───────────────────────────────────────────────
|
|
1008
|
+
function formatTime(iso) {
|
|
1009
|
+
const d = new Date(iso);
|
|
1010
|
+
return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}:${String(d.getSeconds()).padStart(2, "0")}.${String(d.getMilliseconds()).padStart(3, "0")}`;
|
|
1011
|
+
}
|
|
1012
|
+
function fmtSize(n) {
|
|
1013
|
+
return n < 1024 ? n + " chars" : (n / 1024).toFixed(1) + "K";
|
|
1014
|
+
}
|
|
1015
|
+
function escHtml(s) {
|
|
1016
|
+
return String(s)
|
|
1017
|
+
.replace(/&/g, "&")
|
|
1018
|
+
.replace(/</g, "<")
|
|
1019
|
+
.replace(/>/g, ">")
|
|
1020
|
+
.replace(/"/g, """)
|
|
1021
|
+
.replace(/'/g, "'");
|
|
1022
|
+
}
|
|
1023
|
+
function clearLogs() {
|
|
1024
|
+
entries = [];
|
|
1025
|
+
filteredEntries = [];
|
|
1026
|
+
applyFilters();
|
|
1027
|
+
}
|
|
1028
|
+
</script>
|
|
1029
|
+
</body>
|
|
492
1030
|
</html>
|