Files
cameleer-server/examples/route-detail.html

1404 lines
45 KiB
HTML
Raw 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>Cameleer3 — Route Detail: content-based-routing</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700&family=JetBrains+Mono:wght@300;400;500;600&display=swap" rel="stylesheet">
<style>
:root {
--bg-deep: #060a13;
--bg-base: #0a0e17;
--bg-surface: #111827;
--bg-raised: #1a2332;
--bg-hover: #1e2d3d;
--border: #1e2d3d;
--border-subtle: #152030;
--text-primary: #e2e8f0;
--text-secondary: #8b9cb6;
--text-muted: #4a5e7a;
--amber: #f0b429;
--amber-dim: #b8860b;
--amber-glow: rgba(240, 180, 41, 0.15);
--cyan: #22d3ee;
--cyan-dim: #0e7490;
--cyan-glow: rgba(34, 211, 238, 0.12);
--rose: #f43f5e;
--rose-dim: #9f1239;
--rose-glow: rgba(244, 63, 94, 0.12);
--blue: #3b82f6;
--green: #10b981;
--green-glow: rgba(16, 185, 129, 0.12);
--purple: #a855f7;
--font-body: 'DM Sans', system-ui, sans-serif;
--font-mono: 'JetBrains Mono', 'Fira Code', monospace;
--radius-sm: 6px;
--radius-md: 10px;
--radius-lg: 14px;
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
background: var(--bg-deep);
color: var(--text-primary);
font-family: var(--font-body);
font-size: 14px;
line-height: 1.5;
min-height: 100vh;
}
body::before {
content: '';
position: fixed;
inset: 0;
background:
radial-gradient(ellipse 800px 400px at 20% 20%, rgba(240, 180, 41, 0.03), transparent),
radial-gradient(ellipse 600px 600px at 80% 80%, rgba(34, 211, 238, 0.02), transparent);
pointer-events: none;
z-index: 0;
}
body::after {
content: '';
position: fixed;
inset: 0;
opacity: 0.025;
background-image: url("data:image/svg+xml,%3Csvg width='400' height='400' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 200 Q100 150 200 200 T400 200' fill='none' stroke='%23f0b429' stroke-width='1'/%3E%3Cpath d='M0 220 Q120 170 200 220 T400 220' fill='none' stroke='%23f0b429' stroke-width='0.5'/%3E%3Cpath d='M0 180 Q80 130 200 180 T400 180' fill='none' stroke='%23f0b429' stroke-width='0.5'/%3E%3Cpath d='M0 100 Q150 60 200 100 T400 100' fill='none' stroke='%2322d3ee' stroke-width='0.5'/%3E%3Cpath d='M0 300 Q100 260 200 300 T400 300' fill='none' stroke='%2322d3ee' stroke-width='0.5'/%3E%3C/svg%3E");
background-size: 400px 400px;
pointer-events: none;
z-index: 0;
}
a { color: var(--amber); text-decoration: none; }
a:hover { color: var(--text-primary); }
.topnav {
position: sticky;
top: 0;
z-index: 100;
background: rgba(6, 10, 19, 0.85);
backdrop-filter: blur(20px) saturate(1.2);
border-bottom: 1px solid var(--border-subtle);
padding: 0 24px;
display: flex;
align-items: center;
height: 56px;
gap: 32px;
}
.topnav .logo {
font-family: var(--font-mono);
font-weight: 600;
font-size: 16px;
color: var(--amber);
letter-spacing: -0.5px;
display: flex;
align-items: center;
gap: 10px;
flex-shrink: 0;
}
.topnav .logo svg { width: 22px; height: 22px; }
.topnav .nav-links {
display: flex;
gap: 4px;
list-style: none;
}
.topnav .nav-links a {
padding: 8px 16px;
border-radius: var(--radius-sm);
font-size: 13px;
font-weight: 500;
color: var(--text-secondary);
transition: all 0.15s;
}
.topnav .nav-links a:hover { color: var(--text-primary); background: var(--bg-raised); }
.topnav .nav-links a.active { color: var(--amber); background: var(--amber-glow); }
.topnav .nav-right {
margin-left: auto;
display: flex;
align-items: center;
gap: 16px;
}
.env-badge {
font-family: var(--font-mono);
font-size: 11px;
padding: 4px 10px;
border-radius: 99px;
background: var(--green-glow);
color: var(--green);
border: 1px solid rgba(16, 185, 129, 0.2);
font-weight: 500;
}
.cluster-count {
font-size: 12px;
color: var(--text-muted);
font-family: var(--font-mono);
}
.main {
position: relative;
z-index: 1;
max-width: 1440px;
margin: 0 auto;
padding: 24px;
}
/* ─── Breadcrumb ─── */
.breadcrumb {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: var(--text-muted);
margin-bottom: 16px;
}
.breadcrumb a { color: var(--text-secondary); font-weight: 500; }
.breadcrumb a:hover { color: var(--amber); }
.breadcrumb .sep { color: var(--border); }
/* ─── Route Header ─── */
.route-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 24px;
margin-bottom: 24px;
padding: 24px;
background: var(--bg-surface);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-lg);
position: relative;
overflow: hidden;
}
.route-header::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 2px;
background: linear-gradient(90deg, var(--amber), var(--cyan), transparent);
}
.route-header-left h1 {
font-family: var(--font-mono);
font-size: 22px;
font-weight: 600;
letter-spacing: -0.5px;
margin-bottom: 6px;
}
.route-meta {
display: flex;
gap: 20px;
flex-wrap: wrap;
align-items: center;
}
.route-meta-item {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: var(--text-muted);
}
.route-meta-item strong { color: var(--text-secondary); font-weight: 600; }
.route-meta-item .dot { width: 7px; height: 7px; border-radius: 50%; }
.route-header-stats {
display: flex;
gap: 24px;
flex-shrink: 0;
}
.route-stat {
text-align: center;
}
.route-stat .val {
font-family: var(--font-mono);
font-size: 22px;
font-weight: 600;
letter-spacing: -0.5px;
}
.route-stat .lbl {
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.8px;
color: var(--text-muted);
margin-top: 2px;
}
/* ─── Tabs ─── */
.tabs {
display: flex;
gap: 2px;
margin-bottom: 20px;
border-bottom: 1px solid var(--border-subtle);
padding-bottom: 0;
}
.tab {
padding: 10px 20px;
font-size: 13px;
font-weight: 500;
color: var(--text-muted);
cursor: pointer;
border-bottom: 2px solid transparent;
margin-bottom: -1px;
transition: all 0.15s;
}
.tab:hover { color: var(--text-secondary); }
.tab.active { color: var(--amber); border-bottom-color: var(--amber); }
.tab-panel { display: none; }
.tab-panel.active { display: block; }
/* ─── Cards ─── */
.card {
background: var(--bg-surface);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-lg);
overflow: hidden;
transition: border-color 0.2s;
}
.card:hover { border-color: var(--border); }
.card-header {
padding: 16px 20px 12px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid var(--border-subtle);
}
.card-title {
font-size: 13px;
font-weight: 600;
color: var(--text-primary);
}
.card-subtitle {
font-size: 11px;
color: var(--text-muted);
font-family: var(--font-mono);
}
.card-body { padding: 20px; }
/* ─── Route Diagram ─── */
.diagram-container {
background: var(--bg-base);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-md);
padding: 24px;
overflow-x: auto;
position: relative;
min-height: 400px;
}
.diagram-toolbar {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 16px;
}
.diagram-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 14px;
border-radius: var(--radius-sm);
border: 1px solid var(--border);
background: var(--bg-raised);
color: var(--text-secondary);
font-family: var(--font-body);
font-size: 12px;
font-weight: 500;
cursor: pointer;
transition: all 0.15s;
}
.diagram-btn:hover { background: var(--bg-hover); color: var(--text-primary); }
.diagram-btn.active { background: var(--green-glow); border-color: rgba(16,185,129,0.3); color: var(--green); }
.diagram-btn .kbd {
font-size: 9px;
padding: 1px 5px;
border-radius: 3px;
background: var(--bg-base);
border: 1px solid var(--border);
color: var(--text-muted);
font-family: var(--font-mono);
}
.exec-badge {
font-size: 11px;
padding: 4px 10px;
border-radius: 99px;
font-weight: 600;
font-family: var(--font-mono);
background: rgba(16,185,129,0.1);
color: var(--green);
border: 1px solid rgba(16,185,129,0.2);
margin-left: auto;
}
/* SVG Diagram */
.diagram-svg {
width: 100%;
min-width: 800px;
}
.diagram-svg text { font-family: var(--font-mono); }
.node-group { cursor: pointer; transition: opacity 0.2s; }
.node-group:hover .node-rect {
filter: drop-shadow(0 0 8px var(--glow-color, rgba(240,180,41,0.5)));
}
.overlay-mode .node-group.dim { opacity: 0.15; }
.overlay-mode .node-group.dim:hover { opacity: 0.4; }
.overlay-mode .edge-path.dim { opacity: 0.08; }
.overlay-mode .node-group.hot .node-rect {
filter: drop-shadow(0 0 12px rgba(16,185,129,0.6));
}
/* ─── Grid layouts ─── */
.grid-2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin-bottom: 16px;
}
.grid-3 {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 16px;
margin-bottom: 16px;
}
/* ─── Recent Executions Timeline ─── */
.timeline {
position: relative;
}
.timeline::before {
content: '';
position: absolute;
left: 20px;
top: 0;
bottom: 0;
width: 1px;
background: var(--border);
}
.timeline-item {
display: flex;
gap: 16px;
padding: 12px 0;
position: relative;
cursor: pointer;
transition: background 0.1s;
border-radius: var(--radius-sm);
padding-left: 4px;
}
.timeline-item:hover { background: var(--bg-raised); }
.timeline-dot {
width: 12px;
height: 12px;
border-radius: 50%;
flex-shrink: 0;
margin-top: 4px;
z-index: 1;
border: 2px solid var(--bg-surface);
}
.timeline-dot.completed { background: var(--green); box-shadow: 0 0 0 3px var(--green-glow); }
.timeline-dot.failed { background: var(--rose); box-shadow: 0 0 0 3px var(--rose-glow); }
.timeline-content { flex: 1; min-width: 0; }
.timeline-top {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.timeline-id {
font-family: var(--font-mono);
font-size: 12px;
color: var(--text-primary);
font-weight: 500;
}
.timeline-time {
font-family: var(--font-mono);
font-size: 11px;
color: var(--text-muted);
flex-shrink: 0;
}
.timeline-details {
display: flex;
gap: 16px;
margin-top: 4px;
font-size: 11px;
color: var(--text-muted);
}
.timeline-details span {
display: flex;
align-items: center;
gap: 4px;
}
.timeline-error {
font-family: var(--font-mono);
font-size: 11px;
color: var(--rose);
margin-top: 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* ─── Performance Histogram ─── */
.histogram {
display: flex;
align-items: flex-end;
gap: 3px;
height: 140px;
padding-top: 10px;
}
.histogram-bar-wrap {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
height: 100%;
justify-content: flex-end;
cursor: pointer;
position: relative;
}
.histogram-bar {
width: 100%;
border-radius: 3px 3px 0 0;
transition: all 0.2s;
min-height: 2px;
position: relative;
}
.histogram-bar-wrap:hover .histogram-bar {
filter: brightness(1.3);
box-shadow: 0 0 8px rgba(240, 180, 41, 0.3);
}
.histogram-bar-wrap .bar-tooltip {
display: none;
position: absolute;
bottom: calc(100% + 8px);
left: 50%;
transform: translateX(-50%);
background: var(--bg-raised);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 6px 10px;
font-size: 11px;
font-family: var(--font-mono);
color: var(--text-primary);
white-space: nowrap;
z-index: 10;
pointer-events: none;
}
.histogram-bar-wrap:hover .bar-tooltip { display: block; }
.histogram-labels {
display: flex;
gap: 3px;
margin-top: 6px;
}
.histogram-labels span {
flex: 1;
text-align: center;
font-family: var(--font-mono);
font-size: 9px;
color: var(--text-muted);
}
.histogram-percentiles {
display: flex;
gap: 24px;
margin-top: 16px;
padding-top: 12px;
border-top: 1px solid var(--border-subtle);
}
.percentile {
text-align: center;
}
.percentile .pval {
font-family: var(--font-mono);
font-size: 18px;
font-weight: 600;
letter-spacing: -0.5px;
}
.percentile .plbl {
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--text-muted);
margin-top: 2px;
}
/* ─── Throughput Chart ─── */
.svg-chart {
width: 100%;
overflow: visible;
}
.svg-chart text {
font-family: var(--font-mono);
fill: var(--text-muted);
font-size: 10px;
}
.chart-legend {
display: flex;
gap: 16px;
margin-top: 12px;
}
.chart-legend .legend-item {
display: flex;
align-items: center;
gap: 6px;
font-size: 11px;
color: var(--text-muted);
}
.chart-legend .legend-dot {
width: 8px;
height: 8px;
border-radius: 2px;
}
/* ─── Processor Table ─── */
.proc-table {
width: 100%;
border-collapse: collapse;
font-size: 12px;
}
.proc-table thead th {
padding: 10px 12px;
text-align: left;
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.8px;
color: var(--text-muted);
border-bottom: 1px solid var(--border);
background: var(--bg-raised);
}
.proc-table tbody td {
padding: 10px 12px;
border-bottom: 1px solid var(--border-subtle);
vertical-align: middle;
}
.proc-table tbody tr:hover { background: var(--bg-raised); }
.proc-type-badge {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
}
.proc-type-badge.endpoint { background: rgba(59,130,246,0.12); color: var(--blue); }
.proc-type-badge.eip { background: rgba(168,85,247,0.12); color: var(--purple); }
.proc-type-badge.processor { background: var(--green-glow); color: var(--green); }
.pct-bar {
width: 60px;
height: 4px;
background: var(--bg-base);
border-radius: 2px;
overflow: hidden;
display: inline-block;
vertical-align: middle;
margin-right: 6px;
}
.pct-bar-fill {
height: 100%;
border-radius: 2px;
}
/* ─── Animations ─── */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes livePulse {
0%, 100% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.4); }
50% { box-shadow: 0 0 0 6px rgba(16, 185, 129, 0); }
}
.animate-in { animation: fadeIn 0.3s ease-out both; }
.delay-1 { animation-delay: 0.05s; }
.delay-2 { animation-delay: 0.1s; }
.delay-3 { animation-delay: 0.15s; }
.delay-4 { animation-delay: 0.2s; }
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: var(--text-muted); }
@media (max-width: 1200px) {
.grid-2, .grid-3 { grid-template-columns: 1fr; }
.route-header { flex-direction: column; }
.route-header-stats { align-self: stretch; justify-content: space-around; }
}
</style>
</head>
<body>
<nav class="topnav">
<div class="logo">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10 10-4.5 10-10S17.5 2 12 2"/>
<path d="M12 6v6l4 2"/>
</svg>
cameleer3
</div>
<ul class="nav-links">
<li><a href="dashboard.html">Dashboard</a></li>
<li><a href="transaction-explorer.html">Transactions</a></li>
<li><a href="route-detail.html" class="active">Routes</a></li>
</ul>
<div class="nav-right">
<span class="env-badge">PRODUCTION</span>
<span class="cluster-count">47 apps · 1,283 routes</span>
</div>
</nav>
<main class="main">
<!-- Breadcrumb -->
<div class="breadcrumb animate-in">
<a href="dashboard.html">Dashboard</a>
<span class="sep"></span>
<a href="#">order-service-eu</a>
<span class="sep"></span>
<span style="color:var(--text-secondary)">content-based-routing</span>
</div>
<!-- Route Header -->
<div class="route-header animate-in delay-1">
<div class="route-header-left">
<h1>content-based-routing</h1>
<div class="route-meta">
<div class="route-meta-item">
<span class="dot" style="background:var(--green)"></span>
<strong>Running</strong>
</div>
<div class="route-meta-item">
Application: <strong>order-service-eu</strong>
</div>
<div class="route-meta-item">
From: <strong style="font-family:var(--font-mono);font-size:11px">direct:content-based-routing</strong>
</div>
<div class="route-meta-item">
Processors: <strong>14</strong>
</div>
<div class="route-meta-item">
Uptime: <strong>12d 7h 43m</strong>
</div>
</div>
</div>
<div class="route-header-stats">
<div class="route-stat">
<div class="val" style="color:var(--amber)">89.4K</div>
<div class="lbl">Today</div>
</div>
<div class="route-stat">
<div class="val" style="color:var(--green)">99.72%</div>
<div class="lbl">Success</div>
</div>
<div class="route-stat">
<div class="val" style="color:var(--cyan)">47ms</div>
<div class="lbl">p50</div>
</div>
<div class="route-stat">
<div class="val" style="color:var(--rose)">312ms</div>
<div class="lbl">p99</div>
</div>
</div>
</div>
<!-- Tabs -->
<div class="tabs animate-in delay-2">
<div class="tab active" data-tab="diagram">Diagram</div>
<div class="tab" data-tab="performance">Performance</div>
<div class="tab" data-tab="executions">Recent Executions</div>
<div class="tab" data-tab="processors">Processor Breakdown</div>
</div>
<!-- ═══ TAB: Diagram ═══ -->
<div class="tab-panel active" id="tab-diagram">
<div class="card animate-in delay-3">
<div class="card-body" style="padding:16px">
<div class="diagram-toolbar">
<button class="diagram-btn" id="btn-overlay" onclick="toggleOverlay()">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18l6-6-6-6"/></svg>
Execution Overlay
<span class="kbd">E</span>
</button>
<button class="diagram-btn" onclick="this.classList.toggle('active')">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/></svg>
Zoom
</button>
<span class="exec-badge" id="exec-badge" style="display:none">COMPLETED · 47ms · 8 processors</span>
</div>
<div class="diagram-container" id="diagram-container">
<svg class="diagram-svg" id="route-diagram" viewBox="0 0 900 480"></svg>
</div>
</div>
</div>
</div>
<!-- ═══ TAB: Performance ═══ -->
<div class="tab-panel" id="tab-performance">
<div class="grid-2 animate-in delay-3">
<!-- Duration Histogram -->
<div class="card">
<div class="card-header">
<div>
<div class="card-title">Duration Distribution</div>
<div class="card-subtitle">Last 24h · 89,420 exchanges</div>
</div>
</div>
<div class="card-body">
<div class="histogram" id="histogram"></div>
<div class="histogram-labels" id="histogram-labels"></div>
<div class="histogram-percentiles">
<div class="percentile"><div class="pval" style="color:var(--green)">23ms</div><div class="plbl">p50</div></div>
<div class="percentile"><div class="pval" style="color:var(--cyan)">47ms</div><div class="plbl">p75</div></div>
<div class="percentile"><div class="pval" style="color:var(--amber)">142ms</div><div class="plbl">p95</div></div>
<div class="percentile"><div class="pval" style="color:var(--rose)">312ms</div><div class="plbl">p99</div></div>
</div>
</div>
</div>
<!-- Throughput Over Time -->
<div class="card">
<div class="card-header">
<div>
<div class="card-title">Throughput Over Time</div>
<div class="card-subtitle">Exchanges/min · last 24h</div>
</div>
</div>
<div class="card-body">
<svg class="svg-chart" viewBox="0 0 500 200" id="chart-throughput"></svg>
<div class="chart-legend">
<div class="legend-item"><div class="legend-dot" style="background:var(--amber)"></div> Volume</div>
<div class="legend-item"><div class="legend-dot" style="background:var(--rose)"></div> Failures</div>
</div>
</div>
</div>
</div>
<!-- Error Rate vs Latency Correlation -->
<div class="card animate-in delay-4">
<div class="card-header">
<div>
<div class="card-title">Error Rate vs Latency</div>
<div class="card-subtitle">Last 24h · 15-min windows</div>
</div>
</div>
<div class="card-body">
<svg class="svg-chart" viewBox="0 0 900 180" id="chart-correlation"></svg>
<div class="chart-legend">
<div class="legend-item"><div class="legend-dot" style="background:var(--cyan)"></div> Avg Latency (ms)</div>
<div class="legend-item"><div class="legend-dot" style="background:var(--rose)"></div> Error Rate (%)</div>
</div>
</div>
</div>
</div>
<!-- ═══ TAB: Recent Executions ═══ -->
<div class="tab-panel" id="tab-executions">
<div class="card animate-in delay-3">
<div class="card-header">
<div>
<div class="card-title">Recent Executions</div>
<div class="card-subtitle">Last 50 exchanges</div>
</div>
</div>
<div class="card-body">
<div class="timeline" id="timeline"></div>
</div>
</div>
</div>
<!-- ═══ TAB: Processor Breakdown ═══ -->
<div class="tab-panel" id="tab-processors">
<div class="card animate-in delay-3">
<div class="card-header">
<div>
<div class="card-title">Processor Performance Breakdown</div>
<div class="card-subtitle">Average across last 1,000 exchanges</div>
</div>
</div>
<div class="card-body" style="padding:0">
<table class="proc-table">
<thead>
<tr>
<th>#</th>
<th>Processor</th>
<th>Type</th>
<th>Endpoint</th>
<th>Avg Duration</th>
<th>% of Total</th>
<th>Calls</th>
<th>Error Rate</th>
</tr>
</thead>
<tbody id="proc-table-body"></tbody>
</table>
</div>
</div>
</div>
</main>
<script>
// ─── Tab Switching ───
document.querySelectorAll('.tab').forEach(tab => {
tab.addEventListener('click', function() {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
this.classList.add('active');
document.getElementById('tab-' + this.dataset.tab).classList.add('active');
});
});
// ─── Route Diagram ───
const NODES = [
{ id: 'from', label: 'direct:content-based-routing', type: 'endpoint', x: 100, y: 30, w: 200, h: 38 },
{ id: 'log1', label: 'Log: Received', type: 'processor', x: 100, y: 90, w: 200, h: 38 },
{ id: 'setheader', label: 'SetHeader: correlationId', type: 'processor', x: 100, y: 150, w: 200, h: 38 },
{ id: 'choice', label: 'Content Based Router', type: 'eip', x: 100, y: 220, w: 200, h: 38 },
// HIGH branch
{ id: 'when-high', label: 'When: priority=HIGH', type: 'eip', x: 20, y: 290, w: 180, h: 34 },
{ id: 'to-priority', label: 'direct:priority-processing', type: 'endpoint', x: 20, y: 340, w: 180, h: 34 },
// NORMAL branch
{ id: 'when-normal', label: 'When: priority=NORMAL', type: 'eip', x: 220, y: 290, w: 180, h: 34 },
{ id: 'to-standard', label: 'direct:standard-processing', type: 'endpoint', x: 220, y: 340, w: 180, h: 34 },
// LOW branch
{ id: 'otherwise', label: 'Otherwise', type: 'eip', x: 420, y: 290, w: 180, h: 34 },
{ id: 'to-batch', label: 'seda:batch-queue', type: 'endpoint', x: 420, y: 340, w: 180, h: 34 },
// Cross-route references
{ id: 'cross-priority', label: 'priority-processing', type: 'cross-route', x: 20, y: 400, w: 180, h: 30 },
{ id: 'cross-standard', label: 'standard-processing', type: 'cross-route', x: 220, y: 400, w: 180, h: 30 },
// Converge
{ id: 'marshal', label: 'Marshal: JSON', type: 'processor', x: 220, y: 440, w: 180, h: 34 },
{ id: 'to-audit', label: 'jms:queue:AUDIT.LOG', type: 'endpoint', x: 620, y: 220, w: 180, h: 38 },
];
const EDGES = [
{ from: 'from', to: 'log1', type: 'flow' },
{ from: 'log1', to: 'setheader', type: 'flow' },
{ from: 'setheader', to: 'choice', type: 'flow' },
{ from: 'choice', to: 'when-high', type: 'branch', label: 'HIGH' },
{ from: 'choice', to: 'when-normal', type: 'branch', label: 'NORMAL' },
{ from: 'choice', to: 'otherwise', type: 'branch', label: 'OTHER' },
{ from: 'when-high', to: 'to-priority', type: 'flow' },
{ from: 'when-normal', to: 'to-standard', type: 'flow' },
{ from: 'otherwise', to: 'to-batch', type: 'flow' },
{ from: 'to-priority', to: 'cross-priority', type: 'cross-route' },
{ from: 'to-standard', to: 'cross-standard', type: 'cross-route' },
{ from: 'choice', to: 'to-audit', type: 'flow' },
];
// Execution path (highlighted in overlay)
const EXEC_PATH = ['from', 'log1', 'setheader', 'choice', 'when-high', 'to-priority', 'cross-priority', 'marshal'];
const EXEC_DURATIONS = { from: 0, log1: 1, setheader: 1, choice: 0, 'when-high': 0, 'to-priority': 35, 'cross-priority': 0, marshal: 2 };
function getNodeColor(type) {
switch(type) {
case 'endpoint': return { fill: 'rgba(59,130,246,0.12)', stroke: 'rgba(59,130,246,0.5)', text: '#3b82f6' };
case 'processor': return { fill: 'rgba(16,185,129,0.12)', stroke: 'rgba(16,185,129,0.5)', text: '#10b981' };
case 'eip': return { fill: 'rgba(168,85,247,0.12)', stroke: 'rgba(168,85,247,0.5)', text: '#a855f7' };
case 'cross-route': return { fill: 'rgba(34,211,238,0.08)', stroke: 'rgba(34,211,238,0.4)', text: '#22d3ee' };
default: return { fill: 'rgba(139,156,182,0.12)', stroke: 'rgba(139,156,182,0.4)', text: '#8b9cb6' };
}
}
function getEdgeColor(type) {
switch(type) {
case 'flow': return '#4a5e7a';
case 'branch': return '#f0b429';
case 'cross-route': return '#22d3ee';
case 'error': return '#f43f5e';
default: return '#4a5e7a';
}
}
function drawDiagram() {
const svg = document.getElementById('route-diagram');
let html = '';
// Defs for markers
html += `<defs>
<marker id="arrow-flow" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto">
<path d="M0,0 L8,3 L0,6" fill="#4a5e7a"/>
</marker>
<marker id="arrow-branch" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto">
<path d="M0,0 L8,3 L0,6" fill="#f0b429"/>
</marker>
<marker id="arrow-cross" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto">
<path d="M0,0 L8,3 L0,6" fill="#22d3ee"/>
</marker>
<marker id="arrow-exec" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto">
<path d="M0,0 L8,3 L0,6" fill="#10b981"/>
</marker>
<filter id="glow-green">
<feGaussianBlur stdDeviation="3" result="blur"/>
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
</defs>`;
const nodeMap = {};
NODES.forEach(n => nodeMap[n.id] = n);
// Draw edges
EDGES.forEach(e => {
const from = nodeMap[e.from];
const to = nodeMap[e.to];
if (!from || !to) return;
const x1 = from.x + from.w / 2;
const y1 = from.y + from.h;
const x2 = to.x + to.w / 2;
const y2 = to.y;
const color = getEdgeColor(e.type);
const markerId = e.type === 'branch' ? 'arrow-branch' : e.type === 'cross-route' ? 'arrow-cross' : 'arrow-flow';
const dash = e.type === 'cross-route' ? 'stroke-dasharray="5,3"' : '';
// Bezier for non-straight paths
const midY = (y1 + y2) / 2;
const path = `M${x1},${y1} C${x1},${midY} ${x2},${midY} ${x2},${y2}`;
html += `<path class="edge-path" data-from="${e.from}" data-to="${e.to}" d="${path}" fill="none" stroke="${color}" stroke-width="1.5" marker-end="url(#${markerId})" ${dash} opacity="0.6"/>`;
// Branch label
if (e.label) {
const lx = (x1 + x2) / 2 + (x1 < x2 ? -20 : 20);
const ly = midY - 4;
html += `<text x="${lx}" y="${ly}" font-size="9" fill="#f0b429" text-anchor="middle" font-weight="600">${e.label}</text>`;
}
});
// Draw nodes
NODES.forEach(n => {
const c = getNodeColor(n.type);
const isExec = EXEC_PATH.includes(n.id);
html += `<g class="node-group" data-id="${n.id}" style="--glow-color:${c.stroke}">`;
// Rect
const rx = n.type === 'cross-route' ? 14 : 8;
const dashArr = n.type === 'cross-route' ? 'stroke-dasharray="4,3"' : '';
html += `<rect class="node-rect" x="${n.x}" y="${n.y}" width="${n.w}" height="${n.h}" rx="${rx}" fill="${c.fill}" stroke="${c.stroke}" stroke-width="1.5" ${dashArr}/>`;
// Label
const fontSize = n.label.length > 28 ? 9 : 10;
html += `<text x="${n.x + n.w/2}" y="${n.y + n.h/2 + 3}" text-anchor="middle" fill="${c.text}" font-size="${fontSize}" font-weight="500">${n.label}</text>`;
// Type badge (small)
if (n.type === 'cross-route') {
html += `<text x="${n.x + n.w - 8}" y="${n.y + 10}" text-anchor="end" fill="${c.text}" font-size="7" opacity="0.6">↗ route</text>`;
}
html += `</g>`;
});
svg.innerHTML = html;
}
let overlayActive = false;
function toggleOverlay() {
overlayActive = !overlayActive;
const container = document.getElementById('diagram-container');
const btn = document.getElementById('btn-overlay');
const badge = document.getElementById('exec-badge');
if (overlayActive) {
container.classList.add('overlay-mode');
btn.classList.add('active');
badge.style.display = 'inline-flex';
// Dim non-executed nodes
document.querySelectorAll('.node-group').forEach(g => {
const id = g.dataset.id;
if (EXEC_PATH.includes(id)) {
g.classList.add('hot');
g.classList.remove('dim');
// Change stroke to green
const rect = g.querySelector('.node-rect');
rect.setAttribute('stroke', '#10b981');
rect.setAttribute('stroke-width', '2');
rect.setAttribute('filter', 'url(#glow-green)');
} else {
g.classList.add('dim');
g.classList.remove('hot');
}
});
// Dim non-executed edges
document.querySelectorAll('.edge-path').forEach(e => {
const from = e.dataset.from;
const to = e.dataset.to;
const fromIdx = EXEC_PATH.indexOf(from);
const toIdx = EXEC_PATH.indexOf(to);
if (fromIdx >= 0 && toIdx >= 0 && Math.abs(fromIdx - toIdx) <= 2) {
e.classList.remove('dim');
e.setAttribute('stroke', '#10b981');
e.setAttribute('stroke-width', '2.5');
e.setAttribute('opacity', '0.9');
e.setAttribute('marker-end', 'url(#arrow-exec)');
} else {
e.classList.add('dim');
}
});
// Add sequence numbers and duration badges
const svg = document.getElementById('route-diagram');
EXEC_PATH.forEach((id, idx) => {
const node = NODES.find(n => n.id === id);
if (!node) return;
const dur = EXEC_DURATIONS[id] || 0;
// Sequence badge
const badge = document.createElementNS('http://www.w3.org/2000/svg', 'g');
badge.classList.add('exec-overlay-badge');
badge.innerHTML = `
<circle cx="${node.x - 4}" cy="${node.y + node.h/2}" r="10" fill="#10b981" opacity="0.9"/>
<text x="${node.x - 4}" y="${node.y + node.h/2 + 3.5}" text-anchor="middle" fill="#060a13" font-size="9" font-weight="700">${idx + 1}</text>
`;
svg.appendChild(badge);
// Duration pill
if (dur > 0) {
const pill = document.createElementNS('http://www.w3.org/2000/svg', 'g');
pill.classList.add('exec-overlay-badge');
const tw = dur.toString().length * 7 + 24;
pill.innerHTML = `
<rect x="${node.x + node.w/2 - tw/2}" y="${node.y - 18}" width="${tw}" height="16" rx="8" fill="rgba(16,185,129,0.2)" stroke="rgba(16,185,129,0.4)" stroke-width="0.5"/>
<text x="${node.x + node.w/2}" y="${node.y - 7}" text-anchor="middle" fill="#10b981" font-size="9" font-weight="600">${dur}ms</text>
`;
svg.appendChild(pill);
}
});
} else {
container.classList.remove('overlay-mode');
btn.classList.remove('active');
badge.style.display = 'none';
// Remove overlays and restore
document.querySelectorAll('.exec-overlay-badge').forEach(b => b.remove());
document.querySelectorAll('.node-group').forEach(g => {
g.classList.remove('hot', 'dim');
const rect = g.querySelector('.node-rect');
const id = g.dataset.id;
const node = NODES.find(n => n.id === id);
if (node) {
const c = getNodeColor(node.type);
rect.setAttribute('stroke', c.stroke);
rect.setAttribute('stroke-width', '1.5');
rect.removeAttribute('filter');
}
});
document.querySelectorAll('.edge-path').forEach(e => {
e.classList.remove('dim');
const from = e.dataset.from;
const edge = EDGES.find(ed => ed.from === from && ed.to === e.dataset.to);
if (edge) {
e.setAttribute('stroke', getEdgeColor(edge.type));
e.setAttribute('stroke-width', '1.5');
e.setAttribute('opacity', '0.6');
const markerId = edge.type === 'branch' ? 'arrow-branch' : edge.type === 'cross-route' ? 'arrow-cross' : 'arrow-flow';
e.setAttribute('marker-end', `url(#${markerId})`);
}
});
}
}
document.addEventListener('keydown', e => {
if (e.key === 'e' || e.key === 'E') toggleOverlay();
});
drawDiagram();
// ─── Histogram ───
(function() {
const hist = document.getElementById('histogram');
const labels = document.getElementById('histogram-labels');
const buckets = [
{ range: '0-5', count: 8420, pct: 9.4 },
{ range: '5-10', count: 14230, pct: 15.9 },
{ range: '10-20', count: 21560, pct: 24.1 },
{ range: '20-50', count: 25340, pct: 28.3 },
{ range: '50-100', count: 11200, pct: 12.5 },
{ range: '100-200', count: 4890, pct: 5.5 },
{ range: '200-500', count: 2340, pct: 2.6 },
{ range: '500-1k', count: 980, pct: 1.1 },
{ range: '1-2k', count: 340, pct: 0.4 },
{ range: '2-5k', count: 98, pct: 0.1 },
{ range: '5k+', count: 22, pct: 0.02 },
];
const maxCount = Math.max(...buckets.map(b => b.count));
buckets.forEach(b => {
const height = (b.count / maxCount) * 100;
const hue = height > 70 ? 150 : height > 40 ? 40 : 0;
const color = height > 70 ? 'var(--amber)' : height > 40 ? 'rgba(240,180,41,0.7)' : 'rgba(240,180,41,0.4)';
hist.innerHTML += `
<div class="histogram-bar-wrap">
<div class="bar-tooltip">${b.range}ms: ${b.count.toLocaleString()} (${b.pct}%)</div>
<div class="histogram-bar" style="height:${height}%;background:${color}"></div>
</div>`;
labels.innerHTML += `<span>${b.range}</span>`;
});
})();
// ─── Throughput Chart ───
(function() {
const svg = document.getElementById('chart-throughput');
const W = 500, H = 200;
const pad = { top: 10, right: 10, bottom: 25, left: 40 };
const cw = W - pad.left - pad.right;
const ch = H - pad.top - pad.bottom;
let html = '';
// Grid
[0, 25, 50, 75, 100].forEach(val => {
const y = pad.top + ch - (val / 100) * ch;
html += `<line x1="${pad.left}" y1="${y}" x2="${W-pad.right}" y2="${y}" stroke="var(--border-subtle)" stroke-width="0.5"/>`;
html += `<text x="${pad.left-6}" y="${y+3}" text-anchor="end" font-size="9" fill="var(--text-muted)">${val}</text>`;
});
// X labels
['00:00','06:00','12:00','18:00','24:00'].forEach((l, i) => {
const x = pad.left + (i/4) * cw;
html += `<text x="${x}" y="${H-4}" text-anchor="middle" font-size="9" fill="var(--text-muted)">${l}</text>`;
});
// Volume area
const volData = [];
for (let i = 0; i < 96; i++) {
const hour = i / 4;
const base = hour >= 8 && hour <= 18 ? 70 : hour >= 6 && hour <= 20 ? 40 : 10;
volData.push(base + Math.random() * 20 - 10);
}
const volPoints = volData.map((v, i) => {
const x = pad.left + (i / 95) * cw;
const y = pad.top + ch - (v / 100) * ch;
return `${x},${y}`;
});
html += `<defs><linearGradient id="vol-grad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="var(--amber)" stop-opacity="0.2"/>
<stop offset="100%" stop-color="var(--amber)" stop-opacity="0"/>
</linearGradient></defs>`;
html += `<polygon points="${pad.left},${pad.top+ch} ${volPoints.join(' ')} ${W-pad.right},${pad.top+ch}" fill="url(#vol-grad)"/>`;
html += `<polyline points="${volPoints.join(' ')}" fill="none" stroke="var(--amber)" stroke-width="1.5"/>`;
// Failure line (scaled up for visibility)
const failPoints = volData.map((v, i) => {
const x = pad.left + (i / 95) * cw;
const fRate = Math.random() * 3 + 0.5;
const y = pad.top + ch - (fRate / 100) * ch * 20; // scaled
return `${x},${y}`;
});
html += `<polyline points="${failPoints.join(' ')}" fill="none" stroke="var(--rose)" stroke-width="1" opacity="0.7"/>`;
svg.innerHTML = html;
})();
// ─── Correlation Chart ───
(function() {
const svg = document.getElementById('chart-correlation');
const W = 900, H = 180;
const pad = { top: 10, right: 10, bottom: 25, left: 40 };
const cw = W - pad.left - pad.right;
const ch = H - pad.top - pad.bottom;
let html = '';
// Grid
[0, 50, 100, 150, 200].forEach(val => {
const y = pad.top + ch - (val / 200) * ch;
html += `<line x1="${pad.left}" y1="${y}" x2="${W-pad.right}" y2="${y}" stroke="var(--border-subtle)" stroke-width="0.5"/>`;
html += `<text x="${pad.left-6}" y="${y+3}" text-anchor="end" font-size="9" fill="var(--text-muted)">${val}ms</text>`;
});
const labels = ['00:00','03:00','06:00','09:00','12:00','15:00','18:00','21:00','24:00'];
labels.forEach((l, i) => {
const x = pad.left + (i / (labels.length-1)) * cw;
html += `<text x="${x}" y="${H-4}" text-anchor="middle" font-size="9" fill="var(--text-muted)">${l}</text>`;
});
// Latency area
const latData = [];
for (let i = 0; i < 96; i++) latData.push(30 + Math.random() * 80);
const latPoints = latData.map((v, i) => `${pad.left + (i/95)*cw},${pad.top + ch - (v/200)*ch}`);
html += `<defs><linearGradient id="lat-grad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="var(--cyan)" stop-opacity="0.15"/>
<stop offset="100%" stop-color="var(--cyan)" stop-opacity="0"/>
</linearGradient></defs>`;
html += `<polygon points="${pad.left},${pad.top+ch} ${latPoints.join(' ')} ${W-pad.right},${pad.top+ch}" fill="url(#lat-grad)"/>`;
html += `<polyline points="${latPoints.join(' ')}" fill="none" stroke="var(--cyan)" stroke-width="1.5"/>`;
// Error rate bars
for (let i = 0; i < 96; i++) {
const x = pad.left + (i / 95) * cw;
const rate = Math.random() * 2;
const barH = (rate / 200) * ch * 30;
html += `<rect x="${x-2}" y="${pad.top + ch - barH}" width="4" height="${barH}" fill="var(--rose)" opacity="0.3" rx="1"/>`;
}
svg.innerHTML = html;
})();
// ─── Timeline ───
(function() {
const timeline = document.getElementById('timeline');
const now = Date.now();
const executions = [];
for (let i = 0; i < 30; i++) {
const failed = Math.random() < 0.05;
executions.push({
id: Math.random().toString(16).slice(2, 18),
time: new Date(now - i * (Math.random() * 3000 + 1000)),
status: failed ? 'failed' : 'completed',
duration: failed ? Math.floor(Math.random() * 5000) + 200 : Math.floor(Math.random() * 300) + 10,
processors: Math.floor(Math.random() * 10) + 4,
correlationId: Math.random().toString(16).slice(2, 18),
error: failed ? 'HttpOperationFailedException: HTTP 503 from payment-provider-api' : null,
});
}
executions.forEach(exec => {
const timeStr = exec.time.toTimeString().slice(0, 12);
const durColor = exec.duration > 1000 ? 'var(--rose)' : exec.duration > 200 ? 'var(--amber)' : 'var(--green)';
timeline.innerHTML += `
<div class="timeline-item">
<div class="timeline-dot ${exec.status}"></div>
<div class="timeline-content">
<div class="timeline-top">
<span class="timeline-id">${exec.id}</span>
<span class="timeline-time">${timeStr}</span>
</div>
<div class="timeline-details">
<span style="color:${durColor};font-family:var(--font-mono);font-weight:600">${exec.duration}ms</span>
<span>${exec.processors} processors</span>
<span style="color:var(--text-muted);font-family:var(--font-mono);font-size:10px">corr: ${exec.correlationId}</span>
</div>
${exec.error ? `<div class="timeline-error">${exec.error}</div>` : ''}
</div>
</div>`;
});
})();
// ─── Processor Breakdown Table ───
(function() {
const tbody = document.getElementById('proc-table-body');
const processors = [
{ id: 'from1', type: 'Endpoint', cat: 'endpoint', uri: 'direct:content-based-routing', avgMs: 0.3, pct: 0.6, calls: 89420, errRate: 0 },
{ id: 'log1', type: 'Log', cat: 'processor', uri: 'Received order: ${body}', avgMs: 0.8, pct: 1.7, calls: 89420, errRate: 0 },
{ id: 'setHeader1', type: 'SetHeader', cat: 'processor', uri: 'X-Cameleer-CorrelationId', avgMs: 0.4, pct: 0.8, calls: 89420, errRate: 0 },
{ id: 'choice1', type: 'Choice (CBR)', cat: 'eip', uri: 'content-based-router', avgMs: 0.2, pct: 0.4, calls: 89420, errRate: 0 },
{ id: 'when1', type: 'When', cat: 'eip', uri: 'priority == HIGH', avgMs: 0.1, pct: 0.2, calls: 34210, errRate: 0 },
{ id: 'to-prio', type: 'To', cat: 'endpoint', uri: 'direct:priority-processing', avgMs: 35.2, pct: 74.5, calls: 34210, errRate: 0.31 },
{ id: 'when2', type: 'When', cat: 'eip', uri: 'priority == NORMAL', avgMs: 0.1, pct: 0.2, calls: 42850, errRate: 0 },
{ id: 'to-std', type: 'To', cat: 'endpoint', uri: 'direct:standard-processing', avgMs: 8.4, pct: 17.8, calls: 42850, errRate: 0.08 },
{ id: 'other', type: 'Otherwise', cat: 'eip', uri: '(default branch)', avgMs: 0.1, pct: 0.2, calls: 12360, errRate: 0 },
{ id: 'to-batch', type: 'To', cat: 'endpoint', uri: 'seda:batch-queue', avgMs: 1.2, pct: 2.5, calls: 12360, errRate: 0 },
{ id: 'marshal', type: 'Marshal', cat: 'processor', uri: 'JSON (Jackson)', avgMs: 0.5, pct: 1.1, calls: 89420, errRate: 0 },
];
processors.forEach((p, idx) => {
const errColor = p.errRate > 0.1 ? 'var(--rose)' : p.errRate > 0 ? 'var(--amber)' : 'var(--text-muted)';
const pctColor = p.pct > 50 ? 'var(--rose)' : p.pct > 20 ? 'var(--amber)' : 'var(--green)';
tbody.innerHTML += `
<tr>
<td style="font-family:var(--font-mono);font-size:11px;color:var(--text-muted)">${idx + 1}</td>
<td style="font-weight:600;font-size:12px">${p.id}</td>
<td><span class="proc-type-badge ${p.cat}">${p.type}</span></td>
<td style="font-family:var(--font-mono);font-size:11px;color:var(--text-muted);max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${p.uri}</td>
<td style="font-family:var(--font-mono);font-size:12px;font-weight:600">${p.avgMs}ms</td>
<td>
<div class="pct-bar"><div class="pct-bar-fill" style="width:${p.pct}%;background:${pctColor}"></div></div>
<span style="font-family:var(--font-mono);font-size:11px;color:${pctColor}">${p.pct}%</span>
</td>
<td style="font-family:var(--font-mono);font-size:11px;color:var(--text-secondary)">${p.calls.toLocaleString()}</td>
<td style="font-family:var(--font-mono);font-size:11px;color:${errColor}">${p.errRate > 0 ? p.errRate + '%' : '—'}</td>
</tr>`;
});
})();
</script>
</body>
</html>