Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
292 changes: 222 additions & 70 deletions mcpgateway/static/admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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`;
Expand All @@ -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";
Expand All @@ -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);
}
}

Expand Down Expand Up @@ -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`;
Expand Down
Loading