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.
Files changed (96) hide show
  1. package/dist/cli.js +4 -2
  2. package/dist/cli.js.map +1 -1
  3. package/dist/commands/catalog.d.ts.map +1 -1
  4. package/dist/commands/catalog.js +22 -0
  5. package/dist/commands/catalog.js.map +1 -1
  6. package/dist/commands/connectors.d.ts.map +1 -1
  7. package/dist/commands/connectors.js +10 -3
  8. package/dist/commands/connectors.js.map +1 -1
  9. package/dist/commands/index.d.ts +1 -0
  10. package/dist/commands/index.d.ts.map +1 -1
  11. package/dist/commands/index.js +2 -0
  12. package/dist/commands/index.js.map +1 -1
  13. package/dist/commands/monitor.d.ts +6 -0
  14. package/dist/commands/monitor.d.ts.map +1 -0
  15. package/dist/commands/monitor.js +59 -0
  16. package/dist/commands/monitor.js.map +1 -0
  17. package/dist/html/analytics.d.ts +58 -0
  18. package/dist/html/analytics.d.ts.map +1 -0
  19. package/dist/html/analytics.js +337 -0
  20. package/dist/html/analytics.js.map +1 -0
  21. package/dist/html/index.d.ts +5 -2
  22. package/dist/html/index.d.ts.map +1 -1
  23. package/dist/html/index.js +5 -1
  24. package/dist/html/index.js.map +1 -1
  25. package/dist/html/rpc-inspector.d.ts +43 -0
  26. package/dist/html/rpc-inspector.d.ts.map +1 -0
  27. package/dist/html/rpc-inspector.js +922 -0
  28. package/dist/html/rpc-inspector.js.map +1 -0
  29. package/dist/html/templates.d.ts +9 -1
  30. package/dist/html/templates.d.ts.map +1 -1
  31. package/dist/html/templates.js +701 -78
  32. package/dist/html/templates.js.map +1 -1
  33. package/dist/html/types.d.ts +129 -0
  34. package/dist/html/types.d.ts.map +1 -1
  35. package/dist/html/types.js.map +1 -1
  36. package/dist/monitor/data/aggregator.d.ts +13 -0
  37. package/dist/monitor/data/aggregator.d.ts.map +1 -0
  38. package/dist/monitor/data/aggregator.js +101 -0
  39. package/dist/monitor/data/aggregator.js.map +1 -0
  40. package/dist/monitor/data/connectors.d.ts +13 -0
  41. package/dist/monitor/data/connectors.d.ts.map +1 -0
  42. package/dist/monitor/data/connectors.js +326 -0
  43. package/dist/monitor/data/connectors.js.map +1 -0
  44. package/dist/monitor/data/popl.d.ts +30 -0
  45. package/dist/monitor/data/popl.d.ts.map +1 -0
  46. package/dist/monitor/data/popl.js +310 -0
  47. package/dist/monitor/data/popl.js.map +1 -0
  48. package/dist/monitor/index.d.ts +6 -0
  49. package/dist/monitor/index.d.ts.map +1 -0
  50. package/dist/monitor/index.js +6 -0
  51. package/dist/monitor/index.js.map +1 -0
  52. package/dist/monitor/routes/api.d.ts +7 -0
  53. package/dist/monitor/routes/api.d.ts.map +1 -0
  54. package/dist/monitor/routes/api.js +63 -0
  55. package/dist/monitor/routes/api.js.map +1 -0
  56. package/dist/monitor/routes/connectors.d.ts +7 -0
  57. package/dist/monitor/routes/connectors.d.ts.map +1 -0
  58. package/dist/monitor/routes/connectors.js +417 -0
  59. package/dist/monitor/routes/connectors.js.map +1 -0
  60. package/dist/monitor/routes/home.d.ts +7 -0
  61. package/dist/monitor/routes/home.d.ts.map +1 -0
  62. package/dist/monitor/routes/home.js +15 -0
  63. package/dist/monitor/routes/home.js.map +1 -0
  64. package/dist/monitor/routes/index.d.ts +10 -0
  65. package/dist/monitor/routes/index.d.ts.map +1 -0
  66. package/dist/monitor/routes/index.js +19 -0
  67. package/dist/monitor/routes/index.js.map +1 -0
  68. package/dist/monitor/routes/popl.d.ts +7 -0
  69. package/dist/monitor/routes/popl.d.ts.map +1 -0
  70. package/dist/monitor/routes/popl.js +84 -0
  71. package/dist/monitor/routes/popl.js.map +1 -0
  72. package/dist/monitor/server.d.ts +24 -0
  73. package/dist/monitor/server.d.ts.map +1 -0
  74. package/dist/monitor/server.js +52 -0
  75. package/dist/monitor/server.js.map +1 -0
  76. package/dist/monitor/templates/components.d.ts +21 -0
  77. package/dist/monitor/templates/components.d.ts.map +1 -0
  78. package/dist/monitor/templates/components.js +405 -0
  79. package/dist/monitor/templates/components.js.map +1 -0
  80. package/dist/monitor/templates/home.d.ts +9 -0
  81. package/dist/monitor/templates/home.d.ts.map +1 -0
  82. package/dist/monitor/templates/home.js +322 -0
  83. package/dist/monitor/templates/home.js.map +1 -0
  84. package/dist/monitor/templates/layout.d.ts +26 -0
  85. package/dist/monitor/templates/layout.d.ts.map +1 -0
  86. package/dist/monitor/templates/layout.js +186 -0
  87. package/dist/monitor/templates/layout.js.map +1 -0
  88. package/dist/monitor/templates/popl.d.ts +33 -0
  89. package/dist/monitor/templates/popl.d.ts.map +1 -0
  90. package/dist/monitor/templates/popl.js +654 -0
  91. package/dist/monitor/templates/popl.js.map +1 -0
  92. package/dist/monitor/types.d.ts +121 -0
  93. package/dist/monitor/types.d.ts.map +1 -0
  94. package/dist/monitor/types.js +5 -0
  95. package/dist/monitor/types.js.map +1 -0
  96. package/package.json +3 -1
