openclaw-observability 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config.d.ts +60 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +140 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1114 -0
- package/dist/index.js.map +1 -0
- package/dist/redaction.d.ts +20 -0
- package/dist/redaction.d.ts.map +1 -0
- package/dist/redaction.js +93 -0
- package/dist/redaction.js.map +1 -0
- package/dist/security/chain-detector.d.ts +37 -0
- package/dist/security/chain-detector.d.ts.map +1 -0
- package/dist/security/chain-detector.js +187 -0
- package/dist/security/chain-detector.js.map +1 -0
- package/dist/security/rules.d.ts +22 -0
- package/dist/security/rules.d.ts.map +1 -0
- package/dist/security/rules.js +479 -0
- package/dist/security/rules.js.map +1 -0
- package/dist/security/scanner.d.ts +47 -0
- package/dist/security/scanner.d.ts.map +1 -0
- package/dist/security/scanner.js +150 -0
- package/dist/security/scanner.js.map +1 -0
- package/dist/security/types.d.ts +47 -0
- package/dist/security/types.d.ts.map +1 -0
- package/dist/security/types.js +23 -0
- package/dist/security/types.js.map +1 -0
- package/dist/storage/buffer.d.ts +64 -0
- package/dist/storage/buffer.d.ts.map +1 -0
- package/dist/storage/buffer.js +120 -0
- package/dist/storage/buffer.js.map +1 -0
- package/dist/storage/duckdb-local-writer.d.ts +26 -0
- package/dist/storage/duckdb-local-writer.d.ts.map +1 -0
- package/dist/storage/duckdb-local-writer.js +454 -0
- package/dist/storage/duckdb-local-writer.js.map +1 -0
- package/dist/storage/mysql-writer.d.ts +55 -0
- package/dist/storage/mysql-writer.d.ts.map +1 -0
- package/dist/storage/mysql-writer.js +287 -0
- package/dist/storage/mysql-writer.js.map +1 -0
- package/dist/storage/schema.d.ts +13 -0
- package/dist/storage/schema.d.ts.map +1 -0
- package/dist/storage/schema.js +94 -0
- package/dist/storage/schema.js.map +1 -0
- package/dist/storage/writer.d.ts +31 -0
- package/dist/storage/writer.d.ts.map +1 -0
- package/dist/storage/writer.js +7 -0
- package/dist/storage/writer.js.map +1 -0
- package/dist/types.d.ts +72 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +44 -0
- package/dist/types.js.map +1 -0
- package/dist/web/api.d.ts +115 -0
- package/dist/web/api.d.ts.map +1 -0
- package/dist/web/api.js +219 -0
- package/dist/web/api.js.map +1 -0
- package/dist/web/routes.d.ts +20 -0
- package/dist/web/routes.d.ts.map +1 -0
- package/dist/web/routes.js +175 -0
- package/dist/web/routes.js.map +1 -0
- package/dist/web/ui.d.ts +9 -0
- package/dist/web/ui.d.ts.map +1 -0
- package/dist/web/ui.js +1327 -0
- package/dist/web/ui.js.map +1 -0
- package/openclaw.plugin.json +231 -0
- package/package.json +41 -0
package/dist/web/ui.js
ADDED
|
@@ -0,0 +1,1327 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Audit panel SPA — single-page HTML application
|
|
4
|
+
* Contains Dashboard overview + Session Trace detail views
|
|
5
|
+
* Uses hash routing: #/ = Dashboard, #/trace/{sessionId} = Trace
|
|
6
|
+
*
|
|
7
|
+
* Fully aligned with OpenClaw Control design system (CSS variables + dark/light themes)
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.getAppHtml = getAppHtml;
|
|
11
|
+
function getAppHtml() {
|
|
12
|
+
return '<!DOCTYPE html>\n' +
|
|
13
|
+
'<html lang="en">\n' +
|
|
14
|
+
'<head>\n' +
|
|
15
|
+
'<meta charset="UTF-8">\n' +
|
|
16
|
+
'<meta name="viewport" content="width=device-width, initial-scale=1.0">\n' +
|
|
17
|
+
'<title>OpenClaw Audit Traces</title>\n' +
|
|
18
|
+
'<meta name="color-scheme" content="dark light">\n' +
|
|
19
|
+
'<style>\n' +
|
|
20
|
+
CSS +
|
|
21
|
+
'</style>\n' +
|
|
22
|
+
'</head>\n' +
|
|
23
|
+
'<body>\n' +
|
|
24
|
+
'<div id="app"></div>\n' +
|
|
25
|
+
'<script>\n' +
|
|
26
|
+
CLIENT_JS +
|
|
27
|
+
'</script>\n' +
|
|
28
|
+
'</body>\n' +
|
|
29
|
+
'</html>';
|
|
30
|
+
}
|
|
31
|
+
/* ================================================================== */
|
|
32
|
+
/* CSS — OpenClaw native design tokens */
|
|
33
|
+
/* ================================================================== */
|
|
34
|
+
const CSS = `
|
|
35
|
+
/* ---- OpenClaw Design Tokens (dark default, light via [data-theme=light]) ---- */
|
|
36
|
+
:root {
|
|
37
|
+
--bg:#12141a;--bg-accent:#14161d;--bg-elevated:#1a1d25;--bg-hover:#262a35;
|
|
38
|
+
--card:#181b22;--card-foreground:#f4f4f5;--card-highlight:rgba(255,255,255,.05);
|
|
39
|
+
--panel:#12141a;--panel-strong:#1a1d25;
|
|
40
|
+
--text:#e4e4e7;--text-strong:#fafafa;
|
|
41
|
+
--muted:#71717a;--muted-strong:#52525b;
|
|
42
|
+
--border:#27272a;--border-strong:#3f3f46;--border-hover:#52525b;
|
|
43
|
+
--input:#27272a;
|
|
44
|
+
--accent:#ff5c5c;--accent-hover:#ff7070;--accent-subtle:rgba(255,92,92,.15);--accent-foreground:#fafafa;--accent-glow:rgba(255,92,92,.25);
|
|
45
|
+
--primary:#ff5c5c;--primary-foreground:#ffffff;
|
|
46
|
+
--secondary:#1e2028;--secondary-foreground:#f4f4f5;
|
|
47
|
+
--ok:#22c55e;--ok-subtle:rgba(34,197,94,.12);
|
|
48
|
+
--warn:#f59e0b;--warn-subtle:rgba(245,158,11,.12);
|
|
49
|
+
--danger:#ef4444;--danger-subtle:rgba(239,68,68,.12);
|
|
50
|
+
--info:#3b82f6;
|
|
51
|
+
--ring:#ff5c5c;
|
|
52
|
+
--focus-ring:0 0 0 2px var(--bg),0 0 0 4px var(--ring);
|
|
53
|
+
--mono:"JetBrains Mono",ui-monospace,SFMono-Regular,"SF Mono",Menlo,Monaco,Consolas,monospace;
|
|
54
|
+
--font-body:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
|
|
55
|
+
--shadow-sm:0 1px 2px rgba(0,0,0,.2);
|
|
56
|
+
--shadow-md:0 4px 12px rgba(0,0,0,.25),0 0 0 1px rgba(255,255,255,.03);
|
|
57
|
+
--radius-sm:6px;--radius-md:8px;--radius-lg:12px;--radius-full:9999px;
|
|
58
|
+
--duration-fast:.12s;--duration-normal:.2s;
|
|
59
|
+
--ease-out:cubic-bezier(.16,1,.3,1);
|
|
60
|
+
color-scheme:dark
|
|
61
|
+
}
|
|
62
|
+
:root[data-theme=light]{
|
|
63
|
+
--bg:#fafafa;--bg-accent:#f5f5f5;--bg-elevated:#ffffff;--bg-hover:#f0f0f0;
|
|
64
|
+
--card:#ffffff;--card-foreground:#18181b;--card-highlight:rgba(0,0,0,.03);
|
|
65
|
+
--panel:#fafafa;--panel-strong:#f5f5f5;
|
|
66
|
+
--text:#3f3f46;--text-strong:#18181b;
|
|
67
|
+
--muted:#71717a;--muted-strong:#52525b;
|
|
68
|
+
--border:#e4e4e7;--border-strong:#d4d4d8;--border-hover:#a1a1aa;
|
|
69
|
+
--input:#e4e4e7;
|
|
70
|
+
--accent:#dc2626;--accent-hover:#ef4444;--accent-subtle:rgba(220,38,38,.12);--accent-foreground:#ffffff;--accent-glow:rgba(220,38,38,.15);
|
|
71
|
+
--primary:#dc2626;--primary-foreground:#ffffff;
|
|
72
|
+
--secondary:#f4f4f5;--secondary-foreground:#3f3f46;
|
|
73
|
+
--shadow-sm:0 1px 2px rgba(0,0,0,.06);
|
|
74
|
+
--shadow-md:0 4px 12px rgba(0,0,0,.08),0 0 0 1px rgba(0,0,0,.04);
|
|
75
|
+
color-scheme:light
|
|
76
|
+
}
|
|
77
|
+
/* ---- Reset ---- */
|
|
78
|
+
*{margin:0;padding:0;box-sizing:border-box}
|
|
79
|
+
html,body{height:100%}
|
|
80
|
+
body{font:400 14px/1.55 var(--font-body);letter-spacing:-.02em;background:var(--bg);color:var(--text);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}
|
|
81
|
+
a{color:var(--accent);text-decoration:none}
|
|
82
|
+
a:hover{text-decoration:underline}
|
|
83
|
+
::selection{background:var(--accent-subtle);color:var(--text-strong)}
|
|
84
|
+
::-webkit-scrollbar{width:8px;height:8px}
|
|
85
|
+
::-webkit-scrollbar-track{background:transparent}
|
|
86
|
+
::-webkit-scrollbar-thumb{background:var(--border);border-radius:var(--radius-full)}
|
|
87
|
+
::-webkit-scrollbar-thumb:hover{background:var(--border-strong)}
|
|
88
|
+
|
|
89
|
+
/* ---- Shell layout ---- */
|
|
90
|
+
.shell{min-height:100vh;display:flex;flex-direction:column}
|
|
91
|
+
.shell--trace{height:100vh;max-height:100vh;min-height:100vh;overflow:hidden}
|
|
92
|
+
|
|
93
|
+
/* ---- Topbar (matches OpenClaw .topbar) ---- */
|
|
94
|
+
.topbar{position:sticky;top:0;z-index:40;display:flex;justify-content:space-between;align-items:center;gap:16px;padding:0 20px;height:56px;border-bottom:1px solid var(--border);background:var(--bg)}
|
|
95
|
+
.topbar-left{display:flex;align-items:center;gap:12px}
|
|
96
|
+
.brand{display:flex;align-items:center;gap:10px}
|
|
97
|
+
.brand-logo{width:28px;height:28px;flex-shrink:0;display:flex;align-items:center;justify-content:center}
|
|
98
|
+
.brand-logo svg{width:24px;height:24px;stroke:var(--accent);fill:none;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round}
|
|
99
|
+
.brand-text{display:flex;flex-direction:column;gap:1px}
|
|
100
|
+
.brand-title{font-size:16px;font-weight:700;letter-spacing:-.03em;line-height:1.1;color:var(--text-strong)}
|
|
101
|
+
.brand-sub{font-size:10px;font-weight:500;color:var(--muted);letter-spacing:.05em;text-transform:uppercase;line-height:1}
|
|
102
|
+
.topbar-nav{display:flex;gap:4px;margin-left:24px}
|
|
103
|
+
.topbar-nav a{color:var(--muted);text-decoration:none;padding:6px 14px;border-radius:var(--radius-md);font-size:13px;font-weight:500;transition:all var(--duration-fast) var(--ease-out)}
|
|
104
|
+
.topbar-nav a:hover{color:var(--text);background:var(--bg-hover);text-decoration:none}
|
|
105
|
+
.topbar-nav a.active{color:var(--accent-foreground);background:var(--accent)}
|
|
106
|
+
.topbar-right{display:flex;align-items:center;gap:8px}
|
|
107
|
+
.topbar-right .back-link{display:inline-flex;align-items:center;gap:6px;color:var(--muted);font-size:13px;font-weight:500;padding:6px 12px;border:1px solid var(--border);border-radius:var(--radius-md);background:var(--bg-elevated);text-decoration:none;transition:all var(--duration-fast) var(--ease-out)}
|
|
108
|
+
.topbar-right .back-link:hover{color:var(--text);border-color:var(--border-strong);background:var(--bg-hover);text-decoration:none}
|
|
109
|
+
.topbar-right .back-link svg{width:14px;height:14px;stroke:currentColor;fill:none;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round}
|
|
110
|
+
.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)}
|
|
111
|
+
.theme-btn:hover{color:var(--text);border-color:var(--border-strong);background:var(--bg-hover)}
|
|
112
|
+
.theme-btn svg{width:16px;height:16px;stroke:currentColor;fill:none;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round}
|
|
113
|
+
|
|
114
|
+
/* ---- Content ---- */
|
|
115
|
+
.content{flex:1;padding:16px 20px 32px;min-height:0;overflow-y:auto}
|
|
116
|
+
.content-inner{max-width:1280px;margin:0 auto}
|
|
117
|
+
|
|
118
|
+
/* ---- Stats (matches OpenClaw .stat) ---- */
|
|
119
|
+
.stat-grid{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));margin-bottom:24px}
|
|
120
|
+
.stat{background:var(--card);border-radius:var(--radius-md);padding:14px 16px;border:1px solid var(--border);transition:border-color var(--duration-normal) var(--ease-out),box-shadow var(--duration-normal) var(--ease-out);box-shadow:inset 0 1px 0 var(--card-highlight)}
|
|
121
|
+
.stat:hover{border-color:var(--border-strong);box-shadow:var(--shadow-sm),inset 0 1px 0 var(--card-highlight)}
|
|
122
|
+
.stat-label{color:var(--muted);font-size:11px;font-weight:500;text-transform:uppercase;letter-spacing:.04em}
|
|
123
|
+
.stat-value{font-size:24px;font-weight:700;margin-top:6px;letter-spacing:-.03em;line-height:1.1;color:var(--text-strong)}
|
|
124
|
+
|
|
125
|
+
/* ---- Filter bar ---- */
|
|
126
|
+
.filter-bar{display:flex;gap:10px;align-items:center;margin-bottom:16px;flex-wrap:wrap}
|
|
127
|
+
.filter-bar input[type=text]{background:var(--bg-elevated);border:1px solid var(--border);border-radius:var(--radius-md);padding:7px 12px;font-size:13px;color:var(--text);outline:none;transition:border-color var(--duration-fast);flex:1;min-width:200px}
|
|
128
|
+
.filter-bar input[type=text]:focus{border-color:var(--accent);box-shadow:var(--focus-ring)}
|
|
129
|
+
.filter-bar .filter-sep{width:1px;height:24px;background:var(--border);flex-shrink:0}
|
|
130
|
+
.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)}
|
|
131
|
+
.filter-bar .btn-clear:hover{color:var(--text);border-color:var(--border-strong);background:var(--bg-hover)}
|
|
132
|
+
/* Time range dropdown */
|
|
133
|
+
.time-dropdown{position:relative}
|
|
134
|
+
.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)}
|
|
135
|
+
.time-btn:hover{border-color:var(--border-strong);background:var(--bg-hover)}
|
|
136
|
+
.time-btn svg{width:14px;height:14px;stroke:currentColor;fill:none;stroke-width:1.5;flex-shrink:0}
|
|
137
|
+
.time-menu{position:absolute;top:100%;left:0;margin-top:4px;background:var(--card);border:1px solid var(--border);border-radius:var(--radius-md);box-shadow:var(--shadow-md);z-index:50;min-width:160px;padding:4px 0;display:none}
|
|
138
|
+
.time-menu.open{display:block}
|
|
139
|
+
.time-menu-item{padding:8px 16px;font-size:13px;color:var(--text);cursor:pointer;display:flex;align-items:center;gap:8px;transition:background var(--duration-fast)}
|
|
140
|
+
.time-menu-item:hover{background:var(--bg-hover)}
|
|
141
|
+
.time-menu-item.active{color:var(--accent);font-weight:600}
|
|
142
|
+
.time-menu-item .check{width:14px;display:inline-block;text-align:center;font-size:12px}
|
|
143
|
+
|
|
144
|
+
/* ---- Section title ---- */
|
|
145
|
+
.section-title{font-size:15px;font-weight:600;color:var(--text-strong);margin-bottom:14px;display:flex;align-items:center;justify-content:space-between;letter-spacing:-.02em}
|
|
146
|
+
.section-title .count{font-size:12px;color:var(--muted);font-weight:400;background:var(--secondary);padding:2px 10px;border-radius:var(--radius-full);border:1px solid var(--border)}
|
|
147
|
+
|
|
148
|
+
/* ---- Session list (matches OpenClaw .list-item) ---- */
|
|
149
|
+
.session-list{display:flex;flex-direction:column;gap:8px}
|
|
150
|
+
.session-card{background:var(--card);padding:14px 18px;border-radius:var(--radius-md);cursor:pointer;border:1px solid var(--border);transition:border-color var(--duration-fast) ease,box-shadow var(--duration-fast) ease,transform var(--duration-fast) ease;box-shadow:inset 0 1px 0 var(--card-highlight)}
|
|
151
|
+
.session-card:hover{border-color:var(--border-strong);box-shadow:var(--shadow-md),inset 0 1px 0 var(--card-highlight);transform:translateY(-1px)}
|
|
152
|
+
.session-top{display:flex;align-items:center;gap:12px;flex-wrap:wrap}
|
|
153
|
+
.session-id{font-family:var(--mono);font-size:12px;color:var(--accent);font-weight:600;background:var(--accent-subtle);padding:2px 8px;border-radius:var(--radius-sm)}
|
|
154
|
+
.session-model{font-size:12px;color:var(--muted);background:var(--secondary);padding:2px 8px;border-radius:var(--radius-sm);border:1px solid var(--border)}
|
|
155
|
+
.session-user{font-size:12px;color:var(--muted)}
|
|
156
|
+
.session-time{font-size:11px;color:var(--muted);margin-left:auto;font-family:var(--mono)}
|
|
157
|
+
.session-bottom{display:flex;align-items:center;gap:16px;margin-top:10px}
|
|
158
|
+
.session-stat{font-size:12px;color:var(--muted)}
|
|
159
|
+
.session-stat b{color:var(--text-strong);font-weight:600}
|
|
160
|
+
.mini-trace{display:flex;gap:3px;flex-wrap:wrap;margin-left:auto}
|
|
161
|
+
.mini-dot{width:18px;height:6px;border-radius:3px;opacity:.75}
|
|
162
|
+
.mini-dot-more{font-size:10px;color:var(--muted);margin-left:2px;white-space:nowrap}
|
|
163
|
+
|
|
164
|
+
/* ---- Pagination (matches OpenClaw .btn) ---- */
|
|
165
|
+
.pagination{display:flex;justify-content:center;align-items:center;gap:8px;margin-top:20px;padding:12px 0}
|
|
166
|
+
.pagination button{display:inline-flex;align-items:center;justify-content:center;padding:6px 14px;border:1px solid var(--border);border-radius:var(--radius-md);background:var(--bg-elevated);color:var(--text);cursor:pointer;font-size:13px;font-weight:500;transition:all var(--duration-fast) var(--ease-out)}
|
|
167
|
+
.pagination button:hover:not(:disabled){background:var(--bg-hover);border-color:var(--border-strong)}
|
|
168
|
+
.pagination button:disabled{opacity:.4;cursor:default}
|
|
169
|
+
.pagination .page-info{font-size:13px;color:var(--muted)}
|
|
170
|
+
|
|
171
|
+
/* ==================== Trace view (split pane) ==================== */
|
|
172
|
+
.content--trace{padding:0!important;overflow:hidden!important;flex:1!important;min-height:0!important;display:flex!important;flex-direction:column!important}
|
|
173
|
+
.content--trace .content-inner{max-width:1280px;width:100%;margin:0 auto;flex:1;min-height:0;display:flex;flex-direction:column;overflow:hidden}
|
|
174
|
+
|
|
175
|
+
.trace-top{flex:1;min-height:120px;overflow-y:scroll;scrollbar-gutter:stable}
|
|
176
|
+
.trace-header{background:var(--card);padding:16px 24px;border-bottom:1px solid var(--border);box-shadow:inset 0 1px 0 var(--card-highlight)}
|
|
177
|
+
.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}
|
|
178
|
+
.trace-back:hover{text-decoration:underline}
|
|
179
|
+
.trace-title{font-size:16px;font-weight:700;color:var(--text-strong);margin-bottom:4px;word-break:break-all;letter-spacing:-.02em}
|
|
180
|
+
.trace-meta{display:flex;gap:16px;flex-wrap:wrap}
|
|
181
|
+
.trace-meta-item{font-size:13px;color:var(--muted)}
|
|
182
|
+
.trace-meta-item b{color:var(--text-strong);font-weight:600}
|
|
183
|
+
|
|
184
|
+
/* ---- Waterfall ---- */
|
|
185
|
+
.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)}
|
|
186
|
+
.wf-header{display:grid;grid-template-columns:300px 90px 1fr;padding:10px 16px;background:var(--bg-accent);border-bottom:1px solid var(--border);font-size:12px;color:var(--muted);font-weight:600;text-transform:uppercase;letter-spacing:.04em}
|
|
187
|
+
.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)}
|
|
188
|
+
.wf-row:hover{background:var(--bg-hover)}
|
|
189
|
+
.wf-row.selected{background:var(--accent-subtle)}
|
|
190
|
+
.wf-row:last-child{border-bottom:none}
|
|
191
|
+
.wf-name{font-size:13px;display:flex;align-items:center;gap:6px;overflow:hidden;color:var(--text)}
|
|
192
|
+
.wf-name .indent{flex-shrink:0;color:var(--muted)}
|
|
193
|
+
.wf-name .dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
|
|
194
|
+
.wf-name .text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
195
|
+
.wf-dur{font-size:12px;color:var(--muted);text-align:right;padding-right:14px;font-family:var(--mono)}
|
|
196
|
+
.wf-bar-wrap{position:relative;height:18px;border-radius:3px}
|
|
197
|
+
.wf-bar{position:absolute;height:100%;border-radius:3px;min-width:3px;opacity:.75;transition:opacity var(--duration-fast)}
|
|
198
|
+
.wf-row:hover .wf-bar{opacity:1}
|
|
199
|
+
.wf-row--hidden{display:none}
|
|
200
|
+
.wf-fold{padding:8px 16px;text-align:center;cursor:pointer;background:var(--bg-accent);border-bottom:1px solid var(--border);color:var(--accent);font-size:13px;font-weight:500;transition:background var(--duration-fast)}
|
|
201
|
+
.wf-fold:hover{background:var(--bg-hover)}
|
|
202
|
+
.wf-ticks{display:grid;grid-template-columns:300px 90px 1fr;padding:4px 16px 6px;border-top:1px solid var(--border);background:var(--bg-accent)}
|
|
203
|
+
.wf-ticks-bar{display:flex;justify-content:space-between}
|
|
204
|
+
.wf-tick{font-size:10px;color:var(--muted);font-family:var(--mono)}
|
|
205
|
+
|
|
206
|
+
/* ---- Resize handle ---- */
|
|
207
|
+
.trace-resize{flex:0 0 7px;cursor:row-resize;background:var(--border);position:relative;z-index:10;transition:background var(--duration-fast)}
|
|
208
|
+
.trace-resize:hover,.trace-resize.active{background:var(--accent)}
|
|
209
|
+
.trace-resize::before{content:'';position:absolute;left:0;right:0;top:-4px;bottom:-4px;z-index:1}
|
|
210
|
+
.trace-resize::after{content:'';position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);width:40px;height:3px;border-radius:2px;background:var(--muted);opacity:.5}
|
|
211
|
+
.trace-resize:hover::after,.trace-resize.active::after{background:var(--accent-foreground);opacity:.8}
|
|
212
|
+
body.resizing{cursor:row-resize!important;-webkit-user-select:none!important;user-select:none!important}
|
|
213
|
+
body.resizing *{cursor:row-resize!important}
|
|
214
|
+
|
|
215
|
+
/* ---- Detail panel (bottom pane) ---- */
|
|
216
|
+
.trace-bottom{flex:0 0 320px;min-height:140px;max-height:70vh;overflow-y:scroll;scrollbar-gutter:stable;background:var(--bg-accent);border-top:none}
|
|
217
|
+
.trace-bottom .detail-panel{background:var(--card);overflow:hidden;border:none;border-radius:0;margin:0;width:100%}
|
|
218
|
+
.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}
|
|
219
|
+
.detail-header .dot{width:10px;height:10px;border-radius:50%;flex-shrink:0}
|
|
220
|
+
.detail-header .name{font-size:14px;font-weight:600;color:var(--text-strong);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
221
|
+
.detail-header .dur{margin-left:auto;font-size:13px;color:var(--accent);font-weight:600;font-family:var(--mono);flex-shrink:0}
|
|
222
|
+
.detail-meta{padding:10px 18px;display:flex;gap:16px;flex-wrap:wrap;font-size:12px;color:var(--muted);border-bottom:1px solid var(--border)}
|
|
223
|
+
.detail-meta b{color:var(--text-strong)}
|
|
224
|
+
.detail-body{display:grid;grid-template-columns:1fr 1fr;gap:0;overflow:hidden}
|
|
225
|
+
@media(max-width:800px){.detail-body{grid-template-columns:1fr}}
|
|
226
|
+
.detail-section{padding:12px 18px;border-right:1px solid var(--border);overflow:hidden;min-width:0}
|
|
227
|
+
.detail-section:last-child{border-right:none}
|
|
228
|
+
.detail-section h4{font-size:12px;color:var(--accent);text-transform:uppercase;letter-spacing:.04em;margin-bottom:8px;font-weight:600}
|
|
229
|
+
.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)}
|
|
230
|
+
.json-key{color:#a78bfa}
|
|
231
|
+
.json-str{color:#34d399;white-space:pre-wrap}
|
|
232
|
+
.json-num{color:#fbbf24}
|
|
233
|
+
.json-bool{color:#60a5fa}
|
|
234
|
+
.json-null{color:var(--muted)}
|
|
235
|
+
.json-bracket{color:var(--muted)}
|
|
236
|
+
.json-comma{color:var(--muted)}
|
|
237
|
+
:root[data-theme=light] .json-key{color:#7c3aed}
|
|
238
|
+
:root[data-theme=light] .json-str{color:#059669}
|
|
239
|
+
:root[data-theme=light] .json-num{color:#d97706}
|
|
240
|
+
:root[data-theme=light] .json-bool{color:#2563eb}
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
/* ---- Empty detail placeholder ---- */
|
|
244
|
+
.detail-empty{display:flex;align-items:center;justify-content:center;height:100%;color:var(--muted);font-size:13px;padding:24px}
|
|
245
|
+
|
|
246
|
+
/* ---- Security alert cards ---- */
|
|
247
|
+
.alert-list{display:flex;flex-direction:column;gap:8px}
|
|
248
|
+
.alert-card{background:var(--card);padding:14px 18px;border-radius:var(--radius-md);border:1px solid var(--border);transition:border-color var(--duration-fast) ease,box-shadow var(--duration-fast) ease;box-shadow:inset 0 1px 0 var(--card-highlight);cursor:pointer}
|
|
249
|
+
.alert-card:hover{border-color:var(--border-strong);box-shadow:var(--shadow-md),inset 0 1px 0 var(--card-highlight)}
|
|
250
|
+
.alert-card.sev-critical{border-left:3px solid var(--danger)}
|
|
251
|
+
.alert-card.sev-warn{border-left:3px solid var(--warn)}
|
|
252
|
+
.alert-card.sev-info{border-left:3px solid var(--info)}
|
|
253
|
+
.alert-top{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:6px}
|
|
254
|
+
.alert-sev{font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;padding:2px 8px;border-radius:var(--radius-sm)}
|
|
255
|
+
.alert-sev.critical{background:var(--danger-subtle);color:var(--danger)}
|
|
256
|
+
.alert-sev.warn{background:var(--warn-subtle);color:var(--warn)}
|
|
257
|
+
.alert-sev.info{background:rgba(59,130,246,.12);color:var(--info)}
|
|
258
|
+
.alert-rule{font-size:13px;font-weight:600;color:var(--text-strong)}
|
|
259
|
+
.alert-cat{font-size:11px;color:var(--muted);background:var(--secondary);padding:2px 8px;border-radius:var(--radius-sm);border:1px solid var(--border)}
|
|
260
|
+
.alert-status{font-size:11px;padding:2px 8px;border-radius:var(--radius-sm);margin-left:auto}
|
|
261
|
+
.alert-status.open{background:var(--danger-subtle);color:var(--danger)}
|
|
262
|
+
.alert-status.acknowledged{background:var(--warn-subtle);color:var(--warn)}
|
|
263
|
+
.alert-status.resolved{background:var(--ok-subtle);color:var(--ok)}
|
|
264
|
+
.alert-status.false_positive{background:var(--secondary);color:var(--muted)}
|
|
265
|
+
.alert-finding{font-size:12px;color:var(--text);font-family:var(--mono);margin-bottom:4px}
|
|
266
|
+
.alert-meta{font-size:11px;color:var(--muted);display:flex;gap:12px;flex-wrap:wrap}
|
|
267
|
+
.alert-meta a{color:var(--accent);font-family:var(--mono);font-size:11px}
|
|
268
|
+
|
|
269
|
+
/* ---- Stat value color modifiers ---- */
|
|
270
|
+
.stat-value.val-critical{color:var(--danger)}
|
|
271
|
+
.stat-value.val-warn{color:var(--warn)}
|
|
272
|
+
.stat-value.val-ok{color:var(--ok)}
|
|
273
|
+
|
|
274
|
+
/* ---- Alert detail modal (inline) ---- */
|
|
275
|
+
.alert-detail{background:var(--card);border:1px solid var(--border);border-radius:var(--radius-lg);padding:20px;margin-bottom:16px;box-shadow:var(--shadow-md)}
|
|
276
|
+
.alert-detail h3{font-size:16px;font-weight:700;color:var(--text-strong);margin-bottom:12px;display:flex;align-items:center;gap:8px}
|
|
277
|
+
.alert-detail-meta{display:flex;gap:16px;flex-wrap:wrap;font-size:13px;color:var(--muted);margin-bottom:16px;padding-bottom:12px;border-bottom:1px solid var(--border)}
|
|
278
|
+
.alert-detail-meta b{color:var(--text-strong)}
|
|
279
|
+
.alert-detail .context-block{background:var(--secondary);padding:12px;border-radius:var(--radius-md);font-family:var(--mono);font-size:12px;line-height:1.5;white-space:pre-wrap;word-break:break-all;color:var(--text);border:1px solid var(--border);margin-bottom:12px}
|
|
280
|
+
.alert-detail .actions-row{display:flex;gap:8px;margin-top:12px}
|
|
281
|
+
.alert-detail .actions-row button{padding:6px 14px;border-radius:var(--radius-md);border:1px solid var(--border);background:var(--bg-elevated);color:var(--text);cursor:pointer;font-size:12px;font-weight:500;transition:all var(--duration-fast)}
|
|
282
|
+
.alert-detail .actions-row button:hover{border-color:var(--border-strong);background:var(--bg-hover)}
|
|
283
|
+
.alert-detail .actions-row button.btn-resolve{border-color:var(--ok);color:var(--ok)}
|
|
284
|
+
.alert-detail .actions-row button.btn-dismiss{border-color:var(--muted);color:var(--muted)}
|
|
285
|
+
|
|
286
|
+
/* ---- Nav badge ---- */
|
|
287
|
+
.nav-badge{display:inline-flex;align-items:center;justify-content:center;min-width:18px;height:18px;padding:0 5px;border-radius:var(--radius-full);background:var(--danger);color:#fff;font-size:10px;font-weight:700;margin-left:4px;line-height:1}
|
|
288
|
+
|
|
289
|
+
/* ---- Trace alert banner ---- */
|
|
290
|
+
.trace-alert-banner{margin:8px 20px;padding:10px 16px;border-radius:var(--radius-md);display:flex;align-items:center;gap:10px;font-size:13px;cursor:pointer;transition:all var(--duration-fast)}
|
|
291
|
+
.trace-alert-banner.sev-critical{background:var(--danger-subtle);border:1px solid rgba(239,68,68,.3);color:var(--danger)}
|
|
292
|
+
.trace-alert-banner.sev-warn{background:var(--warn-subtle);border:1px solid rgba(245,158,11,.3);color:var(--warn)}
|
|
293
|
+
.trace-alert-banner:hover{filter:brightness(1.1)}
|
|
294
|
+
.trace-alert-banner .count{font-weight:700}
|
|
295
|
+
|
|
296
|
+
/* ---- Empty / Loading ---- */
|
|
297
|
+
.empty{text-align:center;padding:48px 24px;color:var(--muted)}
|
|
298
|
+
.empty .icon{font-size:36px;margin-bottom:12px}
|
|
299
|
+
.empty .text{font-size:14px}
|
|
300
|
+
.loading{text-align:center;padding:48px;color:var(--muted);font-size:14px}
|
|
301
|
+
|
|
302
|
+
/* ---- Responsive ---- */
|
|
303
|
+
@media(max-width:900px){.stat-grid{grid-template-columns:repeat(3,1fr)}}
|
|
304
|
+
@media(max-width:560px){
|
|
305
|
+
.stat-grid{grid-template-columns:repeat(2,1fr)}
|
|
306
|
+
.topbar{padding:0 12px;height:48px}
|
|
307
|
+
.content{padding:12px}
|
|
308
|
+
.waterfall{margin:10px}
|
|
309
|
+
.wf-header,.wf-row,.wf-ticks{grid-template-columns:200px 70px 1fr}
|
|
310
|
+
}
|
|
311
|
+
`;
|
|
312
|
+
/* ================================================================== */
|
|
313
|
+
/* Client-side JavaScript */
|
|
314
|
+
/* ================================================================== */
|
|
315
|
+
const CLIENT_JS = `
|
|
316
|
+
"use strict";
|
|
317
|
+
|
|
318
|
+
/* ---------- constants ---------- */
|
|
319
|
+
var API = window.location.pathname.replace(/\\/+$/, '') + '/api';
|
|
320
|
+
var TYPE_COLORS = {
|
|
321
|
+
message:'#8b5cf6', tool_call:'#f59e0b', tool_persist:'#f97316',
|
|
322
|
+
prompt_build:'#3b82f6', model_resolve:'#06b6d4', agent_end:'#10b981',
|
|
323
|
+
session_start:'#22c55e', session_end:'#ef4444', compaction:'#14b8a6',
|
|
324
|
+
reset:'#f43f5e', user_message:'#0ea5e9', assistant_msg:'#a855f7',
|
|
325
|
+
msg_sending:'#d946ef', subagent_spawn:'#84cc16', subagent_end:'#64748b',
|
|
326
|
+
gateway_start:'#94a3b8', gateway_stop:'#475569'
|
|
327
|
+
};
|
|
328
|
+
var TYPE_LABELS = {
|
|
329
|
+
message:'LLM Call', tool_call:'Tool Call', tool_persist:'Tool Persist',
|
|
330
|
+
prompt_build:'Prompt Build', model_resolve:'Model Resolve', agent_end:'Agent End',
|
|
331
|
+
session_start:'Session Start', session_end:'Session End', compaction:'Compaction',
|
|
332
|
+
reset:'Reset', user_message:'User Message', assistant_msg:'Assistant Msg',
|
|
333
|
+
msg_sending:'Msg Sending', subagent_spawn:'Subagent Spawn', subagent_end:'Subagent End',
|
|
334
|
+
gateway_start:'Gateway Start', gateway_stop:'Gateway Stop'
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
var app = document.getElementById('app');
|
|
338
|
+
var currentPage = 1;
|
|
339
|
+
var filterSearch = '';
|
|
340
|
+
var filterTimeRange = ''; // '' = All time, or preset key
|
|
341
|
+
|
|
342
|
+
var TIME_PRESETS = [
|
|
343
|
+
{ key:'30m', label:'30 min', ms: 30*60*1000 },
|
|
344
|
+
{ key:'1h', label:'1 hour', ms: 60*60*1000 },
|
|
345
|
+
{ key:'6h', label:'6 hours', ms: 6*60*60*1000 },
|
|
346
|
+
{ key:'24h', label:'24 hours', ms: 24*60*60*1000 },
|
|
347
|
+
{ key:'3d', label:'3 days', ms: 3*24*60*60*1000 },
|
|
348
|
+
{ key:'7d', label:'7 days', ms: 7*24*60*60*1000 },
|
|
349
|
+
{ key:'14d', label:'14 days', ms: 14*24*60*60*1000 },
|
|
350
|
+
{ key:'1mo', label:'1 month', ms: 30*24*60*60*1000 },
|
|
351
|
+
{ key:'3mo', label:'3 months', ms: 90*24*60*60*1000 },
|
|
352
|
+
{ key:'', label:'All time', ms: 0 }
|
|
353
|
+
];
|
|
354
|
+
|
|
355
|
+
function getTimeFromISO() {
|
|
356
|
+
if (!filterTimeRange) return '';
|
|
357
|
+
var preset = TIME_PRESETS.find(function(p){ return p.key === filterTimeRange; });
|
|
358
|
+
if (!preset || !preset.ms) return '';
|
|
359
|
+
return new Date(Date.now() - preset.ms).toISOString();
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function getTimeLabel() {
|
|
363
|
+
if (!filterTimeRange) return 'All time';
|
|
364
|
+
var preset = TIME_PRESETS.find(function(p){ return p.key === filterTimeRange; });
|
|
365
|
+
return preset ? preset.label : 'All time';
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/* ---------- theme ---------- */
|
|
369
|
+
function getTheme() {
|
|
370
|
+
var saved = localStorage.getItem('oc-audit-theme');
|
|
371
|
+
if (saved) return saved;
|
|
372
|
+
return window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
|
|
373
|
+
}
|
|
374
|
+
function applyTheme(t) {
|
|
375
|
+
if (t === 'light') {
|
|
376
|
+
document.documentElement.setAttribute('data-theme', 'light');
|
|
377
|
+
} else {
|
|
378
|
+
document.documentElement.removeAttribute('data-theme');
|
|
379
|
+
}
|
|
380
|
+
localStorage.setItem('oc-audit-theme', t);
|
|
381
|
+
}
|
|
382
|
+
function toggleTheme() {
|
|
383
|
+
var cur = getTheme();
|
|
384
|
+
applyTheme(cur === 'dark' ? 'light' : 'dark');
|
|
385
|
+
}
|
|
386
|
+
applyTheme(getTheme());
|
|
387
|
+
|
|
388
|
+
/* ---------- helpers ---------- */
|
|
389
|
+
function esc(s) { if (!s) return ''; var d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
|
|
390
|
+
function fmtDur(ms) {
|
|
391
|
+
if (ms == null) return '-';
|
|
392
|
+
if (ms < 1000) return ms + 'ms';
|
|
393
|
+
if (ms < 60000) return (ms/1000).toFixed(1) + 's';
|
|
394
|
+
return (ms/60000).toFixed(1) + 'm';
|
|
395
|
+
}
|
|
396
|
+
function fmtTime(t) {
|
|
397
|
+
if (!t) return '-';
|
|
398
|
+
var d = new Date(t);
|
|
399
|
+
return d.toLocaleString(undefined, {month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit',second:'2-digit'});
|
|
400
|
+
}
|
|
401
|
+
function fmtNum(n) {
|
|
402
|
+
if (n == null || isNaN(n)) return '0';
|
|
403
|
+
if (n >= 1000000) return (n/1000000).toFixed(1) + 'M';
|
|
404
|
+
if (n >= 1000) return (n/1000).toFixed(1) + 'K';
|
|
405
|
+
return String(n);
|
|
406
|
+
}
|
|
407
|
+
function typeColor(t) { return TYPE_COLORS[t] || '#94a3b8'; }
|
|
408
|
+
function typeLabel(t) { return TYPE_LABELS[t] || t; }
|
|
409
|
+
|
|
410
|
+
function parseJson(s) {
|
|
411
|
+
if (!s) return null;
|
|
412
|
+
try { return typeof s === 'string' ? JSON.parse(s) : s; } catch(e) { return s; }
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/* ---------- Pretty JSON renderer (recursive) ---------- */
|
|
416
|
+
function prettyJson(val, indent) {
|
|
417
|
+
if (indent === undefined) indent = 0;
|
|
418
|
+
var pad = '';
|
|
419
|
+
for (var pi = 0; pi < indent; pi++) pad += ' ';
|
|
420
|
+
var pad1 = pad + ' ';
|
|
421
|
+
|
|
422
|
+
if (val === null || val === undefined) return '<span class="json-null">null</span>';
|
|
423
|
+
if (typeof val === 'boolean') return '<span class="json-bool">' + val + '</span>';
|
|
424
|
+
if (typeof val === 'number') return '<span class="json-num">' + val + '</span>';
|
|
425
|
+
|
|
426
|
+
if (typeof val === 'string') {
|
|
427
|
+
// Try auto-parse nested JSON string
|
|
428
|
+
if (val.length > 2 && (val.charAt(0) === '{' || val.charAt(0) === '[')) {
|
|
429
|
+
try {
|
|
430
|
+
var nested = JSON.parse(val);
|
|
431
|
+
if (typeof nested === 'object' && nested !== null) {
|
|
432
|
+
return prettyJson(nested, indent);
|
|
433
|
+
}
|
|
434
|
+
} catch(e) {}
|
|
435
|
+
}
|
|
436
|
+
// Render string: show actual content with real line breaks
|
|
437
|
+
return '<span class="json-str">"' + esc(val) + '"</span>';
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (Array.isArray(val)) {
|
|
441
|
+
if (val.length === 0) return '<span class="json-bracket">[]</span>';
|
|
442
|
+
var ah = '<span class="json-bracket">[</span>\\n';
|
|
443
|
+
for (var ai = 0; ai < val.length; ai++) {
|
|
444
|
+
ah += pad1 + prettyJson(val[ai], indent + 1);
|
|
445
|
+
if (ai < val.length - 1) ah += '<span class="json-comma">,</span>';
|
|
446
|
+
ah += '\\n';
|
|
447
|
+
}
|
|
448
|
+
ah += pad + '<span class="json-bracket">]</span>';
|
|
449
|
+
return ah;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Object
|
|
453
|
+
var keys = Object.keys(val);
|
|
454
|
+
if (keys.length === 0) return '<span class="json-bracket">{}</span>';
|
|
455
|
+
var oh = '<span class="json-bracket">{</span>\\n';
|
|
456
|
+
for (var ki = 0; ki < keys.length; ki++) {
|
|
457
|
+
var k = keys[ki];
|
|
458
|
+
oh += pad1 + '<span class="json-key">"' + esc(k) + '"</span>: ' + prettyJson(val[k], indent + 1);
|
|
459
|
+
if (ki < keys.length - 1) oh += '<span class="json-comma">,</span>';
|
|
460
|
+
oh += '\\n';
|
|
461
|
+
}
|
|
462
|
+
oh += pad + '<span class="json-bracket">}</span>';
|
|
463
|
+
return oh;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
async function fetchApi(path) {
|
|
467
|
+
var resp = await fetch(API + path);
|
|
468
|
+
if (!resp.ok) throw new Error('API error: ' + resp.status);
|
|
469
|
+
return resp.json();
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/* ---------- SVG icons ---------- */
|
|
473
|
+
var ICON_BACK = '<svg viewBox="0 0 24 24"><path d="M19 12H5M12 19l-7-7 7-7"/></svg>';
|
|
474
|
+
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>';
|
|
475
|
+
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>';
|
|
476
|
+
var ICON_ACTIVITY = '<svg viewBox="0 0 24 24"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>';
|
|
477
|
+
|
|
478
|
+
/* ---------- router ---------- */
|
|
479
|
+
function parseHashParams(hash) {
|
|
480
|
+
var qIdx = hash.indexOf('?');
|
|
481
|
+
if (qIdx < 0) return {};
|
|
482
|
+
var params = {};
|
|
483
|
+
hash.substring(qIdx + 1).split('&').forEach(function(p) {
|
|
484
|
+
var kv = p.split('=');
|
|
485
|
+
if (kv.length === 2) params[decodeURIComponent(kv[0])] = decodeURIComponent(kv[1]);
|
|
486
|
+
});
|
|
487
|
+
return params;
|
|
488
|
+
}
|
|
489
|
+
function router() {
|
|
490
|
+
var hash = location.hash || '#/';
|
|
491
|
+
if (hash.indexOf('#/trace/') === 0) {
|
|
492
|
+
var raw = hash.substring(8);
|
|
493
|
+
var qIdx = raw.indexOf('?');
|
|
494
|
+
var sid = decodeURIComponent(qIdx >= 0 ? raw.substring(0, qIdx) : raw);
|
|
495
|
+
var params = parseHashParams(hash);
|
|
496
|
+
renderTrace(sid, params.action, params.t);
|
|
497
|
+
} else if (hash.indexOf('#/security') === 0) {
|
|
498
|
+
renderSecurity();
|
|
499
|
+
} else {
|
|
500
|
+
renderDashboard();
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
window.addEventListener('hashchange', router);
|
|
504
|
+
|
|
505
|
+
/* ---------- layout ---------- */
|
|
506
|
+
function renderLayout(active, content) {
|
|
507
|
+
var themeIcon = getTheme() === 'dark' ? ICON_SUN : ICON_MOON;
|
|
508
|
+
var isTrace = active === 'trace';
|
|
509
|
+
var shellCls = isTrace ? 'shell shell--trace' : 'shell';
|
|
510
|
+
var contentCls = isTrace ? 'content content--trace' : 'content';
|
|
511
|
+
return '<div class="' + shellCls + '">' +
|
|
512
|
+
'<div class="topbar">' +
|
|
513
|
+
'<div class="topbar-left">' +
|
|
514
|
+
'<div class="brand">' +
|
|
515
|
+
'<div class="brand-logo">' + ICON_ACTIVITY + '</div>' +
|
|
516
|
+
'<div class="brand-text">' +
|
|
517
|
+
'<div class="brand-title">OpenClaw</div>' +
|
|
518
|
+
'<div class="brand-sub">Audit Traces</div>' +
|
|
519
|
+
'</div>' +
|
|
520
|
+
'</div>' +
|
|
521
|
+
'<div class="topbar-nav">' +
|
|
522
|
+
'<a href="#/" class="' + (active==='dashboard'?'active':'') + '">Dashboard</a>' +
|
|
523
|
+
'<a href="#/security" class="' + (active==='security'?'active':'') + '">Security' + (window.__alertCount > 0 ? '<span class="nav-badge">' + window.__alertCount + '</span>' : '') + '</a>' +
|
|
524
|
+
'</div>' +
|
|
525
|
+
'</div>' +
|
|
526
|
+
'<div class="topbar-right">' +
|
|
527
|
+
'<button class="theme-btn" onclick="toggleTheme();router()" title="Toggle theme">' + themeIcon + '</button>' +
|
|
528
|
+
'<a href="/" class="back-link">' + ICON_BACK + ' Control Panel</a>' +
|
|
529
|
+
'</div>' +
|
|
530
|
+
'</div>' +
|
|
531
|
+
'<div class="' + contentCls + '"><div class="content-inner">' + content + '</div></div>' +
|
|
532
|
+
'</div>';
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/* ================================================================ */
|
|
536
|
+
/* Dashboard */
|
|
537
|
+
/* ================================================================ */
|
|
538
|
+
|
|
539
|
+
async function renderDashboard() {
|
|
540
|
+
app.innerHTML = renderLayout('dashboard', '<div class="loading">Loading...</div>');
|
|
541
|
+
|
|
542
|
+
try {
|
|
543
|
+
// Build sessions request with filter params
|
|
544
|
+
var sessQs = 'page=' + currentPage + '&limit=20';
|
|
545
|
+
if (filterSearch) sessQs += '&search=' + encodeURIComponent(filterSearch);
|
|
546
|
+
var timeFromISO = getTimeFromISO();
|
|
547
|
+
if (timeFromISO) sessQs += '&timeFrom=' + encodeURIComponent(timeFromISO);
|
|
548
|
+
|
|
549
|
+
var data = await Promise.all([
|
|
550
|
+
fetchApi('/stats'),
|
|
551
|
+
fetchApi('/sessions?' + sessQs)
|
|
552
|
+
]);
|
|
553
|
+
var stats = data[0];
|
|
554
|
+
var sessData = data[1];
|
|
555
|
+
|
|
556
|
+
var html = '';
|
|
557
|
+
|
|
558
|
+
// Stats cards
|
|
559
|
+
html += '<div class="stat-grid">';
|
|
560
|
+
html += statCard('Sessions', fmtNum(stats.totalSessions));
|
|
561
|
+
html += statCard('Actions', fmtNum(stats.totalActions));
|
|
562
|
+
html += statCard('Tokens', fmtNum(stats.totalTokens));
|
|
563
|
+
html += statCard('Avg Latency', fmtDur(stats.avgLatencyMs));
|
|
564
|
+
html += statCard('Success', stats.successRate + '%');
|
|
565
|
+
html += '</div>';
|
|
566
|
+
|
|
567
|
+
// Filter bar
|
|
568
|
+
var hasFilter = filterSearch || filterTimeRange;
|
|
569
|
+
html += '<div class="filter-bar">';
|
|
570
|
+
html += '<input type="text" id="f-search" placeholder="Search session ID, key, user, model..." value="' + esc(filterSearch) + '" onkeydown="if(event.key===\\'Enter\\')applyFilter()">';
|
|
571
|
+
html += '<span class="filter-sep"></span>';
|
|
572
|
+
// Time range dropdown
|
|
573
|
+
html += '<div class="time-dropdown" id="time-dropdown">';
|
|
574
|
+
html += '<button class="time-btn" onclick="toggleTimeMenu(event)">';
|
|
575
|
+
html += '<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>';
|
|
576
|
+
html += esc(getTimeLabel());
|
|
577
|
+
html += ' <svg viewBox="0 0 24 24" style="width:12px;height:12px"><polyline points="6 9 12 15 18 9"/></svg>';
|
|
578
|
+
html += '</button>';
|
|
579
|
+
html += '<div class="time-menu" id="time-menu">';
|
|
580
|
+
TIME_PRESETS.forEach(function(p) {
|
|
581
|
+
var cls = (p.key === filterTimeRange) ? ' active' : '';
|
|
582
|
+
html += '<div class="time-menu-item' + cls + '" onclick="selectTimeRange(\\'' + p.key + '\\')">';
|
|
583
|
+
html += '<span class="check">' + (p.key === filterTimeRange ? '✓' : '') + '</span>';
|
|
584
|
+
html += esc(p.label);
|
|
585
|
+
html += '</div>';
|
|
586
|
+
});
|
|
587
|
+
html += '</div></div>';
|
|
588
|
+
if (hasFilter) {
|
|
589
|
+
html += '<button class="btn-clear" onclick="clearFilter()">✕ Clear</button>';
|
|
590
|
+
}
|
|
591
|
+
html += '</div>';
|
|
592
|
+
|
|
593
|
+
// Session list
|
|
594
|
+
html += '<div class="section-title">Traces <span class="count">' + sessData.total + (hasFilter ? ' matched' : ' total') + '</span></div>';
|
|
595
|
+
html += '<div class="session-list">';
|
|
596
|
+
|
|
597
|
+
if (sessData.sessions.length === 0) {
|
|
598
|
+
html += '<div class="empty"><div class="icon">📭</div><div class="text">No sessions recorded yet</div></div>';
|
|
599
|
+
} else {
|
|
600
|
+
sessData.sessions.forEach(function(s) {
|
|
601
|
+
var dur = (s.start_time && s.end_time)
|
|
602
|
+
? fmtDur(new Date(s.end_time).getTime() - new Date(s.start_time).getTime())
|
|
603
|
+
: '-';
|
|
604
|
+
|
|
605
|
+
html += '<div class="session-card" onclick="location.hash=\\'#/trace/' + encodeURIComponent(s.session_id) + '\\'">';
|
|
606
|
+
html += '<div class="session-top">';
|
|
607
|
+
html += '<span class="session-id">' + esc(s.session_id) + '</span>';
|
|
608
|
+
html += '<span class="session-model">' + esc(s.model_name || '-') + '</span>';
|
|
609
|
+
html += '<span class="session-user">🤖 ' + esc(s.user_id || '-') + '</span>';
|
|
610
|
+
html += '<span class="session-time">' + fmtTime(s.start_time) + '</span>';
|
|
611
|
+
html += '</div>';
|
|
612
|
+
html += '<div class="session-bottom">';
|
|
613
|
+
html += '<span class="session-stat"><b>' + s.total_actions + '</b> actions</span>';
|
|
614
|
+
html += '<span class="session-stat"><b>' + fmtNum(s.total_tokens) + '</b> tokens</span>';
|
|
615
|
+
html += '<span class="session-stat"><b>' + dur + '</b></span>';
|
|
616
|
+
html += '</div>';
|
|
617
|
+
html += '</div>';
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
html += '</div>';
|
|
622
|
+
|
|
623
|
+
// Pagination
|
|
624
|
+
var totalPages = Math.ceil(sessData.total / 20) || 1;
|
|
625
|
+
html += '<div class="pagination">';
|
|
626
|
+
html += '<button onclick="goPage(' + (currentPage-1) + ')" ' + (currentPage<=1?'disabled':'') + '>« Prev</button>';
|
|
627
|
+
html += '<span class="page-info">Page ' + currentPage + ' / ' + totalPages + '</span>';
|
|
628
|
+
html += '<button onclick="goPage(' + (currentPage+1) + ')" ' + (currentPage>=totalPages?'disabled':'') + '>Next »</button>';
|
|
629
|
+
html += '</div>';
|
|
630
|
+
|
|
631
|
+
app.innerHTML = renderLayout('dashboard', html);
|
|
632
|
+
|
|
633
|
+
} catch(err) {
|
|
634
|
+
app.innerHTML = renderLayout('dashboard',
|
|
635
|
+
'<div class="empty"><div class="icon">⚠️</div><div class="text">Failed to load: ' + esc(String(err)) + '</div></div>');
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function statCard(label, value) {
|
|
640
|
+
return '<div class="stat"><div class="stat-label">' + label + '</div><div class="stat-value">' + value + '</div></div>';
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
window.goPage = function(p) {
|
|
644
|
+
if (p < 1) return;
|
|
645
|
+
currentPage = p;
|
|
646
|
+
renderDashboard();
|
|
647
|
+
};
|
|
648
|
+
|
|
649
|
+
window.applyFilter = function() {
|
|
650
|
+
var s = document.getElementById('f-search');
|
|
651
|
+
filterSearch = s ? s.value.trim() : '';
|
|
652
|
+
currentPage = 1;
|
|
653
|
+
renderDashboard();
|
|
654
|
+
};
|
|
655
|
+
|
|
656
|
+
window.clearFilter = function() {
|
|
657
|
+
filterSearch = '';
|
|
658
|
+
filterTimeRange = '';
|
|
659
|
+
currentPage = 1;
|
|
660
|
+
renderDashboard();
|
|
661
|
+
};
|
|
662
|
+
|
|
663
|
+
window.toggleTimeMenu = function(e) {
|
|
664
|
+
e.stopPropagation();
|
|
665
|
+
var menu = document.getElementById('time-menu');
|
|
666
|
+
if (menu) menu.classList.toggle('open');
|
|
667
|
+
};
|
|
668
|
+
|
|
669
|
+
window.selectTimeRange = function(key) {
|
|
670
|
+
filterTimeRange = key;
|
|
671
|
+
currentPage = 1;
|
|
672
|
+
var menu = document.getElementById('time-menu');
|
|
673
|
+
if (menu) menu.classList.remove('open');
|
|
674
|
+
renderDashboard();
|
|
675
|
+
};
|
|
676
|
+
|
|
677
|
+
// Click anywhere on page to close dropdowns
|
|
678
|
+
document.addEventListener('click', function() {
|
|
679
|
+
var menu = document.getElementById('time-menu');
|
|
680
|
+
if (menu) menu.classList.remove('open');
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
/* ================================================================ */
|
|
684
|
+
/* Security tab — alert badge count */
|
|
685
|
+
/* ================================================================ */
|
|
686
|
+
|
|
687
|
+
window.__alertCount = 0;
|
|
688
|
+
|
|
689
|
+
// Fetch open alert count on init for navigation badge
|
|
690
|
+
(async function loadAlertBadge() {
|
|
691
|
+
try {
|
|
692
|
+
var st = await fetchApi('/alerts/stats');
|
|
693
|
+
window.__alertCount = (st.byStatus && st.byStatus.open) || 0;
|
|
694
|
+
} catch(e) { /* ignore */ }
|
|
695
|
+
})();
|
|
696
|
+
|
|
697
|
+
/* ---- Security filter state ---- */
|
|
698
|
+
var secFilterSearch = '';
|
|
699
|
+
var secFilterSeverity = '';
|
|
700
|
+
var secFilterCategory = '';
|
|
701
|
+
var secFilterStatus = 'open';
|
|
702
|
+
var secFilterTimeRange = '';
|
|
703
|
+
var secPage = 1;
|
|
704
|
+
|
|
705
|
+
/* ---- severity helpers ---- */
|
|
706
|
+
var SEV_ICON = {critical:'🔴',warn:'🟡',info:'ℹ️'};
|
|
707
|
+
var SEV_LABEL = {critical:'CRITICAL',warn:'WARNING',info:'INFO'};
|
|
708
|
+
var CAT_LABEL = {
|
|
709
|
+
secret_leakage:'Secret Leakage',high_risk_operation:'High Risk Operation',
|
|
710
|
+
data_exfiltration:'Data Exfiltration',prompt_injection:'Prompt Injection',
|
|
711
|
+
skill_anomaly:'Skill Anomaly'
|
|
712
|
+
};
|
|
713
|
+
var STATUS_LABEL = {open:'Open',acknowledged:'Acknowledged',resolved:'Resolved',false_positive:'False Positive'};
|
|
714
|
+
|
|
715
|
+
var SEV_OPTIONS = [{k:'',l:'All Severity'},{k:'critical',l:'CRITICAL'},{k:'warn',l:'WARNING'},{k:'info',l:'INFO'}];
|
|
716
|
+
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'}];
|
|
717
|
+
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'}];
|
|
718
|
+
|
|
719
|
+
function secGetTimeFromISO() {
|
|
720
|
+
if (!secFilterTimeRange) return '';
|
|
721
|
+
var preset = TIME_PRESETS.find(function(p){ return p.key === secFilterTimeRange; });
|
|
722
|
+
if (!preset || !preset.ms) return '';
|
|
723
|
+
return new Date(Date.now() - preset.ms).toISOString();
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function secGetTimeLabel() {
|
|
727
|
+
if (!secFilterTimeRange) return 'All time';
|
|
728
|
+
var preset = TIME_PRESETS.find(function(p){ return p.key === secFilterTimeRange; });
|
|
729
|
+
return preset ? preset.label : 'All time';
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function secSelectedLabel(opts, curKey) {
|
|
733
|
+
var found = opts.find(function(o){ return o.k === curKey; });
|
|
734
|
+
return found ? found.l : opts[0].l;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/* build one custom dropdown (same look as Dashboard time picker) */
|
|
738
|
+
function secDropdown(id, icon, opts, curKey) {
|
|
739
|
+
var label = secSelectedLabel(opts, curKey);
|
|
740
|
+
var h = '';
|
|
741
|
+
h += '<div class="time-dropdown" id="sec-dd-' + id + '">';
|
|
742
|
+
h += '<button class="time-btn" onclick="secToggleMenu(event,\\'' + id + '\\')">';
|
|
743
|
+
h += icon;
|
|
744
|
+
h += esc(label);
|
|
745
|
+
h += ' <svg viewBox="0 0 24 24" style="width:12px;height:12px"><polyline points="6 9 12 15 18 9"/></svg>';
|
|
746
|
+
h += '</button>';
|
|
747
|
+
h += '<div class="time-menu" id="sec-menu-' + id + '">';
|
|
748
|
+
opts.forEach(function(o){
|
|
749
|
+
var cls = (o.k === curKey) ? ' active' : '';
|
|
750
|
+
h += '<div class="time-menu-item' + cls + '" onclick="secSelect(\\'' + id + '\\',\\'' + o.k + '\\')">';
|
|
751
|
+
h += '<span class="check">' + (o.k === curKey ? '✓' : '') + '</span>';
|
|
752
|
+
h += esc(o.l);
|
|
753
|
+
h += '</div>';
|
|
754
|
+
});
|
|
755
|
+
h += '</div></div>';
|
|
756
|
+
return h;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
async function renderSecurity() {
|
|
760
|
+
app.innerHTML = renderLayout('security', '<div class="loading">Loading security alerts...</div>');
|
|
761
|
+
|
|
762
|
+
try {
|
|
763
|
+
var qs = 'page=' + secPage + '&limit=20';
|
|
764
|
+
if (secFilterSearch) qs += '&search=' + encodeURIComponent(secFilterSearch);
|
|
765
|
+
if (secFilterSeverity) qs += '&severity=' + secFilterSeverity;
|
|
766
|
+
if (secFilterCategory) qs += '&category=' + secFilterCategory;
|
|
767
|
+
if (secFilterStatus) qs += '&status=' + secFilterStatus;
|
|
768
|
+
var secTimeFromISO = secGetTimeFromISO();
|
|
769
|
+
if (secTimeFromISO) qs += '&timeFrom=' + encodeURIComponent(secTimeFromISO);
|
|
770
|
+
|
|
771
|
+
var res = await Promise.all([
|
|
772
|
+
fetchApi('/alerts/stats'),
|
|
773
|
+
fetchApi('/alerts?' + qs)
|
|
774
|
+
]);
|
|
775
|
+
var alertStats = res[0];
|
|
776
|
+
var alertData = res[1];
|
|
777
|
+
|
|
778
|
+
// Update badge count
|
|
779
|
+
window.__alertCount = (alertStats.byStatus && alertStats.byStatus.open) || 0;
|
|
780
|
+
|
|
781
|
+
var html = '';
|
|
782
|
+
|
|
783
|
+
// --- Stat cards (reuse Dashboard .stat-grid / .stat) ---
|
|
784
|
+
var critCount = (alertStats.bySeverity && alertStats.bySeverity.critical) || 0;
|
|
785
|
+
var warnCount = (alertStats.bySeverity && alertStats.bySeverity.warn) || 0;
|
|
786
|
+
var openCount = (alertStats.byStatus && alertStats.byStatus.open) || 0;
|
|
787
|
+
var totalAlerts = alertStats.total || 0;
|
|
788
|
+
|
|
789
|
+
html += '<div class="stat-grid">';
|
|
790
|
+
html += statCard('Total Alerts', String(totalAlerts));
|
|
791
|
+
html += '<div class="stat"><div class="stat-label">Critical</div><div class="stat-value ' + (critCount>0?'val-critical':'') + '">' + critCount + '</div></div>';
|
|
792
|
+
html += '<div class="stat"><div class="stat-label">Warnings</div><div class="stat-value ' + (warnCount>0?'val-warn':'') + '">' + warnCount + '</div></div>';
|
|
793
|
+
html += '<div class="stat"><div class="stat-label">Open</div><div class="stat-value ' + (openCount>0?'val-critical':'val-ok') + '">' + openCount + '</div></div>';
|
|
794
|
+
html += statCard('Last 24h', String(alertStats.recent24h || 0));
|
|
795
|
+
html += '</div>';
|
|
796
|
+
|
|
797
|
+
// --- Filter bar (same .filter-bar as Dashboard) ---
|
|
798
|
+
var hasFilter = secFilterSearch || secFilterSeverity || secFilterCategory || secFilterStatus || secFilterTimeRange;
|
|
799
|
+
html += '<div class="filter-bar">';
|
|
800
|
+
html += '<input type="text" id="sec-f-search" placeholder="Search rule, finding, session..." value="' + esc(secFilterSearch) + '" onkeydown="if(event.key===\\'Enter\\')secApplyFilter()">';
|
|
801
|
+
html += '<span class="filter-sep"></span>';
|
|
802
|
+
|
|
803
|
+
// Severity dropdown
|
|
804
|
+
var sevIcon = '<svg viewBox="0 0 24 24" style="width:14px;height:14px;stroke:currentColor;fill:none;stroke-width:1.5"><path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/></svg> ';
|
|
805
|
+
html += secDropdown('severity', sevIcon, SEV_OPTIONS, secFilterSeverity);
|
|
806
|
+
|
|
807
|
+
// Category dropdown
|
|
808
|
+
var catIcon = '<svg viewBox="0 0 24 24" style="width:14px;height:14px;stroke:currentColor;fill:none;stroke-width:1.5"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg> ';
|
|
809
|
+
html += secDropdown('category', catIcon, CAT_OPTIONS, secFilterCategory);
|
|
810
|
+
|
|
811
|
+
// Status dropdown
|
|
812
|
+
var staIcon = '<svg viewBox="0 0 24 24" style="width:14px;height:14px;stroke:currentColor;fill:none;stroke-width:1.5"><path d="M22 11.08V12a10 10 0 11-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg> ';
|
|
813
|
+
html += secDropdown('status', staIcon, STA_OPTIONS, secFilterStatus);
|
|
814
|
+
|
|
815
|
+
// Time range dropdown (reuse TIME_PRESETS)
|
|
816
|
+
html += '<div class="time-dropdown" id="sec-dd-time">';
|
|
817
|
+
html += '<button class="time-btn" onclick="secToggleMenu(event,\\'time\\')">';
|
|
818
|
+
html += '<svg viewBox="0 0 24 24" style="width:14px;height:14px;stroke:currentColor;fill:none;stroke-width:1.5"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg> ';
|
|
819
|
+
html += esc(secGetTimeLabel());
|
|
820
|
+
html += ' <svg viewBox="0 0 24 24" style="width:12px;height:12px"><polyline points="6 9 12 15 18 9"/></svg>';
|
|
821
|
+
html += '</button>';
|
|
822
|
+
html += '<div class="time-menu" id="sec-menu-time">';
|
|
823
|
+
TIME_PRESETS.forEach(function(p) {
|
|
824
|
+
var cls = (p.key === secFilterTimeRange) ? ' active' : '';
|
|
825
|
+
html += '<div class="time-menu-item' + cls + '" onclick="secSelect(\\'time\\',\\'' + p.key + '\\')">';
|
|
826
|
+
html += '<span class="check">' + (p.key === secFilterTimeRange ? '✓' : '') + '</span>';
|
|
827
|
+
html += esc(p.label);
|
|
828
|
+
html += '</div>';
|
|
829
|
+
});
|
|
830
|
+
html += '</div></div>';
|
|
831
|
+
|
|
832
|
+
if (hasFilter) {
|
|
833
|
+
html += '<button class="btn-clear" onclick="secClearFilter()">✕ Clear</button>';
|
|
834
|
+
}
|
|
835
|
+
html += '</div>';
|
|
836
|
+
|
|
837
|
+
// --- Alert list ---
|
|
838
|
+
html += '<div class="section-title">Alerts <span class="count">' + alertData.total + (hasFilter ? ' matched' : ' total') + '</span></div>';
|
|
839
|
+
html += '<div class="alert-list">';
|
|
840
|
+
|
|
841
|
+
if (alertData.alerts.length === 0) {
|
|
842
|
+
html += '<div class="empty"><div class="icon">✅</div><div class="text">No alerts found</div></div>';
|
|
843
|
+
} else {
|
|
844
|
+
alertData.alerts.forEach(function(a) {
|
|
845
|
+
html += '<div class="alert-card sev-' + a.severity + '" onclick="toggleAlertDetail(\\'' + a.alert_id + '\\')">';
|
|
846
|
+
html += '<div class="alert-top">';
|
|
847
|
+
html += '<span class="alert-sev ' + a.severity + '">' + (SEV_ICON[a.severity]||'') + ' ' + (SEV_LABEL[a.severity]||a.severity) + '</span>';
|
|
848
|
+
html += '<span class="alert-rule">' + esc(a.rule_name) + '</span>';
|
|
849
|
+
html += '<span class="alert-cat">' + esc(CAT_LABEL[a.category]||a.category) + '</span>';
|
|
850
|
+
html += '<span class="alert-status ' + a.status + '">' + (STATUS_LABEL[a.status]||a.status) + '</span>';
|
|
851
|
+
html += '</div>';
|
|
852
|
+
html += '<div class="alert-finding">' + esc(a.finding) + '</div>';
|
|
853
|
+
html += '<div class="alert-meta">';
|
|
854
|
+
html += '<span>Rule: ' + esc(a.rule_id) + '</span>';
|
|
855
|
+
var traceLink = '#/trace/' + encodeURIComponent(a.session_id) + '?action=' + encodeURIComponent(a.action_name) + '&t=' + encodeURIComponent(a.created_at || '');
|
|
856
|
+
html += '<span>Session: <a href="' + traceLink + '">' + esc(a.session_id.substring(0,12)) + '…</a></span>';
|
|
857
|
+
html += '<span>Action: ' + esc(a.action_name) + '</span>';
|
|
858
|
+
html += '<span>' + fmtTime(a.created_at) + '</span>';
|
|
859
|
+
html += '</div>';
|
|
860
|
+
|
|
861
|
+
// Expandable detail area
|
|
862
|
+
html += '<div class="alert-detail" id="alert-detail-' + a.alert_id + '" style="display:none;margin-top:12px">';
|
|
863
|
+
html += '<div class="alert-detail-meta">';
|
|
864
|
+
html += '<span><b>Alert ID:</b> ' + esc(a.alert_id) + '</span>';
|
|
865
|
+
html += '<span><b>Agent:</b> ' + esc(a.user_id || '-') + '</span>';
|
|
866
|
+
html += '<span><b>Model:</b> ' + esc(a.model_name || '-') + '</span>';
|
|
867
|
+
html += '</div>';
|
|
868
|
+
html += '<div class="context-block">' + esc(a.context) + '</div>';
|
|
869
|
+
html += '<div class="actions-row">';
|
|
870
|
+
html += '<a href="' + traceLink + '" onclick="event.stopPropagation()" style="display:inline-flex;align-items:center;gap:4px;padding:6px 14px;border-radius:var(--radius-md);border:1px solid var(--accent);background:var(--accent-subtle);color:var(--accent);font-size:12px;font-weight:600;text-decoration:none">🔍 View in Trace</a>';
|
|
871
|
+
if (a.status === 'open') {
|
|
872
|
+
html += '<button onclick="event.stopPropagation();updateAlertSt(\\'' + a.alert_id + '\\',\\'acknowledged\\')">👁 Acknowledge</button>';
|
|
873
|
+
}
|
|
874
|
+
if (a.status === 'open' || a.status === 'acknowledged') {
|
|
875
|
+
html += '<button class="btn-resolve" onclick="event.stopPropagation();updateAlertSt(\\'' + a.alert_id + '\\',\\'resolved\\')">✓ Resolve</button>';
|
|
876
|
+
html += '<button class="btn-dismiss" onclick="event.stopPropagation();updateAlertSt(\\'' + a.alert_id + '\\',\\'false_positive\\')">✕ False Positive</button>';
|
|
877
|
+
}
|
|
878
|
+
html += '</div>';
|
|
879
|
+
html += '</div>';
|
|
880
|
+
|
|
881
|
+
html += '</div>'; // .alert-card
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
html += '</div>';
|
|
885
|
+
|
|
886
|
+
// Pagination
|
|
887
|
+
var totalPages = Math.ceil(alertData.total / 20) || 1;
|
|
888
|
+
html += '<div class="pagination">';
|
|
889
|
+
html += '<button onclick="secGoPage(' + (secPage-1) + ')" ' + (secPage<=1?'disabled':'') + '>« Prev</button>';
|
|
890
|
+
html += '<span class="page-info">Page ' + secPage + ' / ' + totalPages + '</span>';
|
|
891
|
+
html += '<button onclick="secGoPage(' + (secPage+1) + ')" ' + (secPage>=totalPages?'disabled':'') + '>Next »</button>';
|
|
892
|
+
html += '</div>';
|
|
893
|
+
|
|
894
|
+
app.innerHTML = renderLayout('security', html);
|
|
895
|
+
|
|
896
|
+
} catch(err) {
|
|
897
|
+
app.innerHTML = renderLayout('security',
|
|
898
|
+
'<div class="empty"><div class="icon">⚠️</div><div class="text">Failed to load: ' + esc(String(err)) + '</div></div>');
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
window.toggleAlertDetail = function(alertId) {
|
|
903
|
+
var el = document.getElementById('alert-detail-' + alertId);
|
|
904
|
+
if (el) {
|
|
905
|
+
el.style.display = el.style.display === 'none' ? 'block' : 'none';
|
|
906
|
+
}
|
|
907
|
+
};
|
|
908
|
+
|
|
909
|
+
// Unified dropdown toggle / select logic
|
|
910
|
+
var SEC_MENUS = ['severity','category','status','time'];
|
|
911
|
+
|
|
912
|
+
window.secToggleMenu = function(e, id) {
|
|
913
|
+
e.stopPropagation();
|
|
914
|
+
SEC_MENUS.forEach(function(m) {
|
|
915
|
+
var menu = document.getElementById('sec-menu-' + m);
|
|
916
|
+
if (menu) { if (m === id) menu.classList.toggle('open'); else menu.classList.remove('open'); }
|
|
917
|
+
});
|
|
918
|
+
};
|
|
919
|
+
|
|
920
|
+
window.secSelect = function(id, key) {
|
|
921
|
+
if (id === 'severity') secFilterSeverity = key;
|
|
922
|
+
if (id === 'category') secFilterCategory = key;
|
|
923
|
+
if (id === 'status') secFilterStatus = key;
|
|
924
|
+
if (id === 'time') secFilterTimeRange = key;
|
|
925
|
+
secPage = 1;
|
|
926
|
+
var menu = document.getElementById('sec-menu-' + id);
|
|
927
|
+
if (menu) menu.classList.remove('open');
|
|
928
|
+
renderSecurity();
|
|
929
|
+
};
|
|
930
|
+
|
|
931
|
+
window.secApplyFilter = function() {
|
|
932
|
+
var s = document.getElementById('sec-f-search');
|
|
933
|
+
secFilterSearch = s ? s.value.trim() : '';
|
|
934
|
+
secPage = 1;
|
|
935
|
+
renderSecurity();
|
|
936
|
+
};
|
|
937
|
+
|
|
938
|
+
window.secClearFilter = function() {
|
|
939
|
+
secFilterSearch = '';
|
|
940
|
+
secFilterSeverity = '';
|
|
941
|
+
secFilterCategory = '';
|
|
942
|
+
secFilterStatus = '';
|
|
943
|
+
secFilterTimeRange = '';
|
|
944
|
+
secPage = 1;
|
|
945
|
+
renderSecurity();
|
|
946
|
+
};
|
|
947
|
+
|
|
948
|
+
window.secGoPage = function(p) {
|
|
949
|
+
if (p < 1) return;
|
|
950
|
+
secPage = p;
|
|
951
|
+
renderSecurity();
|
|
952
|
+
};
|
|
953
|
+
|
|
954
|
+
window.updateAlertSt = async function(alertId, newStatus) {
|
|
955
|
+
try {
|
|
956
|
+
await fetch(API + '/alerts/' + encodeURIComponent(alertId) + '/status', {
|
|
957
|
+
method: 'POST',
|
|
958
|
+
headers: {'Content-Type': 'application/json'},
|
|
959
|
+
body: JSON.stringify({status: newStatus})
|
|
960
|
+
});
|
|
961
|
+
renderSecurity();
|
|
962
|
+
} catch(e) {
|
|
963
|
+
console.error('Failed to update alert:', e);
|
|
964
|
+
}
|
|
965
|
+
};
|
|
966
|
+
|
|
967
|
+
// Click anywhere on page to close security page dropdowns
|
|
968
|
+
document.addEventListener('click', function() {
|
|
969
|
+
SEC_MENUS.forEach(function(m) {
|
|
970
|
+
var menu = document.getElementById('sec-menu-' + m);
|
|
971
|
+
if (menu) menu.classList.remove('open');
|
|
972
|
+
});
|
|
973
|
+
});
|
|
974
|
+
|
|
975
|
+
/* ================================================================ */
|
|
976
|
+
/* Trace view */
|
|
977
|
+
/* ================================================================ */
|
|
978
|
+
|
|
979
|
+
var selectedActionIdx = -1;
|
|
980
|
+
|
|
981
|
+
async function renderTrace(sessionId, highlightAction, highlightTime) {
|
|
982
|
+
selectedActionIdx = -1;
|
|
983
|
+
app.innerHTML = renderLayout('trace',
|
|
984
|
+
'<div class="trace-header"><div class="loading">Loading trace...</div></div>');
|
|
985
|
+
|
|
986
|
+
try {
|
|
987
|
+
var dataArr = await Promise.all([
|
|
988
|
+
fetchApi('/sessions/' + encodeURIComponent(sessionId) + '/actions'),
|
|
989
|
+
fetchApi('/sessions/' + encodeURIComponent(sessionId) + '/alerts').catch(function(){ return {alerts:[]}; })
|
|
990
|
+
]);
|
|
991
|
+
var data = dataArr[0];
|
|
992
|
+
var actions = data.actions || [];
|
|
993
|
+
// Security alerts
|
|
994
|
+
var traceAlerts = (dataArr[1] && dataArr[1].alerts) ? dataArr[1].alerts : [];
|
|
995
|
+
|
|
996
|
+
if (actions.length === 0) {
|
|
997
|
+
app.innerHTML = renderLayout('trace',
|
|
998
|
+
'<div class="trace-header">' +
|
|
999
|
+
'<a href="#/" class="trace-back">← Back</a>' +
|
|
1000
|
+
'<div class="empty"><div class="icon">📭</div><div class="text">No actions found for this session</div></div>' +
|
|
1001
|
+
'</div>');
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
// Build spans with timing
|
|
1006
|
+
var spans = actions.map(function(a, idx) {
|
|
1007
|
+
var endMs = new Date(a.created_at).getTime();
|
|
1008
|
+
var startMs = a.duration_ms ? endMs - a.duration_ms : endMs;
|
|
1009
|
+
return {
|
|
1010
|
+
idx: idx,
|
|
1011
|
+
action: a,
|
|
1012
|
+
startMs: startMs,
|
|
1013
|
+
endMs: endMs,
|
|
1014
|
+
level: 0,
|
|
1015
|
+
children: []
|
|
1016
|
+
};
|
|
1017
|
+
});
|
|
1018
|
+
|
|
1019
|
+
// Determine time range
|
|
1020
|
+
var sessionStart = Math.min.apply(null, spans.map(function(s){ return s.startMs; }));
|
|
1021
|
+
var sessionEnd = Math.max.apply(null, spans.map(function(s){ return s.endMs; }));
|
|
1022
|
+
var totalDur = sessionEnd - sessionStart || 1;
|
|
1023
|
+
|
|
1024
|
+
// ---- Sorting + auto-nesting ----
|
|
1025
|
+
// Semantic priority: some action_types represent outer lifecycle, should rank first as parents
|
|
1026
|
+
// agent_end = entire agent execution cycle, message(llm_call) = LLM call cycle
|
|
1027
|
+
function typePriority(actionType) {
|
|
1028
|
+
if (actionType === 'agent_end') return 0; // outermost
|
|
1029
|
+
if (actionType === 'message') return 1; // LLM call
|
|
1030
|
+
return 10; // others
|
|
1031
|
+
}
|
|
1032
|
+
// Sort by startMs asc; when startMs is close (within 2s), prioritize by semantic priority; then by duration desc
|
|
1033
|
+
spans.sort(function(a, b) {
|
|
1034
|
+
var startDiff = a.startMs - b.startMs;
|
|
1035
|
+
if (Math.abs(startDiff) > 2000) return startDiff;
|
|
1036
|
+
var priA = typePriority(a.action.action_type);
|
|
1037
|
+
var priB = typePriority(b.action.action_type);
|
|
1038
|
+
if (priA !== priB) return priA - priB;
|
|
1039
|
+
return (b.endMs - b.startMs) - (a.endMs - a.startMs);
|
|
1040
|
+
});
|
|
1041
|
+
|
|
1042
|
+
// Auto-nesting: use "open parent" stack to determine depth
|
|
1043
|
+
// If a span's time range is fully contained within an earlier span, indent it
|
|
1044
|
+
var parentStack = []; // { startMs, endMs, level }
|
|
1045
|
+
var flatSpans = [];
|
|
1046
|
+
|
|
1047
|
+
spans.forEach(function(span) {
|
|
1048
|
+
// Pop completed parents
|
|
1049
|
+
while (parentStack.length > 0 &&
|
|
1050
|
+
parentStack[parentStack.length - 1].endMs < span.startMs - 1000) {
|
|
1051
|
+
parentStack.pop();
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
// Current span depth = number of containing parents
|
|
1055
|
+
span.level = parentStack.length;
|
|
1056
|
+
|
|
1057
|
+
flatSpans.push(span);
|
|
1058
|
+
|
|
1059
|
+
// Spans with duration can become parents ("wrapper" bars in waterfall chart)
|
|
1060
|
+
var dur = span.endMs - span.startMs;
|
|
1061
|
+
if (dur > 100) { // only spans > 100ms become parents
|
|
1062
|
+
parentStack.push({ startMs: span.startMs, endMs: span.endMs, level: span.level });
|
|
1063
|
+
}
|
|
1064
|
+
});
|
|
1065
|
+
|
|
1066
|
+
// Session metadata
|
|
1067
|
+
var firstAction = actions[0];
|
|
1068
|
+
var lastAction = actions[actions.length - 1];
|
|
1069
|
+
var modelName = '';
|
|
1070
|
+
var userId = firstAction.user_id || '';
|
|
1071
|
+
for (var i = 0; i < actions.length; i++) {
|
|
1072
|
+
if (actions[i].model_name) { modelName = actions[i].model_name; break; }
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
var html = '';
|
|
1076
|
+
|
|
1077
|
+
// === TOP PANE: trace header + waterfall (scrollable) ===
|
|
1078
|
+
html += '<div class="trace-top">';
|
|
1079
|
+
|
|
1080
|
+
// Trace header
|
|
1081
|
+
html += '<div class="trace-header">';
|
|
1082
|
+
html += '<a href="#/" class="trace-back">← Back to Traces</a>';
|
|
1083
|
+
html += '<div class="trace-title">Session: ' + esc(sessionId) + '</div>';
|
|
1084
|
+
html += '<div class="trace-meta">';
|
|
1085
|
+
html += '<span class="trace-meta-item"><b>Agent:</b> ' + esc(userId) + '</span>';
|
|
1086
|
+
html += '<span class="trace-meta-item"><b>Model:</b> ' + esc(modelName) + '</span>';
|
|
1087
|
+
html += '<span class="trace-meta-item"><b>Duration:</b> ' + fmtDur(totalDur) + '</span>';
|
|
1088
|
+
html += '<span class="trace-meta-item"><b>Actions:</b> ' + actions.length + '</span>';
|
|
1089
|
+
html += '<span class="trace-meta-item"><b>Time:</b> ' + fmtTime(firstAction.created_at) + ' → ' + fmtTime(lastAction.created_at) + '</span>';
|
|
1090
|
+
html += '</div></div>';
|
|
1091
|
+
|
|
1092
|
+
// Security alert banner (if any)
|
|
1093
|
+
if (traceAlerts.length > 0) {
|
|
1094
|
+
var critAlerts = traceAlerts.filter(function(a){ return a.severity === 'critical'; });
|
|
1095
|
+
var warnAlerts = traceAlerts.filter(function(a){ return a.severity === 'warn'; });
|
|
1096
|
+
var bannerSev = critAlerts.length > 0 ? 'critical' : 'warn';
|
|
1097
|
+
html += '<div class="trace-alert-banner sev-' + bannerSev + '" onclick="location.hash=\\'#/security\\'">';
|
|
1098
|
+
html += '🛡️ <span class="count">' + traceAlerts.length + '</span> security alert' + (traceAlerts.length>1?'s':'') + ' detected';
|
|
1099
|
+
if (critAlerts.length > 0) html += ' (' + critAlerts.length + ' critical)';
|
|
1100
|
+
html += ' — click to view';
|
|
1101
|
+
html += '</div>';
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
// Waterfall
|
|
1105
|
+
html += '<div class="waterfall">';
|
|
1106
|
+
html += '<div class="wf-header"><span>Name</span><span style="text-align:right;padding-right:14px">Duration</span><span>Timeline</span></div>';
|
|
1107
|
+
|
|
1108
|
+
var WF_FOLD_LIMIT = 100;
|
|
1109
|
+
var needFold = flatSpans.length > WF_FOLD_LIMIT;
|
|
1110
|
+
|
|
1111
|
+
flatSpans.forEach(function(span, fi) {
|
|
1112
|
+
var a = span.action;
|
|
1113
|
+
var color = typeColor(a.action_type);
|
|
1114
|
+
var leftPct = ((span.startMs - sessionStart) / totalDur * 100).toFixed(2);
|
|
1115
|
+
var widthPct = Math.max(((span.endMs - span.startMs) / totalDur * 100), 0.3).toFixed(2);
|
|
1116
|
+
var indent = span.level * 20;
|
|
1117
|
+
|
|
1118
|
+
// When over 100 rows, fold the rest
|
|
1119
|
+
if (needFold && fi === WF_FOLD_LIMIT) {
|
|
1120
|
+
html += '<div class="wf-fold" id="wf-fold-btn" onclick="expandWaterfall()">';
|
|
1121
|
+
html += '<span>▼ Show remaining ' + (flatSpans.length - WF_FOLD_LIMIT) + ' actions (' + flatSpans.length + ' total)</span>';
|
|
1122
|
+
html += '</div>';
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
var hiddenCls = (needFold && fi >= WF_FOLD_LIMIT) ? ' wf-row--hidden' : '';
|
|
1126
|
+
html += '<div class="wf-row' + hiddenCls + (fi === selectedActionIdx ? ' selected' : '') + '" data-idx="' + fi + '" onclick="selectAction(' + fi + ')">';
|
|
1127
|
+
html += '<div class="wf-name">';
|
|
1128
|
+
if (indent > 0) html += '<span class="indent" style="width:' + indent + 'px;display:inline-flex;justify-content:center">└</span>';
|
|
1129
|
+
html += '<span class="dot" style="background:' + color + '"></span>';
|
|
1130
|
+
html += '<span class="text">' + esc(a.action_name) + '</span>';
|
|
1131
|
+
html += '</div>';
|
|
1132
|
+
html += '<div class="wf-dur">' + fmtDur(a.duration_ms) + '</div>';
|
|
1133
|
+
html += '<div class="wf-bar-wrap"><div class="wf-bar" style="left:' + leftPct + '%;width:' + widthPct + '%;background:' + color + '"></div></div>';
|
|
1134
|
+
html += '</div>';
|
|
1135
|
+
});
|
|
1136
|
+
|
|
1137
|
+
// Timeline ticks
|
|
1138
|
+
html += '<div class="wf-ticks"><span></span><span></span><div class="wf-ticks-bar">';
|
|
1139
|
+
for (var t = 0; t <= 4; t++) {
|
|
1140
|
+
html += '<span class="wf-tick">' + fmtDur(Math.round(totalDur * t / 4)) + '</span>';
|
|
1141
|
+
}
|
|
1142
|
+
html += '</div></div>';
|
|
1143
|
+
html += '</div>'; // .waterfall
|
|
1144
|
+
html += '</div>'; // .trace-top
|
|
1145
|
+
|
|
1146
|
+
// === RESIZE HANDLE ===
|
|
1147
|
+
html += '<div class="trace-resize" id="trace-resize"></div>';
|
|
1148
|
+
|
|
1149
|
+
// === BOTTOM PANE: detail panel (fixed, scrollable) ===
|
|
1150
|
+
html += '<div class="trace-bottom" id="detail-container">';
|
|
1151
|
+
html += '<div class="detail-empty">Click an action above to view details</div>';
|
|
1152
|
+
html += '</div>';
|
|
1153
|
+
|
|
1154
|
+
app.innerHTML = renderLayout('trace', html);
|
|
1155
|
+
|
|
1156
|
+
// Store spans for detail rendering
|
|
1157
|
+
window.__traceSpans = flatSpans;
|
|
1158
|
+
|
|
1159
|
+
// Init resize drag
|
|
1160
|
+
initResizeDrag();
|
|
1161
|
+
|
|
1162
|
+
// Auto-select: if highlight params provided (from security alert), find matching action
|
|
1163
|
+
var autoIdx = -1;
|
|
1164
|
+
if (highlightAction && highlightTime) {
|
|
1165
|
+
autoIdx = flatSpans.findIndex(function(s) {
|
|
1166
|
+
return s.action.action_name === highlightAction &&
|
|
1167
|
+
s.action.created_at && s.action.created_at.indexOf(highlightTime) >= 0;
|
|
1168
|
+
});
|
|
1169
|
+
// Fallback: match by action_name only
|
|
1170
|
+
if (autoIdx < 0) {
|
|
1171
|
+
autoIdx = flatSpans.findIndex(function(s) {
|
|
1172
|
+
return s.action.action_name === highlightAction;
|
|
1173
|
+
});
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
// If no highlight match, default to first LLM call or first action
|
|
1177
|
+
if (autoIdx < 0) {
|
|
1178
|
+
autoIdx = flatSpans.findIndex(function(s){ return s.action.action_type === 'message'; });
|
|
1179
|
+
}
|
|
1180
|
+
if (autoIdx < 0) autoIdx = 0;
|
|
1181
|
+
|
|
1182
|
+
// If the target action is in the folded section, expand first
|
|
1183
|
+
if (needFold && autoIdx >= WF_FOLD_LIMIT) {
|
|
1184
|
+
expandWaterfall();
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
selectAction(autoIdx);
|
|
1188
|
+
|
|
1189
|
+
} catch(err) {
|
|
1190
|
+
app.innerHTML = renderLayout('trace',
|
|
1191
|
+
'<div class="trace-header">' +
|
|
1192
|
+
'<a href="#/" class="trace-back">← Back</a>' +
|
|
1193
|
+
'<div class="empty"><div class="icon">⚠️</div><div class="text">Failed to load: ' + esc(String(err)) + '</div></div>' +
|
|
1194
|
+
'</div>');
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
/* ---------- expand folded waterfall rows ---------- */
|
|
1199
|
+
window.expandWaterfall = function() {
|
|
1200
|
+
var hidden = document.querySelectorAll('.wf-row--hidden');
|
|
1201
|
+
hidden.forEach(function(r) { r.classList.remove('wf-row--hidden'); });
|
|
1202
|
+
var btn = document.getElementById('wf-fold-btn');
|
|
1203
|
+
if (btn) btn.style.display = 'none';
|
|
1204
|
+
};
|
|
1205
|
+
|
|
1206
|
+
/* ---------- resize drag logic ---------- */
|
|
1207
|
+
function initResizeDrag() {
|
|
1208
|
+
var handle = document.getElementById('trace-resize');
|
|
1209
|
+
if (!handle) return;
|
|
1210
|
+
|
|
1211
|
+
var startY = 0;
|
|
1212
|
+
var startBottomH = 0;
|
|
1213
|
+
var bottomEl = null;
|
|
1214
|
+
|
|
1215
|
+
function onMouseDown(e) {
|
|
1216
|
+
e.preventDefault();
|
|
1217
|
+
e.stopPropagation();
|
|
1218
|
+
bottomEl = document.querySelector('.trace-bottom');
|
|
1219
|
+
if (!bottomEl) return;
|
|
1220
|
+
startY = e.clientY != null ? e.clientY : (e.touches ? e.touches[0].clientY : 0);
|
|
1221
|
+
startBottomH = bottomEl.getBoundingClientRect().height;
|
|
1222
|
+
handle.classList.add('active');
|
|
1223
|
+
document.body.classList.add('resizing');
|
|
1224
|
+
document.addEventListener('mousemove', onMouseMove, true);
|
|
1225
|
+
document.addEventListener('mouseup', onMouseUp, true);
|
|
1226
|
+
document.addEventListener('touchmove', onMouseMove, {passive:false, capture:true});
|
|
1227
|
+
document.addEventListener('touchend', onMouseUp, true);
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
function onMouseMove(e) {
|
|
1231
|
+
e.preventDefault();
|
|
1232
|
+
e.stopPropagation();
|
|
1233
|
+
var clientY = e.clientY != null ? e.clientY : (e.touches ? e.touches[0].clientY : 0);
|
|
1234
|
+
var diff = startY - clientY;
|
|
1235
|
+
var newH = Math.max(140, Math.min(window.innerHeight * 0.7, startBottomH + diff));
|
|
1236
|
+
bottomEl.style.flexBasis = newH + 'px';
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
function onMouseUp(e) {
|
|
1240
|
+
handle.classList.remove('active');
|
|
1241
|
+
document.body.classList.remove('resizing');
|
|
1242
|
+
document.removeEventListener('mousemove', onMouseMove, true);
|
|
1243
|
+
document.removeEventListener('mouseup', onMouseUp, true);
|
|
1244
|
+
document.removeEventListener('touchmove', onMouseMove, true);
|
|
1245
|
+
document.removeEventListener('touchend', onMouseUp, true);
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
handle.addEventListener('mousedown', onMouseDown);
|
|
1249
|
+
handle.addEventListener('touchstart', onMouseDown, {passive:false});
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
/* ---------- select action (renders detail in bottom pane) ---------- */
|
|
1253
|
+
window.selectAction = function(fi) {
|
|
1254
|
+
selectedActionIdx = fi;
|
|
1255
|
+
var spans = window.__traceSpans;
|
|
1256
|
+
if (!spans || fi < 0 || fi >= spans.length) return;
|
|
1257
|
+
|
|
1258
|
+
// Update selected row styling
|
|
1259
|
+
var rows = document.querySelectorAll('.wf-row');
|
|
1260
|
+
rows.forEach(function(r, i) {
|
|
1261
|
+
r.classList.toggle('selected', i === fi);
|
|
1262
|
+
});
|
|
1263
|
+
|
|
1264
|
+
// Scroll the selected row into view within the top pane
|
|
1265
|
+
if (rows[fi]) {
|
|
1266
|
+
rows[fi].scrollIntoView({block:'nearest',behavior:'smooth'});
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
var span = spans[fi];
|
|
1270
|
+
var a = span.action;
|
|
1271
|
+
var color = typeColor(a.action_type);
|
|
1272
|
+
var input = parseJson(a.input_params);
|
|
1273
|
+
var output = parseJson(a.output_result);
|
|
1274
|
+
|
|
1275
|
+
var html = '<div class="detail-panel">';
|
|
1276
|
+
|
|
1277
|
+
// Header (sticky)
|
|
1278
|
+
html += '<div class="detail-header">';
|
|
1279
|
+
html += '<span class="dot" style="background:' + color + '"></span>';
|
|
1280
|
+
html += '<span class="name">' + esc(a.action_name) + '</span>';
|
|
1281
|
+
if (a.duration_ms != null) html += '<span class="dur">' + fmtDur(a.duration_ms) + '</span>';
|
|
1282
|
+
html += '</div>';
|
|
1283
|
+
|
|
1284
|
+
// Meta
|
|
1285
|
+
html += '<div class="detail-meta">';
|
|
1286
|
+
html += '<span><b>Type:</b> ' + typeLabel(a.action_type) + '</span>';
|
|
1287
|
+
html += '<span><b>Time:</b> ' + fmtTime(a.created_at) + '</span>';
|
|
1288
|
+
if (a.model_name) html += '<span><b>Model:</b> ' + esc(a.model_name) + '</span>';
|
|
1289
|
+
if (a.user_id) html += '<span><b>Agent:</b> ' + esc(a.user_id) + '</span>';
|
|
1290
|
+
if (a.prompt_tokens != null) html += '<span><b>Prompt Tokens:</b> ' + a.prompt_tokens + '</span>';
|
|
1291
|
+
if (a.completion_tokens != null) html += '<span><b>Completion Tokens:</b> ' + a.completion_tokens + '</span>';
|
|
1292
|
+
html += '</div>';
|
|
1293
|
+
|
|
1294
|
+
// Body: Input / Output side by side
|
|
1295
|
+
html += '<div class="detail-body">';
|
|
1296
|
+
|
|
1297
|
+
html += '<div class="detail-section">';
|
|
1298
|
+
html += '<h4>Input</h4>';
|
|
1299
|
+
if (input) {
|
|
1300
|
+
html += '<div class="json-view">' + prettyJson(input) + '</div>';
|
|
1301
|
+
} else {
|
|
1302
|
+
html += '<div class="json-view" style="color:var(--muted)">null</div>';
|
|
1303
|
+
}
|
|
1304
|
+
html += '</div>';
|
|
1305
|
+
|
|
1306
|
+
html += '<div class="detail-section">';
|
|
1307
|
+
html += '<h4>Output</h4>';
|
|
1308
|
+
if (output) {
|
|
1309
|
+
html += '<div class="json-view">' + prettyJson(output) + '</div>';
|
|
1310
|
+
} else {
|
|
1311
|
+
html += '<div class="json-view" style="color:var(--muted)">null</div>';
|
|
1312
|
+
}
|
|
1313
|
+
html += '</div>';
|
|
1314
|
+
|
|
1315
|
+
html += '</div></div>';
|
|
1316
|
+
|
|
1317
|
+
var container = document.getElementById('detail-container');
|
|
1318
|
+
if (container) {
|
|
1319
|
+
container.innerHTML = html;
|
|
1320
|
+
container.scrollTop = 0; // scroll detail pane to top
|
|
1321
|
+
}
|
|
1322
|
+
};
|
|
1323
|
+
|
|
1324
|
+
/* ---------- init ---------- */
|
|
1325
|
+
router();
|
|
1326
|
+
`;
|
|
1327
|
+
//# sourceMappingURL=ui.js.map
|