proofscan 0.10.62 → 0.11.1

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 (199) hide show
  1. package/README.ja.md +1 -0
  2. package/README.md +2 -0
  3. package/dist/a2a/agent-card.d.ts +2 -0
  4. package/dist/a2a/agent-card.d.ts.map +1 -1
  5. package/dist/a2a/agent-card.js +2 -2
  6. package/dist/a2a/agent-card.js.map +1 -1
  7. package/dist/a2a/client.d.ts +74 -12
  8. package/dist/a2a/client.d.ts.map +1 -1
  9. package/dist/a2a/client.js +228 -29
  10. package/dist/a2a/client.js.map +1 -1
  11. package/dist/a2a/normalizer.d.ts +4 -0
  12. package/dist/a2a/normalizer.d.ts.map +1 -1
  13. package/dist/a2a/normalizer.js +7 -4
  14. package/dist/a2a/normalizer.js.map +1 -1
  15. package/dist/a2a/session-manager.d.ts +81 -0
  16. package/dist/a2a/session-manager.d.ts.map +1 -0
  17. package/dist/a2a/session-manager.js +176 -0
  18. package/dist/a2a/session-manager.js.map +1 -0
  19. package/dist/a2a/types.d.ts +60 -0
  20. package/dist/a2a/types.d.ts.map +1 -1
  21. package/dist/cli.d.ts +2 -1
  22. package/dist/cli.d.ts.map +1 -1
  23. package/dist/cli.js +6 -3
  24. package/dist/cli.js.map +1 -1
  25. package/dist/commands/agent.d.ts.map +1 -1
  26. package/dist/commands/agent.js +35 -10
  27. package/dist/commands/agent.js.map +1 -1
  28. package/dist/commands/analyze.d.ts.map +1 -1
  29. package/dist/commands/analyze.js +12 -10
  30. package/dist/commands/analyze.js.map +1 -1
  31. package/dist/commands/connectors.js +2 -2
  32. package/dist/commands/connectors.js.map +1 -1
  33. package/dist/commands/index.d.ts +1 -0
  34. package/dist/commands/index.d.ts.map +1 -1
  35. package/dist/commands/index.js +1 -0
  36. package/dist/commands/index.js.map +1 -1
  37. package/dist/commands/plans.js +1 -1
  38. package/dist/commands/plans.js.map +1 -1
  39. package/dist/commands/record.js +5 -4
  40. package/dist/commands/record.js.map +1 -1
  41. package/dist/commands/rpc.d.ts.map +1 -1
  42. package/dist/commands/rpc.js +90 -28
  43. package/dist/commands/rpc.js.map +1 -1
  44. package/dist/commands/scan.d.ts.map +1 -1
  45. package/dist/commands/scan.js +8 -10
  46. package/dist/commands/scan.js.map +1 -1
  47. package/dist/commands/secrets.d.ts.map +1 -1
  48. package/dist/commands/secrets.js +11 -10
  49. package/dist/commands/secrets.js.map +1 -1
  50. package/dist/commands/sessions.js +2 -2
  51. package/dist/commands/sessions.js.map +1 -1
  52. package/dist/commands/summary.d.ts.map +1 -1
  53. package/dist/commands/summary.js +4 -2
  54. package/dist/commands/summary.js.map +1 -1
  55. package/dist/commands/task.d.ts +14 -0
  56. package/dist/commands/task.d.ts.map +1 -0
  57. package/dist/commands/task.js +520 -0
  58. package/dist/commands/task.js.map +1 -0
  59. package/dist/db/connection.d.ts.map +1 -1
  60. package/dist/db/connection.js +68 -21
  61. package/dist/db/connection.js.map +1 -1
  62. package/dist/db/events-store.d.ts +307 -8
  63. package/dist/db/events-store.d.ts.map +1 -1
  64. package/dist/db/events-store.js +620 -26
  65. package/dist/db/events-store.js.map +1 -1
  66. package/dist/db/proofs-store.d.ts +8 -1
  67. package/dist/db/proofs-store.d.ts.map +1 -1
  68. package/dist/db/proofs-store.js +18 -8
  69. package/dist/db/proofs-store.js.map +1 -1
  70. package/dist/db/schema.d.ts +15 -3
  71. package/dist/db/schema.d.ts.map +1 -1
  72. package/dist/db/schema.js +150 -5
  73. package/dist/db/schema.js.map +1 -1
  74. package/dist/db/tool-analysis.d.ts +15 -3
  75. package/dist/db/tool-analysis.d.ts.map +1 -1
  76. package/dist/db/tool-analysis.js +35 -17
  77. package/dist/db/tool-analysis.js.map +1 -1
  78. package/dist/db/types.d.ts +64 -1
  79. package/dist/db/types.d.ts.map +1 -1
  80. package/dist/filter/fields.d.ts.map +1 -1
  81. package/dist/filter/fields.js +22 -0
  82. package/dist/filter/fields.js.map +1 -1
  83. package/dist/filter/parser.js +2 -2
  84. package/dist/filter/parser.js.map +1 -1
  85. package/dist/filter/types.d.ts +1 -1
  86. package/dist/filter/types.d.ts.map +1 -1
  87. package/dist/html/analytics.test.ts +682 -0
  88. package/dist/html/analytics.ts +499 -0
  89. package/dist/html/browser.ts +39 -0
  90. package/dist/html/index.ts +97 -0
  91. package/dist/html/rpc-inspector.test.ts +529 -0
  92. package/dist/html/rpc-inspector.ts +1700 -0
  93. package/dist/html/templates.js +4 -4
  94. package/dist/html/templates.js.map +1 -1
  95. package/dist/html/templates.test.ts +861 -0
  96. package/dist/html/templates.ts +3163 -0
  97. package/dist/html/trace-viewer.html +624 -0
  98. package/dist/html/types.d.ts +3 -3
  99. package/dist/html/types.d.ts.map +1 -1
  100. package/dist/html/types.ts +491 -0
  101. package/dist/html/utils.ts +107 -0
  102. package/dist/monitor/data/connectors.d.ts.map +1 -1
  103. package/dist/monitor/data/connectors.js +113 -8
  104. package/dist/monitor/data/connectors.js.map +1 -1
  105. package/dist/monitor/data/popl.js +2 -2
  106. package/dist/monitor/data/popl.js.map +1 -1
  107. package/dist/monitor/routes/api.js +2 -2
  108. package/dist/monitor/routes/api.js.map +1 -1
  109. package/dist/monitor/routes/connectors.js +15 -15
  110. package/dist/monitor/routes/connectors.js.map +1 -1
  111. package/dist/monitor/routes/popl.js +5 -5
  112. package/dist/monitor/routes/popl.js.map +1 -1
  113. package/dist/monitor/templates/components.js +2 -2
  114. package/dist/monitor/templates/components.js.map +1 -1
  115. package/dist/monitor/templates/popl.js +4 -4
  116. package/dist/monitor/templates/popl.js.map +1 -1
  117. package/dist/monitor/types.d.ts +2 -2
  118. package/dist/monitor/types.d.ts.map +1 -1
  119. package/dist/proxy/bridge-utils.d.ts +41 -0
  120. package/dist/proxy/bridge-utils.d.ts.map +1 -0
  121. package/dist/proxy/bridge-utils.js +60 -0
  122. package/dist/proxy/bridge-utils.js.map +1 -0
  123. package/dist/proxy/ipc-client.d.ts.map +1 -1
  124. package/dist/proxy/ipc-client.js +1 -2
  125. package/dist/proxy/ipc-client.js.map +1 -1
  126. package/dist/proxy/ipc-server.d.ts.map +1 -1
  127. package/dist/proxy/ipc-server.js +4 -2
  128. package/dist/proxy/ipc-server.js.map +1 -1
  129. package/dist/proxy/mcp-server.d.ts +31 -0
  130. package/dist/proxy/mcp-server.d.ts.map +1 -1
  131. package/dist/proxy/mcp-server.js +393 -4
  132. package/dist/proxy/mcp-server.js.map +1 -1
  133. package/dist/proxy/types.d.ts +95 -0
  134. package/dist/proxy/types.d.ts.map +1 -1
  135. package/dist/secrets/management.d.ts +2 -2
  136. package/dist/secrets/management.d.ts.map +1 -1
  137. package/dist/secrets/management.js +7 -7
  138. package/dist/secrets/management.js.map +1 -1
  139. package/dist/shell/completer.d.ts.map +1 -1
  140. package/dist/shell/completer.js +16 -0
  141. package/dist/shell/completer.js.map +1 -1
  142. package/dist/shell/context-applicator.d.ts.map +1 -1
  143. package/dist/shell/context-applicator.js +32 -0
  144. package/dist/shell/context-applicator.js.map +1 -1
  145. package/dist/shell/filter-mappers.d.ts +5 -1
  146. package/dist/shell/filter-mappers.d.ts.map +1 -1
  147. package/dist/shell/filter-mappers.js +12 -0
  148. package/dist/shell/filter-mappers.js.map +1 -1
  149. package/dist/shell/find-command.js +13 -13
  150. package/dist/shell/find-command.js.map +1 -1
  151. package/dist/shell/inscribe-commands.js +5 -5
  152. package/dist/shell/inscribe-commands.js.map +1 -1
  153. package/dist/shell/pager/less-pager.d.ts +1 -1
  154. package/dist/shell/pager/less-pager.d.ts.map +1 -1
  155. package/dist/shell/pager/less-pager.js +5 -2
  156. package/dist/shell/pager/less-pager.js.map +1 -1
  157. package/dist/shell/pager/more-pager.d.ts +1 -1
  158. package/dist/shell/pager/more-pager.d.ts.map +1 -1
  159. package/dist/shell/pager/more-pager.js +3 -2
  160. package/dist/shell/pager/more-pager.js.map +1 -1
  161. package/dist/shell/pager/renderer.d.ts.map +1 -1
  162. package/dist/shell/pager/renderer.js +66 -15
  163. package/dist/shell/pager/renderer.js.map +1 -1
  164. package/dist/shell/pager/types.d.ts +5 -2
  165. package/dist/shell/pager/types.d.ts.map +1 -1
  166. package/dist/shell/pager/utils.d.ts +5 -2
  167. package/dist/shell/pager/utils.d.ts.map +1 -1
  168. package/dist/shell/pager/utils.js +14 -17
  169. package/dist/shell/pager/utils.js.map +1 -1
  170. package/dist/shell/pipeline-types.d.ts +12 -4
  171. package/dist/shell/pipeline-types.d.ts.map +1 -1
  172. package/dist/shell/ref-commands.js +7 -7
  173. package/dist/shell/ref-commands.js.map +1 -1
  174. package/dist/shell/ref-resolver.d.ts +15 -15
  175. package/dist/shell/ref-resolver.d.ts.map +1 -1
  176. package/dist/shell/ref-resolver.js +34 -20
  177. package/dist/shell/ref-resolver.js.map +1 -1
  178. package/dist/shell/repl.d.ts +25 -0
  179. package/dist/shell/repl.d.ts.map +1 -1
  180. package/dist/shell/repl.js +285 -51
  181. package/dist/shell/repl.js.map +1 -1
  182. package/dist/shell/router-commands.d.ts +30 -0
  183. package/dist/shell/router-commands.d.ts.map +1 -1
  184. package/dist/shell/router-commands.js +1011 -62
  185. package/dist/shell/router-commands.js.map +1 -1
  186. package/dist/shell/selector.d.ts +1 -1
  187. package/dist/shell/selector.d.ts.map +1 -1
  188. package/dist/shell/selector.js +1 -1
  189. package/dist/shell/selector.js.map +1 -1
  190. package/dist/shell/types.d.ts.map +1 -1
  191. package/dist/shell/types.js +3 -1
  192. package/dist/shell/types.js.map +1 -1
  193. package/dist/shell/where-command.d.ts.map +1 -1
  194. package/dist/shell/where-command.js +19 -3
  195. package/dist/shell/where-command.js.map +1 -1
  196. package/dist/utils/output.d.ts.map +1 -1
  197. package/dist/utils/output.js +7 -1
  198. package/dist/utils/output.js.map +1 -1
  199. package/package.json +2 -2
