openclaw-observability 2026.3.21 → 2026.3.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config.d.ts +11 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +6 -2
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +12 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +805 -98
- package/dist/index.js.map +1 -1
- package/dist/redaction.d.ts.map +1 -1
- package/dist/redaction.js +8 -1
- package/dist/redaction.js.map +1 -1
- package/dist/security/scanner.d.ts +15 -1
- package/dist/security/scanner.d.ts.map +1 -1
- package/dist/security/scanner.js +148 -11
- package/dist/security/scanner.js.map +1 -1
- package/dist/security/types.d.ts +1 -0
- package/dist/security/types.d.ts.map +1 -1
- package/dist/security/types.js +1 -0
- package/dist/security/types.js.map +1 -1
- package/dist/storage/duckdb-local-writer.d.ts +2 -1
- package/dist/storage/duckdb-local-writer.d.ts.map +1 -1
- package/dist/storage/duckdb-local-writer.js +20 -7
- package/dist/storage/duckdb-local-writer.js.map +1 -1
- package/dist/storage/mysql-writer.d.ts +3 -1
- package/dist/storage/mysql-writer.d.ts.map +1 -1
- package/dist/storage/mysql-writer.js +11 -6
- package/dist/storage/mysql-writer.js.map +1 -1
- package/dist/types.d.ts +3 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -1
- package/dist/web/api.d.ts +2 -2
- package/dist/web/api.d.ts.map +1 -1
- package/dist/web/api.js +121 -57
- package/dist/web/api.js.map +1 -1
- package/dist/web/routes.d.ts +7 -3
- package/dist/web/routes.d.ts.map +1 -1
- package/dist/web/routes.js +176 -25
- package/dist/web/routes.js.map +1 -1
- package/dist/web/ui.js +1190 -141
- package/dist/web/ui.js.map +1 -1
- package/openclaw.plugin.json +2 -2
- package/package.json +1 -1
package/dist/web/ui.js
CHANGED
|
@@ -113,6 +113,9 @@ a:hover{text-decoration:underline}
|
|
|
113
113
|
.theme-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;border:1px solid var(--border);border-radius:var(--radius-md);background:var(--bg-elevated);color:var(--muted);cursor:pointer;transition:all var(--duration-fast)}
|
|
114
114
|
.theme-btn:hover{color:var(--text);border-color:var(--border-strong);background:var(--bg-hover)}
|
|
115
115
|
.theme-btn svg{width:16px;height:16px;stroke:currentColor;fill:none;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round}
|
|
116
|
+
.icon-refresh-btn{width:32px;height:32px;display:inline-flex;align-items:center;justify-content:center;border:1px solid var(--border);border-radius:var(--radius-md);background:var(--bg-elevated);color:var(--muted);cursor:pointer;transition:all var(--duration-fast);padding:0;flex-shrink:0}
|
|
117
|
+
.icon-refresh-btn:hover{color:var(--text);border-color:var(--border-strong);background:var(--bg-hover)}
|
|
118
|
+
.icon-refresh-btn svg{width:14px;height:14px;stroke:currentColor;fill:none;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round}
|
|
116
119
|
|
|
117
120
|
/* ---- Content ---- */
|
|
118
121
|
.content{flex:1;padding:16px 20px 32px;min-height:0;overflow-y:auto}
|
|
@@ -132,6 +135,59 @@ a:hover{text-decoration:underline}
|
|
|
132
135
|
.filter-bar .filter-sep{width:1px;height:24px;background:var(--border);flex-shrink:0}
|
|
133
136
|
.filter-bar .btn-clear{background:none;border:1px solid var(--border);border-radius:var(--radius-md);padding:6px 12px;font-size:12px;color:var(--muted);cursor:pointer;transition:all var(--duration-fast)}
|
|
134
137
|
.filter-bar .btn-clear:hover{color:var(--text);border-color:var(--border-strong);background:var(--bg-hover)}
|
|
138
|
+
.sec-runtime{background:var(--card);border:1px solid var(--border);border-radius:var(--radius-lg);padding:16px 18px;margin-bottom:14px;box-shadow:inset 0 1px 0 var(--card-highlight)}
|
|
139
|
+
.sec-runtime-head{display:flex;align-items:flex-end;justify-content:space-between;gap:12px;flex-wrap:wrap;margin-bottom:12px}
|
|
140
|
+
.sec-runtime-title{font-size:14px;font-weight:700;color:var(--text-strong);letter-spacing:-.02em}
|
|
141
|
+
.sec-runtime-sub{font-size:12px;color:var(--muted)}
|
|
142
|
+
.sec-runtime-summary{font-size:11px;color:var(--muted);background:var(--secondary);border:1px solid var(--border);border-radius:var(--radius-full);padding:3px 10px}
|
|
143
|
+
.sec-runtime-grid{display:grid;grid-template-columns:minmax(340px,1fr) minmax(300px,1fr);gap:12px;align-items:start}
|
|
144
|
+
.sec-rule-list{border:1px solid var(--border);border-radius:var(--radius-md);background:var(--bg-elevated);overflow:hidden}
|
|
145
|
+
.sec-rule-item{display:flex;align-items:center;gap:10px;padding:11px 12px;border-bottom:1px solid var(--border);cursor:pointer;transition:background var(--duration-fast),border-left-color var(--duration-fast);border-left:3px solid transparent}
|
|
146
|
+
.sec-rule-item:last-child{border-bottom:none}
|
|
147
|
+
.sec-rule-item:hover{background:var(--bg-hover)}
|
|
148
|
+
.sec-rule-item.active{background:var(--accent-subtle);border-left-color:var(--accent)}
|
|
149
|
+
.sec-rule-copy{min-width:0;flex:1}
|
|
150
|
+
.sec-rule-name{font-size:13px;font-weight:600;color:var(--text-strong)}
|
|
151
|
+
.sec-rule-brief{font-size:11px;color:var(--muted);margin-top:1px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
152
|
+
.sec-state-pill{flex-shrink:0;font-size:10px;font-weight:700;padding:2px 7px;border-radius:var(--radius-full);text-transform:uppercase;letter-spacing:.04em}
|
|
153
|
+
.sec-state-pill.on{color:var(--ok);background:var(--ok-subtle);border:1px solid rgba(34,197,94,.25)}
|
|
154
|
+
.sec-state-pill.off{color:var(--danger);background:var(--danger-subtle);border:1px solid rgba(239,68,68,.25)}
|
|
155
|
+
.sec-switch{position:relative;display:inline-flex;align-items:center;width:38px;height:22px;border:none;background:transparent;padding:0;cursor:pointer;flex-shrink:0}
|
|
156
|
+
.sec-switch-track{position:absolute;inset:0;border-radius:999px;background:var(--muted-strong);opacity:.55;transition:background var(--duration-fast),opacity var(--duration-fast)}
|
|
157
|
+
.sec-switch-thumb{position:absolute;left:2px;top:2px;width:18px;height:18px;border-radius:50%;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.25);transition:transform var(--duration-fast)}
|
|
158
|
+
.sec-switch.on .sec-switch-track{background:var(--ok);opacity:.92}
|
|
159
|
+
.sec-switch.on .sec-switch-thumb{transform:translateX(16px)}
|
|
160
|
+
.sec-rule-detail{border:1px solid var(--border);border-radius:var(--radius-md);background:var(--bg-elevated);padding:14px 14px 12px;max-height:340px;overflow:auto}
|
|
161
|
+
.sec-rule-detail-title{display:flex;align-items:center;gap:8px;margin-bottom:6px}
|
|
162
|
+
.sec-rule-detail-title h4{font-size:14px;font-weight:700;color:var(--text-strong);letter-spacing:-.02em}
|
|
163
|
+
.sec-rule-detail p{font-size:12px;color:var(--text);line-height:1.6;margin-bottom:10px}
|
|
164
|
+
.sec-rule-detail-meta{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:10px}
|
|
165
|
+
.sec-rule-explain{border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--card);padding:10px 10px 8px;margin-bottom:10px}
|
|
166
|
+
.sec-rule-explain-text{font-size:12px;color:var(--text);line-height:1.6}
|
|
167
|
+
.sec-meta-chip{font-size:10px;padding:2px 8px;border-radius:var(--radius-full);border:1px solid var(--border);background:var(--secondary);color:var(--muted);text-transform:uppercase;letter-spacing:.04em}
|
|
168
|
+
.sec-meta-chip.level-critical{color:var(--danger);border-color:rgba(239,68,68,.35);background:var(--danger-subtle)}
|
|
169
|
+
.sec-meta-chip.level-high{color:#ea580c;border-color:rgba(234,88,12,.35);background:rgba(234,88,12,.1)}
|
|
170
|
+
.sec-meta-chip.level-medium{color:var(--warn);border-color:rgba(245,158,11,.35);background:var(--warn-subtle)}
|
|
171
|
+
.sec-meta-chip.level-low{color:var(--ok);border-color:rgba(34,197,94,.35);background:var(--ok-subtle)}
|
|
172
|
+
.sec-rule-detail-list{border-top:1px dashed var(--border);padding-top:8px}
|
|
173
|
+
.sec-rule-detail-list-title{font-size:11px;font-weight:600;color:var(--muted);margin-bottom:4px;text-transform:uppercase;letter-spacing:.04em}
|
|
174
|
+
.sec-rule-detail-list-item{font-size:12px;color:var(--text);padding:2px 0}
|
|
175
|
+
.sec-custom-editor{margin-top:10px;border-top:1px dashed var(--border);padding-top:10px}
|
|
176
|
+
.sec-custom-form{display:grid;grid-template-columns:1fr 1.8fr auto;gap:8px;align-items:end}
|
|
177
|
+
.sec-custom-field{display:flex;flex-direction:column;gap:4px;min-width:0}
|
|
178
|
+
.sec-custom-field-label{font-size:10px;letter-spacing:.04em;text-transform:uppercase;color:var(--muted);font-weight:600}
|
|
179
|
+
.sec-custom-form input,.sec-custom-form select{height:30px;border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--bg);color:var(--text);padding:0 9px;font-size:12px;outline:none}
|
|
180
|
+
.sec-custom-form input:focus,.sec-custom-form select:focus{border-color:var(--accent);box-shadow:var(--focus-ring)}
|
|
181
|
+
.sec-custom-form button{height:30px;border:1px solid var(--accent);border-radius:var(--radius-sm);background:var(--accent);color:var(--accent-foreground);padding:0 12px;font-size:12px;font-weight:700;cursor:pointer;letter-spacing:.01em}
|
|
182
|
+
.sec-custom-form button:hover{filter:brightness(1.06)}
|
|
183
|
+
.sec-custom-list{margin-top:10px;display:flex;flex-direction:column;gap:6px;max-height:220px;overflow:auto;padding-right:2px}
|
|
184
|
+
.sec-custom-item{display:flex;align-items:flex-start;gap:8px;border:1px solid var(--border);border-radius:var(--radius-sm);padding:7px 8px;background:var(--card)}
|
|
185
|
+
.sec-custom-item-main{min-width:0;flex:1}
|
|
186
|
+
.sec-custom-item-name{font-size:12px;font-weight:600;color:var(--text-strong)}
|
|
187
|
+
.sec-custom-item-pattern{font-size:11px;color:var(--muted);font-family:var(--mono);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
188
|
+
.sec-custom-item-actions{display:flex;align-items:center;gap:6px;flex-shrink:0}
|
|
189
|
+
.sec-custom-item-del{height:22px;min-width:56px;border:1px solid rgba(239,68,68,.35);border-radius:999px;background:var(--danger-subtle);color:var(--danger);padding:0 10px;font-size:11px;line-height:1;cursor:pointer;font-weight:700}
|
|
190
|
+
.sec-custom-item-del:hover{background:rgba(239,68,68,.2);border-color:rgba(239,68,68,.55)}
|
|
135
191
|
/* Time range dropdown */
|
|
136
192
|
.time-dropdown{position:relative}
|
|
137
193
|
.time-btn{display:inline-flex;align-items:center;gap:6px;background:var(--bg-elevated);border:1px solid var(--border);border-radius:var(--radius-md);padding:7px 12px;font-size:13px;color:var(--text);cursor:pointer;white-space:nowrap;transition:all var(--duration-fast)}
|
|
@@ -174,16 +230,27 @@ a:hover{text-decoration:underline}
|
|
|
174
230
|
|
|
175
231
|
/* ==================== Trace view (split pane) ==================== */
|
|
176
232
|
.content--trace{padding:0!important;overflow:hidden!important;flex:1!important;min-height:0!important;display:flex!important;flex-direction:column!important}
|
|
177
|
-
.content--trace .content-inner{max-width:
|
|
233
|
+
.content--trace .content-inner{max-width:none;width:100%;margin:0;flex:1;min-height:0;display:flex;flex-direction:row;overflow:hidden}
|
|
178
234
|
|
|
179
|
-
.trace-top{flex:1;min-height:120px;overflow-y:scroll;scrollbar-gutter:stable}
|
|
180
|
-
.trace-header{background:var(--card);padding:16px 24px;border-bottom:1px solid var(--border);box-shadow:inset 0 1px 0 var(--card-highlight)}
|
|
235
|
+
.trace-top{flex:1;min-width:0;min-height:120px;overflow-y:scroll;scrollbar-gutter:stable;position:relative}
|
|
236
|
+
.trace-header{position:sticky;top:0;z-index:12;background:var(--card);padding:16px 24px;border-bottom:1px solid var(--border);box-shadow:inset 0 1px 0 var(--card-highlight),0 6px 18px rgba(0,0,0,.08)}
|
|
237
|
+
.trace-header-top{display:flex;align-items:center;justify-content:space-between;gap:12px}
|
|
181
238
|
.trace-back{display:inline-flex;align-items:center;gap:6px;color:var(--accent);text-decoration:none;font-size:13px;margin-bottom:10px;font-weight:500}
|
|
239
|
+
.trace-header-top .trace-back{margin-bottom:0}
|
|
182
240
|
.trace-back:hover{text-decoration:underline}
|
|
183
241
|
.trace-title{font-size:16px;font-weight:700;color:var(--text-strong);margin-bottom:4px;word-break:break-all;letter-spacing:-.02em}
|
|
184
242
|
.trace-meta{display:flex;gap:16px;flex-wrap:wrap}
|
|
185
243
|
.trace-meta-item{font-size:13px;color:var(--muted)}
|
|
186
244
|
.trace-meta-item b{color:var(--text-strong);font-weight:600}
|
|
245
|
+
.trace-search-bar{margin-top:14px;margin-bottom:0}
|
|
246
|
+
.trace-search-bar input[type=text]{min-width:280px}
|
|
247
|
+
.trace-search-stats{font-size:12px;color:var(--muted);white-space:nowrap}
|
|
248
|
+
.trace-search-nav{display:flex;align-items:center;gap:6px}
|
|
249
|
+
.trace-search-nav button{display:inline-flex;align-items:center;justify-content:center;padding:6px 10px;border:1px solid var(--border);border-radius:var(--radius-md);background:var(--bg-elevated);color:var(--text);cursor:pointer;font-size:12px;transition:all var(--duration-fast)}
|
|
250
|
+
.trace-search-nav .icon-refresh-btn{padding:0;width:32px;min-width:32px}
|
|
251
|
+
.trace-search-nav button:hover:not(:disabled){background:var(--bg-hover);border-color:var(--border-strong)}
|
|
252
|
+
.trace-search-nav button:disabled{opacity:.45;cursor:default}
|
|
253
|
+
.trace-search-empty{margin:10px 20px 0;padding:10px 14px;background:var(--warn-subtle);border:1px solid rgba(245,158,11,.25);border-radius:var(--radius-md);font-size:12px;color:var(--warn)}
|
|
187
254
|
|
|
188
255
|
/* ---- Waterfall ---- */
|
|
189
256
|
.waterfall{background:var(--card);border:1px solid var(--border);margin:16px 20px;border-radius:var(--radius-lg);overflow:hidden;box-shadow:inset 0 1px 0 var(--card-highlight)}
|
|
@@ -191,7 +258,7 @@ a:hover{text-decoration:underline}
|
|
|
191
258
|
.wf-row{display:grid;grid-template-columns:300px 90px 1fr;padding:7px 16px;border-bottom:1px solid var(--border);cursor:pointer;transition:background var(--duration-fast)}
|
|
192
259
|
.wf-row:hover{background:var(--bg-hover)}
|
|
193
260
|
.wf-row.selected{background:var(--accent-subtle)}
|
|
194
|
-
.wf-row:
|
|
261
|
+
.wf-row.wf-row--match:not(.selected){background:rgba(245,158,11,.08)}
|
|
195
262
|
.wf-name{font-size:13px;display:flex;align-items:center;gap:6px;overflow:hidden;color:var(--text)}
|
|
196
263
|
.wf-name .indent{flex-shrink:0;color:var(--muted)}
|
|
197
264
|
.wf-name .dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
|
|
@@ -200,24 +267,33 @@ a:hover{text-decoration:underline}
|
|
|
200
267
|
.wf-bar-wrap{position:relative;height:18px;border-radius:3px}
|
|
201
268
|
.wf-bar{position:absolute;height:100%;border-radius:3px;min-width:3px;opacity:.75;transition:opacity var(--duration-fast)}
|
|
202
269
|
.wf-row:hover .wf-bar{opacity:1}
|
|
203
|
-
.wf-
|
|
204
|
-
.wf-
|
|
205
|
-
.wf-
|
|
206
|
-
.wf-
|
|
207
|
-
.wf-
|
|
208
|
-
.wf-
|
|
270
|
+
.wf-group-section{position:relative}
|
|
271
|
+
.wf-group-section.collapsed .wf-row{display:none}
|
|
272
|
+
.wf-group-section:last-child .wf-row:last-child{border-bottom:none}
|
|
273
|
+
.wf-group{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:10px 16px;background:var(--bg-elevated);border-top:1px solid var(--border);border-bottom:1px solid var(--border);border-left:3px solid var(--border-strong);cursor:pointer;transition:background var(--duration-fast),border-left-color var(--duration-fast)}
|
|
274
|
+
.wf-group:hover{background:var(--bg-hover);border-left-color:var(--accent)}
|
|
275
|
+
.wf-group:first-of-type{border-top:none}
|
|
276
|
+
.wf-group-main{display:flex;align-items:flex-start;gap:10px;min-width:0;flex:1}
|
|
277
|
+
.wf-group-toggle{font-size:12px;color:var(--muted);font-family:var(--mono);width:12px;text-align:center;flex-shrink:0}
|
|
278
|
+
.wf-group-copy{display:flex;flex-direction:column;gap:4px;min-width:0}
|
|
279
|
+
.wf-group-title-row{display:flex;align-items:center;gap:8px;min-width:0}
|
|
280
|
+
.wf-group-title{font-size:12px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;color:var(--text-strong)}
|
|
281
|
+
.wf-group-hit{font-size:10px;padding:1px 6px;border-radius:var(--radius-full);background:var(--accent-subtle);color:var(--accent);font-weight:700;flex-shrink:0}
|
|
282
|
+
.wf-group-snippet{font-size:12px;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%}
|
|
283
|
+
.wf-group-meta{font-size:11px;color:var(--muted);font-family:var(--mono);background:var(--secondary);border:1px solid var(--border);border-radius:var(--radius-full);padding:3px 10px;flex-shrink:0}
|
|
284
|
+
.search-hit{background:rgba(245,158,11,.22);color:var(--text-strong);border-radius:3px;padding:0 2px}
|
|
209
285
|
|
|
210
286
|
/* ---- Resize handle ---- */
|
|
211
|
-
.trace-resize{flex:0 0 7px;cursor:
|
|
287
|
+
.trace-resize{flex:0 0 7px;cursor:col-resize;background:var(--border);position:relative;z-index:10;transition:background var(--duration-fast)}
|
|
212
288
|
.trace-resize:hover,.trace-resize.active{background:var(--accent)}
|
|
213
|
-
.trace-resize::before{content:'';position:absolute;left
|
|
214
|
-
.trace-resize::after{content:'';position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);width:
|
|
289
|
+
.trace-resize::before{content:'';position:absolute;left:-4px;right:-4px;top:0;bottom:0;z-index:1}
|
|
290
|
+
.trace-resize::after{content:'';position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);width:3px;height:40px;border-radius:2px;background:var(--muted);opacity:.5}
|
|
215
291
|
.trace-resize:hover::after,.trace-resize.active::after{background:var(--accent-foreground);opacity:.8}
|
|
216
|
-
body.resizing{cursor:
|
|
217
|
-
body.resizing *{cursor:
|
|
292
|
+
body.resizing{cursor:col-resize!important;-webkit-user-select:none!important;user-select:none!important}
|
|
293
|
+
body.resizing *{cursor:col-resize!important}
|
|
218
294
|
|
|
219
|
-
/* ---- Detail panel (
|
|
220
|
-
.trace-bottom{flex:0 0
|
|
295
|
+
/* ---- Detail panel (right pane) ---- */
|
|
296
|
+
.trace-bottom{flex:0 0 420px;min-width:320px;max-width:720px;overflow-y:scroll;scrollbar-gutter:stable;background:var(--bg-accent);border-left:1px solid var(--border)}
|
|
221
297
|
.trace-bottom .detail-panel{background:var(--card);overflow:hidden;border:none;border-radius:0;margin:0;width:100%}
|
|
222
298
|
.trace-bottom .detail-header{padding:10px 18px;background:var(--bg-accent);border-bottom:1px solid var(--border);display:flex;align-items:center;gap:10px;position:sticky;top:0;z-index:5}
|
|
223
299
|
.detail-header .dot{width:10px;height:10px;border-radius:50%;flex-shrink:0}
|
|
@@ -226,11 +302,28 @@ body.resizing *{cursor:row-resize!important}
|
|
|
226
302
|
.detail-meta{padding:10px 18px;display:flex;gap:16px;flex-wrap:wrap;font-size:12px;color:var(--muted);border-bottom:1px solid var(--border)}
|
|
227
303
|
.detail-meta b{color:var(--text-strong)}
|
|
228
304
|
.detail-body{display:grid;grid-template-columns:1fr 1fr;gap:0;overflow:hidden}
|
|
305
|
+
.trace-bottom .detail-body{grid-template-columns:1fr}
|
|
229
306
|
@media(max-width:800px){.detail-body{grid-template-columns:1fr}}
|
|
230
307
|
.detail-section{padding:12px 18px;border-right:1px solid var(--border);overflow:hidden;min-width:0}
|
|
308
|
+
.trace-bottom .detail-section{border-right:none;border-bottom:1px solid var(--border)}
|
|
309
|
+
.trace-bottom .detail-section:last-child{border-bottom:none}
|
|
231
310
|
.detail-section:last-child{border-right:none}
|
|
232
311
|
.detail-section h4{font-size:12px;color:var(--accent);text-transform:uppercase;letter-spacing:.04em;margin-bottom:8px;font-weight:600}
|
|
233
312
|
.json-view{background:var(--secondary);padding:10px 12px;border-radius:var(--radius-md);font-family:var(--mono);font-size:12px;line-height:1.6;white-space:pre-wrap;word-break:break-all;max-height:none;overflow-y:auto;color:var(--text);border:1px solid var(--border)}
|
|
313
|
+
.kv-view{background:var(--bg-elevated);border:1px solid var(--border);border-radius:var(--radius-md);padding:4px 12px}
|
|
314
|
+
.kv-item{padding:10px 0;border-bottom:1px solid var(--border)}
|
|
315
|
+
.kv-item:last-child{border-bottom:none}
|
|
316
|
+
.kv-key{font-size:12px;font-weight:700;color:#3b82f6;line-height:1.4;letter-spacing:.01em}
|
|
317
|
+
:root[data-theme=light] .kv-key{color:#2563eb}
|
|
318
|
+
.kv-value{margin-top:6px;font-family:var(--mono);font-size:12px;line-height:1.6;color:var(--text-strong);white-space:pre-wrap;word-break:break-word}
|
|
319
|
+
.kv-collapse{margin-top:6px}
|
|
320
|
+
.kv-collapse summary{cursor:pointer;color:var(--muted);font-size:11px;user-select:none}
|
|
321
|
+
.kv-collapse summary:hover{color:var(--text)}
|
|
322
|
+
.kv-collapse .kv-value{margin-top:8px}
|
|
323
|
+
.media-preview{margin-top:10px;display:flex;flex-wrap:wrap;gap:10px}
|
|
324
|
+
.media-card{display:block;width:110px;height:110px;border:1px solid var(--border);border-radius:10px;overflow:hidden;background:var(--secondary);box-shadow:inset 0 1px 0 var(--card-highlight);transition:transform var(--duration-fast),border-color var(--duration-fast),box-shadow var(--duration-fast)}
|
|
325
|
+
.media-card:hover{transform:translateY(-1px);border-color:var(--accent);box-shadow:var(--shadow-sm),inset 0 1px 0 var(--card-highlight)}
|
|
326
|
+
.media-card img{width:100%;height:100%;object-fit:cover;display:block;background:var(--bg-accent)}
|
|
234
327
|
.json-key{color:#a78bfa}
|
|
235
328
|
.json-str{color:#34d399;white-space:pre-wrap}
|
|
236
329
|
.json-num{color:#fbbf24}
|
|
@@ -316,12 +409,12 @@ body.resizing *{cursor:row-resize!important}
|
|
|
316
409
|
.chart-bar-seg{width:100%;min-height:0;transition:opacity var(--duration-fast)}
|
|
317
410
|
.chart-bar-seg:last-child{border-radius:3px 3px 0 0}
|
|
318
411
|
.chart-x-labels{display:flex;gap:3px;padding:6px 4px 0;border-top:1px solid var(--border);justify-content:center}
|
|
319
|
-
.chart-x-labels span{flex:1;text-align:center;font-size:9px;color:var(--muted);font-family:var(--mono);
|
|
412
|
+
.chart-x-labels span{flex:1;text-align:center;font-size:9px;color:var(--muted);font-family:var(--mono);line-height:1.2;white-space:normal;overflow:visible}
|
|
320
413
|
.chart-y-axis{display:flex;flex-direction:column;justify-content:space-between;align-items:flex-end;height:160px;padding-right:8px;min-width:40px}
|
|
321
414
|
.chart-y-axis span{font-size:10px;color:var(--muted);font-family:var(--mono)}
|
|
322
415
|
.chart-container{display:flex}
|
|
323
416
|
.chart-main{flex:1;min-width:0}
|
|
324
|
-
.chart-tooltip{position:absolute;bottom:100%;left:50%;transform:translateX(-50%);background:var(--card);border:1px solid var(--border);border-radius:var(--radius-sm);padding:
|
|
417
|
+
.chart-tooltip{position:absolute;bottom:100%;left:50%;transform:translateX(-50%);background:var(--card);border:1px solid var(--border);border-radius:var(--radius-sm);padding:6px 8px;font-size:11px;color:var(--text);line-height:1.35;white-space:normal;word-break:break-word;max-width:220px;min-width:150px;pointer-events:none;z-index:20;box-shadow:var(--shadow-md);display:none}
|
|
325
418
|
.chart-bar-col:hover .chart-tooltip{display:block}
|
|
326
419
|
.chart-legend{display:flex;gap:16px;justify-content:center;margin-top:12px;flex-wrap:wrap}
|
|
327
420
|
.chart-legend-item{display:flex;align-items:center;gap:5px;font-size:11px;color:var(--muted)}
|
|
@@ -377,12 +470,19 @@ body.resizing *{cursor:row-resize!important}
|
|
|
377
470
|
|
|
378
471
|
/* ---- Responsive ---- */
|
|
379
472
|
@media(max-width:900px){.stat-grid{grid-template-columns:repeat(3,1fr)}}
|
|
473
|
+
@media(max-width:900px){
|
|
474
|
+
.content--trace .content-inner{flex-direction:column}
|
|
475
|
+
.trace-resize{display:none}
|
|
476
|
+
.trace-bottom{flex:0 0 320px;min-width:0;max-width:none;border-left:none;border-top:1px solid var(--border)}
|
|
477
|
+
.sec-runtime-grid{grid-template-columns:1fr}
|
|
478
|
+
.sec-custom-form{grid-template-columns:1fr}
|
|
479
|
+
}
|
|
380
480
|
@media(max-width:560px){
|
|
381
481
|
.stat-grid{grid-template-columns:repeat(2,1fr)}
|
|
382
482
|
.topbar{padding:0 12px;height:48px}
|
|
383
483
|
.content{padding:12px}
|
|
384
484
|
.waterfall{margin:10px}
|
|
385
|
-
.wf-header,.wf-row
|
|
485
|
+
.wf-header,.wf-row{grid-template-columns:200px 70px 1fr}
|
|
386
486
|
}
|
|
387
487
|
`;
|
|
388
488
|
/* ================================================================== */
|
|
@@ -394,7 +494,7 @@ const CLIENT_JS = `
|
|
|
394
494
|
/* ---------- constants ---------- */
|
|
395
495
|
var API = window.location.pathname.replace(/\\/+$/, '') + '/api';
|
|
396
496
|
var TYPE_COLORS = {
|
|
397
|
-
message:'#8b5cf6', tool_call:'#f59e0b', tool_persist:'#f97316',
|
|
497
|
+
message:'#8b5cf6', assistant_stream:'#7c3aed', thinking:'#0f766e', tool_call:'#f59e0b', tool_update:'#fb923c', tool_persist:'#f97316',
|
|
398
498
|
prompt_build:'#3b82f6', model_resolve:'#06b6d4', agent_end:'#10b981',
|
|
399
499
|
session_start:'#22c55e', session_end:'#ef4444', compaction:'#14b8a6',
|
|
400
500
|
reset:'#f43f5e', user_message:'#0ea5e9', assistant_msg:'#a855f7',
|
|
@@ -402,7 +502,7 @@ var TYPE_COLORS = {
|
|
|
402
502
|
gateway_start:'#94a3b8', gateway_stop:'#475569'
|
|
403
503
|
};
|
|
404
504
|
var TYPE_LABELS = {
|
|
405
|
-
message:'LLM Call', tool_call:'Tool Call', tool_persist:'Tool Persist',
|
|
505
|
+
message:'LLM Call', assistant_stream:'Assistant Stream', thinking:'Thinking', tool_call:'Tool Call', tool_update:'Tool Update', tool_persist:'Tool Persist',
|
|
406
506
|
prompt_build:'Prompt Build', model_resolve:'Model Resolve', agent_end:'Agent End',
|
|
407
507
|
session_start:'Session Start', session_end:'Session End', compaction:'Compaction',
|
|
408
508
|
reset:'Reset', user_message:'User Message', assistant_msg:'Assistant Msg',
|
|
@@ -463,6 +563,7 @@ applyTheme(getTheme());
|
|
|
463
563
|
|
|
464
564
|
/* ---------- helpers ---------- */
|
|
465
565
|
function esc(s) { if (!s) return ''; var d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
|
|
566
|
+
function escapeRegExp(s) { return s.replace(/[.*+?^{}()|[\]\\$]/g, '\\$&'); }
|
|
466
567
|
function fmtDur(ms) {
|
|
467
568
|
if (ms == null) return '-';
|
|
468
569
|
if (ms < 1000) return ms + 'ms';
|
|
@@ -487,6 +588,189 @@ function parseJson(s) {
|
|
|
487
588
|
if (!s) return null;
|
|
488
589
|
try { return typeof s === 'string' ? JSON.parse(s) : s; } catch(e) { return s; }
|
|
489
590
|
}
|
|
591
|
+
function getAuthTokenFromQuery() {
|
|
592
|
+
var params = new URLSearchParams(window.location.search);
|
|
593
|
+
return params.get('token') || '';
|
|
594
|
+
}
|
|
595
|
+
function buildMediaUrl(rawPath) {
|
|
596
|
+
var url = API + '/media?path=' + encodeURIComponent(rawPath);
|
|
597
|
+
var token = getAuthTokenFromQuery();
|
|
598
|
+
if (token) url += '&token=' + encodeURIComponent(token);
|
|
599
|
+
return url;
|
|
600
|
+
}
|
|
601
|
+
function onMediaImgError(imgEl) {
|
|
602
|
+
if (!imgEl) return;
|
|
603
|
+
var card = imgEl.closest('.media-card');
|
|
604
|
+
if (card) card.remove();
|
|
605
|
+
var preview = imgEl.closest('.media-preview');
|
|
606
|
+
if (preview && preview.children.length === 0) {
|
|
607
|
+
preview.remove();
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
function collectMediaPaths(value, out, depth) {
|
|
611
|
+
if (!out) out = [];
|
|
612
|
+
if (!depth) depth = 0;
|
|
613
|
+
if (depth > 6 || value == null) return out;
|
|
614
|
+
|
|
615
|
+
if (typeof value === 'string') {
|
|
616
|
+
var s = value;
|
|
617
|
+
var i = 0;
|
|
618
|
+
while (i < s.length) {
|
|
619
|
+
var pos = s.indexOf('file://', i);
|
|
620
|
+
if (pos < 0) break;
|
|
621
|
+
var end = pos;
|
|
622
|
+
while (end < s.length) {
|
|
623
|
+
var ch = s[end];
|
|
624
|
+
if (ch === ' ' || ch === '\\n' || ch === '\\r' || ch === '\\t' || ch === '"' || ch.charCodeAt(0) === 39 || ch === ')' || ch === '<' || ch === '>') break;
|
|
625
|
+
end++;
|
|
626
|
+
}
|
|
627
|
+
var candidate = s.slice(pos, end);
|
|
628
|
+
if (candidate) out.push(candidate);
|
|
629
|
+
i = Math.max(end, pos + 7);
|
|
630
|
+
}
|
|
631
|
+
return out;
|
|
632
|
+
}
|
|
633
|
+
if (Array.isArray(value)) {
|
|
634
|
+
value.forEach(function(v) { collectMediaPaths(v, out, depth + 1); });
|
|
635
|
+
return out;
|
|
636
|
+
}
|
|
637
|
+
if (typeof value === 'object') {
|
|
638
|
+
Object.keys(value).forEach(function(k) { collectMediaPaths(value[k], out, depth + 1); });
|
|
639
|
+
return out;
|
|
640
|
+
}
|
|
641
|
+
return out;
|
|
642
|
+
}
|
|
643
|
+
function getToolCallId(action) {
|
|
644
|
+
function readToolCallId(obj) {
|
|
645
|
+
if (!obj || typeof obj !== 'object') return '';
|
|
646
|
+
if (typeof obj.toolCallId === 'string' && obj.toolCallId) return obj.toolCallId;
|
|
647
|
+
if (typeof obj.tool_call_id === 'string' && obj.tool_call_id) return obj.tool_call_id;
|
|
648
|
+
if (obj.toolCall && typeof obj.toolCall === 'object') {
|
|
649
|
+
if (typeof obj.toolCall.id === 'string' && obj.toolCall.id) return obj.toolCall.id;
|
|
650
|
+
}
|
|
651
|
+
return '';
|
|
652
|
+
}
|
|
653
|
+
var input = parseJson(action && action.input_params);
|
|
654
|
+
var id = readToolCallId(input);
|
|
655
|
+
if (id) return id;
|
|
656
|
+
var output = parseJson(action && action.output_result);
|
|
657
|
+
return readToolCallId(output);
|
|
658
|
+
}
|
|
659
|
+
function getActionRunId(action) {
|
|
660
|
+
if (!action) return '';
|
|
661
|
+
var input = parseJson(action.input_params);
|
|
662
|
+
if (input && typeof input === 'object' && typeof input.runId === 'string' && input.runId) {
|
|
663
|
+
return input.runId;
|
|
664
|
+
}
|
|
665
|
+
var output = parseJson(action.output_result);
|
|
666
|
+
if (output && typeof output === 'object' && typeof output.runId === 'string' && output.runId) {
|
|
667
|
+
return output.runId;
|
|
668
|
+
}
|
|
669
|
+
return '';
|
|
670
|
+
}
|
|
671
|
+
function normalizeTraceSearch(s) {
|
|
672
|
+
return (s || '').trim().toLowerCase();
|
|
673
|
+
}
|
|
674
|
+
function stringifySearchValue(v) {
|
|
675
|
+
if (v == null) return '';
|
|
676
|
+
if (typeof v === 'string') return v;
|
|
677
|
+
try { return JSON.stringify(v); } catch(e) { return String(v); }
|
|
678
|
+
}
|
|
679
|
+
function getActionSearchText(action) {
|
|
680
|
+
return [
|
|
681
|
+
action.action_name,
|
|
682
|
+
action.model_name,
|
|
683
|
+
action.input_params,
|
|
684
|
+
action.output_result
|
|
685
|
+
].map(stringifySearchValue).filter(Boolean).join('\\n');
|
|
686
|
+
}
|
|
687
|
+
function getActionSearchSnippet(action, query) {
|
|
688
|
+
if (!query) return '';
|
|
689
|
+
var fields = [
|
|
690
|
+
action.action_name,
|
|
691
|
+
action.input_params,
|
|
692
|
+
action.output_result,
|
|
693
|
+
action.model_name
|
|
694
|
+
];
|
|
695
|
+
for (var i = 0; i < fields.length; i++) {
|
|
696
|
+
var text = stringifySearchValue(fields[i]);
|
|
697
|
+
if (!text) continue;
|
|
698
|
+
var normalized = text.toLowerCase();
|
|
699
|
+
var idx = normalized.indexOf(query);
|
|
700
|
+
if (idx < 0) continue;
|
|
701
|
+
var start = Math.max(0, idx - 36);
|
|
702
|
+
var end = Math.min(text.length, idx + query.length + 52);
|
|
703
|
+
var snippet = text.slice(start, end).replace(/\s+/g, ' ').trim();
|
|
704
|
+
if (start > 0) snippet = '...' + snippet;
|
|
705
|
+
if (end < text.length) snippet = snippet + '...';
|
|
706
|
+
return snippet;
|
|
707
|
+
}
|
|
708
|
+
return '';
|
|
709
|
+
}
|
|
710
|
+
function highlightSearchText(text, query) {
|
|
711
|
+
var safeText = esc(text || '');
|
|
712
|
+
if (!query) return safeText;
|
|
713
|
+
var re = new RegExp('(' + escapeRegExp(query) + ')', 'ig');
|
|
714
|
+
return safeText.replace(re, '<span class="search-hit">$1</span>');
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function isToolActionType(actionType) {
|
|
718
|
+
return typeof actionType === 'string' && actionType.indexOf('tool_') === 0;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
var KV_VALUE_COLLAPSE_LIMIT = 2000;
|
|
722
|
+
|
|
723
|
+
function collectTopLevelEntries(value) {
|
|
724
|
+
if (value === null || value === undefined) return [];
|
|
725
|
+
if (typeof value !== 'object' || Array.isArray(value)) {
|
|
726
|
+
return [{ key: 'value', value: value }];
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
var keys = Object.keys(value);
|
|
730
|
+
if (keys.length === 0) return [{ key: 'value', value: '{}' }];
|
|
731
|
+
return keys.map(function(k) {
|
|
732
|
+
return { key: k, value: value[k] };
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function renderKvValue(raw) {
|
|
737
|
+
var text;
|
|
738
|
+
if (raw === null || raw === undefined) {
|
|
739
|
+
text = 'null';
|
|
740
|
+
} else if (typeof raw === 'string') {
|
|
741
|
+
text = raw;
|
|
742
|
+
} else if (typeof raw === 'object') {
|
|
743
|
+
try { text = JSON.stringify(raw, null, 2); } catch(e) { text = String(raw); }
|
|
744
|
+
} else {
|
|
745
|
+
text = String(raw);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
if (text.length > KV_VALUE_COLLAPSE_LIMIT) {
|
|
749
|
+
return '<details class="kv-collapse">' +
|
|
750
|
+
'<summary>Show value (' + text.length + ' chars)</summary>' +
|
|
751
|
+
'<div class="kv-value">' + esc(text) + '</div>' +
|
|
752
|
+
'</details>';
|
|
753
|
+
}
|
|
754
|
+
return '<div class="kv-value">' + esc(text) + '</div>';
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
function renderStructuredFields(value) {
|
|
758
|
+
var entries = collectTopLevelEntries(value);
|
|
759
|
+
if (!entries.length) {
|
|
760
|
+
return '<div class="json-view" style="color:var(--muted)">null</div>';
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
var html = '<div class="kv-view">';
|
|
764
|
+
for (var i = 0; i < entries.length; i++) {
|
|
765
|
+
var item = entries[i];
|
|
766
|
+
html += '<div class="kv-item">';
|
|
767
|
+
html += '<div class="kv-key">' + esc(item.key) + '</div>';
|
|
768
|
+
html += renderKvValue(item.value);
|
|
769
|
+
html += '</div>';
|
|
770
|
+
}
|
|
771
|
+
html += '</div>';
|
|
772
|
+
return html;
|
|
773
|
+
}
|
|
490
774
|
|
|
491
775
|
/* ---------- Pretty JSON renderer (recursive) ---------- */
|
|
492
776
|
function prettyJson(val, indent) {
|
|
@@ -553,12 +837,28 @@ async function fetchApi(path) {
|
|
|
553
837
|
if (!resp.ok) throw new Error('API error: ' + resp.status);
|
|
554
838
|
return resp.json();
|
|
555
839
|
}
|
|
840
|
+
async function postApi(path, payload) {
|
|
841
|
+
var urlParams = new URLSearchParams(window.location.search);
|
|
842
|
+
var token = urlParams.get('token');
|
|
843
|
+
var headers = { 'Content-Type': 'application/json' };
|
|
844
|
+
if (token) {
|
|
845
|
+
headers['Authorization'] = 'Bearer ' + token;
|
|
846
|
+
}
|
|
847
|
+
var resp = await fetch(API + path, {
|
|
848
|
+
method: 'POST',
|
|
849
|
+
headers: headers,
|
|
850
|
+
body: JSON.stringify(payload || {})
|
|
851
|
+
});
|
|
852
|
+
if (!resp.ok) throw new Error('API error: ' + resp.status);
|
|
853
|
+
return resp.json();
|
|
854
|
+
}
|
|
556
855
|
|
|
557
856
|
/* ---------- SVG icons ---------- */
|
|
558
857
|
var ICON_BACK = '<svg viewBox="0 0 24 24"><path d="M19 12H5M12 19l-7-7 7-7"/></svg>';
|
|
559
858
|
var ICON_SUN = '<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="5"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/></svg>';
|
|
560
859
|
var ICON_MOON = '<svg viewBox="0 0 24 24"><path d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z"/></svg>';
|
|
561
860
|
var ICON_ACTIVITY = '<svg viewBox="0 0 24 24"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>';
|
|
861
|
+
var ICON_REFRESH = '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M20 4v6h-6"/><path d="M4 20v-6h6"/><path d="M19.2 14A8 8 0 0 1 6.1 18.3L4 14"/><path d="M4.8 10A8 8 0 0 1 17.9 5.7L20 10"/></svg>';
|
|
562
862
|
|
|
563
863
|
/* ---------- router ---------- */
|
|
564
864
|
function parseHashParams(hash) {
|
|
@@ -673,6 +973,7 @@ async function renderTraceList() {
|
|
|
673
973
|
html += '</div>';
|
|
674
974
|
});
|
|
675
975
|
html += '</div></div>';
|
|
976
|
+
html += '<button class="icon-refresh-btn" onclick="refreshTraceList()" title="Refresh trace list">' + ICON_REFRESH + '</button>';
|
|
676
977
|
if (hasFilter) {
|
|
677
978
|
html += '<button class="btn-clear" onclick="clearFilter()">✕ Clear</button>';
|
|
678
979
|
}
|
|
@@ -685,7 +986,11 @@ async function renderTraceList() {
|
|
|
685
986
|
if (sessData.sessions.length === 0) {
|
|
686
987
|
html += '<div class="empty"><div class="icon">📭</div><div class="text">No sessions recorded yet</div></div>';
|
|
687
988
|
} else {
|
|
688
|
-
sessData.sessions.
|
|
989
|
+
var visibleSessions = (sessData.sessions || []).filter(function(s) {
|
|
990
|
+
var sid = String((s && s.session_id) || '').toLowerCase();
|
|
991
|
+
return sid !== 'unknown' && sid !== 'unkown' && sid !== '-';
|
|
992
|
+
});
|
|
993
|
+
visibleSessions.forEach(function(s) {
|
|
689
994
|
var dur = (s.start_time && s.end_time)
|
|
690
995
|
? fmtDur(new Date(s.end_time).getTime() - new Date(s.start_time).getTime())
|
|
691
996
|
: '-';
|
|
@@ -705,6 +1010,9 @@ async function renderTraceList() {
|
|
|
705
1010
|
html += '</div>';
|
|
706
1011
|
html += '</div>';
|
|
707
1012
|
});
|
|
1013
|
+
if (visibleSessions.length === 0) {
|
|
1014
|
+
html += '<div class="empty"><div class="icon">📭</div><div class="text">No sessions recorded yet</div></div>';
|
|
1015
|
+
}
|
|
708
1016
|
}
|
|
709
1017
|
|
|
710
1018
|
html += '</div>';
|
|
@@ -749,6 +1057,10 @@ window.clearFilter = function() {
|
|
|
749
1057
|
renderTraceList();
|
|
750
1058
|
};
|
|
751
1059
|
|
|
1060
|
+
window.refreshTraceList = function() {
|
|
1061
|
+
renderTraceList();
|
|
1062
|
+
};
|
|
1063
|
+
|
|
752
1064
|
window.toggleTimeMenu = function(e) {
|
|
753
1065
|
e.stopPropagation();
|
|
754
1066
|
var menu = document.getElementById('time-menu');
|
|
@@ -854,7 +1166,7 @@ async function renderDashboard() {
|
|
|
854
1166
|
// Traces by time (bar chart)
|
|
855
1167
|
html += '<div class="an-card">';
|
|
856
1168
|
var gran = data.granularity || 'day';
|
|
857
|
-
var granLabel = gran === 'hour' ? (
|
|
1169
|
+
var granLabel = gran === 'hour' ? anGetTimeLabel() : (ts.length + ' days');
|
|
858
1170
|
html += '<h3><span class="icon">📊</span> Activity Over Time';
|
|
859
1171
|
html += '<span class="sub">' + granLabel + '</span></h3>';
|
|
860
1172
|
html += buildTimeSeriesChart(ts, anMetricTab, gran);
|
|
@@ -916,6 +1228,8 @@ function buildTimeSeriesChart(ts, metric, gran) {
|
|
|
916
1228
|
if (metric === 'actions') return { v1: p.actions, v2: 0, total: p.actions, label: p.date };
|
|
917
1229
|
return { v1: p.sessions, v2: 0, total: p.sessions, label: p.date };
|
|
918
1230
|
});
|
|
1231
|
+
// Keep chronological order (older -> newer, left to right)
|
|
1232
|
+
vals = vals.slice();
|
|
919
1233
|
var maxVal = Math.max.apply(null, vals.map(function(v){ return v.total; }));
|
|
920
1234
|
if (maxVal === 0) maxVal = 1;
|
|
921
1235
|
|
|
@@ -934,24 +1248,41 @@ function buildTimeSeriesChart(ts, metric, gran) {
|
|
|
934
1248
|
var barColor1 = metric === 'tokens' ? '#8b5cf6' : (metric === 'actions' ? '#3b82f6' : 'var(--accent)');
|
|
935
1249
|
var barColor2 = '#f59e0b';
|
|
936
1250
|
|
|
937
|
-
|
|
938
|
-
var
|
|
939
|
-
|
|
1251
|
+
function formatFullTimeLabel(rawLabel) {
|
|
1252
|
+
var raw = String(rawLabel || '');
|
|
1253
|
+
if (!raw) return '-';
|
|
1254
|
+
// hour bucket: "YYYY-MM-DD HH" -> "YYYY-MM-DD HH:00:00"
|
|
1255
|
+
if (gran === 'hour' && raw.length >= 13) {
|
|
1256
|
+
return raw.slice(0, 13) + ':00:00';
|
|
1257
|
+
}
|
|
1258
|
+
// day bucket: "YYYY-MM-DD" -> "YYYY-MM-DD 00:00:00"
|
|
1259
|
+
if (raw.length >= 10) {
|
|
1260
|
+
return raw.slice(0, 10) + ' 00:00:00';
|
|
1261
|
+
}
|
|
1262
|
+
return raw;
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
function formatAxisLabel(rawLabel) {
|
|
1266
|
+
var raw = String(rawLabel || '');
|
|
1267
|
+
if (!raw) return '';
|
|
940
1268
|
if (gran === 'hour') {
|
|
941
|
-
// "2026-03-12 14"
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
} else {
|
|
945
|
-
dayLabel = v.label.length > 5 ? v.label.slice(5) : v.label; // MM-DD
|
|
1269
|
+
// "2026-03-12 14" -> "03-12<br>14:00"
|
|
1270
|
+
if (raw.length >= 13) return raw.slice(5, 10) + '<br>' + raw.slice(11, 13) + ':00';
|
|
1271
|
+
return raw;
|
|
946
1272
|
}
|
|
1273
|
+
// "2026-03-12" -> "03-12"
|
|
1274
|
+
return raw.length >= 10 ? raw.slice(5, 10) : raw;
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
vals.forEach(function(v) {
|
|
1278
|
+
var pct = Math.max((v.total / maxVal) * 100, 1);
|
|
1279
|
+
var fullLabel = formatFullTimeLabel(v.label);
|
|
947
1280
|
h += '<div class="chart-bar-col">';
|
|
948
|
-
h += '<div class="chart-tooltip">' +
|
|
1281
|
+
h += '<div class="chart-tooltip"><div><b>Time:</b> ' + esc(fullLabel) + '</div><div><b>Value:</b> ' + fmtNum(v.total);
|
|
949
1282
|
if (showTokenSplit) h += ' (in:' + fmtNum(v.v1) + ' out:' + fmtNum(v.v2) + ')';
|
|
950
|
-
h += '</div>';
|
|
1283
|
+
h += '</div></div>';
|
|
951
1284
|
|
|
952
1285
|
if (showTokenSplit && v.v1 + v.v2 > 0) {
|
|
953
|
-
var pct1 = (v.v1 / maxVal) * 100;
|
|
954
|
-
var pct2 = (v.v2 / maxVal) * 100;
|
|
955
1286
|
h += '<div class="chart-bar-stack" style="height:' + pct + '%">';
|
|
956
1287
|
h += '<div class="chart-bar-seg" style="height:' + (v.v2 / v.total * 100) + '%;background:' + barColor2 + '"></div>';
|
|
957
1288
|
h += '<div class="chart-bar-seg" style="height:' + (v.v1 / v.total * 100) + '%;background:' + barColor1 + '"></div>';
|
|
@@ -965,16 +1296,13 @@ function buildTimeSeriesChart(ts, metric, gran) {
|
|
|
965
1296
|
|
|
966
1297
|
// X labels (show max 15 labels)
|
|
967
1298
|
h += '<div class="chart-x-labels">';
|
|
968
|
-
var
|
|
1299
|
+
var maxLabels = gran === 'hour' ? 8 : 10;
|
|
1300
|
+
var step = Math.max(1, Math.ceil(vals.length / maxLabels));
|
|
1301
|
+
var lastIdx = vals.length - 1;
|
|
969
1302
|
vals.forEach(function(v, i) {
|
|
970
|
-
var xLabel;
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
xLabel = hp + ':00';
|
|
974
|
-
} else {
|
|
975
|
-
xLabel = v.label.length > 5 ? v.label.slice(5) : v.label;
|
|
976
|
-
}
|
|
977
|
-
h += '<span>' + (i % step === 0 ? xLabel : '') + '</span>';
|
|
1303
|
+
var xLabel = formatAxisLabel(v.label);
|
|
1304
|
+
var show = (i === 0) || (i === lastIdx) || (i % step === 0);
|
|
1305
|
+
h += '<span title="' + esc(formatFullTimeLabel(v.label)) + '">' + (show ? xLabel : '') + '</span>';
|
|
978
1306
|
});
|
|
979
1307
|
h += '</div>';
|
|
980
1308
|
h += '</div></div>'; // chart-main, chart-container
|
|
@@ -1089,14 +1417,23 @@ function buildActionDistribution(ad) {
|
|
|
1089
1417
|
}
|
|
1090
1418
|
|
|
1091
1419
|
function buildAgentsTable(ta) {
|
|
1092
|
-
|
|
1420
|
+
var rows = (ta || []).filter(function(a) {
|
|
1421
|
+
var name = (a && a.agent ? String(a.agent) : '').toLowerCase();
|
|
1422
|
+
return name !== 'unknown' && name !== 'unkown';
|
|
1423
|
+
}).sort(function(a, b) {
|
|
1424
|
+
var aSystem = String(a.agent || '').toLowerCase() === 'system' ? 1 : 0;
|
|
1425
|
+
var bSystem = String(b.agent || '').toLowerCase() === 'system' ? 1 : 0;
|
|
1426
|
+
if (aSystem !== bSystem) return aSystem - bSystem;
|
|
1427
|
+
return b.sessions - a.sessions;
|
|
1428
|
+
});
|
|
1429
|
+
|
|
1430
|
+
if (!rows || rows.length === 0) {
|
|
1093
1431
|
return '<div class="empty" style="padding:24px"><div class="text">No agent data</div></div>';
|
|
1094
1432
|
}
|
|
1095
1433
|
|
|
1096
|
-
var maxSess = Math.max.apply(null, ta.map(function(a){ return a.sessions; }));
|
|
1097
1434
|
var h = '<table class="an-table">';
|
|
1098
1435
|
h += '<tr><th>Agent</th><th>Sessions</th><th>Actions</th><th>Tokens</th></tr>';
|
|
1099
|
-
|
|
1436
|
+
rows.forEach(function(a) {
|
|
1100
1437
|
h += '<tr>';
|
|
1101
1438
|
h += '<td>🤖 ' + esc(a.agent) + '</td>';
|
|
1102
1439
|
h += '<td class="mono">' + a.sessions + '</td>';
|
|
@@ -1133,14 +1470,35 @@ window.anSwitchMetric = function(metric) {
|
|
|
1133
1470
|
/* ================================================================ */
|
|
1134
1471
|
|
|
1135
1472
|
window.__alertCount = 0;
|
|
1473
|
+
var __alertBadgeLoading = false;
|
|
1474
|
+
var __alertBadgeLoaded = false;
|
|
1475
|
+
var __alertBadgeScheduled = false;
|
|
1136
1476
|
|
|
1137
|
-
|
|
1138
|
-
(
|
|
1477
|
+
async function loadAlertBadge() {
|
|
1478
|
+
if (__alertBadgeLoading || __alertBadgeLoaded) return;
|
|
1479
|
+
__alertBadgeLoading = true;
|
|
1139
1480
|
try {
|
|
1140
1481
|
var st = await fetchApi('/alerts/stats');
|
|
1141
1482
|
window.__alertCount = (st.byStatus && st.byStatus.open) || 0;
|
|
1483
|
+
__alertBadgeLoaded = true;
|
|
1142
1484
|
} catch(e) { /* ignore */ }
|
|
1143
|
-
|
|
1485
|
+
__alertBadgeLoading = false;
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
function scheduleAlertBadgeLoad() {
|
|
1489
|
+
if (__alertBadgeScheduled || __alertBadgeLoaded) return;
|
|
1490
|
+
__alertBadgeScheduled = true;
|
|
1491
|
+
var run = function() {
|
|
1492
|
+
setTimeout(function() {
|
|
1493
|
+
loadAlertBadge();
|
|
1494
|
+
}, 800); // avoid competing with first-screen critical requests
|
|
1495
|
+
};
|
|
1496
|
+
if (typeof window.requestIdleCallback === 'function') {
|
|
1497
|
+
window.requestIdleCallback(run, { timeout: 2000 });
|
|
1498
|
+
} else {
|
|
1499
|
+
setTimeout(run, 1200);
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1144
1502
|
|
|
1145
1503
|
/* ---- Security filter state ---- */
|
|
1146
1504
|
var secFilterSearch = '';
|
|
@@ -1149,6 +1507,8 @@ var secFilterCategory = '';
|
|
|
1149
1507
|
var secFilterStatus = 'open';
|
|
1150
1508
|
var secFilterTimeRange = '';
|
|
1151
1509
|
var secPage = 1;
|
|
1510
|
+
var secRuntimeCfg = null;
|
|
1511
|
+
var secActiveRuleKey = 'secretLeakage';
|
|
1152
1512
|
|
|
1153
1513
|
/* ---- severity helpers ---- */
|
|
1154
1514
|
var SEV_ICON = {critical:'🔴',warn:'🟡',info:'ℹ️'};
|
|
@@ -1156,13 +1516,72 @@ var SEV_LABEL = {critical:'CRITICAL',warn:'WARNING',info:'INFO'};
|
|
|
1156
1516
|
var CAT_LABEL = {
|
|
1157
1517
|
secret_leakage:'Secret Leakage',high_risk_operation:'High Risk Operation',
|
|
1158
1518
|
data_exfiltration:'Data Exfiltration',prompt_injection:'Prompt Injection',
|
|
1519
|
+
custom_regex:'Custom Regex',
|
|
1159
1520
|
skill_anomaly:'Skill Anomaly'
|
|
1160
1521
|
};
|
|
1161
1522
|
var STATUS_LABEL = {open:'Open',acknowledged:'Acknowledged',resolved:'Resolved',false_positive:'False Positive'};
|
|
1162
1523
|
|
|
1163
1524
|
var SEV_OPTIONS = [{k:'',l:'All Severity'},{k:'critical',l:'CRITICAL'},{k:'warn',l:'WARNING'},{k:'info',l:'INFO'}];
|
|
1164
|
-
var CAT_OPTIONS = [{k:'',l:'All Category'},{k:'secret_leakage',l:'Secret Leakage'},{k:'high_risk_operation',l:'High Risk Op'},{k:'data_exfiltration',l:'Data Exfiltration'},{k:'prompt_injection',l:'Prompt Injection'},{k:'skill_anomaly',l:'Skill Anomaly'}];
|
|
1525
|
+
var CAT_OPTIONS = [{k:'',l:'All Category'},{k:'secret_leakage',l:'Secret Leakage'},{k:'high_risk_operation',l:'High Risk Op'},{k:'data_exfiltration',l:'Data Exfiltration'},{k:'prompt_injection',l:'Prompt Injection'},{k:'custom_regex',l:'Custom Regex'},{k:'skill_anomaly',l:'Skill Anomaly'}];
|
|
1165
1526
|
var STA_OPTIONS = [{k:'',l:'All Status'},{k:'open',l:'Open'},{k:'acknowledged',l:'Acknowledged'},{k:'resolved',l:'Resolved'},{k:'false_positive',l:'False Positive'}];
|
|
1527
|
+
var SEC_RULE_META = {
|
|
1528
|
+
secretLeakage: {
|
|
1529
|
+
key: 'secretLeakage',
|
|
1530
|
+
title: 'Secret Leakage',
|
|
1531
|
+
brief: 'Detect exposed AK/SK, token, password and key materials',
|
|
1532
|
+
level: 'critical',
|
|
1533
|
+
description: 'Scans tool input/output and model responses for secret patterns, including keys, tokens and sensitive credential strings.',
|
|
1534
|
+
detects: ['Hardcoded access key or token values', 'Credential output copied from files or env blocks']
|
|
1535
|
+
},
|
|
1536
|
+
highRiskOps: {
|
|
1537
|
+
key: 'highRiskOps',
|
|
1538
|
+
title: 'High Risk Ops',
|
|
1539
|
+
brief: 'Detect dangerous operations such as delete/reset/wipe',
|
|
1540
|
+
level: 'high',
|
|
1541
|
+
description: 'Flags risky tool operations that may alter or destroy data, especially shell and file-system actions.',
|
|
1542
|
+
detects: ['Destructive shell commands', 'Irreversible write/remove actions']
|
|
1543
|
+
},
|
|
1544
|
+
dataExfiltration: {
|
|
1545
|
+
key: 'dataExfiltration',
|
|
1546
|
+
title: 'External Access',
|
|
1547
|
+
brief: 'Detect outbound requests and potential data exfiltration',
|
|
1548
|
+
level: 'high',
|
|
1549
|
+
description: 'Monitors requests to external websites or domains and raises alerts when outbound access may carry sensitive data.',
|
|
1550
|
+
detects: ['Tool calls to non-whitelisted external domains', 'Potential outbound data transfer patterns']
|
|
1551
|
+
},
|
|
1552
|
+
customRegex: {
|
|
1553
|
+
key: 'customRegex',
|
|
1554
|
+
title: 'Custom Regex',
|
|
1555
|
+
brief: 'Customer-defined regex rules with live matching',
|
|
1556
|
+
level: 'medium',
|
|
1557
|
+
description: 'Create your own regex rules for sensitive keywords, IDs, or organization-specific policy patterns.',
|
|
1558
|
+
detects: ['Custom compliance vocabulary', 'Business-specific sensitive identifiers']
|
|
1559
|
+
},
|
|
1560
|
+
promptInjection: {
|
|
1561
|
+
key: 'promptInjection',
|
|
1562
|
+
title: 'Prompt Injection',
|
|
1563
|
+
brief: 'Detect jailbreak or instruction override attempts',
|
|
1564
|
+
level: 'high',
|
|
1565
|
+
description: 'Finds suspicious prompts attempting to override system policy, bypass guardrails or exfiltrate hidden context.',
|
|
1566
|
+
detects: ['Ignore previous instructions patterns', 'Role hijacking and policy bypass phrasing']
|
|
1567
|
+
},
|
|
1568
|
+
chainDetection: {
|
|
1569
|
+
key: 'chainDetection',
|
|
1570
|
+
title: 'Chain Detection',
|
|
1571
|
+
brief: 'Detect suspicious multi-step attack chains',
|
|
1572
|
+
level: 'medium',
|
|
1573
|
+
description: 'Correlates multiple actions in a session and raises alerts for risky sequences that are mild alone but dangerous in combination.',
|
|
1574
|
+
detects: ['Recon + secret read + external write pattern', 'Repeated escalations over short window']
|
|
1575
|
+
}
|
|
1576
|
+
};
|
|
1577
|
+
var SEC_RULE_ORDER = ['secretLeakage', 'highRiskOps', 'promptInjection', 'chainDetection', 'customRegex', 'dataExfiltration'];
|
|
1578
|
+
|
|
1579
|
+
function severityRank(sev) {
|
|
1580
|
+
if (sev === 'critical') return 0;
|
|
1581
|
+
if (sev === 'warn') return 1;
|
|
1582
|
+
if (sev === 'info') return 2;
|
|
1583
|
+
return 3;
|
|
1584
|
+
}
|
|
1166
1585
|
|
|
1167
1586
|
function secGetTimeFromISO() {
|
|
1168
1587
|
if (!secFilterTimeRange) return '';
|
|
@@ -1182,6 +1601,17 @@ function secSelectedLabel(opts, curKey) {
|
|
|
1182
1601
|
return found ? found.l : opts[0].l;
|
|
1183
1602
|
}
|
|
1184
1603
|
|
|
1604
|
+
function secRuleEnabled(ruleKey) {
|
|
1605
|
+
if (!secRuntimeCfg) return false;
|
|
1606
|
+
if (ruleKey === 'enabled') return !!secRuntimeCfg.enabled;
|
|
1607
|
+
return !!(secRuntimeCfg.rules && secRuntimeCfg.rules[ruleKey]);
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
function secSafeActiveRuleKey() {
|
|
1611
|
+
if (SEC_RULE_ORDER.indexOf(secActiveRuleKey) >= 0) return secActiveRuleKey;
|
|
1612
|
+
return 'secretLeakage';
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1185
1615
|
/* build one custom dropdown (same look as Dashboard time picker) */
|
|
1186
1616
|
function secDropdown(id, icon, opts, curKey) {
|
|
1187
1617
|
var label = secSelectedLabel(opts, curKey);
|
|
@@ -1218,10 +1648,12 @@ async function renderSecurity() {
|
|
|
1218
1648
|
|
|
1219
1649
|
var res = await Promise.all([
|
|
1220
1650
|
fetchApi('/alerts/stats'),
|
|
1221
|
-
fetchApi('/alerts?' + qs)
|
|
1651
|
+
fetchApi('/alerts?' + qs),
|
|
1652
|
+
fetchApi('/security/config').catch(function(){ return null; })
|
|
1222
1653
|
]);
|
|
1223
1654
|
var alertStats = res[0];
|
|
1224
1655
|
var alertData = res[1];
|
|
1656
|
+
secRuntimeCfg = res[2] && res[2].config ? res[2].config : null;
|
|
1225
1657
|
|
|
1226
1658
|
// Update badge count
|
|
1227
1659
|
window.__alertCount = (alertStats.byStatus && alertStats.byStatus.open) || 0;
|
|
@@ -1242,6 +1674,107 @@ async function renderSecurity() {
|
|
|
1242
1674
|
html += statCard('Last 24h', String(alertStats.recent24h || 0));
|
|
1243
1675
|
html += '</div>';
|
|
1244
1676
|
|
|
1677
|
+
// --- Runtime security rule toggles ---
|
|
1678
|
+
if (secRuntimeCfg) {
|
|
1679
|
+
var activeRuleKey = secSafeActiveRuleKey();
|
|
1680
|
+
var activeMeta = SEC_RULE_META[activeRuleKey] || SEC_RULE_META.secretLeakage;
|
|
1681
|
+
var activeOn = secRuleEnabled(activeRuleKey);
|
|
1682
|
+
var enabledRules = SEC_RULE_ORDER.filter(function(k){
|
|
1683
|
+
return secRuleEnabled(k);
|
|
1684
|
+
}).length;
|
|
1685
|
+
var customRegexRules = Array.isArray(secRuntimeCfg.customRegexRules) ? secRuntimeCfg.customRegexRules : [];
|
|
1686
|
+
|
|
1687
|
+
html += '<div class="sec-runtime">';
|
|
1688
|
+
html += '<div class="sec-runtime-head">';
|
|
1689
|
+
html += '<div>';
|
|
1690
|
+
html += '<div class="sec-runtime-title">Runtime Security Rules</div>';
|
|
1691
|
+
html += '</div>';
|
|
1692
|
+
html += '<div class="sec-runtime-summary">' + enabledRules + '/' + SEC_RULE_ORDER.length + ' rule(s) enabled</div>';
|
|
1693
|
+
html += '</div>';
|
|
1694
|
+
|
|
1695
|
+
html += '<div class="sec-runtime-grid">';
|
|
1696
|
+
html += '<div class="sec-rule-list">';
|
|
1697
|
+
SEC_RULE_ORDER.forEach(function(ruleKey) {
|
|
1698
|
+
var meta = SEC_RULE_META[ruleKey];
|
|
1699
|
+
if (!meta) return;
|
|
1700
|
+
var on = secRuleEnabled(ruleKey);
|
|
1701
|
+
html += '<div class="sec-rule-item' + (ruleKey === activeRuleKey ? ' active' : '') + '" onclick="secSelectRule(\\'' + ruleKey + '\\')">';
|
|
1702
|
+
html += '<div class="sec-rule-copy">';
|
|
1703
|
+
html += '<div class="sec-rule-name">' + esc(meta.title) + '</div>';
|
|
1704
|
+
html += '<div class="sec-rule-brief">' + esc(meta.brief) + '</div>';
|
|
1705
|
+
html += '</div>';
|
|
1706
|
+
html += '<span class="sec-state-pill ' + (on ? 'on' : 'off') + '">' + (on ? 'ON' : 'OFF') + '</span>';
|
|
1707
|
+
html += '<button class="sec-switch ' + (on ? 'on' : 'off') + '" onclick="event.stopPropagation();secToggleRuntime(\\'' + ruleKey + '\\')" aria-label="Toggle ' + esc(meta.title) + '">';
|
|
1708
|
+
html += '<span class="sec-switch-track"></span><span class="sec-switch-thumb"></span>';
|
|
1709
|
+
html += '</button>';
|
|
1710
|
+
html += '</div>';
|
|
1711
|
+
});
|
|
1712
|
+
html += '</div>';
|
|
1713
|
+
|
|
1714
|
+
html += '<div class="sec-rule-detail">';
|
|
1715
|
+
html += '<div class="sec-rule-detail-title">';
|
|
1716
|
+
html += '<h4>' + esc(activeMeta.title) + '</h4>';
|
|
1717
|
+
html += '<span class="sec-state-pill ' + (activeOn ? 'on' : 'off') + '">' + (activeOn ? 'ON' : 'OFF') + '</span>';
|
|
1718
|
+
html += '</div>';
|
|
1719
|
+
html += '<div class="sec-rule-detail-meta">';
|
|
1720
|
+
html += '<span class="sec-meta-chip level-' + esc(activeMeta.level) + '">Risk ' + esc(activeMeta.level || 'unknown') + '</span>';
|
|
1721
|
+
if (activeRuleKey === 'customRegex') {
|
|
1722
|
+
html += '<span class="sec-meta-chip">Severity Fixed: CRITICAL</span>';
|
|
1723
|
+
}
|
|
1724
|
+
html += '</div>';
|
|
1725
|
+
if (activeRuleKey !== 'customRegex') {
|
|
1726
|
+
html += '<div class="sec-rule-explain"><div class="sec-rule-explain-text">' + esc(activeMeta.description || 'No description') + '</div></div>';
|
|
1727
|
+
html += '<div class="sec-rule-detail-list">';
|
|
1728
|
+
html += '<div class="sec-rule-detail-list-title">Coverage examples</div>';
|
|
1729
|
+
(activeMeta.detects || []).forEach(function(item){
|
|
1730
|
+
html += '<div class="sec-rule-detail-list-item">* ' + esc(item) + '</div>';
|
|
1731
|
+
});
|
|
1732
|
+
html += '</div>';
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
if (activeRuleKey === 'customRegex') {
|
|
1736
|
+
html += '<div class="sec-custom-editor">';
|
|
1737
|
+
html += '<div class="sec-rule-detail-list-title">Add custom regex rule</div>';
|
|
1738
|
+
html += '<div class="sec-custom-form">';
|
|
1739
|
+
html += '<div class="sec-custom-field">';
|
|
1740
|
+
html += '<div class="sec-custom-field-label">Rule Name</div>';
|
|
1741
|
+
html += '<input id="sec-cr-name" type="text" placeholder="e.g. Internal Ticket ID">';
|
|
1742
|
+
html += '</div>';
|
|
1743
|
+
html += '<div class="sec-custom-field">';
|
|
1744
|
+
html += '<div class="sec-custom-field-label">Regex Pattern</div>';
|
|
1745
|
+
html += '<input id="sec-cr-pattern" type="text" placeholder="e.g. TKT-[0-9]{6}">';
|
|
1746
|
+
html += '</div>';
|
|
1747
|
+
html += '<button onclick="secAddCustomRegex()">+ Create Rule</button>';
|
|
1748
|
+
html += '</div>';
|
|
1749
|
+
|
|
1750
|
+
html += '<div class="sec-custom-list">';
|
|
1751
|
+
if (customRegexRules.length === 0) {
|
|
1752
|
+
html += '<div class="sec-rule-detail-list-item">No custom regex rules yet.</div>';
|
|
1753
|
+
} else {
|
|
1754
|
+
customRegexRules.forEach(function(rule, idx){
|
|
1755
|
+
var on = rule.enabled !== false;
|
|
1756
|
+
var flags = rule.flags || 'i';
|
|
1757
|
+
html += '<div class="sec-custom-item">';
|
|
1758
|
+
html += '<div class="sec-custom-item-main">';
|
|
1759
|
+
html += '<div class="sec-custom-item-name">' + esc(rule.name || ('Rule ' + (idx + 1))) + '</div>';
|
|
1760
|
+
html += '<div class="sec-custom-item-pattern">/' + esc(rule.pattern || '') + '/' + esc(flags) + ' · ' + esc((rule.severity || 'warn').toUpperCase()) + '</div>';
|
|
1761
|
+
html += '</div>';
|
|
1762
|
+
html += '<div class="sec-custom-item-actions">';
|
|
1763
|
+
html += '<button class="sec-switch ' + (on ? 'on' : 'off') + '" onclick="secToggleCustomRegexRule(' + idx + ')"><span class="sec-switch-track"></span><span class="sec-switch-thumb"></span></button>';
|
|
1764
|
+
html += '<button class="sec-custom-item-del" title="Delete rule" onclick="secDeleteCustomRegexRule(' + idx + ')">Delete</button>';
|
|
1765
|
+
html += '</div>';
|
|
1766
|
+
html += '</div>';
|
|
1767
|
+
});
|
|
1768
|
+
}
|
|
1769
|
+
html += '</div>';
|
|
1770
|
+
html += '</div>';
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
html += '</div>';
|
|
1774
|
+
html += '</div>';
|
|
1775
|
+
html += '</div>';
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1245
1778
|
// --- Filter bar (same .filter-bar as Dashboard) ---
|
|
1246
1779
|
var hasFilter = secFilterSearch || secFilterSeverity || secFilterCategory || secFilterStatus || secFilterTimeRange;
|
|
1247
1780
|
html += '<div class="filter-bar">';
|
|
@@ -1282,15 +1815,21 @@ async function renderSecurity() {
|
|
|
1282
1815
|
}
|
|
1283
1816
|
html += '</div>';
|
|
1284
1817
|
|
|
1818
|
+
var sortedAlerts = (alertData.alerts || []).slice().sort(function(a, b) {
|
|
1819
|
+
var sevDiff = severityRank(a.severity) - severityRank(b.severity);
|
|
1820
|
+
if (sevDiff !== 0) return sevDiff;
|
|
1821
|
+
return new Date(b.created_at || 0).getTime() - new Date(a.created_at || 0).getTime();
|
|
1822
|
+
});
|
|
1823
|
+
|
|
1285
1824
|
// --- Alert list ---
|
|
1286
1825
|
html += '<div class="section-title">Alerts <span class="count">' + alertData.total + (hasFilter ? ' matched' : ' total') + '</span></div>';
|
|
1287
1826
|
html += '<div class="alert-list">';
|
|
1288
1827
|
|
|
1289
|
-
if (
|
|
1828
|
+
if (sortedAlerts.length === 0) {
|
|
1290
1829
|
html += '<div class="empty"><div class="icon">✅</div><div class="text">No alerts found</div></div>';
|
|
1291
1830
|
} else {
|
|
1292
|
-
|
|
1293
|
-
html += '<div class="alert-card sev-' + a.severity + '" onclick="toggleAlertDetail(\\'' + a.alert_id + '\\')">';
|
|
1831
|
+
sortedAlerts.forEach(function(a) {
|
|
1832
|
+
html += '<div class="alert-card sev-' + a.severity + '" id="alert-card-' + a.alert_id + '" onclick="toggleAlertDetail(\\'' + a.alert_id + '\\')">';
|
|
1294
1833
|
html += '<div class="alert-top">';
|
|
1295
1834
|
html += '<span class="alert-sev ' + a.severity + '">' + (SEV_ICON[a.severity]||'') + ' ' + (SEV_LABEL[a.severity]||a.severity) + '</span>';
|
|
1296
1835
|
html += '<span class="alert-rule">' + esc(a.rule_name) + '</span>';
|
|
@@ -1321,7 +1860,6 @@ async function renderSecurity() {
|
|
|
1321
1860
|
}
|
|
1322
1861
|
if (a.status === 'open' || a.status === 'acknowledged') {
|
|
1323
1862
|
html += '<button class="btn-resolve" onclick="event.stopPropagation();updateAlertSt(\\'' + a.alert_id + '\\',\\'resolved\\')">✓ Resolve</button>';
|
|
1324
|
-
html += '<button class="btn-dismiss" onclick="event.stopPropagation();updateAlertSt(\\'' + a.alert_id + '\\',\\'false_positive\\')">✕ False Positive</button>';
|
|
1325
1863
|
}
|
|
1326
1864
|
html += '</div>';
|
|
1327
1865
|
html += '</div>';
|
|
@@ -1393,6 +1931,98 @@ window.secClearFilter = function() {
|
|
|
1393
1931
|
renderSecurity();
|
|
1394
1932
|
};
|
|
1395
1933
|
|
|
1934
|
+
window.secSelectRule = function(ruleKey) {
|
|
1935
|
+
if (SEC_RULE_ORDER.indexOf(ruleKey) < 0) return;
|
|
1936
|
+
secActiveRuleKey = ruleKey;
|
|
1937
|
+
renderSecurity();
|
|
1938
|
+
};
|
|
1939
|
+
|
|
1940
|
+
window.secToggleRuntime = async function(key) {
|
|
1941
|
+
if (!secRuntimeCfg) return;
|
|
1942
|
+
var patch = {};
|
|
1943
|
+
if (key === 'enabled') {
|
|
1944
|
+
patch.enabled = !secRuntimeCfg.enabled;
|
|
1945
|
+
} else {
|
|
1946
|
+
patch.rules = {};
|
|
1947
|
+
patch.rules[key] = !secRuntimeCfg.rules[key];
|
|
1948
|
+
}
|
|
1949
|
+
try {
|
|
1950
|
+
await postApi('/security/config', patch);
|
|
1951
|
+
renderSecurity();
|
|
1952
|
+
} catch (e) {
|
|
1953
|
+
console.error('Failed to update security config:', e);
|
|
1954
|
+
}
|
|
1955
|
+
};
|
|
1956
|
+
|
|
1957
|
+
window.secAddCustomRegex = async function() {
|
|
1958
|
+
if (!secRuntimeCfg) return;
|
|
1959
|
+
var nameEl = document.getElementById('sec-cr-name');
|
|
1960
|
+
var patternEl = document.getElementById('sec-cr-pattern');
|
|
1961
|
+
var name = nameEl ? nameEl.value.trim() : '';
|
|
1962
|
+
var pattern = patternEl ? patternEl.value.trim() : '';
|
|
1963
|
+
var flags = 'i';
|
|
1964
|
+
var severity = 'critical';
|
|
1965
|
+
if (!pattern) return;
|
|
1966
|
+
|
|
1967
|
+
try {
|
|
1968
|
+
new RegExp(pattern, flags || 'i');
|
|
1969
|
+
} catch (e) {
|
|
1970
|
+
alert('Invalid regex pattern or flags');
|
|
1971
|
+
return;
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
var nextRules = Array.isArray(secRuntimeCfg.customRegexRules) ? secRuntimeCfg.customRegexRules.slice() : [];
|
|
1975
|
+
nextRules.push({
|
|
1976
|
+
id: 'cr_' + Date.now() + '_' + Math.floor(Math.random() * 10000),
|
|
1977
|
+
name: name || 'Custom Regex',
|
|
1978
|
+
pattern: pattern,
|
|
1979
|
+
flags: flags || 'i',
|
|
1980
|
+
severity: severity,
|
|
1981
|
+
enabled: true
|
|
1982
|
+
});
|
|
1983
|
+
|
|
1984
|
+
try {
|
|
1985
|
+
await postApi('/security/config', { customRegexRules: nextRules });
|
|
1986
|
+
renderSecurity();
|
|
1987
|
+
} catch (e) {
|
|
1988
|
+
console.error('Failed to add custom regex rule:', e);
|
|
1989
|
+
}
|
|
1990
|
+
};
|
|
1991
|
+
|
|
1992
|
+
window.secToggleCustomRegexRule = async function(idx) {
|
|
1993
|
+
if (!secRuntimeCfg || !Array.isArray(secRuntimeCfg.customRegexRules)) return;
|
|
1994
|
+
if (idx < 0 || idx >= secRuntimeCfg.customRegexRules.length) return;
|
|
1995
|
+
var nextRules = secRuntimeCfg.customRegexRules.slice();
|
|
1996
|
+
var rule = nextRules[idx];
|
|
1997
|
+
nextRules[idx] = {
|
|
1998
|
+
id: rule.id,
|
|
1999
|
+
name: rule.name,
|
|
2000
|
+
pattern: rule.pattern,
|
|
2001
|
+
flags: rule.flags,
|
|
2002
|
+
severity: rule.severity,
|
|
2003
|
+
enabled: rule.enabled === false ? true : false
|
|
2004
|
+
};
|
|
2005
|
+
try {
|
|
2006
|
+
await postApi('/security/config', { customRegexRules: nextRules });
|
|
2007
|
+
renderSecurity();
|
|
2008
|
+
} catch (e) {
|
|
2009
|
+
console.error('Failed to toggle custom regex rule:', e);
|
|
2010
|
+
}
|
|
2011
|
+
};
|
|
2012
|
+
|
|
2013
|
+
window.secDeleteCustomRegexRule = async function(idx) {
|
|
2014
|
+
if (!secRuntimeCfg || !Array.isArray(secRuntimeCfg.customRegexRules)) return;
|
|
2015
|
+
if (idx < 0 || idx >= secRuntimeCfg.customRegexRules.length) return;
|
|
2016
|
+
var nextRules = secRuntimeCfg.customRegexRules.slice();
|
|
2017
|
+
nextRules.splice(idx, 1);
|
|
2018
|
+
try {
|
|
2019
|
+
await postApi('/security/config', { customRegexRules: nextRules });
|
|
2020
|
+
renderSecurity();
|
|
2021
|
+
} catch (e) {
|
|
2022
|
+
console.error('Failed to delete custom regex rule:', e);
|
|
2023
|
+
}
|
|
2024
|
+
};
|
|
2025
|
+
|
|
1396
2026
|
window.secGoPage = function(p) {
|
|
1397
2027
|
if (p < 1) return;
|
|
1398
2028
|
secPage = p;
|
|
@@ -1401,12 +2031,34 @@ window.secGoPage = function(p) {
|
|
|
1401
2031
|
|
|
1402
2032
|
window.updateAlertSt = async function(alertId, newStatus) {
|
|
1403
2033
|
try {
|
|
1404
|
-
await
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
2034
|
+
await postApi('/alerts/' + encodeURIComponent(alertId) + '/status', { status: newStatus });
|
|
2035
|
+
|
|
2036
|
+
var card = document.getElementById('alert-card-' + alertId);
|
|
2037
|
+
if (card) {
|
|
2038
|
+
card.style.transition = 'opacity .12s ease, transform .12s ease';
|
|
2039
|
+
card.style.opacity = '0';
|
|
2040
|
+
card.style.transform = 'translateY(-2px)';
|
|
2041
|
+
setTimeout(function() {
|
|
2042
|
+
if (card && card.parentNode) card.parentNode.removeChild(card);
|
|
2043
|
+
}, 130);
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
if (window.__alertCount > 0) {
|
|
2047
|
+
window.__alertCount -= 1;
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2050
|
+
var statNodes = document.querySelectorAll('.stat .stat-label');
|
|
2051
|
+
for (var i = 0; i < statNodes.length; i++) {
|
|
2052
|
+
var labelNode = statNodes[i];
|
|
2053
|
+
if (!labelNode || !labelNode.textContent) continue;
|
|
2054
|
+
if (labelNode.textContent.trim().toLowerCase() === 'open') {
|
|
2055
|
+
var valueNode = labelNode.parentElement ? labelNode.parentElement.querySelector('.stat-value') : null;
|
|
2056
|
+
if (valueNode) {
|
|
2057
|
+
var cur = parseInt((valueNode.textContent || '0').trim(), 10);
|
|
2058
|
+
if (!isNaN(cur) && cur > 0) valueNode.textContent = String(cur - 1);
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
1410
2062
|
} catch(e) {
|
|
1411
2063
|
console.error('Failed to update alert:', e);
|
|
1412
2064
|
}
|
|
@@ -1425,9 +2077,16 @@ document.addEventListener('click', function() {
|
|
|
1425
2077
|
/* ================================================================ */
|
|
1426
2078
|
|
|
1427
2079
|
var selectedActionIdx = -1;
|
|
2080
|
+
var currentTraceSessionId = '';
|
|
2081
|
+
var currentTraceHighlightAction = '';
|
|
2082
|
+
var currentTraceHighlightTime = '';
|
|
2083
|
+
var traceSearchQuery = '';
|
|
1428
2084
|
|
|
1429
2085
|
async function renderTraceDetail(sessionId, highlightAction, highlightTime) {
|
|
1430
2086
|
selectedActionIdx = -1;
|
|
2087
|
+
currentTraceSessionId = sessionId;
|
|
2088
|
+
currentTraceHighlightAction = highlightAction || '';
|
|
2089
|
+
currentTraceHighlightTime = highlightTime || '';
|
|
1431
2090
|
app.innerHTML = renderLayout('trace',
|
|
1432
2091
|
'<div class="trace-header"><div class="loading">Loading trace...</div></div>');
|
|
1433
2092
|
|
|
@@ -1459,61 +2118,289 @@ async function renderTraceDetail(sessionId, highlightAction, highlightTime) {
|
|
|
1459
2118
|
action: a,
|
|
1460
2119
|
startMs: startMs,
|
|
1461
2120
|
endMs: endMs,
|
|
2121
|
+
displayDurationMs: a.duration_ms,
|
|
1462
2122
|
level: 0,
|
|
1463
2123
|
children: []
|
|
1464
2124
|
};
|
|
1465
2125
|
});
|
|
1466
2126
|
|
|
2127
|
+
function findOwnerLlmStartMs(thinkingSpan, allSpans) {
|
|
2128
|
+
var owner = null;
|
|
2129
|
+
for (var i = 0; i < allSpans.length; i++) {
|
|
2130
|
+
var span = allSpans[i];
|
|
2131
|
+
if (span.action.action_type !== 'message') continue;
|
|
2132
|
+
if (!span.action.action_name || span.action.action_name.indexOf('llm_call:') !== 0) continue;
|
|
2133
|
+
if (span.startMs <= thinkingSpan.endMs && span.endMs >= thinkingSpan.endMs) {
|
|
2134
|
+
if (!owner || span.endMs < owner.endMs) owner = span;
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
return owner ? owner.startMs : null;
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
// For thinking rows without explicit duration, infer a visible span from the
|
|
2141
|
+
// previous action end to current thinking timestamp.
|
|
2142
|
+
var spansByEndTime = spans.slice().sort(function(a, b) {
|
|
2143
|
+
var diff = a.endMs - b.endMs;
|
|
2144
|
+
if (diff !== 0) return diff;
|
|
2145
|
+
return a.idx - b.idx;
|
|
2146
|
+
});
|
|
2147
|
+
var prevEndMs = null;
|
|
2148
|
+
spansByEndTime.forEach(function(span) {
|
|
2149
|
+
if (
|
|
2150
|
+
span.action.action_type === 'thinking' &&
|
|
2151
|
+
(span.action.duration_ms == null || span.action.duration_ms <= 0) &&
|
|
2152
|
+
prevEndMs != null &&
|
|
2153
|
+
span.endMs > prevEndMs
|
|
2154
|
+
) {
|
|
2155
|
+
var ownerLlmStartMs = findOwnerLlmStartMs(span, spans);
|
|
2156
|
+
var inferredStartMs = prevEndMs;
|
|
2157
|
+
if (ownerLlmStartMs != null) {
|
|
2158
|
+
inferredStartMs = Math.max(inferredStartMs, ownerLlmStartMs);
|
|
2159
|
+
}
|
|
2160
|
+
span.startMs = inferredStartMs;
|
|
2161
|
+
span.displayDurationMs = span.endMs - span.startMs;
|
|
2162
|
+
}
|
|
2163
|
+
prevEndMs = Math.max(prevEndMs == null ? span.endMs : prevEndMs, span.endMs);
|
|
2164
|
+
});
|
|
2165
|
+
|
|
1467
2166
|
// Determine time range
|
|
1468
2167
|
var sessionStart = Math.min.apply(null, spans.map(function(s){ return s.startMs; }));
|
|
1469
2168
|
var sessionEnd = Math.max.apply(null, spans.map(function(s){ return s.endMs; }));
|
|
1470
2169
|
var totalDur = sessionEnd - sessionStart || 1;
|
|
1471
2170
|
|
|
1472
2171
|
// ---- Sorting + auto-nesting ----
|
|
1473
|
-
//
|
|
1474
|
-
//
|
|
2172
|
+
// We keep the data in chronological order inside a trace. Structural span
|
|
2173
|
+
// priority is only a tie-breaker for near-identical timestamps.
|
|
1475
2174
|
function typePriority(actionType) {
|
|
1476
|
-
if (actionType === 'agent_end') return 0;
|
|
1477
|
-
if (actionType === 'message')
|
|
1478
|
-
return
|
|
2175
|
+
if (actionType === 'agent_end') return 0;
|
|
2176
|
+
if (actionType === 'message') return 1;
|
|
2177
|
+
if (actionType === 'model_resolve') return 2;
|
|
2178
|
+
if (actionType === 'prompt_build') return 3;
|
|
2179
|
+
if (actionType === 'tool_call') return 2;
|
|
2180
|
+
if (actionType === 'assistant_stream') return 4;
|
|
2181
|
+
if (actionType === 'thinking') return 5;
|
|
2182
|
+
if (actionType === 'tool_update') return 6;
|
|
2183
|
+
return 10;
|
|
2184
|
+
}
|
|
2185
|
+
function canBecomeParent(actionType) {
|
|
2186
|
+
return actionType === 'agent_end' || actionType === 'message' || actionType === 'tool_call';
|
|
1479
2187
|
}
|
|
1480
2188
|
// Sort by startMs asc; when startMs is close (within 2s), prioritize by semantic priority; then by duration desc
|
|
1481
2189
|
spans.sort(function(a, b) {
|
|
1482
2190
|
var startDiff = a.startMs - b.startMs;
|
|
1483
|
-
if (
|
|
2191
|
+
if (startDiff !== 0) return startDiff;
|
|
1484
2192
|
var priA = typePriority(a.action.action_type);
|
|
1485
2193
|
var priB = typePriority(b.action.action_type);
|
|
1486
2194
|
if (priA !== priB) return priA - priB;
|
|
1487
|
-
|
|
2195
|
+
var endDiff = a.endMs - b.endMs;
|
|
2196
|
+
if (endDiff !== 0) return endDiff;
|
|
2197
|
+
return a.idx - b.idx;
|
|
1488
2198
|
});
|
|
1489
2199
|
|
|
2200
|
+
var toolParentsById = {};
|
|
2201
|
+
spans.forEach(function(span) {
|
|
2202
|
+
if (span.action.action_type === 'tool_call') {
|
|
2203
|
+
var toolCallId = getToolCallId(span.action);
|
|
2204
|
+
if (!toolCallId) return;
|
|
2205
|
+
if (!toolParentsById[toolCallId]) toolParentsById[toolCallId] = [];
|
|
2206
|
+
toolParentsById[toolCallId].push(span);
|
|
2207
|
+
}
|
|
2208
|
+
});
|
|
2209
|
+
Object.keys(toolParentsById).forEach(function(id) {
|
|
2210
|
+
toolParentsById[id].sort(function(a, b) {
|
|
2211
|
+
var diff = a.startMs - b.startMs;
|
|
2212
|
+
if (diff !== 0) return diff;
|
|
2213
|
+
return a.idx - b.idx;
|
|
2214
|
+
});
|
|
2215
|
+
});
|
|
2216
|
+
|
|
2217
|
+
function resolveExactToolParent(span, toolCallId) {
|
|
2218
|
+
var candidates = toolParentsById[toolCallId];
|
|
2219
|
+
if (!candidates || candidates.length === 0) return null;
|
|
2220
|
+
var containing = null;
|
|
2221
|
+
for (var i = 0; i < candidates.length; i++) {
|
|
2222
|
+
var parent = candidates[i];
|
|
2223
|
+
if (parent.startMs <= span.endMs + 50 && parent.endMs >= span.startMs - 1000) {
|
|
2224
|
+
containing = parent;
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
if (containing) return containing;
|
|
2228
|
+
|
|
2229
|
+
var nearest = null;
|
|
2230
|
+
var nearestGap = Infinity;
|
|
2231
|
+
for (var j = 0; j < candidates.length; j++) {
|
|
2232
|
+
var p = candidates[j];
|
|
2233
|
+
var gap = Math.abs(p.endMs - span.endMs);
|
|
2234
|
+
if (gap < nearestGap) {
|
|
2235
|
+
nearest = p;
|
|
2236
|
+
nearestGap = gap;
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
return nearest;
|
|
2240
|
+
}
|
|
2241
|
+
|
|
1490
2242
|
// Auto-nesting: use "open parent" stack to determine depth
|
|
1491
2243
|
// If a span's time range is fully contained within an earlier span, indent it
|
|
1492
|
-
var parentStack = []; // { startMs, endMs, level }
|
|
2244
|
+
var parentStack = []; // { startMs, endMs, level, actionType }
|
|
1493
2245
|
var flatSpans = [];
|
|
1494
2246
|
|
|
1495
2247
|
spans.forEach(function(span) {
|
|
2248
|
+
var exactToolParent = null;
|
|
2249
|
+
var spanToolCallId = getToolCallId(span.action);
|
|
2250
|
+
if (span.action.action_type !== 'tool_call' && spanToolCallId) {
|
|
2251
|
+
exactToolParent = resolveExactToolParent(span, spanToolCallId);
|
|
2252
|
+
}
|
|
2253
|
+
|
|
1496
2254
|
// Pop completed parents
|
|
1497
2255
|
while (parentStack.length > 0 &&
|
|
1498
2256
|
parentStack[parentStack.length - 1].endMs < span.startMs - 1000) {
|
|
1499
2257
|
parentStack.pop();
|
|
1500
2258
|
}
|
|
1501
2259
|
|
|
1502
|
-
//
|
|
1503
|
-
|
|
2260
|
+
// Prefer exact tool-call linkage over duration-based guesses.
|
|
2261
|
+
if (exactToolParent) {
|
|
2262
|
+
span.level = exactToolParent.level + 1;
|
|
2263
|
+
} else if (span.action.action_type === 'tool_call') {
|
|
2264
|
+
// Tool calls should be peer spans under the current non-tool parent
|
|
2265
|
+
// (e.g. under llm_call), not nested under previous tool_call rows.
|
|
2266
|
+
span.level = parentStack.filter(function(p) {
|
|
2267
|
+
return p.actionType !== 'tool_call';
|
|
2268
|
+
}).length;
|
|
2269
|
+
} else if (span.action.action_type === 'thinking') {
|
|
2270
|
+
// Keep thinking on the same layer as tool_call to avoid drifting
|
|
2271
|
+
// into nested tool sub-rows.
|
|
2272
|
+
span.level = parentStack.filter(function(p) {
|
|
2273
|
+
return p.actionType !== 'tool_call';
|
|
2274
|
+
}).length;
|
|
2275
|
+
} else {
|
|
2276
|
+
// Current span depth = number of containing parents
|
|
2277
|
+
span.level = parentStack.length;
|
|
2278
|
+
}
|
|
1504
2279
|
|
|
1505
2280
|
flatSpans.push(span);
|
|
1506
2281
|
|
|
1507
|
-
//
|
|
2282
|
+
// Only structural spans with real duration can become parents in the tree.
|
|
1508
2283
|
var dur = span.endMs - span.startMs;
|
|
1509
|
-
if (dur > 100) {
|
|
1510
|
-
parentStack.push({
|
|
2284
|
+
if (dur > 100 && canBecomeParent(span.action.action_type)) {
|
|
2285
|
+
parentStack.push({
|
|
2286
|
+
startMs: span.startMs,
|
|
2287
|
+
endMs: span.endMs,
|
|
2288
|
+
level: span.level,
|
|
2289
|
+
actionType: span.action.action_type
|
|
2290
|
+
});
|
|
1511
2291
|
}
|
|
1512
2292
|
});
|
|
1513
2293
|
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
2294
|
+
flatSpans.forEach(function(span, idx) {
|
|
2295
|
+
span.flatIdx = idx;
|
|
2296
|
+
});
|
|
2297
|
+
|
|
2298
|
+
function splitTraceGroups(spansInOrder) {
|
|
2299
|
+
var runGroupsById = {};
|
|
2300
|
+
var runGroups = [];
|
|
2301
|
+
|
|
2302
|
+
spansInOrder.forEach(function(span) {
|
|
2303
|
+
var runId = getActionRunId(span.action);
|
|
2304
|
+
if (!runId) return;
|
|
2305
|
+
if (!runGroupsById[runId]) {
|
|
2306
|
+
var group = {
|
|
2307
|
+
runId: runId,
|
|
2308
|
+
rootSpan: null,
|
|
2309
|
+
items: [],
|
|
2310
|
+
startMs: span.startMs,
|
|
2311
|
+
endMs: span.endMs
|
|
2312
|
+
};
|
|
2313
|
+
runGroupsById[runId] = group;
|
|
2314
|
+
runGroups.push(group);
|
|
2315
|
+
}
|
|
2316
|
+
var g = runGroupsById[runId];
|
|
2317
|
+
g.items.push(span);
|
|
2318
|
+
g.startMs = Math.min(g.startMs, span.startMs);
|
|
2319
|
+
g.endMs = Math.max(g.endMs, span.endMs);
|
|
2320
|
+
});
|
|
2321
|
+
|
|
2322
|
+
if (runGroups.length > 0) {
|
|
2323
|
+
return runGroups
|
|
2324
|
+
.map(function(g) { return { runId: g.runId, rootSpan: null, items: g.items }; })
|
|
2325
|
+
.filter(function(g) { return g.items && g.items.length; });
|
|
2326
|
+
}
|
|
2327
|
+
|
|
2328
|
+
var rootSpans = spansInOrder.filter(function(span) {
|
|
2329
|
+
return span.level === 0 && span.action.action_type === 'agent_end';
|
|
2330
|
+
});
|
|
2331
|
+
|
|
2332
|
+
if (rootSpans.length === 0) {
|
|
2333
|
+
return spansInOrder.length ? [{ rootSpan: null, items: spansInOrder.slice() }] : [];
|
|
2334
|
+
}
|
|
2335
|
+
|
|
2336
|
+
var groups = rootSpans.map(function(rootSpan) {
|
|
2337
|
+
return {
|
|
2338
|
+
rootSpan: rootSpan,
|
|
2339
|
+
items: [],
|
|
2340
|
+
windowStart: rootSpan.startMs,
|
|
2341
|
+
windowEnd: rootSpan.endMs + 1000
|
|
2342
|
+
};
|
|
2343
|
+
});
|
|
2344
|
+
|
|
2345
|
+
var orphanSpans = [];
|
|
2346
|
+
|
|
2347
|
+
spansInOrder.forEach(function(span) {
|
|
2348
|
+
if (span.level === 0 && span.action.action_type === 'agent_end') return;
|
|
2349
|
+
|
|
2350
|
+
var targetGroup = null;
|
|
2351
|
+
for (var i = 0; i < groups.length; i++) {
|
|
2352
|
+
var group = groups[i];
|
|
2353
|
+
var overlapsWindow = span.endMs >= group.windowStart - 1000 && span.startMs <= group.windowEnd;
|
|
2354
|
+
if (overlapsWindow) {
|
|
2355
|
+
targetGroup = group;
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
|
|
2359
|
+
if (targetGroup) {
|
|
2360
|
+
targetGroup.items.push(span);
|
|
2361
|
+
} else {
|
|
2362
|
+
orphanSpans.push(span);
|
|
2363
|
+
}
|
|
2364
|
+
});
|
|
2365
|
+
|
|
2366
|
+
var normalizedGroups = groups.map(function(group) {
|
|
2367
|
+
return {
|
|
2368
|
+
runId: '',
|
|
2369
|
+
rootSpan: group.rootSpan,
|
|
2370
|
+
items: group.items
|
|
2371
|
+
};
|
|
2372
|
+
});
|
|
2373
|
+
|
|
2374
|
+
if (orphanSpans.length > 0) {
|
|
2375
|
+
normalizedGroups.unshift({ runId: '', rootSpan: null, items: orphanSpans });
|
|
2376
|
+
}
|
|
2377
|
+
|
|
2378
|
+
return normalizedGroups;
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
var normalizedTraceQuery = normalizeTraceSearch(traceSearchQuery);
|
|
2382
|
+
var matchedFlatIdxs = [];
|
|
2383
|
+
var matchedByIdx = {};
|
|
2384
|
+
var snippetByIdx = {};
|
|
2385
|
+
|
|
2386
|
+
if (normalizedTraceQuery) {
|
|
2387
|
+
flatSpans.forEach(function(span) {
|
|
2388
|
+
if (span.action.action_type === 'agent_end' && span.level === 0) return;
|
|
2389
|
+
var searchText = getActionSearchText(span.action).toLowerCase();
|
|
2390
|
+
if (searchText.indexOf(normalizedTraceQuery) >= 0) {
|
|
2391
|
+
matchedFlatIdxs.push(span.flatIdx);
|
|
2392
|
+
matchedByIdx[span.flatIdx] = true;
|
|
2393
|
+
snippetByIdx[span.flatIdx] = getActionSearchSnippet(span.action, normalizedTraceQuery);
|
|
2394
|
+
}
|
|
2395
|
+
});
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2398
|
+
// Session metadata (chronological boundaries)
|
|
2399
|
+
var actionsByTime = actions.slice().sort(function(a, b) {
|
|
2400
|
+
return new Date(a.created_at).getTime() - new Date(b.created_at).getTime();
|
|
2401
|
+
});
|
|
2402
|
+
var firstAction = actionsByTime[0];
|
|
2403
|
+
var lastAction = actionsByTime[actionsByTime.length - 1];
|
|
1517
2404
|
var modelName = '';
|
|
1518
2405
|
var userId = firstAction.user_id || '';
|
|
1519
2406
|
for (var i = 0; i < actions.length; i++) {
|
|
@@ -1527,7 +2414,9 @@ async function renderTraceDetail(sessionId, highlightAction, highlightTime) {
|
|
|
1527
2414
|
|
|
1528
2415
|
// Trace header
|
|
1529
2416
|
html += '<div class="trace-header">';
|
|
2417
|
+
html += '<div class="trace-header-top">';
|
|
1530
2418
|
html += '<a href="#/traces" class="trace-back">← Back to Traces</a>';
|
|
2419
|
+
html += '</div>';
|
|
1531
2420
|
html += '<div class="trace-title">Session: ' + esc(sessionId) + '</div>';
|
|
1532
2421
|
html += '<div class="trace-meta">';
|
|
1533
2422
|
html += '<span class="trace-meta-item"><b>Agent:</b> ' + esc(userId) + '</span>';
|
|
@@ -1535,7 +2424,27 @@ async function renderTraceDetail(sessionId, highlightAction, highlightTime) {
|
|
|
1535
2424
|
html += '<span class="trace-meta-item"><b>Duration:</b> ' + fmtDur(totalDur) + '</span>';
|
|
1536
2425
|
html += '<span class="trace-meta-item"><b>Actions:</b> ' + actions.length + '</span>';
|
|
1537
2426
|
html += '<span class="trace-meta-item"><b>Time:</b> ' + fmtTime(firstAction.created_at) + ' → ' + fmtTime(lastAction.created_at) + '</span>';
|
|
1538
|
-
html += '</div
|
|
2427
|
+
html += '</div>';
|
|
2428
|
+
html += '<div class="filter-bar trace-search-bar">';
|
|
2429
|
+
html += '<input type="text" id="trace-search-input" placeholder="Search this session: action, input, output, model..." value="' + esc(traceSearchQuery) + '" onkeydown="if(event.key===\\'Enter\\')applyTraceSearch()">';
|
|
2430
|
+
html += '<div class="trace-search-nav">';
|
|
2431
|
+
html += '<button class="icon-refresh-btn" onclick="refreshCurrentTrace()" title="Refresh current trace">' + ICON_REFRESH + '</button>';
|
|
2432
|
+
html += '<button onclick="applyTraceSearch()">Search</button>';
|
|
2433
|
+
html += '<button onclick="traceSearchStep(-1)"' + (matchedFlatIdxs.length ? '' : ' disabled') + '>Prev</button>';
|
|
2434
|
+
html += '<button onclick="traceSearchStep(1)"' + (matchedFlatIdxs.length ? '' : ' disabled') + '>Next</button>';
|
|
2435
|
+
html += '<button class="btn-clear" onclick="clearTraceSearch()">Clear</button>';
|
|
2436
|
+
html += '</div>';
|
|
2437
|
+
html += '<div class="trace-search-stats">';
|
|
2438
|
+
if (normalizedTraceQuery) {
|
|
2439
|
+
html += matchedFlatIdxs.length
|
|
2440
|
+
? ('Matched ' + matchedFlatIdxs.length + ' action' + (matchedFlatIdxs.length > 1 ? 's' : '') + ' in this session')
|
|
2441
|
+
: 'No matches in this session';
|
|
2442
|
+
} else {
|
|
2443
|
+
html += 'Search within this session';
|
|
2444
|
+
}
|
|
2445
|
+
html += '</div>';
|
|
2446
|
+
html += '</div>';
|
|
2447
|
+
html += '</div>';
|
|
1539
2448
|
|
|
1540
2449
|
// Security alert banner (if any)
|
|
1541
2450
|
if (traceAlerts.length > 0) {
|
|
@@ -1550,44 +2459,94 @@ async function renderTraceDetail(sessionId, highlightAction, highlightTime) {
|
|
|
1550
2459
|
}
|
|
1551
2460
|
|
|
1552
2461
|
// Waterfall
|
|
2462
|
+
if (normalizedTraceQuery && matchedFlatIdxs.length === 0) {
|
|
2463
|
+
html += '<div class="trace-search-empty">No action matched "' + esc(traceSearchQuery) + '". Try a tool name, model, prompt fragment, or error text.</div>';
|
|
2464
|
+
}
|
|
1553
2465
|
html += '<div class="waterfall">';
|
|
1554
2466
|
html += '<div class="wf-header"><span>Name</span><span style="text-align:right;padding-right:14px">Duration</span><span>Timeline</span></div>';
|
|
1555
2467
|
|
|
1556
|
-
|
|
1557
|
-
|
|
2468
|
+
function getVisibleGroupItems(group) {
|
|
2469
|
+
return group.items && group.items.length ? group.items : (group.rootSpan ? [group.rootSpan] : []);
|
|
2470
|
+
}
|
|
1558
2471
|
|
|
1559
|
-
|
|
1560
|
-
var
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
var widthPct = Math.max(((span.endMs - span.startMs) / totalDur * 100), 0.3).toFixed(2);
|
|
1564
|
-
var indent = span.level * 20;
|
|
2472
|
+
function getPrimaryGroupActionIdx(group) {
|
|
2473
|
+
var visibleItems = getVisibleGroupItems(group);
|
|
2474
|
+
return visibleItems[0] ? visibleItems[0].flatIdx : -1;
|
|
2475
|
+
}
|
|
1565
2476
|
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
2477
|
+
var traceGroups = splitTraceGroups(flatSpans).reverse();
|
|
2478
|
+
var TRACE_COLLAPSE_LIMIT = 30;
|
|
2479
|
+
|
|
2480
|
+
traceGroups.forEach(function(group, groupIdx) {
|
|
2481
|
+
var allGroupSpans = group.rootSpan ? [group.rootSpan].concat(group.items) : group.items.slice();
|
|
2482
|
+
var visibleGroupItems = getVisibleGroupItems(group).slice().sort(function(a, b) {
|
|
2483
|
+
var startDiff = a.startMs - b.startMs;
|
|
2484
|
+
if (startDiff !== 0) return startDiff;
|
|
2485
|
+
var priA = typePriority(a.action.action_type);
|
|
2486
|
+
var priB = typePriority(b.action.action_type);
|
|
2487
|
+
if (priA !== priB) return priA - priB;
|
|
2488
|
+
var endDiff = a.endMs - b.endMs;
|
|
2489
|
+
if (endDiff !== 0) return endDiff;
|
|
2490
|
+
return a.idx - b.idx;
|
|
2491
|
+
});
|
|
2492
|
+
var orderedByCreated = allGroupSpans.slice().sort(function(a, b) {
|
|
2493
|
+
return new Date(a.action.created_at).getTime() - new Date(b.action.created_at).getTime();
|
|
2494
|
+
});
|
|
2495
|
+
var groupStart = orderedByCreated[0];
|
|
2496
|
+
var groupEnd = orderedByCreated[orderedByCreated.length - 1];
|
|
2497
|
+
var groupStartMs = Math.min.apply(null, allGroupSpans.map(function(span) { return span.startMs; }));
|
|
2498
|
+
var groupEndMs = Math.max.apply(null, allGroupSpans.map(function(span) { return span.endMs; }));
|
|
2499
|
+
var groupTotalDur = groupEndMs - groupStartMs || 1;
|
|
2500
|
+
var groupMatchIdxs = visibleGroupItems.filter(function(span) { return !!matchedByIdx[span.flatIdx]; }).map(function(span) { return span.flatIdx; });
|
|
2501
|
+
var groupSnippet = groupMatchIdxs.length ? snippetByIdx[groupMatchIdxs[0]] : '';
|
|
2502
|
+
var shouldCollapseForSearch = normalizedTraceQuery && groupMatchIdxs.length === 0;
|
|
2503
|
+
var shouldCollapseForLength = !normalizedTraceQuery && visibleGroupItems.length > TRACE_COLLAPSE_LIMIT;
|
|
2504
|
+
var runDuration = group.rootSpan ? group.rootSpan.action.duration_ms : (groupEndMs - groupStartMs);
|
|
2505
|
+
var collapsedCls = (shouldCollapseForSearch || shouldCollapseForLength) ? ' collapsed' : '';
|
|
2506
|
+
html += '<div class="wf-group-section' + collapsedCls + '" data-group-idx="' + groupIdx + '">';
|
|
2507
|
+
html += '<div class="wf-group" onclick="toggleTraceGroup(' + groupIdx + ')">';
|
|
2508
|
+
html += '<div class="wf-group-main">';
|
|
2509
|
+
html += '<span class="wf-group-toggle" data-group-toggle="' + groupIdx + '">' + (collapsedCls ? '▸' : '▾') + '</span>';
|
|
2510
|
+
html += '<div class="wf-group-copy">';
|
|
2511
|
+
html += '<div class="wf-group-title-row">';
|
|
2512
|
+
var runTitle = 'Trace ' + (groupIdx + 1);
|
|
2513
|
+
html += '<span class="wf-group-title">' + runTitle + '</span>';
|
|
2514
|
+
if (runDuration != null) {
|
|
2515
|
+
html += '<span class="wf-group-hit">' + fmtDur(runDuration) + '</span>';
|
|
2516
|
+
}
|
|
2517
|
+
if (groupMatchIdxs.length) {
|
|
2518
|
+
html += '<span class="wf-group-hit">' + groupMatchIdxs.length + ' hit' + (groupMatchIdxs.length > 1 ? 's' : '') + '</span>';
|
|
2519
|
+
}
|
|
2520
|
+
html += '</div>';
|
|
2521
|
+
if (groupSnippet) {
|
|
2522
|
+
html += '<div class="wf-group-snippet">' + highlightSearchText(groupSnippet, normalizedTraceQuery) + '</div>';
|
|
2523
|
+
} else if (group.rootSpan) {
|
|
2524
|
+
html += '<div class="wf-group-snippet">' + esc(group.rootSpan.action.action_name) + ' completed this run</div>';
|
|
1571
2525
|
}
|
|
1572
|
-
|
|
1573
|
-
var hiddenCls = (needFold && fi >= WF_FOLD_LIMIT) ? ' wf-row--hidden' : '';
|
|
1574
|
-
html += '<div class="wf-row' + hiddenCls + (fi === selectedActionIdx ? ' selected' : '') + '" data-idx="' + fi + '" onclick="selectAction(' + fi + ')">';
|
|
1575
|
-
html += '<div class="wf-name">';
|
|
1576
|
-
if (indent > 0) html += '<span class="indent" style="width:' + indent + 'px;display:inline-flex;justify-content:center">└</span>';
|
|
1577
|
-
html += '<span class="dot" style="background:' + color + '"></span>';
|
|
1578
|
-
html += '<span class="text">' + esc(a.action_name) + '</span>';
|
|
1579
2526
|
html += '</div>';
|
|
1580
|
-
html += '<div class="wf-dur">' + fmtDur(a.duration_ms) + '</div>';
|
|
1581
|
-
html += '<div class="wf-bar-wrap"><div class="wf-bar" style="left:' + leftPct + '%;width:' + widthPct + '%;background:' + color + '"></div></div>';
|
|
1582
2527
|
html += '</div>';
|
|
1583
|
-
|
|
2528
|
+
html += '<span class="wf-group-meta">' + fmtTime(new Date(groupStartMs).toISOString()) + ' → ' + fmtTime(groupEnd.action.created_at) + ' | ' + visibleGroupItems.length + ' actions</span>';
|
|
2529
|
+
html += '</div>';
|
|
1584
2530
|
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
2531
|
+
visibleGroupItems.forEach(function(span) {
|
|
2532
|
+
var a = span.action;
|
|
2533
|
+
var color = typeColor(a.action_type);
|
|
2534
|
+
var leftPct = ((span.startMs - groupStartMs) / groupTotalDur * 100).toFixed(2);
|
|
2535
|
+
var widthPct = Math.max(((span.endMs - span.startMs) / groupTotalDur * 100), 0.3).toFixed(2);
|
|
2536
|
+
var indent = span.level * 20;
|
|
2537
|
+
var matchCls = matchedByIdx[span.flatIdx] ? ' wf-row--match' : '';
|
|
2538
|
+
html += '<div class="wf-row' + matchCls + (span.flatIdx === selectedActionIdx ? ' selected' : '') + '" data-action-idx="' + span.flatIdx + '" onclick="selectAction(' + span.flatIdx + ', false)">';
|
|
2539
|
+
html += '<div class="wf-name">';
|
|
2540
|
+
if (indent > 0) html += '<span class="indent" style="width:' + indent + 'px;display:inline-flex;justify-content:center">└</span>';
|
|
2541
|
+
html += '<span class="dot" style="background:' + color + '"></span>';
|
|
2542
|
+
html += '<span class="text">' + highlightSearchText(a.action_name, normalizedTraceQuery) + '</span>';
|
|
2543
|
+
html += '</div>';
|
|
2544
|
+
html += '<div class="wf-dur">' + fmtDur(span.displayDurationMs != null ? span.displayDurationMs : a.duration_ms) + '</div>';
|
|
2545
|
+
html += '<div class="wf-bar-wrap"><div class="wf-bar" style="left:' + leftPct + '%;width:' + widthPct + '%;background:' + color + '"></div></div>';
|
|
2546
|
+
html += '</div>';
|
|
2547
|
+
});
|
|
2548
|
+
html += '</div>';
|
|
2549
|
+
});
|
|
1591
2550
|
html += '</div>'; // .waterfall
|
|
1592
2551
|
html += '</div>'; // .trace-top
|
|
1593
2552
|
|
|
@@ -1603,6 +2562,8 @@ async function renderTraceDetail(sessionId, highlightAction, highlightTime) {
|
|
|
1603
2562
|
|
|
1604
2563
|
// Store spans for detail rendering
|
|
1605
2564
|
window.__traceSpans = flatSpans;
|
|
2565
|
+
window.__traceMatchActionIdxs = matchedFlatIdxs;
|
|
2566
|
+
window.__traceSearchCursor = 0;
|
|
1606
2567
|
|
|
1607
2568
|
// Init resize drag
|
|
1608
2569
|
initResizeDrag();
|
|
@@ -1621,18 +2582,31 @@ async function renderTraceDetail(sessionId, highlightAction, highlightTime) {
|
|
|
1621
2582
|
});
|
|
1622
2583
|
}
|
|
1623
2584
|
}
|
|
1624
|
-
|
|
2585
|
+
if (autoIdx < 0 && matchedFlatIdxs.length > 0) {
|
|
2586
|
+
autoIdx = matchedFlatIdxs[0];
|
|
2587
|
+
}
|
|
2588
|
+
if (autoIdx >= 0) {
|
|
2589
|
+
var autoSpan = flatSpans[autoIdx];
|
|
2590
|
+
if (autoSpan && autoSpan.action.action_type === 'agent_end' && autoSpan.level === 0) {
|
|
2591
|
+
var containingGroup = traceGroups.find(function(group) {
|
|
2592
|
+
return group.rootSpan && group.rootSpan.flatIdx === autoIdx;
|
|
2593
|
+
});
|
|
2594
|
+
autoIdx = containingGroup ? getPrimaryGroupActionIdx(containingGroup) : autoIdx;
|
|
2595
|
+
}
|
|
2596
|
+
}
|
|
2597
|
+
// If no highlight match, default to the newest trace's first action
|
|
1625
2598
|
if (autoIdx < 0) {
|
|
1626
|
-
autoIdx =
|
|
2599
|
+
autoIdx = traceGroups[0] ? getPrimaryGroupActionIdx(traceGroups[0]) : -1;
|
|
1627
2600
|
}
|
|
1628
2601
|
if (autoIdx < 0) autoIdx = 0;
|
|
1629
2602
|
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
2603
|
+
if (matchedFlatIdxs.length > 0) {
|
|
2604
|
+
var selectedMatchIdx = matchedFlatIdxs.indexOf(autoIdx);
|
|
2605
|
+
window.__traceSearchCursor = selectedMatchIdx >= 0 ? selectedMatchIdx : 0;
|
|
1633
2606
|
}
|
|
1634
2607
|
|
|
1635
|
-
|
|
2608
|
+
var shouldAutoScroll = !!normalizedTraceQuery || !!highlightAction || !!highlightTime;
|
|
2609
|
+
selectAction(autoIdx, shouldAutoScroll);
|
|
1636
2610
|
|
|
1637
2611
|
} catch(err) {
|
|
1638
2612
|
app.innerHTML = renderLayout('trace',
|
|
@@ -1643,12 +2617,41 @@ async function renderTraceDetail(sessionId, highlightAction, highlightTime) {
|
|
|
1643
2617
|
}
|
|
1644
2618
|
}
|
|
1645
2619
|
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
var
|
|
1651
|
-
if (
|
|
2620
|
+
window.toggleTraceGroup = function(groupIdx) {
|
|
2621
|
+
var section = document.querySelector('.wf-group-section[data-group-idx="' + groupIdx + '"]');
|
|
2622
|
+
if (!section) return;
|
|
2623
|
+
section.classList.toggle('collapsed');
|
|
2624
|
+
var toggle = section.querySelector('[data-group-toggle="' + groupIdx + '"]');
|
|
2625
|
+
if (toggle) {
|
|
2626
|
+
toggle.textContent = section.classList.contains('collapsed') ? '▸' : '▾';
|
|
2627
|
+
}
|
|
2628
|
+
};
|
|
2629
|
+
|
|
2630
|
+
window.applyTraceSearch = function() {
|
|
2631
|
+
var input = document.getElementById('trace-search-input');
|
|
2632
|
+
traceSearchQuery = input ? input.value.trim() : '';
|
|
2633
|
+
if (!currentTraceSessionId) return;
|
|
2634
|
+
renderTraceDetail(currentTraceSessionId, currentTraceHighlightAction, currentTraceHighlightTime);
|
|
2635
|
+
};
|
|
2636
|
+
|
|
2637
|
+
window.clearTraceSearch = function() {
|
|
2638
|
+
traceSearchQuery = '';
|
|
2639
|
+
if (!currentTraceSessionId) return;
|
|
2640
|
+
renderTraceDetail(currentTraceSessionId, currentTraceHighlightAction, currentTraceHighlightTime);
|
|
2641
|
+
};
|
|
2642
|
+
|
|
2643
|
+
window.refreshCurrentTrace = function() {
|
|
2644
|
+
if (!currentTraceSessionId) return;
|
|
2645
|
+
renderTraceDetail(currentTraceSessionId, currentTraceHighlightAction, currentTraceHighlightTime);
|
|
2646
|
+
};
|
|
2647
|
+
|
|
2648
|
+
window.traceSearchStep = function(delta) {
|
|
2649
|
+
var matches = window.__traceMatchActionIdxs || [];
|
|
2650
|
+
if (!matches.length) return;
|
|
2651
|
+
var cursor = Number(window.__traceSearchCursor || 0);
|
|
2652
|
+
cursor = (cursor + delta + matches.length) % matches.length;
|
|
2653
|
+
window.__traceSearchCursor = cursor;
|
|
2654
|
+
selectAction(matches[cursor], true);
|
|
1652
2655
|
};
|
|
1653
2656
|
|
|
1654
2657
|
/* ---------- resize drag logic ---------- */
|
|
@@ -1656,17 +2659,18 @@ function initResizeDrag() {
|
|
|
1656
2659
|
var handle = document.getElementById('trace-resize');
|
|
1657
2660
|
if (!handle) return;
|
|
1658
2661
|
|
|
1659
|
-
var
|
|
1660
|
-
var
|
|
1661
|
-
var
|
|
2662
|
+
var startX = 0;
|
|
2663
|
+
var startRightW = 0;
|
|
2664
|
+
var rightEl = null;
|
|
1662
2665
|
|
|
1663
2666
|
function onMouseDown(e) {
|
|
1664
2667
|
e.preventDefault();
|
|
1665
2668
|
e.stopPropagation();
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
2669
|
+
if (window.innerWidth <= 900) return;
|
|
2670
|
+
rightEl = document.querySelector('.trace-bottom');
|
|
2671
|
+
if (!rightEl) return;
|
|
2672
|
+
startX = e.clientX != null ? e.clientX : (e.touches ? e.touches[0].clientX : 0);
|
|
2673
|
+
startRightW = rightEl.getBoundingClientRect().width;
|
|
1670
2674
|
handle.classList.add('active');
|
|
1671
2675
|
document.body.classList.add('resizing');
|
|
1672
2676
|
document.addEventListener('mousemove', onMouseMove, true);
|
|
@@ -1678,10 +2682,10 @@ function initResizeDrag() {
|
|
|
1678
2682
|
function onMouseMove(e) {
|
|
1679
2683
|
e.preventDefault();
|
|
1680
2684
|
e.stopPropagation();
|
|
1681
|
-
var
|
|
1682
|
-
var diff =
|
|
1683
|
-
var
|
|
1684
|
-
|
|
2685
|
+
var clientX = e.clientX != null ? e.clientX : (e.touches ? e.touches[0].clientX : 0);
|
|
2686
|
+
var diff = startX - clientX;
|
|
2687
|
+
var newW = Math.max(320, Math.min(window.innerWidth * 0.55, startRightW + diff));
|
|
2688
|
+
rightEl.style.flexBasis = newW + 'px';
|
|
1685
2689
|
}
|
|
1686
2690
|
|
|
1687
2691
|
function onMouseUp(e) {
|
|
@@ -1698,20 +2702,44 @@ function initResizeDrag() {
|
|
|
1698
2702
|
}
|
|
1699
2703
|
|
|
1700
2704
|
/* ---------- select action (renders detail in bottom pane) ---------- */
|
|
1701
|
-
window.selectAction = function(fi) {
|
|
2705
|
+
window.selectAction = function(fi, shouldScroll) {
|
|
1702
2706
|
selectedActionIdx = fi;
|
|
1703
2707
|
var spans = window.__traceSpans;
|
|
1704
2708
|
if (!spans || fi < 0 || fi >= spans.length) return;
|
|
2709
|
+
var matches = window.__traceMatchActionIdxs || [];
|
|
2710
|
+
var matchPos = matches.indexOf(fi);
|
|
2711
|
+
if (matchPos >= 0) {
|
|
2712
|
+
window.__traceSearchCursor = matchPos;
|
|
2713
|
+
}
|
|
1705
2714
|
|
|
1706
2715
|
// Update selected row styling
|
|
1707
2716
|
var rows = document.querySelectorAll('.wf-row');
|
|
1708
|
-
rows.forEach(function(r
|
|
1709
|
-
r.classList.toggle('selected',
|
|
2717
|
+
rows.forEach(function(r) {
|
|
2718
|
+
r.classList.toggle('selected', Number(r.getAttribute('data-action-idx')) === fi);
|
|
1710
2719
|
});
|
|
1711
2720
|
|
|
1712
2721
|
// Scroll the selected row into view within the top pane
|
|
1713
|
-
|
|
1714
|
-
|
|
2722
|
+
var row = document.querySelector('.wf-row[data-action-idx="' + fi + '"]');
|
|
2723
|
+
if (row) {
|
|
2724
|
+
var section = row.closest('.wf-group-section');
|
|
2725
|
+
if (section && section.classList.contains('collapsed')) {
|
|
2726
|
+
section.classList.remove('collapsed');
|
|
2727
|
+
var toggle = section.querySelector('[data-group-toggle]');
|
|
2728
|
+
if (toggle) toggle.textContent = '▾';
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
if (row && shouldScroll === true) {
|
|
2732
|
+
var traceTop = document.querySelector('.trace-top');
|
|
2733
|
+
if (traceTop) {
|
|
2734
|
+
var header = traceTop.querySelector('.trace-header');
|
|
2735
|
+
var headerHeight = header ? header.getBoundingClientRect().height : 0;
|
|
2736
|
+
var traceTopRect = traceTop.getBoundingClientRect();
|
|
2737
|
+
var rowRect = row.getBoundingClientRect();
|
|
2738
|
+
var targetTop = (rowRect.top - traceTopRect.top) + traceTop.scrollTop - headerHeight - 8;
|
|
2739
|
+
traceTop.scrollTo({ top: Math.max(0, targetTop), behavior: 'smooth' });
|
|
2740
|
+
} else {
|
|
2741
|
+
row.scrollIntoView({ block: 'start', behavior: 'smooth' });
|
|
2742
|
+
}
|
|
1715
2743
|
}
|
|
1716
2744
|
|
|
1717
2745
|
var span = spans[fi];
|
|
@@ -1719,6 +2747,7 @@ window.selectAction = function(fi) {
|
|
|
1719
2747
|
var color = typeColor(a.action_type);
|
|
1720
2748
|
var input = parseJson(a.input_params);
|
|
1721
2749
|
var output = parseJson(a.output_result);
|
|
2750
|
+
var mediaPaths = Array.from(new Set(collectMediaPaths(input))).slice(0, 8);
|
|
1722
2751
|
|
|
1723
2752
|
var html = '<div class="detail-panel">';
|
|
1724
2753
|
|
|
@@ -1742,20 +2771,39 @@ window.selectAction = function(fi) {
|
|
|
1742
2771
|
|
|
1743
2772
|
// Body: Input / Output side by side
|
|
1744
2773
|
html += '<div class="detail-body">';
|
|
2774
|
+
var useStructuredView = !isToolActionType(a.action_type);
|
|
1745
2775
|
|
|
1746
2776
|
html += '<div class="detail-section">';
|
|
1747
2777
|
html += '<h4>Input</h4>';
|
|
1748
|
-
if (input) {
|
|
1749
|
-
|
|
2778
|
+
if (input !== null && input !== undefined) {
|
|
2779
|
+
if (useStructuredView) {
|
|
2780
|
+
html += renderStructuredFields(input);
|
|
2781
|
+
} else {
|
|
2782
|
+
html += '<div class="json-view">' + prettyJson(input) + '</div>';
|
|
2783
|
+
}
|
|
1750
2784
|
} else {
|
|
1751
2785
|
html += '<div class="json-view" style="color:var(--muted)">null</div>';
|
|
1752
2786
|
}
|
|
2787
|
+
if (mediaPaths.length > 0) {
|
|
2788
|
+
html += '<div class="media-preview">';
|
|
2789
|
+
mediaPaths.forEach(function(p) {
|
|
2790
|
+
var mediaUrl = buildMediaUrl(p);
|
|
2791
|
+
html += '<a class="media-card" href="' + mediaUrl + '" target="_blank" rel="noopener noreferrer" title="Open full image">';
|
|
2792
|
+
html += '<img src="' + mediaUrl + '" loading="lazy" alt="inbound-media" onerror="onMediaImgError(this)">';
|
|
2793
|
+
html += '</a>';
|
|
2794
|
+
});
|
|
2795
|
+
html += '</div>';
|
|
2796
|
+
}
|
|
1753
2797
|
html += '</div>';
|
|
1754
2798
|
|
|
1755
2799
|
html += '<div class="detail-section">';
|
|
1756
2800
|
html += '<h4>Output</h4>';
|
|
1757
|
-
if (output) {
|
|
1758
|
-
|
|
2801
|
+
if (output !== null && output !== undefined) {
|
|
2802
|
+
if (useStructuredView) {
|
|
2803
|
+
html += renderStructuredFields(output);
|
|
2804
|
+
} else {
|
|
2805
|
+
html += '<div class="json-view">' + prettyJson(output) + '</div>';
|
|
2806
|
+
}
|
|
1759
2807
|
} else {
|
|
1760
2808
|
html += '<div class="json-view" style="color:var(--muted)">null</div>';
|
|
1761
2809
|
}
|
|
@@ -1772,5 +2820,6 @@ window.selectAction = function(fi) {
|
|
|
1772
2820
|
|
|
1773
2821
|
/* ---------- init ---------- */
|
|
1774
2822
|
router();
|
|
2823
|
+
scheduleAlertBadgeLoad();
|
|
1775
2824
|
`;
|
|
1776
2825
|
//# sourceMappingURL=ui.js.map
|