@@ -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
- ' <dl>' +
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>Request Size</dt><dd>' + formatBytes(rpc.request.size) + '</dd>' +
472
- ' <dt>Response Size</dt><dd>' + formatBytes(rpc.response.size) + '</dd>' +
473
- ' </dl>' +
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
- ' <h2>Response</h2>' +
481
- ' ' + renderPayload('', rpc.response, 'res-' + idx).replace('<h3> ', '').replace('</h3>', '') +
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
- const embeddedJson = escapeJsonForScript(JSON.stringify(report));
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
- function renderPayload(payload, elementId) {
1210
- let content, notes = '';
1211
-
1212
- if (payload.truncated) {
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
- return notes + '<pre id="' + elementId + '"><code>' + content + '</code></pre>';
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
- ' <dl class="session-info">' +
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>Request Size</dt><dd>' + formatBytes(rpc.request.size) + '</dd>' +
1236
- ' <dt>Response Size</dt><dd>' + formatBytes(rpc.response.size) + '</dd>' +
1237
- ' </dl>' +
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
- ' <h2>Response <button class="copy-btn" onclick="copyToClipboard(\\'res-' + sessionId + '-' + idx + '\\', this)">Copy</button></h2>' +
1245
- ' ' + renderPayload(rpc.response, 'res-' + sessionId + '-' + idx) +
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
- // Select first session by default
1339
- if (sessions.length > 0) {
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 serverInfoHtml = '';
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(' ') : '<span style="color: var(--text-secondary)">(none)</span>';
1469
- serverInfoHtml = `
1470
- <dt>Server</dt>
1471
- <dd>${escapeHtml(serverName)} ${escapeHtml(serverVersion)}</dd>
1472
- <dt>Protocol</dt>
1473
- <dd>${escapeHtml(protocolDisplay)}</dd>
1474
- <dt>Capabilities</dt>
1475
- <dd class="capabilities">${capsDisplay}</dd>`;
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
- const embeddedJson = escapeJsonForScript(JSON.stringify(report));
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
- <h1>Connector: <span class="badge">${escapeHtml(connector.connector_id)}</span></h1>
1498
- <p class="meta">Generated by ${escapeHtml(meta.generatedBy)} at ${formatTimestamp(meta.generatedAt)}${meta.redacted ? ' (redacted)' : ''} | ${paginationInfo}</p>
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-info expanded">
1502
- <div class="connector-info-toggle">
1503
- <h2>Connector Info</h2>
1504
- <span class="toggle-icon">▼</span>
1505
- </div>
1506
- <div class="connector-info-content">
1507
- <dl>
1508
- <dt>Transport</dt>
1509
- <dd><span class="badge">${escapeHtml(connector.transport.type)}</span></dd>
1510
- <dt>${connector.transport.type === 'stdio' ? 'Command' : 'URL'}</dt>
1511
- <dd><code>${escapeHtml(transportDisplay)}</code></dd>
1512
- <dt>Enabled</dt>
1513
- <dd>${connector.enabled ? '<span class="badge status-OK">yes</span>' : '<span class="badge status-ERR">no</span>'}</dd>
1514
- ${serverInfoHtml}
1515
- </dl>
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">