Files
cameleer-server/examples/route-diagram-light.html

1342 lines
46 KiB
HTML
Raw Permalink Normal View History

2026-03-13 10:52:43 +01:00
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cameleer — Route Diagram: split-and-multicast (Light)</title>
2026-03-13 10:52:43 +01:00
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
background: #f7f5f2;
color: #57534e;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding: 32px 16px;
}
header {
max-width: 960px;
width: 100%;
margin-bottom: 24px;
}
header h1 {
font-size: 22px;
font-weight: 700;
color: #1c1917;
margin-bottom: 6px;
}
header .meta {
font-size: 13px;
color: #a8a29e;
display: flex;
gap: 20px;
flex-wrap: wrap;
align-items: center;
}
header .meta span { display: inline-flex; align-items: center; gap: 5px; }
header .meta .dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
header .meta .dot.green { background: #047857; }
/* Toolbar */
.toolbar {
max-width: 960px;
width: 100%;
margin-bottom: 16px;
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.btn {
padding: 7px 16px;
border-radius: 6px;
border: 1px solid #d4cfc8;
background: #ffffff;
color: #57534e;
font-size: 13px;
font-weight: 500;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 6px;
transition: background 0.15s, border-color 0.15s;
user-select: none;
}
.btn:hover { background: #f3f1ee; border-color: #d4cfc8; color: #1c1917; }
.btn.active {
background: rgba(180, 83, 9, 0.07);
border-color: rgba(180, 83, 9, 0.3);
color: #b45309;
}
.btn .kbd {
font-size: 10px;
padding: 1px 5px;
border-radius: 3px;
background: #f3f1ee;
border: 1px solid #d4cfc8;
color: #a8a29e;
font-family: monospace;
}
.exec-badge {
font-size: 12px;
padding: 4px 10px;
border-radius: 12px;
font-weight: 500;
display: none;
}
.exec-badge.visible { display: inline-flex; align-items: center; gap: 5px; }
.exec-badge.completed { background: rgba(4, 120, 87, 0.08); color: #047857; border: 1px solid rgba(4, 120, 87, 0.3); }
.exec-badge.failed { background: rgba(190, 18, 60, 0.08); color: #be123c; border: 1px solid rgba(190, 18, 60, 0.3); }
.canvas-wrapper {
background: #ffffff;
border: 1px solid #e4e0db;
border-radius: 14px;
overflow: auto;
max-width: 960px;
width: 100%;
position: relative;
box-shadow: 0 1px 3px rgba(0,0,0,0.04);
}
svg text { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif; }
/* Hover glow on node groups */
.node-group { cursor: default; transition: opacity 0.3s; }
.node-group:hover .node-rect { filter: drop-shadow(0 0 8px var(--glow-color, rgba(29, 78, 216, 0.3))); }
/* Execution overlay states */
.overlay-active .node-group.dimmed { opacity: 0.3; }
.overlay-active .node-group.dimmed:hover { opacity: 0.5; }
.overlay-active .edge-path.dimmed { opacity: 0.15; }
.overlay-active .node-group.executed .node-rect {
filter: drop-shadow(0 0 8px var(--glow-color, rgba(4, 120, 87, 0.4)));
}
.overlay-active .edge-path.executed {
stroke-width: 2.5 !important;
filter: drop-shadow(0 0 4px var(--edge-glow));
}
/* Animated flow particles */
@keyframes flowParticle {
0% { offset-distance: 0%; opacity: 0; }
5% { opacity: 1; }
95% { opacity: 1; }
100% { offset-distance: 100%; opacity: 0; }
}
.flow-particle {
width: 6px;
height: 6px;
border-radius: 50%;
background: #047857;
position: absolute;
offset-rotate: 0deg;
animation: flowParticle 1.5s linear infinite;
box-shadow: 0 0 8px #04785766, 0 0 16px #04785733;
display: none;
pointer-events: none;
}
.overlay-active .flow-particle { display: block; }
/* Duration badge on nodes */
.duration-badge { display: none; }
.overlay-active .duration-badge { display: block; }
/* Iteration count badge on split nodes */
.iter-badge { display: none; }
.overlay-active .iter-badge { display: block; }
/* Execution detail panel */
.exec-panel {
max-width: 960px;
width: 100%;
margin-top: 16px;
background: #ffffff;
border: 1px solid #e4e0db;
border-radius: 14px;
overflow: hidden;
display: none;
box-shadow: 0 1px 3px rgba(0,0,0,0.04);
}
.exec-panel.visible { display: block; }
.exec-panel-header {
padding: 12px 16px;
border-bottom: 1px solid #e4e0db;
display: flex;
justify-content: space-between;
align-items: center;
}
.exec-panel-header h3 {
font-size: 14px;
font-weight: 600;
color: #1c1917;
}
.exec-panel-header .timing {
font-size: 12px;
color: #a8a29e;
}
.exec-steps {
display: flex;
overflow-x: auto;
padding: 16px;
gap: 0;
}
.exec-step {
flex-shrink: 0;
width: 180px;
position: relative;
}
.exec-step::after {
content: '';
position: absolute;
top: 18px;
right: -1px;
width: 100%;
height: 2px;
background: rgba(4, 120, 87, 0.3);
z-index: 0;
}
.exec-step:last-child::after { display: none; }
.step-dot {
width: 12px;
height: 12px;
border-radius: 50%;
background: #047857;
border: 2px solid #ffffff;
position: relative;
z-index: 1;
margin-bottom: 8px;
}
.step-label {
font-size: 11px;
font-weight: 600;
color: #1c1917;
margin-bottom: 2px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 160px;
}
.step-type {
font-size: 10px;
color: #a8a29e;
margin-bottom: 6px;
}
.step-duration {
font-size: 10px;
color: #047857;
font-weight: 500;
font-variant-numeric: tabular-nums;
}
.step-body {
font-size: 10px;
color: #a8a29e;
margin-top: 4px;
font-family: 'SF Mono', 'Fira Code', monospace;
line-height: 1.4;
max-width: 160px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.step-body .arrow { color: #d4cfc8; }
/* Iteration selector tabs */
.iter-tabs {
display: flex;
gap: 0;
padding: 0 16px;
border-bottom: 1px solid #e4e0db;
}
.iter-tab {
padding: 8px 16px;
font-size: 12px;
font-weight: 500;
color: #a8a29e;
cursor: pointer;
border-bottom: 2px solid transparent;
transition: color 0.15s, border-color 0.15s;
user-select: none;
}
.iter-tab:hover { color: #57534e; }
.iter-tab.active {
color: #047857;
border-bottom-color: #047857;
}
.iter-tab .iter-idx {
font-size: 10px;
padding: 1px 5px;
border-radius: 3px;
background: #f3f1ee;
margin-left: 4px;
}
.iter-tab.active .iter-idx {
background: rgba(4, 120, 87, 0.08);
color: #047857;
}
/* Legend */
.legend {
max-width: 960px;
width: 100%;
margin-top: 16px;
background: #ffffff;
border: 1px solid #e4e0db;
border-radius: 14px;
padding: 16px 20px;
display: flex;
flex-wrap: wrap;
gap: 28px;
box-shadow: 0 1px 3px rgba(0,0,0,0.04);
}
.legend-section h3 {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.8px;
color: #a8a29e;
margin-bottom: 8px;
}
.legend-items { display: flex; flex-wrap: wrap; gap: 14px; }
.legend-item {
display: inline-flex;
align-items: center;
gap: 7px;
font-size: 12px;
color: #57534e;
}
.legend-swatch {
width: 28px;
height: 16px;
border-radius: 4px;
border: 1.5px solid;
flex-shrink: 0;
}
.legend-line {
width: 28px;
height: 2px;
flex-shrink: 0;
border-radius: 1px;
}
.legend-line.dashed {
height: 0;
border-top: 2px dashed;
}
.legend-swatch.exec-glow {
background: rgba(4, 120, 87, 0.08);
border-color: rgba(4, 120, 87, 0.5);
box-shadow: 0 0 6px rgba(4, 120, 87, 0.25);
}
</style>
</head>
<body>
<header>
<h1>split-and-multicast</h1>
<div class="meta">
<span><span class="dot green"></span> Route active</span>
<span>From: <code style="color:#1d4ed8">direct:splitAndMulticast</code></span>
<span>Nodes: 8</span>
<span>Split: 3 iterations</span>
</div>
</header>
<div class="toolbar">
<button class="btn" id="toggleOverlay" onclick="toggleExecution()">
<span id="overlayIcon">\u25b6</span> Show Execution
<span class="kbd">E</span>
</button>
<span class="exec-badge" id="execBadge">
<span class="dot green"></span>
COMPLETED in 85ms (3 iterations)
</span>
<span style="flex:1"></span>
<span style="font-size:12px;color:#d4cfc8" id="execMeta"></span>
</div>
<div class="canvas-wrapper" id="canvas-wrapper">
<svg id="diagram" xmlns="http://www.w3.org/2000/svg"></svg>
</div>
<!-- Execution detail panel -->
<div class="exec-panel" id="execPanel">
<div class="exec-panel-header">
<h3>Execution Trace</h3>
<span class="timing" id="execTiming"></span>
</div>
<div class="iter-tabs" id="iterTabs"></div>
<div class="exec-steps" id="execSteps"></div>
</div>
<div class="legend">
<div class="legend-section">
<h3>Node Types</h3>
<div class="legend-items">
<span class="legend-item"><span class="legend-swatch" style="background:rgba(29,78,216,0.06);border-color:rgba(29,78,216,0.35)"></span>Endpoint</span>
<span class="legend-item"><span class="legend-swatch" style="background:rgba(124,58,237,0.06);border-color:rgba(124,58,237,0.35)"></span>EIP Pattern</span>
<span class="legend-item"><span class="legend-swatch" style="background:rgba(4,120,87,0.06);border-color:rgba(4,120,87,0.35)"></span>Processor</span>
<span class="legend-item"><span class="legend-swatch" style="background:rgba(190,18,60,0.05);border-color:rgba(190,18,60,0.3)"></span>Error</span>
<span class="legend-item"><span class="legend-swatch" style="background:transparent;border-color:rgba(14,116,144,0.35);border-style:dashed"></span>Cross-route</span>
</div>
</div>
<div class="legend-section">
<h3>Edge Types</h3>
<div class="legend-items">
<span class="legend-item"><span class="legend-line" style="background:#c4bfb8"></span>Flow</span>
<span class="legend-item"><span class="legend-line" style="background:#b45309"></span>Branch</span>
<span class="legend-item"><span class="legend-line dashed" style="border-color:#be123c"></span>Error</span>
<span class="legend-item"><span class="legend-line dashed" style="border-color:#0e7490"></span>Cross-route</span>
</div>
</div>
<div class="legend-section">
<h3>Execution Overlay</h3>
<div class="legend-items">
<span class="legend-item"><span class="legend-swatch exec-glow"></span>Executed node</span>
<span class="legend-item"><span class="legend-line" style="background:#047857"></span>Execution path</span>
<span class="legend-item"><span style="width:6px;height:6px;border-radius:50%;background:#047857;box-shadow:0 0 6px rgba(4,120,87,0.5);flex-shrink:0"></span>Flow particle</span>
<span class="legend-item"><span class="legend-swatch" style="background:#f3f1ee;border-color:#e4e0db;opacity:0.35"></span>Not executed</span>
</div>
</div>
</div>
<script>
// -- Route diagram data ----------------------------------------------------------
// Split-and-multicast route: receives an order, logs it, splits the items
// array, processes each item (log + setHeader + to mock:warehouse), then
// aggregates and sends to direct:audit.
const routeData = {
routeId: "split-and-multicast",
from: "direct:splitAndMulticast",
nodes: [
{ id: "n1", type: "DIRECT", label: "direct:splitAndMulticast", endpointUri: "direct:splitAndMulticast" },
{ id: "n2", type: "LOG", label: "Processing order items" },
{ id: "n3", type: "EIP_SPLIT", label: "split", expression: "${body.items}" },
{ id: "n4", type: "LOG", label: "Item: ${body.name}" },
{ id: "n5", type: "SET_HEADER", label: "setHeader: X-ItemId" },
{ id: "n6", type: "TO", label: "mock:warehouse", endpointUri: "mock:warehouse" },
{ id: "n7", type: "LOG", label: "All items processed" },
{ id: "n8", type: "TO", label: "direct:audit", endpointUri: "direct:audit", crossRoute: true }
],
edges: [
{ source: "n1", target: "n2", edgeType: "FLOW" },
{ source: "n2", target: "n3", edgeType: "FLOW" },
{ source: "n3", target: "n4", edgeType: "FLOW" },
{ source: "n4", target: "n5", edgeType: "FLOW" },
{ source: "n5", target: "n6", edgeType: "FLOW" },
{ source: "n3", target: "n7", edgeType: "FLOW" },
{ source: "n7", target: "n8", edgeType: "FLOW" },
{ source: "n8", target: "direct:audit", edgeType: "CROSS_ROUTE", label: "\u2192 audit" }
]
};
// -- Execution data (3-item split) ------------------------------------------------
// Simulates a real CAMELEER_EXECUTION JSON for an order with 3 items
2026-03-13 10:52:43 +01:00
// flowing through the split-and-multicast route.
const executionData = {
routeId: "split-and-multicast",
exchangeId: "ID-cameleer-dev-1741443600-0-2",
correlationId: "b4f9d3e2-8c5e-4f0b-a7d4-3e2g1f9b6d58",
status: "COMPLETED",
startTime: "2026-03-08T14:00:01.200Z",
endTime: "2026-03-08T14:00:01.285Z",
durationMs: 85,
inputSnapshot: {
body: '{"orderId":"ORD-2048","items":[{"name":"Widget A","qty":2},{"name":"Gadget B","qty":1},{"name":"Gizmo C","qty":5}]}',
headers: { "Content-Type": "application/json", "X-Cameleer-CorrelationId": "b4f9d3e2-8c5e-4f0b-a7d4-3e2g1f9b6d58" }
},
outputSnapshot: {
body: '{"orderId":"ORD-2048","items":[...],"processed":true}',
headers: { "Content-Type": "application/json", "X-Cameleer-CorrelationId": "b4f9d3e2-8c5e-4f0b-a7d4-3e2g1f9b6d58" }
},
// Processors before the split, then the split itself (with iterations as children)
processors: [
{
nodeId: "n1", processorType: "from", label: "direct:splitAndMulticast",
status: "COMPLETED", durationMs: 1,
inputBody: '{"orderId":"ORD-2048","items":[...3 items]}',
outputBody: '{"orderId":"ORD-2048","items":[...3 items]}'
},
{
nodeId: "n2", processorType: "log", label: "Processing order items",
status: "COMPLETED", durationMs: 2,
inputBody: '{"orderId":"ORD-2048","items":[...3 items]}',
outputBody: '{"orderId":"ORD-2048","items":[...3 items]}'
},
{
nodeId: "n3", processorType: "split", label: "split: ${body.items} \u00d7 3",
status: "COMPLETED", durationMs: 62,
splitSize: 3,
inputBody: '{"orderId":"ORD-2048","items":[...3 items]}',
outputBody: '{"orderId":"ORD-2048","items":[...3 items]}',
// Each iteration shows processors within that split sub-exchange
iterations: [
{
splitIndex: 0, splitSize: 3, status: "COMPLETED", durationMs: 18,
inputBody: '{"name":"Widget A","qty":2}',
processors: [
{ nodeId: "n4", processorType: "log", label: "Item: Widget A", status: "COMPLETED", durationMs: 2, inputBody: '{"name":"Widget A","qty":2}', outputBody: '{"name":"Widget A","qty":2}' },
{ nodeId: "n5", processorType: "setHeader", label: "X-ItemId = WIDGET-A", status: "COMPLETED", durationMs: 1, inputBody: '{"name":"Widget A","qty":2}', outputBody: '{"name":"Widget A","qty":2}', inputHeaders: {}, outputHeaders: { "X-ItemId": "WIDGET-A" } },
{ nodeId: "n6", processorType: "to", label: "\u2192 mock:warehouse", status: "COMPLETED", durationMs: 12, inputBody: '{"name":"Widget A","qty":2}', outputBody: '{"name":"Widget A","qty":2,"shipped":true}', endpointUri: "mock:warehouse" }
]
},
{
splitIndex: 1, splitSize: 3, status: "COMPLETED", durationMs: 20,
inputBody: '{"name":"Gadget B","qty":1}',
processors: [
{ nodeId: "n4", processorType: "log", label: "Item: Gadget B", status: "COMPLETED", durationMs: 2, inputBody: '{"name":"Gadget B","qty":1}', outputBody: '{"name":"Gadget B","qty":1}' },
{ nodeId: "n5", processorType: "setHeader", label: "X-ItemId = GADGET-B", status: "COMPLETED", durationMs: 1, inputBody: '{"name":"Gadget B","qty":1}', outputBody: '{"name":"Gadget B","qty":1}', inputHeaders: {}, outputHeaders: { "X-ItemId": "GADGET-B" } },
{ nodeId: "n6", processorType: "to", label: "\u2192 mock:warehouse", status: "COMPLETED", durationMs: 15, inputBody: '{"name":"Gadget B","qty":1}', outputBody: '{"name":"Gadget B","qty":1,"shipped":true}', endpointUri: "mock:warehouse" }
]
},
{
splitIndex: 2, splitSize: 3, status: "COMPLETED", durationMs: 16,
inputBody: '{"name":"Gizmo C","qty":5}',
processors: [
{ nodeId: "n4", processorType: "log", label: "Item: Gizmo C", status: "COMPLETED", durationMs: 1, inputBody: '{"name":"Gizmo C","qty":5}', outputBody: '{"name":"Gizmo C","qty":5}' },
{ nodeId: "n5", processorType: "setHeader", label: "X-ItemId = GIZMO-C", status: "COMPLETED", durationMs: 1, inputBody: '{"name":"Gizmo C","qty":5}', outputBody: '{"name":"Gizmo C","qty":5}', inputHeaders: {}, outputHeaders: { "X-ItemId": "GIZMO-C" } },
{ nodeId: "n6", processorType: "to", label: "\u2192 mock:warehouse", status: "COMPLETED", durationMs: 11, inputBody: '{"name":"Gizmo C","qty":5}', outputBody: '{"name":"Gizmo C","qty":5,"shipped":true}', endpointUri: "mock:warehouse" }
]
}
]
},
{
nodeId: "n7", processorType: "log", label: "All items processed",
status: "COMPLETED", durationMs: 2,
inputBody: '{"orderId":"ORD-2048","items":[...3 items]}',
outputBody: '{"orderId":"ORD-2048","items":[...3 items]}'
},
{
nodeId: "n8", processorType: "to", label: "\u2192 direct:audit",
status: "COMPLETED", durationMs: 15,
inputBody: '{"orderId":"ORD-2048","items":[...3 items]}',
outputBody: '{"orderId":"ORD-2048","items":[...3 items],"audited":true}',
endpointUri: "direct:audit"
}
]
};
// -- Build executed node/edge sets (handles split iterations) ---------------------
const executedNodes = new Set();
const executedEdges = new Set();
// Track per-node execution count for iteration badges
const nodeExecCounts = {};
// Aggregate duration per node (sum across iterations)
const nodeAggregateDuration = {};
function collectExecutedFromProcessors(procs) {
for (let i = 0; i < procs.length; i++) {
const p = procs[i];
executedNodes.add(p.nodeId);
nodeExecCounts[p.nodeId] = (nodeExecCounts[p.nodeId] || 0) + 1;
nodeAggregateDuration[p.nodeId] = (nodeAggregateDuration[p.nodeId] || 0) + p.durationMs;
if (i > 0) {
executedEdges.add(procs[i - 1].nodeId + "->" + p.nodeId);
}
// Recurse into split iterations
if (p.iterations) {
for (const iter of p.iterations) {
collectExecutedFromProcessors(iter.processors);
}
}
}
}
// Connect sequential processors at the top level
for (let i = 0; i < executionData.processors.length - 1; i++) {
executedEdges.add(executionData.processors[i].nodeId + "->" + executionData.processors[i + 1].nodeId);
}
// Also connect split node to its children and back to post-split
const splitProc = executionData.processors.find(p => p.iterations);
if (splitProc && splitProc.iterations.length > 0) {
const firstIterProc = splitProc.iterations[0].processors[0];
if (firstIterProc) executedEdges.add(splitProc.nodeId + "->" + firstIterProc.nodeId);
// Connect last iteration processor back to post-split
const splitIdx = executionData.processors.indexOf(splitProc);
const postSplit = executionData.processors[splitIdx + 1];
if (postSplit) {
for (const iter of splitProc.iterations) {
const lastP = iter.processors[iter.processors.length - 1];
if (lastP) executedEdges.add(lastP.nodeId + "->" + postSplit.nodeId);
}
executedEdges.add(splitProc.nodeId + "->" + postSplit.nodeId);
}
}
collectExecutedFromProcessors(executionData.processors);
// Current iteration index for detail panel (null = "All")
let currentIteration = null;
// -- Node style config (light theme) ---------------------------------------------
const STYLES = {
endpoint: { fill: "rgba(29, 78, 216, 0.06)", stroke: "rgba(29, 78, 216, 0.35)", text: "#1d4ed8", glow: "rgba(29, 78, 216, 0.3)", icon: "\u25c9" },
eip: { fill: "rgba(124, 58, 237, 0.06)", stroke: "rgba(124, 58, 237, 0.35)", text: "#7c3aed", glow: "rgba(124, 58, 237, 0.3)", icon: "\u25c6" },
processor: { fill: "rgba(4, 120, 87, 0.06)", stroke: "rgba(4, 120, 87, 0.35)", text: "#047857", glow: "rgba(4, 120, 87, 0.3)", icon: "\u25b8" },
error: { fill: "rgba(190, 18, 60, 0.05)", stroke: "rgba(190, 18, 60, 0.3)", text: "#be123c", glow: "rgba(190, 18, 60, 0.3)", icon: "\u26a0" },
crossRoute:{ fill: "rgba(14, 116, 144, 0.06)", stroke: "rgba(14, 116, 144, 0.35)", text: "#0e7490", glow: "rgba(14, 116, 144, 0.3)", icon: "\u2197" }
};
function nodeCategory(node) {
if (node.crossRoute) return "crossRoute";
const t = node.type;
if (["DIRECT","ENDPOINT","SEDA"].includes(t)) return "endpoint";
if (t.startsWith("EIP_")) return "eip";
if (["ERROR_HANDLER","ON_EXCEPTION","TRY_CATCH","DO_TRY","DO_CATCH","DO_FINALLY"].includes(t)) return "error";
if (t === "TO" || t === "TO_DYNAMIC") return "endpoint";
return "processor";
}
const EDGE_COLORS = {
FLOW: "#c4bfb8",
BRANCH: "#b45309",
ERROR: "#be123c",
CROSS_ROUTE: "#0e7490"
};
// -- Layout constants -------------------------------------------------------------
const NODE_W = 200;
const NODE_H = 40;
const V_GAP = 50;
const H_GAP = 40;
const PAD = 60;
// -- Build node lookup ------------------------------------------------------------
const nodeMap = {};
routeData.nodes.forEach(n => nodeMap[n.id] = n);
// -- Tree layout for split route --------------------------------------------------
// Linear chain: n1 -> n2 -> n3(split) -> n4 -> n5 -> n6 (split body)
// Then: n3 -> n7 -> n8 (post-split)
// Layout: vertical chain with split body indented to the right
const positions = {};
function layoutSplitRoute() {
const centerX = PAD + NODE_W / 2 + 60;
// Pre-split: n1, n2 centered
positions["n1"] = { x: centerX - NODE_W / 2, y: PAD };
positions["n2"] = { x: centerX - NODE_W / 2, y: PAD + (NODE_H + V_GAP) };
// Split node: n3
positions["n3"] = { x: centerX - NODE_W / 2, y: PAD + 2 * (NODE_H + V_GAP) };
// Split body (indented right): n4, n5, n6
const splitBodyX = centerX + NODE_W / 2 + H_GAP / 2;
const splitBodyStartY = PAD + 2 * (NODE_H + V_GAP) + 10;
positions["n4"] = { x: splitBodyX, y: splitBodyStartY + 0 * (NODE_H + V_GAP) };
positions["n5"] = { x: splitBodyX, y: splitBodyStartY + 1 * (NODE_H + V_GAP) };
positions["n6"] = { x: splitBodyX, y: splitBodyStartY + 2 * (NODE_H + V_GAP) };
// Post-split: n7, n8 centered (below split body)
const postSplitY = splitBodyStartY + 3 * (NODE_H + V_GAP);
positions["n7"] = { x: centerX - NODE_W / 2, y: postSplitY };
positions["n8"] = { x: centerX - NODE_W / 2, y: postSplitY + (NODE_H + V_GAP) };
}
layoutSplitRoute();
// -- Compute SVG dimensions -------------------------------------------------------
let maxX = 0, maxY = 0;
Object.values(positions).forEach(p => {
if (p.x + NODE_W > maxX) maxX = p.x + NODE_W;
if (p.y + NODE_H > maxY) maxY = p.y + NODE_H;
});
const svgW = maxX + PAD;
const svgH = maxY + PAD + 30;
// -- SVG rendering ----------------------------------------------------------------
const NS = "http://www.w3.org/2000/svg";
const svg = document.getElementById("diagram");
svg.setAttribute("width", svgW);
svg.setAttribute("height", svgH);
svg.setAttribute("viewBox", `0 0 ${svgW} ${svgH}`);
// Defs: arrow markers + execution glow filter
const defs = document.createElementNS(NS, "defs");
function createMarker(id, color) {
const marker = document.createElementNS(NS, "marker");
marker.setAttribute("id", id);
marker.setAttribute("viewBox", "0 0 10 7");
marker.setAttribute("refX", "10");
marker.setAttribute("refY", "3.5");
marker.setAttribute("markerWidth", "8");
marker.setAttribute("markerHeight", "6");
marker.setAttribute("orient", "auto");
const poly = document.createElementNS(NS, "polygon");
poly.setAttribute("points", "0 0, 10 3.5, 0 7");
poly.setAttribute("fill", color);
marker.appendChild(poly);
defs.appendChild(marker);
}
Object.entries(EDGE_COLORS).forEach(([type, color]) => {
createMarker("arrow-" + type, color);
});
// Green execution arrow
createMarker("arrow-exec", "#047857");
svg.appendChild(defs);
// -- Draw edges -------------------------------------------------------------------
const edgeGroup = document.createElementNS(NS, "g");
edgeGroup.setAttribute("class", "edges");
// Store edge elements and paths for overlay
const edgeElements = [];
routeData.edges.forEach(edge => {
const sp = positions[edge.source];
const tp = positions[edge.target];
if (!sp || !tp) return;
const color = EDGE_COLORS[edge.edgeType] || EDGE_COLORS.FLOW;
const isDashed = edge.edgeType === "ERROR" || edge.edgeType === "CROSS_ROUTE";
const edgeKey = edge.source + "->" + edge.target;
const isExecuted = executedEdges.has(edgeKey);
const sx = sp.x + NODE_W / 2;
const sy = sp.y + NODE_H;
const tx = tp.x + NODE_W / 2;
const ty = tp.y;
const path = document.createElementNS(NS, "path");
path.setAttribute("class", "edge-path");
path.dataset.edgeKey = edgeKey;
let d;
if (edge.edgeType === "CROSS_ROUTE") {
const sx2 = sp.x + NODE_W / 2;
const sy2 = sp.y + NODE_H;
const tx2 = tp.x + NODE_W / 2;
const ty2 = tp.y + NODE_H;
const midY = Math.max(sy2, ty2) + 25;
d = `M ${sx2} ${sy2} C ${sx2} ${midY}, ${tx2} ${midY}, ${tx2} ${ty2}`;
} else if (Math.abs(sx - tx) < 2) {
d = `M ${sx} ${sy} L ${tx} ${ty}`;
} else {
const midY = sy + (ty - sy) * 0.5;
d = `M ${sx} ${sy} C ${sx} ${midY}, ${tx} ${midY}, ${tx} ${ty}`;
}
path.setAttribute("d", d);
path.setAttribute("fill", "none");
path.setAttribute("stroke", color);
path.setAttribute("stroke-width", edge.edgeType === "BRANCH" ? "2" : "1.5");
if (isDashed) path.setAttribute("stroke-dasharray", "6 3");
path.setAttribute("marker-end", `url(#arrow-${edge.edgeType})`);
path.style.setProperty("--edge-glow", color);
path.style.transition = "opacity 0.3s, stroke 0.3s, stroke-width 0.3s";
edgeGroup.appendChild(path);
edgeElements.push({ el: path, edge, d, isExecuted });
// Edge label
if (edge.label && edge.edgeType !== "CROSS_ROUTE") {
const lx = (sx + tx) / 2 + 8;
const ly = (sy + ty) / 2;
const text = document.createElementNS(NS, "text");
text.setAttribute("x", lx);
text.setAttribute("y", ly);
text.setAttribute("fill", color);
text.setAttribute("font-size", "11");
text.setAttribute("font-weight", "500");
text.setAttribute("class", "edge-label");
text.dataset.edgeKey = edgeKey;
text.style.transition = "opacity 0.3s";
text.textContent = edge.label;
edgeGroup.appendChild(text);
}
});
svg.appendChild(edgeGroup);
// -- Execution path overlay edges (drawn on top, hidden by default) ---------------
const execEdgeGroup = document.createElementNS(NS, "g");
execEdgeGroup.setAttribute("class", "exec-edges");
execEdgeGroup.style.display = "none";
edgeElements.filter(e => e.isExecuted).forEach(({ d, edge }) => {
const path = document.createElementNS(NS, "path");
path.setAttribute("d", d);
path.setAttribute("fill", "none");
path.setAttribute("stroke", "#047857");
path.setAttribute("stroke-width", "2.5");
path.setAttribute("marker-end", "url(#arrow-exec)");
path.style.setProperty("--edge-glow", "#047857");
path.style.filter = "drop-shadow(0 0 4px rgba(4, 120, 87, 0.4))";
execEdgeGroup.appendChild(path);
});
svg.appendChild(execEdgeGroup);
// -- Draw nodes -------------------------------------------------------------------
const nodeGroup = document.createElementNS(NS, "g");
nodeGroup.setAttribute("class", "nodes");
const nodeElements = {};
routeData.nodes.forEach(node => {
const pos = positions[node.id];
if (!pos) return;
const cat = nodeCategory(node);
const style = STYLES[cat];
const isExec = executedNodes.has(node.id);
const g = document.createElementNS(NS, "g");
g.setAttribute("class", "node-group");
g.dataset.nodeId = node.id;
g.style.setProperty("--glow-color", style.glow);
// Tooltip -- enhanced with execution data when available
const title = document.createElementNS(NS, "title");
let tip = `${node.type}: ${node.label}`;
if (node.expression) tip += `\nExpression: ${node.expression}`;
if (node.endpointUri) tip += `\nURI: ${node.endpointUri}`;
// Find execution data (top-level processor or aggregated from iterations)
const execProc = executionData.processors.find(p => p.nodeId === node.id);
const execCount = nodeExecCounts[node.id] || 0;
const aggDuration = nodeAggregateDuration[node.id] || 0;
if (execProc) {
tip += `\n\u2500\u2500\u2500 Execution \u2500\u2500\u2500`;
tip += `\nStatus: ${execProc.status}`;
tip += `\nDuration: ${execProc.durationMs}ms`;
if (execProc.splitSize) tip += `\nSplit: ${execProc.splitSize} iterations, ${aggDuration}ms total`;
if (execProc.inputBody) tip += `\nInput: ${execProc.inputBody}`;
if (execProc.outputBody) tip += `\nOutput: ${execProc.outputBody}`;
} else if (execCount > 1) {
tip += `\n\u2500\u2500\u2500 Execution \u2500\u2500\u2500`;
tip += `\nExecuted ${execCount}\u00d7 across split iterations`;
tip += `\nAggregate duration: ${aggDuration}ms`;
}
title.textContent = tip;
g.appendChild(title);
// Rect
const rect = document.createElementNS(NS, "rect");
rect.setAttribute("class", "node-rect");
rect.setAttribute("x", pos.x);
rect.setAttribute("y", pos.y);
rect.setAttribute("width", NODE_W);
rect.setAttribute("height", NODE_H);
rect.setAttribute("rx", "8");
rect.setAttribute("fill", style.fill);
rect.setAttribute("stroke", style.stroke);
rect.setAttribute("stroke-width", "1.5");
if (node.crossRoute) rect.setAttribute("stroke-dasharray", "5 3");
g.appendChild(rect);
// Icon
const icon = document.createElementNS(NS, "text");
icon.setAttribute("x", pos.x + 14);
icon.setAttribute("y", pos.y + NODE_H / 2 + 1);
icon.setAttribute("fill", style.text);
icon.setAttribute("font-size", "14");
icon.setAttribute("dominant-baseline", "central");
icon.textContent = style.icon;
g.appendChild(icon);
// Label
let labelText = node.label;
if (labelText.length > 24) labelText = labelText.substring(0, 22) + "\u2026";
const label = document.createElementNS(NS, "text");
label.setAttribute("x", pos.x + 30);
label.setAttribute("y", pos.y + NODE_H / 2 + 1);
label.setAttribute("fill", "#1c1917");
label.setAttribute("font-size", "12.5");
label.setAttribute("font-weight", "500");
label.setAttribute("dominant-baseline", "central");
label.textContent = labelText;
g.appendChild(label);
// Type badge below node
const badge = document.createElementNS(NS, "text");
badge.setAttribute("x", pos.x + NODE_W / 2);
badge.setAttribute("y", pos.y + NODE_H + 14);
badge.setAttribute("fill", style.text);
badge.setAttribute("font-size", "9.5");
badge.setAttribute("text-anchor", "middle");
badge.textContent = node.type.replace("EIP_", "");
g.appendChild(badge);
// Duration badge (only visible during execution overlay)
const showDuration = execProc || execCount > 0;
if (showDuration) {
const displayDuration = execProc ? execProc.durationMs : aggDuration;
const dbg = document.createElementNS(NS, "g");
dbg.setAttribute("class", "duration-badge");
const pillW = 42;
const pillH = 16;
const px = pos.x + NODE_W - pillW - 4;
const py = pos.y - pillH - 3;
const pill = document.createElementNS(NS, "rect");
pill.setAttribute("x", px);
pill.setAttribute("y", py);
pill.setAttribute("width", pillW);
pill.setAttribute("height", pillH);
pill.setAttribute("rx", "8");
pill.setAttribute("fill", "#ffffff");
pill.setAttribute("stroke", "#e4e0db");
pill.setAttribute("stroke-width", "1");
dbg.appendChild(pill);
const dtext = document.createElementNS(NS, "text");
dtext.setAttribute("x", px + pillW / 2);
dtext.setAttribute("y", py + pillH / 2 + 1);
// Color based on speed: green < 10ms, amber 10-50ms, rose > 50ms
let durColor = "#047857";
if (displayDuration > 50) durColor = "#be123c";
else if (displayDuration >= 10) durColor = "#b45309";
dtext.setAttribute("fill", durColor);
dtext.setAttribute("font-size", "10");
dtext.setAttribute("font-weight", "600");
dtext.setAttribute("text-anchor", "middle");
dtext.setAttribute("dominant-baseline", "central");
dtext.setAttribute("font-variant-numeric", "tabular-nums");
dtext.textContent = `${displayDuration}ms`;
dbg.appendChild(dtext);
g.appendChild(dbg);
}
// Iteration count badge for split nodes (e.g., "x3")
if (execProc && execProc.splitSize) {
const ibg = document.createElementNS(NS, "g");
ibg.setAttribute("class", "iter-badge");
const ibW = 28;
const ibH = 16;
const ibx = pos.x + 4;
const iby = pos.y - ibH - 3;
const ibPill = document.createElementNS(NS, "rect");
ibPill.setAttribute("x", ibx);
ibPill.setAttribute("y", iby);
ibPill.setAttribute("width", ibW);
ibPill.setAttribute("height", ibH);
ibPill.setAttribute("rx", "8");
ibPill.setAttribute("fill", "#047857");
ibPill.setAttribute("stroke", "none");
ibg.appendChild(ibPill);
const ibText = document.createElementNS(NS, "text");
ibText.setAttribute("x", ibx + ibW / 2);
ibText.setAttribute("y", iby + ibH / 2 + 1);
ibText.setAttribute("fill", "#ffffff");
ibText.setAttribute("font-size", "10");
ibText.setAttribute("font-weight", "700");
ibText.setAttribute("text-anchor", "middle");
ibText.setAttribute("dominant-baseline", "central");
ibText.textContent = `\u00d7${execProc.splitSize}`;
ibg.appendChild(ibText);
g.appendChild(ibg);
}
// Iteration count badge for nodes executed multiple times (inside split body)
if (!execProc && execCount > 1) {
const ibg = document.createElementNS(NS, "g");
ibg.setAttribute("class", "iter-badge");
const ibW = 28;
const ibH = 16;
const ibx = pos.x + 4;
const iby = pos.y - ibH - 3;
const ibPill = document.createElementNS(NS, "rect");
ibPill.setAttribute("x", ibx);
ibPill.setAttribute("y", iby);
ibPill.setAttribute("width", ibW);
ibPill.setAttribute("height", ibH);
ibPill.setAttribute("rx", "8");
ibPill.setAttribute("fill", "#047857");
ibPill.setAttribute("stroke", "none");
ibg.appendChild(ibPill);
const ibText = document.createElementNS(NS, "text");
ibText.setAttribute("x", ibx + ibW / 2);
ibText.setAttribute("y", iby + ibH / 2 + 1);
ibText.setAttribute("fill", "#ffffff");
ibText.setAttribute("font-size", "10");
ibText.setAttribute("font-weight", "700");
ibText.setAttribute("text-anchor", "middle");
ibText.setAttribute("dominant-baseline", "central");
ibText.textContent = `\u00d7${execCount}`;
ibg.appendChild(ibText);
g.appendChild(ibg);
}
// Cross-route external route pill
if (node.crossRoute) {
const pillW = 130;
const pillH = 18;
const px = pos.x + (NODE_W - pillW) / 2;
const py = pos.y + NODE_H + 22;
const pill = document.createElementNS(NS, "rect");
pill.setAttribute("x", px);
pill.setAttribute("y", py);
pill.setAttribute("width", pillW);
pill.setAttribute("height", pillH);
pill.setAttribute("rx", "9");
pill.setAttribute("fill", "rgba(14, 116, 144, 0.06)");
pill.setAttribute("stroke", "rgba(14, 116, 144, 0.35)");
pill.setAttribute("stroke-width", "1");
pill.setAttribute("stroke-dasharray", "3 2");
g.appendChild(pill);
const pillText = document.createElementNS(NS, "text");
pillText.setAttribute("x", px + pillW / 2);
pillText.setAttribute("y", py + pillH / 2 + 1);
pillText.setAttribute("fill", "#0e7490");
pillText.setAttribute("font-size", "9.5");
pillText.setAttribute("text-anchor", "middle");
pillText.setAttribute("dominant-baseline", "central");
pillText.textContent = "\u2197 external route";
g.appendChild(pillText);
}
nodeElements[node.id] = g;
nodeGroup.appendChild(g);
});
svg.appendChild(nodeGroup);
// -- Execution sequence number overlay (hidden by default) ------------------------
const seqGroup = document.createElementNS(NS, "g");
seqGroup.setAttribute("class", "seq-numbers");
seqGroup.style.display = "none";
// Flatten top-level processor sequence (skip iteration children for seq numbers)
const topLevelSeq = executionData.processors.filter(p => !p.iterations || true);
const seenSeqNodes = new Set();
topLevelSeq.forEach((proc, i) => {
if (seenSeqNodes.has(proc.nodeId)) return;
seenSeqNodes.add(proc.nodeId);
const pos = positions[proc.nodeId];
if (!pos) return;
const cx = pos.x + 10;
const cy = pos.y + 10;
const circle = document.createElementNS(NS, "circle");
circle.setAttribute("cx", cx);
circle.setAttribute("cy", cy);
circle.setAttribute("r", "9");
circle.setAttribute("fill", "#047857");
circle.setAttribute("stroke", "#ffffff");
circle.setAttribute("stroke-width", "2");
seqGroup.appendChild(circle);
const num = document.createElementNS(NS, "text");
num.setAttribute("x", cx);
num.setAttribute("y", cy + 1);
num.setAttribute("fill", "#ffffff");
num.setAttribute("font-size", "10");
num.setAttribute("font-weight", "700");
num.setAttribute("text-anchor", "middle");
num.setAttribute("dominant-baseline", "central");
num.textContent = i + 1;
seqGroup.appendChild(num);
});
// Add sequence numbers for split body nodes (using sub-numbering like 3a, 3b, 3c)
if (splitProc && splitProc.iterations.length > 0) {
const splitIdx = executionData.processors.indexOf(splitProc);
const firstIter = splitProc.iterations[0];
firstIter.processors.forEach((proc, j) => {
if (seenSeqNodes.has(proc.nodeId)) return;
seenSeqNodes.add(proc.nodeId);
const pos = positions[proc.nodeId];
if (!pos) return;
const cx = pos.x + 10;
const cy = pos.y + 10;
const circle = document.createElementNS(NS, "circle");
circle.setAttribute("cx", cx);
circle.setAttribute("cy", cy);
circle.setAttribute("r", "9");
circle.setAttribute("fill", "#047857");
circle.setAttribute("stroke", "#ffffff");
circle.setAttribute("stroke-width", "2");
seqGroup.appendChild(circle);
const num = document.createElementNS(NS, "text");
num.setAttribute("x", cx);
num.setAttribute("y", cy + 1);
num.setAttribute("fill", "#ffffff");
num.setAttribute("font-size", "9");
num.setAttribute("font-weight", "700");
num.setAttribute("text-anchor", "middle");
num.setAttribute("dominant-baseline", "central");
num.textContent = `${splitIdx + 1}${String.fromCharCode(97 + j)}`;
seqGroup.appendChild(num);
});
}
svg.appendChild(seqGroup);
// -- Build execution detail panel with iteration tabs -----------------------------
function buildExecPanel() {
const tabsEl = document.getElementById("iterTabs");
const stepsEl = document.getElementById("execSteps");
const timingEl = document.getElementById("execTiming");
timingEl.textContent = `Exchange: ${executionData.exchangeId} \u00b7 ${executionData.startTime} \u00b7 ${executionData.durationMs}ms total`;
// Build iteration tabs if there's a split processor
if (splitProc && splitProc.iterations.length > 0) {
// "All" tab
const allTab = document.createElement("div");
allTab.className = "iter-tab active";
allTab.textContent = "All Steps";
allTab.onclick = () => selectIteration(null);
tabsEl.appendChild(allTab);
splitProc.iterations.forEach((iter, i) => {
const tab = document.createElement("div");
tab.className = "iter-tab";
tab.innerHTML = `Iteration <span class="iter-idx">${i}</span>`;
tab.title = `Split item: ${iter.inputBody}`;
tab.onclick = () => selectIteration(i);
tabsEl.appendChild(tab);
});
}
renderSteps(null);
}
function selectIteration(iterIdx) {
currentIteration = iterIdx;
// Update tab active state
document.querySelectorAll(".iter-tab").forEach((tab, i) => {
tab.classList.toggle("active", iterIdx === null ? i === 0 : i === iterIdx + 1);
});
renderSteps(iterIdx);
}
function renderSteps(iterIdx) {
const stepsEl = document.getElementById("execSteps");
stepsEl.innerHTML = "";
let procs;
if (iterIdx === null) {
// Show all top-level processors (split shows as aggregate)
procs = executionData.processors;
} else {
// Show pre-split procs + specific iteration procs + post-split procs
const splitIndex = executionData.processors.indexOf(splitProc);
const preSplit = executionData.processors.slice(0, splitIndex);
const iterProcs = splitProc.iterations[iterIdx].processors;
const postSplit = executionData.processors.slice(splitIndex + 1);
procs = [...preSplit, ...iterProcs, ...postSplit];
}
procs.forEach((proc, i) => {
const step = document.createElement("div");
step.className = "exec-step";
const dot = document.createElement("div");
dot.className = "step-dot";
if (proc.status === "FAILED") dot.style.background = "#be123c";
step.appendChild(dot);
const lbl = document.createElement("div");
lbl.className = "step-label";
let labelPrefix = `${i + 1}. `;
if (proc.splitIndex !== undefined) labelPrefix = `${i + 1}. [${proc.splitIndex}] `;
lbl.textContent = labelPrefix + proc.label;
lbl.title = proc.label;
step.appendChild(lbl);
const type = document.createElement("div");
type.className = "step-type";
let typeText = proc.processorType;
if (proc.iterations) typeText += ` (\u00d7${proc.splitSize})`;
type.textContent = typeText;
step.appendChild(type);
const dur = document.createElement("div");
dur.className = "step-duration";
dur.textContent = `${proc.durationMs}ms`;
step.appendChild(dur);
// Show body details
if (proc.inputBody && proc.outputBody) {
const bodyDiv = document.createElement("div");
bodyDiv.className = "step-body";
if (proc.outputHeaders && proc.inputHeaders) {
const newKeys = Object.keys(proc.outputHeaders).filter(k => !(k in proc.inputHeaders));
if (newKeys.length > 0) {
bodyDiv.textContent = `+${newKeys.map(k => `${k}: ${proc.outputHeaders[k]}`).join(", ")}`;
bodyDiv.style.color = "#047857";
}
}
if (proc.processorType === "to" && proc.inputBody !== proc.outputBody) {
const outObj = proc.outputBody;
bodyDiv.innerHTML = `<span class="arrow">\u2192</span> body changed`;
bodyDiv.style.color = "#047857";
}
if (proc.iterations) {
bodyDiv.textContent = `${proc.splitSize} iterations, ${proc.iterations.reduce((s, it) => s + it.durationMs, 0)}ms total`;
bodyDiv.style.color = "#1d4ed8";
}
if (bodyDiv.textContent || bodyDiv.innerHTML) step.appendChild(bodyDiv);
}
stepsEl.appendChild(step);
});
}
buildExecPanel();
// -- Toggle execution overlay -----------------------------------------------------
let overlayActive = false;
function toggleExecution() {
overlayActive = !overlayActive;
const svgEl = document.getElementById("diagram");
const btn = document.getElementById("toggleOverlay");
const badge = document.getElementById("execBadge");
const panel = document.getElementById("execPanel");
const meta = document.getElementById("execMeta");
const iconSpan = document.getElementById("overlayIcon");
if (overlayActive) {
// Activate overlay
svgEl.classList.add("overlay-active");
btn.classList.add("active");
btn.innerHTML = '<span id="overlayIcon">\u25a0</span> Hide Execution <span class="kbd">E</span>';
badge.className = "exec-badge visible completed";
panel.classList.add("visible");
meta.textContent = `Correlation: ${executionData.correlationId}`;
// Dim non-executed nodes and edges
routeData.nodes.forEach(node => {
const el = nodeElements[node.id];
if (!el) return;
if (executedNodes.has(node.id)) {
el.classList.add("executed");
el.classList.remove("dimmed");
} else {
el.classList.add("dimmed");
el.classList.remove("executed");
}
});
document.querySelectorAll(".edge-path").forEach(el => {
const key = el.dataset.edgeKey;
if (executedEdges.has(key)) {
el.classList.add("executed");
el.classList.remove("dimmed");
} else {
el.classList.add("dimmed");
el.classList.remove("executed");
}
});
document.querySelectorAll(".edge-label").forEach(el => {
const key = el.dataset.edgeKey;
el.style.opacity = executedEdges.has(key) ? "1" : "0.15";
});
// Show execution edges + sequence numbers
execEdgeGroup.style.display = "";
seqGroup.style.display = "";
} else {
// Deactivate overlay
svgEl.classList.remove("overlay-active");
btn.classList.remove("active");
btn.innerHTML = '<span id="overlayIcon">\u25b6</span> Show Execution <span class="kbd">E</span>';
badge.className = "exec-badge";
panel.classList.remove("visible");
meta.textContent = "";
// Remove dim/executed classes
routeData.nodes.forEach(node => {
const el = nodeElements[node.id];
if (!el) return;
el.classList.remove("executed", "dimmed");
});
document.querySelectorAll(".edge-path").forEach(el => {
el.classList.remove("executed", "dimmed");
});
document.querySelectorAll(".edge-label").forEach(el => {
el.style.opacity = "";
});
execEdgeGroup.style.display = "none";
seqGroup.style.display = "none";
}
}
// Keyboard shortcut: E to toggle
document.addEventListener("keydown", (e) => {
if (e.key === "e" || e.key === "E") {
if (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA") return;
toggleExecution();
}
});
</script>
</body>
</html>