proofscan 0.10.25 → 0.10.26

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.
@@ -5,7 +5,7 @@
5
5
  * Dark theme with neon blue accent badges.
6
6
  */
7
7
  import { formatBytes } from '../eventline/types.js';
8
- import { getStatusSymbol } from './types.js';
8
+ import { getStatusSymbol, SHORT_ID_LENGTH } from './types.js';
9
9
  /**
10
10
  * Escape HTML special characters to prevent XSS
11
11
  */
@@ -753,4 +753,788 @@ ${rpcRows}
753
753
  </body>
754
754
  </html>`;
755
755
  }
756
+ // ============================================================================
757
+ // Connector HTML Report (Phase 5.1)
758
+ // ============================================================================
759
+ /**
760
+ * Get CSS styles for Connector HTML (3-hierarchy: Connector -> Sessions -> RPCs)
761
+ */
762
+ function getConnectorReportStyles() {
763
+ return `
764
+ :root {
765
+ --bg-primary: #0d1117;
766
+ --bg-secondary: #161b22;
767
+ --bg-tertiary: #21262d;
768
+ --text-primary: #e6edf3;
769
+ --text-secondary: #8b949e;
770
+ --accent-blue: #00d4ff;
771
+ --status-ok: #3fb950;
772
+ --status-err: #f85149;
773
+ --status-pending: #d29922;
774
+ --border-color: #30363d;
775
+ --link-color: #58a6ff;
776
+ --sessions-pane-width: 280px;
777
+ --left-pane-width: 420px;
778
+ }
779
+ * { box-sizing: border-box; }
780
+ html, body {
781
+ height: 100%;
782
+ margin: 0;
783
+ padding: 0;
784
+ overflow: hidden;
785
+ }
786
+ body {
787
+ background: var(--bg-primary);
788
+ color: var(--text-primary);
789
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
790
+ line-height: 1.5;
791
+ display: flex;
792
+ flex-direction: column;
793
+ }
794
+ header {
795
+ padding: 12px 20px;
796
+ border-bottom: 1px solid var(--border-color);
797
+ flex-shrink: 0;
798
+ }
799
+ h1 {
800
+ margin: 0 0 4px 0;
801
+ font-size: 1.3em;
802
+ font-weight: 600;
803
+ }
804
+ h2 {
805
+ margin: 0 0 8px 0;
806
+ font-size: 1em;
807
+ font-weight: 600;
808
+ color: var(--text-primary);
809
+ border-bottom: 1px solid var(--border-color);
810
+ padding-bottom: 6px;
811
+ }
812
+ h3 {
813
+ margin: 12px 0 6px 0;
814
+ font-size: 0.9em;
815
+ font-weight: 600;
816
+ color: var(--text-secondary);
817
+ }
818
+ h3:first-child { margin-top: 0; }
819
+ a { color: var(--link-color); text-decoration: none; }
820
+ a:hover { text-decoration: underline; }
821
+ .meta { color: var(--text-secondary); margin: 0; font-size: 0.8em; }
822
+ .badge {
823
+ display: inline-block;
824
+ padding: 1px 6px;
825
+ border: 1px solid var(--accent-blue);
826
+ border-radius: 4px;
827
+ color: var(--accent-blue);
828
+ font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
829
+ font-size: 0.8em;
830
+ background: transparent;
831
+ }
832
+ .badge.status-OK { border-color: var(--status-ok); color: var(--status-ok); }
833
+ .badge.status-ERR { border-color: var(--status-err); color: var(--status-err); }
834
+ .badge.status-PENDING { border-color: var(--status-pending); color: var(--status-pending); }
835
+ .badge.cap-enabled { border-color: var(--accent-blue); color: var(--accent-blue); background: rgba(0, 212, 255, 0.1); }
836
+
837
+ /* Connector info section (collapsible) */
838
+ .connector-info {
839
+ background: var(--bg-secondary);
840
+ padding: 12px 20px;
841
+ border-bottom: 1px solid var(--border-color);
842
+ flex-shrink: 0;
843
+ }
844
+ .connector-info-toggle {
845
+ display: flex;
846
+ align-items: center;
847
+ justify-content: space-between;
848
+ cursor: pointer;
849
+ user-select: none;
850
+ }
851
+ .connector-info-toggle h2 {
852
+ margin: 0;
853
+ border: none;
854
+ padding: 0;
855
+ }
856
+ .connector-info-toggle .toggle-icon {
857
+ color: var(--text-secondary);
858
+ font-size: 0.85em;
859
+ transition: transform 0.2s;
860
+ }
861
+ .connector-info-content {
862
+ display: none;
863
+ margin-top: 12px;
864
+ }
865
+ .connector-info.expanded .connector-info-content {
866
+ display: block;
867
+ }
868
+ .connector-info.expanded .toggle-icon {
869
+ transform: rotate(180deg);
870
+ }
871
+ .connector-info dl {
872
+ display: grid;
873
+ grid-template-columns: auto 1fr;
874
+ gap: 4px 16px;
875
+ margin: 0;
876
+ font-size: 0.85em;
877
+ }
878
+ .connector-info dt { color: var(--text-secondary); }
879
+ .connector-info dd { margin: 0; }
880
+ .capabilities {
881
+ display: flex;
882
+ gap: 6px;
883
+ flex-wrap: wrap;
884
+ }
885
+
886
+ /* Main 3-pane container */
887
+ .main-container {
888
+ display: flex;
889
+ flex: 1;
890
+ overflow: hidden;
891
+ }
892
+
893
+ /* Sessions pane (leftmost) */
894
+ .sessions-pane {
895
+ width: var(--sessions-pane-width);
896
+ flex-shrink: 0;
897
+ border-right: 1px solid var(--border-color);
898
+ display: flex;
899
+ flex-direction: column;
900
+ overflow: hidden;
901
+ background: var(--bg-secondary);
902
+ }
903
+ .sessions-header {
904
+ padding: 10px 12px;
905
+ border-bottom: 1px solid var(--border-color);
906
+ background: var(--bg-tertiary);
907
+ flex-shrink: 0;
908
+ }
909
+ .sessions-header h2 {
910
+ margin: 0;
911
+ border: none;
912
+ padding: 0;
913
+ font-size: 0.9em;
914
+ }
915
+ .sessions-list {
916
+ flex: 1;
917
+ overflow-y: auto;
918
+ padding: 8px;
919
+ }
920
+ .session-item {
921
+ padding: 10px 12px;
922
+ border-radius: 6px;
923
+ cursor: pointer;
924
+ margin-bottom: 6px;
925
+ border: 1px solid transparent;
926
+ background: var(--bg-primary);
927
+ }
928
+ .session-item:hover {
929
+ background: rgba(0, 212, 255, 0.1);
930
+ border-color: rgba(0, 212, 255, 0.3);
931
+ }
932
+ .session-item.selected {
933
+ border-color: var(--accent-blue);
934
+ background: rgba(0, 212, 255, 0.15);
935
+ }
936
+ .session-item-header {
937
+ display: flex;
938
+ align-items: center;
939
+ justify-content: space-between;
940
+ margin-bottom: 4px;
941
+ }
942
+ .session-item .session-id {
943
+ font-family: 'SFMono-Regular', Consolas, monospace;
944
+ color: var(--accent-blue);
945
+ font-size: 0.85em;
946
+ }
947
+ .session-item .session-meta {
948
+ font-size: 0.75em;
949
+ color: var(--text-secondary);
950
+ }
951
+ .session-item .session-stats {
952
+ display: flex;
953
+ gap: 8px;
954
+ font-size: 0.75em;
955
+ color: var(--text-secondary);
956
+ }
957
+
958
+ /* Session detail pane (middle) */
959
+ .session-detail-pane {
960
+ flex: 1;
961
+ display: flex;
962
+ flex-direction: column;
963
+ overflow: hidden;
964
+ min-width: 0;
965
+ }
966
+ .session-detail-empty {
967
+ flex: 1;
968
+ display: flex;
969
+ align-items: center;
970
+ justify-content: center;
971
+ color: var(--text-secondary);
972
+ }
973
+
974
+ /* Re-use session HTML styles for the detail view */
975
+ .session-content {
976
+ display: none;
977
+ flex-direction: column;
978
+ height: 100%;
979
+ overflow: hidden;
980
+ }
981
+ .session-content.active {
982
+ display: flex;
983
+ }
984
+ .inner-container {
985
+ display: flex;
986
+ flex: 1;
987
+ overflow: hidden;
988
+ }
989
+ .left-pane {
990
+ width: var(--left-pane-width);
991
+ min-width: 300px;
992
+ max-width: 600px;
993
+ border-right: 1px solid var(--border-color);
994
+ display: flex;
995
+ flex-direction: column;
996
+ overflow: hidden;
997
+ }
998
+ .right-pane {
999
+ flex: 1;
1000
+ overflow-y: auto;
1001
+ padding: 16px;
1002
+ }
1003
+ .session-info {
1004
+ background: var(--bg-secondary);
1005
+ padding: 12px;
1006
+ border-bottom: 1px solid var(--border-color);
1007
+ flex-shrink: 0;
1008
+ }
1009
+ .session-info dl {
1010
+ display: grid;
1011
+ grid-template-columns: auto 1fr;
1012
+ gap: 4px 12px;
1013
+ margin: 0;
1014
+ font-size: 0.85em;
1015
+ }
1016
+ .session-info dt { color: var(--text-secondary); }
1017
+ .session-info dd { margin: 0; }
1018
+ .rpc-list {
1019
+ flex: 1;
1020
+ overflow-y: auto;
1021
+ }
1022
+ .rpc-table {
1023
+ width: 100%;
1024
+ border-collapse: collapse;
1025
+ font-size: 0.85em;
1026
+ }
1027
+ .rpc-table th {
1028
+ text-align: left;
1029
+ color: var(--text-secondary);
1030
+ border-bottom: 1px solid var(--border-color);
1031
+ padding: 6px 8px;
1032
+ font-weight: 500;
1033
+ position: sticky;
1034
+ top: 0;
1035
+ background: var(--bg-primary);
1036
+ z-index: 1;
1037
+ }
1038
+ .rpc-table td {
1039
+ padding: 6px 8px;
1040
+ border-bottom: 1px solid var(--border-color);
1041
+ white-space: nowrap;
1042
+ }
1043
+ .rpc-row {
1044
+ cursor: pointer;
1045
+ }
1046
+ .rpc-row:hover {
1047
+ background: rgba(0, 212, 255, 0.1);
1048
+ }
1049
+ .rpc-row.selected {
1050
+ background: rgba(0, 212, 255, 0.2);
1051
+ }
1052
+ .detail-placeholder {
1053
+ color: var(--text-secondary);
1054
+ text-align: center;
1055
+ padding: 40px;
1056
+ }
1057
+ .detail-section {
1058
+ background: var(--bg-secondary);
1059
+ border-radius: 8px;
1060
+ padding: 16px;
1061
+ margin-bottom: 16px;
1062
+ }
1063
+ pre {
1064
+ background: var(--bg-primary);
1065
+ border: 1px solid var(--border-color);
1066
+ border-radius: 6px;
1067
+ padding: 12px;
1068
+ overflow-x: auto;
1069
+ margin: 8px 0;
1070
+ font-size: 0.85em;
1071
+ max-height: 400px;
1072
+ overflow-y: auto;
1073
+ }
1074
+ code {
1075
+ font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
1076
+ color: var(--text-primary);
1077
+ }
1078
+ .copy-btn {
1079
+ background: var(--bg-secondary);
1080
+ border: 1px solid var(--border-color);
1081
+ color: var(--text-secondary);
1082
+ padding: 3px 6px;
1083
+ border-radius: 4px;
1084
+ cursor: pointer;
1085
+ font-size: 0.75em;
1086
+ margin-left: 8px;
1087
+ }
1088
+ .copy-btn:hover {
1089
+ border-color: var(--accent-blue);
1090
+ color: var(--accent-blue);
1091
+ }
1092
+ .truncated-note {
1093
+ color: var(--status-pending);
1094
+ font-size: 0.8em;
1095
+ margin: 4px 0;
1096
+ }
1097
+ .spill-link {
1098
+ color: var(--link-color);
1099
+ font-size: 0.8em;
1100
+ }
1101
+ .resize-handle {
1102
+ width: 4px;
1103
+ background: var(--border-color);
1104
+ cursor: col-resize;
1105
+ transition: background 0.2s;
1106
+ }
1107
+ .resize-handle:hover {
1108
+ background: var(--accent-blue);
1109
+ }
1110
+ `;
1111
+ }
1112
+ /**
1113
+ * Get JavaScript for Connector HTML (3-hierarchy navigation)
1114
+ */
1115
+ function getConnectorReportScript() {
1116
+ return `
1117
+ // Report data
1118
+ const reportData = JSON.parse(document.getElementById('report-data').textContent);
1119
+ const sessions = reportData.sessions;
1120
+ const sessionReports = reportData.session_reports;
1121
+
1122
+ let currentSessionId = null;
1123
+ let currentRpcIdx = null;
1124
+
1125
+ // Connector info toggle
1126
+ const connectorInfo = document.querySelector('.connector-info');
1127
+ const connectorToggle = document.querySelector('.connector-info-toggle');
1128
+ if (connectorToggle) {
1129
+ connectorToggle.addEventListener('click', () => {
1130
+ connectorInfo.classList.toggle('expanded');
1131
+ });
1132
+ }
1133
+
1134
+ // Format JSON for display
1135
+ function formatJson(data) {
1136
+ if (data === null || data === undefined) return '(no data)';
1137
+ try {
1138
+ return JSON.stringify(data, null, 2);
1139
+ } catch {
1140
+ return String(data);
1141
+ }
1142
+ }
1143
+
1144
+ // Escape HTML
1145
+ function escapeHtml(text) {
1146
+ const div = document.createElement('div');
1147
+ div.textContent = text;
1148
+ return div.innerHTML;
1149
+ }
1150
+
1151
+ // Format bytes
1152
+ function formatBytes(bytes) {
1153
+ if (bytes === 0) return '0 B';
1154
+ const k = 1024;
1155
+ const sizes = ['B', 'KB', 'MB', 'GB'];
1156
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
1157
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
1158
+ }
1159
+
1160
+ // Show session detail
1161
+ function showSession(sessionId) {
1162
+ if (currentSessionId === sessionId) return;
1163
+ currentSessionId = sessionId;
1164
+ currentRpcIdx = null;
1165
+
1166
+ // Update session list selection
1167
+ document.querySelectorAll('.session-item').forEach(item => {
1168
+ item.classList.toggle('selected', item.dataset.sessionId === sessionId);
1169
+ });
1170
+
1171
+ // Hide all session contents, show selected
1172
+ document.querySelectorAll('.session-content').forEach(content => {
1173
+ content.classList.toggle('active', content.dataset.sessionId === sessionId);
1174
+ });
1175
+
1176
+ // Select first RPC in the newly shown session
1177
+ const sessionContent = document.querySelector('.session-content[data-session-id="' + sessionId + '"]');
1178
+ if (sessionContent) {
1179
+ const firstRpcRow = sessionContent.querySelector('.rpc-row');
1180
+ if (firstRpcRow) {
1181
+ const idx = parseInt(firstRpcRow.dataset.rpcIdx);
1182
+ showRpcDetail(sessionId, idx);
1183
+ }
1184
+ }
1185
+ }
1186
+
1187
+ // Show RPC detail in right pane
1188
+ function showRpcDetail(sessionId, idx) {
1189
+ const report = sessionReports[sessionId];
1190
+ if (!report || idx < 0 || idx >= report.rpcs.length) return;
1191
+
1192
+ const rpc = report.rpcs[idx];
1193
+ const sessionContent = document.querySelector('.session-content[data-session-id="' + sessionId + '"]');
1194
+ if (!sessionContent) return;
1195
+
1196
+ const rightPane = sessionContent.querySelector('.right-pane');
1197
+ if (!rightPane) return;
1198
+
1199
+ // Update RPC row selection
1200
+ sessionContent.querySelectorAll('.rpc-row').forEach((r, i) => {
1201
+ r.classList.toggle('selected', i === idx);
1202
+ });
1203
+ currentRpcIdx = idx;
1204
+
1205
+ const statusClass = 'status-' + rpc.status;
1206
+ const statusSymbol = rpc.status === 'OK' ? '\\u2713' : rpc.status === 'ERR' ? '\\u2717' : '?';
1207
+ const latency = rpc.latency_ms !== null ? rpc.latency_ms + 'ms' : '(pending)';
1208
+
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
+ }
1223
+
1224
+ return notes + '<pre id="' + elementId + '"><code>' + content + '</code></pre>';
1225
+ }
1226
+
1227
+ rightPane.innerHTML =
1228
+ '<div class="detail-section">' +
1229
+ ' <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) +
1242
+ '</div>' +
1243
+ '<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) +
1246
+ '</div>';
1247
+ }
1248
+
1249
+ // Copy to clipboard
1250
+ async function copyToClipboard(elementId, btn) {
1251
+ const target = document.getElementById(elementId);
1252
+ if (target) {
1253
+ try {
1254
+ await navigator.clipboard.writeText(target.textContent || '');
1255
+ const originalText = btn.textContent;
1256
+ btn.textContent = 'Copied!';
1257
+ setTimeout(() => { btn.textContent = originalText; }, 1500);
1258
+ } catch (err) {
1259
+ console.error('Copy failed:', err);
1260
+ }
1261
+ }
1262
+ }
1263
+
1264
+ // Session item click handlers
1265
+ document.querySelectorAll('.session-item').forEach(item => {
1266
+ item.addEventListener('click', () => {
1267
+ showSession(item.dataset.sessionId);
1268
+ });
1269
+ });
1270
+
1271
+ // RPC row click handlers (delegated)
1272
+ document.querySelectorAll('.session-content').forEach(content => {
1273
+ content.addEventListener('click', (e) => {
1274
+ const row = e.target.closest('.rpc-row');
1275
+ if (row) {
1276
+ const idx = parseInt(row.dataset.rpcIdx);
1277
+ showRpcDetail(content.dataset.sessionId, idx);
1278
+ }
1279
+ });
1280
+ });
1281
+
1282
+ // Keyboard navigation
1283
+ document.addEventListener('keydown', (e) => {
1284
+ if (!currentSessionId) return;
1285
+
1286
+ const report = sessionReports[currentSessionId];
1287
+ if (!report) return;
1288
+
1289
+ if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
1290
+ e.preventDefault();
1291
+ const rpcs = report.rpcs;
1292
+ if (currentRpcIdx === null && rpcs.length > 0) {
1293
+ showRpcDetail(currentSessionId, 0);
1294
+ return;
1295
+ }
1296
+ if (e.key === 'ArrowDown' && currentRpcIdx < rpcs.length - 1) {
1297
+ showRpcDetail(currentSessionId, currentRpcIdx + 1);
1298
+ } else if (e.key === 'ArrowUp' && currentRpcIdx > 0) {
1299
+ showRpcDetail(currentSessionId, currentRpcIdx - 1);
1300
+ }
1301
+ // Scroll selected row into view
1302
+ const sessionContent = document.querySelector('.session-content[data-session-id="' + currentSessionId + '"]');
1303
+ if (sessionContent) {
1304
+ const row = sessionContent.querySelector('.rpc-row.selected');
1305
+ if (row) row.scrollIntoView({ block: 'nearest' });
1306
+ }
1307
+ }
1308
+ });
1309
+
1310
+ // Resize handle for inner left pane
1311
+ document.querySelectorAll('.session-content').forEach(content => {
1312
+ const resizeHandle = content.querySelector('.resize-handle');
1313
+ const leftPane = content.querySelector('.left-pane');
1314
+ if (resizeHandle && leftPane) {
1315
+ let startX, startWidth;
1316
+
1317
+ resizeHandle.addEventListener('mousedown', (e) => {
1318
+ startX = e.clientX;
1319
+ startWidth = leftPane.offsetWidth;
1320
+ document.addEventListener('mousemove', onMouseMove);
1321
+ document.addEventListener('mouseup', onMouseUp);
1322
+ e.preventDefault();
1323
+ });
1324
+
1325
+ function onMouseMove(e) {
1326
+ const diff = e.clientX - startX;
1327
+ const newWidth = Math.max(300, Math.min(600, startWidth + diff));
1328
+ leftPane.style.width = newWidth + 'px';
1329
+ }
1330
+
1331
+ function onMouseUp() {
1332
+ document.removeEventListener('mousemove', onMouseMove);
1333
+ document.removeEventListener('mouseup', onMouseUp);
1334
+ }
1335
+ }
1336
+ });
1337
+
1338
+ // Select first session by default
1339
+ if (sessions.length > 0) {
1340
+ showSession(sessions[0].session_id);
1341
+ }
1342
+ `;
1343
+ }
1344
+ /**
1345
+ * Render a session item for the sessions pane
1346
+ */
1347
+ function renderConnectorSessionItem(session) {
1348
+ const timeStr = formatTimestamp(session.started_at).split(' ')[1]?.slice(0, 8) || '-';
1349
+ const dateStr = formatTimestamp(session.started_at).split(' ')[0] || '-';
1350
+ const statusBadge = session.error_count > 0
1351
+ ? '<span class="badge status-ERR">ERR</span>'
1352
+ : '<span class="badge status-OK">OK</span>';
1353
+ return `
1354
+ <div class="session-item" data-session-id="${escapeHtml(session.session_id)}">
1355
+ <div class="session-item-header">
1356
+ <span class="session-id">[${escapeHtml(session.short_id)}]</span>
1357
+ ${statusBadge}
1358
+ </div>
1359
+ <div class="session-meta">${dateStr} ${timeStr}</div>
1360
+ <div class="session-stats">
1361
+ <span>${session.rpc_count} RPCs</span>
1362
+ ${session.error_count > 0 ? `<span style="color: var(--status-err)">${session.error_count} errors</span>` : ''}
1363
+ </div>
1364
+ </div>`;
1365
+ }
1366
+ /**
1367
+ * Render session detail content (reuses session HTML layout)
1368
+ */
1369
+ function renderSessionDetailContent(sessionId, report) {
1370
+ const { session, rpcs } = report;
1371
+ const totalLatencyDisplay = session.total_latency_ms !== null
1372
+ ? `${session.total_latency_ms}ms`
1373
+ : '-';
1374
+ const rpcRows = rpcs.map((rpc, idx) => {
1375
+ const statusClass = `status-${rpc.status}`;
1376
+ const statusSymbol = getStatusSymbol(rpc.status);
1377
+ const rpcIdShort = rpc.rpc_id.slice(0, SHORT_ID_LENGTH);
1378
+ const timeShort = formatTimestamp(rpc.request_ts).split(' ')[1]?.slice(0, 12) || '-';
1379
+ const latency = rpc.latency_ms !== null ? `${rpc.latency_ms}ms` : '-';
1380
+ return `
1381
+ <tr class="rpc-row" data-rpc-idx="${idx}">
1382
+ <td>${timeShort}</td>
1383
+ <td><span class="badge ${statusClass}">${statusSymbol}</span></td>
1384
+ <td><span class="badge">${escapeHtml(rpcIdShort)}</span></td>
1385
+ <td>${escapeHtml(rpc.method)}</td>
1386
+ <td>${latency}</td>
1387
+ </tr>`;
1388
+ }).join('\n');
1389
+ return `
1390
+ <div class="session-content" data-session-id="${escapeHtml(sessionId)}">
1391
+ <div class="inner-container">
1392
+ <div class="left-pane">
1393
+ <div class="session-info">
1394
+ <h2>Session Info</h2>
1395
+ <dl>
1396
+ <dt>Session ID</dt>
1397
+ <dd><span class="badge">${escapeHtml(session.session_id)}</span></dd>
1398
+ <dt>Started</dt>
1399
+ <dd>${formatTimestamp(session.started_at)}</dd>
1400
+ <dt>Ended</dt>
1401
+ <dd>${session.ended_at ? formatTimestamp(session.ended_at) : '(active)'}</dd>
1402
+ <dt>Exit Reason</dt>
1403
+ <dd>${session.exit_reason || '(none)'}</dd>
1404
+ <dt>RPC Count</dt>
1405
+ <dd><span class="badge">${session.rpc_count}</span></dd>
1406
+ <dt>Event Count</dt>
1407
+ <dd><span class="badge">${session.event_count}</span></dd>
1408
+ <dt>Total Latency</dt>
1409
+ <dd><span class="badge">${totalLatencyDisplay}</span></dd>
1410
+ </dl>
1411
+ </div>
1412
+ <div class="rpc-list">
1413
+ <table class="rpc-table">
1414
+ <thead>
1415
+ <tr>
1416
+ <th>Time</th>
1417
+ <th>St</th>
1418
+ <th>ID</th>
1419
+ <th>Method</th>
1420
+ <th>Latency</th>
1421
+ </tr>
1422
+ </thead>
1423
+ <tbody>
1424
+ ${rpcRows}
1425
+ </tbody>
1426
+ </table>
1427
+ </div>
1428
+ </div>
1429
+ <div class="resize-handle"></div>
1430
+ <div class="right-pane">
1431
+ <div class="detail-placeholder">
1432
+ ${rpcs.length > 0 ? 'Select an RPC call from the list to view details' : 'No RPC calls in this session'}
1433
+ </div>
1434
+ </div>
1435
+ </div>
1436
+ </div>`;
1437
+ }
1438
+ /**
1439
+ * Generate Connector HTML report (3-hierarchy: Connector -> Sessions -> RPCs)
1440
+ */
1441
+ export function generateConnectorHtml(report) {
1442
+ const { meta, connector, sessions, session_reports } = report;
1443
+ // Pagination info
1444
+ const fromNum = connector.offset + 1;
1445
+ const toNum = connector.offset + connector.displayed_sessions;
1446
+ const paginationInfo = connector.session_count > 0
1447
+ ? `Showing ${fromNum}-${toNum} of ${connector.session_count} sessions`
1448
+ : 'No sessions';
1449
+ // Connector info section
1450
+ const transportDisplay = connector.transport.type === 'stdio'
1451
+ ? connector.transport.command || '(unknown command)'
1452
+ : connector.transport.url || '(unknown URL)';
1453
+ // Server info (if available)
1454
+ let serverInfoHtml = '';
1455
+ if (connector.server) {
1456
+ const { name, version, protocolVersion, capabilities } = connector.server;
1457
+ const serverName = name || '(unknown)';
1458
+ const serverVersion = version ? `v${version}` : '';
1459
+ const protocolDisplay = protocolVersion ? `MCP ${protocolVersion}` : '';
1460
+ // Capabilities badges
1461
+ const capBadges = [];
1462
+ if (capabilities.tools)
1463
+ capBadges.push('<span class="badge cap-enabled">tools</span>');
1464
+ if (capabilities.resources)
1465
+ capBadges.push('<span class="badge cap-enabled">resources</span>');
1466
+ if (capabilities.prompts)
1467
+ 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>`;
1476
+ }
1477
+ // Session items
1478
+ const sessionItems = sessions.map(s => renderConnectorSessionItem(s)).join('\n');
1479
+ // Session contents (pre-rendered, hidden by default)
1480
+ const sessionContents = sessions.map(s => {
1481
+ const sessionReport = session_reports[s.session_id];
1482
+ if (!sessionReport)
1483
+ return '';
1484
+ return renderSessionDetailContent(s.session_id, sessionReport);
1485
+ }).join('\n');
1486
+ const embeddedJson = escapeJsonForScript(JSON.stringify(report));
1487
+ return `<!DOCTYPE html>
1488
+ <html lang="en">
1489
+ <head>
1490
+ <meta charset="UTF-8">
1491
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1492
+ <title>Connector: ${escapeHtml(connector.connector_id)} - proofscan</title>
1493
+ <style>${getConnectorReportStyles()}</style>
1494
+ </head>
1495
+ <body>
1496
+ <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>
1499
+ </header>
1500
+
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>
1516
+ </div>
1517
+ </div>
1518
+
1519
+ <div class="main-container">
1520
+ <div class="sessions-pane">
1521
+ <div class="sessions-header">
1522
+ <h2>Sessions</h2>
1523
+ </div>
1524
+ <div class="sessions-list">
1525
+ ${sessionItems}
1526
+ </div>
1527
+ </div>
1528
+
1529
+ <div class="session-detail-pane">
1530
+ ${sessions.length === 0 ? '<div class="session-detail-empty">No sessions available</div>' : ''}
1531
+ ${sessionContents}
1532
+ </div>
1533
+ </div>
1534
+
1535
+ <script type="application/json" id="report-data">${embeddedJson}</script>
1536
+ <script>${getConnectorReportScript()}</script>
1537
+ </body>
1538
+ </html>`;
1539
+ }
756
1540
  //# sourceMappingURL=templates.js.map