proofscan 0.10.26 → 0.10.27

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 (92) 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 +58 -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 +3 -2
  22. package/dist/html/index.d.ts.map +1 -1
  23. package/dist/html/index.js +3 -1
  24. package/dist/html/index.js.map +1 -1
  25. package/dist/html/templates.d.ts +9 -1
  26. package/dist/html/templates.d.ts.map +1 -1
  27. package/dist/html/templates.js +580 -30
  28. package/dist/html/templates.js.map +1 -1
  29. package/dist/html/types.d.ts +93 -0
  30. package/dist/html/types.d.ts.map +1 -1
  31. package/dist/html/types.js.map +1 -1
  32. package/dist/monitor/data/aggregator.d.ts +13 -0
  33. package/dist/monitor/data/aggregator.d.ts.map +1 -0
  34. package/dist/monitor/data/aggregator.js +101 -0
  35. package/dist/monitor/data/aggregator.js.map +1 -0
  36. package/dist/monitor/data/connectors.d.ts +13 -0
  37. package/dist/monitor/data/connectors.d.ts.map +1 -0
  38. package/dist/monitor/data/connectors.js +326 -0
  39. package/dist/monitor/data/connectors.js.map +1 -0
  40. package/dist/monitor/data/popl.d.ts +30 -0
  41. package/dist/monitor/data/popl.d.ts.map +1 -0
  42. package/dist/monitor/data/popl.js +310 -0
  43. package/dist/monitor/data/popl.js.map +1 -0
  44. package/dist/monitor/index.d.ts +6 -0
  45. package/dist/monitor/index.d.ts.map +1 -0
  46. package/dist/monitor/index.js +6 -0
  47. package/dist/monitor/index.js.map +1 -0
  48. package/dist/monitor/routes/api.d.ts +7 -0
  49. package/dist/monitor/routes/api.d.ts.map +1 -0
  50. package/dist/monitor/routes/api.js +63 -0
  51. package/dist/monitor/routes/api.js.map +1 -0
  52. package/dist/monitor/routes/connectors.d.ts +7 -0
  53. package/dist/monitor/routes/connectors.d.ts.map +1 -0
  54. package/dist/monitor/routes/connectors.js +417 -0
  55. package/dist/monitor/routes/connectors.js.map +1 -0
  56. package/dist/monitor/routes/home.d.ts +7 -0
  57. package/dist/monitor/routes/home.d.ts.map +1 -0
  58. package/dist/monitor/routes/home.js +15 -0
  59. package/dist/monitor/routes/home.js.map +1 -0
  60. package/dist/monitor/routes/index.d.ts +10 -0
  61. package/dist/monitor/routes/index.d.ts.map +1 -0
  62. package/dist/monitor/routes/index.js +19 -0
  63. package/dist/monitor/routes/index.js.map +1 -0
  64. package/dist/monitor/routes/popl.d.ts +7 -0
  65. package/dist/monitor/routes/popl.d.ts.map +1 -0
  66. package/dist/monitor/routes/popl.js +84 -0
  67. package/dist/monitor/routes/popl.js.map +1 -0
  68. package/dist/monitor/server.d.ts +24 -0
  69. package/dist/monitor/server.d.ts.map +1 -0
  70. package/dist/monitor/server.js +52 -0
  71. package/dist/monitor/server.js.map +1 -0
  72. package/dist/monitor/templates/components.d.ts +21 -0
  73. package/dist/monitor/templates/components.d.ts.map +1 -0
  74. package/dist/monitor/templates/components.js +405 -0
  75. package/dist/monitor/templates/components.js.map +1 -0
  76. package/dist/monitor/templates/home.d.ts +9 -0
  77. package/dist/monitor/templates/home.d.ts.map +1 -0
  78. package/dist/monitor/templates/home.js +322 -0
  79. package/dist/monitor/templates/home.js.map +1 -0
  80. package/dist/monitor/templates/layout.d.ts +26 -0
  81. package/dist/monitor/templates/layout.d.ts.map +1 -0
  82. package/dist/monitor/templates/layout.js +186 -0
  83. package/dist/monitor/templates/layout.js.map +1 -0
  84. package/dist/monitor/templates/popl.d.ts +33 -0
  85. package/dist/monitor/templates/popl.d.ts.map +1 -0
  86. package/dist/monitor/templates/popl.js +654 -0
  87. package/dist/monitor/templates/popl.js.map +1 -0
  88. package/dist/monitor/types.d.ts +121 -0
  89. package/dist/monitor/types.d.ts.map +1 -0
  90. package/dist/monitor/types.js +5 -0
  91. package/dist/monitor/types.js.map +1 -0
  92. package/package.json +3 -1