@@ -0,0 +1,3163 @@
1
+ /**
2
+ * HTML Templates (Phase 5.0)
3
+ *
4
+ * Generates standalone HTML files for RPC and Session reports.
5
+ * Dark theme with neon blue accent badges.
6
+ */
7
+
8
+ import { formatBytes } from '../eventline/types.js';
9
+ import type {
10
+ HtmlConnectorAnalyticsV1,
11
+ HtmlConnectorKpis,
12
+ HtmlConnectorReportV1,
13
+ HtmlConnectorSessionRow,
14
+ HtmlHeatmapData,
15
+ HtmlLatencyHistogram,
16
+ HtmlMethodDistribution,
17
+ HtmlMethodLatencyData,
18
+ HtmlRpcReportV1,
19
+ HtmlSessionReportV1,
20
+ HtmlTopToolsData,
21
+ PayloadData,
22
+ RpcStatus,
23
+ SessionRpcDetail,
24
+ } from './types.js';
25
+ import { getStatusSymbol, SHORT_ID_LENGTH } from './types.js';
26
+ import {
27
+ getRpcInspectorStyles,
28
+ getRpcInspectorScript,
29
+ renderJsonWithPaths,
30
+ renderRequestSummary,
31
+ renderResponseSummary,
32
+ renderSummaryRowsHtml,
33
+ detectSensitiveKeys,
34
+ } from './rpc-inspector.js';
35
+
36
+ /**
37
+ * Escape HTML special characters to prevent XSS
38
+ */
39
+ export function escapeHtml(text: string): string {
40
+ return text
41
+ .replace(/&/g, '&')
42
+ .replace(/</g, '&lt;')
43
+ .replace(/>/g, '&gt;')
44
+ .replace(/"/g, '&quot;')
45
+ .replace(/'/g, '&#39;');
46
+ }
47
+
48
+ /**
49
+ * Escape JSON for embedding in <script type="application/json">
50
+ * Must escape </script> sequences to prevent premature tag closing
51
+ * Also escape U+2028/U+2029 which are valid in JSON but break JS string literals
52
+ */
53
+ export function escapeJsonForScript(json: string): string {
54
+ return json
55
+ .replace(/<\/script/gi, '<\\/script')
56
+ .replace(/\u2028/g, '\\u2028') // Line Separator - valid JSON but breaks JS
57
+ .replace(/\u2029/g, '\\u2029'); // Paragraph Separator - valid JSON but breaks JS
58
+ }
59
+
60
+ /**
61
+ * Format timestamp for display
62
+ */
63
+ function formatTimestamp(ts: string): string {
64
+ try {
65
+ const date = new Date(ts);
66
+ return date.toISOString().replace('T', ' ').replace('Z', ' UTC');
67
+ } catch {
68
+ return ts;
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Shorten ID for display
74
+ */
75
+ function shortenId(id: string, length: number = 8): string {
76
+ return id.slice(0, length);
77
+ }
78
+
79
+ /**
80
+ * Get CSS styles for HTML reports (single column for RPC)
81
+ */
82
+ function getRpcReportStyles(): string {
83
+ return `
84
+ :root {
85
+ --bg-primary: #0d1117;
86
+ --bg-secondary: #161b22;
87
+ --text-primary: #e6edf3;
88
+ --text-secondary: #8b949e;
89
+ --accent-blue: #00d4ff;
90
+ --status-ok: #00d4ff;
91
+ --status-err: #f85149;
92
+ --status-pending: #d29922;
93
+ --border-color: #30363d;
94
+ --link-color: #58a6ff;
95
+ }
96
+ * { box-sizing: border-box; }
97
+ body {
98
+ background: var(--bg-primary);
99
+ color: var(--text-primary);
100
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
101
+ margin: 0;
102
+ padding: 20px;
103
+ line-height: 1.5;
104
+ }
105
+ h1 {
106
+ margin: 0 0 8px 0;
107
+ font-size: 1.5em;
108
+ font-weight: 600;
109
+ }
110
+ h2 {
111
+ margin: 0 0 12px 0;
112
+ font-size: 1.1em;
113
+ font-weight: 600;
114
+ color: var(--text-primary);
115
+ border-bottom: 1px solid var(--border-color);
116
+ padding-bottom: 8px;
117
+ }
118
+ h3 {
119
+ margin: 16px 0 8px 0;
120
+ font-size: 0.95em;
121
+ font-weight: 600;
122
+ color: var(--text-secondary);
123
+ }
124
+ h3:first-child { margin-top: 0; }
125
+ a { color: var(--link-color); text-decoration: none; }
126
+ a:hover { text-decoration: underline; }
127
+ .meta { color: var(--text-secondary); margin: 0 0 20px 0; font-size: 0.85em; }
128
+ .badge {
129
+ display: inline-block;
130
+ padding: 2px 8px;
131
+ border: 1px solid var(--accent-blue);
132
+ border-radius: 4px;
133
+ color: var(--accent-blue);
134
+ font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
135
+ font-size: 0.85em;
136
+ background: transparent;
137
+ }
138
+ .badge.status-OK { border-color: var(--status-ok); color: var(--status-ok); }
139
+ .badge.status-ERR { border-color: var(--status-err); color: var(--status-err); }
140
+ .badge.status-PENDING { border-color: var(--status-pending); color: var(--status-pending); }
141
+ /* Sensitive content warning badge (Phase 12.x-c) */
142
+ .sensitive-badge {
143
+ display: inline-flex;
144
+ align-items: center;
145
+ gap: 4px;
146
+ padding: 2px 8px;
147
+ margin-left: 8px;
148
+ background: rgba(210, 153, 34, 0.15);
149
+ border: 1px solid rgba(210, 153, 34, 0.3);
150
+ border-radius: 12px;
151
+ font-size: 11px;
152
+ font-weight: 500;
153
+ color: #d29922;
154
+ vertical-align: middle;
155
+ }
156
+ .section {
157
+ background: var(--bg-secondary);
158
+ border-radius: 8px;
159
+ padding: 16px;
160
+ margin: 16px 0;
161
+ }
162
+ dl {
163
+ display: grid;
164
+ grid-template-columns: auto 1fr;
165
+ gap: 8px 16px;
166
+ margin: 0;
167
+ }
168
+ dt { color: var(--text-secondary); }
169
+ dd { margin: 0; }
170
+ pre {
171
+ background: var(--bg-primary);
172
+ border: 1px solid var(--border-color);
173
+ border-radius: 6px;
174
+ padding: 12px;
175
+ overflow-x: auto;
176
+ margin: 8px 0;
177
+ font-size: 0.85em;
178
+ }
179
+ code {
180
+ font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
181
+ color: var(--text-primary);
182
+ }
183
+ .copy-btn {
184
+ background: var(--bg-secondary);
185
+ border: 1px solid var(--border-color);
186
+ color: var(--text-secondary);
187
+ padding: 4px 8px;
188
+ border-radius: 4px;
189
+ cursor: pointer;
190
+ font-size: 0.8em;
191
+ margin-left: 8px;
192
+ }
193
+ .copy-btn:hover {
194
+ border-color: var(--accent-blue);
195
+ color: var(--accent-blue);
196
+ }
197
+ .truncated-note {
198
+ color: var(--status-pending);
199
+ font-size: 0.85em;
200
+ margin: 4px 0;
201
+ }
202
+ .spill-link {
203
+ color: var(--link-color);
204
+ font-size: 0.85em;
205
+ }
206
+ `;
207
+ }
208
+
209
+ /**
210
+ * Get CSS styles for Session HTML (2-pane Wireshark layout)
211
+ */
212
+ function getSessionReportStyles(): string {
213
+ return `
214
+ :root {
215
+ --bg-primary: #0d1117;
216
+ --bg-secondary: #161b22;
217
+ --text-primary: #e6edf3;
218
+ --text-secondary: #8b949e;
219
+ --accent-blue: #00d4ff;
220
+ --status-ok: #00d4ff;
221
+ --status-err: #f85149;
222
+ --status-pending: #d29922;
223
+ --border-color: #30363d;
224
+ --link-color: #58a6ff;
225
+ --left-pane-width: 420px;
226
+ }
227
+ * { box-sizing: border-box; }
228
+ html, body {
229
+ height: 100%;
230
+ margin: 0;
231
+ padding: 0;
232
+ overflow: hidden;
233
+ }
234
+ body {
235
+ background: var(--bg-primary);
236
+ color: var(--text-primary);
237
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
238
+ line-height: 1.5;
239
+ display: flex;
240
+ flex-direction: column;
241
+ }
242
+ header {
243
+ padding: 12px 20px;
244
+ border-bottom: 1px solid var(--border-color);
245
+ flex-shrink: 0;
246
+ }
247
+ h1 {
248
+ margin: 0 0 4px 0;
249
+ font-size: 1.3em;
250
+ font-weight: 600;
251
+ }
252
+ h2 {
253
+ margin: 0 0 8px 0;
254
+ font-size: 1em;
255
+ font-weight: 600;
256
+ color: var(--text-primary);
257
+ border-bottom: 1px solid var(--border-color);
258
+ padding-bottom: 6px;
259
+ }
260
+ h3 {
261
+ margin: 12px 0 6px 0;
262
+ font-size: 0.9em;
263
+ font-weight: 600;
264
+ color: var(--text-secondary);
265
+ }
266
+ h3:first-child { margin-top: 0; }
267
+ a { color: var(--link-color); text-decoration: none; }
268
+ a:hover { text-decoration: underline; }
269
+ .meta { color: var(--text-secondary); margin: 0; font-size: 0.8em; }
270
+ .badge {
271
+ display: inline-block;
272
+ padding: 1px 6px;
273
+ border: 1px solid var(--accent-blue);
274
+ border-radius: 4px;
275
+ color: var(--accent-blue);
276
+ font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
277
+ font-size: 0.8em;
278
+ background: transparent;
279
+ }
280
+ .badge.status-OK { border-color: var(--status-ok); color: var(--status-ok); }
281
+ .badge.status-ERR { border-color: var(--status-err); color: var(--status-err); }
282
+ .badge.status-PENDING { border-color: var(--status-pending); color: var(--status-pending); }
283
+ /* Sensitive content warning badge (Phase 12.x-c) */
284
+ .sensitive-badge {
285
+ display: inline-flex;
286
+ align-items: center;
287
+ gap: 4px;
288
+ padding: 2px 8px;
289
+ margin-left: 8px;
290
+ background: rgba(210, 153, 34, 0.15);
291
+ border: 1px solid rgba(210, 153, 34, 0.3);
292
+ border-radius: 12px;
293
+ font-size: 11px;
294
+ font-weight: 500;
295
+ color: #d29922;
296
+ vertical-align: middle;
297
+ }
298
+ /* Two-pane layout */
299
+ .container {
300
+ display: flex;
301
+ flex: 1;
302
+ overflow: hidden;
303
+ }
304
+ .left-pane {
305
+ width: var(--left-pane-width);
306
+ min-width: 300px;
307
+ max-width: 600px;
308
+ border-right: 1px solid var(--border-color);
309
+ display: flex;
310
+ flex-direction: column;
311
+ overflow: hidden;
312
+ }
313
+ .right-pane {
314
+ flex: 1;
315
+ display: flex;
316
+ flex-direction: column;
317
+ overflow: hidden;
318
+ padding: 16px;
319
+ min-height: 0;
320
+ }
321
+ .right-pane > .rpc-inspector {
322
+ flex: 1;
323
+ min-height: 0;
324
+ }
325
+ .session-info {
326
+ background: var(--bg-secondary);
327
+ padding: 12px;
328
+ border-bottom: 1px solid var(--border-color);
329
+ flex-shrink: 0;
330
+ }
331
+ .session-info dl {
332
+ display: grid;
333
+ grid-template-columns: auto 1fr;
334
+ gap: 4px 12px;
335
+ margin: 0;
336
+ font-size: 0.85em;
337
+ }
338
+ .session-info dt { color: var(--text-secondary); }
339
+ .session-info dd { margin: 0; }
340
+ .rpc-list {
341
+ flex: 1;
342
+ overflow-y: auto;
343
+ }
344
+ .rpc-table {
345
+ width: 100%;
346
+ border-collapse: collapse;
347
+ font-size: 0.85em;
348
+ }
349
+ .rpc-table th {
350
+ text-align: left;
351
+ color: var(--text-secondary);
352
+ border-bottom: 1px solid var(--border-color);
353
+ padding: 6px 8px;
354
+ font-weight: 500;
355
+ position: sticky;
356
+ top: 0;
357
+ background: var(--bg-primary);
358
+ z-index: 1;
359
+ }
360
+ .rpc-table td {
361
+ padding: 6px 8px;
362
+ border-bottom: 1px solid var(--border-color);
363
+ white-space: nowrap;
364
+ }
365
+ .rpc-row {
366
+ cursor: pointer;
367
+ }
368
+ .rpc-row:hover {
369
+ background: rgba(0, 212, 255, 0.1);
370
+ }
371
+ .rpc-row.selected {
372
+ background: rgba(0, 212, 255, 0.2);
373
+ }
374
+ /* Right pane detail */
375
+ .detail-placeholder {
376
+ color: var(--text-secondary);
377
+ text-align: center;
378
+ padding: 40px;
379
+ }
380
+ .detail-section {
381
+ background: var(--bg-secondary);
382
+ border-radius: 8px;
383
+ padding: 16px;
384
+ margin-bottom: 16px;
385
+ }
386
+ pre {
387
+ background: var(--bg-primary);
388
+ border: 1px solid var(--border-color);
389
+ border-radius: 6px;
390
+ padding: 12px;
391
+ overflow-x: auto;
392
+ margin: 8px 0;
393
+ font-size: 0.85em;
394
+ max-height: 400px;
395
+ overflow-y: auto;
396
+ }
397
+ code {
398
+ font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
399
+ color: var(--text-primary);
400
+ }
401
+ .copy-btn {
402
+ background: var(--bg-secondary);
403
+ border: 1px solid var(--border-color);
404
+ color: var(--text-secondary);
405
+ padding: 3px 6px;
406
+ border-radius: 4px;
407
+ cursor: pointer;
408
+ font-size: 0.75em;
409
+ margin-left: 8px;
410
+ }
411
+ .copy-btn:hover {
412
+ border-color: var(--accent-blue);
413
+ color: var(--accent-blue);
414
+ }
415
+ .truncated-note {
416
+ color: var(--status-pending);
417
+ font-size: 0.8em;
418
+ margin: 4px 0;
419
+ }
420
+ .spill-link {
421
+ color: var(--link-color);
422
+ font-size: 0.8em;
423
+ }
424
+ /* Resize handle */
425
+ .resize-handle {
426
+ width: 4px;
427
+ background: var(--border-color);
428
+ cursor: col-resize;
429
+ transition: background 0.2s;
430
+ }
431
+ .resize-handle:hover {
432
+ background: var(--accent-blue);
433
+ }
434
+ /* RPC Inspector styles */
435
+ ${getRpcInspectorStyles()}
436
+ `;
437
+ }
438
+
439
+ /**
440
+ * Get JavaScript for RPC report (copy button only)
441
+ */
442
+ function getRpcReportScript(): string {
443
+ return `
444
+ // Copy button functionality
445
+ document.querySelectorAll('.copy-btn').forEach(btn => {
446
+ btn.addEventListener('click', async (e) => {
447
+ e.stopPropagation();
448
+ const targetId = btn.getAttribute('data-target');
449
+ const target = document.getElementById(targetId);
450
+ if (target) {
451
+ try {
452
+ await navigator.clipboard.writeText(target.textContent || '');
453
+ const originalText = btn.textContent;
454
+ btn.textContent = 'Copied!';
455
+ setTimeout(() => { btn.textContent = originalText; }, 1500);
456
+ } catch (err) {
457
+ console.error('Copy failed:', err);
458
+ }
459
+ }
460
+ });
461
+ });
462
+ `;
463
+ }
464
+
465
+ /**
466
+ * Get JavaScript for Session report (2-pane with selection)
467
+ */
468
+ function getSessionReportScript(): string {
469
+ return `
470
+ // Report data
471
+ const reportData = JSON.parse(document.getElementById('report-data').textContent);
472
+ const rpcs = reportData.rpcs;
473
+ let selectedIdx = null;
474
+
475
+ // Format JSON for display
476
+ function formatJson(data) {
477
+ if (data === null || data === undefined) return '(no data)';
478
+ try {
479
+ return JSON.stringify(data, null, 2);
480
+ } catch {
481
+ return String(data);
482
+ }
483
+ }
484
+
485
+ // Escape HTML
486
+ function escapeHtml(text) {
487
+ const div = document.createElement('div');
488
+ div.textContent = text;
489
+ return div.innerHTML;
490
+ }
491
+
492
+ // Format bytes
493
+ function formatBytes(bytes) {
494
+ if (bytes === 0) return '0 B';
495
+ const k = 1024;
496
+ const sizes = ['B', 'KB', 'MB', 'GB'];
497
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
498
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
499
+ }
500
+
501
+ // Render payload section
502
+ function renderPayload(title, payload, elementId) {
503
+ let content, notes = '';
504
+
505
+ if (payload.truncated) {
506
+ notes = '<p class="truncated-note">Payload truncated (' + formatBytes(payload.size) + ', showing first 4096 chars)</p>';
507
+ if (payload.spillFile) {
508
+ notes += '<p class="spill-link">Full payload: <a href="' + escapeHtml(payload.spillFile) + '">' + escapeHtml(payload.spillFile) + '</a></p>';
509
+ }
510
+ content = payload.preview ? escapeHtml(payload.preview) + '\\n... (truncated)' : '(no data)';
511
+ } else if (payload.json !== null) {
512
+ content = escapeHtml(formatJson(payload.json));
513
+ } else {
514
+ content = '(no data)';
515
+ }
516
+
517
+ return '<h3>' + title + ' <button class="copy-btn" onclick="copyToClipboard(\\'' + elementId + '\\', this)">Copy</button></h3>' +
518
+ notes +
519
+ '<pre id="' + elementId + '"><code>' + content + '</code></pre>';
520
+ }
521
+
522
+ // Show RPC detail in right pane (2-column Wireshark-style layout)
523
+ // Summary and Raw JSON now both toggle with Req/Res buttons
524
+ function showRpcDetail(idx) {
525
+ if (idx < 0 || idx >= rpcs.length) return;
526
+
527
+ const rpc = rpcs[idx];
528
+ const rightPane = document.getElementById('right-pane');
529
+
530
+ // Update selection state
531
+ document.querySelectorAll('.rpc-row').forEach((r, i) => {
532
+ r.classList.toggle('selected', i === idx);
533
+ });
534
+ selectedIdx = idx;
535
+
536
+ const statusClass = 'status-' + rpc.status;
537
+ const statusSymbol = rpc.status === 'OK' ? '✓' : rpc.status === 'ERR' ? '✗' : '?';
538
+ const latency = rpc.latency_ms !== null ? rpc.latency_ms + 'ms' : '(pending)';
539
+
540
+ // Get pre-rendered summary and raw JSON (separate request/response)
541
+ const requestSummaryHtml = rpc._requestSummaryHtml || '<div class="summary-row summary-header">No summary available</div>';
542
+ const responseSummaryHtml = rpc._responseSummaryHtml || '<div class="summary-row summary-header">No summary available</div>';
543
+ const requestRawHtml = rpc._requestRawHtml || '<span class="json-null">(no data)</span>';
544
+ const responseRawHtml = rpc._responseRawHtml || '<span class="json-null">(no data)</span>';
545
+
546
+ // Sensitive content warning badge (Phase 12.x-c)
547
+ // Escape keys to prevent XSS via malicious key names
548
+ const sensitiveKeys = (rpc._sensitiveKeys || []).map(function(k) { return escapeHtml(k); });
549
+ const sensitiveTooltip = sensitiveKeys.length > 5
550
+ ? 'Contains ' + sensitiveKeys.length + ' sensitive keys: ' + sensitiveKeys.slice(0, 5).join(', ') + '...'
551
+ : 'Contains sensitive keys: ' + sensitiveKeys.join(', ');
552
+ const sensitiveBadge = rpc._hasSensitive
553
+ ? '<span class="sensitive-badge" title="' + escapeHtml(sensitiveTooltip) + '">⚠ Sensitive</span>'
554
+ : '';
555
+
556
+ // Determine default target based on method (response-focused methods default to response)
557
+ const defaultTarget = (rpc.method === 'tools/list' || rpc.method === 'initialize' || rpc.method.startsWith('resources/') || rpc.method.startsWith('prompts/')) ? 'response' : 'request';
558
+
559
+ rightPane.innerHTML =
560
+ '<div class="detail-section">' +
561
+ ' <h2>RPC Info' + sensitiveBadge + '</h2>' +
562
+ ' <div class="rpc-info-grid">' +
563
+ ' <div class="rpc-info-item"><dt>RPC ID</dt><dd><span class="badge">' + escapeHtml(rpc.rpc_id) + '</span></dd></div>' +
564
+ ' <div class="rpc-info-item"><dt>Method</dt><dd><span class="badge">' + escapeHtml(rpc.method) + '</span></dd></div>' +
565
+ ' <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>' +
566
+ ' <div class="rpc-info-item"><dt>Latency</dt><dd><span class="badge">' + latency + '</span></dd></div>' +
567
+ ' <div class="rpc-info-item"><dt>Request</dt><dd>' + escapeHtml(rpc.request_ts) + '</dd></div>' +
568
+ ' <div class="rpc-info-item"><dt>Response</dt><dd>' + escapeHtml(rpc.response_ts || '-') + '</dd></div>' +
569
+ ' </div>' +
570
+ '</div>' +
571
+ '<div class="detail-section">' +
572
+ ' <div class="rpc-toggle-bar">' +
573
+ ' <button id="toggle-req" class="rpc-toggle-btn' + (defaultTarget === 'request' ? ' active' : '') + '">[Req]</button>' +
574
+ ' <button id="toggle-res" class="rpc-toggle-btn' + (defaultTarget === 'response' ? ' active' : '') + '">[Res]</button>' +
575
+ ' </div>' +
576
+ ' <div class="rpc-inspector">' +
577
+ ' <div class="rpc-inspector-summary">' +
578
+ ' <h3>Summary</h3>' +
579
+ ' <div id="summary-request" style="display:' + (defaultTarget === 'request' ? 'block' : 'none') + '">' + requestSummaryHtml + '</div>' +
580
+ ' <div id="summary-response" style="display:' + (defaultTarget === 'response' ? 'block' : 'none') + '">' + responseSummaryHtml + '</div>' +
581
+ ' </div>' +
582
+ ' <div class="rpc-inspector-raw">' +
583
+ ' <div class="rpc-raw-json">' +
584
+ ' <div id="raw-json-request" style="display:' + (defaultTarget === 'request' ? 'block' : 'none') + '">' + requestRawHtml + '</div>' +
585
+ ' <div id="raw-json-response" style="display:' + (defaultTarget === 'response' ? 'block' : 'none') + '">' + responseRawHtml + '</div>' +
586
+ ' </div>' +
587
+ ' </div>' +
588
+ ' </div>' +
589
+ '</div>';
590
+
591
+ // Re-initialize RPC Inspector handlers
592
+ if (window.initRpcInspector) {
593
+ window.initRpcInspector();
594
+ }
595
+ }
596
+
597
+ // Copy to clipboard
598
+ async function copyToClipboard(elementId, btn) {
599
+ const target = document.getElementById(elementId);
600
+ if (target) {
601
+ try {
602
+ await navigator.clipboard.writeText(target.textContent || '');
603
+ const originalText = btn.textContent;
604
+ btn.textContent = 'Copied!';
605
+ setTimeout(() => { btn.textContent = originalText; }, 1500);
606
+ } catch (err) {
607
+ console.error('Copy failed:', err);
608
+ }
609
+ }
610
+ }
611
+
612
+ // RPC row click handlers
613
+ document.querySelectorAll('.rpc-row').forEach((row, idx) => {
614
+ row.addEventListener('click', () => showRpcDetail(idx));
615
+ });
616
+
617
+ // Keyboard navigation
618
+ document.addEventListener('keydown', (e) => {
619
+ if (selectedIdx === null) return;
620
+ if (e.key === 'ArrowDown' && selectedIdx < rpcs.length - 1) {
621
+ e.preventDefault();
622
+ showRpcDetail(selectedIdx + 1);
623
+ // Scroll selected row into view
624
+ const row = document.querySelector('.rpc-row.selected');
625
+ if (row) row.scrollIntoView({ block: 'nearest' });
626
+ } else if (e.key === 'ArrowUp' && selectedIdx > 0) {
627
+ e.preventDefault();
628
+ showRpcDetail(selectedIdx - 1);
629
+ const row = document.querySelector('.rpc-row.selected');
630
+ if (row) row.scrollIntoView({ block: 'nearest' });
631
+ }
632
+ });
633
+
634
+ // Resize handle drag
635
+ const resizeHandle = document.querySelector('.resize-handle');
636
+ const leftPane = document.querySelector('.left-pane');
637
+ if (resizeHandle && leftPane) {
638
+ let startX, startWidth;
639
+
640
+ resizeHandle.addEventListener('mousedown', (e) => {
641
+ startX = e.clientX;
642
+ startWidth = leftPane.offsetWidth;
643
+ document.addEventListener('mousemove', onMouseMove);
644
+ document.addEventListener('mouseup', onMouseUp);
645
+ e.preventDefault();
646
+ });
647
+
648
+ function onMouseMove(e) {
649
+ const diff = e.clientX - startX;
650
+ const newWidth = Math.max(300, Math.min(600, startWidth + diff));
651
+ leftPane.style.width = newWidth + 'px';
652
+ }
653
+
654
+ function onMouseUp() {
655
+ document.removeEventListener('mousemove', onMouseMove);
656
+ document.removeEventListener('mouseup', onMouseUp);
657
+ }
658
+ }
659
+
660
+ // Select first RPC by default if any exist
661
+ if (rpcs.length > 0) {
662
+ showRpcDetail(0);
663
+ }
664
+
665
+ // RPC Inspector script
666
+ ${getRpcInspectorScript()}
667
+ `;
668
+ }
669
+
670
+ /**
671
+ * Render payload section (request or response)
672
+ */
673
+ function renderPayloadSection(
674
+ title: string,
675
+ payload: PayloadData,
676
+ elementId: string
677
+ ): string {
678
+ const copyBtn = `<button class="copy-btn" data-target="${elementId}">Copy</button>`;
679
+
680
+ let content: string;
681
+ let notes = '';
682
+
683
+ if (payload.truncated) {
684
+ notes = `<p class="truncated-note">Payload truncated (${formatBytes(payload.size)}, showing first 4096 chars)</p>`;
685
+ if (payload.spillFile) {
686
+ notes += `<p class="spill-link">Full payload: <a href="${escapeHtml(payload.spillFile)}">${escapeHtml(payload.spillFile)}</a></p>`;
687
+ }
688
+ content = payload.preview ? escapeHtml(payload.preview) + '\n... (truncated)' : '(no data)';
689
+ } else if (payload.json !== null) {
690
+ try {
691
+ content = escapeHtml(JSON.stringify(payload.json, null, 2));
692
+ } catch {
693
+ content = '(invalid JSON)';
694
+ }
695
+ } else {
696
+ content = '(no data)';
697
+ }
698
+
699
+ return `
700
+ <h3>${title} ${copyBtn}</h3>
701
+ ${notes}
702
+ <pre id="${elementId}"><code>${content}</code></pre>
703
+ `;
704
+ }
705
+
706
+ /**
707
+ * Generate RPC HTML report
708
+ */
709
+ export function generateRpcHtml(report: HtmlRpcReportV1): string {
710
+ const { meta, rpc } = report;
711
+ const sessionShort = shortenId(rpc.session_id, 12);
712
+ const statusClass = `status-${rpc.status}`;
713
+
714
+ const embeddedJson = escapeJsonForScript(JSON.stringify(report));
715
+
716
+ return `<!DOCTYPE html>
717
+ <html lang="en">
718
+ <head>
719
+ <meta charset="UTF-8">
720
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
721
+ <title>RPC: ${escapeHtml(rpc.method)} - proofscan</title>
722
+ <style>${getRpcReportStyles()}</style>
723
+ </head>
724
+ <body>
725
+ <header>
726
+ <h1>RPC: <span class="badge">${escapeHtml(rpc.method)}</span></h1>
727
+ <p class="meta">Generated by ${escapeHtml(meta.generatedBy)} at ${formatTimestamp(meta.generatedAt)}${meta.redacted ? ' (redacted)' : ''}</p>
728
+ </header>
729
+
730
+ <main>
731
+ <section class="section">
732
+ <h2>Info</h2>
733
+ <dl>
734
+ <dt>RPC ID</dt>
735
+ <dd><span class="badge">${escapeHtml(rpc.rpc_id)}</span></dd>
736
+ <dt>Session</dt>
737
+ <dd><span class="badge">${escapeHtml(sessionShort)}...</span></dd>
738
+ <dt>Connector</dt>
739
+ <dd><span class="badge">${escapeHtml(rpc.target_id)}</span></dd>
740
+ <dt>Status</dt>
741
+ <dd><span class="badge ${statusClass}">${rpc.status}${rpc.error_code !== null ? ` (code: ${rpc.error_code})` : ''}</span></dd>
742
+ </dl>
743
+ </section>
744
+
745
+ <section class="section">
746
+ <h2>Timing</h2>
747
+ <dl>
748
+ <dt>Request</dt>
749
+ <dd>${formatTimestamp(rpc.request_ts)}</dd>
750
+ <dt>Response</dt>
751
+ <dd>${rpc.response_ts ? formatTimestamp(rpc.response_ts) : '(pending)'}</dd>
752
+ <dt>Latency</dt>
753
+ <dd>${rpc.latency_ms !== null ? `<span class="badge">${rpc.latency_ms}ms</span>` : '(pending)'}</dd>
754
+ </dl>
755
+ </section>
756
+
757
+ <section class="section">
758
+ <h2>Size</h2>
759
+ <dl>
760
+ <dt>Request</dt>
761
+ <dd>${formatBytes(rpc.request.size)}</dd>
762
+ <dt>Response</dt>
763
+ <dd>${formatBytes(rpc.response.size)}</dd>
764
+ </dl>
765
+ </section>
766
+
767
+ <section class="section">
768
+ <h2>Request</h2>
769
+ ${renderPayloadSection('', rpc.request, 'request-json').replace('<h3>', '').replace('</h3>', '')}
770
+ </section>
771
+
772
+ <section class="section">
773
+ <h2>Response</h2>
774
+ ${renderPayloadSection('', rpc.response, 'response-json').replace('<h3>', '').replace('</h3>', '')}
775
+ </section>
776
+ </main>
777
+
778
+ <script type="application/json" id="report-data">${embeddedJson}</script>
779
+ <script>${getRpcReportScript()}</script>
780
+ </body>
781
+ </html>`;
782
+ }
783
+
784
+ /**
785
+ * Render a single RPC row for Session HTML (left pane table)
786
+ */
787
+ function renderRpcRow(rpc: SessionRpcDetail, idx: number): string {
788
+ const statusClass = `status-${rpc.status}`;
789
+ const statusSymbol = getStatusSymbol(rpc.status);
790
+ const rpcIdShort = shortenId(rpc.rpc_id);
791
+ // Shorter time format for left pane (HH:MM:SS.mmm)
792
+ const timeShort = formatTimestamp(rpc.request_ts).split(' ')[1]?.slice(0, 12) || '-';
793
+ const latency = rpc.latency_ms !== null ? `${rpc.latency_ms}ms` : '-';
794
+
795
+ return `
796
+ <tr class="rpc-row">
797
+ <td>${timeShort}</td>
798
+ <td><span class="badge ${statusClass}">${statusSymbol}</span></td>
799
+ <td><span class="badge">${escapeHtml(rpcIdShort)}</span></td>
800
+ <td>${escapeHtml(rpc.method)}</td>
801
+ <td>${latency}</td>
802
+ </tr>`;
803
+ }
804
+
805
+ /**
806
+ * Generate Session HTML report (2-pane Wireshark-style layout)
807
+ */
808
+ export function generateSessionHtml(report: HtmlSessionReportV1): string {
809
+ const { meta, session, rpcs } = report;
810
+ const sessionShort = shortenId(session.session_id, 12);
811
+
812
+ const rpcRows = rpcs.map((rpc, idx) => renderRpcRow(rpc, idx)).join('\n');
813
+
814
+ // Pre-render summary and raw JSON HTML for each RPC (for RPC Inspector)
815
+ // Now generates separate request/response summaries for Req/Res toggle
816
+ // Also detect sensitive content for warning badge (Phase 12.x-c)
817
+ const rpcsWithInspectorHtml = rpcs.map((rpc) => {
818
+ const requestSummaryRows = renderRequestSummary(rpc.method, rpc.request.json);
819
+ const responseSummaryRows = renderResponseSummary(rpc.method, rpc.response.json);
820
+ // Detect sensitive keys in request/response
821
+ const reqSensitiveKeys = detectSensitiveKeys(rpc.request.json);
822
+ const resSensitiveKeys = detectSensitiveKeys(rpc.response.json);
823
+ const hasSensitive = reqSensitiveKeys.length > 0 || resSensitiveKeys.length > 0;
824
+ return {
825
+ ...rpc,
826
+ _requestSummaryHtml: renderSummaryRowsHtml(requestSummaryRows),
827
+ _responseSummaryHtml: renderSummaryRowsHtml(responseSummaryRows),
828
+ _requestRawHtml: renderJsonWithPaths(rpc.request.json, '#'),
829
+ _responseRawHtml: renderJsonWithPaths(rpc.response.json, '#'),
830
+ _hasSensitive: hasSensitive,
831
+ _sensitiveKeys: [...reqSensitiveKeys, ...resSensitiveKeys],
832
+ };
833
+ });
834
+
835
+ const reportWithInspectorHtml = {
836
+ ...report,
837
+ rpcs: rpcsWithInspectorHtml,
838
+ };
839
+ const embeddedJson = escapeJsonForScript(JSON.stringify(reportWithInspectorHtml));
840
+
841
+ // Format total latency
842
+ const totalLatencyDisplay = session.total_latency_ms !== null
843
+ ? `${session.total_latency_ms}ms`
844
+ : '-';
845
+
846
+ return `<!DOCTYPE html>
847
+ <html lang="en">
848
+ <head>
849
+ <meta charset="UTF-8">
850
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
851
+ <title>Session: ${escapeHtml(sessionShort)}... - proofscan</title>
852
+ <style>${getSessionReportStyles()}</style>
853
+ </head>
854
+ <body>
855
+ <header>
856
+ <h1>Session: <span class="badge">${escapeHtml(sessionShort)}...</span></h1>
857
+ <p class="meta">Generated by ${escapeHtml(meta.generatedBy)} at ${formatTimestamp(meta.generatedAt)}${meta.redacted ? ' (redacted)' : ''}</p>
858
+ </header>
859
+
860
+ <div class="container">
861
+ <div class="left-pane">
862
+ <div class="session-info">
863
+ <h2>Session Info</h2>
864
+ <dl>
865
+ <dt>Session ID</dt>
866
+ <dd><span class="badge">${escapeHtml(session.session_id)}</span></dd>
867
+ <dt>Connector</dt>
868
+ <dd><span class="badge">${escapeHtml(session.target_id)}</span></dd>
869
+ <dt>Started</dt>
870
+ <dd>${formatTimestamp(session.started_at)}</dd>
871
+ <dt>Ended</dt>
872
+ <dd>${session.ended_at ? formatTimestamp(session.ended_at) : '(active)'}</dd>
873
+ <dt>RPC Count</dt>
874
+ <dd><span class="badge">${session.rpc_count}</span></dd>
875
+ <dt>Event Count</dt>
876
+ <dd><span class="badge">${session.event_count}</span></dd>
877
+ <dt>Total Latency</dt>
878
+ <dd><span class="badge">${totalLatencyDisplay}</span></dd>
879
+ </dl>
880
+ </div>
881
+ <div class="rpc-list">
882
+ <table class="rpc-table">
883
+ <thead>
884
+ <tr>
885
+ <th>Time</th>
886
+ <th>St</th>
887
+ <th>ID</th>
888
+ <th>Method</th>
889
+ <th>Latency</th>
890
+ </tr>
891
+ </thead>
892
+ <tbody>
893
+ ${rpcRows}
894
+ </tbody>
895
+ </table>
896
+ </div>
897
+ </div>
898
+ <div class="resize-handle"></div>
899
+ <div class="right-pane" id="right-pane">
900
+ <div class="detail-placeholder">
901
+ ${rpcs.length > 0 ? 'Select an RPC call from the list to view details' : 'No RPC calls in this session'}
902
+ </div>
903
+ </div>
904
+ </div>
905
+
906
+ <script type="application/json" id="report-data">${embeddedJson}</script>
907
+ <script>${getSessionReportScript()}</script>
908
+ </body>
909
+ </html>`;
910
+ }
911
+
912
+ // ============================================================================
913
+ // Connector HTML Report (Phase 5.1)
914
+ // ============================================================================
915
+
916
+ /**
917
+ * Get CSS styles for Connector HTML (3-hierarchy: Connector -> Sessions -> RPCs)
918
+ */
919
+ function getConnectorReportStyles(): string {
920
+ return `
921
+ :root {
922
+ --bg-primary: #0d1117;
923
+ --bg-secondary: #161b22;
924
+ --bg-tertiary: #21262d;
925
+ --text-primary: #e6edf3;
926
+ --text-secondary: #8b949e;
927
+ --accent-blue: #00d4ff;
928
+ --status-ok: #3fb950;
929
+ --status-err: #f85149;
930
+ --status-pending: #d29922;
931
+ --border-color: #30363d;
932
+ --link-color: #58a6ff;
933
+ --sessions-pane-width: 360px;
934
+ --left-pane-width: 480px;
935
+ --raw-pane-max-width: 480px;
936
+ }
937
+ * { box-sizing: border-box; }
938
+ html, body {
939
+ height: 100%;
940
+ margin: 0;
941
+ padding: 0;
942
+ overflow: hidden;
943
+ }
944
+ body {
945
+ background: var(--bg-primary);
946
+ color: var(--text-primary);
947
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
948
+ line-height: 1.5;
949
+ display: flex;
950
+ flex-direction: column;
951
+ }
952
+ /* Unified header (matches Home/Ledger/Artifact pages) */
953
+ .header {
954
+ display: flex;
955
+ justify-content: space-between;
956
+ align-items: center;
957
+ padding: 12px 20px;
958
+ background: var(--bg-secondary);
959
+ border-bottom: 1px solid var(--border-color);
960
+ flex-shrink: 0;
961
+ }
962
+ .header-left {
963
+ display: flex;
964
+ align-items: center;
965
+ gap: 16px;
966
+ }
967
+ .header-title {
968
+ font-size: 18px;
969
+ font-weight: 600;
970
+ color: var(--text-primary);
971
+ }
972
+ .header-back {
973
+ color: var(--accent-blue);
974
+ font-size: 13px;
975
+ text-decoration: none;
976
+ }
977
+ .header-back:hover {
978
+ text-decoration: underline;
979
+ }
980
+ .header-meta {
981
+ display: flex;
982
+ align-items: center;
983
+ gap: 12px;
984
+ font-size: 12px;
985
+ color: var(--text-secondary);
986
+ }
987
+ .offline-badge {
988
+ display: inline-flex;
989
+ align-items: center;
990
+ gap: 4px;
991
+ padding: 2px 8px;
992
+ background: var(--bg-tertiary);
993
+ border: 1px solid var(--border-color);
994
+ border-radius: 12px;
995
+ font-size: 11px;
996
+ color: var(--text-secondary);
997
+ }
998
+ .offline-badge::before {
999
+ content: '';
1000
+ width: 6px;
1001
+ height: 6px;
1002
+ background: #6e7681;
1003
+ border-radius: 50%;
1004
+ }
1005
+ /* Auto-check toggle (Phase 12.1) */
1006
+ .auto-check-toggle {
1007
+ display: inline-flex;
1008
+ align-items: center;
1009
+ gap: 4px;
1010
+ background: var(--bg-tertiary);
1011
+ border: 1px solid var(--border-color);
1012
+ border-radius: 12px;
1013
+ padding: 2px 4px;
1014
+ }
1015
+ .auto-check-toggle .auto-check-label {
1016
+ font-size: 10px;
1017
+ color: var(--text-secondary);
1018
+ padding-left: 4px;
1019
+ }
1020
+ .auto-check-toggle button {
1021
+ background: transparent;
1022
+ border: none;
1023
+ padding: 2px 8px;
1024
+ border-radius: 10px;
1025
+ font-size: 11px;
1026
+ color: var(--text-secondary);
1027
+ cursor: pointer;
1028
+ transition: all 0.15s;
1029
+ }
1030
+ .auto-check-toggle button:hover {
1031
+ color: var(--text-primary);
1032
+ }
1033
+ .auto-check-toggle button.active {
1034
+ background: rgba(0, 212, 255, 0.15);
1035
+ color: var(--accent-blue);
1036
+ }
1037
+ /* New data banner */
1038
+ .new-data-banner {
1039
+ display: none;
1040
+ align-items: center;
1041
+ gap: 8px;
1042
+ padding: 4px 12px;
1043
+ background: rgba(0, 212, 255, 0.15);
1044
+ border: 1px solid var(--accent-blue);
1045
+ border-radius: 12px;
1046
+ font-size: 11px;
1047
+ color: var(--accent-blue);
1048
+ }
1049
+ .new-data-banner.active { display: inline-flex; }
1050
+ .new-data-banner button {
1051
+ background: var(--accent-blue);
1052
+ border: none;
1053
+ border-radius: 6px;
1054
+ padding: 2px 8px;
1055
+ font-size: 10px;
1056
+ color: var(--bg-primary);
1057
+ cursor: pointer;
1058
+ }
1059
+ /* Page header (Connector name + KPI row) */
1060
+ .page-header {
1061
+ padding: 8px 20px;
1062
+ border-bottom: 1px solid var(--border-color);
1063
+ flex-shrink: 0;
1064
+ display: flex;
1065
+ justify-content: space-between;
1066
+ align-items: center;
1067
+ }
1068
+ h1 {
1069
+ margin: 0;
1070
+ font-size: 1.3em;
1071
+ font-weight: 600;
1072
+ }
1073
+ h2 {
1074
+ margin: 0 0 8px 0;
1075
+ font-size: 1em;
1076
+ font-weight: 600;
1077
+ color: var(--text-primary);
1078
+ border-bottom: 1px solid var(--border-color);
1079
+ padding-bottom: 6px;
1080
+ }
1081
+ h3 {
1082
+ margin: 12px 0 6px 0;
1083
+ font-size: 0.9em;
1084
+ font-weight: 600;
1085
+ color: var(--text-secondary);
1086
+ }
1087
+ h3:first-child { margin-top: 0; }
1088
+ a { color: var(--link-color); text-decoration: none; }
1089
+ a:hover { text-decoration: underline; }
1090
+ .meta { color: var(--text-secondary); margin: 0; font-size: 0.8em; }
1091
+ .badge {
1092
+ display: inline-block;
1093
+ padding: 1px 6px;
1094
+ border: 1px solid var(--accent-blue);
1095
+ border-radius: 4px;
1096
+ color: var(--accent-blue);
1097
+ font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
1098
+ font-size: 0.8em;
1099
+ background: transparent;
1100
+ }
1101
+ .badge.status-OK { border-color: var(--status-ok); color: var(--status-ok); }
1102
+ .badge.status-ERR { border-color: var(--status-err); color: var(--status-err); }
1103
+ .badge.status-PENDING { border-color: var(--status-pending); color: var(--status-pending); }
1104
+ .badge.cap-enabled { border-color: var(--accent-blue); color: var(--accent-blue); background: rgba(0, 212, 255, 0.1); }
1105
+ .badge.cap-disabled { border-color: var(--border-color); color: var(--text-secondary); background: transparent; opacity: 0.5; }
1106
+ /* Sensitive content warning badge (Phase 12.x-c) */
1107
+ .sensitive-badge {
1108
+ display: inline-flex;
1109
+ align-items: center;
1110
+ gap: 4px;
1111
+ padding: 2px 8px;
1112
+ margin-left: 8px;
1113
+ background: rgba(210, 153, 34, 0.15);
1114
+ border: 1px solid rgba(210, 153, 34, 0.3);
1115
+ border-radius: 12px;
1116
+ font-size: 11px;
1117
+ font-weight: 500;
1118
+ color: #d29922;
1119
+ vertical-align: middle;
1120
+ }
1121
+
1122
+ /* Connector info cards container (side by side) */
1123
+ .connector-info-cards {
1124
+ display: flex;
1125
+ gap: 0;
1126
+ }
1127
+ .connector-info-cards > .connector-info {
1128
+ flex: 1;
1129
+ border-right: 1px solid var(--border-color);
1130
+ }
1131
+ .connector-info-cards > .connector-info:last-child {
1132
+ border-right: none;
1133
+ padding-left: 24px;
1134
+ }
1135
+
1136
+ /* Connector info section (collapsible) */
1137
+ .connector-info {
1138
+ background: var(--bg-secondary);
1139
+ padding: 12px 20px;
1140
+ border-bottom: 1px solid var(--border-color);
1141
+ flex-shrink: 0;
1142
+ }
1143
+ .connector-info-toggle {
1144
+ display: flex;
1145
+ align-items: center;
1146
+ justify-content: space-between;
1147
+ cursor: pointer;
1148
+ user-select: none;
1149
+ }
1150
+ .connector-info-toggle h2 {
1151
+ margin: 0;
1152
+ border: none;
1153
+ padding: 0;
1154
+ }
1155
+ .connector-info-toggle .toggle-icon {
1156
+ color: var(--text-secondary);
1157
+ font-size: 0.85em;
1158
+ transition: transform 0.2s;
1159
+ }
1160
+ .connector-info-content {
1161
+ display: none;
1162
+ margin-top: 12px;
1163
+ }
1164
+ .connector-info.expanded .connector-info-content {
1165
+ display: block;
1166
+ }
1167
+ .connector-info.expanded .toggle-icon {
1168
+ transform: rotate(180deg);
1169
+ }
1170
+ .connector-info dl {
1171
+ display: grid;
1172
+ grid-template-columns: auto 1fr;
1173
+ gap: 4px 16px;
1174
+ margin: 0;
1175
+ font-size: 0.85em;
1176
+ }
1177
+ .connector-info dt { color: var(--text-secondary); }
1178
+ .connector-info dd { margin: 0; }
1179
+ .capabilities {
1180
+ display: flex;
1181
+ gap: 6px;
1182
+ flex-wrap: wrap;
1183
+ }
1184
+
1185
+ /* Main 3-pane container */
1186
+ .main-container {
1187
+ display: flex;
1188
+ flex: 1;
1189
+ overflow: hidden;
1190
+ }
1191
+
1192
+ /* Sessions pane (leftmost) */
1193
+ .sessions-pane {
1194
+ width: var(--sessions-pane-width);
1195
+ flex-shrink: 0;
1196
+ border-right: 1px solid var(--border-color);
1197
+ display: flex;
1198
+ flex-direction: column;
1199
+ overflow: hidden;
1200
+ background: var(--bg-secondary);
1201
+ }
1202
+ .sessions-header {
1203
+ padding: 10px 12px;
1204
+ border-bottom: 1px solid var(--border-color);
1205
+ background: var(--bg-tertiary);
1206
+ flex-shrink: 0;
1207
+ }
1208
+ .sessions-header h2 {
1209
+ margin: 0;
1210
+ border: none;
1211
+ padding: 0;
1212
+ font-size: 0.9em;
1213
+ }
1214
+ .sessions-list {
1215
+ flex: 1;
1216
+ overflow-y: auto;
1217
+ padding: 4px 8px;
1218
+ }
1219
+ .sessions-header-row {
1220
+ display: grid;
1221
+ grid-template-columns: 70px 1fr 60px 50px;
1222
+ gap: 8px;
1223
+ padding: 4px 8px;
1224
+ font-size: 10px;
1225
+ color: var(--text-secondary);
1226
+ text-transform: uppercase;
1227
+ border-bottom: 1px solid var(--border-color);
1228
+ margin-bottom: 4px;
1229
+ }
1230
+ .session-item {
1231
+ display: grid;
1232
+ grid-template-columns: 70px 1fr auto 50px 50px;
1233
+ gap: 8px;
1234
+ align-items: center;
1235
+ padding: 6px 8px;
1236
+ border-radius: 4px;
1237
+ cursor: pointer;
1238
+ margin-bottom: 2px;
1239
+ border: 1px solid transparent;
1240
+ background: var(--bg-primary);
1241
+ font-size: 11px;
1242
+ }
1243
+ .session-item .session-counts {
1244
+ display: flex;
1245
+ gap: 6px;
1246
+ font-size: 10px;
1247
+ color: var(--text-secondary);
1248
+ }
1249
+ .session-item .session-counts span {
1250
+ white-space: nowrap;
1251
+ }
1252
+ .session-item .session-extra {
1253
+ justify-self: end;
1254
+ }
1255
+ .session-item:hover {
1256
+ background: rgba(0, 212, 255, 0.1);
1257
+ border-color: rgba(0, 212, 255, 0.3);
1258
+ }
1259
+ .session-item.selected {
1260
+ border-color: var(--accent-blue);
1261
+ background: rgba(0, 212, 255, 0.15);
1262
+ }
1263
+ .session-item.highlight {
1264
+ animation: highlightPulse 2s ease-out;
1265
+ }
1266
+ @keyframes highlightPulse {
1267
+ 0% { box-shadow: 0 0 0 3px rgba(0, 212, 255, 0.6); }
1268
+ 100% { box-shadow: 0 0 0 0 rgba(0, 212, 255, 0); }
1269
+ }
1270
+ .session-item .session-id {
1271
+ font-family: 'SFMono-Regular', Consolas, monospace;
1272
+ color: var(--accent-blue);
1273
+ overflow: hidden;
1274
+ text-overflow: ellipsis;
1275
+ white-space: nowrap;
1276
+ }
1277
+ .session-item .session-timestamp {
1278
+ color: var(--text-secondary);
1279
+ overflow: hidden;
1280
+ text-overflow: ellipsis;
1281
+ white-space: nowrap;
1282
+ }
1283
+ .session-item .session-latency {
1284
+ color: var(--text-secondary);
1285
+ text-align: right;
1286
+ }
1287
+
1288
+ /* Session detail pane (middle) */
1289
+ .session-detail-pane {
1290
+ flex: 1;
1291
+ display: flex;
1292
+ flex-direction: column;
1293
+ overflow: hidden;
1294
+ min-width: 0;
1295
+ }
1296
+ .session-detail-empty {
1297
+ flex: 1;
1298
+ display: flex;
1299
+ align-items: center;
1300
+ justify-content: center;
1301
+ color: var(--text-secondary);
1302
+ }
1303
+
1304
+ /* Re-use session HTML styles for the detail view */
1305
+ .session-content {
1306
+ display: none;
1307
+ flex-direction: column;
1308
+ height: 100%;
1309
+ overflow: hidden;
1310
+ }
1311
+ .session-content.active {
1312
+ display: flex;
1313
+ }
1314
+ .inner-container {
1315
+ display: flex;
1316
+ flex: 1;
1317
+ overflow: hidden;
1318
+ min-height: 0;
1319
+ }
1320
+ .left-pane {
1321
+ width: var(--left-pane-width);
1322
+ min-width: 300px;
1323
+ max-width: 600px;
1324
+ border-right: 1px solid var(--border-color);
1325
+ display: flex;
1326
+ flex-direction: column;
1327
+ overflow: hidden;
1328
+ }
1329
+ .right-pane {
1330
+ flex: 1;
1331
+ display: flex;
1332
+ flex-direction: column;
1333
+ overflow: hidden;
1334
+ padding: 16px;
1335
+ min-height: 0;
1336
+ }
1337
+ .right-pane > .rpc-inspector {
1338
+ flex: 1;
1339
+ min-height: 0;
1340
+ }
1341
+ .session-info {
1342
+ background: var(--bg-secondary);
1343
+ padding: 12px;
1344
+ border-bottom: 1px solid var(--border-color);
1345
+ flex-shrink: 0;
1346
+ }
1347
+ .session-info dl {
1348
+ display: grid;
1349
+ grid-template-columns: auto 1fr;
1350
+ gap: 4px 12px;
1351
+ margin: 0;
1352
+ font-size: 0.85em;
1353
+ }
1354
+ .session-info dt { color: var(--text-secondary); }
1355
+ .session-info dd { margin: 0; }
1356
+ .rpc-list {
1357
+ flex: 1;
1358
+ overflow-y: auto;
1359
+ }
1360
+ .rpc-table {
1361
+ width: 100%;
1362
+ border-collapse: collapse;
1363
+ font-size: 0.85em;
1364
+ }
1365
+ .rpc-table th {
1366
+ text-align: left;
1367
+ color: var(--text-secondary);
1368
+ border-bottom: 1px solid var(--border-color);
1369
+ padding: 6px 8px;
1370
+ font-weight: 500;
1371
+ position: sticky;
1372
+ top: 0;
1373
+ background: var(--bg-primary);
1374
+ z-index: 1;
1375
+ }
1376
+ .rpc-table td {
1377
+ padding: 6px 8px;
1378
+ border-bottom: 1px solid var(--border-color);
1379
+ white-space: nowrap;
1380
+ }
1381
+ .rpc-row {
1382
+ cursor: pointer;
1383
+ }
1384
+ .rpc-row:hover {
1385
+ background: rgba(0, 212, 255, 0.1);
1386
+ }
1387
+ .rpc-row.selected {
1388
+ background: rgba(0, 212, 255, 0.2);
1389
+ }
1390
+ .detail-placeholder {
1391
+ color: var(--text-secondary);
1392
+ text-align: center;
1393
+ padding: 40px;
1394
+ }
1395
+ .detail-section {
1396
+ background: var(--bg-secondary);
1397
+ border-radius: 8px;
1398
+ padding: 16px;
1399
+ margin-bottom: 16px;
1400
+ }
1401
+ pre {
1402
+ background: var(--bg-primary);
1403
+ border: 1px solid var(--border-color);
1404
+ border-radius: 6px;
1405
+ padding: 12px;
1406
+ overflow-x: auto;
1407
+ margin: 8px 0;
1408
+ font-size: 0.85em;
1409
+ max-height: 400px;
1410
+ overflow-y: auto;
1411
+ }
1412
+ code {
1413
+ font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
1414
+ color: var(--text-primary);
1415
+ }
1416
+ .copy-btn {
1417
+ background: var(--bg-secondary);
1418
+ border: 1px solid var(--border-color);
1419
+ color: var(--text-secondary);
1420
+ padding: 3px 6px;
1421
+ border-radius: 4px;
1422
+ cursor: pointer;
1423
+ font-size: 0.75em;
1424
+ margin-left: 8px;
1425
+ }
1426
+ .copy-btn:hover {
1427
+ border-color: var(--accent-blue);
1428
+ color: var(--accent-blue);
1429
+ }
1430
+ .truncated-note {
1431
+ color: var(--status-pending);
1432
+ font-size: 0.8em;
1433
+ margin: 4px 0;
1434
+ }
1435
+ .spill-link {
1436
+ color: var(--link-color);
1437
+ font-size: 0.8em;
1438
+ }
1439
+ .resize-handle {
1440
+ width: 4px;
1441
+ background: var(--border-color);
1442
+ cursor: col-resize;
1443
+ transition: background 0.2s;
1444
+ }
1445
+ .resize-handle:hover {
1446
+ background: var(--accent-blue);
1447
+ }
1448
+
1449
+ /* Events View Toggle (Issue #59) */
1450
+ .view-toggle {
1451
+ display: flex;
1452
+ gap: 2px;
1453
+ padding: 8px 12px;
1454
+ border-bottom: 1px solid var(--border-color);
1455
+ background: var(--bg-secondary);
1456
+ }
1457
+ .view-toggle-btn {
1458
+ background: transparent;
1459
+ border: 1px solid var(--border-color);
1460
+ color: var(--text-secondary);
1461
+ padding: 4px 12px;
1462
+ border-radius: 4px;
1463
+ cursor: pointer;
1464
+ font-size: 11px;
1465
+ transition: all 0.15s;
1466
+ }
1467
+ .view-toggle-btn:first-child {
1468
+ border-radius: 4px 0 0 4px;
1469
+ }
1470
+ .view-toggle-btn:last-child {
1471
+ border-radius: 0 4px 4px 0;
1472
+ }
1473
+ .view-toggle-btn:hover {
1474
+ border-color: var(--accent-blue);
1475
+ color: var(--text-primary);
1476
+ }
1477
+ .view-toggle-btn.active {
1478
+ background: rgba(0, 212, 255, 0.15);
1479
+ border-color: var(--accent-blue);
1480
+ color: var(--accent-blue);
1481
+ }
1482
+ .view-toggle-count {
1483
+ font-size: 10px;
1484
+ color: var(--text-secondary);
1485
+ margin-left: 4px;
1486
+ }
1487
+
1488
+ /* Events List (Issue #59) */
1489
+ .events-list {
1490
+ flex: 1;
1491
+ overflow-y: auto;
1492
+ display: none;
1493
+ }
1494
+ .events-list.active {
1495
+ display: block;
1496
+ }
1497
+ .rpc-list.hidden {
1498
+ display: none;
1499
+ }
1500
+ .events-table {
1501
+ width: 100%;
1502
+ border-collapse: collapse;
1503
+ font-size: 0.85em;
1504
+ }
1505
+ .events-table th {
1506
+ text-align: left;
1507
+ color: var(--text-secondary);
1508
+ border-bottom: 1px solid var(--border-color);
1509
+ padding: 6px 8px;
1510
+ font-weight: 500;
1511
+ position: sticky;
1512
+ top: 0;
1513
+ background: var(--bg-primary);
1514
+ z-index: 1;
1515
+ }
1516
+ .events-table td {
1517
+ padding: 6px 8px;
1518
+ border-bottom: 1px solid var(--border-color);
1519
+ white-space: nowrap;
1520
+ }
1521
+ .event-row {
1522
+ cursor: pointer;
1523
+ }
1524
+ .event-row:hover {
1525
+ background: rgba(0, 212, 255, 0.1);
1526
+ }
1527
+ .event-row.selected {
1528
+ background: rgba(0, 212, 255, 0.2);
1529
+ }
1530
+
1531
+ /* Event kind badges */
1532
+ .badge-kind-request {
1533
+ background: rgba(0, 212, 255, 0.15);
1534
+ color: var(--accent-blue);
1535
+ border: 1px solid rgba(0, 212, 255, 0.3);
1536
+ }
1537
+ .badge-kind-response {
1538
+ background: rgba(63, 185, 80, 0.15);
1539
+ color: var(--accent-green);
1540
+ border: 1px solid rgba(63, 185, 80, 0.3);
1541
+ }
1542
+ .badge-kind-notification {
1543
+ background: rgba(210, 153, 34, 0.15);
1544
+ color: var(--accent-yellow);
1545
+ border: 1px solid rgba(210, 153, 34, 0.3);
1546
+ }
1547
+ .badge-kind-transport_event {
1548
+ background: var(--bg-tertiary);
1549
+ color: var(--text-secondary);
1550
+ border: 1px solid var(--border-color);
1551
+ }
1552
+
1553
+ /* Event direction arrows */
1554
+ .direction-arrow {
1555
+ font-size: 18px;
1556
+ font-weight: bold;
1557
+ line-height: 1;
1558
+ cursor: help;
1559
+ }
1560
+ .direction-arrow.outgoing {
1561
+ color: var(--accent-blue);
1562
+ }
1563
+ .direction-arrow.incoming {
1564
+ color: var(--accent-green);
1565
+ }
1566
+
1567
+ /* Events loading state */
1568
+ .events-loading {
1569
+ padding: 24px;
1570
+ text-align: center;
1571
+ color: var(--text-secondary);
1572
+ }
1573
+
1574
+ /* Analytics Panel (Phase 5.2) - Revised Layout */
1575
+
1576
+ /* Header with KPI stats inline */
1577
+ header {
1578
+ display: flex;
1579
+ align-items: center;
1580
+ justify-content: space-between;
1581
+ flex-wrap: wrap;
1582
+ gap: 12px;
1583
+ }
1584
+ .page-header-left {
1585
+ flex-shrink: 0;
1586
+ }
1587
+ /* Capability badges - active (blue) / inactive (gray) */
1588
+ .badge.cap-disabled {
1589
+ border-color: var(--border-color);
1590
+ color: var(--text-secondary);
1591
+ background: transparent;
1592
+ opacity: 0.5;
1593
+ }
1594
+ .kpi-row {
1595
+ display: flex;
1596
+ gap: 16px;
1597
+ flex-wrap: wrap;
1598
+ align-items: baseline;
1599
+ }
1600
+ .kpi-item {
1601
+ display: flex;
1602
+ flex-direction: column;
1603
+ align-items: center;
1604
+ padding: 0;
1605
+ background: transparent;
1606
+ min-width: 50px;
1607
+ }
1608
+ .kpi-item .kpi-value {
1609
+ font-size: 0.95em;
1610
+ font-weight: 600;
1611
+ color: var(--accent-blue);
1612
+ font-family: 'SFMono-Regular', Consolas, monospace;
1613
+ line-height: 1.2;
1614
+ }
1615
+ .kpi-item .kpi-label {
1616
+ font-size: 0.55em;
1617
+ color: var(--text-secondary);
1618
+ text-transform: uppercase;
1619
+ letter-spacing: 0.5px;
1620
+ }
1621
+ /* All KPI values use accent-blue for unified appearance */
1622
+
1623
+ /* Connector top section: info + charts row */
1624
+ .connector-top {
1625
+ display: flex;
1626
+ gap: 16px;
1627
+ padding: 12px 20px;
1628
+ border-bottom: 1px solid var(--border-color);
1629
+ background: var(--bg-secondary);
1630
+ }
1631
+ .connector-top .connector-info {
1632
+ flex: 0 0 360px;
1633
+ max-width: 360px;
1634
+ border-bottom: none;
1635
+ padding: 0;
1636
+ }
1637
+ .analytics-panel {
1638
+ flex: 1;
1639
+ display: flex;
1640
+ gap: 12px;
1641
+ align-items: stretch;
1642
+ }
1643
+
1644
+ /* Charts row - 4 items horizontal with custom flex ratios */
1645
+ .heatmap-container {
1646
+ flex: 0.8;
1647
+ background: var(--bg-primary);
1648
+ border: 1px solid var(--border-color);
1649
+ border-radius: 6px;
1650
+ padding: 8px;
1651
+ min-width: 0;
1652
+ }
1653
+ .latency-histogram {
1654
+ flex: 1.4;
1655
+ background: var(--bg-primary);
1656
+ border: 1px solid var(--border-color);
1657
+ border-radius: 6px;
1658
+ padding: 8px;
1659
+ min-width: 0;
1660
+ }
1661
+ .top-tools, .method-distribution {
1662
+ flex: 1;
1663
+ background: var(--bg-primary);
1664
+ border: 1px solid var(--border-color);
1665
+ border-radius: 6px;
1666
+ padding: 8px;
1667
+ min-width: 0;
1668
+ }
1669
+ .chart-title {
1670
+ font-size: 0.75em;
1671
+ color: var(--text-secondary);
1672
+ margin-bottom: 4px;
1673
+ }
1674
+
1675
+ /* Heatmap - using neon blue gradient for consistency with theme */
1676
+ .heatmap-title {
1677
+ font-size: 0.75em;
1678
+ color: var(--text-secondary);
1679
+ margin-bottom: 4px;
1680
+ line-height: 1.4;
1681
+ }
1682
+ .heatmap-range {
1683
+ font-size: 0.9em;
1684
+ opacity: 0.8;
1685
+ }
1686
+ .heatmap-level-0 { fill: var(--bg-tertiary); }
1687
+ .heatmap-level-1 { fill: #0a3d4d; }
1688
+ .heatmap-level-2 { fill: #0d5c73; }
1689
+ .heatmap-level-3 { fill: #0097b2; }
1690
+ .heatmap-level-4 { fill: #00d4ff; }
1691
+
1692
+ /* Histogram / Method Latency Heatmap */
1693
+ .histogram-bar { fill: var(--accent-blue); }
1694
+ .histogram-label { fill: var(--text-secondary); font-size: 9px; }
1695
+ .latency-heatmap { font-size: 0.75em; }
1696
+ .latency-heatmap-header, .latency-heatmap-row {
1697
+ display: flex;
1698
+ align-items: center;
1699
+ gap: 4px;
1700
+ margin-bottom: 3px;
1701
+ }
1702
+ .latency-heatmap-header { color: var(--text-secondary); margin-bottom: 6px; }
1703
+ .latency-heatmap-method { width: 28px; flex-shrink: 0; text-align: right; color: var(--text-secondary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 10px; }
1704
+ .latency-heatmap-cells { display: flex; gap: 1px; }
1705
+ .latency-heatmap-cell { width: 30px; height: 12px; border-radius: 2px; cursor: default; }
1706
+ .latency-heatmap-cell.heatmap-level-0 { background: #161b22; }
1707
+ .latency-heatmap-cell.heatmap-level-1 { background: #0e4429; }
1708
+ .latency-heatmap-cell.heatmap-level-2 { background: #006d32; }
1709
+ .latency-heatmap-cell.heatmap-level-3 { background: #0097b2; }
1710
+ .latency-heatmap-cell.heatmap-level-4 { background: #00d4ff; }
1711
+ .latency-heatmap-header-cell { width: 30px; font-size: 8px; text-align: center; color: var(--text-secondary); white-space: nowrap; }
1712
+
1713
+ /* Top Tools */
1714
+ .top-tool-row {
1715
+ display: flex;
1716
+ align-items: center;
1717
+ gap: 6px;
1718
+ margin-bottom: 3px;
1719
+ font-size: 0.75em;
1720
+ }
1721
+ .top-tool-rank {
1722
+ color: var(--text-secondary);
1723
+ width: 14px;
1724
+ flex-shrink: 0;
1725
+ }
1726
+ .top-tool-name {
1727
+ flex: 1;
1728
+ min-width: 0;
1729
+ overflow: hidden;
1730
+ text-overflow: ellipsis;
1731
+ white-space: nowrap;
1732
+ font-family: 'SFMono-Regular', Consolas, monospace;
1733
+ }
1734
+ .top-tool-bar-container {
1735
+ width: 50px;
1736
+ height: 6px;
1737
+ background: var(--bg-tertiary);
1738
+ border-radius: 3px;
1739
+ overflow: hidden;
1740
+ flex-shrink: 0;
1741
+ }
1742
+ .top-tool-bar {
1743
+ height: 100%;
1744
+ background: var(--accent-blue);
1745
+ border-radius: 3px;
1746
+ }
1747
+ .top-tool-pct {
1748
+ color: var(--text-secondary);
1749
+ width: 28px;
1750
+ text-align: right;
1751
+ flex-shrink: 0;
1752
+ }
1753
+ .no-data-message {
1754
+ color: var(--text-secondary);
1755
+ font-size: 0.75em;
1756
+ text-align: center;
1757
+ padding: 8px;
1758
+ }
1759
+
1760
+ /* Method Distribution Donut Chart */
1761
+ .donut-container {
1762
+ display: flex;
1763
+ align-items: center;
1764
+ gap: 8px;
1765
+ }
1766
+ .donut-legend {
1767
+ flex: 1;
1768
+ font-size: 0.7em;
1769
+ min-width: 0;
1770
+ }
1771
+ .donut-legend-item {
1772
+ display: flex;
1773
+ align-items: center;
1774
+ gap: 4px;
1775
+ margin-bottom: 2px;
1776
+ white-space: nowrap;
1777
+ overflow: hidden;
1778
+ }
1779
+ .donut-legend-color {
1780
+ width: 8px;
1781
+ height: 8px;
1782
+ border-radius: 2px;
1783
+ flex-shrink: 0;
1784
+ }
1785
+ .donut-legend-label {
1786
+ overflow: hidden;
1787
+ text-overflow: ellipsis;
1788
+ flex: 1;
1789
+ min-width: 0;
1790
+ }
1791
+ .donut-legend-pct {
1792
+ color: var(--text-secondary);
1793
+ flex-shrink: 0;
1794
+ }
1795
+ /* RPC Inspector styles */
1796
+ ${getRpcInspectorStyles()}
1797
+ `;
1798
+ }
1799
+
1800
+ /**
1801
+ * Get JavaScript for Connector HTML (3-hierarchy navigation)
1802
+ */
1803
+ function getConnectorReportScript(): string {
1804
+ return `
1805
+ // Auto-check functionality (Phase 12.1)
1806
+ (function() {
1807
+ // Only run on monitor pages (not static HTML export)
1808
+ if (!document.body.dataset.app || document.body.dataset.app !== 'monitor') {
1809
+ return;
1810
+ }
1811
+
1812
+ let checkInterval = null;
1813
+ let lastDigest = null;
1814
+ let newDataDetected = false;
1815
+ const INTERVAL_MS = 10000;
1816
+
1817
+ const toggle = document.getElementById('autoCheckToggle');
1818
+ const banner = document.getElementById('newDataBanner');
1819
+ const refreshBtn = document.getElementById('refreshNowBtn');
1820
+ if (!toggle) return;
1821
+
1822
+ const buttons = toggle.querySelectorAll('button');
1823
+ const enabled = localStorage.getItem('proofscan-auto-check') === 'true';
1824
+
1825
+ // Initial state
1826
+ buttons.forEach(function(btn) {
1827
+ btn.classList.toggle('active', (btn.dataset.enabled === 'true') === enabled);
1828
+ });
1829
+ if (enabled) startChecking();
1830
+
1831
+ function startChecking() {
1832
+ checkForUpdates(); // First check
1833
+ checkInterval = setInterval(checkForUpdates, INTERVAL_MS);
1834
+ }
1835
+
1836
+ function stopChecking() {
1837
+ if (checkInterval) clearInterval(checkInterval);
1838
+ checkInterval = null;
1839
+ }
1840
+
1841
+ function checkForUpdates() {
1842
+ if (newDataDetected) return; // Banner already shown
1843
+ fetch('/api/monitor/summary')
1844
+ .then(function(res) { return res.ok ? res.json() : null; })
1845
+ .then(function(data) {
1846
+ if (!data) return;
1847
+ if (lastDigest === null) {
1848
+ lastDigest = data.digest; // Baseline
1849
+ } else if (data.digest !== lastDigest) {
1850
+ newDataDetected = true;
1851
+ if (banner) banner.classList.add('active');
1852
+ }
1853
+ })
1854
+ .catch(function(err) { console.debug('[Auto-check] Poll failed:', err); });
1855
+ }
1856
+
1857
+ buttons.forEach(function(btn) {
1858
+ btn.addEventListener('click', function() {
1859
+ var on = btn.dataset.enabled === 'true';
1860
+ localStorage.setItem('proofscan-auto-check', String(on));
1861
+ buttons.forEach(function(b) {
1862
+ b.classList.toggle('active', (b.dataset.enabled === 'true') === on);
1863
+ });
1864
+ if (on) {
1865
+ startChecking();
1866
+ } else {
1867
+ stopChecking();
1868
+ if (banner) banner.classList.remove('active');
1869
+ newDataDetected = false;
1870
+ }
1871
+ });
1872
+ });
1873
+
1874
+ if (refreshBtn) {
1875
+ refreshBtn.addEventListener('click', function() { location.reload(); });
1876
+ }
1877
+ })();
1878
+
1879
+ // Report data
1880
+ const reportData = JSON.parse(document.getElementById('report-data').textContent);
1881
+ const sessions = reportData.sessions;
1882
+ const sessionReports = reportData.session_reports;
1883
+
1884
+ let currentSessionId = null;
1885
+ let currentRpcIdx = null;
1886
+ let currentEventIdx = null;
1887
+ let currentViewMode = 'rpc'; // 'rpc' or 'events'
1888
+
1889
+ // Connector info toggle
1890
+ const connectorInfo = document.querySelector('.connector-info');
1891
+ const connectorToggle = document.querySelector('.connector-info-toggle');
1892
+ if (connectorToggle) {
1893
+ connectorToggle.addEventListener('click', () => {
1894
+ connectorInfo.classList.toggle('expanded');
1895
+ });
1896
+ }
1897
+
1898
+ // Format JSON for display
1899
+ function formatJson(data) {
1900
+ if (data === null || data === undefined) return '(no data)';
1901
+ try {
1902
+ return JSON.stringify(data, null, 2);
1903
+ } catch {
1904
+ return String(data);
1905
+ }
1906
+ }
1907
+
1908
+ // Escape HTML
1909
+ function escapeHtml(text) {
1910
+ const div = document.createElement('div');
1911
+ div.textContent = text;
1912
+ return div.innerHTML;
1913
+ }
1914
+
1915
+ // Format bytes
1916
+ function formatBytes(bytes) {
1917
+ if (bytes === 0) return '0 B';
1918
+ const k = 1024;
1919
+ const sizes = ['B', 'KB', 'MB', 'GB'];
1920
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
1921
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
1922
+ }
1923
+
1924
+ // Show session detail
1925
+ function showSession(sessionId) {
1926
+ if (currentSessionId === sessionId) return;
1927
+ currentSessionId = sessionId;
1928
+ currentRpcIdx = null;
1929
+ currentEventIdx = null;
1930
+ currentViewMode = 'rpc'; // Reset to RPC view
1931
+
1932
+ // Update session list selection
1933
+ document.querySelectorAll('.session-item').forEach(item => {
1934
+ item.classList.toggle('selected', item.dataset.sessionId === sessionId);
1935
+ });
1936
+
1937
+ // Hide all session contents, show selected
1938
+ document.querySelectorAll('.session-content').forEach(content => {
1939
+ content.classList.toggle('active', content.dataset.sessionId === sessionId);
1940
+ });
1941
+
1942
+ // Select first RPC in the newly shown session
1943
+ const sessionContent = document.querySelector('.session-content[data-session-id="' + sessionId + '"]');
1944
+ if (sessionContent) {
1945
+ const firstRpcRow = sessionContent.querySelector('.rpc-row');
1946
+ if (firstRpcRow) {
1947
+ const idx = parseInt(firstRpcRow.dataset.rpcIdx);
1948
+ showRpcDetail(sessionId, idx);
1949
+ }
1950
+ }
1951
+ }
1952
+
1953
+ // Show RPC detail in right pane using pre-rendered HTML
1954
+ // This approach avoids JavaScript string concatenation issues with special characters
1955
+ function showRpcDetail(sessionId, idx) {
1956
+ const report = sessionReports[sessionId];
1957
+ if (!report || idx < 0 || idx >= report.rpcs.length) return;
1958
+
1959
+ const sessionContent = document.querySelector('.session-content[data-session-id="' + sessionId + '"]');
1960
+ if (!sessionContent) return;
1961
+
1962
+ const rightPane = sessionContent.querySelector('.right-pane');
1963
+ if (!rightPane) return;
1964
+
1965
+ // Update RPC row selection
1966
+ sessionContent.querySelectorAll('.rpc-row').forEach((r, i) => {
1967
+ r.classList.toggle('selected', i === idx);
1968
+ });
1969
+ currentRpcIdx = idx;
1970
+
1971
+ // Hide placeholder, show details container
1972
+ const placeholder = rightPane.querySelector('.detail-placeholder');
1973
+ const detailsContainer = rightPane.querySelector('.rpc-details-container');
1974
+ if (placeholder) placeholder.style.display = 'none';
1975
+ if (detailsContainer) detailsContainer.style.display = 'block';
1976
+
1977
+ // Hide all RPC detail divs, show selected one
1978
+ const allDetails = rightPane.querySelectorAll('.rpc-detail-content');
1979
+ allDetails.forEach(function(detail) {
1980
+ detail.style.display = detail.dataset.rpcIdx === String(idx) ? 'block' : 'none';
1981
+ });
1982
+
1983
+ // Re-initialize RPC Inspector handlers for the visible detail
1984
+ if (window.initRpcInspector) {
1985
+ window.initRpcInspector();
1986
+ }
1987
+ }
1988
+
1989
+ // Copy to clipboard
1990
+ async function copyToClipboard(elementId, btn) {
1991
+ const target = document.getElementById(elementId);
1992
+ if (target) {
1993
+ try {
1994
+ await navigator.clipboard.writeText(target.textContent || '');
1995
+ const originalText = btn.textContent;
1996
+ btn.textContent = 'Copied!';
1997
+ setTimeout(() => { btn.textContent = originalText; }, 1500);
1998
+ } catch (err) {
1999
+ console.error('Copy failed:', err);
2000
+ }
2001
+ }
2002
+ }
2003
+
2004
+ // Session item click handlers
2005
+ document.querySelectorAll('.session-item').forEach(item => {
2006
+ item.addEventListener('click', () => {
2007
+ showSession(item.dataset.sessionId);
2008
+ });
2009
+ });
2010
+
2011
+ // RPC row click handlers (delegated)
2012
+ document.querySelectorAll('.session-content').forEach(content => {
2013
+ content.addEventListener('click', (e) => {
2014
+ const row = e.target.closest('.rpc-row');
2015
+ if (row) {
2016
+ const idx = parseInt(row.dataset.rpcIdx);
2017
+ showRpcDetail(content.dataset.sessionId, idx);
2018
+ }
2019
+ });
2020
+ });
2021
+
2022
+ // Keyboard navigation (handles both RPC and Events views)
2023
+ document.addEventListener('keydown', (e) => {
2024
+ if (!currentSessionId) return;
2025
+ if (e.key !== 'ArrowDown' && e.key !== 'ArrowUp') return;
2026
+
2027
+ e.preventDefault();
2028
+ const sessionContent = document.querySelector('.session-content[data-session-id="' + currentSessionId + '"]');
2029
+ if (!sessionContent) return;
2030
+
2031
+ // Check which view is active
2032
+ if (currentViewMode === 'events') {
2033
+ // Events navigation
2034
+ const eventRows = sessionContent.querySelectorAll('.event-row');
2035
+ if (eventRows.length === 0) return;
2036
+
2037
+ let newIdx = currentEventIdx;
2038
+ if (currentEventIdx === null) {
2039
+ newIdx = 0;
2040
+ } else if (e.key === 'ArrowDown' && currentEventIdx < eventRows.length - 1) {
2041
+ newIdx = currentEventIdx + 1;
2042
+ } else if (e.key === 'ArrowUp' && currentEventIdx > 0) {
2043
+ newIdx = currentEventIdx - 1;
2044
+ }
2045
+
2046
+ if (newIdx !== currentEventIdx) {
2047
+ currentEventIdx = newIdx;
2048
+ // Update selection visually
2049
+ eventRows.forEach((r, i) => r.classList.toggle('selected', i === newIdx));
2050
+ // Scroll into view
2051
+ eventRows[newIdx].scrollIntoView({ block: 'nearest' });
2052
+ // Trigger click to show detail (if has payload)
2053
+ eventRows[newIdx].click();
2054
+ }
2055
+ } else {
2056
+ // RPC navigation
2057
+ const report = sessionReports[currentSessionId];
2058
+ if (!report) return;
2059
+ const rpcs = report.rpcs;
2060
+ if (rpcs.length === 0) return;
2061
+
2062
+ if (currentRpcIdx === null && rpcs.length > 0) {
2063
+ showRpcDetail(currentSessionId, 0);
2064
+ return;
2065
+ }
2066
+ if (e.key === 'ArrowDown' && currentRpcIdx < rpcs.length - 1) {
2067
+ showRpcDetail(currentSessionId, currentRpcIdx + 1);
2068
+ } else if (e.key === 'ArrowUp' && currentRpcIdx > 0) {
2069
+ showRpcDetail(currentSessionId, currentRpcIdx - 1);
2070
+ }
2071
+ // Scroll selected row into view
2072
+ const row = sessionContent.querySelector('.rpc-row.selected');
2073
+ if (row) row.scrollIntoView({ block: 'nearest' });
2074
+ }
2075
+ });
2076
+
2077
+ // Resize handle for inner left pane
2078
+ document.querySelectorAll('.session-content').forEach(content => {
2079
+ const resizeHandle = content.querySelector('.resize-handle');
2080
+ const leftPane = content.querySelector('.left-pane');
2081
+ if (resizeHandle && leftPane) {
2082
+ let startX, startWidth;
2083
+
2084
+ resizeHandle.addEventListener('mousedown', (e) => {
2085
+ startX = e.clientX;
2086
+ startWidth = leftPane.offsetWidth;
2087
+ document.addEventListener('mousemove', onMouseMove);
2088
+ document.addEventListener('mouseup', onMouseUp);
2089
+ e.preventDefault();
2090
+ });
2091
+
2092
+ function onMouseMove(e) {
2093
+ const diff = e.clientX - startX;
2094
+ const newWidth = Math.max(300, Math.min(600, startWidth + diff));
2095
+ leftPane.style.width = newWidth + 'px';
2096
+ }
2097
+
2098
+ function onMouseUp() {
2099
+ document.removeEventListener('mousemove', onMouseMove);
2100
+ document.removeEventListener('mouseup', onMouseUp);
2101
+ }
2102
+ }
2103
+ });
2104
+
2105
+ // Check for session parameter in URL
2106
+ function getSessionFromUrl() {
2107
+ const params = new URLSearchParams(window.location.search);
2108
+ return params.get('session');
2109
+ }
2110
+
2111
+ // Scroll session item into view
2112
+ function scrollSessionIntoView(sessionId) {
2113
+ const sessionItem = document.querySelector('.session-item[data-session-id="' + sessionId + '"]');
2114
+ if (sessionItem) {
2115
+ sessionItem.scrollIntoView({ block: 'center', behavior: 'smooth' });
2116
+ // Add highlight effect
2117
+ sessionItem.classList.add('highlight');
2118
+ setTimeout(() => sessionItem.classList.remove('highlight'), 2000);
2119
+ }
2120
+ }
2121
+
2122
+ // Select session from URL or first session by default
2123
+ const urlSession = getSessionFromUrl();
2124
+ if (urlSession) {
2125
+ // Try to find matching session (full or partial match)
2126
+ const matchingSession = sessions.find(s =>
2127
+ s.session_id === urlSession || s.session_id.startsWith(urlSession)
2128
+ );
2129
+ if (matchingSession) {
2130
+ showSession(matchingSession.session_id);
2131
+ // Scroll into view after a short delay to ensure DOM is ready
2132
+ setTimeout(() => scrollSessionIntoView(matchingSession.session_id), 100);
2133
+ } else if (sessions.length > 0) {
2134
+ showSession(sessions[0].session_id);
2135
+ }
2136
+ } else if (sessions.length > 0) {
2137
+ showSession(sessions[0].session_id);
2138
+ }
2139
+
2140
+ // Events View toggle and data loading (Issue #59)
2141
+ (function() {
2142
+ // Cache for loaded events
2143
+ const eventsCache = {};
2144
+
2145
+ // Event kind display labels
2146
+ const kindLabels = {
2147
+ request: 'REQ',
2148
+ response: 'RES',
2149
+ notification: 'NOTIF',
2150
+ transport_event: 'TRANS'
2151
+ };
2152
+
2153
+ // Format time for events table
2154
+ function formatEventTime(ts) {
2155
+ try {
2156
+ const date = new Date(ts);
2157
+ return date.toISOString().split('T')[1].slice(0, 12);
2158
+ } catch {
2159
+ return ts;
2160
+ }
2161
+ }
2162
+
2163
+ // Render events table
2164
+ function renderEventsTable(events) {
2165
+ if (!events || events.length === 0) {
2166
+ return '<div class="events-loading">No events in this session</div>';
2167
+ }
2168
+
2169
+ const rows = events.map(function(event, idx) {
2170
+ const dirClass = event.direction === 'client_to_server' ? 'outgoing' : 'incoming';
2171
+ // Large arrows with tooltip: ⇨ (blue) = Client→Server, ⇦ (green) = Server→Client
2172
+ const dirArrow = event.direction === 'client_to_server' ? '\\u21E8' : '\\u21E6';
2173
+ const dirTooltip = event.direction === 'client_to_server'
2174
+ ? 'Client \\u2192 Server'
2175
+ : 'Server \\u2192 Client';
2176
+ const kindClass = 'badge-kind-' + event.kind;
2177
+ const kindLabel = kindLabels[event.kind] || event.kind;
2178
+ // Method/Summary fallback: method > summary > payload_type (e.g., "connected")
2179
+ const method = event.method || event.summary || event.payload_type || '';
2180
+ const timeStr = formatEventTime(event.ts);
2181
+ const hasPayload = event.has_payload ? '\\u2713' : '';
2182
+
2183
+ return '<tr class="event-row" data-event-idx="' + idx + '" data-event-id="' + escapeHtml(event.event_id) + '">' +
2184
+ '<td>' + timeStr + '</td>' +
2185
+ '<td><span class="direction-arrow ' + dirClass + '" title="' + dirTooltip + '">' + dirArrow + '</span></td>' +
2186
+ '<td><span class="badge ' + kindClass + '">' + kindLabel + '</span></td>' +
2187
+ '<td>' + escapeHtml(method) + '</td>' +
2188
+ '<td>' + hasPayload + '</td>' +
2189
+ '</tr>';
2190
+ }).join('');
2191
+
2192
+ return '<table class="events-table">' +
2193
+ '<thead><tr>' +
2194
+ '<th>Time</th>' +
2195
+ '<th>Dir</th>' +
2196
+ '<th>Kind</th>' +
2197
+ '<th>Method/Summary</th>' +
2198
+ '<th>Data</th>' +
2199
+ '</tr></thead>' +
2200
+ '<tbody>' + rows + '</tbody>' +
2201
+ '</table>';
2202
+ }
2203
+
2204
+ // Load events for a session
2205
+ function loadEvents(sessionId, eventsList) {
2206
+ if (eventsCache[sessionId]) {
2207
+ eventsList.innerHTML = renderEventsTable(eventsCache[sessionId]);
2208
+ // Attach click handlers even when using cached data
2209
+ attachEventRowHandlers(eventsList, sessionId, eventsCache[sessionId]);
2210
+ return;
2211
+ }
2212
+
2213
+ // Check if we're in offline mode (static HTML) or live server
2214
+ // Try to fetch from API, fallback to "no data" message
2215
+ eventsList.innerHTML = '<div class="events-loading">Loading events...</div>';
2216
+
2217
+ fetch('/api/sessions/' + encodeURIComponent(sessionId) + '/events')
2218
+ .then(function(res) {
2219
+ if (!res.ok) throw new Error('API not available');
2220
+ return res.json();
2221
+ })
2222
+ .then(function(data) {
2223
+ eventsCache[sessionId] = data.events;
2224
+ eventsList.innerHTML = renderEventsTable(data.events);
2225
+ // Attach click handlers for event rows
2226
+ attachEventRowHandlers(eventsList, sessionId, data.events);
2227
+ })
2228
+ .catch(function() {
2229
+ eventsList.innerHTML = '<div class="events-loading">Events data not available (API offline)</div>';
2230
+ });
2231
+ }
2232
+
2233
+ // Attach click handlers for event rows
2234
+ function attachEventRowHandlers(eventsList, sessionId, events) {
2235
+ eventsList.querySelectorAll('.event-row').forEach(function(row) {
2236
+ row.addEventListener('click', function() {
2237
+ const idx = parseInt(row.dataset.eventIdx);
2238
+ const event = events[idx];
2239
+ if (!event || !event.has_payload) return;
2240
+
2241
+ // Clear previous selection
2242
+ eventsList.querySelectorAll('.event-row').forEach(function(r) {
2243
+ r.classList.remove('selected');
2244
+ });
2245
+ row.classList.add('selected');
2246
+
2247
+ // Show event detail in right pane
2248
+ showEventDetail(sessionId, event);
2249
+ });
2250
+ });
2251
+ }
2252
+
2253
+ // Show event detail in right pane (2-column layout like RPC Inspector)
2254
+ function showEventDetail(sessionId, event) {
2255
+ const sessionContent = document.querySelector('.session-content[data-session-id="' + sessionId + '"]');
2256
+ if (!sessionContent) return;
2257
+
2258
+ const rightPane = sessionContent.querySelector('.right-pane');
2259
+ if (!rightPane) return;
2260
+
2261
+ // Fetch full event detail
2262
+ fetch('/api/events/' + encodeURIComponent(event.event_id))
2263
+ .then(function(res) {
2264
+ if (!res.ok) throw new Error('Event not found');
2265
+ return res.json();
2266
+ })
2267
+ .then(function(data) {
2268
+ const evt = data.event;
2269
+ const kindClass = 'badge-kind-' + evt.kind;
2270
+ const dirClass = evt.direction === 'client_to_server' ? 'outgoing' : 'incoming';
2271
+ // Large arrows with tooltip: ⇨ (blue) = Client→Server, ⇦ (green) = Server→Client
2272
+ const dirArrow = evt.direction === 'client_to_server' ? '\\u21E8' : '\\u21E6';
2273
+ const dirTooltip = evt.direction === 'client_to_server'
2274
+ ? 'Client \\u2192 Server'
2275
+ : 'Server \\u2192 Client';
2276
+ const method = evt.method || evt.summary || '(unknown)';
2277
+ const rawJson = evt.raw_json ? JSON.parse(evt.raw_json) : null;
2278
+ const formattedJson = rawJson ? JSON.stringify(rawJson, null, 2) : '(no data)';
2279
+
2280
+ // Build summary section
2281
+ var summaryHtml = '<div class="summary-row summary-header">Event Info</div>';
2282
+ 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>';
2283
+ 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>';
2284
+ summaryHtml += '<div class="summary-row summary-property"><span class="summary-prop-name">Method</span><span class="summary-prop-value">' + escapeHtml(method) + '</span></div>';
2285
+ summaryHtml += '<div class="summary-row summary-property"><span class="summary-prop-name">Timestamp</span><span class="summary-prop-value">' + escapeHtml(evt.ts) + '</span></div>';
2286
+ if (evt.seq !== null) {
2287
+ summaryHtml += '<div class="summary-row summary-property"><span class="summary-prop-name">Sequence</span><span class="summary-prop-value">' + evt.seq + '</span></div>';
2288
+ }
2289
+
2290
+ // If JSON has recognizable structure, add more summary
2291
+ if (rawJson) {
2292
+ if (rawJson.method) {
2293
+ summaryHtml += '<div class="summary-row summary-header" style="margin-top: 12px;">JSON-RPC</div>';
2294
+ summaryHtml += '<div class="summary-row summary-property"><span class="summary-prop-name">method</span><span class="summary-prop-value">' + escapeHtml(rawJson.method) + '</span></div>';
2295
+ }
2296
+ if (rawJson.id !== undefined) {
2297
+ 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>';
2298
+ }
2299
+ if (rawJson.error) {
2300
+ 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>';
2301
+ }
2302
+ }
2303
+
2304
+ // 2-column layout: Summary (left) + Raw JSON (right)
2305
+ rightPane.innerHTML =
2306
+ '<div class="rpc-inspector">' +
2307
+ '<div class="rpc-inspector-summary">' +
2308
+ '<div class="summary-container">' + summaryHtml + '</div>' +
2309
+ '</div>' +
2310
+ '<div class="rpc-inspector-raw">' +
2311
+ '<div class="rpc-raw-header">' +
2312
+ '<span class="rpc-raw-title">Payload</span>' +
2313
+ '</div>' +
2314
+ '<div class="rpc-raw-json"><pre><code>' + escapeHtml(formattedJson) + '</code></pre></div>' +
2315
+ '</div>' +
2316
+ '</div>';
2317
+ })
2318
+ .catch(function() {
2319
+ rightPane.innerHTML = '<div class="detail-placeholder">Failed to load event detail</div>';
2320
+ });
2321
+ }
2322
+
2323
+ // Handle view toggle clicks
2324
+ document.querySelectorAll('.view-toggle').forEach(function(toggle) {
2325
+ const sessionContent = toggle.closest('.session-content');
2326
+ if (!sessionContent) return;
2327
+
2328
+ const sessionId = sessionContent.dataset.sessionId;
2329
+ const rpcList = sessionContent.querySelector('.rpc-list');
2330
+ const eventsList = sessionContent.querySelector('.events-list');
2331
+ const buttons = toggle.querySelectorAll('.view-toggle-btn');
2332
+
2333
+ buttons.forEach(function(btn) {
2334
+ btn.addEventListener('click', function() {
2335
+ const view = btn.dataset.view;
2336
+
2337
+ // Update current view mode
2338
+ currentViewMode = view;
2339
+
2340
+ // Update button states
2341
+ buttons.forEach(function(b) {
2342
+ b.classList.toggle('active', b.dataset.view === view);
2343
+ });
2344
+
2345
+ // Toggle lists
2346
+ if (view === 'events') {
2347
+ rpcList.classList.add('hidden');
2348
+ eventsList.classList.add('active');
2349
+ loadEvents(sessionId, eventsList);
2350
+ } else {
2351
+ rpcList.classList.remove('hidden');
2352
+ eventsList.classList.remove('active');
2353
+ // Re-show selected RPC detail when switching back to RPCs view
2354
+ if (currentRpcIdx !== null && sessionId === currentSessionId) {
2355
+ showRpcDetail(sessionId, currentRpcIdx);
2356
+ }
2357
+ }
2358
+ });
2359
+ });
2360
+ });
2361
+ })();
2362
+
2363
+ // RPC Inspector script
2364
+ ${getRpcInspectorScript()}
2365
+ `;
2366
+ }
2367
+
2368
+ // ============================================================================
2369
+ // Analytics Panel Rendering (Phase 5.2)
2370
+ // ============================================================================
2371
+
2372
+ /**
2373
+ * Render KPI stats row for header (inline compact display)
2374
+ */
2375
+ function renderKpiRow(kpis: HtmlConnectorKpis): string {
2376
+ // Format large numbers with K/M suffix
2377
+ const formatNumber = (n: number): string => {
2378
+ if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
2379
+ if (n >= 1000) return (n / 1000).toFixed(1) + 'K';
2380
+ return String(n);
2381
+ };
2382
+
2383
+ // Format bytes for display
2384
+ const formatBytesCompact = (bytes: number): string => {
2385
+ if (bytes === 0) return '0';
2386
+ const k = 1024;
2387
+ const sizes = ['B', 'KB', 'MB', 'GB'];
2388
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
2389
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + sizes[i];
2390
+ };
2391
+
2392
+ return `
2393
+ <div class="kpi-row">
2394
+ <div class="kpi-item">
2395
+ <div class="kpi-value">${formatNumber(kpis.sessions_displayed)}</div>
2396
+ <div class="kpi-label">Sessions</div>
2397
+ </div>
2398
+ <div class="kpi-item">
2399
+ <div class="kpi-value">${formatNumber(kpis.rpc_total)}</div>
2400
+ <div class="kpi-label">RPCs</div>
2401
+ </div>
2402
+ <div class="kpi-item">
2403
+ <div class="kpi-value">${formatNumber(kpis.rpc_err)}</div>
2404
+ <div class="kpi-label">Error</div>
2405
+ </div>
2406
+ <div class="kpi-item">
2407
+ <div class="kpi-value">${kpis.avg_latency_ms !== null ? kpis.avg_latency_ms : '-'}</div>
2408
+ <div class="kpi-label">Avg Latency</div>
2409
+ </div>
2410
+ <div class="kpi-item">
2411
+ <div class="kpi-value">${kpis.p95_latency_ms !== null ? kpis.p95_latency_ms : '-'}</div>
2412
+ <div class="kpi-label">P95 Latency</div>
2413
+ </div>
2414
+ <div class="kpi-item">
2415
+ <div class="kpi-value">${formatBytesCompact(kpis.total_request_bytes)}</div>
2416
+ <div class="kpi-label">Req Size</div>
2417
+ </div>
2418
+ <div class="kpi-item">
2419
+ <div class="kpi-value">${formatBytesCompact(kpis.total_response_bytes)}</div>
2420
+ <div class="kpi-label">Res Size</div>
2421
+ </div>
2422
+ </div>`;
2423
+ }
2424
+
2425
+ /**
2426
+ * Get intensity level (0-4) for heatmap cell based on count and max
2427
+ */
2428
+ function getHeatmapLevel(count: number, maxCount: number): number {
2429
+ if (count === 0 || maxCount === 0) return 0;
2430
+ const ratio = count / maxCount;
2431
+ if (ratio <= 0.25) return 1;
2432
+ if (ratio <= 0.5) return 2;
2433
+ if (ratio <= 0.75) return 3;
2434
+ return 4;
2435
+ }
2436
+
2437
+ /**
2438
+ * Render activity heatmap (GitHub contributions style, SVG)
2439
+ */
2440
+ export function renderHeatmap(heatmap: HtmlHeatmapData): string {
2441
+ const cellSize = 10;
2442
+ const cellGap = 2;
2443
+ const cellTotal = cellSize + cellGap;
2444
+
2445
+ // Group cells by week (7 days per column)
2446
+ const weeks: Array<typeof heatmap.cells> = [];
2447
+ let currentWeek: typeof heatmap.cells = [];
2448
+
2449
+ // Find the day of week for the start date (0 = Sunday)
2450
+ const startDow = new Date(heatmap.start_date + 'T00:00:00Z').getUTCDay();
2451
+
2452
+ // Add empty cells for days before start_date
2453
+ for (let i = 0; i < startDow; i++) {
2454
+ currentWeek.push({ date: '', count: -1 }); // -1 indicates empty
2455
+ }
2456
+
2457
+ for (const cell of heatmap.cells) {
2458
+ currentWeek.push(cell);
2459
+ if (currentWeek.length === 7) {
2460
+ weeks.push(currentWeek);
2461
+ currentWeek = [];
2462
+ }
2463
+ }
2464
+ if (currentWeek.length > 0) {
2465
+ weeks.push(currentWeek);
2466
+ }
2467
+
2468
+ // Calculate SVG dimensions
2469
+ const svgWidth = weeks.length * cellTotal;
2470
+ const svgHeight = 7 * cellTotal;
2471
+
2472
+ // Generate SVG rects
2473
+ let rects = '';
2474
+ weeks.forEach((week, weekIdx) => {
2475
+ week.forEach((cell, dayIdx) => {
2476
+ if (cell.count < 0) return; // Skip empty cells
2477
+ const level = getHeatmapLevel(cell.count, heatmap.max_count);
2478
+ const x = weekIdx * cellTotal;
2479
+ const y = dayIdx * cellTotal;
2480
+ const title = cell.date ? `${cell.date}: ${cell.count} RPCs` : '';
2481
+ rects += `<rect x="${x}" y="${y}" width="${cellSize}" height="${cellSize}" rx="2" class="heatmap-level-${level}"><title>${escapeHtml(title)}</title></rect>`;
2482
+ });
2483
+ });
2484
+
2485
+ return `
2486
+ <div class="heatmap-container">
2487
+ <div class="heatmap-title">Activity<br><span class="heatmap-range">${escapeHtml(heatmap.start_date)} to ${escapeHtml(heatmap.end_date)}</span></div>
2488
+ <svg width="${svgWidth}" height="${svgHeight}" viewBox="0 0 ${svgWidth} ${svgHeight}">
2489
+ ${rects}
2490
+ </svg>
2491
+ </div>`;
2492
+ }
2493
+
2494
+ /**
2495
+ * Render method-based latency heatmap (Activity-style)
2496
+ * Rows: method names
2497
+ * Columns: latency buckets (log scale)
2498
+ * Cell color: intensity based on count
2499
+ */
2500
+ function renderMethodLatencyChart(methodLatency: HtmlMethodLatencyData): string {
2501
+ if (methodLatency.sample_size === 0 || methodLatency.methods.length === 0) {
2502
+ return `
2503
+ <div class="latency-histogram">
2504
+ <div class="chart-title">Latency by Method</div>
2505
+ <div class="no-data-message">No latency data</div>
2506
+ </div>`;
2507
+ }
2508
+
2509
+ // Latency buckets (log scale thresholds)
2510
+ const buckets = [
2511
+ { label: '<10ms', max: 10 },
2512
+ { label: '<100ms', max: 100 },
2513
+ { label: '<1s', max: 1000 },
2514
+ { label: '<5s', max: 5000 },
2515
+ { label: '5s+', max: Infinity },
2516
+ ];
2517
+
2518
+ // Group latencies into buckets for each method
2519
+ const methods = methodLatency.methods;
2520
+
2521
+ // Find global max count for intensity scaling
2522
+ let globalMaxCount = 0;
2523
+ const methodBucketCounts: Map<string, number[]> = new Map();
2524
+
2525
+ for (const method of methods) {
2526
+ const counts = buckets.map(() => 0);
2527
+ for (const latency of method.latencies) {
2528
+ for (let i = 0; i < buckets.length; i++) {
2529
+ if (i === 0 && latency < buckets[i].max) {
2530
+ counts[i]++;
2531
+ break;
2532
+ } else if (i > 0 && latency >= buckets[i - 1].max && latency < buckets[i].max) {
2533
+ counts[i]++;
2534
+ break;
2535
+ } else if (i === buckets.length - 1 && latency >= buckets[i - 1].max) {
2536
+ counts[i]++;
2537
+ break;
2538
+ }
2539
+ }
2540
+ }
2541
+ methodBucketCounts.set(method.method, counts);
2542
+ globalMaxCount = Math.max(globalMaxCount, ...counts);
2543
+ }
2544
+
2545
+ // Get heatmap level (0-4)
2546
+ const getLevel = (count: number): number => {
2547
+ if (count === 0) return 0;
2548
+ if (globalMaxCount === 0) return 0;
2549
+ const ratio = count / globalMaxCount;
2550
+ if (ratio <= 0.25) return 1;
2551
+ if (ratio <= 0.5) return 2;
2552
+ if (ratio <= 0.75) return 3;
2553
+ return 4;
2554
+ };
2555
+
2556
+ // Build HTML rows
2557
+ let rows = '';
2558
+ for (const method of methods) {
2559
+ const shortName = shortenMethodName(method.method);
2560
+ const counts = methodBucketCounts.get(method.method) || [];
2561
+
2562
+ let cells = '';
2563
+ for (let i = 0; i < buckets.length; i++) {
2564
+ const count = counts[i];
2565
+ const level = getLevel(count);
2566
+ const tooltip = `${method.method}\n${buckets[i].label}: ${count} calls`;
2567
+ cells += `<div class="latency-heatmap-cell heatmap-level-${level}" title="${escapeHtml(tooltip)}"></div>`;
2568
+ }
2569
+
2570
+ rows += `
2571
+ <div class="latency-heatmap-row">
2572
+ <div class="latency-heatmap-method">${escapeHtml(shortName)}</div>
2573
+ <div class="latency-heatmap-cells">${cells}</div>
2574
+ </div>`;
2575
+ }
2576
+
2577
+ // Header row with bucket labels
2578
+ let headerCells = '';
2579
+ for (const bucket of buckets) {
2580
+ headerCells += `<div class="latency-heatmap-header-cell">${bucket.label}</div>`;
2581
+ }
2582
+
2583
+ return `
2584
+ <div class="latency-histogram">
2585
+ <div class="chart-title">Latency by Method (${methodLatency.sample_size} samples)</div>
2586
+ <div class="latency-heatmap">
2587
+ <div class="latency-heatmap-header">
2588
+ <div class="latency-heatmap-method"></div>
2589
+ <div class="latency-heatmap-cells">${headerCells}</div>
2590
+ </div>
2591
+ ${rows}
2592
+ </div>
2593
+ </div>`;
2594
+ }
2595
+
2596
+ /**
2597
+ * Shorten method name for display (e.g., "tools/list" -> "list", "initialize" -> "init")
2598
+ */
2599
+ function shortenMethodName(method: string): string {
2600
+ // Remove prefix
2601
+ if (method.startsWith('tools/')) {
2602
+ return method.slice(6);
2603
+ }
2604
+ if (method.startsWith('resources/')) {
2605
+ return 'r/' + method.slice(10);
2606
+ }
2607
+ if (method.startsWith('prompts/')) {
2608
+ return 'p/' + method.slice(8);
2609
+ }
2610
+ if (method === 'initialize') {
2611
+ return 'init';
2612
+ }
2613
+ if (method === 'notifications/initialized') {
2614
+ return 'n/init';
2615
+ }
2616
+ // Truncate if too long
2617
+ if (method.length > 6) {
2618
+ return method.slice(0, 5) + '…';
2619
+ }
2620
+ return method;
2621
+ }
2622
+
2623
+ /**
2624
+ * Render top 5 tools
2625
+ */
2626
+ function renderTopTools(topTools: HtmlTopToolsData): string {
2627
+ if (topTools.items.length === 0) {
2628
+ return `
2629
+ <div class="top-tools">
2630
+ <div class="chart-title">Top Tools</div>
2631
+ <div class="no-data-message">No tool calls</div>
2632
+ </div>`;
2633
+ }
2634
+
2635
+ const rows = topTools.items
2636
+ .map((tool, idx) => {
2637
+ return `
2638
+ <div class="top-tool-row" data-tool-name="${escapeHtml(tool.name)}">
2639
+ <span class="top-tool-rank">${idx + 1}.</span>
2640
+ <span class="top-tool-name" title="${escapeHtml(tool.name)}">${escapeHtml(tool.name)}</span>
2641
+ <div class="top-tool-bar-container">
2642
+ <div class="top-tool-bar" style="width: ${tool.pct}%"></div>
2643
+ </div>
2644
+ <span class="top-tool-pct">${tool.pct}%</span>
2645
+ </div>`;
2646
+ })
2647
+ .join('');
2648
+
2649
+ return `
2650
+ <div class="top-tools">
2651
+ <div class="chart-title">Top Tools (${topTools.total_calls} calls)</div>
2652
+ ${rows}
2653
+ </div>`;
2654
+ }
2655
+
2656
+ /**
2657
+ * Donut chart colors (blue gradient palette)
2658
+ */
2659
+ const DONUT_COLORS = [
2660
+ '#00d4ff', // Neon blue (brightest)
2661
+ '#0097b2', // Medium bright blue
2662
+ '#0d5c73', // Medium blue
2663
+ '#0a4d5c', // Darker blue
2664
+ '#083d47', // Dark blue
2665
+ '#5a6a70', // Blue-gray (for "Others")
2666
+ ];
2667
+
2668
+ /**
2669
+ * Render method distribution donut chart (SVG)
2670
+ */
2671
+ export function renderMethodDistribution(methodDist: HtmlMethodDistribution): string {
2672
+ if (methodDist.slices.length === 0) {
2673
+ return `
2674
+ <div class="method-distribution">
2675
+ <div class="chart-title">Method Distribution</div>
2676
+ <div class="no-data-message">No RPCs</div>
2677
+ </div>`;
2678
+ }
2679
+
2680
+ // SVG donut chart parameters
2681
+ const size = 60;
2682
+ const cx = size / 2;
2683
+ const cy = size / 2;
2684
+ const outerRadius = 26;
2685
+ const innerRadius = 16; // Creates the donut hole
2686
+
2687
+ // Generate SVG path segments
2688
+ let paths = '';
2689
+ let currentAngle = -90; // Start from top (12 o'clock)
2690
+
2691
+ methodDist.slices.forEach((slice, idx) => {
2692
+ const angle = (slice.pct / 100) * 360;
2693
+ const startAngle = currentAngle;
2694
+ const endAngle = currentAngle + angle;
2695
+
2696
+ // Convert angles to radians
2697
+ const startRad = (startAngle * Math.PI) / 180;
2698
+ const endRad = (endAngle * Math.PI) / 180;
2699
+
2700
+ // Calculate arc points for outer radius
2701
+ const x1Outer = cx + outerRadius * Math.cos(startRad);
2702
+ const y1Outer = cy + outerRadius * Math.sin(startRad);
2703
+ const x2Outer = cx + outerRadius * Math.cos(endRad);
2704
+ const y2Outer = cy + outerRadius * Math.sin(endRad);
2705
+
2706
+ // Calculate arc points for inner radius
2707
+ const x1Inner = cx + innerRadius * Math.cos(endRad);
2708
+ const y1Inner = cy + innerRadius * Math.sin(endRad);
2709
+ const x2Inner = cx + innerRadius * Math.cos(startRad);
2710
+ const y2Inner = cy + innerRadius * Math.sin(startRad);
2711
+
2712
+ // Large arc flag
2713
+ const largeArc = angle > 180 ? 1 : 0;
2714
+
2715
+ // Color
2716
+ const color = DONUT_COLORS[idx % DONUT_COLORS.length];
2717
+
2718
+ // SVG path for donut segment
2719
+ const d = [
2720
+ `M ${x1Outer} ${y1Outer}`, // Start at outer edge
2721
+ `A ${outerRadius} ${outerRadius} 0 ${largeArc} 1 ${x2Outer} ${y2Outer}`, // Outer arc
2722
+ `L ${x1Inner} ${y1Inner}`, // Line to inner edge
2723
+ `A ${innerRadius} ${innerRadius} 0 ${largeArc} 0 ${x2Inner} ${y2Inner}`, // Inner arc (reverse)
2724
+ 'Z', // Close path
2725
+ ].join(' ');
2726
+
2727
+ const title = `${slice.method}: ${slice.count} (${slice.pct}%)`;
2728
+ paths += `<path d="${d}" fill="${color}"><title>${escapeHtml(title)}</title></path>`;
2729
+
2730
+ currentAngle = endAngle;
2731
+ });
2732
+
2733
+ // Generate legend
2734
+ const legendItems = methodDist.slices
2735
+ .map((slice, idx) => {
2736
+ const color = DONUT_COLORS[idx % DONUT_COLORS.length];
2737
+ return `
2738
+ <div class="donut-legend-item">
2739
+ <div class="donut-legend-color" style="background: ${color}"></div>
2740
+ <span class="donut-legend-label" title="${escapeHtml(slice.method)}">${escapeHtml(slice.method)}</span>
2741
+ <span class="donut-legend-pct">${slice.pct}%</span>
2742
+ </div>`;
2743
+ })
2744
+ .join('');
2745
+
2746
+ return `
2747
+ <div class="method-distribution">
2748
+ <div class="chart-title">Methods (${methodDist.total_rpcs} RPCs)</div>
2749
+ <div class="donut-container">
2750
+ <svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}">
2751
+ ${paths}
2752
+ </svg>
2753
+ <div class="donut-legend">
2754
+ ${legendItems}
2755
+ </div>
2756
+ </div>
2757
+ </div>`;
2758
+ }
2759
+
2760
+ /**
2761
+ * Render the analytics panel (4 charts horizontally)
2762
+ */
2763
+ function renderAnalyticsPanel(analytics: HtmlConnectorAnalyticsV1): string {
2764
+ return `
2765
+ <div class="analytics-panel">
2766
+ ${renderHeatmap(analytics.heatmap)}
2767
+ ${renderMethodLatencyChart(analytics.method_latency)}
2768
+ ${renderTopTools(analytics.top_tools)}
2769
+ ${renderMethodDistribution(analytics.method_distribution)}
2770
+ </div>`;
2771
+ }
2772
+
2773
+ /**
2774
+ * Render a session item for the sessions pane (compact grid view)
2775
+ */
2776
+ function renderConnectorSessionItem(session: HtmlConnectorSessionRow): string {
2777
+ // Format timestamp compactly: MM/DD HH:MM
2778
+ const timestamp = formatCompactTimestamp(session.started_at);
2779
+ const latencyStr = session.total_latency_ms !== null
2780
+ ? `${session.total_latency_ms}ms`
2781
+ : '-';
2782
+
2783
+ return `
2784
+ <div class="session-item"
2785
+ data-session-id="${escapeHtml(session.session_id)}"
2786
+ 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}">
2787
+ <span class="session-id">[${escapeHtml(session.short_id)}]</span>
2788
+ <span class="session-timestamp">${timestamp}</span>
2789
+ <span class="session-counts"><span>R:${session.rpc_count}</span><span>E:${session.event_count}</span></span>
2790
+ <span class="session-latency">${latencyStr}</span>
2791
+ <span class="session-extra"></span>
2792
+ </div>`;
2793
+ }
2794
+
2795
+ /**
2796
+ * Format timestamp compactly for grid display (UTC)
2797
+ * Returns format: HH:MM:SS.mmm (time with milliseconds)
2798
+ * @public - exported for testing
2799
+ */
2800
+ export function formatCompactTimestamp(isoStr: string): string {
2801
+ try {
2802
+ const date = new Date(isoStr);
2803
+ if (isNaN(date.getTime())) {
2804
+ return '-';
2805
+ }
2806
+ const hours = String(date.getUTCHours()).padStart(2, '0');
2807
+ const minutes = String(date.getUTCMinutes()).padStart(2, '0');
2808
+ const seconds = String(date.getUTCSeconds()).padStart(2, '0');
2809
+ const millis = String(date.getUTCMilliseconds()).padStart(3, '0');
2810
+ return `${hours}:${minutes}:${seconds}.${millis}`;
2811
+ } catch {
2812
+ return '-';
2813
+ }
2814
+ }
2815
+
2816
+ /**
2817
+ * Render session detail content (reuses session HTML layout)
2818
+ */
2819
+ /**
2820
+ * Render a single RPC detail HTML (pre-rendered for direct DOM insertion)
2821
+ * This avoids JavaScript string concatenation issues with special characters
2822
+ */
2823
+ function renderRpcDetailHtml(rpc: SessionRpcDetail, idx: number): string {
2824
+ const statusClass = `status-${rpc.status}`;
2825
+ const statusSymbol = getStatusSymbol(rpc.status);
2826
+ const latency = rpc.latency_ms !== null ? `${rpc.latency_ms}ms` : '(pending)';
2827
+
2828
+ // Pre-render summary and raw JSON HTML
2829
+ const requestSummaryRows = renderRequestSummary(rpc.method, rpc.request.json);
2830
+ const responseSummaryRows = renderResponseSummary(rpc.method, rpc.response.json);
2831
+ const requestSummaryHtml = renderSummaryRowsHtml(requestSummaryRows);
2832
+ const responseSummaryHtml = renderSummaryRowsHtml(responseSummaryRows);
2833
+ const requestRawHtml = renderJsonWithPaths(rpc.request.json, '#');
2834
+ const responseRawHtml = renderJsonWithPaths(rpc.response.json, '#');
2835
+
2836
+ // Detect sensitive keys
2837
+ const reqSensitiveKeys = detectSensitiveKeys(rpc.request.json);
2838
+ const resSensitiveKeys = detectSensitiveKeys(rpc.response.json);
2839
+ const allSensitiveKeys = [...reqSensitiveKeys, ...resSensitiveKeys];
2840
+ const hasSensitive = allSensitiveKeys.length > 0;
2841
+
2842
+ // Sensitive badge
2843
+ let sensitiveBadge = '';
2844
+ if (hasSensitive) {
2845
+ const escapedKeys = allSensitiveKeys.map(k => escapeHtml(k));
2846
+ const sensitiveTooltip = escapedKeys.length > 5
2847
+ ? `Contains ${escapedKeys.length} sensitive keys: ${escapedKeys.slice(0, 5).join(', ')}...`
2848
+ : `Contains sensitive keys: ${escapedKeys.join(', ')}`;
2849
+ sensitiveBadge = `<span class="sensitive-badge" title="${escapeHtml(sensitiveTooltip)}">⚠ Sensitive</span>`;
2850
+ }
2851
+
2852
+ // Determine default target based on method
2853
+ const defaultTarget = (rpc.method === 'tools/list' || rpc.method === 'initialize' || rpc.method.startsWith('resources/') || rpc.method.startsWith('prompts/')) ? 'response' : 'request';
2854
+
2855
+ return `<div class="rpc-detail-content" data-rpc-idx="${idx}" style="display: none;">
2856
+ <div class="detail-section">
2857
+ <h2>RPC Info${sensitiveBadge}</h2>
2858
+ <div class="rpc-info-grid">
2859
+ <div class="rpc-info-item"><dt>RPC ID</dt><dd><span class="badge">${escapeHtml(rpc.rpc_id)}</span></dd></div>
2860
+ <div class="rpc-info-item"><dt>Method</dt><dd><span class="badge">${escapeHtml(rpc.method)}</span></dd></div>
2861
+ <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>
2862
+ <div class="rpc-info-item"><dt>Latency</dt><dd><span class="badge">${latency}</span></dd></div>
2863
+ <div class="rpc-info-item"><dt>Request</dt><dd>${escapeHtml(rpc.request_ts)}</dd></div>
2864
+ <div class="rpc-info-item"><dt>Response</dt><dd>${escapeHtml(rpc.response_ts || '-')}</dd></div>
2865
+ </div>
2866
+ </div>
2867
+ <div class="detail-section">
2868
+ <div class="rpc-toggle-bar">
2869
+ <button class="rpc-toggle-btn${defaultTarget === 'request' ? ' active' : ''}" data-target="request">[Req]</button>
2870
+ <button class="rpc-toggle-btn${defaultTarget === 'response' ? ' active' : ''}" data-target="response">[Res]</button>
2871
+ </div>
2872
+ <div class="rpc-inspector">
2873
+ <div class="rpc-inspector-summary">
2874
+ <h3>Summary</h3>
2875
+ <div class="summary-request" style="display: ${defaultTarget === 'request' ? 'block' : 'none'}">${requestSummaryHtml}</div>
2876
+ <div class="summary-response" style="display: ${defaultTarget === 'response' ? 'block' : 'none'}">${responseSummaryHtml}</div>
2877
+ </div>
2878
+ <div class="rpc-inspector-raw">
2879
+ <div class="rpc-raw-json">
2880
+ <div class="raw-json-request" style="display: ${defaultTarget === 'request' ? 'block' : 'none'}">${requestRawHtml}</div>
2881
+ <div class="raw-json-response" style="display: ${defaultTarget === 'response' ? 'block' : 'none'}">${responseRawHtml}</div>
2882
+ </div>
2883
+ </div>
2884
+ </div>
2885
+ </div>
2886
+ </div>`;
2887
+ }
2888
+
2889
+ function renderSessionDetailContent(
2890
+ sessionId: string,
2891
+ report: HtmlSessionReportV1
2892
+ ): string {
2893
+ const { session, rpcs } = report;
2894
+
2895
+ const totalLatencyDisplay = session.total_latency_ms !== null
2896
+ ? `${session.total_latency_ms}ms`
2897
+ : '-';
2898
+
2899
+ const rpcRows = rpcs.map((rpc, idx) => {
2900
+ const statusClass = `status-${rpc.status}`;
2901
+ const statusSymbol = getStatusSymbol(rpc.status);
2902
+ const rpcIdShort = rpc.rpc_id.slice(0, SHORT_ID_LENGTH);
2903
+ const timeShort = formatTimestamp(rpc.request_ts).split(' ')[1]?.slice(0, 12) || '-';
2904
+ const latency = rpc.latency_ms !== null ? `${rpc.latency_ms}ms` : '-';
2905
+
2906
+ return `
2907
+ <tr class="rpc-row" data-rpc-idx="${idx}">
2908
+ <td>${timeShort}</td>
2909
+ <td><span class="badge ${statusClass}">${statusSymbol}</span></td>
2910
+ <td><span class="badge">${escapeHtml(rpcIdShort)}</span></td>
2911
+ <td>${escapeHtml(rpc.method)}</td>
2912
+ <td>${latency}</td>
2913
+ </tr>`;
2914
+ }).join('\n');
2915
+
2916
+ // Pre-render all RPC detail HTML (avoids JS string concatenation issues)
2917
+ const rpcDetailDivs = rpcs.map((rpc, idx) => renderRpcDetailHtml(rpc, idx)).join('\n');
2918
+
2919
+ return `
2920
+ <div class="session-content" data-session-id="${escapeHtml(sessionId)}">
2921
+ <div class="inner-container">
2922
+ <div class="left-pane">
2923
+ <div class="session-info">
2924
+ <h2>Session Info</h2>
2925
+ <dl>
2926
+ <dt>Session ID</dt>
2927
+ <dd><span class="badge">${escapeHtml(session.session_id)}</span></dd>
2928
+ <dt>Started</dt>
2929
+ <dd>${formatTimestamp(session.started_at)}</dd>
2930
+ <dt>Ended</dt>
2931
+ <dd>${session.ended_at ? formatTimestamp(session.ended_at) : '(active)'}</dd>
2932
+ <dt>Exit Reason</dt>
2933
+ <dd>${session.exit_reason || '(none)'}</dd>
2934
+ <dt>RPC Count</dt>
2935
+ <dd><span class="badge">${session.rpc_count}</span></dd>
2936
+ <dt>Event Count</dt>
2937
+ <dd><span class="badge">${session.event_count}</span></dd>
2938
+ <dt>Total Latency</dt>
2939
+ <dd><span class="badge">${totalLatencyDisplay}</span></dd>
2940
+ </dl>
2941
+ </div>
2942
+ <div class="view-toggle">
2943
+ <button class="view-toggle-btn active" data-view="rpc">
2944
+ RPCs<span class="view-toggle-count">(${session.rpc_count})</span>
2945
+ </button>
2946
+ <button class="view-toggle-btn" data-view="events">
2947
+ Events<span class="view-toggle-count">(${session.event_count})</span>
2948
+ </button>
2949
+ </div>
2950
+ <div class="rpc-list">
2951
+ <table class="rpc-table">
2952
+ <thead>
2953
+ <tr>
2954
+ <th>Time</th>
2955
+ <th>St</th>
2956
+ <th>ID</th>
2957
+ <th>Method</th>
2958
+ <th>Latency</th>
2959
+ </tr>
2960
+ </thead>
2961
+ <tbody>
2962
+ ${rpcRows}
2963
+ </tbody>
2964
+ </table>
2965
+ </div>
2966
+ <div class="events-list">
2967
+ <div class="events-loading">Loading events...</div>
2968
+ </div>
2969
+ </div>
2970
+ <div class="resize-handle"></div>
2971
+ <div class="right-pane">
2972
+ <div class="detail-placeholder">
2973
+ ${rpcs.length > 0 ? 'Select an RPC call from the list to view details' : 'No RPC calls in this session'}
2974
+ </div>
2975
+ <div class="rpc-details-container" style="display: none;">
2976
+ ${rpcDetailDivs}
2977
+ </div>
2978
+ </div>
2979
+ </div>
2980
+ </div>`;
2981
+ }
2982
+
2983
+ /**
2984
+ * Generate Connector HTML report (3-hierarchy: Connector -> Sessions -> RPCs)
2985
+ */
2986
+ export function generateConnectorHtml(report: HtmlConnectorReportV1): string {
2987
+ const { meta, connector, sessions, session_reports, analytics } = report;
2988
+
2989
+ // Pagination info
2990
+ const fromNum = connector.offset + 1;
2991
+ const toNum = connector.offset + connector.displayed_sessions;
2992
+ const paginationInfo = connector.session_count > 0
2993
+ ? `Showing ${fromNum}-${toNum} of ${connector.session_count} sessions`
2994
+ : 'No sessions';
2995
+
2996
+ // Connector info section
2997
+ const transportDisplay = connector.transport.type === 'stdio'
2998
+ ? connector.transport.command || '(unknown command)'
2999
+ : connector.transport.url || '(unknown URL)';
3000
+
3001
+ // Server Response Info card (if available, from initialize response)
3002
+ let serverResponseInfoCard = '';
3003
+ if (connector.server) {
3004
+ const { name, version, protocolVersion, capabilities } = connector.server;
3005
+ const serverName = name || '(unknown)';
3006
+ const serverVersion = version ? `v${version}` : '';
3007
+ const protocolDisplay = protocolVersion ? `MCP ${protocolVersion}` : 'Unknown';
3008
+
3009
+ // Capabilities badges with all options (active/inactive state)
3010
+ const allCaps = ['tools', 'resources', 'prompts'] as const;
3011
+ const capBadges = allCaps.map(cap => {
3012
+ const isActive = capabilities[cap];
3013
+ const cls = isActive ? 'badge cap-enabled' : 'badge cap-disabled';
3014
+ return `<span class="${cls}">${cap}</span>`;
3015
+ }).join(' ');
3016
+
3017
+ serverResponseInfoCard = `
3018
+ <div class="connector-info expanded">
3019
+ <div class="connector-info-toggle">
3020
+ <h2>Server Response Info</h2>
3021
+ <span class="toggle-icon">▼</span>
3022
+ </div>
3023
+ <div class="connector-info-content">
3024
+ <dl>
3025
+ <dt>Server</dt>
3026
+ <dd><code>${escapeHtml(serverName)} ${escapeHtml(serverVersion)}</code></dd>
3027
+ <dt>Protocol</dt>
3028
+ <dd><span class="badge">${escapeHtml(protocolDisplay)}</span></dd>
3029
+ <dt>Capabilities</dt>
3030
+ <dd>${capBadges}</dd>
3031
+ </dl>
3032
+ </div>
3033
+ </div>`;
3034
+ }
3035
+
3036
+ // Session items
3037
+ const sessionItems = sessions.map(s => renderConnectorSessionItem(s)).join('\n');
3038
+
3039
+ // Session contents (pre-rendered, hidden by default)
3040
+ const sessionContents = sessions.map(s => {
3041
+ const sessionReport = session_reports[s.session_id];
3042
+ if (!sessionReport) return '';
3043
+ return renderSessionDetailContent(s.session_id, sessionReport);
3044
+ }).join('\n');
3045
+
3046
+ // Pre-render summary and raw JSON HTML for each RPC in each session (for RPC Inspector)
3047
+ // Now generates separate request/response summaries for Req/Res toggle
3048
+ const sessionReportsWithInspectorHtml: Record<string, HtmlSessionReportV1 & { rpcs: Array<SessionRpcDetail & { _requestSummaryHtml: string; _responseSummaryHtml: string; _requestRawHtml: string; _responseRawHtml: string }> }> = {};
3049
+ for (const [sessionId, sessionReport] of Object.entries(session_reports)) {
3050
+ sessionReportsWithInspectorHtml[sessionId] = {
3051
+ ...sessionReport,
3052
+ rpcs: sessionReport.rpcs.map((rpc) => {
3053
+ const requestSummaryRows = renderRequestSummary(rpc.method, rpc.request.json);
3054
+ const responseSummaryRows = renderResponseSummary(rpc.method, rpc.response.json);
3055
+ // Detect sensitive keys in request/response (Phase 12.x-c)
3056
+ const reqSensitiveKeys = detectSensitiveKeys(rpc.request.json);
3057
+ const resSensitiveKeys = detectSensitiveKeys(rpc.response.json);
3058
+ const hasSensitive = reqSensitiveKeys.length > 0 || resSensitiveKeys.length > 0;
3059
+ return {
3060
+ ...rpc,
3061
+ _requestSummaryHtml: renderSummaryRowsHtml(requestSummaryRows),
3062
+ _responseSummaryHtml: renderSummaryRowsHtml(responseSummaryRows),
3063
+ _requestRawHtml: renderJsonWithPaths(rpc.request.json, '#'),
3064
+ _responseRawHtml: renderJsonWithPaths(rpc.response.json, '#'),
3065
+ _hasSensitive: hasSensitive,
3066
+ _sensitiveKeys: [...reqSensitiveKeys, ...resSensitiveKeys],
3067
+ };
3068
+ }),
3069
+ };
3070
+ }
3071
+
3072
+ const reportWithInspectorHtml = {
3073
+ ...report,
3074
+ session_reports: sessionReportsWithInspectorHtml,
3075
+ };
3076
+ const embeddedJson = escapeJsonForScript(JSON.stringify(reportWithInspectorHtml));
3077
+
3078
+ return `<!DOCTYPE html>
3079
+ <html lang="en">
3080
+ <head>
3081
+ <meta charset="UTF-8">
3082
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
3083
+ <title>Connector: ${escapeHtml(connector.target_id)} - proofscan</title>
3084
+ <style>${getConnectorReportStyles()}</style>
3085
+ </head>
3086
+ <body>
3087
+ <header class="header">
3088
+ <div class="header-left">
3089
+ <div class="header-title">ProofScan Monitor</div>
3090
+ <a href="/" class="header-back">← Home</a>
3091
+ </div>
3092
+ <div class="header-meta">
3093
+ <div class="auto-check-toggle" id="autoCheckToggle">
3094
+ <span class="auto-check-label">Auto-check:</span>
3095
+ <button data-enabled="false" class="active">OFF</button>
3096
+ <button data-enabled="true">ON</button>
3097
+ </div>
3098
+ <div class="new-data-banner" id="newDataBanner">
3099
+ <span>New data available</span>
3100
+ <button id="refreshNowBtn">Refresh now</button>
3101
+ </div>
3102
+ <span class="offline-badge">Offline</span>
3103
+ <span>Generated: ${formatTimestamp(meta.generatedAt)}${meta.redacted ? ' (redacted)' : ''}</span>
3104
+ </div>
3105
+ </header>
3106
+ <div class="page-header">
3107
+ <div class="page-header-left">
3108
+ <h1>Connector: <span class="badge">${escapeHtml(connector.target_id)}</span></h1>
3109
+ </div>
3110
+ ${renderKpiRow(analytics.kpis)}
3111
+ </div>
3112
+
3113
+ <div class="connector-top">
3114
+ <div class="connector-info-cards">
3115
+ <div class="connector-info expanded">
3116
+ <div class="connector-info-toggle">
3117
+ <h2>Connector Info</h2>
3118
+ <span class="toggle-icon">▼</span>
3119
+ </div>
3120
+ <div class="connector-info-content">
3121
+ <dl>
3122
+ <dt>Transport</dt>
3123
+ <dd><span class="badge">${escapeHtml(connector.transport.type)}</span></dd>
3124
+ <dt>${connector.transport.type === 'stdio' ? 'Command' : 'URL'}</dt>
3125
+ <dd><code>${escapeHtml(transportDisplay)}</code></dd>
3126
+ <dt>Enabled</dt>
3127
+ <dd>${connector.enabled ? '<span class="badge status-OK">yes</span>' : '<span class="badge status-ERR">no</span>'}</dd>
3128
+ </dl>
3129
+ </div>
3130
+ </div>
3131
+ ${serverResponseInfoCard}
3132
+ </div>
3133
+ ${renderAnalyticsPanel(analytics)}
3134
+ </div>
3135
+
3136
+ <div class="main-container">
3137
+ <div class="sessions-pane">
3138
+ <div class="sessions-header">
3139
+ <h2>Sessions</h2>
3140
+ <span class="pagination-info">${paginationInfo}</span>
3141
+ </div>
3142
+ <div class="sessions-header-row">
3143
+ <span>ID</span>
3144
+ <span>Time (UTC)</span>
3145
+ <span style="text-align:right">Latency</span>
3146
+ <span></span>
3147
+ </div>
3148
+ <div class="sessions-list">
3149
+ ${sessionItems}
3150
+ </div>
3151
+ </div>
3152
+
3153
+ <div class="session-detail-pane">
3154
+ ${sessions.length === 0 ? '<div class="session-detail-empty">No sessions available</div>' : ''}
3155
+ ${sessionContents}
3156
+ </div>
3157
+ </div>
3158
+
3159
+ <script type="application/json" id="report-data">${embeddedJson}</script>
3160
+ <script>${getConnectorReportScript()}</script>
3161
+ </body>
3162
+ </html>`;
3163
+ }