diff --git a/.github/workflows/deploy_dashboard.yml b/.github/workflows/deploy_dashboard.yml
index 7ca01adc..454aaa00 100644
--- a/.github/workflows/deploy_dashboard.yml
+++ b/.github/workflows/deploy_dashboard.yml
@@ -37,6 +37,8 @@ jobs:
echo "Syncing latest interop logs from SDK rolling releases..."
# Download Python Metrics directly into the itk-dashboard directory
curl -L -f -o itk-dashboard/itk_python.json https://github.com/a2aproject/a2a-python/releases/download/nightly-metrics/itk_python.json || echo "{}" > itk-dashboard/itk_python.json
+ # Download Go Metrics directly into the itk-dashboard directory
+ curl -L -f -o itk-dashboard/itk_go.json https://github.com/a2aproject/a2a-go/releases/download/nightly-metrics/itk_go.json || echo "{}" > itk-dashboard/itk_go.json
echo "Metrics synchronized successfully!"
- name: Prepare static site structure
diff --git a/itk-dashboard/app.js b/itk-dashboard/app.js
index 11067da8..dbec5bad 100644
--- a/itk-dashboard/app.js
+++ b/itk-dashboard/app.js
@@ -2,13 +2,12 @@ document.addEventListener("DOMContentLoaded", () => {
const sdkTabs = document.getElementById("sdk-tabs");
const loadingState = document.getElementById("loading-state");
const dashboardBody = document.getElementById("dashboard-body");
- const lastRunTime = document.getElementById("last-run-time");
- const lastCommit = document.getElementById("last-commit");
const historyCount = document.getElementById("history-count");
const historyTimelineList = document.getElementById(
"history-timeline-list"
);
- const topologiesList = document.getElementById("topologies-list");
+ const summaryContainer = document.getElementById("summary-container");
+ const pairwiseList = document.getElementById("pairwise-list");
// List of protocols and behaviors to draw the matrix grid
const PROTOCOLS = ["jsonrpc", "grpc", "http_json"];
@@ -42,6 +41,7 @@ document.addEventListener("DOMContentLoaded", () => {
let activeSDK = "python";
let historyData = [];
let activeRunIndex = 0;
+ let maxTopologySdks = []; // Dynamic list of all SDK nodes in the full parent topology
// Initial setup: add event listeners to tabs
sdkTabs.querySelectorAll(".tab-btn").forEach((btn) => {
@@ -57,6 +57,24 @@ document.addEventListener("DOMContentLoaded", () => {
});
});
+ // Dynamically track max SDKs from the current run for helper calculations
+ function initializeMaxTopologySdks(run) {
+ const scenarios = run.scenarios || [];
+ let maxSDKs = 0;
+ maxTopologySdks = [];
+ scenarios.forEach((scenario) => {
+ const sdks = scenario.sdks || [];
+ if (sdks.length > maxSDKs) {
+ maxSDKs = sdks.length;
+ maxTopologySdks = [...sdks];
+ }
+ });
+ console.log(
+ `[ITK-DASHBOARD] Dynamically derived parent SDKs list:`,
+ maxTopologySdks
+ );
+ }
+
// Load data by URL natively (CORS-free served locally)
async function loadDashboardData(url) {
console.log(
@@ -110,6 +128,7 @@ document.addEventListener("DOMContentLoaded", () => {
);
activeRunIndex = 0;
+ initializeMaxTopologySdks(historyData[0]);
renderDashboard();
console.log(`[ITK-DASHBOARD] Dashboard populated successfully!`);
} catch (err) {
@@ -172,6 +191,7 @@ document.addEventListener("DOMContentLoaded", () => {
`;
item.addEventListener("click", () => {
activeRunIndex = index;
+ initializeMaxTopologySdks(historyData[index]);
// Re-render active run context
document
.querySelectorAll(".timeline-item")
@@ -186,161 +206,324 @@ document.addEventListener("DOMContentLoaded", () => {
renderActiveRun();
}
- // Render currently selected run
+ // Render currently selected run statically (Summary Star Topology at top, Pairwise at bottom)
function renderActiveRun() {
const run = historyData[activeRunIndex];
if (!run) return;
- // Update Header Meta
- const runDate = new Date(run.timestamp);
- lastRunTime.textContent = runDate.toLocaleString();
- lastCommit.innerHTML = `${run.commit_sha.substring(0, 7)}`;
+ summaryContainer.innerHTML = "";
+ pairwiseList.innerHTML = "";
- topologiesList.innerHTML = "";
-
- // Process and render each scenario / topology card
const scenarios = run.scenarios || [];
if (scenarios.length === 0) {
- topologiesList.innerHTML =
+ summaryContainer.innerHTML =
'
-
-
+ const summarySdks = summaryTopologyKey
+ ? summaryTopologyKey.split(",")
+ : [];
+
+ if (summaryTopologyKey) {
+ const summaryScenarios = scenarios.filter(
+ (s) => (s.sdks || []).join(",") === summaryTopologyKey
+ );
+
+ if (summaryScenarios.length > 0) {
+ const edges = summaryScenarios[0].edges || [];
+ const card = document.createElement("div");
+ card.className = "panel glass topology-card";
+
+ const cardId = "topology-card-summary";
+ const svgId = "svg-canvas-summary";
-
-
-
-
-
- | Protocol |
- ${BEHAVIORS.map((b) => `${b.label} | `).join("")}
-
-
-
-
-
-
+ const commitLink = `
${run.commit_sha.substring(0, 7)}`;
+ const runDateStr = new Date(run.timestamp).toLocaleString();
+
+ card.innerHTML = `
+
-
- `;
+
+
+
+
+
+
+
+
+
+ | Behavior / Feature |
+ ${PROTOCOLS.map((p) => `${p} | `).join("")}
+
+
+
+
+
+
+
+
+ `;
+
+ summaryContainer.appendChild(card);
- topologiesList.appendChild(card);
+ // Draw node graph and render fully filled behavior rows vs protocol columns compatibility matrix
+ drawTopologyGraph(svgId, summarySdks, edges);
+ renderGroupMatrix(`matrix-body-${svgId}`, summaryScenarios);
+ }
+ } else {
+ summaryContainer.innerHTML =
+ '
No summary scenario found.
';
+ }
- // 1. Draw topology graph SVG
- drawTopologyGraph(svgId, group.sdks, group.edges, topologyName);
+ // 2. Identify all unique pairwise (exactly 2 nodes) topology keys and render their fully-filled grids
+ const uniquePairwiseKeys = [];
+ scenarios.forEach((scenario) => {
+ const sdks = scenario.sdks || [];
+ if (sdks.length === 2) {
+ const key = sdks.join(",");
+ if (!uniquePairwiseKeys.includes(key)) {
+ uniquePairwiseKeys.push(key);
+ }
+ }
+ });
- // 2. Render the consolidated matrix content for this group
- renderGroupMatrix(`matrix-body-${idx}`, group.scenarios);
+ // Sort pairwise keys deterministically based on the order of SDKs in the summary visualization
+ uniquePairwiseKeys.sort((keyA, keyB) => {
+ const getRank = (key) => {
+ const sdks = key.split(",");
+ const ranks = sdks.map((s) => {
+ const idx = summarySdks.findIndex(
+ (sumSdk) => sumSdk.toLowerCase() === s.toLowerCase()
+ );
+ return idx !== -1 ? idx : 999;
+ });
+ ranks.sort((a, b) => a - b);
+ return ranks;
+ };
+ const rankA = getRank(keyA);
+ const rankB = getRank(keyB);
+ if (rankA[0] !== rankB[0]) {
+ return rankA[0] - rankB[0];
+ }
+ return rankA[1] - rankB[1];
});
+
+ if (uniquePairwiseKeys.length > 0) {
+ uniquePairwiseKeys.forEach((key, idx) => {
+ const pairwiseSdks = key.split(",");
+ const pairwiseScenarios = scenarios.filter(
+ (s) => (s.sdks || []).join(",") === key
+ );
+
+ if (pairwiseScenarios.length > 0) {
+ const edges = pairwiseScenarios[0].edges || [];
+ const card = document.createElement("div");
+ card.className = "panel glass topology-card";
+
+ const cardId = `topology-card-pairwise-${idx}`;
+ const svgId = `svg-canvas-pairwise-${idx}`;
+
+ card.innerHTML = `
+
+
+
+
+
+
+
+
+
+ | Behavior / Feature |
+ ${PROTOCOLS.map((p) => `${p} | `).join("")}
+
+
+
+
+
+
+
+
+ `;
+
+ pairwiseList.appendChild(card);
+
+ drawTopologyGraph(svgId, pairwiseSdks, edges);
+ renderGroupMatrix(
+ `matrix-body-${svgId}`,
+ pairwiseScenarios
+ );
+ }
+ });
+ } else {
+ pairwiseList.innerHTML =
+ '
No pairwise scenarios executed in this run.
';
+ }
+ }
+
+ // Helper: Format SDK string into pretty display names
+ function formatSdkName(sdk) {
+ if (!sdk) return "";
+ if (sdk.toLowerCase() === "current") return "Current";
+
+ const parts = sdk.split("_");
+ let lang = parts[0];
+ if (lang.toLowerCase() === "dotnet") {
+ lang = ".NET";
+ } else {
+ lang = lang.charAt(0).toUpperCase() + lang.slice(1);
+ }
+
+ if (parts.length > 1) {
+ let ver = parts[1];
+ if (ver.startsWith("v") && ver.length === 3) {
+ ver = `v${ver[1]}.${ver[2]}`;
+ }
+ return `${lang} ${ver}`;
+ }
+ return lang;
+ }
+
+ // Helper: Get premium styling colors for a given language/SDK string
+ function getSdkColors(sdk) {
+ const str = (sdk || "").toLowerCase();
+ if (str === "current") {
+ return { fill: "#1e3a8a", stroke: "#3b82f6", text: "#eff6ff" };
+ }
+ if (str.startsWith("python")) {
+ return { fill: "#422006", stroke: "#eab308", text: "#fef08a" }; // Yellow Gold
+ }
+ if (str.startsWith("go")) {
+ return { fill: "#083344", stroke: "#06b6d4", text: "#cffafe" }; // Cyan
+ }
+ if (str.startsWith("java")) {
+ return { fill: "#450a0a", stroke: "#ef4444", text: "#fee2e2" }; // Red/Orange
+ }
+ if (str.startsWith("dotnet")) {
+ return { fill: "#2e1065", stroke: "#a855f7", text: "#f3e8ff" }; // Purple
+ }
+ return { fill: "#1e293b", stroke: "#64748b", text: "#f8fafc" }; // Slate Default
+ }
+
+ // Helper: Calculate exact intersection of line segment with target node rectangle border
+ function getBorderIntersection(fromPos, toPos) {
+ const dx = toPos.x - fromPos.x;
+ const dy = toPos.y - fromPos.y;
+ if (dx === 0 && dy === 0) return { x: fromPos.x, y: fromPos.y };
+
+ const w = 55; // half width of pill rectangle
+ const h = 14; // half height of pill rectangle
+
+ const tx = dx !== 0 ? w / Math.abs(dx) : Infinity;
+ const ty = dy !== 0 ? h / Math.abs(dy) : Infinity;
+ const t = Math.min(tx, ty);
+
+ return {
+ x: fromPos.x + t * dx,
+ y: fromPos.y + t * dy,
+ };
}
- // Helper: Render SVG Topology Node-Link diagram
- function drawTopologyGraph(svgId, sdks, edges, scenarioName) {
+ // Helper: Render SVG Topology Node-Link diagram in a gorgeous tree layout
+ function drawTopologyGraph(svgId, sdks, edges) {
const svg = document.getElementById(svgId);
if (!svg) return;
- const width = 400;
+ // End and start markers are statically pre-configured in the SVG defs template
+
const height = 300;
- const centerX = width / 2;
- const centerY = height / 2;
const numNodes = sdks.length;
+ // Determine root index (Current is usually index 0)
+ let rootIdx = sdks.findIndex((s) => s.toLowerCase() === "current");
+ if (rootIdx === -1) rootIdx = 0;
+
const nodePositions = {};
- // Determine Layout strategy
- const isStar =
- scenarioName.toLowerCase().includes("star") || numNodes > 3;
- const isChain =
- scenarioName.toLowerCase().includes("chain") ||
- scenarioName.toLowerCase().includes("linear");
-
- if (isStar) {
- // Node 0 in center, others arranged in circle
- nodePositions[0] = { x: centerX, y: centerY, isCenter: true };
- const radius = 115;
- for (let i = 1; i < numNodes; i++) {
- const angle =
- ((i - 1) / (numNodes - 1)) * 2 * Math.PI - Math.PI / 2;
- nodePositions[i] = {
- x: centerX + radius * Math.cos(angle),
- y: centerY + radius * Math.sin(angle),
- isCenter: false,
- };
- }
- } else if (isChain && numNodes > 1) {
- // Arrange horizontally in a line
- const startX = 60;
- const spacing = (width - 2 * startX) / (numNodes - 1);
- for (let i = 0; i < numNodes; i++) {
- nodePositions[i] = {
- x: startX + i * spacing,
- y: centerY,
- isCenter: false,
- };
- }
- } else {
- // Standard circular layout
- const radius = 110;
- for (let i = 0; i < numNodes; i++) {
- const angle = (i / numNodes) * 2 * Math.PI - Math.PI / 2;
- nodePositions[i] = {
- x: centerX + radius * Math.cos(angle),
- y: centerY + radius * Math.sin(angle),
- isCenter: false,
+ // Root positioned on the left
+ nodePositions[rootIdx] = { x: 80, y: height / 2, isRoot: true };
+
+ // Other nodes spreading to the right in a clean tree hierarchy
+ const childrenIndices = [];
+ for (let i = 0; i < numNodes; i++) {
+ if (i !== rootIdx) childrenIndices.push(i);
+ }
+
+ const numChildren = childrenIndices.length;
+ if (numChildren === 1) {
+ nodePositions[childrenIndices[0]] = {
+ x: 320,
+ y: height / 2,
+ isRoot: false,
+ };
+ } else if (numChildren > 1) {
+ const startY = 50;
+ const endY = height - 50;
+ const spacing = (endY - startY) / (numChildren - 1);
+ childrenIndices.forEach((childIdx, i) => {
+ nodePositions[childIdx] = {
+ x: 320,
+ y: startY + i * spacing,
+ isRoot: false,
};
- }
+ });
}
- // 1. Draw edges/links
+ // 1. Draw single connecting lines strictly between the dynamic inner rectangle borders of connected nodes
+ const drawnPairs = new Set();
edges.forEach((edge) => {
const parts = edge.split("->");
if (parts.length !== 2) return;
const fromIdx = parseInt(parts[0], 10);
const toIdx = parseInt(parts[1], 10);
+ if (fromIdx === toIdx) return;
+
+ const pairKey =
+ Math.min(fromIdx, toIdx) + "-" + Math.max(fromIdx, toIdx);
+ if (drawnPairs.has(pairKey)) return;
+ drawnPairs.add(pairKey);
+
const fromNode = nodePositions[fromIdx];
const toNode = nodePositions[toIdx];
@@ -349,27 +532,100 @@ document.addEventListener("DOMContentLoaded", () => {
"http://www.w3.org/2000/svg",
"line"
);
- line.setAttribute("x1", fromNode.x);
- line.setAttribute("y1", fromNode.y);
- line.setAttribute("x2", toNode.x);
- line.setAttribute("y2", toNode.y);
+
+ // Dynamically calculate precise border intersection points for any arbitrary connection angle
+ const startPt = getBorderIntersection(fromNode, toNode);
+ const endPt = getBorderIntersection(toNode, fromNode);
+
+ line.setAttribute("x1", startPt.x);
+ line.setAttribute("y1", startPt.y);
+ line.setAttribute("x2", endPt.x);
+ line.setAttribute("y2", endPt.y);
line.setAttribute("class", "svg-edge");
- line.setAttribute(
- "marker-end",
- `url(#arrow-${svgId.split("-").pop()})`
- );
- // Style bi-directional links subtly different
+ // Check if reverse edge exists to show bi-directional arrows pointing both ways
const reverseEdge = `${toIdx}->${fromIdx}`;
- if (edges.includes(reverseEdge)) {
- line.setAttribute("class", "svg-edge bi-directional");
+ const isBidirectional = edges.includes(reverseEdge);
+
+ if (isBidirectional) {
+ line.classList.add("bi-directional");
+ line.style.stroke = "rgba(255, 255, 255, 0.25)";
+ } else {
+ line.style.stroke = "rgba(255, 255, 255, 0.15)";
}
svg.appendChild(line);
+
+ // Explicitly render arrowheads near the middle of the edge to avoid any node container overlap
+ const dx = endPt.x - startPt.x;
+ const dy = endPt.y - startPt.y;
+ const length = Math.hypot(dx, dy);
+ if (length > 20) {
+ const ux = dx / length;
+ const uy = dy / length;
+ const mx = (startPt.x + endPt.x) / 2;
+ const my = (startPt.y + endPt.y) / 2;
+ const arrowLen = 9;
+ const arrowHalfWidth = 4.5;
+ const arrowFill = "rgba(255, 255, 255, 0.3)";
+
+ // Helper to append a polygonal arrowhead given tip position and normalized vector pointing to tip
+ const drawArrowhead = (tipX, tipY, vx, vy) => {
+ const baseX = tipX - arrowLen * vx;
+ const baseY = tipY - arrowLen * vy;
+ // Perpendicular vector (-vy, vx)
+ const c1x = baseX - arrowHalfWidth * -vy;
+ const c1y = baseY - arrowHalfWidth * vx;
+ const c2x = baseX + arrowHalfWidth * -vy;
+ const c2y = baseY + arrowHalfWidth * vx;
+
+ const poly = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "polygon"
+ );
+ poly.setAttribute(
+ "points",
+ `${tipX},${tipY} ${c1x},${c1y} ${c2x},${c2y}`
+ );
+ poly.setAttribute("fill", arrowFill);
+ svg.appendChild(poly);
+ };
+
+ if (isBidirectional) {
+ // Calculate 1/4 and 3/4 split points along the line segment
+ const pt1_4X = startPt.x + 0.25 * dx;
+ const pt1_4Y = startPt.y + 0.25 * dy;
+ const pt3_4X = startPt.x + 0.75 * dx;
+ const pt3_4Y = startPt.y + 0.75 * dy;
+
+ // Forward arrow centered at 3/4 split pointing towards target node
+ drawArrowhead(
+ pt3_4X + (arrowLen / 2) * ux,
+ pt3_4Y + (arrowLen / 2) * uy,
+ ux,
+ uy
+ );
+ // Reverse arrow centered at 1/4 split pointing towards source node
+ drawArrowhead(
+ pt1_4X - (arrowLen / 2) * ux,
+ pt1_4Y - (arrowLen / 2) * uy,
+ -ux,
+ -uy
+ );
+ } else {
+ // Single direction arrow pointing to target node exactly at midpoint
+ drawArrowhead(
+ mx + (arrowLen / 2) * ux,
+ my + (arrowLen / 2) * uy,
+ ux,
+ uy
+ );
+ }
+ }
}
});
- // 2. Draw nodes
+ // 2. Draw pretty stylized SDK nodes
sdks.forEach((sdk, idx) => {
const pos = nodePositions[idx];
if (!pos) return;
@@ -380,11 +636,12 @@ document.addEventListener("DOMContentLoaded", () => {
);
g.setAttribute(
"class",
- `svg-node ${pos.isCenter ? "center-node" : ""}`
+ `svg-node ${pos.isRoot ? "root-node" : ""}`
);
- const rectWidth = 100;
- const rectHeight = 24;
+ const rectWidth = 110;
+ const rectHeight = 28;
+ const colors = getSdkColors(sdk);
const rect = document.createElementNS(
"http://www.w3.org/2000/svg",
@@ -394,6 +651,13 @@ document.addEventListener("DOMContentLoaded", () => {
rect.setAttribute("y", pos.y - rectHeight / 2);
rect.setAttribute("width", rectWidth);
rect.setAttribute("height", rectHeight);
+ rect.setAttribute("rx", "6");
+ rect.setAttribute("ry", "6");
+
+ // Apply dedicated language color codes inline
+ rect.style.fill = colors.fill;
+ rect.style.stroke = colors.stroke;
+ rect.style.strokeWidth = pos.isRoot ? "2px" : "1.5px";
const text = document.createElementNS(
"http://www.w3.org/2000/svg",
@@ -401,7 +665,10 @@ document.addEventListener("DOMContentLoaded", () => {
);
text.setAttribute("x", pos.x);
text.setAttribute("y", pos.y);
- text.textContent = sdk;
+ text.textContent = formatSdkName(sdk);
+ text.style.fill = colors.text;
+ text.style.fontSize = "11px";
+ text.style.fontWeight = "600";
g.appendChild(rect);
g.appendChild(text);
@@ -416,17 +683,17 @@ document.addEventListener("DOMContentLoaded", () => {
tbody.innerHTML = "";
- PROTOCOLS.forEach((proto) => {
+ BEHAVIORS.forEach((behavior) => {
const tr = document.createElement("tr");
- // Cell 1: Protocol Name
- const protocolTd = document.createElement("td");
- protocolTd.className = "protocol-name";
- protocolTd.textContent = proto;
- tr.appendChild(protocolTd);
+ // Cell 1: Behavior Name
+ const behaviorTd = document.createElement("td");
+ behaviorTd.className = "protocol-name";
+ behaviorTd.textContent = behavior.label;
+ tr.appendChild(behaviorTd);
- // Cells 2..5: Behaviors
- BEHAVIORS.forEach((behavior) => {
+ // Cells 2..N: Protocols
+ PROTOCOLS.forEach((proto) => {
const td = document.createElement("td");
// Find all scenarios in the group that cover this Protocol and Behavior
diff --git a/itk-dashboard/index.html b/itk-dashboard/index.html
index 0f105b5d..68350f2f 100644
--- a/itk-dashboard/index.html
+++ b/itk-dashboard/index.html
@@ -26,18 +26,6 @@
A2A SDKs Compatibility
-
@@ -110,16 +98,25 @@