@@ -933,6 +933,13 @@ function getConnectorReportStyles() {
933
933
  border-color: var(--accent-blue);
934
934
  background: rgba(0, 212, 255, 0.15);
935
935
  }
936
+ .session-item.highlight {
937
+ animation: highlightPulse 2s ease-out;
938
+ }
939
+ @keyframes highlightPulse {
940
+ 0% { box-shadow: 0 0 0 3px rgba(0, 212, 255, 0.6); }
941
+ 100% { box-shadow: 0 0 0 0 rgba(0, 212, 255, 0); }
942
+ }
936
943
  .session-item-header {
937
944
  display: flex;
938
945
  align-items: center;
@@ -1107,6 +1114,226 @@ function getConnectorReportStyles() {
1107
1114
  .resize-handle:hover {
1108
1115
  background: var(--accent-blue);
1109
1116
  }
1117
+
1118
+ /* Analytics Panel (Phase 5.2) - Revised Layout */
1119
+
1120
+ /* Header with KPI stats inline */
1121
+ header {
1122
+ display: flex;
1123
+ align-items: center;
1124
+ justify-content: space-between;
1125
+ flex-wrap: wrap;
1126
+ gap: 12px;
1127
+ }
1128
+ .header-left {
1129
+ flex-shrink: 0;
1130
+ }
1131
+ .header-server-row {
1132
+ display: flex;
1133
+ flex-direction: column;
1134
+ align-items: center;
1135
+ gap: 4px;
1136
+ }
1137
+ .header-caps {
1138
+ display: flex;
1139
+ gap: 4px;
1140
+ }
1141
+ .header-server-info {
1142
+ display: flex;
1143
+ gap: 12px;
1144
+ font-size: 0.75em;
1145
+ color: var(--text-secondary);
1146
+ }
1147
+ .header-server-info .server-name {
1148
+ color: var(--text-primary);
1149
+ }
1150
+ .header-label {
1151
+ color: var(--text-secondary);
1152
+ font-size: 0.9em;
1153
+ }
1154
+ .no-caps {
1155
+ color: var(--text-secondary);
1156
+ font-style: italic;
1157
+ }
1158
+ .kpi-row {
1159
+ display: flex;
1160
+ gap: 16px;
1161
+ flex-wrap: wrap;
1162
+ align-items: baseline;
1163
+ }
1164
+ .kpi-item {
1165
+ display: flex;
1166
+ flex-direction: column;
1167
+ align-items: center;
1168
+ padding: 0;
1169
+ background: transparent;
1170
+ min-width: 50px;
1171
+ }
1172
+ .kpi-item .kpi-value {
1173
+ font-size: 0.95em;
1174
+ font-weight: 600;
1175
+ color: var(--accent-blue);
1176
+ font-family: 'SFMono-Regular', Consolas, monospace;
1177
+ line-height: 1.2;
1178
+ }
1179
+ .kpi-item .kpi-label {
1180
+ font-size: 0.55em;
1181
+ color: var(--text-secondary);
1182
+ text-transform: uppercase;
1183
+ letter-spacing: 0.5px;
1184
+ }
1185
+ /* All KPI values use accent-blue for unified appearance */
1186
+
1187
+ /* Connector top section: info + charts row */
1188
+ .connector-top {
1189
+ display: flex;
1190
+ gap: 16px;
1191
+ padding: 12px 20px;
1192
+ border-bottom: 1px solid var(--border-color);
1193
+ background: var(--bg-secondary);
1194
+ }
1195
+ .connector-top .connector-info {
1196
+ flex: 0 0 360px;
1197
+ max-width: 360px;
1198
+ border-bottom: none;
1199
+ padding: 0;
1200
+ }
1201
+ .analytics-panel {
1202
+ flex: 1;
1203
+ display: flex;
1204
+ gap: 12px;
1205
+ align-items: stretch;
1206
+ }
1207
+
1208
+ /* Charts row - 4 items horizontal with custom flex ratios */
1209
+ .heatmap-container {
1210
+ flex: 0.8;
1211
+ background: var(--bg-primary);
1212
+ border: 1px solid var(--border-color);
1213
+ border-radius: 6px;
1214
+ padding: 8px;
1215
+ min-width: 0;
1216
+ }
1217
+ .latency-histogram {
1218
+ flex: 1.4;
1219
+ background: var(--bg-primary);
1220
+ border: 1px solid var(--border-color);
1221
+ border-radius: 6px;
1222
+ padding: 8px;
1223
+ min-width: 0;
1224
+ }
1225
+ .top-tools, .method-distribution {
1226
+ flex: 1;
1227
+ background: var(--bg-primary);
1228
+ border: 1px solid var(--border-color);
1229
+ border-radius: 6px;
1230
+ padding: 8px;
1231
+ min-width: 0;
1232
+ }
1233
+ .chart-title {
1234
+ font-size: 0.75em;
1235
+ color: var(--text-secondary);
1236
+ margin-bottom: 4px;
1237
+ }
1238
+
1239
+ /* Heatmap - using neon blue gradient for consistency with theme */
1240
+ .heatmap-title {
1241
+ font-size: 0.75em;
1242
+ color: var(--text-secondary);
1243
+ margin-bottom: 4px;
1244
+ }
1245
+ .heatmap-level-0 { fill: var(--bg-tertiary); }
1246
+ .heatmap-level-1 { fill: #0a3d4d; }
1247
+ .heatmap-level-2 { fill: #0d5c73; }
1248
+ .heatmap-level-3 { fill: #0097b2; }
1249
+ .heatmap-level-4 { fill: #00d4ff; }
1250
+
1251
+ /* Histogram */
1252
+ .histogram-bar { fill: var(--accent-blue); }
1253
+ .histogram-label { fill: var(--text-secondary); font-size: 9px; }
1254
+
1255
+ /* Top Tools */
1256
+ .top-tool-row {
1257
+ display: flex;
1258
+ align-items: center;
1259
+ gap: 6px;
1260
+ margin-bottom: 3px;
1261
+ font-size: 0.75em;
1262
+ }
1263
+ .top-tool-rank {
1264
+ color: var(--text-secondary);
1265
+ width: 14px;
1266
+ flex-shrink: 0;
1267
+ }
1268
+ .top-tool-name {
1269
+ flex: 1;
1270
+ min-width: 0;
1271
+ overflow: hidden;
1272
+ text-overflow: ellipsis;
1273
+ white-space: nowrap;
1274
+ font-family: 'SFMono-Regular', Consolas, monospace;
1275
+ }
1276
+ .top-tool-bar-container {
1277
+ width: 50px;
1278
+ height: 6px;
1279
+ background: var(--bg-tertiary);
1280
+ border-radius: 3px;
1281
+ overflow: hidden;
1282
+ flex-shrink: 0;
1283
+ }
1284
+ .top-tool-bar {
1285
+ height: 100%;
1286
+ background: var(--accent-blue);
1287
+ border-radius: 3px;
1288
+ }
1289
+ .top-tool-pct {
1290
+ color: var(--text-secondary);
1291
+ width: 28px;
1292
+ text-align: right;
1293
+ flex-shrink: 0;
1294
+ }
1295
+ .no-data-message {
1296
+ color: var(--text-secondary);
1297
+ font-size: 0.75em;
1298
+ text-align: center;
1299
+ padding: 8px;
1300
+ }
1301
+
1302
+ /* Method Distribution Donut Chart */
1303
+ .donut-container {
1304
+ display: flex;
1305
+ align-items: center;
1306
+ gap: 8px;
1307
+ }
1308
+ .donut-legend {
1309
+ flex: 1;
1310
+ font-size: 0.7em;
1311
+ min-width: 0;
1312
+ }
1313
+ .donut-legend-item {
1314
+ display: flex;
1315
+ align-items: center;
1316
+ gap: 4px;
1317
+ margin-bottom: 2px;
1318
+ white-space: nowrap;
1319
+ overflow: hidden;
1320
+ }
1321
+ .donut-legend-color {
1322
+ width: 8px;
1323
+ height: 8px;
1324
+ border-radius: 2px;
1325
+ flex-shrink: 0;
1326
+ }
1327
+ .donut-legend-label {
1328
+ overflow: hidden;
1329
+ text-overflow: ellipsis;
1330
+ flex: 1;
1331
+ min-width: 0;
1332
+ }
1333
+ .donut-legend-pct {
1334
+ color: var(--text-secondary);
1335
+ flex-shrink: 0;
1336
+ }
1110
1337
  `;
1111
1338
  }
1112
1339
  /**
@@ -1335,12 +1562,328 @@ function getConnectorReportScript() {
1335
1562
  }
1336
1563
  });
1337
1564
 
1338
- // Select first session by default
1339
- if (sessions.length > 0) {
1565
+ // Check for session parameter in URL
1566
+ function getSessionFromUrl() {
1567
+ const params = new URLSearchParams(window.location.search);
1568
+ return params.get('session');
1569
+ }
1570
+
1571
+ // Scroll session item into view
1572
+ function scrollSessionIntoView(sessionId) {
1573
+ const sessionItem = document.querySelector('.session-item[data-session-id="' + sessionId + '"]');
1574
+ if (sessionItem) {
1575
+ sessionItem.scrollIntoView({ block: 'center', behavior: 'smooth' });
1576
+ // Add highlight effect
1577
+ sessionItem.classList.add('highlight');
1578
+ setTimeout(() => sessionItem.classList.remove('highlight'), 2000);
1579
+ }
1580
+ }
1581
+
1582
+ // Select session from URL or first session by default
1583
+ const urlSession = getSessionFromUrl();
1584
+ if (urlSession) {
1585
+ // Try to find matching session (full or partial match)
1586
+ const matchingSession = sessions.find(s =>
1587
+ s.session_id === urlSession || s.session_id.startsWith(urlSession)
1588
+ );
1589
+ if (matchingSession) {
1590
+ showSession(matchingSession.session_id);
1591
+ // Scroll into view after a short delay to ensure DOM is ready
1592
+ setTimeout(() => scrollSessionIntoView(matchingSession.session_id), 100);
1593
+ } else if (sessions.length > 0) {
1594
+ showSession(sessions[0].session_id);
1595
+ }
1596
+ } else if (sessions.length > 0) {
1340
1597
  showSession(sessions[0].session_id);
1341
1598
  }
1342
1599
  `;
1343
1600
  }
1601
+ // ============================================================================
1602
+ // Analytics Panel Rendering (Phase 5.2)
1603
+ // ============================================================================
1604
+ /**
1605
+ * Render KPI stats row for header (inline compact display)
1606
+ */
1607
+ function renderKpiRow(kpis) {
1608
+ // Format large numbers with K/M suffix
1609
+ const formatNumber = (n) => {
1610
+ if (n >= 1000000)
1611
+ return (n / 1000000).toFixed(1) + 'M';
1612
+ if (n >= 1000)
1613
+ return (n / 1000).toFixed(1) + 'K';
1614
+ return String(n);
1615
+ };
1616
+ // Format bytes for display
1617
+ const formatBytesCompact = (bytes) => {
1618
+ if (bytes === 0)
1619
+ return '0';
1620
+ const k = 1024;
1621
+ const sizes = ['B', 'KB', 'MB', 'GB'];
1622
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
1623
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + sizes[i];
1624
+ };
1625
+ return `
1626
+ <div class="kpi-row">
1627
+ <div class="kpi-item">
1628
+ <div class="kpi-value">${formatNumber(kpis.sessions_displayed)}</div>
1629
+ <div class="kpi-label">Sessions</div>
1630
+ </div>
1631
+ <div class="kpi-item">
1632
+ <div class="kpi-value">${formatNumber(kpis.rpc_total)}</div>
1633
+ <div class="kpi-label">RPCs</div>
1634
+ </div>
1635
+ <div class="kpi-item">
1636
+ <div class="kpi-value">${formatNumber(kpis.rpc_err)}</div>
1637
+ <div class="kpi-label">Error</div>
1638
+ </div>
1639
+ <div class="kpi-item">
1640
+ <div class="kpi-value">${kpis.avg_latency_ms !== null ? kpis.avg_latency_ms : '-'}</div>
1641
+ <div class="kpi-label">Avg Latency</div>
1642
+ </div>
1643
+ <div class="kpi-item">
1644
+ <div class="kpi-value">${kpis.p95_latency_ms !== null ? kpis.p95_latency_ms : '-'}</div>
1645
+ <div class="kpi-label">P95 Latency</div>
1646
+ </div>
1647
+ <div class="kpi-item">
1648
+ <div class="kpi-value">${formatBytesCompact(kpis.total_request_bytes)}</div>
1649
+ <div class="kpi-label">Req Size</div>
1650
+ </div>
1651
+ <div class="kpi-item">
1652
+ <div class="kpi-value">${formatBytesCompact(kpis.total_response_bytes)}</div>
1653
+ <div class="kpi-label">Res Size</div>
1654
+ </div>
1655
+ </div>`;
1656
+ }
1657
+ /**
1658
+ * Get intensity level (0-4) for heatmap cell based on count and max
1659
+ */
1660
+ function getHeatmapLevel(count, maxCount) {
1661
+ if (count === 0 || maxCount === 0)
1662
+ return 0;
1663
+ const ratio = count / maxCount;
1664
+ if (ratio <= 0.25)
1665
+ return 1;
1666
+ if (ratio <= 0.5)
1667
+ return 2;
1668
+ if (ratio <= 0.75)
1669
+ return 3;
1670
+ return 4;
1671
+ }
1672
+ /**
1673
+ * Render activity heatmap (GitHub contributions style, SVG)
1674
+ */
1675
+ export function renderHeatmap(heatmap) {
1676
+ const cellSize = 10;
1677
+ const cellGap = 2;
1678
+ const cellTotal = cellSize + cellGap;
1679
+ // Group cells by week (7 days per column)
1680
+ const weeks = [];
1681
+ let currentWeek = [];
1682
+ // Find the day of week for the start date (0 = Sunday)
1683
+ const startDow = new Date(heatmap.start_date + 'T00:00:00Z').getUTCDay();
1684
+ // Add empty cells for days before start_date
1685
+ for (let i = 0; i < startDow; i++) {
1686
+ currentWeek.push({ date: '', count: -1 }); // -1 indicates empty
1687
+ }
1688
+ for (const cell of heatmap.cells) {
1689
+ currentWeek.push(cell);
1690
+ if (currentWeek.length === 7) {
1691
+ weeks.push(currentWeek);
1692
+ currentWeek = [];
1693
+ }
1694
+ }
1695
+ if (currentWeek.length > 0) {
1696
+ weeks.push(currentWeek);
1697
+ }
1698
+ // Calculate SVG dimensions
1699
+ const svgWidth = weeks.length * cellTotal;
1700
+ const svgHeight = 7 * cellTotal;
1701
+ // Generate SVG rects
1702
+ let rects = '';
1703
+ weeks.forEach((week, weekIdx) => {
1704
+ week.forEach((cell, dayIdx) => {
1705
+ if (cell.count < 0)
1706
+ return; // Skip empty cells
1707
+ const level = getHeatmapLevel(cell.count, heatmap.max_count);
1708
+ const x = weekIdx * cellTotal;
1709
+ const y = dayIdx * cellTotal;
1710
+ const title = cell.date ? `${cell.date}: ${cell.count} RPCs` : '';
1711
+ rects += `<rect x="${x}" y="${y}" width="${cellSize}" height="${cellSize}" rx="2" class="heatmap-level-${level}"><title>${escapeHtml(title)}</title></rect>`;
1712
+ });
1713
+ });
1714
+ return `
1715
+ <div class="heatmap-container">
1716
+ <div class="heatmap-title">Activity (${escapeHtml(heatmap.start_date)} to ${escapeHtml(heatmap.end_date)})</div>
1717
+ <svg width="${svgWidth}" height="${svgHeight}" viewBox="0 0 ${svgWidth} ${svgHeight}">
1718
+ ${rects}
1719
+ </svg>
1720
+ </div>`;
1721
+ }
1722
+ /**
1723
+ * Render latency histogram (SVG bar chart)
1724
+ */
1725
+ function renderLatencyHistogram(latency) {
1726
+ if (latency.sample_size === 0) {
1727
+ return `
1728
+ <div class="latency-histogram">
1729
+ <div class="chart-title">Latency Distribution</div>
1730
+ <div class="no-data-message">No latency data</div>
1731
+ </div>`;
1732
+ }
1733
+ const maxCount = Math.max(...latency.buckets.map((b) => b.count), 1);
1734
+ const barWidth = 30;
1735
+ const barGap = 4;
1736
+ const chartWidth = latency.buckets.length * (barWidth + barGap);
1737
+ const chartHeight = 60;
1738
+ const labelHeight = 16;
1739
+ let bars = '';
1740
+ latency.buckets.forEach((bucket, idx) => {
1741
+ const barHeight = maxCount > 0 ? (bucket.count / maxCount) * chartHeight : 0;
1742
+ const x = idx * (barWidth + barGap);
1743
+ const y = chartHeight - barHeight;
1744
+ const title = `${bucket.label}ms: ${bucket.count} RPCs`;
1745
+ bars += `<rect x="${x}" y="${y}" width="${barWidth}" height="${barHeight}" class="histogram-bar"><title>${escapeHtml(title)}</title></rect>`;
1746
+ bars += `<text x="${x + barWidth / 2}" y="${chartHeight + labelHeight - 4}" text-anchor="middle" class="histogram-label">${escapeHtml(bucket.label)}</text>`;
1747
+ });
1748
+ return `
1749
+ <div class="latency-histogram">
1750
+ <div class="chart-title">Latency Distribution (${latency.sample_size} samples)</div>
1751
+ <svg width="${chartWidth}" height="${chartHeight + labelHeight}" viewBox="0 0 ${chartWidth} ${chartHeight + labelHeight}">
1752
+ ${bars}
1753
+ </svg>
1754
+ </div>`;
1755
+ }
1756
+ /**
1757
+ * Render top 5 tools
1758
+ */
1759
+ function renderTopTools(topTools) {
1760
+ if (topTools.items.length === 0) {
1761
+ return `
1762
+ <div class="top-tools">
1763
+ <div class="chart-title">Top Tools</div>
1764
+ <div class="no-data-message">No tool calls</div>
1765
+ </div>`;
1766
+ }
1767
+ const rows = topTools.items
1768
+ .map((tool, idx) => {
1769
+ return `
1770
+ <div class="top-tool-row">
1771
+ <span class="top-tool-rank">${idx + 1}.</span>
1772
+ <span class="top-tool-name" title="${escapeHtml(tool.name)}">${escapeHtml(tool.name)}</span>
1773
+ <div class="top-tool-bar-container">
1774
+ <div class="top-tool-bar" style="width: ${tool.pct}%"></div>
1775
+ </div>
1776
+ <span class="top-tool-pct">${tool.pct}%</span>
1777
+ </div>`;
1778
+ })
1779
+ .join('');
1780
+ return `
1781
+ <div class="top-tools">
1782
+ <div class="chart-title">Top Tools (${topTools.total_calls} calls)</div>
1783
+ ${rows}
1784
+ </div>`;
1785
+ }
1786
+ /**
1787
+ * Donut chart colors (blue gradient palette)
1788
+ */
1789
+ const DONUT_COLORS = [
1790
+ '#00d4ff', // Neon blue (brightest)
1791
+ '#0097b2', // Medium bright blue
1792
+ '#0d5c73', // Medium blue
1793
+ '#0a4d5c', // Darker blue
1794
+ '#083d47', // Dark blue
1795
+ '#5a6a70', // Blue-gray (for "Others")
1796
+ ];
1797
+ /**
1798
+ * Render method distribution donut chart (SVG)
1799
+ */
1800
+ export function renderMethodDistribution(methodDist) {
1801
+ if (methodDist.slices.length === 0) {
1802
+ return `
1803
+ <div class="method-distribution">
1804
+ <div class="chart-title">Method Distribution</div>
1805
+ <div class="no-data-message">No RPCs</div>
1806
+ </div>`;
1807
+ }
1808
+ // SVG donut chart parameters
1809
+ const size = 60;
1810
+ const cx = size / 2;
1811
+ const cy = size / 2;
1812
+ const outerRadius = 26;
1813
+ const innerRadius = 16; // Creates the donut hole
1814
+ // Generate SVG path segments
1815
+ let paths = '';
1816
+ let currentAngle = -90; // Start from top (12 o'clock)
1817
+ methodDist.slices.forEach((slice, idx) => {
1818
+ const angle = (slice.pct / 100) * 360;
1819
+ const startAngle = currentAngle;
1820
+ const endAngle = currentAngle + angle;
1821
+ // Convert angles to radians
1822
+ const startRad = (startAngle * Math.PI) / 180;
1823
+ const endRad = (endAngle * Math.PI) / 180;
1824
+ // Calculate arc points for outer radius
1825
+ const x1Outer = cx + outerRadius * Math.cos(startRad);
1826
+ const y1Outer = cy + outerRadius * Math.sin(startRad);
1827
+ const x2Outer = cx + outerRadius * Math.cos(endRad);
1828
+ const y2Outer = cy + outerRadius * Math.sin(endRad);
1829
+ // Calculate arc points for inner radius
1830
+ const x1Inner = cx + innerRadius * Math.cos(endRad);
1831
+ const y1Inner = cy + innerRadius * Math.sin(endRad);
1832
+ const x2Inner = cx + innerRadius * Math.cos(startRad);
1833
+ const y2Inner = cy + innerRadius * Math.sin(startRad);
1834
+ // Large arc flag
1835
+ const largeArc = angle > 180 ? 1 : 0;
1836
+ // Color
1837
+ const color = DONUT_COLORS[idx % DONUT_COLORS.length];
1838
+ // SVG path for donut segment
1839
+ const d = [
1840
+ `M ${x1Outer} ${y1Outer}`, // Start at outer edge
1841
+ `A ${outerRadius} ${outerRadius} 0 ${largeArc} 1 ${x2Outer} ${y2Outer}`, // Outer arc
1842
+ `L ${x1Inner} ${y1Inner}`, // Line to inner edge
1843
+ `A ${innerRadius} ${innerRadius} 0 ${largeArc} 0 ${x2Inner} ${y2Inner}`, // Inner arc (reverse)
1844
+ 'Z', // Close path
1845
+ ].join(' ');
1846
+ const title = `${slice.method}: ${slice.count} (${slice.pct}%)`;
1847
+ paths += `<path d="${d}" fill="${color}"><title>${escapeHtml(title)}</title></path>`;
1848
+ currentAngle = endAngle;
1849
+ });
1850
+ // Generate legend
1851
+ const legendItems = methodDist.slices
1852
+ .map((slice, idx) => {
1853
+ const color = DONUT_COLORS[idx % DONUT_COLORS.length];
1854
+ return `
1855
+ <div class="donut-legend-item">
1856
+ <div class="donut-legend-color" style="background: ${color}"></div>
1857
+ <span class="donut-legend-label" title="${escapeHtml(slice.method)}">${escapeHtml(slice.method)}</span>
1858
+ <span class="donut-legend-pct">${slice.pct}%</span>
1859
+ </div>`;
1860
+ })
1861
+ .join('');
1862
+ return `
1863
+ <div class="method-distribution">
1864
+ <div class="chart-title">Methods (${methodDist.total_rpcs} RPCs)</div>
1865
+ <div class="donut-container">
1866
+ <svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}">
1867
+ ${paths}
1868
+ </svg>
1869
+ <div class="donut-legend">
1870
+ ${legendItems}
1871
+ </div>
1872
+ </div>
1873
+ </div>`;
1874
+ }
1875
+ /**
1876
+ * Render the analytics panel (4 charts horizontally)
1877
+ */
1878
+ function renderAnalyticsPanel(analytics) {
1879
+ return `
1880
+ <div class="analytics-panel">
1881
+ ${renderHeatmap(analytics.heatmap)}
1882
+ ${renderLatencyHistogram(analytics.latency)}
1883
+ ${renderTopTools(analytics.top_tools)}
1884
+ ${renderMethodDistribution(analytics.method_distribution)}
1885
+ </div>`;
1886
+ }
1344
1887
  /**
1345
1888
  * Render a session item for the sessions pane
1346
1889
  */
@@ -1439,7 +1982,7 @@ ${rpcRows}
1439
1982
  * Generate Connector HTML report (3-hierarchy: Connector -> Sessions -> RPCs)
1440
1983
  */
1441
1984
  export function generateConnectorHtml(report) {
1442
- const { meta, connector, sessions, session_reports } = report;
1985
+ const { meta, connector, sessions, session_reports, analytics } = report;
1443
1986
  // Pagination info
1444
1987
  const fromNum = connector.offset + 1;
1445
1988
  const toNum = connector.offset + connector.displayed_sessions;
@@ -1450,8 +1993,8 @@ export function generateConnectorHtml(report) {
1450
1993
  const transportDisplay = connector.transport.type === 'stdio'
1451
1994
  ? connector.transport.command || '(unknown command)'
1452
1995
  : connector.transport.url || '(unknown URL)';
1453
- // Server info (if available)
1454
- let serverInfoHtml = '';
1996
+ // Server info for header (if available)
1997
+ let headerServerHtml = '';
1455
1998
  if (connector.server) {
1456
1999
  const { name, version, protocolVersion, capabilities } = connector.server;
1457
2000
  const serverName = name || '(unknown)';
@@ -1465,14 +2008,15 @@ export function generateConnectorHtml(report) {
1465
2008
  capBadges.push('<span class="badge cap-enabled">resources</span>');
1466
2009
  if (capabilities.prompts)
1467
2010
  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>`;
2011
+ const capsDisplay = capBadges.length > 0 ? capBadges.join(' ') : '';
2012
+ headerServerHtml = `
2013
+ <div class="header-server-row">
2014
+ <div class="header-caps"><span class="header-label">Capabilities:</span> ${capsDisplay || '<span class="no-caps">(none)</span>'}</div>
2015
+ <div class="header-server-info">
2016
+ <span class="server-name"><span class="header-label">Server:</span> ${escapeHtml(serverName)} ${escapeHtml(serverVersion)}</span>
2017
+ <span class="server-protocol"><span class="header-label">Protocol:</span> ${escapeHtml(protocolDisplay)}</span>
2018
+ </div>
2019
+ </div>`;
1476
2020
  }
1477
2021
  // Session items
1478
2022
  const sessionItems = sessions.map(s => renderConnectorSessionItem(s)).join('\n');
@@ -1494,26 +2038,32 @@ export function generateConnectorHtml(report) {
1494
2038
  </head>
1495
2039
  <body>
1496
2040
  <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>
2041
+ <div class="header-left">
2042
+ <h1>Connector: <span class="badge">${escapeHtml(connector.connector_id)}</span></h1>
2043
+ <p class="meta">Generated by ${escapeHtml(meta.generatedBy)} at ${formatTimestamp(meta.generatedAt)}${meta.redacted ? ' (redacted)' : ''} | ${paginationInfo}</p>
2044
+ </div>
2045
+ ${headerServerHtml}
2046
+ ${renderKpiRow(analytics.kpis)}
1499
2047
  </header>
1500
2048
 
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>
2049
+ <div class="connector-top">
2050
+ <div class="connector-info expanded">
2051
+ <div class="connector-info-toggle">
2052
+ <h2>Connector Info</h2>
2053
+ <span class="toggle-icon">▼</span>
2054
+ </div>
2055
+ <div class="connector-info-content">
2056
+ <dl>
2057
+ <dt>Transport</dt>
2058
+ <dd><span class="badge">${escapeHtml(connector.transport.type)}</span></dd>
2059
+ <dt>${connector.transport.type === 'stdio' ? 'Command' : 'URL'}</dt>
2060
+ <dd><code>${escapeHtml(transportDisplay)}</code></dd>
2061
+ <dt>Enabled</dt>
2062
+ <dd>${connector.enabled ? '<span class="badge status-OK">yes</span>' : '<span class="badge status-ERR">no</span>'}</dd>
2063
+ </dl>
2064
+ </div>
1516
2065
  </div>
2066
+ ${renderAnalyticsPanel(analytics)}
1517
2067
  </div>
1518
2068
 
1519
2069
  <div class="main-container">