From cbb4700453bcffa0fc814e95a60fdf6cfbefd0b5 Mon Sep 17 00:00:00 2001 From: NAYANAR Date: Tue, 23 Sep 2025 12:33:34 +0530 Subject: [PATCH] The system executed 5 runs with a 0% success rate, an average response time of 0.393 ms, and an error rate of 0%. Signed-off-by: NAYANAR --- mcpgateway/static/admin.js | 292 ++++++++++++++++++++++++++++--------- 1 file changed, 222 insertions(+), 70 deletions(-) diff --git a/mcpgateway/static/admin.js b/mcpgateway/static/admin.js index d61d9e601..dbff2996b 100644 --- a/mcpgateway/static/admin.js +++ b/mcpgateway/static/admin.js @@ -1101,7 +1101,6 @@ function createKPISection(kpiData) { const section = document.createElement("div"); section.className = "grid grid-cols-1 md:grid-cols-4 gap-4"; - // Define KPI indicators with safe configuration const kpis = [ { key: "totalExecutions", @@ -1114,26 +1113,35 @@ function createKPISection(kpiData) { label: "Success Rate", icon: "✅", color: "green", - suffix: "%", }, { key: "avgResponseTime", label: "Avg Response Time", icon: "⚡", color: "yellow", - suffix: "ms", - }, - { - key: "errorRate", - label: "Error Rate", - icon: "❌", - color: "red", - suffix: "%", }, + { key: "errorRate", label: "Error Rate", icon: "❌", color: "red" }, ]; kpis.forEach((kpi) => { - const value = kpiData[kpi.key] ?? "N/A"; + let value = kpiData[kpi.key]; + if (value === null || value === undefined || value === "N/A") { + value = "N/A"; + } else { + if (kpi.key === "avgResponseTime") { + // ensure numeric then 3 decimals + unit + value = isNaN(Number(value)) + ? "N/A" + : Number(value).toFixed(3) + " ms"; + } else if ( + kpi.key === "successRate" || + kpi.key === "errorRate" + ) { + value = String(value) + "%"; + } else { + value = String(value); + } + } const kpiCard = document.createElement("div"); 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) { const valueSpan = document.createElement("div"); valueSpan.className = `text-2xl font-bold text-${kpi.color}-600`; - valueSpan.textContent = - (value === "N/A" ? "N/A" : String(value)) + (kpi.suffix || ""); + valueSpan.textContent = value; const labelSpan = document.createElement("div"); labelSpan.className = "text-sm text-gray-500 dark:text-gray-400"; @@ -1166,73 +1173,205 @@ function createKPISection(kpiData) { }); return section; - } catch (error) { - console.error("Error creating KPI section:", error); - return document.createElement("div"); // Safe fallback + } catch (err) { + console.error("Error creating KPI section:", err); + return document.createElement("div"); } } /** * SECURITY: Extract and calculate KPI data with validation */ +function formatValue(value, key) { + if (value === null || value === undefined || value === "N/A") { + return "N/A"; + } + + if (key === "avgResponseTime") { + return isNaN(Number(value)) ? "N/A" : Number(value).toFixed(3) + " ms"; + } + + if (key === "successRate" || key === "errorRate") { + return `${value}%`; + } + + if (typeof value === "number" && Number.isNaN(value)) { + return "N/A"; + } + + return String(value).trim() === "" ? "N/A" : String(value); +} + function extractKPIData(data) { try { - const kpiData = {}; - - // Initialize calculation variables let totalExecutions = 0; let totalSuccessful = 0; let totalFailed = 0; - const responseTimes = []; + let weightedResponseSum = 0; - // Process each category safely - const categories = [ - "tools", - "resources", - "prompts", - "gateways", - "servers", + const categoryKeys = [ + ["tools", "Tools Metrics", "Tools", "tools_metrics"], + [ + "resources", + "Resources Metrics", + "Resources", + "resources_metrics", + ], + ["prompts", "Prompts Metrics", "Prompts", "prompts_metrics"], + ["servers", "Servers Metrics", "Servers", "servers_metrics"], + ["gateways", "Gateways Metrics", "Gateways", "gateways_metrics"], + [ + "virtualServers", + "Virtual Servers", + "VirtualServers", + "virtual_servers", + ], ]; - categories.forEach((category) => { - if (data[category]) { - const categoryData = data[category]; - totalExecutions += Number(categoryData.totalExecutions || 0); - totalSuccessful += Number( - categoryData.successfulExecutions || 0, - ); - totalFailed += Number(categoryData.failedExecutions || 0); - if ( - categoryData.avgResponseTime && - categoryData.avgResponseTime !== "N/A" - ) { - responseTimes.push(Number(categoryData.avgResponseTime)); + categoryKeys.forEach((aliases) => { + let categoryData = null; + for (const key of aliases) { + if (data && data[key]) { + categoryData = data[key]; + break; } } + if (!categoryData) { + return; + } + + // Build a lowercase-key map so "Successful Executions" and "successfulExecutions" both match + const normalized = {}; + Object.entries(categoryData).forEach(([k, v]) => { + normalized[k.toString().trim().toLowerCase()] = v; + }); + + const executions = Number( + normalized["total executions"] ?? + normalized.totalexecutions ?? + normalized.execution_count ?? + normalized["execution-count"] ?? + normalized.executions ?? + normalized.total_executions ?? + 0, + ); + + const successful = Number( + normalized["successful executions"] ?? + normalized.successfulexecutions ?? + normalized.successful ?? + normalized.successful_executions ?? + 0, + ); + + const failed = Number( + normalized["failed executions"] ?? + normalized.failedexecutions ?? + normalized.failed ?? + normalized.failed_executions ?? + 0, + ); + + const avgResponseRaw = + normalized["average response time"] ?? + normalized.avgresponsetime ?? + normalized.avg_response_time ?? + normalized.avgresponsetime ?? + null; + + totalExecutions += Number.isNaN(executions) ? 0 : executions; + totalSuccessful += Number.isNaN(successful) ? 0 : successful; + totalFailed += Number.isNaN(failed) ? 0 : failed; + + if ( + avgResponseRaw !== null && + avgResponseRaw !== undefined && + avgResponseRaw !== "N/A" && + !Number.isNaN(Number(avgResponseRaw)) && + executions > 0 + ) { + weightedResponseSum += executions * Number(avgResponseRaw); + } }); - // Calculate safe aggregate metrics - kpiData.totalExecutions = totalExecutions; - kpiData.successRate = + const avgResponseTime = + totalExecutions > 0 && weightedResponseSum > 0 + ? weightedResponseSum / totalExecutions + : null; + + const successRate = totalExecutions > 0 ? Math.round((totalSuccessful / totalExecutions) * 100) : 0; - kpiData.errorRate = + + const errorRate = totalExecutions > 0 ? Math.round((totalFailed / totalExecutions) * 100) : 0; - kpiData.avgResponseTime = - responseTimes.length > 0 - ? Math.round( - responseTimes.reduce((a, b) => a + b, 0) / - responseTimes.length, - ) - : "N/A"; - - return kpiData; - } catch (error) { - console.error("Error extracting KPI data:", error); - return {}; // Safe fallback + + // Debug: show what we've read from the payload + console.log("KPI Totals:", { + totalExecutions, + totalSuccessful, + totalFailed, + successRate, + errorRate, + avgResponseTime, + }); + + return { totalExecutions, successRate, errorRate, avgResponseTime }; + } catch (err) { + console.error("Error extracting KPI data:", err); + return { + totalExecutions: 0, + successRate: 0, + errorRate: 0, + avgResponseTime: null, + }; + } +} + +// eslint-disable-next-line no-unused-vars +function updateKPICards(kpiData) { + try { + if (!kpiData) { + return; + } + + const idMap = { + "metrics-total-executions": formatValue( + kpiData.totalExecutions, + "totalExecutions", + ), + "metrics-success-rate": formatValue( + kpiData.successRate, + "successRate", + ), + "metrics-avg-response-time": formatValue( + kpiData.avgResponseTime, + "avgResponseTime", + ), + "metrics-error-rate": formatValue(kpiData.errorRate, "errorRate"), + }; + + Object.entries(idMap).forEach(([id, value]) => { + const el = document.getElementById(id); + if (!el) { + return; + } + + // If card has a `.value` span inside, update it, else update directly + const valueEl = + el.querySelector?.(".value") || + el.querySelector?.(".kpi-value"); + if (valueEl) { + valueEl.textContent = value; + } else { + el.textContent = value; + } + }); + } catch (err) { + console.error("updateKPICards error:", err); } } @@ -1389,26 +1528,39 @@ function formatLastUsed(timestamp) { return "Never"; } - const date = new Date(timestamp); - const now = new Date(); - const diffMs = now - date; - const diffMins = Math.floor(diffMs / 60000); - - if (diffMins < 1) { - return "Just now"; + let date; + if (typeof timestamp === "number" || /^\d+$/.test(timestamp)) { + const num = Number(timestamp); + date = new Date(num < 1e12 ? num * 1000 : num); // epoch seconds or ms + } else { + date = new Date(timestamp.endsWith("Z") ? timestamp : timestamp + "Z"); } - if (diffMins < 60) { - return `${diffMins} min ago`; + + if (isNaN(date.getTime())) { + return "Never"; } - if (diffMins < 1440) { - return `${Math.floor(diffMins / 60)} hours ago`; + + const now = Date.now(); + const diff = now - date.getTime(); + + if (diff < 60 * 1000) { + return "Just now"; } - if (diffMins < 10080) { - return `${Math.floor(diffMins / 1440)} days ago`; + if (diff < 60 * 60 * 1000) { + return `${Math.floor(diff / 60000)} min ago`; } - return date.toLocaleDateString(); + return date.toLocaleString(undefined, { + year: "numeric", + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + hour12: true, + timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone, + }); } + function createTopPerformersTable(entityType, data, isActive) { const panel = document.createElement("div"); panel.id = `top-${entityType}-panel`;