proofscan 0.10.62 → 0.11.1
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/README.ja.md +1 -0
- package/README.md +2 -0
- package/dist/a2a/agent-card.d.ts +2 -0
- package/dist/a2a/agent-card.d.ts.map +1 -1
- package/dist/a2a/agent-card.js +2 -2
- package/dist/a2a/agent-card.js.map +1 -1
- package/dist/a2a/client.d.ts +74 -12
- package/dist/a2a/client.d.ts.map +1 -1
- package/dist/a2a/client.js +228 -29
- package/dist/a2a/client.js.map +1 -1
- package/dist/a2a/normalizer.d.ts +4 -0
- package/dist/a2a/normalizer.d.ts.map +1 -1
- package/dist/a2a/normalizer.js +7 -4
- package/dist/a2a/normalizer.js.map +1 -1
- package/dist/a2a/session-manager.d.ts +81 -0
- package/dist/a2a/session-manager.d.ts.map +1 -0
- package/dist/a2a/session-manager.js +176 -0
- package/dist/a2a/session-manager.js.map +1 -0
- package/dist/a2a/types.d.ts +60 -0
- package/dist/a2a/types.d.ts.map +1 -1
- package/dist/cli.d.ts +2 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +6 -3
- package/dist/cli.js.map +1 -1
- package/dist/commands/agent.d.ts.map +1 -1
- package/dist/commands/agent.js +35 -10
- package/dist/commands/agent.js.map +1 -1
- package/dist/commands/analyze.d.ts.map +1 -1
- package/dist/commands/analyze.js +12 -10
- package/dist/commands/analyze.js.map +1 -1
- package/dist/commands/connectors.js +2 -2
- package/dist/commands/connectors.js.map +1 -1
- package/dist/commands/index.d.ts +1 -0
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +1 -0
- package/dist/commands/index.js.map +1 -1
- package/dist/commands/plans.js +1 -1
- package/dist/commands/plans.js.map +1 -1
- package/dist/commands/record.js +5 -4
- package/dist/commands/record.js.map +1 -1
- package/dist/commands/rpc.d.ts.map +1 -1
- package/dist/commands/rpc.js +90 -28
- package/dist/commands/rpc.js.map +1 -1
- package/dist/commands/scan.d.ts.map +1 -1
- package/dist/commands/scan.js +8 -10
- package/dist/commands/scan.js.map +1 -1
- package/dist/commands/secrets.d.ts.map +1 -1
- package/dist/commands/secrets.js +11 -10
- package/dist/commands/secrets.js.map +1 -1
- package/dist/commands/sessions.js +2 -2
- package/dist/commands/sessions.js.map +1 -1
- package/dist/commands/summary.d.ts.map +1 -1
- package/dist/commands/summary.js +4 -2
- package/dist/commands/summary.js.map +1 -1
- package/dist/commands/task.d.ts +14 -0
- package/dist/commands/task.d.ts.map +1 -0
- package/dist/commands/task.js +520 -0
- package/dist/commands/task.js.map +1 -0
- package/dist/db/connection.d.ts.map +1 -1
- package/dist/db/connection.js +68 -21
- package/dist/db/connection.js.map +1 -1
- package/dist/db/events-store.d.ts +307 -8
- package/dist/db/events-store.d.ts.map +1 -1
- package/dist/db/events-store.js +620 -26
- package/dist/db/events-store.js.map +1 -1
- package/dist/db/proofs-store.d.ts +8 -1
- package/dist/db/proofs-store.d.ts.map +1 -1
- package/dist/db/proofs-store.js +18 -8
- package/dist/db/proofs-store.js.map +1 -1
- package/dist/db/schema.d.ts +15 -3
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/db/schema.js +150 -5
- package/dist/db/schema.js.map +1 -1
- package/dist/db/tool-analysis.d.ts +15 -3
- package/dist/db/tool-analysis.d.ts.map +1 -1
- package/dist/db/tool-analysis.js +35 -17
- package/dist/db/tool-analysis.js.map +1 -1
- package/dist/db/types.d.ts +64 -1
- package/dist/db/types.d.ts.map +1 -1
- package/dist/filter/fields.d.ts.map +1 -1
- package/dist/filter/fields.js +22 -0
- package/dist/filter/fields.js.map +1 -1
- package/dist/filter/parser.js +2 -2
- package/dist/filter/parser.js.map +1 -1
- package/dist/filter/types.d.ts +1 -1
- package/dist/filter/types.d.ts.map +1 -1
- package/dist/html/analytics.test.ts +682 -0
- package/dist/html/analytics.ts +499 -0
- package/dist/html/browser.ts +39 -0
- package/dist/html/index.ts +97 -0
- package/dist/html/rpc-inspector.test.ts +529 -0
- package/dist/html/rpc-inspector.ts +1700 -0
- package/dist/html/templates.js +4 -4
- package/dist/html/templates.js.map +1 -1
- package/dist/html/templates.test.ts +861 -0
- package/dist/html/templates.ts +3163 -0
- package/dist/html/trace-viewer.html +624 -0
- package/dist/html/types.d.ts +3 -3
- package/dist/html/types.d.ts.map +1 -1
- package/dist/html/types.ts +491 -0
- package/dist/html/utils.ts +107 -0
- package/dist/monitor/data/connectors.d.ts.map +1 -1
- package/dist/monitor/data/connectors.js +113 -8
- package/dist/monitor/data/connectors.js.map +1 -1
- package/dist/monitor/data/popl.js +2 -2
- package/dist/monitor/data/popl.js.map +1 -1
- package/dist/monitor/routes/api.js +2 -2
- package/dist/monitor/routes/api.js.map +1 -1
- package/dist/monitor/routes/connectors.js +15 -15
- package/dist/monitor/routes/connectors.js.map +1 -1
- package/dist/monitor/routes/popl.js +5 -5
- package/dist/monitor/routes/popl.js.map +1 -1
- package/dist/monitor/templates/components.js +2 -2
- package/dist/monitor/templates/components.js.map +1 -1
- package/dist/monitor/templates/popl.js +4 -4
- package/dist/monitor/templates/popl.js.map +1 -1
- package/dist/monitor/types.d.ts +2 -2
- package/dist/monitor/types.d.ts.map +1 -1
- package/dist/proxy/bridge-utils.d.ts +41 -0
- package/dist/proxy/bridge-utils.d.ts.map +1 -0
- package/dist/proxy/bridge-utils.js +60 -0
- package/dist/proxy/bridge-utils.js.map +1 -0
- package/dist/proxy/ipc-client.d.ts.map +1 -1
- package/dist/proxy/ipc-client.js +1 -2
- package/dist/proxy/ipc-client.js.map +1 -1
- package/dist/proxy/ipc-server.d.ts.map +1 -1
- package/dist/proxy/ipc-server.js +4 -2
- package/dist/proxy/ipc-server.js.map +1 -1
- package/dist/proxy/mcp-server.d.ts +31 -0
- package/dist/proxy/mcp-server.d.ts.map +1 -1
- package/dist/proxy/mcp-server.js +393 -4
- package/dist/proxy/mcp-server.js.map +1 -1
- package/dist/proxy/types.d.ts +95 -0
- package/dist/proxy/types.d.ts.map +1 -1
- package/dist/secrets/management.d.ts +2 -2
- package/dist/secrets/management.d.ts.map +1 -1
- package/dist/secrets/management.js +7 -7
- package/dist/secrets/management.js.map +1 -1
- package/dist/shell/completer.d.ts.map +1 -1
- package/dist/shell/completer.js +16 -0
- package/dist/shell/completer.js.map +1 -1
- package/dist/shell/context-applicator.d.ts.map +1 -1
- package/dist/shell/context-applicator.js +32 -0
- package/dist/shell/context-applicator.js.map +1 -1
- package/dist/shell/filter-mappers.d.ts +5 -1
- package/dist/shell/filter-mappers.d.ts.map +1 -1
- package/dist/shell/filter-mappers.js +12 -0
- package/dist/shell/filter-mappers.js.map +1 -1
- package/dist/shell/find-command.js +13 -13
- package/dist/shell/find-command.js.map +1 -1
- package/dist/shell/inscribe-commands.js +5 -5
- package/dist/shell/inscribe-commands.js.map +1 -1
- package/dist/shell/pager/less-pager.d.ts +1 -1
- package/dist/shell/pager/less-pager.d.ts.map +1 -1
- package/dist/shell/pager/less-pager.js +5 -2
- package/dist/shell/pager/less-pager.js.map +1 -1
- package/dist/shell/pager/more-pager.d.ts +1 -1
- package/dist/shell/pager/more-pager.d.ts.map +1 -1
- package/dist/shell/pager/more-pager.js +3 -2
- package/dist/shell/pager/more-pager.js.map +1 -1
- package/dist/shell/pager/renderer.d.ts.map +1 -1
- package/dist/shell/pager/renderer.js +66 -15
- package/dist/shell/pager/renderer.js.map +1 -1
- package/dist/shell/pager/types.d.ts +5 -2
- package/dist/shell/pager/types.d.ts.map +1 -1
- package/dist/shell/pager/utils.d.ts +5 -2
- package/dist/shell/pager/utils.d.ts.map +1 -1
- package/dist/shell/pager/utils.js +14 -17
- package/dist/shell/pager/utils.js.map +1 -1
- package/dist/shell/pipeline-types.d.ts +12 -4
- package/dist/shell/pipeline-types.d.ts.map +1 -1
- package/dist/shell/ref-commands.js +7 -7
- package/dist/shell/ref-commands.js.map +1 -1
- package/dist/shell/ref-resolver.d.ts +15 -15
- package/dist/shell/ref-resolver.d.ts.map +1 -1
- package/dist/shell/ref-resolver.js +34 -20
- package/dist/shell/ref-resolver.js.map +1 -1
- package/dist/shell/repl.d.ts +25 -0
- package/dist/shell/repl.d.ts.map +1 -1
- package/dist/shell/repl.js +285 -51
- package/dist/shell/repl.js.map +1 -1
- package/dist/shell/router-commands.d.ts +30 -0
- package/dist/shell/router-commands.d.ts.map +1 -1
- package/dist/shell/router-commands.js +1011 -62
- package/dist/shell/router-commands.js.map +1 -1
- package/dist/shell/selector.d.ts +1 -1
- package/dist/shell/selector.d.ts.map +1 -1
- package/dist/shell/selector.js +1 -1
- package/dist/shell/selector.js.map +1 -1
- package/dist/shell/types.d.ts.map +1 -1
- package/dist/shell/types.js +3 -1
- package/dist/shell/types.js.map +1 -1
- package/dist/shell/where-command.d.ts.map +1 -1
- package/dist/shell/where-command.js +19 -3
- package/dist/shell/where-command.js.map +1 -1
- package/dist/utils/output.d.ts.map +1 -1
- package/dist/utils/output.js +7 -1
- package/dist/utils/output.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,3163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTML Templates (Phase 5.0)
|
|
3
|
+
*
|
|
4
|
+
* Generates standalone HTML files for RPC and Session reports.
|
|
5
|
+
* Dark theme with neon blue accent badges.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { formatBytes } from '../eventline/types.js';
|
|
9
|
+
import type {
|
|
10
|
+
HtmlConnectorAnalyticsV1,
|
|
11
|
+
HtmlConnectorKpis,
|
|
12
|
+
HtmlConnectorReportV1,
|
|
13
|
+
HtmlConnectorSessionRow,
|
|
14
|
+
HtmlHeatmapData,
|
|
15
|
+
HtmlLatencyHistogram,
|
|
16
|
+
HtmlMethodDistribution,
|
|
17
|
+
HtmlMethodLatencyData,
|
|
18
|
+
HtmlRpcReportV1,
|
|
19
|
+
HtmlSessionReportV1,
|
|
20
|
+
HtmlTopToolsData,
|
|
21
|
+
PayloadData,
|
|
22
|
+
RpcStatus,
|
|
23
|
+
SessionRpcDetail,
|
|
24
|
+
} from './types.js';
|
|
25
|
+
import { getStatusSymbol, SHORT_ID_LENGTH } from './types.js';
|
|
26
|
+
import {
|
|
27
|
+
getRpcInspectorStyles,
|
|
28
|
+
getRpcInspectorScript,
|
|
29
|
+
renderJsonWithPaths,
|
|
30
|
+
renderRequestSummary,
|
|
31
|
+
renderResponseSummary,
|
|
32
|
+
renderSummaryRowsHtml,
|
|
33
|
+
detectSensitiveKeys,
|
|
34
|
+
} from './rpc-inspector.js';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Escape HTML special characters to prevent XSS
|
|
38
|
+
*/
|
|
39
|
+
export function escapeHtml(text: string): string {
|
|
40
|
+
return text
|
|
41
|
+
.replace(/&/g, '&')
|
|
42
|
+
.replace(/</g, '<')
|
|
43
|
+
.replace(/>/g, '>')
|
|
44
|
+
.replace(/"/g, '"')
|
|
45
|
+
.replace(/'/g, ''');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Escape JSON for embedding in <script type="application/json">
|
|
50
|
+
* Must escape </script> sequences to prevent premature tag closing
|
|
51
|
+
* Also escape U+2028/U+2029 which are valid in JSON but break JS string literals
|
|
52
|
+
*/
|
|
53
|
+
export function escapeJsonForScript(json: string): string {
|
|
54
|
+
return json
|
|
55
|
+
.replace(/<\/script/gi, '<\\/script')
|
|
56
|
+
.replace(/\u2028/g, '\\u2028') // Line Separator - valid JSON but breaks JS
|
|
57
|
+
.replace(/\u2029/g, '\\u2029'); // Paragraph Separator - valid JSON but breaks JS
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Format timestamp for display
|
|
62
|
+
*/
|
|
63
|
+
function formatTimestamp(ts: string): string {
|
|
64
|
+
try {
|
|
65
|
+
const date = new Date(ts);
|
|
66
|
+
return date.toISOString().replace('T', ' ').replace('Z', ' UTC');
|
|
67
|
+
} catch {
|
|
68
|
+
return ts;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Shorten ID for display
|
|
74
|
+
*/
|
|
75
|
+
function shortenId(id: string, length: number = 8): string {
|
|
76
|
+
return id.slice(0, length);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Get CSS styles for HTML reports (single column for RPC)
|
|
81
|
+
*/
|
|
82
|
+
function getRpcReportStyles(): string {
|
|
83
|
+
return `
|
|
84
|
+
:root {
|
|
85
|
+
--bg-primary: #0d1117;
|
|
86
|
+
--bg-secondary: #161b22;
|
|
87
|
+
--text-primary: #e6edf3;
|
|
88
|
+
--text-secondary: #8b949e;
|
|
89
|
+
--accent-blue: #00d4ff;
|
|
90
|
+
--status-ok: #00d4ff;
|
|
91
|
+
--status-err: #f85149;
|
|
92
|
+
--status-pending: #d29922;
|
|
93
|
+
--border-color: #30363d;
|
|
94
|
+
--link-color: #58a6ff;
|
|
95
|
+
}
|
|
96
|
+
* { box-sizing: border-box; }
|
|
97
|
+
body {
|
|
98
|
+
background: var(--bg-primary);
|
|
99
|
+
color: var(--text-primary);
|
|
100
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
|
|
101
|
+
margin: 0;
|
|
102
|
+
padding: 20px;
|
|
103
|
+
line-height: 1.5;
|
|
104
|
+
}
|
|
105
|
+
h1 {
|
|
106
|
+
margin: 0 0 8px 0;
|
|
107
|
+
font-size: 1.5em;
|
|
108
|
+
font-weight: 600;
|
|
109
|
+
}
|
|
110
|
+
h2 {
|
|
111
|
+
margin: 0 0 12px 0;
|
|
112
|
+
font-size: 1.1em;
|
|
113
|
+
font-weight: 600;
|
|
114
|
+
color: var(--text-primary);
|
|
115
|
+
border-bottom: 1px solid var(--border-color);
|
|
116
|
+
padding-bottom: 8px;
|
|
117
|
+
}
|
|
118
|
+
h3 {
|
|
119
|
+
margin: 16px 0 8px 0;
|
|
120
|
+
font-size: 0.95em;
|
|
121
|
+
font-weight: 600;
|
|
122
|
+
color: var(--text-secondary);
|
|
123
|
+
}
|
|
124
|
+
h3:first-child { margin-top: 0; }
|
|
125
|
+
a { color: var(--link-color); text-decoration: none; }
|
|
126
|
+
a:hover { text-decoration: underline; }
|
|
127
|
+
.meta { color: var(--text-secondary); margin: 0 0 20px 0; font-size: 0.85em; }
|
|
128
|
+
.badge {
|
|
129
|
+
display: inline-block;
|
|
130
|
+
padding: 2px 8px;
|
|
131
|
+
border: 1px solid var(--accent-blue);
|
|
132
|
+
border-radius: 4px;
|
|
133
|
+
color: var(--accent-blue);
|
|
134
|
+
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
|
135
|
+
font-size: 0.85em;
|
|
136
|
+
background: transparent;
|
|
137
|
+
}
|
|
138
|
+
.badge.status-OK { border-color: var(--status-ok); color: var(--status-ok); }
|
|
139
|
+
.badge.status-ERR { border-color: var(--status-err); color: var(--status-err); }
|
|
140
|
+
.badge.status-PENDING { border-color: var(--status-pending); color: var(--status-pending); }
|
|
141
|
+
/* Sensitive content warning badge (Phase 12.x-c) */
|
|
142
|
+
.sensitive-badge {
|
|
143
|
+
display: inline-flex;
|
|
144
|
+
align-items: center;
|
|
145
|
+
gap: 4px;
|
|
146
|
+
padding: 2px 8px;
|
|
147
|
+
margin-left: 8px;
|
|
148
|
+
background: rgba(210, 153, 34, 0.15);
|
|
149
|
+
border: 1px solid rgba(210, 153, 34, 0.3);
|
|
150
|
+
border-radius: 12px;
|
|
151
|
+
font-size: 11px;
|
|
152
|
+
font-weight: 500;
|
|
153
|
+
color: #d29922;
|
|
154
|
+
vertical-align: middle;
|
|
155
|
+
}
|
|
156
|
+
.section {
|
|
157
|
+
background: var(--bg-secondary);
|
|
158
|
+
border-radius: 8px;
|
|
159
|
+
padding: 16px;
|
|
160
|
+
margin: 16px 0;
|
|
161
|
+
}
|
|
162
|
+
dl {
|
|
163
|
+
display: grid;
|
|
164
|
+
grid-template-columns: auto 1fr;
|
|
165
|
+
gap: 8px 16px;
|
|
166
|
+
margin: 0;
|
|
167
|
+
}
|
|
168
|
+
dt { color: var(--text-secondary); }
|
|
169
|
+
dd { margin: 0; }
|
|
170
|
+
pre {
|
|
171
|
+
background: var(--bg-primary);
|
|
172
|
+
border: 1px solid var(--border-color);
|
|
173
|
+
border-radius: 6px;
|
|
174
|
+
padding: 12px;
|
|
175
|
+
overflow-x: auto;
|
|
176
|
+
margin: 8px 0;
|
|
177
|
+
font-size: 0.85em;
|
|
178
|
+
}
|
|
179
|
+
code {
|
|
180
|
+
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
|
181
|
+
color: var(--text-primary);
|
|
182
|
+
}
|
|
183
|
+
.copy-btn {
|
|
184
|
+
background: var(--bg-secondary);
|
|
185
|
+
border: 1px solid var(--border-color);
|
|
186
|
+
color: var(--text-secondary);
|
|
187
|
+
padding: 4px 8px;
|
|
188
|
+
border-radius: 4px;
|
|
189
|
+
cursor: pointer;
|
|
190
|
+
font-size: 0.8em;
|
|
191
|
+
margin-left: 8px;
|
|
192
|
+
}
|
|
193
|
+
.copy-btn:hover {
|
|
194
|
+
border-color: var(--accent-blue);
|
|
195
|
+
color: var(--accent-blue);
|
|
196
|
+
}
|
|
197
|
+
.truncated-note {
|
|
198
|
+
color: var(--status-pending);
|
|
199
|
+
font-size: 0.85em;
|
|
200
|
+
margin: 4px 0;
|
|
201
|
+
}
|
|
202
|
+
.spill-link {
|
|
203
|
+
color: var(--link-color);
|
|
204
|
+
font-size: 0.85em;
|
|
205
|
+
}
|
|
206
|
+
`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Get CSS styles for Session HTML (2-pane Wireshark layout)
|
|
211
|
+
*/
|
|
212
|
+
function getSessionReportStyles(): string {
|
|
213
|
+
return `
|
|
214
|
+
:root {
|
|
215
|
+
--bg-primary: #0d1117;
|
|
216
|
+
--bg-secondary: #161b22;
|
|
217
|
+
--text-primary: #e6edf3;
|
|
218
|
+
--text-secondary: #8b949e;
|
|
219
|
+
--accent-blue: #00d4ff;
|
|
220
|
+
--status-ok: #00d4ff;
|
|
221
|
+
--status-err: #f85149;
|
|
222
|
+
--status-pending: #d29922;
|
|
223
|
+
--border-color: #30363d;
|
|
224
|
+
--link-color: #58a6ff;
|
|
225
|
+
--left-pane-width: 420px;
|
|
226
|
+
}
|
|
227
|
+
* { box-sizing: border-box; }
|
|
228
|
+
html, body {
|
|
229
|
+
height: 100%;
|
|
230
|
+
margin: 0;
|
|
231
|
+
padding: 0;
|
|
232
|
+
overflow: hidden;
|
|
233
|
+
}
|
|
234
|
+
body {
|
|
235
|
+
background: var(--bg-primary);
|
|
236
|
+
color: var(--text-primary);
|
|
237
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
|
|
238
|
+
line-height: 1.5;
|
|
239
|
+
display: flex;
|
|
240
|
+
flex-direction: column;
|
|
241
|
+
}
|
|
242
|
+
header {
|
|
243
|
+
padding: 12px 20px;
|
|
244
|
+
border-bottom: 1px solid var(--border-color);
|
|
245
|
+
flex-shrink: 0;
|
|
246
|
+
}
|
|
247
|
+
h1 {
|
|
248
|
+
margin: 0 0 4px 0;
|
|
249
|
+
font-size: 1.3em;
|
|
250
|
+
font-weight: 600;
|
|
251
|
+
}
|
|
252
|
+
h2 {
|
|
253
|
+
margin: 0 0 8px 0;
|
|
254
|
+
font-size: 1em;
|
|
255
|
+
font-weight: 600;
|
|
256
|
+
color: var(--text-primary);
|
|
257
|
+
border-bottom: 1px solid var(--border-color);
|
|
258
|
+
padding-bottom: 6px;
|
|
259
|
+
}
|
|
260
|
+
h3 {
|
|
261
|
+
margin: 12px 0 6px 0;
|
|
262
|
+
font-size: 0.9em;
|
|
263
|
+
font-weight: 600;
|
|
264
|
+
color: var(--text-secondary);
|
|
265
|
+
}
|
|
266
|
+
h3:first-child { margin-top: 0; }
|
|
267
|
+
a { color: var(--link-color); text-decoration: none; }
|
|
268
|
+
a:hover { text-decoration: underline; }
|
|
269
|
+
.meta { color: var(--text-secondary); margin: 0; font-size: 0.8em; }
|
|
270
|
+
.badge {
|
|
271
|
+
display: inline-block;
|
|
272
|
+
padding: 1px 6px;
|
|
273
|
+
border: 1px solid var(--accent-blue);
|
|
274
|
+
border-radius: 4px;
|
|
275
|
+
color: var(--accent-blue);
|
|
276
|
+
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
|
277
|
+
font-size: 0.8em;
|
|
278
|
+
background: transparent;
|
|
279
|
+
}
|
|
280
|
+
.badge.status-OK { border-color: var(--status-ok); color: var(--status-ok); }
|
|
281
|
+
.badge.status-ERR { border-color: var(--status-err); color: var(--status-err); }
|
|
282
|
+
.badge.status-PENDING { border-color: var(--status-pending); color: var(--status-pending); }
|
|
283
|
+
/* Sensitive content warning badge (Phase 12.x-c) */
|
|
284
|
+
.sensitive-badge {
|
|
285
|
+
display: inline-flex;
|
|
286
|
+
align-items: center;
|
|
287
|
+
gap: 4px;
|
|
288
|
+
padding: 2px 8px;
|
|
289
|
+
margin-left: 8px;
|
|
290
|
+
background: rgba(210, 153, 34, 0.15);
|
|
291
|
+
border: 1px solid rgba(210, 153, 34, 0.3);
|
|
292
|
+
border-radius: 12px;
|
|
293
|
+
font-size: 11px;
|
|
294
|
+
font-weight: 500;
|
|
295
|
+
color: #d29922;
|
|
296
|
+
vertical-align: middle;
|
|
297
|
+
}
|
|
298
|
+
/* Two-pane layout */
|
|
299
|
+
.container {
|
|
300
|
+
display: flex;
|
|
301
|
+
flex: 1;
|
|
302
|
+
overflow: hidden;
|
|
303
|
+
}
|
|
304
|
+
.left-pane {
|
|
305
|
+
width: var(--left-pane-width);
|
|
306
|
+
min-width: 300px;
|
|
307
|
+
max-width: 600px;
|
|
308
|
+
border-right: 1px solid var(--border-color);
|
|
309
|
+
display: flex;
|
|
310
|
+
flex-direction: column;
|
|
311
|
+
overflow: hidden;
|
|
312
|
+
}
|
|
313
|
+
.right-pane {
|
|
314
|
+
flex: 1;
|
|
315
|
+
display: flex;
|
|
316
|
+
flex-direction: column;
|
|
317
|
+
overflow: hidden;
|
|
318
|
+
padding: 16px;
|
|
319
|
+
min-height: 0;
|
|
320
|
+
}
|
|
321
|
+
.right-pane > .rpc-inspector {
|
|
322
|
+
flex: 1;
|
|
323
|
+
min-height: 0;
|
|
324
|
+
}
|
|
325
|
+
.session-info {
|
|
326
|
+
background: var(--bg-secondary);
|
|
327
|
+
padding: 12px;
|
|
328
|
+
border-bottom: 1px solid var(--border-color);
|
|
329
|
+
flex-shrink: 0;
|
|
330
|
+
}
|
|
331
|
+
.session-info dl {
|
|
332
|
+
display: grid;
|
|
333
|
+
grid-template-columns: auto 1fr;
|
|
334
|
+
gap: 4px 12px;
|
|
335
|
+
margin: 0;
|
|
336
|
+
font-size: 0.85em;
|
|
337
|
+
}
|
|
338
|
+
.session-info dt { color: var(--text-secondary); }
|
|
339
|
+
.session-info dd { margin: 0; }
|
|
340
|
+
.rpc-list {
|
|
341
|
+
flex: 1;
|
|
342
|
+
overflow-y: auto;
|
|
343
|
+
}
|
|
344
|
+
.rpc-table {
|
|
345
|
+
width: 100%;
|
|
346
|
+
border-collapse: collapse;
|
|
347
|
+
font-size: 0.85em;
|
|
348
|
+
}
|
|
349
|
+
.rpc-table th {
|
|
350
|
+
text-align: left;
|
|
351
|
+
color: var(--text-secondary);
|
|
352
|
+
border-bottom: 1px solid var(--border-color);
|
|
353
|
+
padding: 6px 8px;
|
|
354
|
+
font-weight: 500;
|
|
355
|
+
position: sticky;
|
|
356
|
+
top: 0;
|
|
357
|
+
background: var(--bg-primary);
|
|
358
|
+
z-index: 1;
|
|
359
|
+
}
|
|
360
|
+
.rpc-table td {
|
|
361
|
+
padding: 6px 8px;
|
|
362
|
+
border-bottom: 1px solid var(--border-color);
|
|
363
|
+
white-space: nowrap;
|
|
364
|
+
}
|
|
365
|
+
.rpc-row {
|
|
366
|
+
cursor: pointer;
|
|
367
|
+
}
|
|
368
|
+
.rpc-row:hover {
|
|
369
|
+
background: rgba(0, 212, 255, 0.1);
|
|
370
|
+
}
|
|
371
|
+
.rpc-row.selected {
|
|
372
|
+
background: rgba(0, 212, 255, 0.2);
|
|
373
|
+
}
|
|
374
|
+
/* Right pane detail */
|
|
375
|
+
.detail-placeholder {
|
|
376
|
+
color: var(--text-secondary);
|
|
377
|
+
text-align: center;
|
|
378
|
+
padding: 40px;
|
|
379
|
+
}
|
|
380
|
+
.detail-section {
|
|
381
|
+
background: var(--bg-secondary);
|
|
382
|
+
border-radius: 8px;
|
|
383
|
+
padding: 16px;
|
|
384
|
+
margin-bottom: 16px;
|
|
385
|
+
}
|
|
386
|
+
pre {
|
|
387
|
+
background: var(--bg-primary);
|
|
388
|
+
border: 1px solid var(--border-color);
|
|
389
|
+
border-radius: 6px;
|
|
390
|
+
padding: 12px;
|
|
391
|
+
overflow-x: auto;
|
|
392
|
+
margin: 8px 0;
|
|
393
|
+
font-size: 0.85em;
|
|
394
|
+
max-height: 400px;
|
|
395
|
+
overflow-y: auto;
|
|
396
|
+
}
|
|
397
|
+
code {
|
|
398
|
+
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
|
399
|
+
color: var(--text-primary);
|
|
400
|
+
}
|
|
401
|
+
.copy-btn {
|
|
402
|
+
background: var(--bg-secondary);
|
|
403
|
+
border: 1px solid var(--border-color);
|
|
404
|
+
color: var(--text-secondary);
|
|
405
|
+
padding: 3px 6px;
|
|
406
|
+
border-radius: 4px;
|
|
407
|
+
cursor: pointer;
|
|
408
|
+
font-size: 0.75em;
|
|
409
|
+
margin-left: 8px;
|
|
410
|
+
}
|
|
411
|
+
.copy-btn:hover {
|
|
412
|
+
border-color: var(--accent-blue);
|
|
413
|
+
color: var(--accent-blue);
|
|
414
|
+
}
|
|
415
|
+
.truncated-note {
|
|
416
|
+
color: var(--status-pending);
|
|
417
|
+
font-size: 0.8em;
|
|
418
|
+
margin: 4px 0;
|
|
419
|
+
}
|
|
420
|
+
.spill-link {
|
|
421
|
+
color: var(--link-color);
|
|
422
|
+
font-size: 0.8em;
|
|
423
|
+
}
|
|
424
|
+
/* Resize handle */
|
|
425
|
+
.resize-handle {
|
|
426
|
+
width: 4px;
|
|
427
|
+
background: var(--border-color);
|
|
428
|
+
cursor: col-resize;
|
|
429
|
+
transition: background 0.2s;
|
|
430
|
+
}
|
|
431
|
+
.resize-handle:hover {
|
|
432
|
+
background: var(--accent-blue);
|
|
433
|
+
}
|
|
434
|
+
/* RPC Inspector styles */
|
|
435
|
+
${getRpcInspectorStyles()}
|
|
436
|
+
`;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Get JavaScript for RPC report (copy button only)
|
|
441
|
+
*/
|
|
442
|
+
function getRpcReportScript(): string {
|
|
443
|
+
return `
|
|
444
|
+
// Copy button functionality
|
|
445
|
+
document.querySelectorAll('.copy-btn').forEach(btn => {
|
|
446
|
+
btn.addEventListener('click', async (e) => {
|
|
447
|
+
e.stopPropagation();
|
|
448
|
+
const targetId = btn.getAttribute('data-target');
|
|
449
|
+
const target = document.getElementById(targetId);
|
|
450
|
+
if (target) {
|
|
451
|
+
try {
|
|
452
|
+
await navigator.clipboard.writeText(target.textContent || '');
|
|
453
|
+
const originalText = btn.textContent;
|
|
454
|
+
btn.textContent = 'Copied!';
|
|
455
|
+
setTimeout(() => { btn.textContent = originalText; }, 1500);
|
|
456
|
+
} catch (err) {
|
|
457
|
+
console.error('Copy failed:', err);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
});
|
|
462
|
+
`;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Get JavaScript for Session report (2-pane with selection)
|
|
467
|
+
*/
|
|
468
|
+
function getSessionReportScript(): string {
|
|
469
|
+
return `
|
|
470
|
+
// Report data
|
|
471
|
+
const reportData = JSON.parse(document.getElementById('report-data').textContent);
|
|
472
|
+
const rpcs = reportData.rpcs;
|
|
473
|
+
let selectedIdx = null;
|
|
474
|
+
|
|
475
|
+
// Format JSON for display
|
|
476
|
+
function formatJson(data) {
|
|
477
|
+
if (data === null || data === undefined) return '(no data)';
|
|
478
|
+
try {
|
|
479
|
+
return JSON.stringify(data, null, 2);
|
|
480
|
+
} catch {
|
|
481
|
+
return String(data);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Escape HTML
|
|
486
|
+
function escapeHtml(text) {
|
|
487
|
+
const div = document.createElement('div');
|
|
488
|
+
div.textContent = text;
|
|
489
|
+
return div.innerHTML;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Format bytes
|
|
493
|
+
function formatBytes(bytes) {
|
|
494
|
+
if (bytes === 0) return '0 B';
|
|
495
|
+
const k = 1024;
|
|
496
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
497
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
498
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Render payload section
|
|
502
|
+
function renderPayload(title, payload, elementId) {
|
|
503
|
+
let content, notes = '';
|
|
504
|
+
|
|
505
|
+
if (payload.truncated) {
|
|
506
|
+
notes = '<p class="truncated-note">Payload truncated (' + formatBytes(payload.size) + ', showing first 4096 chars)</p>';
|
|
507
|
+
if (payload.spillFile) {
|
|
508
|
+
notes += '<p class="spill-link">Full payload: <a href="' + escapeHtml(payload.spillFile) + '">' + escapeHtml(payload.spillFile) + '</a></p>';
|
|
509
|
+
}
|
|
510
|
+
content = payload.preview ? escapeHtml(payload.preview) + '\\n... (truncated)' : '(no data)';
|
|
511
|
+
} else if (payload.json !== null) {
|
|
512
|
+
content = escapeHtml(formatJson(payload.json));
|
|
513
|
+
} else {
|
|
514
|
+
content = '(no data)';
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
return '<h3>' + title + ' <button class="copy-btn" onclick="copyToClipboard(\\'' + elementId + '\\', this)">Copy</button></h3>' +
|
|
518
|
+
notes +
|
|
519
|
+
'<pre id="' + elementId + '"><code>' + content + '</code></pre>';
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Show RPC detail in right pane (2-column Wireshark-style layout)
|
|
523
|
+
// Summary and Raw JSON now both toggle with Req/Res buttons
|
|
524
|
+
function showRpcDetail(idx) {
|
|
525
|
+
if (idx < 0 || idx >= rpcs.length) return;
|
|
526
|
+
|
|
527
|
+
const rpc = rpcs[idx];
|
|
528
|
+
const rightPane = document.getElementById('right-pane');
|
|
529
|
+
|
|
530
|
+
// Update selection state
|
|
531
|
+
document.querySelectorAll('.rpc-row').forEach((r, i) => {
|
|
532
|
+
r.classList.toggle('selected', i === idx);
|
|
533
|
+
});
|
|
534
|
+
selectedIdx = idx;
|
|
535
|
+
|
|
536
|
+
const statusClass = 'status-' + rpc.status;
|
|
537
|
+
const statusSymbol = rpc.status === 'OK' ? '✓' : rpc.status === 'ERR' ? '✗' : '?';
|
|
538
|
+
const latency = rpc.latency_ms !== null ? rpc.latency_ms + 'ms' : '(pending)';
|
|
539
|
+
|
|
540
|
+
// Get pre-rendered summary and raw JSON (separate request/response)
|
|
541
|
+
const requestSummaryHtml = rpc._requestSummaryHtml || '<div class="summary-row summary-header">No summary available</div>';
|
|
542
|
+
const responseSummaryHtml = rpc._responseSummaryHtml || '<div class="summary-row summary-header">No summary available</div>';
|
|
543
|
+
const requestRawHtml = rpc._requestRawHtml || '<span class="json-null">(no data)</span>';
|
|
544
|
+
const responseRawHtml = rpc._responseRawHtml || '<span class="json-null">(no data)</span>';
|
|
545
|
+
|
|
546
|
+
// Sensitive content warning badge (Phase 12.x-c)
|
|
547
|
+
// Escape keys to prevent XSS via malicious key names
|
|
548
|
+
const sensitiveKeys = (rpc._sensitiveKeys || []).map(function(k) { return escapeHtml(k); });
|
|
549
|
+
const sensitiveTooltip = sensitiveKeys.length > 5
|
|
550
|
+
? 'Contains ' + sensitiveKeys.length + ' sensitive keys: ' + sensitiveKeys.slice(0, 5).join(', ') + '...'
|
|
551
|
+
: 'Contains sensitive keys: ' + sensitiveKeys.join(', ');
|
|
552
|
+
const sensitiveBadge = rpc._hasSensitive
|
|
553
|
+
? '<span class="sensitive-badge" title="' + escapeHtml(sensitiveTooltip) + '">⚠ Sensitive</span>'
|
|
554
|
+
: '';
|
|
555
|
+
|
|
556
|
+
// Determine default target based on method (response-focused methods default to response)
|
|
557
|
+
const defaultTarget = (rpc.method === 'tools/list' || rpc.method === 'initialize' || rpc.method.startsWith('resources/') || rpc.method.startsWith('prompts/')) ? 'response' : 'request';
|
|
558
|
+
|
|
559
|
+
rightPane.innerHTML =
|
|
560
|
+
'<div class="detail-section">' +
|
|
561
|
+
' <h2>RPC Info' + sensitiveBadge + '</h2>' +
|
|
562
|
+
' <div class="rpc-info-grid">' +
|
|
563
|
+
' <div class="rpc-info-item"><dt>RPC ID</dt><dd><span class="badge">' + escapeHtml(rpc.rpc_id) + '</span></dd></div>' +
|
|
564
|
+
' <div class="rpc-info-item"><dt>Method</dt><dd><span class="badge">' + escapeHtml(rpc.method) + '</span></dd></div>' +
|
|
565
|
+
' <div class="rpc-info-item"><dt>Status</dt><dd><span class="badge ' + statusClass + '">' + statusSymbol + ' ' + rpc.status + (rpc.error_code !== null ? ' (code: ' + rpc.error_code + ')' : '') + '</span></dd></div>' +
|
|
566
|
+
' <div class="rpc-info-item"><dt>Latency</dt><dd><span class="badge">' + latency + '</span></dd></div>' +
|
|
567
|
+
' <div class="rpc-info-item"><dt>Request</dt><dd>' + escapeHtml(rpc.request_ts) + '</dd></div>' +
|
|
568
|
+
' <div class="rpc-info-item"><dt>Response</dt><dd>' + escapeHtml(rpc.response_ts || '-') + '</dd></div>' +
|
|
569
|
+
' </div>' +
|
|
570
|
+
'</div>' +
|
|
571
|
+
'<div class="detail-section">' +
|
|
572
|
+
' <div class="rpc-toggle-bar">' +
|
|
573
|
+
' <button id="toggle-req" class="rpc-toggle-btn' + (defaultTarget === 'request' ? ' active' : '') + '">[Req]</button>' +
|
|
574
|
+
' <button id="toggle-res" class="rpc-toggle-btn' + (defaultTarget === 'response' ? ' active' : '') + '">[Res]</button>' +
|
|
575
|
+
' </div>' +
|
|
576
|
+
' <div class="rpc-inspector">' +
|
|
577
|
+
' <div class="rpc-inspector-summary">' +
|
|
578
|
+
' <h3>Summary</h3>' +
|
|
579
|
+
' <div id="summary-request" style="display:' + (defaultTarget === 'request' ? 'block' : 'none') + '">' + requestSummaryHtml + '</div>' +
|
|
580
|
+
' <div id="summary-response" style="display:' + (defaultTarget === 'response' ? 'block' : 'none') + '">' + responseSummaryHtml + '</div>' +
|
|
581
|
+
' </div>' +
|
|
582
|
+
' <div class="rpc-inspector-raw">' +
|
|
583
|
+
' <div class="rpc-raw-json">' +
|
|
584
|
+
' <div id="raw-json-request" style="display:' + (defaultTarget === 'request' ? 'block' : 'none') + '">' + requestRawHtml + '</div>' +
|
|
585
|
+
' <div id="raw-json-response" style="display:' + (defaultTarget === 'response' ? 'block' : 'none') + '">' + responseRawHtml + '</div>' +
|
|
586
|
+
' </div>' +
|
|
587
|
+
' </div>' +
|
|
588
|
+
' </div>' +
|
|
589
|
+
'</div>';
|
|
590
|
+
|
|
591
|
+
// Re-initialize RPC Inspector handlers
|
|
592
|
+
if (window.initRpcInspector) {
|
|
593
|
+
window.initRpcInspector();
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Copy to clipboard
|
|
598
|
+
async function copyToClipboard(elementId, btn) {
|
|
599
|
+
const target = document.getElementById(elementId);
|
|
600
|
+
if (target) {
|
|
601
|
+
try {
|
|
602
|
+
await navigator.clipboard.writeText(target.textContent || '');
|
|
603
|
+
const originalText = btn.textContent;
|
|
604
|
+
btn.textContent = 'Copied!';
|
|
605
|
+
setTimeout(() => { btn.textContent = originalText; }, 1500);
|
|
606
|
+
} catch (err) {
|
|
607
|
+
console.error('Copy failed:', err);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// RPC row click handlers
|
|
613
|
+
document.querySelectorAll('.rpc-row').forEach((row, idx) => {
|
|
614
|
+
row.addEventListener('click', () => showRpcDetail(idx));
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
// Keyboard navigation
|
|
618
|
+
document.addEventListener('keydown', (e) => {
|
|
619
|
+
if (selectedIdx === null) return;
|
|
620
|
+
if (e.key === 'ArrowDown' && selectedIdx < rpcs.length - 1) {
|
|
621
|
+
e.preventDefault();
|
|
622
|
+
showRpcDetail(selectedIdx + 1);
|
|
623
|
+
// Scroll selected row into view
|
|
624
|
+
const row = document.querySelector('.rpc-row.selected');
|
|
625
|
+
if (row) row.scrollIntoView({ block: 'nearest' });
|
|
626
|
+
} else if (e.key === 'ArrowUp' && selectedIdx > 0) {
|
|
627
|
+
e.preventDefault();
|
|
628
|
+
showRpcDetail(selectedIdx - 1);
|
|
629
|
+
const row = document.querySelector('.rpc-row.selected');
|
|
630
|
+
if (row) row.scrollIntoView({ block: 'nearest' });
|
|
631
|
+
}
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
// Resize handle drag
|
|
635
|
+
const resizeHandle = document.querySelector('.resize-handle');
|
|
636
|
+
const leftPane = document.querySelector('.left-pane');
|
|
637
|
+
if (resizeHandle && leftPane) {
|
|
638
|
+
let startX, startWidth;
|
|
639
|
+
|
|
640
|
+
resizeHandle.addEventListener('mousedown', (e) => {
|
|
641
|
+
startX = e.clientX;
|
|
642
|
+
startWidth = leftPane.offsetWidth;
|
|
643
|
+
document.addEventListener('mousemove', onMouseMove);
|
|
644
|
+
document.addEventListener('mouseup', onMouseUp);
|
|
645
|
+
e.preventDefault();
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
function onMouseMove(e) {
|
|
649
|
+
const diff = e.clientX - startX;
|
|
650
|
+
const newWidth = Math.max(300, Math.min(600, startWidth + diff));
|
|
651
|
+
leftPane.style.width = newWidth + 'px';
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function onMouseUp() {
|
|
655
|
+
document.removeEventListener('mousemove', onMouseMove);
|
|
656
|
+
document.removeEventListener('mouseup', onMouseUp);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// Select first RPC by default if any exist
|
|
661
|
+
if (rpcs.length > 0) {
|
|
662
|
+
showRpcDetail(0);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// RPC Inspector script
|
|
666
|
+
${getRpcInspectorScript()}
|
|
667
|
+
`;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Render payload section (request or response)
|
|
672
|
+
*/
|
|
673
|
+
function renderPayloadSection(
|
|
674
|
+
title: string,
|
|
675
|
+
payload: PayloadData,
|
|
676
|
+
elementId: string
|
|
677
|
+
): string {
|
|
678
|
+
const copyBtn = `<button class="copy-btn" data-target="${elementId}">Copy</button>`;
|
|
679
|
+
|
|
680
|
+
let content: string;
|
|
681
|
+
let notes = '';
|
|
682
|
+
|
|
683
|
+
if (payload.truncated) {
|
|
684
|
+
notes = `<p class="truncated-note">Payload truncated (${formatBytes(payload.size)}, showing first 4096 chars)</p>`;
|
|
685
|
+
if (payload.spillFile) {
|
|
686
|
+
notes += `<p class="spill-link">Full payload: <a href="${escapeHtml(payload.spillFile)}">${escapeHtml(payload.spillFile)}</a></p>`;
|
|
687
|
+
}
|
|
688
|
+
content = payload.preview ? escapeHtml(payload.preview) + '\n... (truncated)' : '(no data)';
|
|
689
|
+
} else if (payload.json !== null) {
|
|
690
|
+
try {
|
|
691
|
+
content = escapeHtml(JSON.stringify(payload.json, null, 2));
|
|
692
|
+
} catch {
|
|
693
|
+
content = '(invalid JSON)';
|
|
694
|
+
}
|
|
695
|
+
} else {
|
|
696
|
+
content = '(no data)';
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
return `
|
|
700
|
+
<h3>${title} ${copyBtn}</h3>
|
|
701
|
+
${notes}
|
|
702
|
+
<pre id="${elementId}"><code>${content}</code></pre>
|
|
703
|
+
`;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* Generate RPC HTML report
|
|
708
|
+
*/
|
|
709
|
+
export function generateRpcHtml(report: HtmlRpcReportV1): string {
|
|
710
|
+
const { meta, rpc } = report;
|
|
711
|
+
const sessionShort = shortenId(rpc.session_id, 12);
|
|
712
|
+
const statusClass = `status-${rpc.status}`;
|
|
713
|
+
|
|
714
|
+
const embeddedJson = escapeJsonForScript(JSON.stringify(report));
|
|
715
|
+
|
|
716
|
+
return `<!DOCTYPE html>
|
|
717
|
+
<html lang="en">
|
|
718
|
+
<head>
|
|
719
|
+
<meta charset="UTF-8">
|
|
720
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
721
|
+
<title>RPC: ${escapeHtml(rpc.method)} - proofscan</title>
|
|
722
|
+
<style>${getRpcReportStyles()}</style>
|
|
723
|
+
</head>
|
|
724
|
+
<body>
|
|
725
|
+
<header>
|
|
726
|
+
<h1>RPC: <span class="badge">${escapeHtml(rpc.method)}</span></h1>
|
|
727
|
+
<p class="meta">Generated by ${escapeHtml(meta.generatedBy)} at ${formatTimestamp(meta.generatedAt)}${meta.redacted ? ' (redacted)' : ''}</p>
|
|
728
|
+
</header>
|
|
729
|
+
|
|
730
|
+
<main>
|
|
731
|
+
<section class="section">
|
|
732
|
+
<h2>Info</h2>
|
|
733
|
+
<dl>
|
|
734
|
+
<dt>RPC ID</dt>
|
|
735
|
+
<dd><span class="badge">${escapeHtml(rpc.rpc_id)}</span></dd>
|
|
736
|
+
<dt>Session</dt>
|
|
737
|
+
<dd><span class="badge">${escapeHtml(sessionShort)}...</span></dd>
|
|
738
|
+
<dt>Connector</dt>
|
|
739
|
+
<dd><span class="badge">${escapeHtml(rpc.target_id)}</span></dd>
|
|
740
|
+
<dt>Status</dt>
|
|
741
|
+
<dd><span class="badge ${statusClass}">${rpc.status}${rpc.error_code !== null ? ` (code: ${rpc.error_code})` : ''}</span></dd>
|
|
742
|
+
</dl>
|
|
743
|
+
</section>
|
|
744
|
+
|
|
745
|
+
<section class="section">
|
|
746
|
+
<h2>Timing</h2>
|
|
747
|
+
<dl>
|
|
748
|
+
<dt>Request</dt>
|
|
749
|
+
<dd>${formatTimestamp(rpc.request_ts)}</dd>
|
|
750
|
+
<dt>Response</dt>
|
|
751
|
+
<dd>${rpc.response_ts ? formatTimestamp(rpc.response_ts) : '(pending)'}</dd>
|
|
752
|
+
<dt>Latency</dt>
|
|
753
|
+
<dd>${rpc.latency_ms !== null ? `<span class="badge">${rpc.latency_ms}ms</span>` : '(pending)'}</dd>
|
|
754
|
+
</dl>
|
|
755
|
+
</section>
|
|
756
|
+
|
|
757
|
+
<section class="section">
|
|
758
|
+
<h2>Size</h2>
|
|
759
|
+
<dl>
|
|
760
|
+
<dt>Request</dt>
|
|
761
|
+
<dd>${formatBytes(rpc.request.size)}</dd>
|
|
762
|
+
<dt>Response</dt>
|
|
763
|
+
<dd>${formatBytes(rpc.response.size)}</dd>
|
|
764
|
+
</dl>
|
|
765
|
+
</section>
|
|
766
|
+
|
|
767
|
+
<section class="section">
|
|
768
|
+
<h2>Request</h2>
|
|
769
|
+
${renderPayloadSection('', rpc.request, 'request-json').replace('<h3>', '').replace('</h3>', '')}
|
|
770
|
+
</section>
|
|
771
|
+
|
|
772
|
+
<section class="section">
|
|
773
|
+
<h2>Response</h2>
|
|
774
|
+
${renderPayloadSection('', rpc.response, 'response-json').replace('<h3>', '').replace('</h3>', '')}
|
|
775
|
+
</section>
|
|
776
|
+
</main>
|
|
777
|
+
|
|
778
|
+
<script type="application/json" id="report-data">${embeddedJson}</script>
|
|
779
|
+
<script>${getRpcReportScript()}</script>
|
|
780
|
+
</body>
|
|
781
|
+
</html>`;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* Render a single RPC row for Session HTML (left pane table)
|
|
786
|
+
*/
|
|
787
|
+
function renderRpcRow(rpc: SessionRpcDetail, idx: number): string {
|
|
788
|
+
const statusClass = `status-${rpc.status}`;
|
|
789
|
+
const statusSymbol = getStatusSymbol(rpc.status);
|
|
790
|
+
const rpcIdShort = shortenId(rpc.rpc_id);
|
|
791
|
+
// Shorter time format for left pane (HH:MM:SS.mmm)
|
|
792
|
+
const timeShort = formatTimestamp(rpc.request_ts).split(' ')[1]?.slice(0, 12) || '-';
|
|
793
|
+
const latency = rpc.latency_ms !== null ? `${rpc.latency_ms}ms` : '-';
|
|
794
|
+
|
|
795
|
+
return `
|
|
796
|
+
<tr class="rpc-row">
|
|
797
|
+
<td>${timeShort}</td>
|
|
798
|
+
<td><span class="badge ${statusClass}">${statusSymbol}</span></td>
|
|
799
|
+
<td><span class="badge">${escapeHtml(rpcIdShort)}</span></td>
|
|
800
|
+
<td>${escapeHtml(rpc.method)}</td>
|
|
801
|
+
<td>${latency}</td>
|
|
802
|
+
</tr>`;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
/**
|
|
806
|
+
* Generate Session HTML report (2-pane Wireshark-style layout)
|
|
807
|
+
*/
|
|
808
|
+
export function generateSessionHtml(report: HtmlSessionReportV1): string {
|
|
809
|
+
const { meta, session, rpcs } = report;
|
|
810
|
+
const sessionShort = shortenId(session.session_id, 12);
|
|
811
|
+
|
|
812
|
+
const rpcRows = rpcs.map((rpc, idx) => renderRpcRow(rpc, idx)).join('\n');
|
|
813
|
+
|
|
814
|
+
// Pre-render summary and raw JSON HTML for each RPC (for RPC Inspector)
|
|
815
|
+
// Now generates separate request/response summaries for Req/Res toggle
|
|
816
|
+
// Also detect sensitive content for warning badge (Phase 12.x-c)
|
|
817
|
+
const rpcsWithInspectorHtml = rpcs.map((rpc) => {
|
|
818
|
+
const requestSummaryRows = renderRequestSummary(rpc.method, rpc.request.json);
|
|
819
|
+
const responseSummaryRows = renderResponseSummary(rpc.method, rpc.response.json);
|
|
820
|
+
// Detect sensitive keys in request/response
|
|
821
|
+
const reqSensitiveKeys = detectSensitiveKeys(rpc.request.json);
|
|
822
|
+
const resSensitiveKeys = detectSensitiveKeys(rpc.response.json);
|
|
823
|
+
const hasSensitive = reqSensitiveKeys.length > 0 || resSensitiveKeys.length > 0;
|
|
824
|
+
return {
|
|
825
|
+
...rpc,
|
|
826
|
+
_requestSummaryHtml: renderSummaryRowsHtml(requestSummaryRows),
|
|
827
|
+
_responseSummaryHtml: renderSummaryRowsHtml(responseSummaryRows),
|
|
828
|
+
_requestRawHtml: renderJsonWithPaths(rpc.request.json, '#'),
|
|
829
|
+
_responseRawHtml: renderJsonWithPaths(rpc.response.json, '#'),
|
|
830
|
+
_hasSensitive: hasSensitive,
|
|
831
|
+
_sensitiveKeys: [...reqSensitiveKeys, ...resSensitiveKeys],
|
|
832
|
+
};
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
const reportWithInspectorHtml = {
|
|
836
|
+
...report,
|
|
837
|
+
rpcs: rpcsWithInspectorHtml,
|
|
838
|
+
};
|
|
839
|
+
const embeddedJson = escapeJsonForScript(JSON.stringify(reportWithInspectorHtml));
|
|
840
|
+
|
|
841
|
+
// Format total latency
|
|
842
|
+
const totalLatencyDisplay = session.total_latency_ms !== null
|
|
843
|
+
? `${session.total_latency_ms}ms`
|
|
844
|
+
: '-';
|
|
845
|
+
|
|
846
|
+
return `<!DOCTYPE html>
|
|
847
|
+
<html lang="en">
|
|
848
|
+
<head>
|
|
849
|
+
<meta charset="UTF-8">
|
|
850
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
851
|
+
<title>Session: ${escapeHtml(sessionShort)}... - proofscan</title>
|
|
852
|
+
<style>${getSessionReportStyles()}</style>
|
|
853
|
+
</head>
|
|
854
|
+
<body>
|
|
855
|
+
<header>
|
|
856
|
+
<h1>Session: <span class="badge">${escapeHtml(sessionShort)}...</span></h1>
|
|
857
|
+
<p class="meta">Generated by ${escapeHtml(meta.generatedBy)} at ${formatTimestamp(meta.generatedAt)}${meta.redacted ? ' (redacted)' : ''}</p>
|
|
858
|
+
</header>
|
|
859
|
+
|
|
860
|
+
<div class="container">
|
|
861
|
+
<div class="left-pane">
|
|
862
|
+
<div class="session-info">
|
|
863
|
+
<h2>Session Info</h2>
|
|
864
|
+
<dl>
|
|
865
|
+
<dt>Session ID</dt>
|
|
866
|
+
<dd><span class="badge">${escapeHtml(session.session_id)}</span></dd>
|
|
867
|
+
<dt>Connector</dt>
|
|
868
|
+
<dd><span class="badge">${escapeHtml(session.target_id)}</span></dd>
|
|
869
|
+
<dt>Started</dt>
|
|
870
|
+
<dd>${formatTimestamp(session.started_at)}</dd>
|
|
871
|
+
<dt>Ended</dt>
|
|
872
|
+
<dd>${session.ended_at ? formatTimestamp(session.ended_at) : '(active)'}</dd>
|
|
873
|
+
<dt>RPC Count</dt>
|
|
874
|
+
<dd><span class="badge">${session.rpc_count}</span></dd>
|
|
875
|
+
<dt>Event Count</dt>
|
|
876
|
+
<dd><span class="badge">${session.event_count}</span></dd>
|
|
877
|
+
<dt>Total Latency</dt>
|
|
878
|
+
<dd><span class="badge">${totalLatencyDisplay}</span></dd>
|
|
879
|
+
</dl>
|
|
880
|
+
</div>
|
|
881
|
+
<div class="rpc-list">
|
|
882
|
+
<table class="rpc-table">
|
|
883
|
+
<thead>
|
|
884
|
+
<tr>
|
|
885
|
+
<th>Time</th>
|
|
886
|
+
<th>St</th>
|
|
887
|
+
<th>ID</th>
|
|
888
|
+
<th>Method</th>
|
|
889
|
+
<th>Latency</th>
|
|
890
|
+
</tr>
|
|
891
|
+
</thead>
|
|
892
|
+
<tbody>
|
|
893
|
+
${rpcRows}
|
|
894
|
+
</tbody>
|
|
895
|
+
</table>
|
|
896
|
+
</div>
|
|
897
|
+
</div>
|
|
898
|
+
<div class="resize-handle"></div>
|
|
899
|
+
<div class="right-pane" id="right-pane">
|
|
900
|
+
<div class="detail-placeholder">
|
|
901
|
+
${rpcs.length > 0 ? 'Select an RPC call from the list to view details' : 'No RPC calls in this session'}
|
|
902
|
+
</div>
|
|
903
|
+
</div>
|
|
904
|
+
</div>
|
|
905
|
+
|
|
906
|
+
<script type="application/json" id="report-data">${embeddedJson}</script>
|
|
907
|
+
<script>${getSessionReportScript()}</script>
|
|
908
|
+
</body>
|
|
909
|
+
</html>`;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// ============================================================================
|
|
913
|
+
// Connector HTML Report (Phase 5.1)
|
|
914
|
+
// ============================================================================
|
|
915
|
+
|
|
916
|
+
/**
|
|
917
|
+
* Get CSS styles for Connector HTML (3-hierarchy: Connector -> Sessions -> RPCs)
|
|
918
|
+
*/
|
|
919
|
+
function getConnectorReportStyles(): string {
|
|
920
|
+
return `
|
|
921
|
+
:root {
|
|
922
|
+
--bg-primary: #0d1117;
|
|
923
|
+
--bg-secondary: #161b22;
|
|
924
|
+
--bg-tertiary: #21262d;
|
|
925
|
+
--text-primary: #e6edf3;
|
|
926
|
+
--text-secondary: #8b949e;
|
|
927
|
+
--accent-blue: #00d4ff;
|
|
928
|
+
--status-ok: #3fb950;
|
|
929
|
+
--status-err: #f85149;
|
|
930
|
+
--status-pending: #d29922;
|
|
931
|
+
--border-color: #30363d;
|
|
932
|
+
--link-color: #58a6ff;
|
|
933
|
+
--sessions-pane-width: 360px;
|
|
934
|
+
--left-pane-width: 480px;
|
|
935
|
+
--raw-pane-max-width: 480px;
|
|
936
|
+
}
|
|
937
|
+
* { box-sizing: border-box; }
|
|
938
|
+
html, body {
|
|
939
|
+
height: 100%;
|
|
940
|
+
margin: 0;
|
|
941
|
+
padding: 0;
|
|
942
|
+
overflow: hidden;
|
|
943
|
+
}
|
|
944
|
+
body {
|
|
945
|
+
background: var(--bg-primary);
|
|
946
|
+
color: var(--text-primary);
|
|
947
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
|
|
948
|
+
line-height: 1.5;
|
|
949
|
+
display: flex;
|
|
950
|
+
flex-direction: column;
|
|
951
|
+
}
|
|
952
|
+
/* Unified header (matches Home/Ledger/Artifact pages) */
|
|
953
|
+
.header {
|
|
954
|
+
display: flex;
|
|
955
|
+
justify-content: space-between;
|
|
956
|
+
align-items: center;
|
|
957
|
+
padding: 12px 20px;
|
|
958
|
+
background: var(--bg-secondary);
|
|
959
|
+
border-bottom: 1px solid var(--border-color);
|
|
960
|
+
flex-shrink: 0;
|
|
961
|
+
}
|
|
962
|
+
.header-left {
|
|
963
|
+
display: flex;
|
|
964
|
+
align-items: center;
|
|
965
|
+
gap: 16px;
|
|
966
|
+
}
|
|
967
|
+
.header-title {
|
|
968
|
+
font-size: 18px;
|
|
969
|
+
font-weight: 600;
|
|
970
|
+
color: var(--text-primary);
|
|
971
|
+
}
|
|
972
|
+
.header-back {
|
|
973
|
+
color: var(--accent-blue);
|
|
974
|
+
font-size: 13px;
|
|
975
|
+
text-decoration: none;
|
|
976
|
+
}
|
|
977
|
+
.header-back:hover {
|
|
978
|
+
text-decoration: underline;
|
|
979
|
+
}
|
|
980
|
+
.header-meta {
|
|
981
|
+
display: flex;
|
|
982
|
+
align-items: center;
|
|
983
|
+
gap: 12px;
|
|
984
|
+
font-size: 12px;
|
|
985
|
+
color: var(--text-secondary);
|
|
986
|
+
}
|
|
987
|
+
.offline-badge {
|
|
988
|
+
display: inline-flex;
|
|
989
|
+
align-items: center;
|
|
990
|
+
gap: 4px;
|
|
991
|
+
padding: 2px 8px;
|
|
992
|
+
background: var(--bg-tertiary);
|
|
993
|
+
border: 1px solid var(--border-color);
|
|
994
|
+
border-radius: 12px;
|
|
995
|
+
font-size: 11px;
|
|
996
|
+
color: var(--text-secondary);
|
|
997
|
+
}
|
|
998
|
+
.offline-badge::before {
|
|
999
|
+
content: '';
|
|
1000
|
+
width: 6px;
|
|
1001
|
+
height: 6px;
|
|
1002
|
+
background: #6e7681;
|
|
1003
|
+
border-radius: 50%;
|
|
1004
|
+
}
|
|
1005
|
+
/* Auto-check toggle (Phase 12.1) */
|
|
1006
|
+
.auto-check-toggle {
|
|
1007
|
+
display: inline-flex;
|
|
1008
|
+
align-items: center;
|
|
1009
|
+
gap: 4px;
|
|
1010
|
+
background: var(--bg-tertiary);
|
|
1011
|
+
border: 1px solid var(--border-color);
|
|
1012
|
+
border-radius: 12px;
|
|
1013
|
+
padding: 2px 4px;
|
|
1014
|
+
}
|
|
1015
|
+
.auto-check-toggle .auto-check-label {
|
|
1016
|
+
font-size: 10px;
|
|
1017
|
+
color: var(--text-secondary);
|
|
1018
|
+
padding-left: 4px;
|
|
1019
|
+
}
|
|
1020
|
+
.auto-check-toggle button {
|
|
1021
|
+
background: transparent;
|
|
1022
|
+
border: none;
|
|
1023
|
+
padding: 2px 8px;
|
|
1024
|
+
border-radius: 10px;
|
|
1025
|
+
font-size: 11px;
|
|
1026
|
+
color: var(--text-secondary);
|
|
1027
|
+
cursor: pointer;
|
|
1028
|
+
transition: all 0.15s;
|
|
1029
|
+
}
|
|
1030
|
+
.auto-check-toggle button:hover {
|
|
1031
|
+
color: var(--text-primary);
|
|
1032
|
+
}
|
|
1033
|
+
.auto-check-toggle button.active {
|
|
1034
|
+
background: rgba(0, 212, 255, 0.15);
|
|
1035
|
+
color: var(--accent-blue);
|
|
1036
|
+
}
|
|
1037
|
+
/* New data banner */
|
|
1038
|
+
.new-data-banner {
|
|
1039
|
+
display: none;
|
|
1040
|
+
align-items: center;
|
|
1041
|
+
gap: 8px;
|
|
1042
|
+
padding: 4px 12px;
|
|
1043
|
+
background: rgba(0, 212, 255, 0.15);
|
|
1044
|
+
border: 1px solid var(--accent-blue);
|
|
1045
|
+
border-radius: 12px;
|
|
1046
|
+
font-size: 11px;
|
|
1047
|
+
color: var(--accent-blue);
|
|
1048
|
+
}
|
|
1049
|
+
.new-data-banner.active { display: inline-flex; }
|
|
1050
|
+
.new-data-banner button {
|
|
1051
|
+
background: var(--accent-blue);
|
|
1052
|
+
border: none;
|
|
1053
|
+
border-radius: 6px;
|
|
1054
|
+
padding: 2px 8px;
|
|
1055
|
+
font-size: 10px;
|
|
1056
|
+
color: var(--bg-primary);
|
|
1057
|
+
cursor: pointer;
|
|
1058
|
+
}
|
|
1059
|
+
/* Page header (Connector name + KPI row) */
|
|
1060
|
+
.page-header {
|
|
1061
|
+
padding: 8px 20px;
|
|
1062
|
+
border-bottom: 1px solid var(--border-color);
|
|
1063
|
+
flex-shrink: 0;
|
|
1064
|
+
display: flex;
|
|
1065
|
+
justify-content: space-between;
|
|
1066
|
+
align-items: center;
|
|
1067
|
+
}
|
|
1068
|
+
h1 {
|
|
1069
|
+
margin: 0;
|
|
1070
|
+
font-size: 1.3em;
|
|
1071
|
+
font-weight: 600;
|
|
1072
|
+
}
|
|
1073
|
+
h2 {
|
|
1074
|
+
margin: 0 0 8px 0;
|
|
1075
|
+
font-size: 1em;
|
|
1076
|
+
font-weight: 600;
|
|
1077
|
+
color: var(--text-primary);
|
|
1078
|
+
border-bottom: 1px solid var(--border-color);
|
|
1079
|
+
padding-bottom: 6px;
|
|
1080
|
+
}
|
|
1081
|
+
h3 {
|
|
1082
|
+
margin: 12px 0 6px 0;
|
|
1083
|
+
font-size: 0.9em;
|
|
1084
|
+
font-weight: 600;
|
|
1085
|
+
color: var(--text-secondary);
|
|
1086
|
+
}
|
|
1087
|
+
h3:first-child { margin-top: 0; }
|
|
1088
|
+
a { color: var(--link-color); text-decoration: none; }
|
|
1089
|
+
a:hover { text-decoration: underline; }
|
|
1090
|
+
.meta { color: var(--text-secondary); margin: 0; font-size: 0.8em; }
|
|
1091
|
+
.badge {
|
|
1092
|
+
display: inline-block;
|
|
1093
|
+
padding: 1px 6px;
|
|
1094
|
+
border: 1px solid var(--accent-blue);
|
|
1095
|
+
border-radius: 4px;
|
|
1096
|
+
color: var(--accent-blue);
|
|
1097
|
+
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
|
1098
|
+
font-size: 0.8em;
|
|
1099
|
+
background: transparent;
|
|
1100
|
+
}
|
|
1101
|
+
.badge.status-OK { border-color: var(--status-ok); color: var(--status-ok); }
|
|
1102
|
+
.badge.status-ERR { border-color: var(--status-err); color: var(--status-err); }
|
|
1103
|
+
.badge.status-PENDING { border-color: var(--status-pending); color: var(--status-pending); }
|
|
1104
|
+
.badge.cap-enabled { border-color: var(--accent-blue); color: var(--accent-blue); background: rgba(0, 212, 255, 0.1); }
|
|
1105
|
+
.badge.cap-disabled { border-color: var(--border-color); color: var(--text-secondary); background: transparent; opacity: 0.5; }
|
|
1106
|
+
/* Sensitive content warning badge (Phase 12.x-c) */
|
|
1107
|
+
.sensitive-badge {
|
|
1108
|
+
display: inline-flex;
|
|
1109
|
+
align-items: center;
|
|
1110
|
+
gap: 4px;
|
|
1111
|
+
padding: 2px 8px;
|
|
1112
|
+
margin-left: 8px;
|
|
1113
|
+
background: rgba(210, 153, 34, 0.15);
|
|
1114
|
+
border: 1px solid rgba(210, 153, 34, 0.3);
|
|
1115
|
+
border-radius: 12px;
|
|
1116
|
+
font-size: 11px;
|
|
1117
|
+
font-weight: 500;
|
|
1118
|
+
color: #d29922;
|
|
1119
|
+
vertical-align: middle;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
/* Connector info cards container (side by side) */
|
|
1123
|
+
.connector-info-cards {
|
|
1124
|
+
display: flex;
|
|
1125
|
+
gap: 0;
|
|
1126
|
+
}
|
|
1127
|
+
.connector-info-cards > .connector-info {
|
|
1128
|
+
flex: 1;
|
|
1129
|
+
border-right: 1px solid var(--border-color);
|
|
1130
|
+
}
|
|
1131
|
+
.connector-info-cards > .connector-info:last-child {
|
|
1132
|
+
border-right: none;
|
|
1133
|
+
padding-left: 24px;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
/* Connector info section (collapsible) */
|
|
1137
|
+
.connector-info {
|
|
1138
|
+
background: var(--bg-secondary);
|
|
1139
|
+
padding: 12px 20px;
|
|
1140
|
+
border-bottom: 1px solid var(--border-color);
|
|
1141
|
+
flex-shrink: 0;
|
|
1142
|
+
}
|
|
1143
|
+
.connector-info-toggle {
|
|
1144
|
+
display: flex;
|
|
1145
|
+
align-items: center;
|
|
1146
|
+
justify-content: space-between;
|
|
1147
|
+
cursor: pointer;
|
|
1148
|
+
user-select: none;
|
|
1149
|
+
}
|
|
1150
|
+
.connector-info-toggle h2 {
|
|
1151
|
+
margin: 0;
|
|
1152
|
+
border: none;
|
|
1153
|
+
padding: 0;
|
|
1154
|
+
}
|
|
1155
|
+
.connector-info-toggle .toggle-icon {
|
|
1156
|
+
color: var(--text-secondary);
|
|
1157
|
+
font-size: 0.85em;
|
|
1158
|
+
transition: transform 0.2s;
|
|
1159
|
+
}
|
|
1160
|
+
.connector-info-content {
|
|
1161
|
+
display: none;
|
|
1162
|
+
margin-top: 12px;
|
|
1163
|
+
}
|
|
1164
|
+
.connector-info.expanded .connector-info-content {
|
|
1165
|
+
display: block;
|
|
1166
|
+
}
|
|
1167
|
+
.connector-info.expanded .toggle-icon {
|
|
1168
|
+
transform: rotate(180deg);
|
|
1169
|
+
}
|
|
1170
|
+
.connector-info dl {
|
|
1171
|
+
display: grid;
|
|
1172
|
+
grid-template-columns: auto 1fr;
|
|
1173
|
+
gap: 4px 16px;
|
|
1174
|
+
margin: 0;
|
|
1175
|
+
font-size: 0.85em;
|
|
1176
|
+
}
|
|
1177
|
+
.connector-info dt { color: var(--text-secondary); }
|
|
1178
|
+
.connector-info dd { margin: 0; }
|
|
1179
|
+
.capabilities {
|
|
1180
|
+
display: flex;
|
|
1181
|
+
gap: 6px;
|
|
1182
|
+
flex-wrap: wrap;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
/* Main 3-pane container */
|
|
1186
|
+
.main-container {
|
|
1187
|
+
display: flex;
|
|
1188
|
+
flex: 1;
|
|
1189
|
+
overflow: hidden;
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
/* Sessions pane (leftmost) */
|
|
1193
|
+
.sessions-pane {
|
|
1194
|
+
width: var(--sessions-pane-width);
|
|
1195
|
+
flex-shrink: 0;
|
|
1196
|
+
border-right: 1px solid var(--border-color);
|
|
1197
|
+
display: flex;
|
|
1198
|
+
flex-direction: column;
|
|
1199
|
+
overflow: hidden;
|
|
1200
|
+
background: var(--bg-secondary);
|
|
1201
|
+
}
|
|
1202
|
+
.sessions-header {
|
|
1203
|
+
padding: 10px 12px;
|
|
1204
|
+
border-bottom: 1px solid var(--border-color);
|
|
1205
|
+
background: var(--bg-tertiary);
|
|
1206
|
+
flex-shrink: 0;
|
|
1207
|
+
}
|
|
1208
|
+
.sessions-header h2 {
|
|
1209
|
+
margin: 0;
|
|
1210
|
+
border: none;
|
|
1211
|
+
padding: 0;
|
|
1212
|
+
font-size: 0.9em;
|
|
1213
|
+
}
|
|
1214
|
+
.sessions-list {
|
|
1215
|
+
flex: 1;
|
|
1216
|
+
overflow-y: auto;
|
|
1217
|
+
padding: 4px 8px;
|
|
1218
|
+
}
|
|
1219
|
+
.sessions-header-row {
|
|
1220
|
+
display: grid;
|
|
1221
|
+
grid-template-columns: 70px 1fr 60px 50px;
|
|
1222
|
+
gap: 8px;
|
|
1223
|
+
padding: 4px 8px;
|
|
1224
|
+
font-size: 10px;
|
|
1225
|
+
color: var(--text-secondary);
|
|
1226
|
+
text-transform: uppercase;
|
|
1227
|
+
border-bottom: 1px solid var(--border-color);
|
|
1228
|
+
margin-bottom: 4px;
|
|
1229
|
+
}
|
|
1230
|
+
.session-item {
|
|
1231
|
+
display: grid;
|
|
1232
|
+
grid-template-columns: 70px 1fr auto 50px 50px;
|
|
1233
|
+
gap: 8px;
|
|
1234
|
+
align-items: center;
|
|
1235
|
+
padding: 6px 8px;
|
|
1236
|
+
border-radius: 4px;
|
|
1237
|
+
cursor: pointer;
|
|
1238
|
+
margin-bottom: 2px;
|
|
1239
|
+
border: 1px solid transparent;
|
|
1240
|
+
background: var(--bg-primary);
|
|
1241
|
+
font-size: 11px;
|
|
1242
|
+
}
|
|
1243
|
+
.session-item .session-counts {
|
|
1244
|
+
display: flex;
|
|
1245
|
+
gap: 6px;
|
|
1246
|
+
font-size: 10px;
|
|
1247
|
+
color: var(--text-secondary);
|
|
1248
|
+
}
|
|
1249
|
+
.session-item .session-counts span {
|
|
1250
|
+
white-space: nowrap;
|
|
1251
|
+
}
|
|
1252
|
+
.session-item .session-extra {
|
|
1253
|
+
justify-self: end;
|
|
1254
|
+
}
|
|
1255
|
+
.session-item:hover {
|
|
1256
|
+
background: rgba(0, 212, 255, 0.1);
|
|
1257
|
+
border-color: rgba(0, 212, 255, 0.3);
|
|
1258
|
+
}
|
|
1259
|
+
.session-item.selected {
|
|
1260
|
+
border-color: var(--accent-blue);
|
|
1261
|
+
background: rgba(0, 212, 255, 0.15);
|
|
1262
|
+
}
|
|
1263
|
+
.session-item.highlight {
|
|
1264
|
+
animation: highlightPulse 2s ease-out;
|
|
1265
|
+
}
|
|
1266
|
+
@keyframes highlightPulse {
|
|
1267
|
+
0% { box-shadow: 0 0 0 3px rgba(0, 212, 255, 0.6); }
|
|
1268
|
+
100% { box-shadow: 0 0 0 0 rgba(0, 212, 255, 0); }
|
|
1269
|
+
}
|
|
1270
|
+
.session-item .session-id {
|
|
1271
|
+
font-family: 'SFMono-Regular', Consolas, monospace;
|
|
1272
|
+
color: var(--accent-blue);
|
|
1273
|
+
overflow: hidden;
|
|
1274
|
+
text-overflow: ellipsis;
|
|
1275
|
+
white-space: nowrap;
|
|
1276
|
+
}
|
|
1277
|
+
.session-item .session-timestamp {
|
|
1278
|
+
color: var(--text-secondary);
|
|
1279
|
+
overflow: hidden;
|
|
1280
|
+
text-overflow: ellipsis;
|
|
1281
|
+
white-space: nowrap;
|
|
1282
|
+
}
|
|
1283
|
+
.session-item .session-latency {
|
|
1284
|
+
color: var(--text-secondary);
|
|
1285
|
+
text-align: right;
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
/* Session detail pane (middle) */
|
|
1289
|
+
.session-detail-pane {
|
|
1290
|
+
flex: 1;
|
|
1291
|
+
display: flex;
|
|
1292
|
+
flex-direction: column;
|
|
1293
|
+
overflow: hidden;
|
|
1294
|
+
min-width: 0;
|
|
1295
|
+
}
|
|
1296
|
+
.session-detail-empty {
|
|
1297
|
+
flex: 1;
|
|
1298
|
+
display: flex;
|
|
1299
|
+
align-items: center;
|
|
1300
|
+
justify-content: center;
|
|
1301
|
+
color: var(--text-secondary);
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
/* Re-use session HTML styles for the detail view */
|
|
1305
|
+
.session-content {
|
|
1306
|
+
display: none;
|
|
1307
|
+
flex-direction: column;
|
|
1308
|
+
height: 100%;
|
|
1309
|
+
overflow: hidden;
|
|
1310
|
+
}
|
|
1311
|
+
.session-content.active {
|
|
1312
|
+
display: flex;
|
|
1313
|
+
}
|
|
1314
|
+
.inner-container {
|
|
1315
|
+
display: flex;
|
|
1316
|
+
flex: 1;
|
|
1317
|
+
overflow: hidden;
|
|
1318
|
+
min-height: 0;
|
|
1319
|
+
}
|
|
1320
|
+
.left-pane {
|
|
1321
|
+
width: var(--left-pane-width);
|
|
1322
|
+
min-width: 300px;
|
|
1323
|
+
max-width: 600px;
|
|
1324
|
+
border-right: 1px solid var(--border-color);
|
|
1325
|
+
display: flex;
|
|
1326
|
+
flex-direction: column;
|
|
1327
|
+
overflow: hidden;
|
|
1328
|
+
}
|
|
1329
|
+
.right-pane {
|
|
1330
|
+
flex: 1;
|
|
1331
|
+
display: flex;
|
|
1332
|
+
flex-direction: column;
|
|
1333
|
+
overflow: hidden;
|
|
1334
|
+
padding: 16px;
|
|
1335
|
+
min-height: 0;
|
|
1336
|
+
}
|
|
1337
|
+
.right-pane > .rpc-inspector {
|
|
1338
|
+
flex: 1;
|
|
1339
|
+
min-height: 0;
|
|
1340
|
+
}
|
|
1341
|
+
.session-info {
|
|
1342
|
+
background: var(--bg-secondary);
|
|
1343
|
+
padding: 12px;
|
|
1344
|
+
border-bottom: 1px solid var(--border-color);
|
|
1345
|
+
flex-shrink: 0;
|
|
1346
|
+
}
|
|
1347
|
+
.session-info dl {
|
|
1348
|
+
display: grid;
|
|
1349
|
+
grid-template-columns: auto 1fr;
|
|
1350
|
+
gap: 4px 12px;
|
|
1351
|
+
margin: 0;
|
|
1352
|
+
font-size: 0.85em;
|
|
1353
|
+
}
|
|
1354
|
+
.session-info dt { color: var(--text-secondary); }
|
|
1355
|
+
.session-info dd { margin: 0; }
|
|
1356
|
+
.rpc-list {
|
|
1357
|
+
flex: 1;
|
|
1358
|
+
overflow-y: auto;
|
|
1359
|
+
}
|
|
1360
|
+
.rpc-table {
|
|
1361
|
+
width: 100%;
|
|
1362
|
+
border-collapse: collapse;
|
|
1363
|
+
font-size: 0.85em;
|
|
1364
|
+
}
|
|
1365
|
+
.rpc-table th {
|
|
1366
|
+
text-align: left;
|
|
1367
|
+
color: var(--text-secondary);
|
|
1368
|
+
border-bottom: 1px solid var(--border-color);
|
|
1369
|
+
padding: 6px 8px;
|
|
1370
|
+
font-weight: 500;
|
|
1371
|
+
position: sticky;
|
|
1372
|
+
top: 0;
|
|
1373
|
+
background: var(--bg-primary);
|
|
1374
|
+
z-index: 1;
|
|
1375
|
+
}
|
|
1376
|
+
.rpc-table td {
|
|
1377
|
+
padding: 6px 8px;
|
|
1378
|
+
border-bottom: 1px solid var(--border-color);
|
|
1379
|
+
white-space: nowrap;
|
|
1380
|
+
}
|
|
1381
|
+
.rpc-row {
|
|
1382
|
+
cursor: pointer;
|
|
1383
|
+
}
|
|
1384
|
+
.rpc-row:hover {
|
|
1385
|
+
background: rgba(0, 212, 255, 0.1);
|
|
1386
|
+
}
|
|
1387
|
+
.rpc-row.selected {
|
|
1388
|
+
background: rgba(0, 212, 255, 0.2);
|
|
1389
|
+
}
|
|
1390
|
+
.detail-placeholder {
|
|
1391
|
+
color: var(--text-secondary);
|
|
1392
|
+
text-align: center;
|
|
1393
|
+
padding: 40px;
|
|
1394
|
+
}
|
|
1395
|
+
.detail-section {
|
|
1396
|
+
background: var(--bg-secondary);
|
|
1397
|
+
border-radius: 8px;
|
|
1398
|
+
padding: 16px;
|
|
1399
|
+
margin-bottom: 16px;
|
|
1400
|
+
}
|
|
1401
|
+
pre {
|
|
1402
|
+
background: var(--bg-primary);
|
|
1403
|
+
border: 1px solid var(--border-color);
|
|
1404
|
+
border-radius: 6px;
|
|
1405
|
+
padding: 12px;
|
|
1406
|
+
overflow-x: auto;
|
|
1407
|
+
margin: 8px 0;
|
|
1408
|
+
font-size: 0.85em;
|
|
1409
|
+
max-height: 400px;
|
|
1410
|
+
overflow-y: auto;
|
|
1411
|
+
}
|
|
1412
|
+
code {
|
|
1413
|
+
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
|
1414
|
+
color: var(--text-primary);
|
|
1415
|
+
}
|
|
1416
|
+
.copy-btn {
|
|
1417
|
+
background: var(--bg-secondary);
|
|
1418
|
+
border: 1px solid var(--border-color);
|
|
1419
|
+
color: var(--text-secondary);
|
|
1420
|
+
padding: 3px 6px;
|
|
1421
|
+
border-radius: 4px;
|
|
1422
|
+
cursor: pointer;
|
|
1423
|
+
font-size: 0.75em;
|
|
1424
|
+
margin-left: 8px;
|
|
1425
|
+
}
|
|
1426
|
+
.copy-btn:hover {
|
|
1427
|
+
border-color: var(--accent-blue);
|
|
1428
|
+
color: var(--accent-blue);
|
|
1429
|
+
}
|
|
1430
|
+
.truncated-note {
|
|
1431
|
+
color: var(--status-pending);
|
|
1432
|
+
font-size: 0.8em;
|
|
1433
|
+
margin: 4px 0;
|
|
1434
|
+
}
|
|
1435
|
+
.spill-link {
|
|
1436
|
+
color: var(--link-color);
|
|
1437
|
+
font-size: 0.8em;
|
|
1438
|
+
}
|
|
1439
|
+
.resize-handle {
|
|
1440
|
+
width: 4px;
|
|
1441
|
+
background: var(--border-color);
|
|
1442
|
+
cursor: col-resize;
|
|
1443
|
+
transition: background 0.2s;
|
|
1444
|
+
}
|
|
1445
|
+
.resize-handle:hover {
|
|
1446
|
+
background: var(--accent-blue);
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
/* Events View Toggle (Issue #59) */
|
|
1450
|
+
.view-toggle {
|
|
1451
|
+
display: flex;
|
|
1452
|
+
gap: 2px;
|
|
1453
|
+
padding: 8px 12px;
|
|
1454
|
+
border-bottom: 1px solid var(--border-color);
|
|
1455
|
+
background: var(--bg-secondary);
|
|
1456
|
+
}
|
|
1457
|
+
.view-toggle-btn {
|
|
1458
|
+
background: transparent;
|
|
1459
|
+
border: 1px solid var(--border-color);
|
|
1460
|
+
color: var(--text-secondary);
|
|
1461
|
+
padding: 4px 12px;
|
|
1462
|
+
border-radius: 4px;
|
|
1463
|
+
cursor: pointer;
|
|
1464
|
+
font-size: 11px;
|
|
1465
|
+
transition: all 0.15s;
|
|
1466
|
+
}
|
|
1467
|
+
.view-toggle-btn:first-child {
|
|
1468
|
+
border-radius: 4px 0 0 4px;
|
|
1469
|
+
}
|
|
1470
|
+
.view-toggle-btn:last-child {
|
|
1471
|
+
border-radius: 0 4px 4px 0;
|
|
1472
|
+
}
|
|
1473
|
+
.view-toggle-btn:hover {
|
|
1474
|
+
border-color: var(--accent-blue);
|
|
1475
|
+
color: var(--text-primary);
|
|
1476
|
+
}
|
|
1477
|
+
.view-toggle-btn.active {
|
|
1478
|
+
background: rgba(0, 212, 255, 0.15);
|
|
1479
|
+
border-color: var(--accent-blue);
|
|
1480
|
+
color: var(--accent-blue);
|
|
1481
|
+
}
|
|
1482
|
+
.view-toggle-count {
|
|
1483
|
+
font-size: 10px;
|
|
1484
|
+
color: var(--text-secondary);
|
|
1485
|
+
margin-left: 4px;
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
/* Events List (Issue #59) */
|
|
1489
|
+
.events-list {
|
|
1490
|
+
flex: 1;
|
|
1491
|
+
overflow-y: auto;
|
|
1492
|
+
display: none;
|
|
1493
|
+
}
|
|
1494
|
+
.events-list.active {
|
|
1495
|
+
display: block;
|
|
1496
|
+
}
|
|
1497
|
+
.rpc-list.hidden {
|
|
1498
|
+
display: none;
|
|
1499
|
+
}
|
|
1500
|
+
.events-table {
|
|
1501
|
+
width: 100%;
|
|
1502
|
+
border-collapse: collapse;
|
|
1503
|
+
font-size: 0.85em;
|
|
1504
|
+
}
|
|
1505
|
+
.events-table th {
|
|
1506
|
+
text-align: left;
|
|
1507
|
+
color: var(--text-secondary);
|
|
1508
|
+
border-bottom: 1px solid var(--border-color);
|
|
1509
|
+
padding: 6px 8px;
|
|
1510
|
+
font-weight: 500;
|
|
1511
|
+
position: sticky;
|
|
1512
|
+
top: 0;
|
|
1513
|
+
background: var(--bg-primary);
|
|
1514
|
+
z-index: 1;
|
|
1515
|
+
}
|
|
1516
|
+
.events-table td {
|
|
1517
|
+
padding: 6px 8px;
|
|
1518
|
+
border-bottom: 1px solid var(--border-color);
|
|
1519
|
+
white-space: nowrap;
|
|
1520
|
+
}
|
|
1521
|
+
.event-row {
|
|
1522
|
+
cursor: pointer;
|
|
1523
|
+
}
|
|
1524
|
+
.event-row:hover {
|
|
1525
|
+
background: rgba(0, 212, 255, 0.1);
|
|
1526
|
+
}
|
|
1527
|
+
.event-row.selected {
|
|
1528
|
+
background: rgba(0, 212, 255, 0.2);
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
/* Event kind badges */
|
|
1532
|
+
.badge-kind-request {
|
|
1533
|
+
background: rgba(0, 212, 255, 0.15);
|
|
1534
|
+
color: var(--accent-blue);
|
|
1535
|
+
border: 1px solid rgba(0, 212, 255, 0.3);
|
|
1536
|
+
}
|
|
1537
|
+
.badge-kind-response {
|
|
1538
|
+
background: rgba(63, 185, 80, 0.15);
|
|
1539
|
+
color: var(--accent-green);
|
|
1540
|
+
border: 1px solid rgba(63, 185, 80, 0.3);
|
|
1541
|
+
}
|
|
1542
|
+
.badge-kind-notification {
|
|
1543
|
+
background: rgba(210, 153, 34, 0.15);
|
|
1544
|
+
color: var(--accent-yellow);
|
|
1545
|
+
border: 1px solid rgba(210, 153, 34, 0.3);
|
|
1546
|
+
}
|
|
1547
|
+
.badge-kind-transport_event {
|
|
1548
|
+
background: var(--bg-tertiary);
|
|
1549
|
+
color: var(--text-secondary);
|
|
1550
|
+
border: 1px solid var(--border-color);
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
/* Event direction arrows */
|
|
1554
|
+
.direction-arrow {
|
|
1555
|
+
font-size: 18px;
|
|
1556
|
+
font-weight: bold;
|
|
1557
|
+
line-height: 1;
|
|
1558
|
+
cursor: help;
|
|
1559
|
+
}
|
|
1560
|
+
.direction-arrow.outgoing {
|
|
1561
|
+
color: var(--accent-blue);
|
|
1562
|
+
}
|
|
1563
|
+
.direction-arrow.incoming {
|
|
1564
|
+
color: var(--accent-green);
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
/* Events loading state */
|
|
1568
|
+
.events-loading {
|
|
1569
|
+
padding: 24px;
|
|
1570
|
+
text-align: center;
|
|
1571
|
+
color: var(--text-secondary);
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
/* Analytics Panel (Phase 5.2) - Revised Layout */
|
|
1575
|
+
|
|
1576
|
+
/* Header with KPI stats inline */
|
|
1577
|
+
header {
|
|
1578
|
+
display: flex;
|
|
1579
|
+
align-items: center;
|
|
1580
|
+
justify-content: space-between;
|
|
1581
|
+
flex-wrap: wrap;
|
|
1582
|
+
gap: 12px;
|
|
1583
|
+
}
|
|
1584
|
+
.page-header-left {
|
|
1585
|
+
flex-shrink: 0;
|
|
1586
|
+
}
|
|
1587
|
+
/* Capability badges - active (blue) / inactive (gray) */
|
|
1588
|
+
.badge.cap-disabled {
|
|
1589
|
+
border-color: var(--border-color);
|
|
1590
|
+
color: var(--text-secondary);
|
|
1591
|
+
background: transparent;
|
|
1592
|
+
opacity: 0.5;
|
|
1593
|
+
}
|
|
1594
|
+
.kpi-row {
|
|
1595
|
+
display: flex;
|
|
1596
|
+
gap: 16px;
|
|
1597
|
+
flex-wrap: wrap;
|
|
1598
|
+
align-items: baseline;
|
|
1599
|
+
}
|
|
1600
|
+
.kpi-item {
|
|
1601
|
+
display: flex;
|
|
1602
|
+
flex-direction: column;
|
|
1603
|
+
align-items: center;
|
|
1604
|
+
padding: 0;
|
|
1605
|
+
background: transparent;
|
|
1606
|
+
min-width: 50px;
|
|
1607
|
+
}
|
|
1608
|
+
.kpi-item .kpi-value {
|
|
1609
|
+
font-size: 0.95em;
|
|
1610
|
+
font-weight: 600;
|
|
1611
|
+
color: var(--accent-blue);
|
|
1612
|
+
font-family: 'SFMono-Regular', Consolas, monospace;
|
|
1613
|
+
line-height: 1.2;
|
|
1614
|
+
}
|
|
1615
|
+
.kpi-item .kpi-label {
|
|
1616
|
+
font-size: 0.55em;
|
|
1617
|
+
color: var(--text-secondary);
|
|
1618
|
+
text-transform: uppercase;
|
|
1619
|
+
letter-spacing: 0.5px;
|
|
1620
|
+
}
|
|
1621
|
+
/* All KPI values use accent-blue for unified appearance */
|
|
1622
|
+
|
|
1623
|
+
/* Connector top section: info + charts row */
|
|
1624
|
+
.connector-top {
|
|
1625
|
+
display: flex;
|
|
1626
|
+
gap: 16px;
|
|
1627
|
+
padding: 12px 20px;
|
|
1628
|
+
border-bottom: 1px solid var(--border-color);
|
|
1629
|
+
background: var(--bg-secondary);
|
|
1630
|
+
}
|
|
1631
|
+
.connector-top .connector-info {
|
|
1632
|
+
flex: 0 0 360px;
|
|
1633
|
+
max-width: 360px;
|
|
1634
|
+
border-bottom: none;
|
|
1635
|
+
padding: 0;
|
|
1636
|
+
}
|
|
1637
|
+
.analytics-panel {
|
|
1638
|
+
flex: 1;
|
|
1639
|
+
display: flex;
|
|
1640
|
+
gap: 12px;
|
|
1641
|
+
align-items: stretch;
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
/* Charts row - 4 items horizontal with custom flex ratios */
|
|
1645
|
+
.heatmap-container {
|
|
1646
|
+
flex: 0.8;
|
|
1647
|
+
background: var(--bg-primary);
|
|
1648
|
+
border: 1px solid var(--border-color);
|
|
1649
|
+
border-radius: 6px;
|
|
1650
|
+
padding: 8px;
|
|
1651
|
+
min-width: 0;
|
|
1652
|
+
}
|
|
1653
|
+
.latency-histogram {
|
|
1654
|
+
flex: 1.4;
|
|
1655
|
+
background: var(--bg-primary);
|
|
1656
|
+
border: 1px solid var(--border-color);
|
|
1657
|
+
border-radius: 6px;
|
|
1658
|
+
padding: 8px;
|
|
1659
|
+
min-width: 0;
|
|
1660
|
+
}
|
|
1661
|
+
.top-tools, .method-distribution {
|
|
1662
|
+
flex: 1;
|
|
1663
|
+
background: var(--bg-primary);
|
|
1664
|
+
border: 1px solid var(--border-color);
|
|
1665
|
+
border-radius: 6px;
|
|
1666
|
+
padding: 8px;
|
|
1667
|
+
min-width: 0;
|
|
1668
|
+
}
|
|
1669
|
+
.chart-title {
|
|
1670
|
+
font-size: 0.75em;
|
|
1671
|
+
color: var(--text-secondary);
|
|
1672
|
+
margin-bottom: 4px;
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
/* Heatmap - using neon blue gradient for consistency with theme */
|
|
1676
|
+
.heatmap-title {
|
|
1677
|
+
font-size: 0.75em;
|
|
1678
|
+
color: var(--text-secondary);
|
|
1679
|
+
margin-bottom: 4px;
|
|
1680
|
+
line-height: 1.4;
|
|
1681
|
+
}
|
|
1682
|
+
.heatmap-range {
|
|
1683
|
+
font-size: 0.9em;
|
|
1684
|
+
opacity: 0.8;
|
|
1685
|
+
}
|
|
1686
|
+
.heatmap-level-0 { fill: var(--bg-tertiary); }
|
|
1687
|
+
.heatmap-level-1 { fill: #0a3d4d; }
|
|
1688
|
+
.heatmap-level-2 { fill: #0d5c73; }
|
|
1689
|
+
.heatmap-level-3 { fill: #0097b2; }
|
|
1690
|
+
.heatmap-level-4 { fill: #00d4ff; }
|
|
1691
|
+
|
|
1692
|
+
/* Histogram / Method Latency Heatmap */
|
|
1693
|
+
.histogram-bar { fill: var(--accent-blue); }
|
|
1694
|
+
.histogram-label { fill: var(--text-secondary); font-size: 9px; }
|
|
1695
|
+
.latency-heatmap { font-size: 0.75em; }
|
|
1696
|
+
.latency-heatmap-header, .latency-heatmap-row {
|
|
1697
|
+
display: flex;
|
|
1698
|
+
align-items: center;
|
|
1699
|
+
gap: 4px;
|
|
1700
|
+
margin-bottom: 3px;
|
|
1701
|
+
}
|
|
1702
|
+
.latency-heatmap-header { color: var(--text-secondary); margin-bottom: 6px; }
|
|
1703
|
+
.latency-heatmap-method { width: 28px; flex-shrink: 0; text-align: right; color: var(--text-secondary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 10px; }
|
|
1704
|
+
.latency-heatmap-cells { display: flex; gap: 1px; }
|
|
1705
|
+
.latency-heatmap-cell { width: 30px; height: 12px; border-radius: 2px; cursor: default; }
|
|
1706
|
+
.latency-heatmap-cell.heatmap-level-0 { background: #161b22; }
|
|
1707
|
+
.latency-heatmap-cell.heatmap-level-1 { background: #0e4429; }
|
|
1708
|
+
.latency-heatmap-cell.heatmap-level-2 { background: #006d32; }
|
|
1709
|
+
.latency-heatmap-cell.heatmap-level-3 { background: #0097b2; }
|
|
1710
|
+
.latency-heatmap-cell.heatmap-level-4 { background: #00d4ff; }
|
|
1711
|
+
.latency-heatmap-header-cell { width: 30px; font-size: 8px; text-align: center; color: var(--text-secondary); white-space: nowrap; }
|
|
1712
|
+
|
|
1713
|
+
/* Top Tools */
|
|
1714
|
+
.top-tool-row {
|
|
1715
|
+
display: flex;
|
|
1716
|
+
align-items: center;
|
|
1717
|
+
gap: 6px;
|
|
1718
|
+
margin-bottom: 3px;
|
|
1719
|
+
font-size: 0.75em;
|
|
1720
|
+
}
|
|
1721
|
+
.top-tool-rank {
|
|
1722
|
+
color: var(--text-secondary);
|
|
1723
|
+
width: 14px;
|
|
1724
|
+
flex-shrink: 0;
|
|
1725
|
+
}
|
|
1726
|
+
.top-tool-name {
|
|
1727
|
+
flex: 1;
|
|
1728
|
+
min-width: 0;
|
|
1729
|
+
overflow: hidden;
|
|
1730
|
+
text-overflow: ellipsis;
|
|
1731
|
+
white-space: nowrap;
|
|
1732
|
+
font-family: 'SFMono-Regular', Consolas, monospace;
|
|
1733
|
+
}
|
|
1734
|
+
.top-tool-bar-container {
|
|
1735
|
+
width: 50px;
|
|
1736
|
+
height: 6px;
|
|
1737
|
+
background: var(--bg-tertiary);
|
|
1738
|
+
border-radius: 3px;
|
|
1739
|
+
overflow: hidden;
|
|
1740
|
+
flex-shrink: 0;
|
|
1741
|
+
}
|
|
1742
|
+
.top-tool-bar {
|
|
1743
|
+
height: 100%;
|
|
1744
|
+
background: var(--accent-blue);
|
|
1745
|
+
border-radius: 3px;
|
|
1746
|
+
}
|
|
1747
|
+
.top-tool-pct {
|
|
1748
|
+
color: var(--text-secondary);
|
|
1749
|
+
width: 28px;
|
|
1750
|
+
text-align: right;
|
|
1751
|
+
flex-shrink: 0;
|
|
1752
|
+
}
|
|
1753
|
+
.no-data-message {
|
|
1754
|
+
color: var(--text-secondary);
|
|
1755
|
+
font-size: 0.75em;
|
|
1756
|
+
text-align: center;
|
|
1757
|
+
padding: 8px;
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
/* Method Distribution Donut Chart */
|
|
1761
|
+
.donut-container {
|
|
1762
|
+
display: flex;
|
|
1763
|
+
align-items: center;
|
|
1764
|
+
gap: 8px;
|
|
1765
|
+
}
|
|
1766
|
+
.donut-legend {
|
|
1767
|
+
flex: 1;
|
|
1768
|
+
font-size: 0.7em;
|
|
1769
|
+
min-width: 0;
|
|
1770
|
+
}
|
|
1771
|
+
.donut-legend-item {
|
|
1772
|
+
display: flex;
|
|
1773
|
+
align-items: center;
|
|
1774
|
+
gap: 4px;
|
|
1775
|
+
margin-bottom: 2px;
|
|
1776
|
+
white-space: nowrap;
|
|
1777
|
+
overflow: hidden;
|
|
1778
|
+
}
|
|
1779
|
+
.donut-legend-color {
|
|
1780
|
+
width: 8px;
|
|
1781
|
+
height: 8px;
|
|
1782
|
+
border-radius: 2px;
|
|
1783
|
+
flex-shrink: 0;
|
|
1784
|
+
}
|
|
1785
|
+
.donut-legend-label {
|
|
1786
|
+
overflow: hidden;
|
|
1787
|
+
text-overflow: ellipsis;
|
|
1788
|
+
flex: 1;
|
|
1789
|
+
min-width: 0;
|
|
1790
|
+
}
|
|
1791
|
+
.donut-legend-pct {
|
|
1792
|
+
color: var(--text-secondary);
|
|
1793
|
+
flex-shrink: 0;
|
|
1794
|
+
}
|
|
1795
|
+
/* RPC Inspector styles */
|
|
1796
|
+
${getRpcInspectorStyles()}
|
|
1797
|
+
`;
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
/**
|
|
1801
|
+
* Get JavaScript for Connector HTML (3-hierarchy navigation)
|
|
1802
|
+
*/
|
|
1803
|
+
function getConnectorReportScript(): string {
|
|
1804
|
+
return `
|
|
1805
|
+
// Auto-check functionality (Phase 12.1)
|
|
1806
|
+
(function() {
|
|
1807
|
+
// Only run on monitor pages (not static HTML export)
|
|
1808
|
+
if (!document.body.dataset.app || document.body.dataset.app !== 'monitor') {
|
|
1809
|
+
return;
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
let checkInterval = null;
|
|
1813
|
+
let lastDigest = null;
|
|
1814
|
+
let newDataDetected = false;
|
|
1815
|
+
const INTERVAL_MS = 10000;
|
|
1816
|
+
|
|
1817
|
+
const toggle = document.getElementById('autoCheckToggle');
|
|
1818
|
+
const banner = document.getElementById('newDataBanner');
|
|
1819
|
+
const refreshBtn = document.getElementById('refreshNowBtn');
|
|
1820
|
+
if (!toggle) return;
|
|
1821
|
+
|
|
1822
|
+
const buttons = toggle.querySelectorAll('button');
|
|
1823
|
+
const enabled = localStorage.getItem('proofscan-auto-check') === 'true';
|
|
1824
|
+
|
|
1825
|
+
// Initial state
|
|
1826
|
+
buttons.forEach(function(btn) {
|
|
1827
|
+
btn.classList.toggle('active', (btn.dataset.enabled === 'true') === enabled);
|
|
1828
|
+
});
|
|
1829
|
+
if (enabled) startChecking();
|
|
1830
|
+
|
|
1831
|
+
function startChecking() {
|
|
1832
|
+
checkForUpdates(); // First check
|
|
1833
|
+
checkInterval = setInterval(checkForUpdates, INTERVAL_MS);
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
function stopChecking() {
|
|
1837
|
+
if (checkInterval) clearInterval(checkInterval);
|
|
1838
|
+
checkInterval = null;
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
function checkForUpdates() {
|
|
1842
|
+
if (newDataDetected) return; // Banner already shown
|
|
1843
|
+
fetch('/api/monitor/summary')
|
|
1844
|
+
.then(function(res) { return res.ok ? res.json() : null; })
|
|
1845
|
+
.then(function(data) {
|
|
1846
|
+
if (!data) return;
|
|
1847
|
+
if (lastDigest === null) {
|
|
1848
|
+
lastDigest = data.digest; // Baseline
|
|
1849
|
+
} else if (data.digest !== lastDigest) {
|
|
1850
|
+
newDataDetected = true;
|
|
1851
|
+
if (banner) banner.classList.add('active');
|
|
1852
|
+
}
|
|
1853
|
+
})
|
|
1854
|
+
.catch(function(err) { console.debug('[Auto-check] Poll failed:', err); });
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
buttons.forEach(function(btn) {
|
|
1858
|
+
btn.addEventListener('click', function() {
|
|
1859
|
+
var on = btn.dataset.enabled === 'true';
|
|
1860
|
+
localStorage.setItem('proofscan-auto-check', String(on));
|
|
1861
|
+
buttons.forEach(function(b) {
|
|
1862
|
+
b.classList.toggle('active', (b.dataset.enabled === 'true') === on);
|
|
1863
|
+
});
|
|
1864
|
+
if (on) {
|
|
1865
|
+
startChecking();
|
|
1866
|
+
} else {
|
|
1867
|
+
stopChecking();
|
|
1868
|
+
if (banner) banner.classList.remove('active');
|
|
1869
|
+
newDataDetected = false;
|
|
1870
|
+
}
|
|
1871
|
+
});
|
|
1872
|
+
});
|
|
1873
|
+
|
|
1874
|
+
if (refreshBtn) {
|
|
1875
|
+
refreshBtn.addEventListener('click', function() { location.reload(); });
|
|
1876
|
+
}
|
|
1877
|
+
})();
|
|
1878
|
+
|
|
1879
|
+
// Report data
|
|
1880
|
+
const reportData = JSON.parse(document.getElementById('report-data').textContent);
|
|
1881
|
+
const sessions = reportData.sessions;
|
|
1882
|
+
const sessionReports = reportData.session_reports;
|
|
1883
|
+
|
|
1884
|
+
let currentSessionId = null;
|
|
1885
|
+
let currentRpcIdx = null;
|
|
1886
|
+
let currentEventIdx = null;
|
|
1887
|
+
let currentViewMode = 'rpc'; // 'rpc' or 'events'
|
|
1888
|
+
|
|
1889
|
+
// Connector info toggle
|
|
1890
|
+
const connectorInfo = document.querySelector('.connector-info');
|
|
1891
|
+
const connectorToggle = document.querySelector('.connector-info-toggle');
|
|
1892
|
+
if (connectorToggle) {
|
|
1893
|
+
connectorToggle.addEventListener('click', () => {
|
|
1894
|
+
connectorInfo.classList.toggle('expanded');
|
|
1895
|
+
});
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
// Format JSON for display
|
|
1899
|
+
function formatJson(data) {
|
|
1900
|
+
if (data === null || data === undefined) return '(no data)';
|
|
1901
|
+
try {
|
|
1902
|
+
return JSON.stringify(data, null, 2);
|
|
1903
|
+
} catch {
|
|
1904
|
+
return String(data);
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
// Escape HTML
|
|
1909
|
+
function escapeHtml(text) {
|
|
1910
|
+
const div = document.createElement('div');
|
|
1911
|
+
div.textContent = text;
|
|
1912
|
+
return div.innerHTML;
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
// Format bytes
|
|
1916
|
+
function formatBytes(bytes) {
|
|
1917
|
+
if (bytes === 0) return '0 B';
|
|
1918
|
+
const k = 1024;
|
|
1919
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
1920
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
1921
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
// Show session detail
|
|
1925
|
+
function showSession(sessionId) {
|
|
1926
|
+
if (currentSessionId === sessionId) return;
|
|
1927
|
+
currentSessionId = sessionId;
|
|
1928
|
+
currentRpcIdx = null;
|
|
1929
|
+
currentEventIdx = null;
|
|
1930
|
+
currentViewMode = 'rpc'; // Reset to RPC view
|
|
1931
|
+
|
|
1932
|
+
// Update session list selection
|
|
1933
|
+
document.querySelectorAll('.session-item').forEach(item => {
|
|
1934
|
+
item.classList.toggle('selected', item.dataset.sessionId === sessionId);
|
|
1935
|
+
});
|
|
1936
|
+
|
|
1937
|
+
// Hide all session contents, show selected
|
|
1938
|
+
document.querySelectorAll('.session-content').forEach(content => {
|
|
1939
|
+
content.classList.toggle('active', content.dataset.sessionId === sessionId);
|
|
1940
|
+
});
|
|
1941
|
+
|
|
1942
|
+
// Select first RPC in the newly shown session
|
|
1943
|
+
const sessionContent = document.querySelector('.session-content[data-session-id="' + sessionId + '"]');
|
|
1944
|
+
if (sessionContent) {
|
|
1945
|
+
const firstRpcRow = sessionContent.querySelector('.rpc-row');
|
|
1946
|
+
if (firstRpcRow) {
|
|
1947
|
+
const idx = parseInt(firstRpcRow.dataset.rpcIdx);
|
|
1948
|
+
showRpcDetail(sessionId, idx);
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
// Show RPC detail in right pane using pre-rendered HTML
|
|
1954
|
+
// This approach avoids JavaScript string concatenation issues with special characters
|
|
1955
|
+
function showRpcDetail(sessionId, idx) {
|
|
1956
|
+
const report = sessionReports[sessionId];
|
|
1957
|
+
if (!report || idx < 0 || idx >= report.rpcs.length) return;
|
|
1958
|
+
|
|
1959
|
+
const sessionContent = document.querySelector('.session-content[data-session-id="' + sessionId + '"]');
|
|
1960
|
+
if (!sessionContent) return;
|
|
1961
|
+
|
|
1962
|
+
const rightPane = sessionContent.querySelector('.right-pane');
|
|
1963
|
+
if (!rightPane) return;
|
|
1964
|
+
|
|
1965
|
+
// Update RPC row selection
|
|
1966
|
+
sessionContent.querySelectorAll('.rpc-row').forEach((r, i) => {
|
|
1967
|
+
r.classList.toggle('selected', i === idx);
|
|
1968
|
+
});
|
|
1969
|
+
currentRpcIdx = idx;
|
|
1970
|
+
|
|
1971
|
+
// Hide placeholder, show details container
|
|
1972
|
+
const placeholder = rightPane.querySelector('.detail-placeholder');
|
|
1973
|
+
const detailsContainer = rightPane.querySelector('.rpc-details-container');
|
|
1974
|
+
if (placeholder) placeholder.style.display = 'none';
|
|
1975
|
+
if (detailsContainer) detailsContainer.style.display = 'block';
|
|
1976
|
+
|
|
1977
|
+
// Hide all RPC detail divs, show selected one
|
|
1978
|
+
const allDetails = rightPane.querySelectorAll('.rpc-detail-content');
|
|
1979
|
+
allDetails.forEach(function(detail) {
|
|
1980
|
+
detail.style.display = detail.dataset.rpcIdx === String(idx) ? 'block' : 'none';
|
|
1981
|
+
});
|
|
1982
|
+
|
|
1983
|
+
// Re-initialize RPC Inspector handlers for the visible detail
|
|
1984
|
+
if (window.initRpcInspector) {
|
|
1985
|
+
window.initRpcInspector();
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
// Copy to clipboard
|
|
1990
|
+
async function copyToClipboard(elementId, btn) {
|
|
1991
|
+
const target = document.getElementById(elementId);
|
|
1992
|
+
if (target) {
|
|
1993
|
+
try {
|
|
1994
|
+
await navigator.clipboard.writeText(target.textContent || '');
|
|
1995
|
+
const originalText = btn.textContent;
|
|
1996
|
+
btn.textContent = 'Copied!';
|
|
1997
|
+
setTimeout(() => { btn.textContent = originalText; }, 1500);
|
|
1998
|
+
} catch (err) {
|
|
1999
|
+
console.error('Copy failed:', err);
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
// Session item click handlers
|
|
2005
|
+
document.querySelectorAll('.session-item').forEach(item => {
|
|
2006
|
+
item.addEventListener('click', () => {
|
|
2007
|
+
showSession(item.dataset.sessionId);
|
|
2008
|
+
});
|
|
2009
|
+
});
|
|
2010
|
+
|
|
2011
|
+
// RPC row click handlers (delegated)
|
|
2012
|
+
document.querySelectorAll('.session-content').forEach(content => {
|
|
2013
|
+
content.addEventListener('click', (e) => {
|
|
2014
|
+
const row = e.target.closest('.rpc-row');
|
|
2015
|
+
if (row) {
|
|
2016
|
+
const idx = parseInt(row.dataset.rpcIdx);
|
|
2017
|
+
showRpcDetail(content.dataset.sessionId, idx);
|
|
2018
|
+
}
|
|
2019
|
+
});
|
|
2020
|
+
});
|
|
2021
|
+
|
|
2022
|
+
// Keyboard navigation (handles both RPC and Events views)
|
|
2023
|
+
document.addEventListener('keydown', (e) => {
|
|
2024
|
+
if (!currentSessionId) return;
|
|
2025
|
+
if (e.key !== 'ArrowDown' && e.key !== 'ArrowUp') return;
|
|
2026
|
+
|
|
2027
|
+
e.preventDefault();
|
|
2028
|
+
const sessionContent = document.querySelector('.session-content[data-session-id="' + currentSessionId + '"]');
|
|
2029
|
+
if (!sessionContent) return;
|
|
2030
|
+
|
|
2031
|
+
// Check which view is active
|
|
2032
|
+
if (currentViewMode === 'events') {
|
|
2033
|
+
// Events navigation
|
|
2034
|
+
const eventRows = sessionContent.querySelectorAll('.event-row');
|
|
2035
|
+
if (eventRows.length === 0) return;
|
|
2036
|
+
|
|
2037
|
+
let newIdx = currentEventIdx;
|
|
2038
|
+
if (currentEventIdx === null) {
|
|
2039
|
+
newIdx = 0;
|
|
2040
|
+
} else if (e.key === 'ArrowDown' && currentEventIdx < eventRows.length - 1) {
|
|
2041
|
+
newIdx = currentEventIdx + 1;
|
|
2042
|
+
} else if (e.key === 'ArrowUp' && currentEventIdx > 0) {
|
|
2043
|
+
newIdx = currentEventIdx - 1;
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
if (newIdx !== currentEventIdx) {
|
|
2047
|
+
currentEventIdx = newIdx;
|
|
2048
|
+
// Update selection visually
|
|
2049
|
+
eventRows.forEach((r, i) => r.classList.toggle('selected', i === newIdx));
|
|
2050
|
+
// Scroll into view
|
|
2051
|
+
eventRows[newIdx].scrollIntoView({ block: 'nearest' });
|
|
2052
|
+
// Trigger click to show detail (if has payload)
|
|
2053
|
+
eventRows[newIdx].click();
|
|
2054
|
+
}
|
|
2055
|
+
} else {
|
|
2056
|
+
// RPC navigation
|
|
2057
|
+
const report = sessionReports[currentSessionId];
|
|
2058
|
+
if (!report) return;
|
|
2059
|
+
const rpcs = report.rpcs;
|
|
2060
|
+
if (rpcs.length === 0) return;
|
|
2061
|
+
|
|
2062
|
+
if (currentRpcIdx === null && rpcs.length > 0) {
|
|
2063
|
+
showRpcDetail(currentSessionId, 0);
|
|
2064
|
+
return;
|
|
2065
|
+
}
|
|
2066
|
+
if (e.key === 'ArrowDown' && currentRpcIdx < rpcs.length - 1) {
|
|
2067
|
+
showRpcDetail(currentSessionId, currentRpcIdx + 1);
|
|
2068
|
+
} else if (e.key === 'ArrowUp' && currentRpcIdx > 0) {
|
|
2069
|
+
showRpcDetail(currentSessionId, currentRpcIdx - 1);
|
|
2070
|
+
}
|
|
2071
|
+
// Scroll selected row into view
|
|
2072
|
+
const row = sessionContent.querySelector('.rpc-row.selected');
|
|
2073
|
+
if (row) row.scrollIntoView({ block: 'nearest' });
|
|
2074
|
+
}
|
|
2075
|
+
});
|
|
2076
|
+
|
|
2077
|
+
// Resize handle for inner left pane
|
|
2078
|
+
document.querySelectorAll('.session-content').forEach(content => {
|
|
2079
|
+
const resizeHandle = content.querySelector('.resize-handle');
|
|
2080
|
+
const leftPane = content.querySelector('.left-pane');
|
|
2081
|
+
if (resizeHandle && leftPane) {
|
|
2082
|
+
let startX, startWidth;
|
|
2083
|
+
|
|
2084
|
+
resizeHandle.addEventListener('mousedown', (e) => {
|
|
2085
|
+
startX = e.clientX;
|
|
2086
|
+
startWidth = leftPane.offsetWidth;
|
|
2087
|
+
document.addEventListener('mousemove', onMouseMove);
|
|
2088
|
+
document.addEventListener('mouseup', onMouseUp);
|
|
2089
|
+
e.preventDefault();
|
|
2090
|
+
});
|
|
2091
|
+
|
|
2092
|
+
function onMouseMove(e) {
|
|
2093
|
+
const diff = e.clientX - startX;
|
|
2094
|
+
const newWidth = Math.max(300, Math.min(600, startWidth + diff));
|
|
2095
|
+
leftPane.style.width = newWidth + 'px';
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
function onMouseUp() {
|
|
2099
|
+
document.removeEventListener('mousemove', onMouseMove);
|
|
2100
|
+
document.removeEventListener('mouseup', onMouseUp);
|
|
2101
|
+
}
|
|
2102
|
+
}
|
|
2103
|
+
});
|
|
2104
|
+
|
|
2105
|
+
// Check for session parameter in URL
|
|
2106
|
+
function getSessionFromUrl() {
|
|
2107
|
+
const params = new URLSearchParams(window.location.search);
|
|
2108
|
+
return params.get('session');
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
// Scroll session item into view
|
|
2112
|
+
function scrollSessionIntoView(sessionId) {
|
|
2113
|
+
const sessionItem = document.querySelector('.session-item[data-session-id="' + sessionId + '"]');
|
|
2114
|
+
if (sessionItem) {
|
|
2115
|
+
sessionItem.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
2116
|
+
// Add highlight effect
|
|
2117
|
+
sessionItem.classList.add('highlight');
|
|
2118
|
+
setTimeout(() => sessionItem.classList.remove('highlight'), 2000);
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
// Select session from URL or first session by default
|
|
2123
|
+
const urlSession = getSessionFromUrl();
|
|
2124
|
+
if (urlSession) {
|
|
2125
|
+
// Try to find matching session (full or partial match)
|
|
2126
|
+
const matchingSession = sessions.find(s =>
|
|
2127
|
+
s.session_id === urlSession || s.session_id.startsWith(urlSession)
|
|
2128
|
+
);
|
|
2129
|
+
if (matchingSession) {
|
|
2130
|
+
showSession(matchingSession.session_id);
|
|
2131
|
+
// Scroll into view after a short delay to ensure DOM is ready
|
|
2132
|
+
setTimeout(() => scrollSessionIntoView(matchingSession.session_id), 100);
|
|
2133
|
+
} else if (sessions.length > 0) {
|
|
2134
|
+
showSession(sessions[0].session_id);
|
|
2135
|
+
}
|
|
2136
|
+
} else if (sessions.length > 0) {
|
|
2137
|
+
showSession(sessions[0].session_id);
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
// Events View toggle and data loading (Issue #59)
|
|
2141
|
+
(function() {
|
|
2142
|
+
// Cache for loaded events
|
|
2143
|
+
const eventsCache = {};
|
|
2144
|
+
|
|
2145
|
+
// Event kind display labels
|
|
2146
|
+
const kindLabels = {
|
|
2147
|
+
request: 'REQ',
|
|
2148
|
+
response: 'RES',
|
|
2149
|
+
notification: 'NOTIF',
|
|
2150
|
+
transport_event: 'TRANS'
|
|
2151
|
+
};
|
|
2152
|
+
|
|
2153
|
+
// Format time for events table
|
|
2154
|
+
function formatEventTime(ts) {
|
|
2155
|
+
try {
|
|
2156
|
+
const date = new Date(ts);
|
|
2157
|
+
return date.toISOString().split('T')[1].slice(0, 12);
|
|
2158
|
+
} catch {
|
|
2159
|
+
return ts;
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
// Render events table
|
|
2164
|
+
function renderEventsTable(events) {
|
|
2165
|
+
if (!events || events.length === 0) {
|
|
2166
|
+
return '<div class="events-loading">No events in this session</div>';
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
const rows = events.map(function(event, idx) {
|
|
2170
|
+
const dirClass = event.direction === 'client_to_server' ? 'outgoing' : 'incoming';
|
|
2171
|
+
// Large arrows with tooltip: ⇨ (blue) = Client→Server, ⇦ (green) = Server→Client
|
|
2172
|
+
const dirArrow = event.direction === 'client_to_server' ? '\\u21E8' : '\\u21E6';
|
|
2173
|
+
const dirTooltip = event.direction === 'client_to_server'
|
|
2174
|
+
? 'Client \\u2192 Server'
|
|
2175
|
+
: 'Server \\u2192 Client';
|
|
2176
|
+
const kindClass = 'badge-kind-' + event.kind;
|
|
2177
|
+
const kindLabel = kindLabels[event.kind] || event.kind;
|
|
2178
|
+
// Method/Summary fallback: method > summary > payload_type (e.g., "connected")
|
|
2179
|
+
const method = event.method || event.summary || event.payload_type || '';
|
|
2180
|
+
const timeStr = formatEventTime(event.ts);
|
|
2181
|
+
const hasPayload = event.has_payload ? '\\u2713' : '';
|
|
2182
|
+
|
|
2183
|
+
return '<tr class="event-row" data-event-idx="' + idx + '" data-event-id="' + escapeHtml(event.event_id) + '">' +
|
|
2184
|
+
'<td>' + timeStr + '</td>' +
|
|
2185
|
+
'<td><span class="direction-arrow ' + dirClass + '" title="' + dirTooltip + '">' + dirArrow + '</span></td>' +
|
|
2186
|
+
'<td><span class="badge ' + kindClass + '">' + kindLabel + '</span></td>' +
|
|
2187
|
+
'<td>' + escapeHtml(method) + '</td>' +
|
|
2188
|
+
'<td>' + hasPayload + '</td>' +
|
|
2189
|
+
'</tr>';
|
|
2190
|
+
}).join('');
|
|
2191
|
+
|
|
2192
|
+
return '<table class="events-table">' +
|
|
2193
|
+
'<thead><tr>' +
|
|
2194
|
+
'<th>Time</th>' +
|
|
2195
|
+
'<th>Dir</th>' +
|
|
2196
|
+
'<th>Kind</th>' +
|
|
2197
|
+
'<th>Method/Summary</th>' +
|
|
2198
|
+
'<th>Data</th>' +
|
|
2199
|
+
'</tr></thead>' +
|
|
2200
|
+
'<tbody>' + rows + '</tbody>' +
|
|
2201
|
+
'</table>';
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
// Load events for a session
|
|
2205
|
+
function loadEvents(sessionId, eventsList) {
|
|
2206
|
+
if (eventsCache[sessionId]) {
|
|
2207
|
+
eventsList.innerHTML = renderEventsTable(eventsCache[sessionId]);
|
|
2208
|
+
// Attach click handlers even when using cached data
|
|
2209
|
+
attachEventRowHandlers(eventsList, sessionId, eventsCache[sessionId]);
|
|
2210
|
+
return;
|
|
2211
|
+
}
|
|
2212
|
+
|
|
2213
|
+
// Check if we're in offline mode (static HTML) or live server
|
|
2214
|
+
// Try to fetch from API, fallback to "no data" message
|
|
2215
|
+
eventsList.innerHTML = '<div class="events-loading">Loading events...</div>';
|
|
2216
|
+
|
|
2217
|
+
fetch('/api/sessions/' + encodeURIComponent(sessionId) + '/events')
|
|
2218
|
+
.then(function(res) {
|
|
2219
|
+
if (!res.ok) throw new Error('API not available');
|
|
2220
|
+
return res.json();
|
|
2221
|
+
})
|
|
2222
|
+
.then(function(data) {
|
|
2223
|
+
eventsCache[sessionId] = data.events;
|
|
2224
|
+
eventsList.innerHTML = renderEventsTable(data.events);
|
|
2225
|
+
// Attach click handlers for event rows
|
|
2226
|
+
attachEventRowHandlers(eventsList, sessionId, data.events);
|
|
2227
|
+
})
|
|
2228
|
+
.catch(function() {
|
|
2229
|
+
eventsList.innerHTML = '<div class="events-loading">Events data not available (API offline)</div>';
|
|
2230
|
+
});
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
// Attach click handlers for event rows
|
|
2234
|
+
function attachEventRowHandlers(eventsList, sessionId, events) {
|
|
2235
|
+
eventsList.querySelectorAll('.event-row').forEach(function(row) {
|
|
2236
|
+
row.addEventListener('click', function() {
|
|
2237
|
+
const idx = parseInt(row.dataset.eventIdx);
|
|
2238
|
+
const event = events[idx];
|
|
2239
|
+
if (!event || !event.has_payload) return;
|
|
2240
|
+
|
|
2241
|
+
// Clear previous selection
|
|
2242
|
+
eventsList.querySelectorAll('.event-row').forEach(function(r) {
|
|
2243
|
+
r.classList.remove('selected');
|
|
2244
|
+
});
|
|
2245
|
+
row.classList.add('selected');
|
|
2246
|
+
|
|
2247
|
+
// Show event detail in right pane
|
|
2248
|
+
showEventDetail(sessionId, event);
|
|
2249
|
+
});
|
|
2250
|
+
});
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2253
|
+
// Show event detail in right pane (2-column layout like RPC Inspector)
|
|
2254
|
+
function showEventDetail(sessionId, event) {
|
|
2255
|
+
const sessionContent = document.querySelector('.session-content[data-session-id="' + sessionId + '"]');
|
|
2256
|
+
if (!sessionContent) return;
|
|
2257
|
+
|
|
2258
|
+
const rightPane = sessionContent.querySelector('.right-pane');
|
|
2259
|
+
if (!rightPane) return;
|
|
2260
|
+
|
|
2261
|
+
// Fetch full event detail
|
|
2262
|
+
fetch('/api/events/' + encodeURIComponent(event.event_id))
|
|
2263
|
+
.then(function(res) {
|
|
2264
|
+
if (!res.ok) throw new Error('Event not found');
|
|
2265
|
+
return res.json();
|
|
2266
|
+
})
|
|
2267
|
+
.then(function(data) {
|
|
2268
|
+
const evt = data.event;
|
|
2269
|
+
const kindClass = 'badge-kind-' + evt.kind;
|
|
2270
|
+
const dirClass = evt.direction === 'client_to_server' ? 'outgoing' : 'incoming';
|
|
2271
|
+
// Large arrows with tooltip: ⇨ (blue) = Client→Server, ⇦ (green) = Server→Client
|
|
2272
|
+
const dirArrow = evt.direction === 'client_to_server' ? '\\u21E8' : '\\u21E6';
|
|
2273
|
+
const dirTooltip = evt.direction === 'client_to_server'
|
|
2274
|
+
? 'Client \\u2192 Server'
|
|
2275
|
+
: 'Server \\u2192 Client';
|
|
2276
|
+
const method = evt.method || evt.summary || '(unknown)';
|
|
2277
|
+
const rawJson = evt.raw_json ? JSON.parse(evt.raw_json) : null;
|
|
2278
|
+
const formattedJson = rawJson ? JSON.stringify(rawJson, null, 2) : '(no data)';
|
|
2279
|
+
|
|
2280
|
+
// Build summary section
|
|
2281
|
+
var summaryHtml = '<div class="summary-row summary-header">Event Info</div>';
|
|
2282
|
+
summaryHtml += '<div class="summary-row summary-property"><span class="summary-prop-name">Kind</span><span class="summary-prop-value"><span class="badge ' + kindClass + '">' + evt.kind + '</span></span></div>';
|
|
2283
|
+
summaryHtml += '<div class="summary-row summary-property"><span class="summary-prop-name">Direction</span><span class="summary-prop-value"><span class="direction-arrow ' + dirClass + '" title="' + dirTooltip + '">' + dirArrow + '</span> ' + dirTooltip + '</span></div>';
|
|
2284
|
+
summaryHtml += '<div class="summary-row summary-property"><span class="summary-prop-name">Method</span><span class="summary-prop-value">' + escapeHtml(method) + '</span></div>';
|
|
2285
|
+
summaryHtml += '<div class="summary-row summary-property"><span class="summary-prop-name">Timestamp</span><span class="summary-prop-value">' + escapeHtml(evt.ts) + '</span></div>';
|
|
2286
|
+
if (evt.seq !== null) {
|
|
2287
|
+
summaryHtml += '<div class="summary-row summary-property"><span class="summary-prop-name">Sequence</span><span class="summary-prop-value">' + evt.seq + '</span></div>';
|
|
2288
|
+
}
|
|
2289
|
+
|
|
2290
|
+
// If JSON has recognizable structure, add more summary
|
|
2291
|
+
if (rawJson) {
|
|
2292
|
+
if (rawJson.method) {
|
|
2293
|
+
summaryHtml += '<div class="summary-row summary-header" style="margin-top: 12px;">JSON-RPC</div>';
|
|
2294
|
+
summaryHtml += '<div class="summary-row summary-property"><span class="summary-prop-name">method</span><span class="summary-prop-value">' + escapeHtml(rawJson.method) + '</span></div>';
|
|
2295
|
+
}
|
|
2296
|
+
if (rawJson.id !== undefined) {
|
|
2297
|
+
summaryHtml += '<div class="summary-row summary-property"><span class="summary-prop-name">id</span><span class="summary-prop-value">' + escapeHtml(String(rawJson.id)) + '</span></div>';
|
|
2298
|
+
}
|
|
2299
|
+
if (rawJson.error) {
|
|
2300
|
+
summaryHtml += '<div class="summary-row summary-property"><span class="summary-prop-name">error</span><span class="summary-prop-value" style="color: var(--accent-red);">' + escapeHtml(JSON.stringify(rawJson.error)) + '</span></div>';
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
|
|
2304
|
+
// 2-column layout: Summary (left) + Raw JSON (right)
|
|
2305
|
+
rightPane.innerHTML =
|
|
2306
|
+
'<div class="rpc-inspector">' +
|
|
2307
|
+
'<div class="rpc-inspector-summary">' +
|
|
2308
|
+
'<div class="summary-container">' + summaryHtml + '</div>' +
|
|
2309
|
+
'</div>' +
|
|
2310
|
+
'<div class="rpc-inspector-raw">' +
|
|
2311
|
+
'<div class="rpc-raw-header">' +
|
|
2312
|
+
'<span class="rpc-raw-title">Payload</span>' +
|
|
2313
|
+
'</div>' +
|
|
2314
|
+
'<div class="rpc-raw-json"><pre><code>' + escapeHtml(formattedJson) + '</code></pre></div>' +
|
|
2315
|
+
'</div>' +
|
|
2316
|
+
'</div>';
|
|
2317
|
+
})
|
|
2318
|
+
.catch(function() {
|
|
2319
|
+
rightPane.innerHTML = '<div class="detail-placeholder">Failed to load event detail</div>';
|
|
2320
|
+
});
|
|
2321
|
+
}
|
|
2322
|
+
|
|
2323
|
+
// Handle view toggle clicks
|
|
2324
|
+
document.querySelectorAll('.view-toggle').forEach(function(toggle) {
|
|
2325
|
+
const sessionContent = toggle.closest('.session-content');
|
|
2326
|
+
if (!sessionContent) return;
|
|
2327
|
+
|
|
2328
|
+
const sessionId = sessionContent.dataset.sessionId;
|
|
2329
|
+
const rpcList = sessionContent.querySelector('.rpc-list');
|
|
2330
|
+
const eventsList = sessionContent.querySelector('.events-list');
|
|
2331
|
+
const buttons = toggle.querySelectorAll('.view-toggle-btn');
|
|
2332
|
+
|
|
2333
|
+
buttons.forEach(function(btn) {
|
|
2334
|
+
btn.addEventListener('click', function() {
|
|
2335
|
+
const view = btn.dataset.view;
|
|
2336
|
+
|
|
2337
|
+
// Update current view mode
|
|
2338
|
+
currentViewMode = view;
|
|
2339
|
+
|
|
2340
|
+
// Update button states
|
|
2341
|
+
buttons.forEach(function(b) {
|
|
2342
|
+
b.classList.toggle('active', b.dataset.view === view);
|
|
2343
|
+
});
|
|
2344
|
+
|
|
2345
|
+
// Toggle lists
|
|
2346
|
+
if (view === 'events') {
|
|
2347
|
+
rpcList.classList.add('hidden');
|
|
2348
|
+
eventsList.classList.add('active');
|
|
2349
|
+
loadEvents(sessionId, eventsList);
|
|
2350
|
+
} else {
|
|
2351
|
+
rpcList.classList.remove('hidden');
|
|
2352
|
+
eventsList.classList.remove('active');
|
|
2353
|
+
// Re-show selected RPC detail when switching back to RPCs view
|
|
2354
|
+
if (currentRpcIdx !== null && sessionId === currentSessionId) {
|
|
2355
|
+
showRpcDetail(sessionId, currentRpcIdx);
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
});
|
|
2359
|
+
});
|
|
2360
|
+
});
|
|
2361
|
+
})();
|
|
2362
|
+
|
|
2363
|
+
// RPC Inspector script
|
|
2364
|
+
${getRpcInspectorScript()}
|
|
2365
|
+
`;
|
|
2366
|
+
}
|
|
2367
|
+
|
|
2368
|
+
// ============================================================================
|
|
2369
|
+
// Analytics Panel Rendering (Phase 5.2)
|
|
2370
|
+
// ============================================================================
|
|
2371
|
+
|
|
2372
|
+
/**
|
|
2373
|
+
* Render KPI stats row for header (inline compact display)
|
|
2374
|
+
*/
|
|
2375
|
+
function renderKpiRow(kpis: HtmlConnectorKpis): string {
|
|
2376
|
+
// Format large numbers with K/M suffix
|
|
2377
|
+
const formatNumber = (n: number): string => {
|
|
2378
|
+
if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
|
|
2379
|
+
if (n >= 1000) return (n / 1000).toFixed(1) + 'K';
|
|
2380
|
+
return String(n);
|
|
2381
|
+
};
|
|
2382
|
+
|
|
2383
|
+
// Format bytes for display
|
|
2384
|
+
const formatBytesCompact = (bytes: number): string => {
|
|
2385
|
+
if (bytes === 0) return '0';
|
|
2386
|
+
const k = 1024;
|
|
2387
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
2388
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
2389
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + sizes[i];
|
|
2390
|
+
};
|
|
2391
|
+
|
|
2392
|
+
return `
|
|
2393
|
+
<div class="kpi-row">
|
|
2394
|
+
<div class="kpi-item">
|
|
2395
|
+
<div class="kpi-value">${formatNumber(kpis.sessions_displayed)}</div>
|
|
2396
|
+
<div class="kpi-label">Sessions</div>
|
|
2397
|
+
</div>
|
|
2398
|
+
<div class="kpi-item">
|
|
2399
|
+
<div class="kpi-value">${formatNumber(kpis.rpc_total)}</div>
|
|
2400
|
+
<div class="kpi-label">RPCs</div>
|
|
2401
|
+
</div>
|
|
2402
|
+
<div class="kpi-item">
|
|
2403
|
+
<div class="kpi-value">${formatNumber(kpis.rpc_err)}</div>
|
|
2404
|
+
<div class="kpi-label">Error</div>
|
|
2405
|
+
</div>
|
|
2406
|
+
<div class="kpi-item">
|
|
2407
|
+
<div class="kpi-value">${kpis.avg_latency_ms !== null ? kpis.avg_latency_ms : '-'}</div>
|
|
2408
|
+
<div class="kpi-label">Avg Latency</div>
|
|
2409
|
+
</div>
|
|
2410
|
+
<div class="kpi-item">
|
|
2411
|
+
<div class="kpi-value">${kpis.p95_latency_ms !== null ? kpis.p95_latency_ms : '-'}</div>
|
|
2412
|
+
<div class="kpi-label">P95 Latency</div>
|
|
2413
|
+
</div>
|
|
2414
|
+
<div class="kpi-item">
|
|
2415
|
+
<div class="kpi-value">${formatBytesCompact(kpis.total_request_bytes)}</div>
|
|
2416
|
+
<div class="kpi-label">Req Size</div>
|
|
2417
|
+
</div>
|
|
2418
|
+
<div class="kpi-item">
|
|
2419
|
+
<div class="kpi-value">${formatBytesCompact(kpis.total_response_bytes)}</div>
|
|
2420
|
+
<div class="kpi-label">Res Size</div>
|
|
2421
|
+
</div>
|
|
2422
|
+
</div>`;
|
|
2423
|
+
}
|
|
2424
|
+
|
|
2425
|
+
/**
|
|
2426
|
+
* Get intensity level (0-4) for heatmap cell based on count and max
|
|
2427
|
+
*/
|
|
2428
|
+
function getHeatmapLevel(count: number, maxCount: number): number {
|
|
2429
|
+
if (count === 0 || maxCount === 0) return 0;
|
|
2430
|
+
const ratio = count / maxCount;
|
|
2431
|
+
if (ratio <= 0.25) return 1;
|
|
2432
|
+
if (ratio <= 0.5) return 2;
|
|
2433
|
+
if (ratio <= 0.75) return 3;
|
|
2434
|
+
return 4;
|
|
2435
|
+
}
|
|
2436
|
+
|
|
2437
|
+
/**
|
|
2438
|
+
* Render activity heatmap (GitHub contributions style, SVG)
|
|
2439
|
+
*/
|
|
2440
|
+
export function renderHeatmap(heatmap: HtmlHeatmapData): string {
|
|
2441
|
+
const cellSize = 10;
|
|
2442
|
+
const cellGap = 2;
|
|
2443
|
+
const cellTotal = cellSize + cellGap;
|
|
2444
|
+
|
|
2445
|
+
// Group cells by week (7 days per column)
|
|
2446
|
+
const weeks: Array<typeof heatmap.cells> = [];
|
|
2447
|
+
let currentWeek: typeof heatmap.cells = [];
|
|
2448
|
+
|
|
2449
|
+
// Find the day of week for the start date (0 = Sunday)
|
|
2450
|
+
const startDow = new Date(heatmap.start_date + 'T00:00:00Z').getUTCDay();
|
|
2451
|
+
|
|
2452
|
+
// Add empty cells for days before start_date
|
|
2453
|
+
for (let i = 0; i < startDow; i++) {
|
|
2454
|
+
currentWeek.push({ date: '', count: -1 }); // -1 indicates empty
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
for (const cell of heatmap.cells) {
|
|
2458
|
+
currentWeek.push(cell);
|
|
2459
|
+
if (currentWeek.length === 7) {
|
|
2460
|
+
weeks.push(currentWeek);
|
|
2461
|
+
currentWeek = [];
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
if (currentWeek.length > 0) {
|
|
2465
|
+
weeks.push(currentWeek);
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2468
|
+
// Calculate SVG dimensions
|
|
2469
|
+
const svgWidth = weeks.length * cellTotal;
|
|
2470
|
+
const svgHeight = 7 * cellTotal;
|
|
2471
|
+
|
|
2472
|
+
// Generate SVG rects
|
|
2473
|
+
let rects = '';
|
|
2474
|
+
weeks.forEach((week, weekIdx) => {
|
|
2475
|
+
week.forEach((cell, dayIdx) => {
|
|
2476
|
+
if (cell.count < 0) return; // Skip empty cells
|
|
2477
|
+
const level = getHeatmapLevel(cell.count, heatmap.max_count);
|
|
2478
|
+
const x = weekIdx * cellTotal;
|
|
2479
|
+
const y = dayIdx * cellTotal;
|
|
2480
|
+
const title = cell.date ? `${cell.date}: ${cell.count} RPCs` : '';
|
|
2481
|
+
rects += `<rect x="${x}" y="${y}" width="${cellSize}" height="${cellSize}" rx="2" class="heatmap-level-${level}"><title>${escapeHtml(title)}</title></rect>`;
|
|
2482
|
+
});
|
|
2483
|
+
});
|
|
2484
|
+
|
|
2485
|
+
return `
|
|
2486
|
+
<div class="heatmap-container">
|
|
2487
|
+
<div class="heatmap-title">Activity<br><span class="heatmap-range">${escapeHtml(heatmap.start_date)} to ${escapeHtml(heatmap.end_date)}</span></div>
|
|
2488
|
+
<svg width="${svgWidth}" height="${svgHeight}" viewBox="0 0 ${svgWidth} ${svgHeight}">
|
|
2489
|
+
${rects}
|
|
2490
|
+
</svg>
|
|
2491
|
+
</div>`;
|
|
2492
|
+
}
|
|
2493
|
+
|
|
2494
|
+
/**
|
|
2495
|
+
* Render method-based latency heatmap (Activity-style)
|
|
2496
|
+
* Rows: method names
|
|
2497
|
+
* Columns: latency buckets (log scale)
|
|
2498
|
+
* Cell color: intensity based on count
|
|
2499
|
+
*/
|
|
2500
|
+
function renderMethodLatencyChart(methodLatency: HtmlMethodLatencyData): string {
|
|
2501
|
+
if (methodLatency.sample_size === 0 || methodLatency.methods.length === 0) {
|
|
2502
|
+
return `
|
|
2503
|
+
<div class="latency-histogram">
|
|
2504
|
+
<div class="chart-title">Latency by Method</div>
|
|
2505
|
+
<div class="no-data-message">No latency data</div>
|
|
2506
|
+
</div>`;
|
|
2507
|
+
}
|
|
2508
|
+
|
|
2509
|
+
// Latency buckets (log scale thresholds)
|
|
2510
|
+
const buckets = [
|
|
2511
|
+
{ label: '<10ms', max: 10 },
|
|
2512
|
+
{ label: '<100ms', max: 100 },
|
|
2513
|
+
{ label: '<1s', max: 1000 },
|
|
2514
|
+
{ label: '<5s', max: 5000 },
|
|
2515
|
+
{ label: '5s+', max: Infinity },
|
|
2516
|
+
];
|
|
2517
|
+
|
|
2518
|
+
// Group latencies into buckets for each method
|
|
2519
|
+
const methods = methodLatency.methods;
|
|
2520
|
+
|
|
2521
|
+
// Find global max count for intensity scaling
|
|
2522
|
+
let globalMaxCount = 0;
|
|
2523
|
+
const methodBucketCounts: Map<string, number[]> = new Map();
|
|
2524
|
+
|
|
2525
|
+
for (const method of methods) {
|
|
2526
|
+
const counts = buckets.map(() => 0);
|
|
2527
|
+
for (const latency of method.latencies) {
|
|
2528
|
+
for (let i = 0; i < buckets.length; i++) {
|
|
2529
|
+
if (i === 0 && latency < buckets[i].max) {
|
|
2530
|
+
counts[i]++;
|
|
2531
|
+
break;
|
|
2532
|
+
} else if (i > 0 && latency >= buckets[i - 1].max && latency < buckets[i].max) {
|
|
2533
|
+
counts[i]++;
|
|
2534
|
+
break;
|
|
2535
|
+
} else if (i === buckets.length - 1 && latency >= buckets[i - 1].max) {
|
|
2536
|
+
counts[i]++;
|
|
2537
|
+
break;
|
|
2538
|
+
}
|
|
2539
|
+
}
|
|
2540
|
+
}
|
|
2541
|
+
methodBucketCounts.set(method.method, counts);
|
|
2542
|
+
globalMaxCount = Math.max(globalMaxCount, ...counts);
|
|
2543
|
+
}
|
|
2544
|
+
|
|
2545
|
+
// Get heatmap level (0-4)
|
|
2546
|
+
const getLevel = (count: number): number => {
|
|
2547
|
+
if (count === 0) return 0;
|
|
2548
|
+
if (globalMaxCount === 0) return 0;
|
|
2549
|
+
const ratio = count / globalMaxCount;
|
|
2550
|
+
if (ratio <= 0.25) return 1;
|
|
2551
|
+
if (ratio <= 0.5) return 2;
|
|
2552
|
+
if (ratio <= 0.75) return 3;
|
|
2553
|
+
return 4;
|
|
2554
|
+
};
|
|
2555
|
+
|
|
2556
|
+
// Build HTML rows
|
|
2557
|
+
let rows = '';
|
|
2558
|
+
for (const method of methods) {
|
|
2559
|
+
const shortName = shortenMethodName(method.method);
|
|
2560
|
+
const counts = methodBucketCounts.get(method.method) || [];
|
|
2561
|
+
|
|
2562
|
+
let cells = '';
|
|
2563
|
+
for (let i = 0; i < buckets.length; i++) {
|
|
2564
|
+
const count = counts[i];
|
|
2565
|
+
const level = getLevel(count);
|
|
2566
|
+
const tooltip = `${method.method}\n${buckets[i].label}: ${count} calls`;
|
|
2567
|
+
cells += `<div class="latency-heatmap-cell heatmap-level-${level}" title="${escapeHtml(tooltip)}"></div>`;
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2570
|
+
rows += `
|
|
2571
|
+
<div class="latency-heatmap-row">
|
|
2572
|
+
<div class="latency-heatmap-method">${escapeHtml(shortName)}</div>
|
|
2573
|
+
<div class="latency-heatmap-cells">${cells}</div>
|
|
2574
|
+
</div>`;
|
|
2575
|
+
}
|
|
2576
|
+
|
|
2577
|
+
// Header row with bucket labels
|
|
2578
|
+
let headerCells = '';
|
|
2579
|
+
for (const bucket of buckets) {
|
|
2580
|
+
headerCells += `<div class="latency-heatmap-header-cell">${bucket.label}</div>`;
|
|
2581
|
+
}
|
|
2582
|
+
|
|
2583
|
+
return `
|
|
2584
|
+
<div class="latency-histogram">
|
|
2585
|
+
<div class="chart-title">Latency by Method (${methodLatency.sample_size} samples)</div>
|
|
2586
|
+
<div class="latency-heatmap">
|
|
2587
|
+
<div class="latency-heatmap-header">
|
|
2588
|
+
<div class="latency-heatmap-method"></div>
|
|
2589
|
+
<div class="latency-heatmap-cells">${headerCells}</div>
|
|
2590
|
+
</div>
|
|
2591
|
+
${rows}
|
|
2592
|
+
</div>
|
|
2593
|
+
</div>`;
|
|
2594
|
+
}
|
|
2595
|
+
|
|
2596
|
+
/**
|
|
2597
|
+
* Shorten method name for display (e.g., "tools/list" -> "list", "initialize" -> "init")
|
|
2598
|
+
*/
|
|
2599
|
+
function shortenMethodName(method: string): string {
|
|
2600
|
+
// Remove prefix
|
|
2601
|
+
if (method.startsWith('tools/')) {
|
|
2602
|
+
return method.slice(6);
|
|
2603
|
+
}
|
|
2604
|
+
if (method.startsWith('resources/')) {
|
|
2605
|
+
return 'r/' + method.slice(10);
|
|
2606
|
+
}
|
|
2607
|
+
if (method.startsWith('prompts/')) {
|
|
2608
|
+
return 'p/' + method.slice(8);
|
|
2609
|
+
}
|
|
2610
|
+
if (method === 'initialize') {
|
|
2611
|
+
return 'init';
|
|
2612
|
+
}
|
|
2613
|
+
if (method === 'notifications/initialized') {
|
|
2614
|
+
return 'n/init';
|
|
2615
|
+
}
|
|
2616
|
+
// Truncate if too long
|
|
2617
|
+
if (method.length > 6) {
|
|
2618
|
+
return method.slice(0, 5) + '…';
|
|
2619
|
+
}
|
|
2620
|
+
return method;
|
|
2621
|
+
}
|
|
2622
|
+
|
|
2623
|
+
/**
|
|
2624
|
+
* Render top 5 tools
|
|
2625
|
+
*/
|
|
2626
|
+
function renderTopTools(topTools: HtmlTopToolsData): string {
|
|
2627
|
+
if (topTools.items.length === 0) {
|
|
2628
|
+
return `
|
|
2629
|
+
<div class="top-tools">
|
|
2630
|
+
<div class="chart-title">Top Tools</div>
|
|
2631
|
+
<div class="no-data-message">No tool calls</div>
|
|
2632
|
+
</div>`;
|
|
2633
|
+
}
|
|
2634
|
+
|
|
2635
|
+
const rows = topTools.items
|
|
2636
|
+
.map((tool, idx) => {
|
|
2637
|
+
return `
|
|
2638
|
+
<div class="top-tool-row" data-tool-name="${escapeHtml(tool.name)}">
|
|
2639
|
+
<span class="top-tool-rank">${idx + 1}.</span>
|
|
2640
|
+
<span class="top-tool-name" title="${escapeHtml(tool.name)}">${escapeHtml(tool.name)}</span>
|
|
2641
|
+
<div class="top-tool-bar-container">
|
|
2642
|
+
<div class="top-tool-bar" style="width: ${tool.pct}%"></div>
|
|
2643
|
+
</div>
|
|
2644
|
+
<span class="top-tool-pct">${tool.pct}%</span>
|
|
2645
|
+
</div>`;
|
|
2646
|
+
})
|
|
2647
|
+
.join('');
|
|
2648
|
+
|
|
2649
|
+
return `
|
|
2650
|
+
<div class="top-tools">
|
|
2651
|
+
<div class="chart-title">Top Tools (${topTools.total_calls} calls)</div>
|
|
2652
|
+
${rows}
|
|
2653
|
+
</div>`;
|
|
2654
|
+
}
|
|
2655
|
+
|
|
2656
|
+
/**
|
|
2657
|
+
* Donut chart colors (blue gradient palette)
|
|
2658
|
+
*/
|
|
2659
|
+
const DONUT_COLORS = [
|
|
2660
|
+
'#00d4ff', // Neon blue (brightest)
|
|
2661
|
+
'#0097b2', // Medium bright blue
|
|
2662
|
+
'#0d5c73', // Medium blue
|
|
2663
|
+
'#0a4d5c', // Darker blue
|
|
2664
|
+
'#083d47', // Dark blue
|
|
2665
|
+
'#5a6a70', // Blue-gray (for "Others")
|
|
2666
|
+
];
|
|
2667
|
+
|
|
2668
|
+
/**
|
|
2669
|
+
* Render method distribution donut chart (SVG)
|
|
2670
|
+
*/
|
|
2671
|
+
export function renderMethodDistribution(methodDist: HtmlMethodDistribution): string {
|
|
2672
|
+
if (methodDist.slices.length === 0) {
|
|
2673
|
+
return `
|
|
2674
|
+
<div class="method-distribution">
|
|
2675
|
+
<div class="chart-title">Method Distribution</div>
|
|
2676
|
+
<div class="no-data-message">No RPCs</div>
|
|
2677
|
+
</div>`;
|
|
2678
|
+
}
|
|
2679
|
+
|
|
2680
|
+
// SVG donut chart parameters
|
|
2681
|
+
const size = 60;
|
|
2682
|
+
const cx = size / 2;
|
|
2683
|
+
const cy = size / 2;
|
|
2684
|
+
const outerRadius = 26;
|
|
2685
|
+
const innerRadius = 16; // Creates the donut hole
|
|
2686
|
+
|
|
2687
|
+
// Generate SVG path segments
|
|
2688
|
+
let paths = '';
|
|
2689
|
+
let currentAngle = -90; // Start from top (12 o'clock)
|
|
2690
|
+
|
|
2691
|
+
methodDist.slices.forEach((slice, idx) => {
|
|
2692
|
+
const angle = (slice.pct / 100) * 360;
|
|
2693
|
+
const startAngle = currentAngle;
|
|
2694
|
+
const endAngle = currentAngle + angle;
|
|
2695
|
+
|
|
2696
|
+
// Convert angles to radians
|
|
2697
|
+
const startRad = (startAngle * Math.PI) / 180;
|
|
2698
|
+
const endRad = (endAngle * Math.PI) / 180;
|
|
2699
|
+
|
|
2700
|
+
// Calculate arc points for outer radius
|
|
2701
|
+
const x1Outer = cx + outerRadius * Math.cos(startRad);
|
|
2702
|
+
const y1Outer = cy + outerRadius * Math.sin(startRad);
|
|
2703
|
+
const x2Outer = cx + outerRadius * Math.cos(endRad);
|
|
2704
|
+
const y2Outer = cy + outerRadius * Math.sin(endRad);
|
|
2705
|
+
|
|
2706
|
+
// Calculate arc points for inner radius
|
|
2707
|
+
const x1Inner = cx + innerRadius * Math.cos(endRad);
|
|
2708
|
+
const y1Inner = cy + innerRadius * Math.sin(endRad);
|
|
2709
|
+
const x2Inner = cx + innerRadius * Math.cos(startRad);
|
|
2710
|
+
const y2Inner = cy + innerRadius * Math.sin(startRad);
|
|
2711
|
+
|
|
2712
|
+
// Large arc flag
|
|
2713
|
+
const largeArc = angle > 180 ? 1 : 0;
|
|
2714
|
+
|
|
2715
|
+
// Color
|
|
2716
|
+
const color = DONUT_COLORS[idx % DONUT_COLORS.length];
|
|
2717
|
+
|
|
2718
|
+
// SVG path for donut segment
|
|
2719
|
+
const d = [
|
|
2720
|
+
`M ${x1Outer} ${y1Outer}`, // Start at outer edge
|
|
2721
|
+
`A ${outerRadius} ${outerRadius} 0 ${largeArc} 1 ${x2Outer} ${y2Outer}`, // Outer arc
|
|
2722
|
+
`L ${x1Inner} ${y1Inner}`, // Line to inner edge
|
|
2723
|
+
`A ${innerRadius} ${innerRadius} 0 ${largeArc} 0 ${x2Inner} ${y2Inner}`, // Inner arc (reverse)
|
|
2724
|
+
'Z', // Close path
|
|
2725
|
+
].join(' ');
|
|
2726
|
+
|
|
2727
|
+
const title = `${slice.method}: ${slice.count} (${slice.pct}%)`;
|
|
2728
|
+
paths += `<path d="${d}" fill="${color}"><title>${escapeHtml(title)}</title></path>`;
|
|
2729
|
+
|
|
2730
|
+
currentAngle = endAngle;
|
|
2731
|
+
});
|
|
2732
|
+
|
|
2733
|
+
// Generate legend
|
|
2734
|
+
const legendItems = methodDist.slices
|
|
2735
|
+
.map((slice, idx) => {
|
|
2736
|
+
const color = DONUT_COLORS[idx % DONUT_COLORS.length];
|
|
2737
|
+
return `
|
|
2738
|
+
<div class="donut-legend-item">
|
|
2739
|
+
<div class="donut-legend-color" style="background: ${color}"></div>
|
|
2740
|
+
<span class="donut-legend-label" title="${escapeHtml(slice.method)}">${escapeHtml(slice.method)}</span>
|
|
2741
|
+
<span class="donut-legend-pct">${slice.pct}%</span>
|
|
2742
|
+
</div>`;
|
|
2743
|
+
})
|
|
2744
|
+
.join('');
|
|
2745
|
+
|
|
2746
|
+
return `
|
|
2747
|
+
<div class="method-distribution">
|
|
2748
|
+
<div class="chart-title">Methods (${methodDist.total_rpcs} RPCs)</div>
|
|
2749
|
+
<div class="donut-container">
|
|
2750
|
+
<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}">
|
|
2751
|
+
${paths}
|
|
2752
|
+
</svg>
|
|
2753
|
+
<div class="donut-legend">
|
|
2754
|
+
${legendItems}
|
|
2755
|
+
</div>
|
|
2756
|
+
</div>
|
|
2757
|
+
</div>`;
|
|
2758
|
+
}
|
|
2759
|
+
|
|
2760
|
+
/**
|
|
2761
|
+
* Render the analytics panel (4 charts horizontally)
|
|
2762
|
+
*/
|
|
2763
|
+
function renderAnalyticsPanel(analytics: HtmlConnectorAnalyticsV1): string {
|
|
2764
|
+
return `
|
|
2765
|
+
<div class="analytics-panel">
|
|
2766
|
+
${renderHeatmap(analytics.heatmap)}
|
|
2767
|
+
${renderMethodLatencyChart(analytics.method_latency)}
|
|
2768
|
+
${renderTopTools(analytics.top_tools)}
|
|
2769
|
+
${renderMethodDistribution(analytics.method_distribution)}
|
|
2770
|
+
</div>`;
|
|
2771
|
+
}
|
|
2772
|
+
|
|
2773
|
+
/**
|
|
2774
|
+
* Render a session item for the sessions pane (compact grid view)
|
|
2775
|
+
*/
|
|
2776
|
+
function renderConnectorSessionItem(session: HtmlConnectorSessionRow): string {
|
|
2777
|
+
// Format timestamp compactly: MM/DD HH:MM
|
|
2778
|
+
const timestamp = formatCompactTimestamp(session.started_at);
|
|
2779
|
+
const latencyStr = session.total_latency_ms !== null
|
|
2780
|
+
? `${session.total_latency_ms}ms`
|
|
2781
|
+
: '-';
|
|
2782
|
+
|
|
2783
|
+
return `
|
|
2784
|
+
<div class="session-item"
|
|
2785
|
+
data-session-id="${escapeHtml(session.session_id)}"
|
|
2786
|
+
title="Session: ${session.session_id} Started: ${session.started_at} RPCs: ${session.rpc_count} Events: ${session.event_count} Errors: ${session.error_count}">
|
|
2787
|
+
<span class="session-id">[${escapeHtml(session.short_id)}]</span>
|
|
2788
|
+
<span class="session-timestamp">${timestamp}</span>
|
|
2789
|
+
<span class="session-counts"><span>R:${session.rpc_count}</span><span>E:${session.event_count}</span></span>
|
|
2790
|
+
<span class="session-latency">${latencyStr}</span>
|
|
2791
|
+
<span class="session-extra"></span>
|
|
2792
|
+
</div>`;
|
|
2793
|
+
}
|
|
2794
|
+
|
|
2795
|
+
/**
|
|
2796
|
+
* Format timestamp compactly for grid display (UTC)
|
|
2797
|
+
* Returns format: HH:MM:SS.mmm (time with milliseconds)
|
|
2798
|
+
* @public - exported for testing
|
|
2799
|
+
*/
|
|
2800
|
+
export function formatCompactTimestamp(isoStr: string): string {
|
|
2801
|
+
try {
|
|
2802
|
+
const date = new Date(isoStr);
|
|
2803
|
+
if (isNaN(date.getTime())) {
|
|
2804
|
+
return '-';
|
|
2805
|
+
}
|
|
2806
|
+
const hours = String(date.getUTCHours()).padStart(2, '0');
|
|
2807
|
+
const minutes = String(date.getUTCMinutes()).padStart(2, '0');
|
|
2808
|
+
const seconds = String(date.getUTCSeconds()).padStart(2, '0');
|
|
2809
|
+
const millis = String(date.getUTCMilliseconds()).padStart(3, '0');
|
|
2810
|
+
return `${hours}:${minutes}:${seconds}.${millis}`;
|
|
2811
|
+
} catch {
|
|
2812
|
+
return '-';
|
|
2813
|
+
}
|
|
2814
|
+
}
|
|
2815
|
+
|
|
2816
|
+
/**
|
|
2817
|
+
* Render session detail content (reuses session HTML layout)
|
|
2818
|
+
*/
|
|
2819
|
+
/**
|
|
2820
|
+
* Render a single RPC detail HTML (pre-rendered for direct DOM insertion)
|
|
2821
|
+
* This avoids JavaScript string concatenation issues with special characters
|
|
2822
|
+
*/
|
|
2823
|
+
function renderRpcDetailHtml(rpc: SessionRpcDetail, idx: number): string {
|
|
2824
|
+
const statusClass = `status-${rpc.status}`;
|
|
2825
|
+
const statusSymbol = getStatusSymbol(rpc.status);
|
|
2826
|
+
const latency = rpc.latency_ms !== null ? `${rpc.latency_ms}ms` : '(pending)';
|
|
2827
|
+
|
|
2828
|
+
// Pre-render summary and raw JSON HTML
|
|
2829
|
+
const requestSummaryRows = renderRequestSummary(rpc.method, rpc.request.json);
|
|
2830
|
+
const responseSummaryRows = renderResponseSummary(rpc.method, rpc.response.json);
|
|
2831
|
+
const requestSummaryHtml = renderSummaryRowsHtml(requestSummaryRows);
|
|
2832
|
+
const responseSummaryHtml = renderSummaryRowsHtml(responseSummaryRows);
|
|
2833
|
+
const requestRawHtml = renderJsonWithPaths(rpc.request.json, '#');
|
|
2834
|
+
const responseRawHtml = renderJsonWithPaths(rpc.response.json, '#');
|
|
2835
|
+
|
|
2836
|
+
// Detect sensitive keys
|
|
2837
|
+
const reqSensitiveKeys = detectSensitiveKeys(rpc.request.json);
|
|
2838
|
+
const resSensitiveKeys = detectSensitiveKeys(rpc.response.json);
|
|
2839
|
+
const allSensitiveKeys = [...reqSensitiveKeys, ...resSensitiveKeys];
|
|
2840
|
+
const hasSensitive = allSensitiveKeys.length > 0;
|
|
2841
|
+
|
|
2842
|
+
// Sensitive badge
|
|
2843
|
+
let sensitiveBadge = '';
|
|
2844
|
+
if (hasSensitive) {
|
|
2845
|
+
const escapedKeys = allSensitiveKeys.map(k => escapeHtml(k));
|
|
2846
|
+
const sensitiveTooltip = escapedKeys.length > 5
|
|
2847
|
+
? `Contains ${escapedKeys.length} sensitive keys: ${escapedKeys.slice(0, 5).join(', ')}...`
|
|
2848
|
+
: `Contains sensitive keys: ${escapedKeys.join(', ')}`;
|
|
2849
|
+
sensitiveBadge = `<span class="sensitive-badge" title="${escapeHtml(sensitiveTooltip)}">⚠ Sensitive</span>`;
|
|
2850
|
+
}
|
|
2851
|
+
|
|
2852
|
+
// Determine default target based on method
|
|
2853
|
+
const defaultTarget = (rpc.method === 'tools/list' || rpc.method === 'initialize' || rpc.method.startsWith('resources/') || rpc.method.startsWith('prompts/')) ? 'response' : 'request';
|
|
2854
|
+
|
|
2855
|
+
return `<div class="rpc-detail-content" data-rpc-idx="${idx}" style="display: none;">
|
|
2856
|
+
<div class="detail-section">
|
|
2857
|
+
<h2>RPC Info${sensitiveBadge}</h2>
|
|
2858
|
+
<div class="rpc-info-grid">
|
|
2859
|
+
<div class="rpc-info-item"><dt>RPC ID</dt><dd><span class="badge">${escapeHtml(rpc.rpc_id)}</span></dd></div>
|
|
2860
|
+
<div class="rpc-info-item"><dt>Method</dt><dd><span class="badge">${escapeHtml(rpc.method)}</span></dd></div>
|
|
2861
|
+
<div class="rpc-info-item"><dt>Status</dt><dd><span class="badge ${statusClass}">${statusSymbol} ${rpc.status}${rpc.error_code !== null ? ` (code: ${rpc.error_code})` : ''}</span></dd></div>
|
|
2862
|
+
<div class="rpc-info-item"><dt>Latency</dt><dd><span class="badge">${latency}</span></dd></div>
|
|
2863
|
+
<div class="rpc-info-item"><dt>Request</dt><dd>${escapeHtml(rpc.request_ts)}</dd></div>
|
|
2864
|
+
<div class="rpc-info-item"><dt>Response</dt><dd>${escapeHtml(rpc.response_ts || '-')}</dd></div>
|
|
2865
|
+
</div>
|
|
2866
|
+
</div>
|
|
2867
|
+
<div class="detail-section">
|
|
2868
|
+
<div class="rpc-toggle-bar">
|
|
2869
|
+
<button class="rpc-toggle-btn${defaultTarget === 'request' ? ' active' : ''}" data-target="request">[Req]</button>
|
|
2870
|
+
<button class="rpc-toggle-btn${defaultTarget === 'response' ? ' active' : ''}" data-target="response">[Res]</button>
|
|
2871
|
+
</div>
|
|
2872
|
+
<div class="rpc-inspector">
|
|
2873
|
+
<div class="rpc-inspector-summary">
|
|
2874
|
+
<h3>Summary</h3>
|
|
2875
|
+
<div class="summary-request" style="display: ${defaultTarget === 'request' ? 'block' : 'none'}">${requestSummaryHtml}</div>
|
|
2876
|
+
<div class="summary-response" style="display: ${defaultTarget === 'response' ? 'block' : 'none'}">${responseSummaryHtml}</div>
|
|
2877
|
+
</div>
|
|
2878
|
+
<div class="rpc-inspector-raw">
|
|
2879
|
+
<div class="rpc-raw-json">
|
|
2880
|
+
<div class="raw-json-request" style="display: ${defaultTarget === 'request' ? 'block' : 'none'}">${requestRawHtml}</div>
|
|
2881
|
+
<div class="raw-json-response" style="display: ${defaultTarget === 'response' ? 'block' : 'none'}">${responseRawHtml}</div>
|
|
2882
|
+
</div>
|
|
2883
|
+
</div>
|
|
2884
|
+
</div>
|
|
2885
|
+
</div>
|
|
2886
|
+
</div>`;
|
|
2887
|
+
}
|
|
2888
|
+
|
|
2889
|
+
function renderSessionDetailContent(
|
|
2890
|
+
sessionId: string,
|
|
2891
|
+
report: HtmlSessionReportV1
|
|
2892
|
+
): string {
|
|
2893
|
+
const { session, rpcs } = report;
|
|
2894
|
+
|
|
2895
|
+
const totalLatencyDisplay = session.total_latency_ms !== null
|
|
2896
|
+
? `${session.total_latency_ms}ms`
|
|
2897
|
+
: '-';
|
|
2898
|
+
|
|
2899
|
+
const rpcRows = rpcs.map((rpc, idx) => {
|
|
2900
|
+
const statusClass = `status-${rpc.status}`;
|
|
2901
|
+
const statusSymbol = getStatusSymbol(rpc.status);
|
|
2902
|
+
const rpcIdShort = rpc.rpc_id.slice(0, SHORT_ID_LENGTH);
|
|
2903
|
+
const timeShort = formatTimestamp(rpc.request_ts).split(' ')[1]?.slice(0, 12) || '-';
|
|
2904
|
+
const latency = rpc.latency_ms !== null ? `${rpc.latency_ms}ms` : '-';
|
|
2905
|
+
|
|
2906
|
+
return `
|
|
2907
|
+
<tr class="rpc-row" data-rpc-idx="${idx}">
|
|
2908
|
+
<td>${timeShort}</td>
|
|
2909
|
+
<td><span class="badge ${statusClass}">${statusSymbol}</span></td>
|
|
2910
|
+
<td><span class="badge">${escapeHtml(rpcIdShort)}</span></td>
|
|
2911
|
+
<td>${escapeHtml(rpc.method)}</td>
|
|
2912
|
+
<td>${latency}</td>
|
|
2913
|
+
</tr>`;
|
|
2914
|
+
}).join('\n');
|
|
2915
|
+
|
|
2916
|
+
// Pre-render all RPC detail HTML (avoids JS string concatenation issues)
|
|
2917
|
+
const rpcDetailDivs = rpcs.map((rpc, idx) => renderRpcDetailHtml(rpc, idx)).join('\n');
|
|
2918
|
+
|
|
2919
|
+
return `
|
|
2920
|
+
<div class="session-content" data-session-id="${escapeHtml(sessionId)}">
|
|
2921
|
+
<div class="inner-container">
|
|
2922
|
+
<div class="left-pane">
|
|
2923
|
+
<div class="session-info">
|
|
2924
|
+
<h2>Session Info</h2>
|
|
2925
|
+
<dl>
|
|
2926
|
+
<dt>Session ID</dt>
|
|
2927
|
+
<dd><span class="badge">${escapeHtml(session.session_id)}</span></dd>
|
|
2928
|
+
<dt>Started</dt>
|
|
2929
|
+
<dd>${formatTimestamp(session.started_at)}</dd>
|
|
2930
|
+
<dt>Ended</dt>
|
|
2931
|
+
<dd>${session.ended_at ? formatTimestamp(session.ended_at) : '(active)'}</dd>
|
|
2932
|
+
<dt>Exit Reason</dt>
|
|
2933
|
+
<dd>${session.exit_reason || '(none)'}</dd>
|
|
2934
|
+
<dt>RPC Count</dt>
|
|
2935
|
+
<dd><span class="badge">${session.rpc_count}</span></dd>
|
|
2936
|
+
<dt>Event Count</dt>
|
|
2937
|
+
<dd><span class="badge">${session.event_count}</span></dd>
|
|
2938
|
+
<dt>Total Latency</dt>
|
|
2939
|
+
<dd><span class="badge">${totalLatencyDisplay}</span></dd>
|
|
2940
|
+
</dl>
|
|
2941
|
+
</div>
|
|
2942
|
+
<div class="view-toggle">
|
|
2943
|
+
<button class="view-toggle-btn active" data-view="rpc">
|
|
2944
|
+
RPCs<span class="view-toggle-count">(${session.rpc_count})</span>
|
|
2945
|
+
</button>
|
|
2946
|
+
<button class="view-toggle-btn" data-view="events">
|
|
2947
|
+
Events<span class="view-toggle-count">(${session.event_count})</span>
|
|
2948
|
+
</button>
|
|
2949
|
+
</div>
|
|
2950
|
+
<div class="rpc-list">
|
|
2951
|
+
<table class="rpc-table">
|
|
2952
|
+
<thead>
|
|
2953
|
+
<tr>
|
|
2954
|
+
<th>Time</th>
|
|
2955
|
+
<th>St</th>
|
|
2956
|
+
<th>ID</th>
|
|
2957
|
+
<th>Method</th>
|
|
2958
|
+
<th>Latency</th>
|
|
2959
|
+
</tr>
|
|
2960
|
+
</thead>
|
|
2961
|
+
<tbody>
|
|
2962
|
+
${rpcRows}
|
|
2963
|
+
</tbody>
|
|
2964
|
+
</table>
|
|
2965
|
+
</div>
|
|
2966
|
+
<div class="events-list">
|
|
2967
|
+
<div class="events-loading">Loading events...</div>
|
|
2968
|
+
</div>
|
|
2969
|
+
</div>
|
|
2970
|
+
<div class="resize-handle"></div>
|
|
2971
|
+
<div class="right-pane">
|
|
2972
|
+
<div class="detail-placeholder">
|
|
2973
|
+
${rpcs.length > 0 ? 'Select an RPC call from the list to view details' : 'No RPC calls in this session'}
|
|
2974
|
+
</div>
|
|
2975
|
+
<div class="rpc-details-container" style="display: none;">
|
|
2976
|
+
${rpcDetailDivs}
|
|
2977
|
+
</div>
|
|
2978
|
+
</div>
|
|
2979
|
+
</div>
|
|
2980
|
+
</div>`;
|
|
2981
|
+
}
|
|
2982
|
+
|
|
2983
|
+
/**
|
|
2984
|
+
* Generate Connector HTML report (3-hierarchy: Connector -> Sessions -> RPCs)
|
|
2985
|
+
*/
|
|
2986
|
+
export function generateConnectorHtml(report: HtmlConnectorReportV1): string {
|
|
2987
|
+
const { meta, connector, sessions, session_reports, analytics } = report;
|
|
2988
|
+
|
|
2989
|
+
// Pagination info
|
|
2990
|
+
const fromNum = connector.offset + 1;
|
|
2991
|
+
const toNum = connector.offset + connector.displayed_sessions;
|
|
2992
|
+
const paginationInfo = connector.session_count > 0
|
|
2993
|
+
? `Showing ${fromNum}-${toNum} of ${connector.session_count} sessions`
|
|
2994
|
+
: 'No sessions';
|
|
2995
|
+
|
|
2996
|
+
// Connector info section
|
|
2997
|
+
const transportDisplay = connector.transport.type === 'stdio'
|
|
2998
|
+
? connector.transport.command || '(unknown command)'
|
|
2999
|
+
: connector.transport.url || '(unknown URL)';
|
|
3000
|
+
|
|
3001
|
+
// Server Response Info card (if available, from initialize response)
|
|
3002
|
+
let serverResponseInfoCard = '';
|
|
3003
|
+
if (connector.server) {
|
|
3004
|
+
const { name, version, protocolVersion, capabilities } = connector.server;
|
|
3005
|
+
const serverName = name || '(unknown)';
|
|
3006
|
+
const serverVersion = version ? `v${version}` : '';
|
|
3007
|
+
const protocolDisplay = protocolVersion ? `MCP ${protocolVersion}` : 'Unknown';
|
|
3008
|
+
|
|
3009
|
+
// Capabilities badges with all options (active/inactive state)
|
|
3010
|
+
const allCaps = ['tools', 'resources', 'prompts'] as const;
|
|
3011
|
+
const capBadges = allCaps.map(cap => {
|
|
3012
|
+
const isActive = capabilities[cap];
|
|
3013
|
+
const cls = isActive ? 'badge cap-enabled' : 'badge cap-disabled';
|
|
3014
|
+
return `<span class="${cls}">${cap}</span>`;
|
|
3015
|
+
}).join(' ');
|
|
3016
|
+
|
|
3017
|
+
serverResponseInfoCard = `
|
|
3018
|
+
<div class="connector-info expanded">
|
|
3019
|
+
<div class="connector-info-toggle">
|
|
3020
|
+
<h2>Server Response Info</h2>
|
|
3021
|
+
<span class="toggle-icon">▼</span>
|
|
3022
|
+
</div>
|
|
3023
|
+
<div class="connector-info-content">
|
|
3024
|
+
<dl>
|
|
3025
|
+
<dt>Server</dt>
|
|
3026
|
+
<dd><code>${escapeHtml(serverName)} ${escapeHtml(serverVersion)}</code></dd>
|
|
3027
|
+
<dt>Protocol</dt>
|
|
3028
|
+
<dd><span class="badge">${escapeHtml(protocolDisplay)}</span></dd>
|
|
3029
|
+
<dt>Capabilities</dt>
|
|
3030
|
+
<dd>${capBadges}</dd>
|
|
3031
|
+
</dl>
|
|
3032
|
+
</div>
|
|
3033
|
+
</div>`;
|
|
3034
|
+
}
|
|
3035
|
+
|
|
3036
|
+
// Session items
|
|
3037
|
+
const sessionItems = sessions.map(s => renderConnectorSessionItem(s)).join('\n');
|
|
3038
|
+
|
|
3039
|
+
// Session contents (pre-rendered, hidden by default)
|
|
3040
|
+
const sessionContents = sessions.map(s => {
|
|
3041
|
+
const sessionReport = session_reports[s.session_id];
|
|
3042
|
+
if (!sessionReport) return '';
|
|
3043
|
+
return renderSessionDetailContent(s.session_id, sessionReport);
|
|
3044
|
+
}).join('\n');
|
|
3045
|
+
|
|
3046
|
+
// Pre-render summary and raw JSON HTML for each RPC in each session (for RPC Inspector)
|
|
3047
|
+
// Now generates separate request/response summaries for Req/Res toggle
|
|
3048
|
+
const sessionReportsWithInspectorHtml: Record<string, HtmlSessionReportV1 & { rpcs: Array<SessionRpcDetail & { _requestSummaryHtml: string; _responseSummaryHtml: string; _requestRawHtml: string; _responseRawHtml: string }> }> = {};
|
|
3049
|
+
for (const [sessionId, sessionReport] of Object.entries(session_reports)) {
|
|
3050
|
+
sessionReportsWithInspectorHtml[sessionId] = {
|
|
3051
|
+
...sessionReport,
|
|
3052
|
+
rpcs: sessionReport.rpcs.map((rpc) => {
|
|
3053
|
+
const requestSummaryRows = renderRequestSummary(rpc.method, rpc.request.json);
|
|
3054
|
+
const responseSummaryRows = renderResponseSummary(rpc.method, rpc.response.json);
|
|
3055
|
+
// Detect sensitive keys in request/response (Phase 12.x-c)
|
|
3056
|
+
const reqSensitiveKeys = detectSensitiveKeys(rpc.request.json);
|
|
3057
|
+
const resSensitiveKeys = detectSensitiveKeys(rpc.response.json);
|
|
3058
|
+
const hasSensitive = reqSensitiveKeys.length > 0 || resSensitiveKeys.length > 0;
|
|
3059
|
+
return {
|
|
3060
|
+
...rpc,
|
|
3061
|
+
_requestSummaryHtml: renderSummaryRowsHtml(requestSummaryRows),
|
|
3062
|
+
_responseSummaryHtml: renderSummaryRowsHtml(responseSummaryRows),
|
|
3063
|
+
_requestRawHtml: renderJsonWithPaths(rpc.request.json, '#'),
|
|
3064
|
+
_responseRawHtml: renderJsonWithPaths(rpc.response.json, '#'),
|
|
3065
|
+
_hasSensitive: hasSensitive,
|
|
3066
|
+
_sensitiveKeys: [...reqSensitiveKeys, ...resSensitiveKeys],
|
|
3067
|
+
};
|
|
3068
|
+
}),
|
|
3069
|
+
};
|
|
3070
|
+
}
|
|
3071
|
+
|
|
3072
|
+
const reportWithInspectorHtml = {
|
|
3073
|
+
...report,
|
|
3074
|
+
session_reports: sessionReportsWithInspectorHtml,
|
|
3075
|
+
};
|
|
3076
|
+
const embeddedJson = escapeJsonForScript(JSON.stringify(reportWithInspectorHtml));
|
|
3077
|
+
|
|
3078
|
+
return `<!DOCTYPE html>
|
|
3079
|
+
<html lang="en">
|
|
3080
|
+
<head>
|
|
3081
|
+
<meta charset="UTF-8">
|
|
3082
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
3083
|
+
<title>Connector: ${escapeHtml(connector.target_id)} - proofscan</title>
|
|
3084
|
+
<style>${getConnectorReportStyles()}</style>
|
|
3085
|
+
</head>
|
|
3086
|
+
<body>
|
|
3087
|
+
<header class="header">
|
|
3088
|
+
<div class="header-left">
|
|
3089
|
+
<div class="header-title">ProofScan Monitor</div>
|
|
3090
|
+
<a href="/" class="header-back">← Home</a>
|
|
3091
|
+
</div>
|
|
3092
|
+
<div class="header-meta">
|
|
3093
|
+
<div class="auto-check-toggle" id="autoCheckToggle">
|
|
3094
|
+
<span class="auto-check-label">Auto-check:</span>
|
|
3095
|
+
<button data-enabled="false" class="active">OFF</button>
|
|
3096
|
+
<button data-enabled="true">ON</button>
|
|
3097
|
+
</div>
|
|
3098
|
+
<div class="new-data-banner" id="newDataBanner">
|
|
3099
|
+
<span>New data available</span>
|
|
3100
|
+
<button id="refreshNowBtn">Refresh now</button>
|
|
3101
|
+
</div>
|
|
3102
|
+
<span class="offline-badge">Offline</span>
|
|
3103
|
+
<span>Generated: ${formatTimestamp(meta.generatedAt)}${meta.redacted ? ' (redacted)' : ''}</span>
|
|
3104
|
+
</div>
|
|
3105
|
+
</header>
|
|
3106
|
+
<div class="page-header">
|
|
3107
|
+
<div class="page-header-left">
|
|
3108
|
+
<h1>Connector: <span class="badge">${escapeHtml(connector.target_id)}</span></h1>
|
|
3109
|
+
</div>
|
|
3110
|
+
${renderKpiRow(analytics.kpis)}
|
|
3111
|
+
</div>
|
|
3112
|
+
|
|
3113
|
+
<div class="connector-top">
|
|
3114
|
+
<div class="connector-info-cards">
|
|
3115
|
+
<div class="connector-info expanded">
|
|
3116
|
+
<div class="connector-info-toggle">
|
|
3117
|
+
<h2>Connector Info</h2>
|
|
3118
|
+
<span class="toggle-icon">▼</span>
|
|
3119
|
+
</div>
|
|
3120
|
+
<div class="connector-info-content">
|
|
3121
|
+
<dl>
|
|
3122
|
+
<dt>Transport</dt>
|
|
3123
|
+
<dd><span class="badge">${escapeHtml(connector.transport.type)}</span></dd>
|
|
3124
|
+
<dt>${connector.transport.type === 'stdio' ? 'Command' : 'URL'}</dt>
|
|
3125
|
+
<dd><code>${escapeHtml(transportDisplay)}</code></dd>
|
|
3126
|
+
<dt>Enabled</dt>
|
|
3127
|
+
<dd>${connector.enabled ? '<span class="badge status-OK">yes</span>' : '<span class="badge status-ERR">no</span>'}</dd>
|
|
3128
|
+
</dl>
|
|
3129
|
+
</div>
|
|
3130
|
+
</div>
|
|
3131
|
+
${serverResponseInfoCard}
|
|
3132
|
+
</div>
|
|
3133
|
+
${renderAnalyticsPanel(analytics)}
|
|
3134
|
+
</div>
|
|
3135
|
+
|
|
3136
|
+
<div class="main-container">
|
|
3137
|
+
<div class="sessions-pane">
|
|
3138
|
+
<div class="sessions-header">
|
|
3139
|
+
<h2>Sessions</h2>
|
|
3140
|
+
<span class="pagination-info">${paginationInfo}</span>
|
|
3141
|
+
</div>
|
|
3142
|
+
<div class="sessions-header-row">
|
|
3143
|
+
<span>ID</span>
|
|
3144
|
+
<span>Time (UTC)</span>
|
|
3145
|
+
<span style="text-align:right">Latency</span>
|
|
3146
|
+
<span></span>
|
|
3147
|
+
</div>
|
|
3148
|
+
<div class="sessions-list">
|
|
3149
|
+
${sessionItems}
|
|
3150
|
+
</div>
|
|
3151
|
+
</div>
|
|
3152
|
+
|
|
3153
|
+
<div class="session-detail-pane">
|
|
3154
|
+
${sessions.length === 0 ? '<div class="session-detail-empty">No sessions available</div>' : ''}
|
|
3155
|
+
${sessionContents}
|
|
3156
|
+
</div>
|
|
3157
|
+
</div>
|
|
3158
|
+
|
|
3159
|
+
<script type="application/json" id="report-data">${embeddedJson}</script>
|
|
3160
|
+
<script>${getConnectorReportScript()}</script>
|
|
3161
|
+
</body>
|
|
3162
|
+
</html>`;
|
|
3163
|
+
}
|