proofscan 0.10.26 → 0.10.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +4 -2
- package/dist/cli.js.map +1 -1
- package/dist/commands/catalog.d.ts.map +1 -1
- package/dist/commands/catalog.js +22 -0
- package/dist/commands/catalog.js.map +1 -1
- package/dist/commands/connectors.d.ts.map +1 -1
- package/dist/commands/connectors.js +10 -3
- 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 +2 -0
- package/dist/commands/index.js.map +1 -1
- package/dist/commands/monitor.d.ts +6 -0
- package/dist/commands/monitor.d.ts.map +1 -0
- package/dist/commands/monitor.js +59 -0
- package/dist/commands/monitor.js.map +1 -0
- package/dist/html/analytics.d.ts +58 -0
- package/dist/html/analytics.d.ts.map +1 -0
- package/dist/html/analytics.js +337 -0
- package/dist/html/analytics.js.map +1 -0
- package/dist/html/index.d.ts +5 -2
- package/dist/html/index.d.ts.map +1 -1
- package/dist/html/index.js +5 -1
- package/dist/html/index.js.map +1 -1
- package/dist/html/rpc-inspector.d.ts +43 -0
- package/dist/html/rpc-inspector.d.ts.map +1 -0
- package/dist/html/rpc-inspector.js +922 -0
- package/dist/html/rpc-inspector.js.map +1 -0
- package/dist/html/templates.d.ts +9 -1
- package/dist/html/templates.d.ts.map +1 -1
- package/dist/html/templates.js +701 -78
- package/dist/html/templates.js.map +1 -1
- package/dist/html/types.d.ts +129 -0
- package/dist/html/types.d.ts.map +1 -1
- package/dist/html/types.js.map +1 -1
- package/dist/monitor/data/aggregator.d.ts +13 -0
- package/dist/monitor/data/aggregator.d.ts.map +1 -0
- package/dist/monitor/data/aggregator.js +101 -0
- package/dist/monitor/data/aggregator.js.map +1 -0
- package/dist/monitor/data/connectors.d.ts +13 -0
- package/dist/monitor/data/connectors.d.ts.map +1 -0
- package/dist/monitor/data/connectors.js +326 -0
- package/dist/monitor/data/connectors.js.map +1 -0
- package/dist/monitor/data/popl.d.ts +30 -0
- package/dist/monitor/data/popl.d.ts.map +1 -0
- package/dist/monitor/data/popl.js +310 -0
- package/dist/monitor/data/popl.js.map +1 -0
- package/dist/monitor/index.d.ts +6 -0
- package/dist/monitor/index.d.ts.map +1 -0
- package/dist/monitor/index.js +6 -0
- package/dist/monitor/index.js.map +1 -0
- package/dist/monitor/routes/api.d.ts +7 -0
- package/dist/monitor/routes/api.d.ts.map +1 -0
- package/dist/monitor/routes/api.js +63 -0
- package/dist/monitor/routes/api.js.map +1 -0
- package/dist/monitor/routes/connectors.d.ts +7 -0
- package/dist/monitor/routes/connectors.d.ts.map +1 -0
- package/dist/monitor/routes/connectors.js +417 -0
- package/dist/monitor/routes/connectors.js.map +1 -0
- package/dist/monitor/routes/home.d.ts +7 -0
- package/dist/monitor/routes/home.d.ts.map +1 -0
- package/dist/monitor/routes/home.js +15 -0
- package/dist/monitor/routes/home.js.map +1 -0
- package/dist/monitor/routes/index.d.ts +10 -0
- package/dist/monitor/routes/index.d.ts.map +1 -0
- package/dist/monitor/routes/index.js +19 -0
- package/dist/monitor/routes/index.js.map +1 -0
- package/dist/monitor/routes/popl.d.ts +7 -0
- package/dist/monitor/routes/popl.d.ts.map +1 -0
- package/dist/monitor/routes/popl.js +84 -0
- package/dist/monitor/routes/popl.js.map +1 -0
- package/dist/monitor/server.d.ts +24 -0
- package/dist/monitor/server.d.ts.map +1 -0
- package/dist/monitor/server.js +52 -0
- package/dist/monitor/server.js.map +1 -0
- package/dist/monitor/templates/components.d.ts +21 -0
- package/dist/monitor/templates/components.d.ts.map +1 -0
- package/dist/monitor/templates/components.js +405 -0
- package/dist/monitor/templates/components.js.map +1 -0
- package/dist/monitor/templates/home.d.ts +9 -0
- package/dist/monitor/templates/home.d.ts.map +1 -0
- package/dist/monitor/templates/home.js +322 -0
- package/dist/monitor/templates/home.js.map +1 -0
- package/dist/monitor/templates/layout.d.ts +26 -0
- package/dist/monitor/templates/layout.d.ts.map +1 -0
- package/dist/monitor/templates/layout.js +186 -0
- package/dist/monitor/templates/layout.js.map +1 -0
- package/dist/monitor/templates/popl.d.ts +33 -0
- package/dist/monitor/templates/popl.d.ts.map +1 -0
- package/dist/monitor/templates/popl.js +654 -0
- package/dist/monitor/templates/popl.js.map +1 -0
- package/dist/monitor/types.d.ts +121 -0
- package/dist/monitor/types.d.ts.map +1 -0
- package/dist/monitor/types.js +5 -0
- package/dist/monitor/types.js.map +1 -0
- package/package.json +3 -1
package/dist/html/templates.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { formatBytes } from '../eventline/types.js';
|
|
8
8
|
import { getStatusSymbol, SHORT_ID_LENGTH } from './types.js';
|
|
9
|
+
import { getRpcInspectorStyles, getRpcInspectorScript, renderJsonWithPaths, renderMethodSummary, renderSummaryRowsHtml, } from './rpc-inspector.js';
|
|
9
10
|
/**
|
|
10
11
|
* Escape HTML special characters to prevent XSS
|
|
11
12
|
*/
|
|
@@ -359,6 +360,8 @@ function getSessionReportStyles() {
|
|
|
359
360
|
.resize-handle:hover {
|
|
360
361
|
background: var(--accent-blue);
|
|
361
362
|
}
|
|
363
|
+
/* RPC Inspector styles */
|
|
364
|
+
${getRpcInspectorStyles()}
|
|
362
365
|
`;
|
|
363
366
|
}
|
|
364
367
|
/**
|
|
@@ -443,7 +446,7 @@ function getSessionReportScript() {
|
|
|
443
446
|
'<pre id="' + elementId + '"><code>' + content + '</code></pre>';
|
|
444
447
|
}
|
|
445
448
|
|
|
446
|
-
// Show RPC detail in right pane
|
|
449
|
+
// Show RPC detail in right pane (2-column Wireshark-style layout)
|
|
447
450
|
function showRpcDetail(idx) {
|
|
448
451
|
if (idx < 0 || idx >= rpcs.length) return;
|
|
449
452
|
|
|
@@ -460,26 +463,49 @@ function getSessionReportScript() {
|
|
|
460
463
|
const statusSymbol = rpc.status === 'OK' ? '✓' : rpc.status === 'ERR' ? '✗' : '?';
|
|
461
464
|
const latency = rpc.latency_ms !== null ? rpc.latency_ms + 'ms' : '(pending)';
|
|
462
465
|
|
|
466
|
+
// Get pre-rendered summary and raw JSON from data attributes
|
|
467
|
+
const summaryHtml = rpc._summaryHtml || '<div class="summary-row summary-header">No summary available</div>';
|
|
468
|
+
const requestRawHtml = rpc._requestRawHtml || '<span class="json-null">(no data)</span>';
|
|
469
|
+
const responseRawHtml = rpc._responseRawHtml || '<span class="json-null">(no data)</span>';
|
|
470
|
+
|
|
471
|
+
// Determine default target based on method
|
|
472
|
+
const defaultTarget = (rpc.method === 'tools/list' || rpc.method.startsWith('resources/') || rpc.method.startsWith('prompts/')) ? 'response' : 'request';
|
|
473
|
+
|
|
463
474
|
rightPane.innerHTML =
|
|
464
475
|
'<div class="detail-section">' +
|
|
465
476
|
' <h2>RPC Info</h2>' +
|
|
466
|
-
' <
|
|
467
|
-
' <dt>RPC ID</dt><dd><span class="badge">' + escapeHtml(rpc.rpc_id) + '</span></dd>' +
|
|
468
|
-
' <dt>Method</dt><dd><span class="badge">' + escapeHtml(rpc.method) + '</span></dd>' +
|
|
469
|
-
' <dt>Status</dt><dd><span class="badge ' + statusClass + '">' + statusSymbol + ' ' + rpc.status + (rpc.error_code !== null ? ' (code: ' + rpc.error_code + ')' : '') + '</span></dd>' +
|
|
470
|
-
' <dt>Latency</dt><dd><span class="badge">' + latency + '</span></dd>' +
|
|
471
|
-
' <dt>
|
|
472
|
-
' <dt>
|
|
473
|
-
' </
|
|
474
|
-
'</div>' +
|
|
475
|
-
'<div class="detail-section">' +
|
|
476
|
-
' <h2>Request</h2>' +
|
|
477
|
-
' ' + renderPayload('', rpc.request, 'req-' + idx).replace('<h3> ', '').replace('</h3>', '') +
|
|
477
|
+
' <div class="rpc-info-grid">' +
|
|
478
|
+
' <div class="rpc-info-item"><dt>RPC ID</dt><dd><span class="badge">' + escapeHtml(rpc.rpc_id) + '</span></dd></div>' +
|
|
479
|
+
' <div class="rpc-info-item"><dt>Method</dt><dd><span class="badge">' + escapeHtml(rpc.method) + '</span></dd></div>' +
|
|
480
|
+
' <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>' +
|
|
481
|
+
' <div class="rpc-info-item"><dt>Latency</dt><dd><span class="badge">' + latency + '</span></dd></div>' +
|
|
482
|
+
' <div class="rpc-info-item"><dt>Req Size</dt><dd>' + formatBytes(rpc.request.size) + '</dd></div>' +
|
|
483
|
+
' <div class="rpc-info-item"><dt>Res Size</dt><dd>' + formatBytes(rpc.response.size) + '</dd></div>' +
|
|
484
|
+
' </div>' +
|
|
478
485
|
'</div>' +
|
|
479
486
|
'<div class="detail-section">' +
|
|
480
|
-
' <
|
|
481
|
-
'
|
|
487
|
+
' <div class="rpc-inspector">' +
|
|
488
|
+
' <div class="rpc-inspector-summary">' +
|
|
489
|
+
' <h3>Summary</h3>' +
|
|
490
|
+
summaryHtml +
|
|
491
|
+
' </div>' +
|
|
492
|
+
' <div class="rpc-inspector-raw">' +
|
|
493
|
+
' <div class="rpc-toggle-bar">' +
|
|
494
|
+
' <button id="toggle-req" class="rpc-toggle-btn' + (defaultTarget === 'request' ? ' active' : '') + '">[Req]</button>' +
|
|
495
|
+
' <button id="toggle-res" class="rpc-toggle-btn' + (defaultTarget === 'response' ? ' active' : '') + '">[Res]</button>' +
|
|
496
|
+
' </div>' +
|
|
497
|
+
' <div class="rpc-raw-json">' +
|
|
498
|
+
' <div id="raw-json-request" style="display:' + (defaultTarget === 'request' ? 'block' : 'none') + '">' + requestRawHtml + '</div>' +
|
|
499
|
+
' <div id="raw-json-response" style="display:' + (defaultTarget === 'response' ? 'block' : 'none') + '">' + responseRawHtml + '</div>' +
|
|
500
|
+
' </div>' +
|
|
501
|
+
' </div>' +
|
|
502
|
+
' </div>' +
|
|
482
503
|
'</div>';
|
|
504
|
+
|
|
505
|
+
// Re-initialize RPC Inspector handlers
|
|
506
|
+
if (window.initRpcInspector) {
|
|
507
|
+
window.initRpcInspector();
|
|
508
|
+
}
|
|
483
509
|
}
|
|
484
510
|
|
|
485
511
|
// Copy to clipboard
|
|
@@ -549,6 +575,9 @@ function getSessionReportScript() {
|
|
|
549
575
|
if (rpcs.length > 0) {
|
|
550
576
|
showRpcDetail(0);
|
|
551
577
|
}
|
|
578
|
+
|
|
579
|
+
// RPC Inspector script
|
|
580
|
+
${getRpcInspectorScript()}
|
|
552
581
|
`;
|
|
553
582
|
}
|
|
554
583
|
/**
|
|
@@ -683,7 +712,21 @@ export function generateSessionHtml(report) {
|
|
|
683
712
|
const { meta, session, rpcs } = report;
|
|
684
713
|
const sessionShort = shortenId(session.session_id, 12);
|
|
685
714
|
const rpcRows = rpcs.map((rpc, idx) => renderRpcRow(rpc, idx)).join('\n');
|
|
686
|
-
|
|
715
|
+
// Pre-render summary and raw JSON HTML for each RPC (for RPC Inspector)
|
|
716
|
+
const rpcsWithInspectorHtml = rpcs.map((rpc) => {
|
|
717
|
+
const summaryRows = renderMethodSummary(rpc.method, rpc.request.json, rpc.response.json);
|
|
718
|
+
return {
|
|
719
|
+
...rpc,
|
|
720
|
+
_summaryHtml: renderSummaryRowsHtml(summaryRows),
|
|
721
|
+
_requestRawHtml: renderJsonWithPaths(rpc.request.json, '#'),
|
|
722
|
+
_responseRawHtml: renderJsonWithPaths(rpc.response.json, '#'),
|
|
723
|
+
};
|
|
724
|
+
});
|
|
725
|
+
const reportWithInspectorHtml = {
|
|
726
|
+
...report,
|
|
727
|
+
rpcs: rpcsWithInspectorHtml,
|
|
728
|
+
};
|
|
729
|
+
const embeddedJson = escapeJsonForScript(JSON.stringify(reportWithInspectorHtml));
|
|
687
730
|
// Format total latency
|
|
688
731
|
const totalLatencyDisplay = session.total_latency_ms !== null
|
|
689
732
|
? `${session.total_latency_ms}ms`
|
|
@@ -933,6 +976,13 @@ function getConnectorReportStyles() {
|
|
|
933
976
|
border-color: var(--accent-blue);
|
|
934
977
|
background: rgba(0, 212, 255, 0.15);
|
|
935
978
|
}
|
|
979
|
+
.session-item.highlight {
|
|
980
|
+
animation: highlightPulse 2s ease-out;
|
|
981
|
+
}
|
|
982
|
+
@keyframes highlightPulse {
|
|
983
|
+
0% { box-shadow: 0 0 0 3px rgba(0, 212, 255, 0.6); }
|
|
984
|
+
100% { box-shadow: 0 0 0 0 rgba(0, 212, 255, 0); }
|
|
985
|
+
}
|
|
936
986
|
.session-item-header {
|
|
937
987
|
display: flex;
|
|
938
988
|
align-items: center;
|
|
@@ -1107,6 +1157,228 @@ function getConnectorReportStyles() {
|
|
|
1107
1157
|
.resize-handle:hover {
|
|
1108
1158
|
background: var(--accent-blue);
|
|
1109
1159
|
}
|
|
1160
|
+
|
|
1161
|
+
/* Analytics Panel (Phase 5.2) - Revised Layout */
|
|
1162
|
+
|
|
1163
|
+
/* Header with KPI stats inline */
|
|
1164
|
+
header {
|
|
1165
|
+
display: flex;
|
|
1166
|
+
align-items: center;
|
|
1167
|
+
justify-content: space-between;
|
|
1168
|
+
flex-wrap: wrap;
|
|
1169
|
+
gap: 12px;
|
|
1170
|
+
}
|
|
1171
|
+
.header-left {
|
|
1172
|
+
flex-shrink: 0;
|
|
1173
|
+
}
|
|
1174
|
+
.header-server-row {
|
|
1175
|
+
display: flex;
|
|
1176
|
+
flex-direction: column;
|
|
1177
|
+
align-items: center;
|
|
1178
|
+
gap: 4px;
|
|
1179
|
+
}
|
|
1180
|
+
.header-caps {
|
|
1181
|
+
display: flex;
|
|
1182
|
+
gap: 4px;
|
|
1183
|
+
}
|
|
1184
|
+
.header-server-info {
|
|
1185
|
+
display: flex;
|
|
1186
|
+
gap: 12px;
|
|
1187
|
+
font-size: 0.75em;
|
|
1188
|
+
color: var(--text-secondary);
|
|
1189
|
+
}
|
|
1190
|
+
.header-server-info .server-name {
|
|
1191
|
+
color: var(--text-primary);
|
|
1192
|
+
}
|
|
1193
|
+
.header-label {
|
|
1194
|
+
color: var(--text-secondary);
|
|
1195
|
+
font-size: 0.9em;
|
|
1196
|
+
}
|
|
1197
|
+
.no-caps {
|
|
1198
|
+
color: var(--text-secondary);
|
|
1199
|
+
font-style: italic;
|
|
1200
|
+
}
|
|
1201
|
+
.kpi-row {
|
|
1202
|
+
display: flex;
|
|
1203
|
+
gap: 16px;
|
|
1204
|
+
flex-wrap: wrap;
|
|
1205
|
+
align-items: baseline;
|
|
1206
|
+
}
|
|
1207
|
+
.kpi-item {
|
|
1208
|
+
display: flex;
|
|
1209
|
+
flex-direction: column;
|
|
1210
|
+
align-items: center;
|
|
1211
|
+
padding: 0;
|
|
1212
|
+
background: transparent;
|
|
1213
|
+
min-width: 50px;
|
|
1214
|
+
}
|
|
1215
|
+
.kpi-item .kpi-value {
|
|
1216
|
+
font-size: 0.95em;
|
|
1217
|
+
font-weight: 600;
|
|
1218
|
+
color: var(--accent-blue);
|
|
1219
|
+
font-family: 'SFMono-Regular', Consolas, monospace;
|
|
1220
|
+
line-height: 1.2;
|
|
1221
|
+
}
|
|
1222
|
+
.kpi-item .kpi-label {
|
|
1223
|
+
font-size: 0.55em;
|
|
1224
|
+
color: var(--text-secondary);
|
|
1225
|
+
text-transform: uppercase;
|
|
1226
|
+
letter-spacing: 0.5px;
|
|
1227
|
+
}
|
|
1228
|
+
/* All KPI values use accent-blue for unified appearance */
|
|
1229
|
+
|
|
1230
|
+
/* Connector top section: info + charts row */
|
|
1231
|
+
.connector-top {
|
|
1232
|
+
display: flex;
|
|
1233
|
+
gap: 16px;
|
|
1234
|
+
padding: 12px 20px;
|
|
1235
|
+
border-bottom: 1px solid var(--border-color);
|
|
1236
|
+
background: var(--bg-secondary);
|
|
1237
|
+
}
|
|
1238
|
+
.connector-top .connector-info {
|
|
1239
|
+
flex: 0 0 360px;
|
|
1240
|
+
max-width: 360px;
|
|
1241
|
+
border-bottom: none;
|
|
1242
|
+
padding: 0;
|
|
1243
|
+
}
|
|
1244
|
+
.analytics-panel {
|
|
1245
|
+
flex: 1;
|
|
1246
|
+
display: flex;
|
|
1247
|
+
gap: 12px;
|
|
1248
|
+
align-items: stretch;
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
/* Charts row - 4 items horizontal with custom flex ratios */
|
|
1252
|
+
.heatmap-container {
|
|
1253
|
+
flex: 0.8;
|
|
1254
|
+
background: var(--bg-primary);
|
|
1255
|
+
border: 1px solid var(--border-color);
|
|
1256
|
+
border-radius: 6px;
|
|
1257
|
+
padding: 8px;
|
|
1258
|
+
min-width: 0;
|
|
1259
|
+
}
|
|
1260
|
+
.latency-histogram {
|
|
1261
|
+
flex: 1.4;
|
|
1262
|
+
background: var(--bg-primary);
|
|
1263
|
+
border: 1px solid var(--border-color);
|
|
1264
|
+
border-radius: 6px;
|
|
1265
|
+
padding: 8px;
|
|
1266
|
+
min-width: 0;
|
|
1267
|
+
}
|
|
1268
|
+
.top-tools, .method-distribution {
|
|
1269
|
+
flex: 1;
|
|
1270
|
+
background: var(--bg-primary);
|
|
1271
|
+
border: 1px solid var(--border-color);
|
|
1272
|
+
border-radius: 6px;
|
|
1273
|
+
padding: 8px;
|
|
1274
|
+
min-width: 0;
|
|
1275
|
+
}
|
|
1276
|
+
.chart-title {
|
|
1277
|
+
font-size: 0.75em;
|
|
1278
|
+
color: var(--text-secondary);
|
|
1279
|
+
margin-bottom: 4px;
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
/* Heatmap - using neon blue gradient for consistency with theme */
|
|
1283
|
+
.heatmap-title {
|
|
1284
|
+
font-size: 0.75em;
|
|
1285
|
+
color: var(--text-secondary);
|
|
1286
|
+
margin-bottom: 4px;
|
|
1287
|
+
}
|
|
1288
|
+
.heatmap-level-0 { fill: var(--bg-tertiary); }
|
|
1289
|
+
.heatmap-level-1 { fill: #0a3d4d; }
|
|
1290
|
+
.heatmap-level-2 { fill: #0d5c73; }
|
|
1291
|
+
.heatmap-level-3 { fill: #0097b2; }
|
|
1292
|
+
.heatmap-level-4 { fill: #00d4ff; }
|
|
1293
|
+
|
|
1294
|
+
/* Histogram */
|
|
1295
|
+
.histogram-bar { fill: var(--accent-blue); }
|
|
1296
|
+
.histogram-label { fill: var(--text-secondary); font-size: 9px; }
|
|
1297
|
+
|
|
1298
|
+
/* Top Tools */
|
|
1299
|
+
.top-tool-row {
|
|
1300
|
+
display: flex;
|
|
1301
|
+
align-items: center;
|
|
1302
|
+
gap: 6px;
|
|
1303
|
+
margin-bottom: 3px;
|
|
1304
|
+
font-size: 0.75em;
|
|
1305
|
+
}
|
|
1306
|
+
.top-tool-rank {
|
|
1307
|
+
color: var(--text-secondary);
|
|
1308
|
+
width: 14px;
|
|
1309
|
+
flex-shrink: 0;
|
|
1310
|
+
}
|
|
1311
|
+
.top-tool-name {
|
|
1312
|
+
flex: 1;
|
|
1313
|
+
min-width: 0;
|
|
1314
|
+
overflow: hidden;
|
|
1315
|
+
text-overflow: ellipsis;
|
|
1316
|
+
white-space: nowrap;
|
|
1317
|
+
font-family: 'SFMono-Regular', Consolas, monospace;
|
|
1318
|
+
}
|
|
1319
|
+
.top-tool-bar-container {
|
|
1320
|
+
width: 50px;
|
|
1321
|
+
height: 6px;
|
|
1322
|
+
background: var(--bg-tertiary);
|
|
1323
|
+
border-radius: 3px;
|
|
1324
|
+
overflow: hidden;
|
|
1325
|
+
flex-shrink: 0;
|
|
1326
|
+
}
|
|
1327
|
+
.top-tool-bar {
|
|
1328
|
+
height: 100%;
|
|
1329
|
+
background: var(--accent-blue);
|
|
1330
|
+
border-radius: 3px;
|
|
1331
|
+
}
|
|
1332
|
+
.top-tool-pct {
|
|
1333
|
+
color: var(--text-secondary);
|
|
1334
|
+
width: 28px;
|
|
1335
|
+
text-align: right;
|
|
1336
|
+
flex-shrink: 0;
|
|
1337
|
+
}
|
|
1338
|
+
.no-data-message {
|
|
1339
|
+
color: var(--text-secondary);
|
|
1340
|
+
font-size: 0.75em;
|
|
1341
|
+
text-align: center;
|
|
1342
|
+
padding: 8px;
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
/* Method Distribution Donut Chart */
|
|
1346
|
+
.donut-container {
|
|
1347
|
+
display: flex;
|
|
1348
|
+
align-items: center;
|
|
1349
|
+
gap: 8px;
|
|
1350
|
+
}
|
|
1351
|
+
.donut-legend {
|
|
1352
|
+
flex: 1;
|
|
1353
|
+
font-size: 0.7em;
|
|
1354
|
+
min-width: 0;
|
|
1355
|
+
}
|
|
1356
|
+
.donut-legend-item {
|
|
1357
|
+
display: flex;
|
|
1358
|
+
align-items: center;
|
|
1359
|
+
gap: 4px;
|
|
1360
|
+
margin-bottom: 2px;
|
|
1361
|
+
white-space: nowrap;
|
|
1362
|
+
overflow: hidden;
|
|
1363
|
+
}
|
|
1364
|
+
.donut-legend-color {
|
|
1365
|
+
width: 8px;
|
|
1366
|
+
height: 8px;
|
|
1367
|
+
border-radius: 2px;
|
|
1368
|
+
flex-shrink: 0;
|
|
1369
|
+
}
|
|
1370
|
+
.donut-legend-label {
|
|
1371
|
+
overflow: hidden;
|
|
1372
|
+
text-overflow: ellipsis;
|
|
1373
|
+
flex: 1;
|
|
1374
|
+
min-width: 0;
|
|
1375
|
+
}
|
|
1376
|
+
.donut-legend-pct {
|
|
1377
|
+
color: var(--text-secondary);
|
|
1378
|
+
flex-shrink: 0;
|
|
1379
|
+
}
|
|
1380
|
+
/* RPC Inspector styles */
|
|
1381
|
+
${getRpcInspectorStyles()}
|
|
1110
1382
|
`;
|
|
1111
1383
|
}
|
|
1112
1384
|
/**
|
|
@@ -1184,7 +1456,7 @@ function getConnectorReportScript() {
|
|
|
1184
1456
|
}
|
|
1185
1457
|
}
|
|
1186
1458
|
|
|
1187
|
-
// Show RPC detail in right pane
|
|
1459
|
+
// Show RPC detail in right pane (2-column Wireshark-style layout)
|
|
1188
1460
|
function showRpcDetail(sessionId, idx) {
|
|
1189
1461
|
const report = sessionReports[sessionId];
|
|
1190
1462
|
if (!report || idx < 0 || idx >= report.rpcs.length) return;
|
|
@@ -1206,44 +1478,49 @@ function getConnectorReportScript() {
|
|
|
1206
1478
|
const statusSymbol = rpc.status === 'OK' ? '\\u2713' : rpc.status === 'ERR' ? '\\u2717' : '?';
|
|
1207
1479
|
const latency = rpc.latency_ms !== null ? rpc.latency_ms + 'ms' : '(pending)';
|
|
1208
1480
|
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
notes = '<p class="truncated-note">Payload truncated (' + formatBytes(payload.size) + ', showing first 4096 chars)</p>';
|
|
1214
|
-
if (payload.spillFile) {
|
|
1215
|
-
notes += '<p class="spill-link">Full payload: <a href="' + escapeHtml(payload.spillFile) + '">' + escapeHtml(payload.spillFile) + '</a></p>';
|
|
1216
|
-
}
|
|
1217
|
-
content = payload.preview ? escapeHtml(payload.preview) + '\\n... (truncated)' : '(no data)';
|
|
1218
|
-
} else if (payload.json !== null) {
|
|
1219
|
-
content = escapeHtml(formatJson(payload.json));
|
|
1220
|
-
} else {
|
|
1221
|
-
content = '(no data)';
|
|
1222
|
-
}
|
|
1481
|
+
// Get pre-rendered summary and raw JSON from data attributes
|
|
1482
|
+
const summaryHtml = rpc._summaryHtml || '<div class="summary-row summary-header">No summary available</div>';
|
|
1483
|
+
const requestRawHtml = rpc._requestRawHtml || '<span class="json-null">(no data)</span>';
|
|
1484
|
+
const responseRawHtml = rpc._responseRawHtml || '<span class="json-null">(no data)</span>';
|
|
1223
1485
|
|
|
1224
|
-
|
|
1225
|
-
|
|
1486
|
+
// Determine default target based on method
|
|
1487
|
+
const defaultTarget = (rpc.method === 'tools/list' || rpc.method.startsWith('resources/') || rpc.method.startsWith('prompts/')) ? 'response' : 'request';
|
|
1226
1488
|
|
|
1227
1489
|
rightPane.innerHTML =
|
|
1228
1490
|
'<div class="detail-section">' +
|
|
1229
1491
|
' <h2>RPC Info</h2>' +
|
|
1230
|
-
' <
|
|
1231
|
-
' <dt>RPC ID</dt><dd><span class="badge">' + escapeHtml(rpc.rpc_id) + '</span></dd>' +
|
|
1232
|
-
' <dt>Method</dt><dd><span class="badge">' + escapeHtml(rpc.method) + '</span></dd>' +
|
|
1233
|
-
' <dt>Status</dt><dd><span class="badge ' + statusClass + '">' + statusSymbol + ' ' + rpc.status + (rpc.error_code !== null ? ' (code: ' + rpc.error_code + ')' : '') + '</span></dd>' +
|
|
1234
|
-
' <dt>Latency</dt><dd><span class="badge">' + latency + '</span></dd>' +
|
|
1235
|
-
' <dt>
|
|
1236
|
-
' <dt>
|
|
1237
|
-
' </
|
|
1238
|
-
'</div>' +
|
|
1239
|
-
'<div class="detail-section">' +
|
|
1240
|
-
' <h2>Request <button class="copy-btn" onclick="copyToClipboard(\\'req-' + sessionId + '-' + idx + '\\', this)">Copy</button></h2>' +
|
|
1241
|
-
' ' + renderPayload(rpc.request, 'req-' + sessionId + '-' + idx) +
|
|
1492
|
+
' <div class="rpc-info-grid">' +
|
|
1493
|
+
' <div class="rpc-info-item"><dt>RPC ID</dt><dd><span class="badge">' + escapeHtml(rpc.rpc_id) + '</span></dd></div>' +
|
|
1494
|
+
' <div class="rpc-info-item"><dt>Method</dt><dd><span class="badge">' + escapeHtml(rpc.method) + '</span></dd></div>' +
|
|
1495
|
+
' <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>' +
|
|
1496
|
+
' <div class="rpc-info-item"><dt>Latency</dt><dd><span class="badge">' + latency + '</span></dd></div>' +
|
|
1497
|
+
' <div class="rpc-info-item"><dt>Req Size</dt><dd>' + formatBytes(rpc.request.size) + '</dd></div>' +
|
|
1498
|
+
' <div class="rpc-info-item"><dt>Res Size</dt><dd>' + formatBytes(rpc.response.size) + '</dd></div>' +
|
|
1499
|
+
' </div>' +
|
|
1242
1500
|
'</div>' +
|
|
1243
1501
|
'<div class="detail-section">' +
|
|
1244
|
-
' <
|
|
1245
|
-
'
|
|
1502
|
+
' <div class="rpc-inspector">' +
|
|
1503
|
+
' <div class="rpc-inspector-summary">' +
|
|
1504
|
+
' <h3>Summary</h3>' +
|
|
1505
|
+
summaryHtml +
|
|
1506
|
+
' </div>' +
|
|
1507
|
+
' <div class="rpc-inspector-raw">' +
|
|
1508
|
+
' <div class="rpc-toggle-bar">' +
|
|
1509
|
+
' <button id="toggle-req" class="rpc-toggle-btn' + (defaultTarget === 'request' ? ' active' : '') + '">[Req]</button>' +
|
|
1510
|
+
' <button id="toggle-res" class="rpc-toggle-btn' + (defaultTarget === 'response' ? ' active' : '') + '">[Res]</button>' +
|
|
1511
|
+
' </div>' +
|
|
1512
|
+
' <div class="rpc-raw-json">' +
|
|
1513
|
+
' <div id="raw-json-request" style="display:' + (defaultTarget === 'request' ? 'block' : 'none') + '">' + requestRawHtml + '</div>' +
|
|
1514
|
+
' <div id="raw-json-response" style="display:' + (defaultTarget === 'response' ? 'block' : 'none') + '">' + responseRawHtml + '</div>' +
|
|
1515
|
+
' </div>' +
|
|
1516
|
+
' </div>' +
|
|
1517
|
+
' </div>' +
|
|
1246
1518
|
'</div>';
|
|
1519
|
+
|
|
1520
|
+
// Re-initialize RPC Inspector handlers
|
|
1521
|
+
if (window.initRpcInspector) {
|
|
1522
|
+
window.initRpcInspector();
|
|
1523
|
+
}
|
|
1247
1524
|
}
|
|
1248
1525
|
|
|
1249
1526
|
// Copy to clipboard
|
|
@@ -1335,12 +1612,331 @@ function getConnectorReportScript() {
|
|
|
1335
1612
|
}
|
|
1336
1613
|
});
|
|
1337
1614
|
|
|
1338
|
-
//
|
|
1339
|
-
|
|
1615
|
+
// Check for session parameter in URL
|
|
1616
|
+
function getSessionFromUrl() {
|
|
1617
|
+
const params = new URLSearchParams(window.location.search);
|
|
1618
|
+
return params.get('session');
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
// Scroll session item into view
|
|
1622
|
+
function scrollSessionIntoView(sessionId) {
|
|
1623
|
+
const sessionItem = document.querySelector('.session-item[data-session-id="' + sessionId + '"]');
|
|
1624
|
+
if (sessionItem) {
|
|
1625
|
+
sessionItem.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
1626
|
+
// Add highlight effect
|
|
1627
|
+
sessionItem.classList.add('highlight');
|
|
1628
|
+
setTimeout(() => sessionItem.classList.remove('highlight'), 2000);
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
// Select session from URL or first session by default
|
|
1633
|
+
const urlSession = getSessionFromUrl();
|
|
1634
|
+
if (urlSession) {
|
|
1635
|
+
// Try to find matching session (full or partial match)
|
|
1636
|
+
const matchingSession = sessions.find(s =>
|
|
1637
|
+
s.session_id === urlSession || s.session_id.startsWith(urlSession)
|
|
1638
|
+
);
|
|
1639
|
+
if (matchingSession) {
|
|
1640
|
+
showSession(matchingSession.session_id);
|
|
1641
|
+
// Scroll into view after a short delay to ensure DOM is ready
|
|
1642
|
+
setTimeout(() => scrollSessionIntoView(matchingSession.session_id), 100);
|
|
1643
|
+
} else if (sessions.length > 0) {
|
|
1644
|
+
showSession(sessions[0].session_id);
|
|
1645
|
+
}
|
|
1646
|
+
} else if (sessions.length > 0) {
|
|
1340
1647
|
showSession(sessions[0].session_id);
|
|
1341
1648
|
}
|
|
1649
|
+
|
|
1650
|
+
// RPC Inspector script
|
|
1651
|
+
${getRpcInspectorScript()}
|
|
1342
1652
|
`;
|
|
1343
1653
|
}
|
|
1654
|
+
// ============================================================================
|
|
1655
|
+
// Analytics Panel Rendering (Phase 5.2)
|
|
1656
|
+
// ============================================================================
|
|
1657
|
+
/**
|
|
1658
|
+
* Render KPI stats row for header (inline compact display)
|
|
1659
|
+
*/
|
|
1660
|
+
function renderKpiRow(kpis) {
|
|
1661
|
+
// Format large numbers with K/M suffix
|
|
1662
|
+
const formatNumber = (n) => {
|
|
1663
|
+
if (n >= 1000000)
|
|
1664
|
+
return (n / 1000000).toFixed(1) + 'M';
|
|
1665
|
+
if (n >= 1000)
|
|
1666
|
+
return (n / 1000).toFixed(1) + 'K';
|
|
1667
|
+
return String(n);
|
|
1668
|
+
};
|
|
1669
|
+
// Format bytes for display
|
|
1670
|
+
const formatBytesCompact = (bytes) => {
|
|
1671
|
+
if (bytes === 0)
|
|
1672
|
+
return '0';
|
|
1673
|
+
const k = 1024;
|
|
1674
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
1675
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
1676
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + sizes[i];
|
|
1677
|
+
};
|
|
1678
|
+
return `
|
|
1679
|
+
<div class="kpi-row">
|
|
1680
|
+
<div class="kpi-item">
|
|
1681
|
+
<div class="kpi-value">${formatNumber(kpis.sessions_displayed)}</div>
|
|
1682
|
+
<div class="kpi-label">Sessions</div>
|
|
1683
|
+
</div>
|
|
1684
|
+
<div class="kpi-item">
|
|
1685
|
+
<div class="kpi-value">${formatNumber(kpis.rpc_total)}</div>
|
|
1686
|
+
<div class="kpi-label">RPCs</div>
|
|
1687
|
+
</div>
|
|
1688
|
+
<div class="kpi-item">
|
|
1689
|
+
<div class="kpi-value">${formatNumber(kpis.rpc_err)}</div>
|
|
1690
|
+
<div class="kpi-label">Error</div>
|
|
1691
|
+
</div>
|
|
1692
|
+
<div class="kpi-item">
|
|
1693
|
+
<div class="kpi-value">${kpis.avg_latency_ms !== null ? kpis.avg_latency_ms : '-'}</div>
|
|
1694
|
+
<div class="kpi-label">Avg Latency</div>
|
|
1695
|
+
</div>
|
|
1696
|
+
<div class="kpi-item">
|
|
1697
|
+
<div class="kpi-value">${kpis.p95_latency_ms !== null ? kpis.p95_latency_ms : '-'}</div>
|
|
1698
|
+
<div class="kpi-label">P95 Latency</div>
|
|
1699
|
+
</div>
|
|
1700
|
+
<div class="kpi-item">
|
|
1701
|
+
<div class="kpi-value">${formatBytesCompact(kpis.total_request_bytes)}</div>
|
|
1702
|
+
<div class="kpi-label">Req Size</div>
|
|
1703
|
+
</div>
|
|
1704
|
+
<div class="kpi-item">
|
|
1705
|
+
<div class="kpi-value">${formatBytesCompact(kpis.total_response_bytes)}</div>
|
|
1706
|
+
<div class="kpi-label">Res Size</div>
|
|
1707
|
+
</div>
|
|
1708
|
+
</div>`;
|
|
1709
|
+
}
|
|
1710
|
+
/**
|
|
1711
|
+
* Get intensity level (0-4) for heatmap cell based on count and max
|
|
1712
|
+
*/
|
|
1713
|
+
function getHeatmapLevel(count, maxCount) {
|
|
1714
|
+
if (count === 0 || maxCount === 0)
|
|
1715
|
+
return 0;
|
|
1716
|
+
const ratio = count / maxCount;
|
|
1717
|
+
if (ratio <= 0.25)
|
|
1718
|
+
return 1;
|
|
1719
|
+
if (ratio <= 0.5)
|
|
1720
|
+
return 2;
|
|
1721
|
+
if (ratio <= 0.75)
|
|
1722
|
+
return 3;
|
|
1723
|
+
return 4;
|
|
1724
|
+
}
|
|
1725
|
+
/**
|
|
1726
|
+
* Render activity heatmap (GitHub contributions style, SVG)
|
|
1727
|
+
*/
|
|
1728
|
+
export function renderHeatmap(heatmap) {
|
|
1729
|
+
const cellSize = 10;
|
|
1730
|
+
const cellGap = 2;
|
|
1731
|
+
const cellTotal = cellSize + cellGap;
|
|
1732
|
+
// Group cells by week (7 days per column)
|
|
1733
|
+
const weeks = [];
|
|
1734
|
+
let currentWeek = [];
|
|
1735
|
+
// Find the day of week for the start date (0 = Sunday)
|
|
1736
|
+
const startDow = new Date(heatmap.start_date + 'T00:00:00Z').getUTCDay();
|
|
1737
|
+
// Add empty cells for days before start_date
|
|
1738
|
+
for (let i = 0; i < startDow; i++) {
|
|
1739
|
+
currentWeek.push({ date: '', count: -1 }); // -1 indicates empty
|
|
1740
|
+
}
|
|
1741
|
+
for (const cell of heatmap.cells) {
|
|
1742
|
+
currentWeek.push(cell);
|
|
1743
|
+
if (currentWeek.length === 7) {
|
|
1744
|
+
weeks.push(currentWeek);
|
|
1745
|
+
currentWeek = [];
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
if (currentWeek.length > 0) {
|
|
1749
|
+
weeks.push(currentWeek);
|
|
1750
|
+
}
|
|
1751
|
+
// Calculate SVG dimensions
|
|
1752
|
+
const svgWidth = weeks.length * cellTotal;
|
|
1753
|
+
const svgHeight = 7 * cellTotal;
|
|
1754
|
+
// Generate SVG rects
|
|
1755
|
+
let rects = '';
|
|
1756
|
+
weeks.forEach((week, weekIdx) => {
|
|
1757
|
+
week.forEach((cell, dayIdx) => {
|
|
1758
|
+
if (cell.count < 0)
|
|
1759
|
+
return; // Skip empty cells
|
|
1760
|
+
const level = getHeatmapLevel(cell.count, heatmap.max_count);
|
|
1761
|
+
const x = weekIdx * cellTotal;
|
|
1762
|
+
const y = dayIdx * cellTotal;
|
|
1763
|
+
const title = cell.date ? `${cell.date}: ${cell.count} RPCs` : '';
|
|
1764
|
+
rects += `<rect x="${x}" y="${y}" width="${cellSize}" height="${cellSize}" rx="2" class="heatmap-level-${level}"><title>${escapeHtml(title)}</title></rect>`;
|
|
1765
|
+
});
|
|
1766
|
+
});
|
|
1767
|
+
return `
|
|
1768
|
+
<div class="heatmap-container">
|
|
1769
|
+
<div class="heatmap-title">Activity (${escapeHtml(heatmap.start_date)} to ${escapeHtml(heatmap.end_date)})</div>
|
|
1770
|
+
<svg width="${svgWidth}" height="${svgHeight}" viewBox="0 0 ${svgWidth} ${svgHeight}">
|
|
1771
|
+
${rects}
|
|
1772
|
+
</svg>
|
|
1773
|
+
</div>`;
|
|
1774
|
+
}
|
|
1775
|
+
/**
|
|
1776
|
+
* Render latency histogram (SVG bar chart)
|
|
1777
|
+
*/
|
|
1778
|
+
function renderLatencyHistogram(latency) {
|
|
1779
|
+
if (latency.sample_size === 0) {
|
|
1780
|
+
return `
|
|
1781
|
+
<div class="latency-histogram">
|
|
1782
|
+
<div class="chart-title">Latency Distribution</div>
|
|
1783
|
+
<div class="no-data-message">No latency data</div>
|
|
1784
|
+
</div>`;
|
|
1785
|
+
}
|
|
1786
|
+
const maxCount = Math.max(...latency.buckets.map((b) => b.count), 1);
|
|
1787
|
+
const barWidth = 30;
|
|
1788
|
+
const barGap = 4;
|
|
1789
|
+
const chartWidth = latency.buckets.length * (barWidth + barGap);
|
|
1790
|
+
const chartHeight = 60;
|
|
1791
|
+
const labelHeight = 16;
|
|
1792
|
+
let bars = '';
|
|
1793
|
+
latency.buckets.forEach((bucket, idx) => {
|
|
1794
|
+
const barHeight = maxCount > 0 ? (bucket.count / maxCount) * chartHeight : 0;
|
|
1795
|
+
const x = idx * (barWidth + barGap);
|
|
1796
|
+
const y = chartHeight - barHeight;
|
|
1797
|
+
const title = `${bucket.label}ms: ${bucket.count} RPCs`;
|
|
1798
|
+
bars += `<rect x="${x}" y="${y}" width="${barWidth}" height="${barHeight}" class="histogram-bar"><title>${escapeHtml(title)}</title></rect>`;
|
|
1799
|
+
bars += `<text x="${x + barWidth / 2}" y="${chartHeight + labelHeight - 4}" text-anchor="middle" class="histogram-label">${escapeHtml(bucket.label)}</text>`;
|
|
1800
|
+
});
|
|
1801
|
+
return `
|
|
1802
|
+
<div class="latency-histogram">
|
|
1803
|
+
<div class="chart-title">Latency Distribution (${latency.sample_size} samples)</div>
|
|
1804
|
+
<svg width="${chartWidth}" height="${chartHeight + labelHeight}" viewBox="0 0 ${chartWidth} ${chartHeight + labelHeight}">
|
|
1805
|
+
${bars}
|
|
1806
|
+
</svg>
|
|
1807
|
+
</div>`;
|
|
1808
|
+
}
|
|
1809
|
+
/**
|
|
1810
|
+
* Render top 5 tools
|
|
1811
|
+
*/
|
|
1812
|
+
function renderTopTools(topTools) {
|
|
1813
|
+
if (topTools.items.length === 0) {
|
|
1814
|
+
return `
|
|
1815
|
+
<div class="top-tools">
|
|
1816
|
+
<div class="chart-title">Top Tools</div>
|
|
1817
|
+
<div class="no-data-message">No tool calls</div>
|
|
1818
|
+
</div>`;
|
|
1819
|
+
}
|
|
1820
|
+
const rows = topTools.items
|
|
1821
|
+
.map((tool, idx) => {
|
|
1822
|
+
return `
|
|
1823
|
+
<div class="top-tool-row">
|
|
1824
|
+
<span class="top-tool-rank">${idx + 1}.</span>
|
|
1825
|
+
<span class="top-tool-name" title="${escapeHtml(tool.name)}">${escapeHtml(tool.name)}</span>
|
|
1826
|
+
<div class="top-tool-bar-container">
|
|
1827
|
+
<div class="top-tool-bar" style="width: ${tool.pct}%"></div>
|
|
1828
|
+
</div>
|
|
1829
|
+
<span class="top-tool-pct">${tool.pct}%</span>
|
|
1830
|
+
</div>`;
|
|
1831
|
+
})
|
|
1832
|
+
.join('');
|
|
1833
|
+
return `
|
|
1834
|
+
<div class="top-tools">
|
|
1835
|
+
<div class="chart-title">Top Tools (${topTools.total_calls} calls)</div>
|
|
1836
|
+
${rows}
|
|
1837
|
+
</div>`;
|
|
1838
|
+
}
|
|
1839
|
+
/**
|
|
1840
|
+
* Donut chart colors (blue gradient palette)
|
|
1841
|
+
*/
|
|
1842
|
+
const DONUT_COLORS = [
|
|
1843
|
+
'#00d4ff', // Neon blue (brightest)
|
|
1844
|
+
'#0097b2', // Medium bright blue
|
|
1845
|
+
'#0d5c73', // Medium blue
|
|
1846
|
+
'#0a4d5c', // Darker blue
|
|
1847
|
+
'#083d47', // Dark blue
|
|
1848
|
+
'#5a6a70', // Blue-gray (for "Others")
|
|
1849
|
+
];
|
|
1850
|
+
/**
|
|
1851
|
+
* Render method distribution donut chart (SVG)
|
|
1852
|
+
*/
|
|
1853
|
+
export function renderMethodDistribution(methodDist) {
|
|
1854
|
+
if (methodDist.slices.length === 0) {
|
|
1855
|
+
return `
|
|
1856
|
+
<div class="method-distribution">
|
|
1857
|
+
<div class="chart-title">Method Distribution</div>
|
|
1858
|
+
<div class="no-data-message">No RPCs</div>
|
|
1859
|
+
</div>`;
|
|
1860
|
+
}
|
|
1861
|
+
// SVG donut chart parameters
|
|
1862
|
+
const size = 60;
|
|
1863
|
+
const cx = size / 2;
|
|
1864
|
+
const cy = size / 2;
|
|
1865
|
+
const outerRadius = 26;
|
|
1866
|
+
const innerRadius = 16; // Creates the donut hole
|
|
1867
|
+
// Generate SVG path segments
|
|
1868
|
+
let paths = '';
|
|
1869
|
+
let currentAngle = -90; // Start from top (12 o'clock)
|
|
1870
|
+
methodDist.slices.forEach((slice, idx) => {
|
|
1871
|
+
const angle = (slice.pct / 100) * 360;
|
|
1872
|
+
const startAngle = currentAngle;
|
|
1873
|
+
const endAngle = currentAngle + angle;
|
|
1874
|
+
// Convert angles to radians
|
|
1875
|
+
const startRad = (startAngle * Math.PI) / 180;
|
|
1876
|
+
const endRad = (endAngle * Math.PI) / 180;
|
|
1877
|
+
// Calculate arc points for outer radius
|
|
1878
|
+
const x1Outer = cx + outerRadius * Math.cos(startRad);
|
|
1879
|
+
const y1Outer = cy + outerRadius * Math.sin(startRad);
|
|
1880
|
+
const x2Outer = cx + outerRadius * Math.cos(endRad);
|
|
1881
|
+
const y2Outer = cy + outerRadius * Math.sin(endRad);
|
|
1882
|
+
// Calculate arc points for inner radius
|
|
1883
|
+
const x1Inner = cx + innerRadius * Math.cos(endRad);
|
|
1884
|
+
const y1Inner = cy + innerRadius * Math.sin(endRad);
|
|
1885
|
+
const x2Inner = cx + innerRadius * Math.cos(startRad);
|
|
1886
|
+
const y2Inner = cy + innerRadius * Math.sin(startRad);
|
|
1887
|
+
// Large arc flag
|
|
1888
|
+
const largeArc = angle > 180 ? 1 : 0;
|
|
1889
|
+
// Color
|
|
1890
|
+
const color = DONUT_COLORS[idx % DONUT_COLORS.length];
|
|
1891
|
+
// SVG path for donut segment
|
|
1892
|
+
const d = [
|
|
1893
|
+
`M ${x1Outer} ${y1Outer}`, // Start at outer edge
|
|
1894
|
+
`A ${outerRadius} ${outerRadius} 0 ${largeArc} 1 ${x2Outer} ${y2Outer}`, // Outer arc
|
|
1895
|
+
`L ${x1Inner} ${y1Inner}`, // Line to inner edge
|
|
1896
|
+
`A ${innerRadius} ${innerRadius} 0 ${largeArc} 0 ${x2Inner} ${y2Inner}`, // Inner arc (reverse)
|
|
1897
|
+
'Z', // Close path
|
|
1898
|
+
].join(' ');
|
|
1899
|
+
const title = `${slice.method}: ${slice.count} (${slice.pct}%)`;
|
|
1900
|
+
paths += `<path d="${d}" fill="${color}"><title>${escapeHtml(title)}</title></path>`;
|
|
1901
|
+
currentAngle = endAngle;
|
|
1902
|
+
});
|
|
1903
|
+
// Generate legend
|
|
1904
|
+
const legendItems = methodDist.slices
|
|
1905
|
+
.map((slice, idx) => {
|
|
1906
|
+
const color = DONUT_COLORS[idx % DONUT_COLORS.length];
|
|
1907
|
+
return `
|
|
1908
|
+
<div class="donut-legend-item">
|
|
1909
|
+
<div class="donut-legend-color" style="background: ${color}"></div>
|
|
1910
|
+
<span class="donut-legend-label" title="${escapeHtml(slice.method)}">${escapeHtml(slice.method)}</span>
|
|
1911
|
+
<span class="donut-legend-pct">${slice.pct}%</span>
|
|
1912
|
+
</div>`;
|
|
1913
|
+
})
|
|
1914
|
+
.join('');
|
|
1915
|
+
return `
|
|
1916
|
+
<div class="method-distribution">
|
|
1917
|
+
<div class="chart-title">Methods (${methodDist.total_rpcs} RPCs)</div>
|
|
1918
|
+
<div class="donut-container">
|
|
1919
|
+
<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}">
|
|
1920
|
+
${paths}
|
|
1921
|
+
</svg>
|
|
1922
|
+
<div class="donut-legend">
|
|
1923
|
+
${legendItems}
|
|
1924
|
+
</div>
|
|
1925
|
+
</div>
|
|
1926
|
+
</div>`;
|
|
1927
|
+
}
|
|
1928
|
+
/**
|
|
1929
|
+
* Render the analytics panel (4 charts horizontally)
|
|
1930
|
+
*/
|
|
1931
|
+
function renderAnalyticsPanel(analytics) {
|
|
1932
|
+
return `
|
|
1933
|
+
<div class="analytics-panel">
|
|
1934
|
+
${renderHeatmap(analytics.heatmap)}
|
|
1935
|
+
${renderLatencyHistogram(analytics.latency)}
|
|
1936
|
+
${renderTopTools(analytics.top_tools)}
|
|
1937
|
+
${renderMethodDistribution(analytics.method_distribution)}
|
|
1938
|
+
</div>`;
|
|
1939
|
+
}
|
|
1344
1940
|
/**
|
|
1345
1941
|
* Render a session item for the sessions pane
|
|
1346
1942
|
*/
|
|
@@ -1439,7 +2035,7 @@ ${rpcRows}
|
|
|
1439
2035
|
* Generate Connector HTML report (3-hierarchy: Connector -> Sessions -> RPCs)
|
|
1440
2036
|
*/
|
|
1441
2037
|
export function generateConnectorHtml(report) {
|
|
1442
|
-
const { meta, connector, sessions, session_reports } = report;
|
|
2038
|
+
const { meta, connector, sessions, session_reports, analytics } = report;
|
|
1443
2039
|
// Pagination info
|
|
1444
2040
|
const fromNum = connector.offset + 1;
|
|
1445
2041
|
const toNum = connector.offset + connector.displayed_sessions;
|
|
@@ -1450,8 +2046,8 @@ export function generateConnectorHtml(report) {
|
|
|
1450
2046
|
const transportDisplay = connector.transport.type === 'stdio'
|
|
1451
2047
|
? connector.transport.command || '(unknown command)'
|
|
1452
2048
|
: connector.transport.url || '(unknown URL)';
|
|
1453
|
-
// Server info (if available)
|
|
1454
|
-
let
|
|
2049
|
+
// Server info for header (if available)
|
|
2050
|
+
let headerServerHtml = '';
|
|
1455
2051
|
if (connector.server) {
|
|
1456
2052
|
const { name, version, protocolVersion, capabilities } = connector.server;
|
|
1457
2053
|
const serverName = name || '(unknown)';
|
|
@@ -1465,14 +2061,15 @@ export function generateConnectorHtml(report) {
|
|
|
1465
2061
|
capBadges.push('<span class="badge cap-enabled">resources</span>');
|
|
1466
2062
|
if (capabilities.prompts)
|
|
1467
2063
|
capBadges.push('<span class="badge cap-enabled">prompts</span>');
|
|
1468
|
-
const capsDisplay = capBadges.length > 0 ? capBadges.join(' ') : '
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
<
|
|
1474
|
-
<
|
|
1475
|
-
|
|
2064
|
+
const capsDisplay = capBadges.length > 0 ? capBadges.join(' ') : '';
|
|
2065
|
+
headerServerHtml = `
|
|
2066
|
+
<div class="header-server-row">
|
|
2067
|
+
<div class="header-caps"><span class="header-label">Capabilities:</span> ${capsDisplay || '<span class="no-caps">(none)</span>'}</div>
|
|
2068
|
+
<div class="header-server-info">
|
|
2069
|
+
<span class="server-name"><span class="header-label">Server:</span> ${escapeHtml(serverName)} ${escapeHtml(serverVersion)}</span>
|
|
2070
|
+
<span class="server-protocol"><span class="header-label">Protocol:</span> ${escapeHtml(protocolDisplay)}</span>
|
|
2071
|
+
</div>
|
|
2072
|
+
</div>`;
|
|
1476
2073
|
}
|
|
1477
2074
|
// Session items
|
|
1478
2075
|
const sessionItems = sessions.map(s => renderConnectorSessionItem(s)).join('\n');
|
|
@@ -1483,7 +2080,27 @@ export function generateConnectorHtml(report) {
|
|
|
1483
2080
|
return '';
|
|
1484
2081
|
return renderSessionDetailContent(s.session_id, sessionReport);
|
|
1485
2082
|
}).join('\n');
|
|
1486
|
-
|
|
2083
|
+
// Pre-render summary and raw JSON HTML for each RPC in each session (for RPC Inspector)
|
|
2084
|
+
const sessionReportsWithInspectorHtml = {};
|
|
2085
|
+
for (const [sessionId, sessionReport] of Object.entries(session_reports)) {
|
|
2086
|
+
sessionReportsWithInspectorHtml[sessionId] = {
|
|
2087
|
+
...sessionReport,
|
|
2088
|
+
rpcs: sessionReport.rpcs.map((rpc) => {
|
|
2089
|
+
const summaryRows = renderMethodSummary(rpc.method, rpc.request.json, rpc.response.json);
|
|
2090
|
+
return {
|
|
2091
|
+
...rpc,
|
|
2092
|
+
_summaryHtml: renderSummaryRowsHtml(summaryRows),
|
|
2093
|
+
_requestRawHtml: renderJsonWithPaths(rpc.request.json, '#'),
|
|
2094
|
+
_responseRawHtml: renderJsonWithPaths(rpc.response.json, '#'),
|
|
2095
|
+
};
|
|
2096
|
+
}),
|
|
2097
|
+
};
|
|
2098
|
+
}
|
|
2099
|
+
const reportWithInspectorHtml = {
|
|
2100
|
+
...report,
|
|
2101
|
+
session_reports: sessionReportsWithInspectorHtml,
|
|
2102
|
+
};
|
|
2103
|
+
const embeddedJson = escapeJsonForScript(JSON.stringify(reportWithInspectorHtml));
|
|
1487
2104
|
return `<!DOCTYPE html>
|
|
1488
2105
|
<html lang="en">
|
|
1489
2106
|
<head>
|
|
@@ -1494,26 +2111,32 @@ export function generateConnectorHtml(report) {
|
|
|
1494
2111
|
</head>
|
|
1495
2112
|
<body>
|
|
1496
2113
|
<header>
|
|
1497
|
-
<
|
|
1498
|
-
|
|
2114
|
+
<div class="header-left">
|
|
2115
|
+
<h1>Connector: <span class="badge">${escapeHtml(connector.connector_id)}</span></h1>
|
|
2116
|
+
<p class="meta">Generated by ${escapeHtml(meta.generatedBy)} at ${formatTimestamp(meta.generatedAt)}${meta.redacted ? ' (redacted)' : ''} | ${paginationInfo}</p>
|
|
2117
|
+
</div>
|
|
2118
|
+
${headerServerHtml}
|
|
2119
|
+
${renderKpiRow(analytics.kpis)}
|
|
1499
2120
|
</header>
|
|
1500
2121
|
|
|
1501
|
-
<div class="connector-
|
|
1502
|
-
<div class="connector-info
|
|
1503
|
-
<
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
<
|
|
1508
|
-
<
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
2122
|
+
<div class="connector-top">
|
|
2123
|
+
<div class="connector-info expanded">
|
|
2124
|
+
<div class="connector-info-toggle">
|
|
2125
|
+
<h2>Connector Info</h2>
|
|
2126
|
+
<span class="toggle-icon">▼</span>
|
|
2127
|
+
</div>
|
|
2128
|
+
<div class="connector-info-content">
|
|
2129
|
+
<dl>
|
|
2130
|
+
<dt>Transport</dt>
|
|
2131
|
+
<dd><span class="badge">${escapeHtml(connector.transport.type)}</span></dd>
|
|
2132
|
+
<dt>${connector.transport.type === 'stdio' ? 'Command' : 'URL'}</dt>
|
|
2133
|
+
<dd><code>${escapeHtml(transportDisplay)}</code></dd>
|
|
2134
|
+
<dt>Enabled</dt>
|
|
2135
|
+
<dd>${connector.enabled ? '<span class="badge status-OK">yes</span>' : '<span class="badge status-ERR">no</span>'}</dd>
|
|
2136
|
+
</dl>
|
|
2137
|
+
</div>
|
|
1516
2138
|
</div>
|
|
2139
|
+
${renderAnalyticsPanel(analytics)}
|
|
1517
2140
|
</div>
|
|
1518
2141
|
|
|
1519
2142
|
<div class="main-container">
|