@@ -1101,7 +1101,6 @@ function createKPISection(kpiData) {
11011101 const section = document . createElement ( "div" ) ;
11021102 section . className = "grid grid-cols-1 md:grid-cols-4 gap-4" ;
11031103
1104- // Define KPI indicators with safe configuration
11051104 const kpis = [
11061105 {
11071106 key : "totalExecutions" ,
@@ -1114,26 +1113,35 @@ function createKPISection(kpiData) {
11141113 label : "Success Rate" ,
11151114 icon : "✅" ,
11161115 color : "green" ,
1117- suffix : "%" ,
11181116 } ,
11191117 {
11201118 key : "avgResponseTime" ,
11211119 label : "Avg Response Time" ,
11221120 icon : "⚡" ,
11231121 color : "yellow" ,
1124- suffix : "ms" ,
1125- } ,
1126- {
1127- key : "errorRate" ,
1128- label : "Error Rate" ,
1129- icon : "❌" ,
1130- color : "red" ,
1131- suffix : "%" ,
11321122 } ,
1123+ { key : "errorRate" , label : "Error Rate" , icon : "❌" , color : "red" } ,
11331124 ] ;
11341125
11351126 kpis . forEach ( ( kpi ) => {
1136- const value = kpiData [ kpi . key ] ?? "N/A" ;
1127+ let value = kpiData [ kpi . key ] ;
1128+ if ( value === null || value === undefined || value === "N/A" ) {
1129+ value = "N/A" ;
1130+ } else {
1131+ if ( kpi . key === "avgResponseTime" ) {
1132+ // ensure numeric then 3 decimals + unit
1133+ value = isNaN ( Number ( value ) )
1134+ ? "N/A"
1135+ : Number ( value ) . toFixed ( 3 ) + " ms" ;
1136+ } else if (
1137+ kpi . key === "successRate" ||
1138+ kpi . key === "errorRate"
1139+ ) {
1140+ value = String ( value ) + "%" ;
1141+ } else {
1142+ value = String ( value ) ;
1143+ }
1144+ }
11371145
11381146 const kpiCard = document . createElement ( "div" ) ;
11391147 kpiCard . className = `bg-white rounded-lg shadow p-4 border-l-4 border-${ kpi . color } -500 dark:bg-gray-800` ;
@@ -1150,8 +1158,7 @@ function createKPISection(kpiData) {
11501158
11511159 const valueSpan = document . createElement ( "div" ) ;
11521160 valueSpan . className = `text-2xl font-bold text-${ kpi . color } -600` ;
1153- valueSpan . textContent =
1154- ( value === "N/A" ? "N/A" : String ( value ) ) + ( kpi . suffix || "" ) ;
1161+ valueSpan . textContent = value ;
11551162
11561163 const labelSpan = document . createElement ( "div" ) ;
11571164 labelSpan . className = "text-sm text-gray-500 dark:text-gray-400" ;
@@ -1166,73 +1173,205 @@ function createKPISection(kpiData) {
11661173 } ) ;
11671174
11681175 return section ;
1169- } catch ( error ) {
1170- console . error ( "Error creating KPI section:" , error ) ;
1171- return document . createElement ( "div" ) ; // Safe fallback
1176+ } catch ( err ) {
1177+ console . error ( "Error creating KPI section:" , err ) ;
1178+ return document . createElement ( "div" ) ;
11721179 }
11731180}
11741181
11751182/**
11761183 * SECURITY: Extract and calculate KPI data with validation
11771184 */
1185+ function formatValue ( value , key ) {
1186+ if ( value === null || value === undefined || value === "N/A" ) {
1187+ return "N/A" ;
1188+ }
1189+
1190+ if ( key === "avgResponseTime" ) {
1191+ return isNaN ( Number ( value ) ) ? "N/A" : Number ( value ) . toFixed ( 3 ) + " ms" ;
1192+ }
1193+
1194+ if ( key === "successRate" || key === "errorRate" ) {
1195+ return `${ value } %` ;
1196+ }
1197+
1198+ if ( typeof value === "number" && Number . isNaN ( value ) ) {
1199+ return "N/A" ;
1200+ }
1201+
1202+ return String ( value ) . trim ( ) === "" ? "N/A" : String ( value ) ;
1203+ }
1204+
11781205function extractKPIData ( data ) {
11791206 try {
1180- const kpiData = { } ;
1181-
1182- // Initialize calculation variables
11831207 let totalExecutions = 0 ;
11841208 let totalSuccessful = 0 ;
11851209 let totalFailed = 0 ;
1186- const responseTimes = [ ] ;
1210+ let weightedResponseSum = 0 ;
11871211
1188- // Process each category safely
1189- const categories = [
1190- "tools" ,
1191- "resources" ,
1192- "prompts" ,
1193- "gateways" ,
1194- "servers" ,
1212+ const categoryKeys = [
1213+ [ "tools" , "Tools Metrics" , "Tools" , "tools_metrics" ] ,
1214+ [
1215+ "resources" ,
1216+ "Resources Metrics" ,
1217+ "Resources" ,
1218+ "resources_metrics" ,
1219+ ] ,
1220+ [ "prompts" , "Prompts Metrics" , "Prompts" , "prompts_metrics" ] ,
1221+ [ "servers" , "Servers Metrics" , "Servers" , "servers_metrics" ] ,
1222+ [ "gateways" , "Gateways Metrics" , "Gateways" , "gateways_metrics" ] ,
1223+ [
1224+ "virtualServers" ,
1225+ "Virtual Servers" ,
1226+ "VirtualServers" ,
1227+ "virtual_servers" ,
1228+ ] ,
11951229 ] ;
1196- categories . forEach ( ( category ) => {
1197- if ( data [ category ] ) {
1198- const categoryData = data [ category ] ;
1199- totalExecutions += Number ( categoryData . totalExecutions || 0 ) ;
1200- totalSuccessful += Number (
1201- categoryData . successfulExecutions || 0 ,
1202- ) ;
1203- totalFailed += Number ( categoryData . failedExecutions || 0 ) ;
12041230
1205- if (
1206- categoryData . avgResponseTime &&
1207- categoryData . avgResponseTime !== "N/A"
1208- ) {
1209- responseTimes . push ( Number ( categoryData . avgResponseTime ) ) ;
1231+ categoryKeys . forEach ( ( aliases ) => {
1232+ let categoryData = null ;
1233+ for ( const key of aliases ) {
1234+ if ( data && data [ key ] ) {
1235+ categoryData = data [ key ] ;
1236+ break ;
12101237 }
12111238 }
1239+ if ( ! categoryData ) {
1240+ return ;
1241+ }
1242+
1243+ // Build a lowercase-key map so "Successful Executions" and "successfulExecutions" both match
1244+ const normalized = { } ;
1245+ Object . entries ( categoryData ) . forEach ( ( [ k , v ] ) => {
1246+ normalized [ k . toString ( ) . trim ( ) . toLowerCase ( ) ] = v ;
1247+ } ) ;
1248+
1249+ const executions = Number (
1250+ normalized [ "total executions" ] ??
1251+ normalized . totalexecutions ??
1252+ normalized . execution_count ??
1253+ normalized [ "execution-count" ] ??
1254+ normalized . executions ??
1255+ normalized . total_executions ??
1256+ 0 ,
1257+ ) ;
1258+
1259+ const successful = Number (
1260+ normalized [ "successful executions" ] ??
1261+ normalized . successfulexecutions ??
1262+ normalized . successful ??
1263+ normalized . successful_executions ??
1264+ 0 ,
1265+ ) ;
1266+
1267+ const failed = Number (
1268+ normalized [ "failed executions" ] ??
1269+ normalized . failedexecutions ??
1270+ normalized . failed ??
1271+ normalized . failed_executions ??
1272+ 0 ,
1273+ ) ;
1274+
1275+ const avgResponseRaw =
1276+ normalized [ "average response time" ] ??
1277+ normalized . avgresponsetime ??
1278+ normalized . avg_response_time ??
1279+ normalized . avgresponsetime ??
1280+ null ;
1281+
1282+ totalExecutions += Number . isNaN ( executions ) ? 0 : executions ;
1283+ totalSuccessful += Number . isNaN ( successful ) ? 0 : successful ;
1284+ totalFailed += Number . isNaN ( failed ) ? 0 : failed ;
1285+
1286+ if (
1287+ avgResponseRaw !== null &&
1288+ avgResponseRaw !== undefined &&
1289+ avgResponseRaw !== "N/A" &&
1290+ ! Number . isNaN ( Number ( avgResponseRaw ) ) &&
1291+ executions > 0
1292+ ) {
1293+ weightedResponseSum += executions * Number ( avgResponseRaw ) ;
1294+ }
12121295 } ) ;
12131296
1214- // Calculate safe aggregate metrics
1215- kpiData . totalExecutions = totalExecutions ;
1216- kpiData . successRate =
1297+ const avgResponseTime =
1298+ totalExecutions > 0 && weightedResponseSum > 0
1299+ ? weightedResponseSum / totalExecutions
1300+ : null ;
1301+
1302+ const successRate =
12171303 totalExecutions > 0
12181304 ? Math . round ( ( totalSuccessful / totalExecutions ) * 100 )
12191305 : 0 ;
1220- kpiData . errorRate =
1306+
1307+ const errorRate =
12211308 totalExecutions > 0
12221309 ? Math . round ( ( totalFailed / totalExecutions ) * 100 )
12231310 : 0 ;
1224- kpiData . avgResponseTime =
1225- responseTimes . length > 0
1226- ? Math . round (
1227- responseTimes . reduce ( ( a , b ) => a + b , 0 ) /
1228- responseTimes . length ,
1229- )
1230- : "N/A" ;
1231-
1232- return kpiData ;
1233- } catch ( error ) {
1234- console . error ( "Error extracting KPI data:" , error ) ;
1235- return { } ; // Safe fallback
1311+
1312+ // Debug: show what we've read from the payload
1313+ console . log ( "KPI Totals:" , {
1314+ totalExecutions,
1315+ totalSuccessful,
1316+ totalFailed,
1317+ successRate,
1318+ errorRate,
1319+ avgResponseTime,
1320+ } ) ;
1321+
1322+ return { totalExecutions, successRate, errorRate, avgResponseTime } ;
1323+ } catch ( err ) {
1324+ console . error ( "Error extracting KPI data:" , err ) ;
1325+ return {
1326+ totalExecutions : 0 ,
1327+ successRate : 0 ,
1328+ errorRate : 0 ,
1329+ avgResponseTime : null ,
1330+ } ;
1331+ }
1332+ }
1333+
1334+ // eslint-disable-next-line no-unused-vars
1335+ function updateKPICards ( kpiData ) {
1336+ try {
1337+ if ( ! kpiData ) {
1338+ return ;
1339+ }
1340+
1341+ const idMap = {
1342+ "metrics-total-executions" : formatValue (
1343+ kpiData . totalExecutions ,
1344+ "totalExecutions" ,
1345+ ) ,
1346+ "metrics-success-rate" : formatValue (
1347+ kpiData . successRate ,
1348+ "successRate" ,
1349+ ) ,
1350+ "metrics-avg-response-time" : formatValue (
1351+ kpiData . avgResponseTime ,
1352+ "avgResponseTime" ,
1353+ ) ,
1354+ "metrics-error-rate" : formatValue ( kpiData . errorRate , "errorRate" ) ,
1355+ } ;
1356+
1357+ Object . entries ( idMap ) . forEach ( ( [ id , value ] ) => {
1358+ const el = document . getElementById ( id ) ;
1359+ if ( ! el ) {
1360+ return ;
1361+ }
1362+
1363+ // If card has a `.value` span inside, update it, else update directly
1364+ const valueEl =
1365+ el . querySelector ?. ( ".value" ) ||
1366+ el . querySelector ?. ( ".kpi-value" ) ;
1367+ if ( valueEl ) {
1368+ valueEl . textContent = value ;
1369+ } else {
1370+ el . textContent = value ;
1371+ }
1372+ } ) ;
1373+ } catch ( err ) {
1374+ console . error ( "updateKPICards error:" , err ) ;
12361375 }
12371376}
12381377
@@ -1389,26 +1528,39 @@ function formatLastUsed(timestamp) {
13891528 return "Never" ;
13901529 }
13911530
1392- const date = new Date ( timestamp ) ;
1393- const now = new Date ( ) ;
1394- const diffMs = now - date ;
1395- const diffMins = Math . floor ( diffMs / 60000 ) ;
1396-
1397- if ( diffMins < 1 ) {
1398- return "Just now" ;
1531+ let date ;
1532+ if ( typeof timestamp === "number" || / ^ \d + $ / . test ( timestamp ) ) {
1533+ const num = Number ( timestamp ) ;
1534+ date = new Date ( num < 1e12 ? num * 1000 : num ) ; // epoch seconds or ms
1535+ } else {
1536+ date = new Date ( timestamp . endsWith ( "Z" ) ? timestamp : timestamp + "Z" ) ;
13991537 }
1400- if ( diffMins < 60 ) {
1401- return `${ diffMins } min ago` ;
1538+
1539+ if ( isNaN ( date . getTime ( ) ) ) {
1540+ return "Never" ;
14021541 }
1403- if ( diffMins < 1440 ) {
1404- return `${ Math . floor ( diffMins / 60 ) } hours ago` ;
1542+
1543+ const now = Date . now ( ) ;
1544+ const diff = now - date . getTime ( ) ;
1545+
1546+ if ( diff < 60 * 1000 ) {
1547+ return "Just now" ;
14051548 }
1406- if ( diffMins < 10080 ) {
1407- return `${ Math . floor ( diffMins / 1440 ) } days ago` ;
1549+ if ( diff < 60 * 60 * 1000 ) {
1550+ return `${ Math . floor ( diff / 60000 ) } min ago` ;
14081551 }
14091552
1410- return date . toLocaleDateString ( ) ;
1553+ return date . toLocaleString ( undefined , {
1554+ year : "numeric" ,
1555+ month : "short" ,
1556+ day : "numeric" ,
1557+ hour : "2-digit" ,
1558+ minute : "2-digit" ,
1559+ hour12 : true ,
1560+ timeZone : Intl . DateTimeFormat ( ) . resolvedOptions ( ) . timeZone ,
1561+ } ) ;
14111562}
1563+
14121564function createTopPerformersTable ( entityType , data , isActive ) {
14131565 const panel = document . createElement ( "div" ) ;
14141566 panel . id = `top-${ entityType } -panel` ;
0 commit comments