proofscan 0.10.32 → 0.10.34

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 (34) hide show
  1. package/dist/html/rpc-inspector.d.ts +15 -0
  2. package/dist/html/rpc-inspector.d.ts.map +1 -1
  3. package/dist/html/rpc-inspector.js +66 -0
  4. package/dist/html/rpc-inspector.js.map +1 -1
  5. package/dist/html/templates.d.ts +6 -0
  6. package/dist/html/templates.d.ts.map +1 -1
  7. package/dist/html/templates.js +566 -52
  8. package/dist/html/templates.js.map +1 -1
  9. package/dist/monitor/data/connectors.d.ts +19 -0
  10. package/dist/monitor/data/connectors.d.ts.map +1 -1
  11. package/dist/monitor/data/connectors.js +61 -0
  12. package/dist/monitor/data/connectors.js.map +1 -1
  13. package/dist/monitor/data/events.d.ts +21 -0
  14. package/dist/monitor/data/events.d.ts.map +1 -0
  15. package/dist/monitor/data/events.js +151 -0
  16. package/dist/monitor/data/events.js.map +1 -0
  17. package/dist/monitor/routes/api.d.ts.map +1 -1
  18. package/dist/monitor/routes/api.js +39 -1
  19. package/dist/monitor/routes/api.js.map +1 -1
  20. package/dist/monitor/routes/connectors.js +9 -7
  21. package/dist/monitor/routes/connectors.js.map +1 -1
  22. package/dist/monitor/templates/home.d.ts.map +1 -1
  23. package/dist/monitor/templates/home.js +1 -0
  24. package/dist/monitor/templates/home.js.map +1 -1
  25. package/dist/monitor/templates/layout.d.ts +1 -0
  26. package/dist/monitor/templates/layout.d.ts.map +1 -1
  27. package/dist/monitor/templates/layout.js +114 -44
  28. package/dist/monitor/templates/layout.js.map +1 -1
  29. package/dist/monitor/templates/popl.d.ts.map +1 -1
  30. package/dist/monitor/templates/popl.js +3 -0
  31. package/dist/monitor/templates/popl.js.map +1 -1
  32. package/dist/monitor/types.d.ts +24 -0
  33. package/dist/monitor/types.d.ts.map +1 -1
  34. package/package.json +1 -1
@@ -6,7 +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, renderRequestSummary, renderResponseSummary, renderSummaryRowsHtml, } from './rpc-inspector.js';
9
+ import { getRpcInspectorStyles, getRpcInspectorScript, renderJsonWithPaths, renderRequestSummary, renderResponseSummary, renderSummaryRowsHtml, detectSensitiveKeys, } from './rpc-inspector.js';
10
10
  /**
11
11
  * Escape HTML special characters to prevent XSS
12
12
  */
