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