@@ -105,6 +105,21 @@ function getRpcReportStyles() {
105
105
  .badge.status-OK { border-color: var(--status-ok); color: var(--status-ok); }
106
106
  .badge.status-ERR { border-color: var(--status-err); color: var(--status-err); }
107
107
  .badge.status-PENDING { border-color: var(--status-pending); color: var(--status-pending); }
108
+ /* Sensitive content warning badge (Phase 12.x-c) */
109
+ .sensitive-badge {
110
+ display: inline-flex;
111
+ align-items: center;
112
+ gap: 4px;
113
+ padding: 2px 8px;
114
+ margin-left: 8px;
115
+ background: rgba(210, 153, 34, 0.15);
116
+ border: 1px solid rgba(210, 153, 34, 0.3);
117
+ border-radius: 12px;
118
+ font-size: 11px;
119
+ font-weight: 500;
120
+ color: #d29922;
121
+ vertical-align: middle;
122
+ }
108
123
  .section {
109
124
  background: var(--bg-secondary);
110
125
  border-radius: 8px;
@@ -231,6 +246,21 @@ function getSessionReportStyles() {
231
246
  .badge.status-OK { border-color: var(--status-ok); color: var(--status-ok); }
232
247
  .badge.status-ERR { border-color: var(--status-err); color: var(--status-err); }
233
248
  .badge.status-PENDING { border-color: var(--status-pending); color: var(--status-pending); }
249
+ /* Sensitive content warning badge (Phase 12.x-c) */
250
+ .sensitive-badge {
251
+ display: inline-flex;
252
+ align-items: center;
253
+ gap: 4px;
254
+ padding: 2px 8px;
255
+ margin-left: 8px;
256
+ background: rgba(210, 153, 34, 0.15);
257
+ border: 1px solid rgba(210, 153, 34, 0.3);
258
+ border-radius: 12px;
259
+ font-size: 11px;
260
+ font-weight: 500;
261
+ color: #d29922;
262
+ vertical-align: middle;
263
+ }
234
264
  /* Two-pane layout */
235
265
  .container {
236
266
  display: flex;
@@ -470,19 +500,29 @@ function getSessionReportScript() {
470
500
  const requestRawHtml = rpc._requestRawHtml || '<span class="json-null">(no data)</span>';
471
501
  const responseRawHtml = rpc._responseRawHtml || '<span class="json-null">(no data)</span>';
472
502
 
503
+ // Sensitive content warning badge (Phase 12.x-c)
504
+ // Escape keys to prevent XSS via malicious key names
505
+ const sensitiveKeys = (rpc._sensitiveKeys || []).map(function(k) { return escapeHtml(k); });
506
+ const sensitiveTooltip = sensitiveKeys.length > 5
507
+ ? 'Contains ' + sensitiveKeys.length + ' sensitive keys: ' + sensitiveKeys.slice(0, 5).join(', ') + '...'
508
+ : 'Contains sensitive keys: ' + sensitiveKeys.join(', ');
509
+ const sensitiveBadge = rpc._hasSensitive
510
+ ? '<span class="sensitive-badge" title="' + escapeHtml(sensitiveTooltip) + '">⚠ Sensitive</span>'
511
+ : '';
512
+
473
513
  // Determine default target based on method (response-focused methods default to response)
474
514
  const defaultTarget = (rpc.method === 'tools/list' || rpc.method === 'initialize' || rpc.method.startsWith('resources/') || rpc.method.startsWith('prompts/')) ? 'response' : 'request';
475
515
 
476
516
  rightPane.innerHTML =
477
517
  '<div class="detail-section">' +
478
- ' <h2>RPC Info</h2>' +
518
+ ' <h2>RPC Info' + sensitiveBadge + '</h2>' +
479
519
  ' <div class="rpc-info-grid">' +
480
520
  ' <div class="rpc-info-item"><dt>RPC ID</dt><dd><span class="badge">' + escapeHtml(rpc.rpc_id) + '</span></dd></div>' +
481
521
  ' <div class="rpc-info-item"><dt>Method</dt><dd><span class="badge">' + escapeHtml(rpc.method) + '</span></dd></div>' +
482
522
  ' <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>' +
483
523
  ' <div class="rpc-info-item"><dt>Latency</dt><dd><span class="badge">' + latency + '</span></dd></div>' +
484
- ' <div class="rpc-info-item"><dt>Req Size</dt><dd>' + formatBytes(rpc.request.size) + '</dd></div>' +
485
- ' <div class="rpc-info-item"><dt>Res Size</dt><dd>' + formatBytes(rpc.response.size) + '</dd></div>' +
524
+ ' <div class="rpc-info-item"><dt>Request</dt><dd>' + escapeHtml(rpc.request_ts) + '</dd></div>' +
525
+ ' <div class="rpc-info-item"><dt>Response</dt><dd>' + escapeHtml(rpc.response_ts || '-') + '</dd></div>' +
486
526
  ' </div>' +
487
527
  '</div>' +
488
528
  '<div class="detail-section">' +
@@ -717,15 +757,22 @@ export function generateSessionHtml(report) {
717
757
  const rpcRows = rpcs.map((rpc, idx) => renderRpcRow(rpc, idx)).join('\n');
718
758
  // Pre-render summary and raw JSON HTML for each RPC (for RPC Inspector)
719
759
  // Now generates separate request/response summaries for Req/Res toggle
760
+ // Also detect sensitive content for warning badge (Phase 12.x-c)
720
761
  const rpcsWithInspectorHtml = rpcs.map((rpc) => {
721
762
  const requestSummaryRows = renderRequestSummary(rpc.method, rpc.request.json);
722
763
  const responseSummaryRows = renderResponseSummary(rpc.method, rpc.response.json);
764
+ // Detect sensitive keys in request/response
765
+ const reqSensitiveKeys = detectSensitiveKeys(rpc.request.json);
766
+ const resSensitiveKeys = detectSensitiveKeys(rpc.response.json);
767
+ const hasSensitive = reqSensitiveKeys.length > 0 || resSensitiveKeys.length > 0;
723
768
  return {
724
769
  ...rpc,
725
770
  _requestSummaryHtml: renderSummaryRowsHtml(requestSummaryRows),
726
771
  _responseSummaryHtml: renderSummaryRowsHtml(responseSummaryRows),
727
772
  _requestRawHtml: renderJsonWithPaths(rpc.request.json, '#'),
728
773
  _responseRawHtml: renderJsonWithPaths(rpc.response.json, '#'),
774
+ _hasSensitive: hasSensitive,
775
+ _sensitiveKeys: [...reqSensitiveKeys, ...resSensitiveKeys],
729
776
  };
730
777
  });
731
778
  const reportWithInspectorHtml = {
@@ -822,8 +869,9 @@ function getConnectorReportStyles() {
822
869
  --status-pending: #d29922;
823
870
  --border-color: #30363d;
824
871
  --link-color: #58a6ff;
825
- --sessions-pane-width: 280px;
826
- --left-pane-width: 420px;
872
+ --sessions-pane-width: 360px;
873
+ --left-pane-width: 480px;
874
+ --raw-pane-max-width: 480px;
827
875
  }
828
876
  * { box-sizing: border-box; }
829
877
  html, body {
@@ -972,6 +1020,21 @@ function getConnectorReportStyles() {
972
1020
  .badge.status-PENDING { border-color: var(--status-pending); color: var(--status-pending); }
973
1021
  .badge.cap-enabled { border-color: var(--accent-blue); color: var(--accent-blue); background: rgba(0, 212, 255, 0.1); }
974
1022
  .badge.cap-disabled { border-color: var(--border-color); color: var(--text-secondary); background: transparent; opacity: 0.5; }
1023
+ /* Sensitive content warning badge (Phase 12.x-c) */
1024
+ .sensitive-badge {
1025
+ display: inline-flex;
1026
+ align-items: center;
1027
+ gap: 4px;
1028
+ padding: 2px 8px;
1029
+ margin-left: 8px;
1030
+ background: rgba(210, 153, 34, 0.15);
1031
+ border: 1px solid rgba(210, 153, 34, 0.3);
1032
+ border-radius: 12px;
1033
+ font-size: 11px;
1034
+ font-weight: 500;
1035
+ color: #d29922;
1036
+ vertical-align: middle;
1037
+ }
975
1038
 
976
1039
  /* Connector info cards container (side by side) */
977
1040
  .connector-info-cards {
@@ -1068,15 +1131,43 @@ function getConnectorReportStyles() {
1068
1131
  .sessions-list {
1069
1132
  flex: 1;
1070
1133
  overflow-y: auto;
1071
- padding: 8px;
1134
+ padding: 4px 8px;
1135
+ }
1136
+ .sessions-header-row {
1137
+ display: grid;
1138
+ grid-template-columns: 70px 1fr 60px 50px;
1139
+ gap: 8px;
1140
+ padding: 4px 8px;
1141
+ font-size: 10px;
1142
+ color: var(--text-secondary);
1143
+ text-transform: uppercase;
1144
+ border-bottom: 1px solid var(--border-color);
1145
+ margin-bottom: 4px;
1072
1146
  }
1073
1147
  .session-item {
1074
- padding: 10px 12px;
1075
- border-radius: 6px;
1148
+ display: grid;
1149
+ grid-template-columns: 70px 1fr auto 50px 50px;
1150
+ gap: 8px;
1151
+ align-items: center;
1152
+ padding: 6px 8px;
1153
+ border-radius: 4px;
1076
1154
  cursor: pointer;
1077
- margin-bottom: 6px;
1155
+ margin-bottom: 2px;
1078
1156
  border: 1px solid transparent;
1079
1157
  background: var(--bg-primary);
1158
+ font-size: 11px;
1159
+ }
1160
+ .session-item .session-counts {
1161
+ display: flex;
1162
+ gap: 6px;
1163
+ font-size: 10px;
1164
+ color: var(--text-secondary);
1165
+ }
1166
+ .session-item .session-counts span {
1167
+ white-space: nowrap;
1168
+ }
1169
+ .session-item .session-extra {
1170
+ justify-self: end;
1080
1171
  }
1081
1172
  .session-item:hover {
1082
1173
  background: rgba(0, 212, 255, 0.1);
@@ -1093,26 +1184,22 @@ function getConnectorReportStyles() {
1093
1184
  0% { box-shadow: 0 0 0 3px rgba(0, 212, 255, 0.6); }
1094
1185
  100% { box-shadow: 0 0 0 0 rgba(0, 212, 255, 0); }
1095
1186
  }
1096
- .session-item-header {
1097
- display: flex;
1098
- align-items: center;
1099
- justify-content: space-between;
1100
- margin-bottom: 4px;
1101
- }
1102
1187
  .session-item .session-id {
1103
1188
  font-family: 'SFMono-Regular', Consolas, monospace;
1104
1189
  color: var(--accent-blue);
1105
- font-size: 0.85em;
1190
+ overflow: hidden;
1191
+ text-overflow: ellipsis;
1192
+ white-space: nowrap;
1106
1193
  }
1107
- .session-item .session-meta {
1108
- font-size: 0.75em;
1194
+ .session-item .session-timestamp {
1109
1195
  color: var(--text-secondary);
1196
+ overflow: hidden;
1197
+ text-overflow: ellipsis;
1198
+ white-space: nowrap;
1110
1199
  }
1111
- .session-item .session-stats {
1112
- display: flex;
1113
- gap: 8px;
1114
- font-size: 0.75em;
1200
+ .session-item .session-latency {
1115
1201
  color: var(--text-secondary);
1202
+ text-align: right;
1116
1203
  }
1117
1204
 
1118
1205
  /* Session detail pane (middle) */
@@ -1268,6 +1355,131 @@ function getConnectorReportStyles() {
1268
1355
  background: var(--accent-blue);
1269
1356
  }
1270
1357
 
1358
+ /* Events View Toggle (Issue #59) */
1359
+ .view-toggle {
1360
+ display: flex;
1361
+ gap: 2px;
1362
+ padding: 8px 12px;
1363
+ border-bottom: 1px solid var(--border-color);
1364
+ background: var(--bg-secondary);
1365
+ }
1366
+ .view-toggle-btn {
1367
+ background: transparent;
1368
+ border: 1px solid var(--border-color);
1369
+ color: var(--text-secondary);
1370
+ padding: 4px 12px;
1371
+ border-radius: 4px;
1372
+ cursor: pointer;
1373
+ font-size: 11px;
1374
+ transition: all 0.15s;
1375
+ }
1376
+ .view-toggle-btn:first-child {
1377
+ border-radius: 4px 0 0 4px;
1378
+ }
1379
+ .view-toggle-btn:last-child {
1380
+ border-radius: 0 4px 4px 0;
1381
+ }
1382
+ .view-toggle-btn:hover {
1383
+ border-color: var(--accent-blue);
1384
+ color: var(--text-primary);
1385
+ }
1386
+ .view-toggle-btn.active {
1387
+ background: rgba(0, 212, 255, 0.15);
1388
+ border-color: var(--accent-blue);
1389
+ color: var(--accent-blue);
1390
+ }
1391
+ .view-toggle-count {
1392
+ font-size: 10px;
1393
+ color: var(--text-secondary);
1394
+ margin-left: 4px;
1395
+ }
1396
+
1397
+ /* Events List (Issue #59) */
1398
+ .events-list {
1399
+ flex: 1;
1400
+ overflow-y: auto;
1401
+ display: none;
1402
+ }
1403
+ .events-list.active {
1404
+ display: block;
1405
+ }
1406
+ .rpc-list.hidden {
1407
+ display: none;
1408
+ }
1409
+ .events-table {
1410
+ width: 100%;
1411
+ border-collapse: collapse;
1412
+ font-size: 0.85em;
1413
+ }
1414
+ .events-table th {
1415
+ text-align: left;
1416
+ color: var(--text-secondary);
1417
+ border-bottom: 1px solid var(--border-color);
1418
+ padding: 6px 8px;
1419
+ font-weight: 500;
1420
+ position: sticky;
1421
+ top: 0;
1422
+ background: var(--bg-primary);
1423
+ z-index: 1;
1424
+ }
1425
+ .events-table td {
1426
+ padding: 6px 8px;
1427
+ border-bottom: 1px solid var(--border-color);
1428
+ white-space: nowrap;
1429
+ }
1430
+ .event-row {
1431
+ cursor: pointer;
1432
+ }
1433
+ .event-row:hover {
1434
+ background: rgba(0, 212, 255, 0.1);
1435
+ }
1436
+ .event-row.selected {
1437
+ background: rgba(0, 212, 255, 0.2);
1438
+ }
1439
+
1440
+ /* Event kind badges */
1441
+ .badge-kind-request {
1442
+ background: rgba(0, 212, 255, 0.15);
1443
+ color: var(--accent-blue);
1444
+ border: 1px solid rgba(0, 212, 255, 0.3);
1445
+ }
1446
+ .badge-kind-response {
1447
+ background: rgba(63, 185, 80, 0.15);
1448
+ color: var(--accent-green);
1449
+ border: 1px solid rgba(63, 185, 80, 0.3);
1450
+ }
1451
+ .badge-kind-notification {
1452
+ background: rgba(210, 153, 34, 0.15);
1453
+ color: var(--accent-yellow);
1454
+ border: 1px solid rgba(210, 153, 34, 0.3);
1455
+ }
1456
+ .badge-kind-transport_event {
1457
+ background: var(--bg-tertiary);
1458
+ color: var(--text-secondary);
1459
+ border: 1px solid var(--border-color);
1460
+ }
1461
+
1462
+ /* Event direction arrows */
1463
+ .direction-arrow {
1464
+ font-size: 18px;
1465
+ font-weight: bold;
1466
+ line-height: 1;
1467
+ cursor: help;
1468
+ }
1469
+ .direction-arrow.outgoing {
1470
+ color: var(--accent-blue);
1471
+ }
1472
+ .direction-arrow.incoming {
1473
+ color: var(--accent-green);
1474
+ }
1475
+
1476
+ /* Events loading state */
1477
+ .events-loading {
1478
+ padding: 24px;
1479
+ text-align: center;
1480
+ color: var(--text-secondary);
1481
+ }
1482
+
1271
1483
  /* Analytics Panel (Phase 5.2) - Revised Layout */
1272
1484
 
1273
1485
  /* Header with KPI stats inline */
@@ -1549,6 +1761,8 @@ function getConnectorReportScript() {
1549
1761
 
1550
1762
  let currentSessionId = null;
1551
1763
  let currentRpcIdx = null;
1764
+ let currentEventIdx = null;
1765
+ let currentViewMode = 'rpc'; // 'rpc' or 'events'
1552
1766
 
1553
1767
  // Connector info toggle
1554
1768
  const connectorInfo = document.querySelector('.connector-info');
@@ -1590,6 +1804,8 @@ function getConnectorReportScript() {
1590
1804
  if (currentSessionId === sessionId) return;
1591
1805
  currentSessionId = sessionId;
1592
1806
  currentRpcIdx = null;
1807
+ currentEventIdx = null;
1808
+ currentViewMode = 'rpc'; // Reset to RPC view
1593
1809
 
1594
1810
  // Update session list selection
1595
1811
  document.querySelectorAll('.session-item').forEach(item => {
@@ -1641,19 +1857,29 @@ function getConnectorReportScript() {
1641
1857
  const requestRawHtml = rpc._requestRawHtml || '<span class="json-null">(no data)</span>';
1642
1858
  const responseRawHtml = rpc._responseRawHtml || '<span class="json-null">(no data)</span>';
1643
1859
 
1860
+ // Sensitive content warning badge (Phase 12.x-c)
1861
+ // Escape keys to prevent XSS via malicious key names
1862
+ const sensitiveKeys = (rpc._sensitiveKeys || []).map(function(k) { return escapeHtml(k); });
1863
+ const sensitiveTooltip = sensitiveKeys.length > 5
1864
+ ? 'Contains ' + sensitiveKeys.length + ' sensitive keys: ' + sensitiveKeys.slice(0, 5).join(', ') + '...'
1865
+ : 'Contains sensitive keys: ' + sensitiveKeys.join(', ');
1866
+ const sensitiveBadge = rpc._hasSensitive
1867
+ ? '<span class="sensitive-badge" title="' + escapeHtml(sensitiveTooltip) + '">⚠ Sensitive</span>'
1868
+ : '';
1869
+
1644
1870
  // Determine default target based on method (response-focused methods default to response)
1645
1871
  const defaultTarget = (rpc.method === 'tools/list' || rpc.method === 'initialize' || rpc.method.startsWith('resources/') || rpc.method.startsWith('prompts/')) ? 'response' : 'request';
1646
1872
 
1647
1873
  rightPane.innerHTML =
1648
1874
  '<div class="detail-section">' +
1649
- ' <h2>RPC Info</h2>' +
1875
+ ' <h2>RPC Info' + sensitiveBadge + '</h2>' +
1650
1876
  ' <div class="rpc-info-grid">' +
1651
1877
  ' <div class="rpc-info-item"><dt>RPC ID</dt><dd><span class="badge">' + escapeHtml(rpc.rpc_id) + '</span></dd></div>' +
1652
1878
  ' <div class="rpc-info-item"><dt>Method</dt><dd><span class="badge">' + escapeHtml(rpc.method) + '</span></dd></div>' +
1653
1879
  ' <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>' +
1654
1880
  ' <div class="rpc-info-item"><dt>Latency</dt><dd><span class="badge">' + latency + '</span></dd></div>' +
1655
- ' <div class="rpc-info-item"><dt>Req Size</dt><dd>' + formatBytes(rpc.request.size) + '</dd></div>' +
1656
- ' <div class="rpc-info-item"><dt>Res Size</dt><dd>' + formatBytes(rpc.response.size) + '</dd></div>' +
1881
+ ' <div class="rpc-info-item"><dt>Request</dt><dd>' + escapeHtml(rpc.request_ts) + '</dd></div>' +
1882
+ ' <div class="rpc-info-item"><dt>Response</dt><dd>' + escapeHtml(rpc.response_ts || '-') + '</dd></div>' +
1657
1883
  ' </div>' +
1658
1884
  '</div>' +
1659
1885
  '<div class="detail-section">' +
@@ -1715,16 +1941,46 @@ function getConnectorReportScript() {
1715
1941
  });
1716
1942
  });
1717
1943
 
1718
- // Keyboard navigation
1944
+ // Keyboard navigation (handles both RPC and Events views)
1719
1945
  document.addEventListener('keydown', (e) => {
1720
1946
  if (!currentSessionId) return;
1947
+ if (e.key !== 'ArrowDown' && e.key !== 'ArrowUp') return;
1721
1948
 
1722
- const report = sessionReports[currentSessionId];
1723
- if (!report) return;
1949
+ e.preventDefault();
1950
+ const sessionContent = document.querySelector('.session-content[data-session-id="' + currentSessionId + '"]');
1951
+ if (!sessionContent) return;
1724
1952
 
1725
- if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
1726
- e.preventDefault();
1953
+ // Check which view is active
1954
+ if (currentViewMode === 'events') {
1955
+ // Events navigation
1956
+ const eventRows = sessionContent.querySelectorAll('.event-row');
1957
+ if (eventRows.length === 0) return;
1958
+
1959
+ let newIdx = currentEventIdx;
1960
+ if (currentEventIdx === null) {
1961
+ newIdx = 0;
1962
+ } else if (e.key === 'ArrowDown' && currentEventIdx < eventRows.length - 1) {
1963
+ newIdx = currentEventIdx + 1;
1964
+ } else if (e.key === 'ArrowUp' && currentEventIdx > 0) {
1965
+ newIdx = currentEventIdx - 1;
1966
+ }
1967
+
1968
+ if (newIdx !== currentEventIdx) {
1969
+ currentEventIdx = newIdx;
1970
+ // Update selection visually
1971
+ eventRows.forEach((r, i) => r.classList.toggle('selected', i === newIdx));
1972
+ // Scroll into view
1973
+ eventRows[newIdx].scrollIntoView({ block: 'nearest' });
1974
+ // Trigger click to show detail (if has payload)
1975
+ eventRows[newIdx].click();
1976
+ }
1977
+ } else {
1978
+ // RPC navigation
1979
+ const report = sessionReports[currentSessionId];
1980
+ if (!report) return;
1727
1981
  const rpcs = report.rpcs;
1982
+ if (rpcs.length === 0) return;
1983
+
1728
1984
  if (currentRpcIdx === null && rpcs.length > 0) {
1729
1985
  showRpcDetail(currentSessionId, 0);
1730
1986
  return;
@@ -1735,11 +1991,8 @@ function getConnectorReportScript() {
1735
1991
  showRpcDetail(currentSessionId, currentRpcIdx - 1);
1736
1992
  }
1737
1993
  // Scroll selected row into view
1738
- const sessionContent = document.querySelector('.session-content[data-session-id="' + currentSessionId + '"]');
1739
- if (sessionContent) {
1740
- const row = sessionContent.querySelector('.rpc-row.selected');
1741
- if (row) row.scrollIntoView({ block: 'nearest' });
1742
- }
1994
+ const row = sessionContent.querySelector('.rpc-row.selected');
1995
+ if (row) row.scrollIntoView({ block: 'nearest' });
1743
1996
  }
1744
1997
  });
1745
1998
 
@@ -1806,6 +2059,225 @@ function getConnectorReportScript() {
1806
2059
  showSession(sessions[0].session_id);
1807
2060
  }
1808
2061
 
2062
+ // Events View toggle and data loading (Issue #59)
2063
+ (function() {
2064
+ // Cache for loaded events
2065
+ const eventsCache = {};
2066
+
2067
+ // Event kind display labels
2068
+ const kindLabels = {
2069
+ request: 'REQ',
2070
+ response: 'RES',
2071
+ notification: 'NOTIF',
2072
+ transport_event: 'TRANS'
2073
+ };
2074
+
2075
+ // Format time for events table
2076
+ function formatEventTime(ts) {
2077
+ try {
2078
+ const date = new Date(ts);
2079
+ return date.toISOString().split('T')[1].slice(0, 12);
2080
+ } catch {
2081
+ return ts;
2082
+ }
2083
+ }
2084
+
2085
+ // Render events table
2086
+ function renderEventsTable(events) {
2087
+ if (!events || events.length === 0) {
2088
+ return '<div class="events-loading">No events in this session</div>';
2089
+ }
2090
+
2091
+ const rows = events.map(function(event, idx) {
2092
+ const dirClass = event.direction === 'client_to_server' ? 'outgoing' : 'incoming';
2093
+ // Large arrows with tooltip: ⇨ (blue) = Client→Server, ⇦ (green) = Server→Client
2094
+ const dirArrow = event.direction === 'client_to_server' ? '\\u21E8' : '\\u21E6';
2095
+ const dirTooltip = event.direction === 'client_to_server'
2096
+ ? 'Client \\u2192 Server'
2097
+ : 'Server \\u2192 Client';
2098
+ const kindClass = 'badge-kind-' + event.kind;
2099
+ const kindLabel = kindLabels[event.kind] || event.kind;
2100
+ // Method/Summary fallback: method > summary > payload_type (e.g., "connected")
2101
+ const method = event.method || event.summary || event.payload_type || '';
2102
+ const timeStr = formatEventTime(event.ts);
2103
+ const hasPayload = event.has_payload ? '\\u2713' : '';
2104
+
2105
+ return '<tr class="event-row" data-event-idx="' + idx + '" data-event-id="' + escapeHtml(event.event_id) + '">' +
2106
+ '<td>' + timeStr + '</td>' +
2107
+ '<td><span class="direction-arrow ' + dirClass + '" title="' + dirTooltip + '">' + dirArrow + '</span></td>' +
2108
+ '<td><span class="badge ' + kindClass + '">' + kindLabel + '</span></td>' +
2109
+ '<td>' + escapeHtml(method) + '</td>' +
2110
+ '<td>' + hasPayload + '</td>' +
2111
+ '</tr>';
2112
+ }).join('');
2113
+
2114
+ return '<table class="events-table">' +
2115
+ '<thead><tr>' +
2116
+ '<th>Time</th>' +
2117
+ '<th>Dir</th>' +
2118
+ '<th>Kind</th>' +
2119
+ '<th>Method/Summary</th>' +
2120
+ '<th>Data</th>' +
2121
+ '</tr></thead>' +
2122
+ '<tbody>' + rows + '</tbody>' +
2123
+ '</table>';
2124
+ }
2125
+
2126
+ // Load events for a session
2127
+ function loadEvents(sessionId, eventsList) {
2128
+ if (eventsCache[sessionId]) {
2129
+ eventsList.innerHTML = renderEventsTable(eventsCache[sessionId]);
2130
+ // Attach click handlers even when using cached data
2131
+ attachEventRowHandlers(eventsList, sessionId, eventsCache[sessionId]);
2132
+ return;
2133
+ }
2134
+
2135
+ // Check if we're in offline mode (static HTML) or live server
2136
+ // Try to fetch from API, fallback to "no data" message
2137
+ eventsList.innerHTML = '<div class="events-loading">Loading events...</div>';
2138
+
2139
+ fetch('/api/sessions/' + encodeURIComponent(sessionId) + '/events')
2140
+ .then(function(res) {
2141
+ if (!res.ok) throw new Error('API not available');
2142
+ return res.json();
2143
+ })
2144
+ .then(function(data) {
2145
+ eventsCache[sessionId] = data.events;
2146
+ eventsList.innerHTML = renderEventsTable(data.events);
2147
+ // Attach click handlers for event rows
2148
+ attachEventRowHandlers(eventsList, sessionId, data.events);
2149
+ })
2150
+ .catch(function() {
2151
+ eventsList.innerHTML = '<div class="events-loading">Events data not available (API offline)</div>';
2152
+ });
2153
+ }
2154
+
2155
+ // Attach click handlers for event rows
2156
+ function attachEventRowHandlers(eventsList, sessionId, events) {
2157
+ eventsList.querySelectorAll('.event-row').forEach(function(row) {
2158
+ row.addEventListener('click', function() {
2159
+ const idx = parseInt(row.dataset.eventIdx);
2160
+ const event = events[idx];
2161
+ if (!event || !event.has_payload) return;
2162
+
2163
+ // Clear previous selection
2164
+ eventsList.querySelectorAll('.event-row').forEach(function(r) {
2165
+ r.classList.remove('selected');
2166
+ });
2167
+ row.classList.add('selected');
2168
+
2169
+ // Show event detail in right pane
2170
+ showEventDetail(sessionId, event);
2171
+ });
2172
+ });
2173
+ }
2174
+
2175
+ // Show event detail in right pane (2-column layout like RPC Inspector)
2176
+ function showEventDetail(sessionId, event) {
2177
+ const sessionContent = document.querySelector('.session-content[data-session-id="' + sessionId + '"]');
2178
+ if (!sessionContent) return;
2179
+
2180
+ const rightPane = sessionContent.querySelector('.right-pane');
2181
+ if (!rightPane) return;
2182
+
2183
+ // Fetch full event detail
2184
+ fetch('/api/events/' + encodeURIComponent(event.event_id))
2185
+ .then(function(res) {
2186
+ if (!res.ok) throw new Error('Event not found');
2187
+ return res.json();
2188
+ })
2189
+ .then(function(data) {
2190
+ const evt = data.event;
2191
+ const kindClass = 'badge-kind-' + evt.kind;
2192
+ const dirClass = evt.direction === 'client_to_server' ? 'outgoing' : 'incoming';
2193
+ // Large arrows with tooltip: ⇨ (blue) = Client→Server, ⇦ (green) = Server→Client
2194
+ const dirArrow = evt.direction === 'client_to_server' ? '\\u21E8' : '\\u21E6';
2195
+ const dirTooltip = evt.direction === 'client_to_server'
2196
+ ? 'Client \\u2192 Server'
2197
+ : 'Server \\u2192 Client';
2198
+ const method = evt.method || evt.summary || '(unknown)';
2199
+ const rawJson = evt.raw_json ? JSON.parse(evt.raw_json) : null;
2200
+ const formattedJson = rawJson ? JSON.stringify(rawJson, null, 2) : '(no data)';
2201
+
2202
+ // Build summary section
2203
+ var summaryHtml = '<div class="summary-row summary-header">Event Info</div>';
2204
+ summaryHtml += '<div class="summary-row summary-property"><span class="summary-prop-name">Kind</span><span class="summary-prop-value"><span class="badge ' + kindClass + '">' + evt.kind + '</span></span></div>';
2205
+ summaryHtml += '<div class="summary-row summary-property"><span class="summary-prop-name">Direction</span><span class="summary-prop-value"><span class="direction-arrow ' + dirClass + '" title="' + dirTooltip + '">' + dirArrow + '</span> ' + dirTooltip + '</span></div>';
2206
+ summaryHtml += '<div class="summary-row summary-property"><span class="summary-prop-name">Method</span><span class="summary-prop-value">' + escapeHtml(method) + '</span></div>';
2207
+ summaryHtml += '<div class="summary-row summary-property"><span class="summary-prop-name">Timestamp</span><span class="summary-prop-value">' + escapeHtml(evt.ts) + '</span></div>';
2208
+ if (evt.seq !== null) {
2209
+ summaryHtml += '<div class="summary-row summary-property"><span class="summary-prop-name">Sequence</span><span class="summary-prop-value">' + evt.seq + '</span></div>';
2210
+ }
2211
+
2212
+ // If JSON has recognizable structure, add more summary
2213
+ if (rawJson) {
2214
+ if (rawJson.method) {
2215
+ summaryHtml += '<div class="summary-row summary-header" style="margin-top: 12px;">JSON-RPC</div>';
2216
+ summaryHtml += '<div class="summary-row summary-property"><span class="summary-prop-name">method</span><span class="summary-prop-value">' + escapeHtml(rawJson.method) + '</span></div>';
2217
+ }
2218
+ if (rawJson.id !== undefined) {
2219
+ summaryHtml += '<div class="summary-row summary-property"><span class="summary-prop-name">id</span><span class="summary-prop-value">' + escapeHtml(String(rawJson.id)) + '</span></div>';
2220
+ }
2221
+ if (rawJson.error) {
2222
+ summaryHtml += '<div class="summary-row summary-property"><span class="summary-prop-name">error</span><span class="summary-prop-value" style="color: var(--accent-red);">' + escapeHtml(JSON.stringify(rawJson.error)) + '</span></div>';
2223
+ }
2224
+ }
2225
+
2226
+ // 2-column layout: Summary (left) + Raw JSON (right)
2227
+ rightPane.innerHTML =
2228
+ '<div class="rpc-inspector">' +
2229
+ '<div class="rpc-inspector-summary" style="flex: 0 0 280px; max-width: 320px;">' +
2230
+ '<div class="summary-container">' + summaryHtml + '</div>' +
2231
+ '</div>' +
2232
+ '<div class="rpc-inspector-raw" style="flex: 1; min-width: 0;">' +
2233
+ '<div class="rpc-raw-header">' +
2234
+ '<span class="rpc-raw-title">Payload</span>' +
2235
+ '</div>' +
2236
+ '<div class="rpc-raw-json"><pre><code>' + escapeHtml(formattedJson) + '</code></pre></div>' +
2237
+ '</div>' +
2238
+ '</div>';
2239
+ })
2240
+ .catch(function() {
2241
+ rightPane.innerHTML = '<div class="detail-placeholder">Failed to load event detail</div>';
2242
+ });
2243
+ }
2244
+
2245
+ // Handle view toggle clicks
2246
+ document.querySelectorAll('.view-toggle').forEach(function(toggle) {
2247
+ const sessionContent = toggle.closest('.session-content');
2248
+ if (!sessionContent) return;
2249
+
2250
+ const sessionId = sessionContent.dataset.sessionId;
2251
+ const rpcList = sessionContent.querySelector('.rpc-list');
2252
+ const eventsList = sessionContent.querySelector('.events-list');
2253
+ const buttons = toggle.querySelectorAll('.view-toggle-btn');
2254
+
2255
+ buttons.forEach(function(btn) {
2256
+ btn.addEventListener('click', function() {
2257
+ const view = btn.dataset.view;
2258
+
2259
+ // Update current view mode
2260
+ currentViewMode = view;
2261
+
2262
+ // Update button states
2263
+ buttons.forEach(function(b) {
2264
+ b.classList.toggle('active', b.dataset.view === view);
2265
+ });
2266
+
2267
+ // Toggle lists
2268
+ if (view === 'events') {
2269
+ rpcList.classList.add('hidden');
2270
+ eventsList.classList.add('active');
2271
+ loadEvents(sessionId, eventsList);
2272
+ } else {
2273
+ rpcList.classList.remove('hidden');
2274
+ eventsList.classList.remove('active');
2275
+ }
2276
+ });
2277
+ });
2278
+ });
2279
+ })();
2280
+
1809
2281
  // RPC Inspector script
1810
2282
  ${getRpcInspectorScript()}
1811
2283
  `;
@@ -2187,27 +2659,46 @@ function renderAnalyticsPanel(analytics) {
2187
2659
  </div>`;
2188
2660
  }
2189
2661
  /**
2190
- * Render a session item for the sessions pane
2662
+ * Render a session item for the sessions pane (compact grid view)
2191
2663
  */
2192
2664
  function renderConnectorSessionItem(session) {
2193
- const timeStr = formatTimestamp(session.started_at).split(' ')[1]?.slice(0, 8) || '-';
2194
- const dateStr = formatTimestamp(session.started_at).split(' ')[0] || '-';
2195
- const statusBadge = session.error_count > 0
2196
- ? '<span class="badge status-ERR">ERR</span>'
2197
- : '<span class="badge status-OK">OK</span>';
2665
+ // Format timestamp compactly: MM/DD HH:MM
2666
+ const timestamp = formatCompactTimestamp(session.started_at);
2667
+ const latencyStr = session.total_latency_ms !== null
2668
+ ? `${session.total_latency_ms}ms`
2669
+ : '-';
2198
2670
  return `
2199
- <div class="session-item" data-session-id="${escapeHtml(session.session_id)}">
2200
- <div class="session-item-header">
2201
- <span class="session-id">[${escapeHtml(session.short_id)}]</span>
2202
- ${statusBadge}
2203
- </div>
2204
- <div class="session-meta">${dateStr} ${timeStr}</div>
2205
- <div class="session-stats">
2206
- <span>${session.rpc_count} RPCs</span>
2207
- ${session.error_count > 0 ? `<span style="color: var(--status-err)">${session.error_count} errors</span>` : ''}
2208
- </div>
2671
+ <div class="session-item"
2672
+ data-session-id="${escapeHtml(session.session_id)}"
2673
+ title="Session: ${session.session_id}&#10;Started: ${session.started_at}&#10;RPCs: ${session.rpc_count}&#10;Events: ${session.event_count}&#10;Errors: ${session.error_count}">
2674
+ <span class="session-id">[${escapeHtml(session.short_id)}]</span>
2675
+ <span class="session-timestamp">${timestamp}</span>
2676
+ <span class="session-counts"><span>R:${session.rpc_count}</span><span>E:${session.event_count}</span></span>
2677
+ <span class="session-latency">${latencyStr}</span>
2678
+ <span class="session-extra"></span>
2209
2679
  </div>`;
2210
2680
  }
2681
+ /**
2682
+ * Format timestamp compactly for grid display (UTC)
2683
+ * Returns format: HH:MM:SS.mmm (time with milliseconds)
2684
+ * @public - exported for testing
2685
+ */
2686
+ export function formatCompactTimestamp(isoStr) {
2687
+ try {
2688
+ const date = new Date(isoStr);
2689
+ if (isNaN(date.getTime())) {
2690
+ return '-';
2691
+ }
2692
+ const hours = String(date.getUTCHours()).padStart(2, '0');
2693
+ const minutes = String(date.getUTCMinutes()).padStart(2, '0');
2694
+ const seconds = String(date.getUTCSeconds()).padStart(2, '0');
2695
+ const millis = String(date.getUTCMilliseconds()).padStart(3, '0');
2696
+ return `${hours}:${minutes}:${seconds}.${millis}`;
2697
+ }
2698
+ catch {
2699
+ return '-';
2700
+ }
2701
+ }
2211
2702
  /**
2212
2703
  * Render session detail content (reuses session HTML layout)
2213
2704
  */
@@ -2254,6 +2745,14 @@ function renderSessionDetailContent(sessionId, report) {
2254
2745
  <dd><span class="badge">${totalLatencyDisplay}</span></dd>
2255
2746
  </dl>
2256
2747
  </div>
2748
+ <div class="view-toggle">
2749
+ <button class="view-toggle-btn active" data-view="rpc">
2750
+ RPCs<span class="view-toggle-count">(${session.rpc_count})</span>
2751
+ </button>
2752
+ <button class="view-toggle-btn" data-view="events">
2753
+ Events<span class="view-toggle-count">(${session.event_count})</span>
2754
+ </button>
2755
+ </div>
2257
2756
  <div class="rpc-list">
2258
2757
  <table class="rpc-table">
2259
2758
  <thead>
@@ -2270,6 +2769,9 @@ ${rpcRows}
2270
2769
  </tbody>
2271
2770
  </table>
2272
2771
  </div>
2772
+ <div class="events-list">
2773
+ <div class="events-loading">Loading events...</div>
2774
+ </div>
2273
2775
  </div>
2274
2776
  <div class="resize-handle"></div>
2275
2777
  <div class="right-pane">
@@ -2345,12 +2847,18 @@ export function generateConnectorHtml(report) {
2345
2847
  rpcs: sessionReport.rpcs.map((rpc) => {
2346
2848
  const requestSummaryRows = renderRequestSummary(rpc.method, rpc.request.json);
2347
2849
  const responseSummaryRows = renderResponseSummary(rpc.method, rpc.response.json);
2850
+ // Detect sensitive keys in request/response (Phase 12.x-c)
2851
+ const reqSensitiveKeys = detectSensitiveKeys(rpc.request.json);
2852
+ const resSensitiveKeys = detectSensitiveKeys(rpc.response.json);
2853
+ const hasSensitive = reqSensitiveKeys.length > 0 || resSensitiveKeys.length > 0;
2348
2854
  return {
2349
2855
  ...rpc,
2350
2856
  _requestSummaryHtml: renderSummaryRowsHtml(requestSummaryRows),
2351
2857
  _responseSummaryHtml: renderSummaryRowsHtml(responseSummaryRows),
2352
2858
  _requestRawHtml: renderJsonWithPaths(rpc.request.json, '#'),
2353
2859
  _responseRawHtml: renderJsonWithPaths(rpc.response.json, '#'),
2860
+ _hasSensitive: hasSensitive,
2861
+ _sensitiveKeys: [...reqSensitiveKeys, ...resSensitiveKeys],
2354
2862
  };
2355
2863
  }),
2356
2864
  };
@@ -2422,6 +2930,12 @@ export function generateConnectorHtml(report) {
2422
2930
  <h2>Sessions</h2>
2423
2931
  <span class="pagination-info">${paginationInfo}</span>
2424
2932
  </div>
2933
+ <div class="sessions-header-row">
2934
+ <span>ID</span>
2935
+ <span>Time (UTC)</span>
2936
+ <span style="text-align:right">Latency</span>
2937
+ <span></span>
2938
+ </div>
2425
2939
  <div class="sessions-list">
2426
2940
  ${sessionItems}
2427
2941
  </div>