Files
cameleer-server/examples/route-detail-light.html
hsiegeln d229365eaf
All checks were successful
CI / build (push) Successful in 41s
CI / docker (push) Successful in 36s
CI / deploy (push) Successful in 11s
added examples
2026-03-13 10:52:43 +01:00

1409 lines
45 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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: #f7f5f2;
--bg-base: #efecea;
--bg-surface: #ffffff;
--bg-raised: #f3f1ee;
--bg-hover: #eae7e3;
--border: #d4cfc8;
--border-subtle: #e4e0db;
--text-primary: #1c1917;
--text-secondary: #57534e;
--text-muted: #a8a29e;
--amber: #b45309;
--amber-dim: #92400e;
--amber-glow: rgba(180, 83, 9, 0.07);
--cyan: #0e7490;
--cyan-dim: #155e75;
--cyan-glow: rgba(14, 116, 144, 0.06);
--rose: #be123c;
--rose-dim: #9f1239;
--rose-glow: rgba(190, 18, 60, 0.05);
--green: #047857;
--green-glow: rgba(4, 120, 87, 0.06);
--blue: #1d4ed8;
--purple: #7c3aed;
--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(180, 83, 9, 0.03), transparent),
radial-gradient(ellipse 600px 600px at 80% 80%, rgba(14, 116, 144, 0.02), transparent);
pointer-events: none;
z-index: 0;
}
body::after {
content: '';
position: fixed;
inset: 0;
opacity: 0.018;
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='%23b45309' stroke-width='1'/%3E%3Cpath d='M0 220 Q120 170 200 220 T400 220' fill='none' stroke='%23b45309' stroke-width='0.5'/%3E%3Cpath d='M0 180 Q80 130 200 180 T400 180' fill='none' stroke='%23b45309' stroke-width='0.5'/%3E%3Cpath d='M0 100 Q150 60 200 100 T400 100' fill='none' stroke='%230e7490' stroke-width='0.5'/%3E%3Cpath d='M0 300 Q100 260 200 300 T400 300' fill='none' stroke='%230e7490' 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(255, 255, 255, 0.92);
backdrop-filter: blur(20px) saturate(1.2);
border-bottom: 1px solid var(--border-subtle);
box-shadow: 0 1px 3px rgba(0,0,0,0.04);
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(4, 120, 87, 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;
box-shadow: 0 1px 3px rgba(0,0,0,0.04), 0 1px 2px rgba(0,0,0,0.02);
}
.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;
color: var(--text-primary);
}
.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;
box-shadow: 0 1px 3px rgba(0,0,0,0.04), 0 1px 2px rgba(0,0,0,0.02);
}
.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-surface);
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-surface);
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(--amber-glow); border-color: rgba(180,83,9,0.3); color: var(--amber); }
.diagram-btn .kbd {
font-size: 9px;
padding: 1px 5px;
border-radius: 3px;
background: var(--bg-raised);
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(4,120,87,0.08);
color: var(--green);
border: 1px solid rgba(4,120,87,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(180,83,9,0.3)));
}
.overlay-mode .node-group.dim { opacity: 0.35; }
.overlay-mode .node-group.dim:hover { opacity: 0.55; }
.overlay-mode .edge-path.dim { opacity: 0.12; }
.overlay-mode .node-group.hot .node-rect {
filter: drop-shadow(0 0 10px rgba(4,120,87,0.4));
}
/* ─── 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.1);
box-shadow: 0 0 8px rgba(180, 83, 9, 0.2);
}
.histogram-bar-wrap .bar-tooltip {
display: none;
position: absolute;
bottom: calc(100% + 8px);
left: 50%;
transform: translateX(-50%);
background: var(--bg-surface);
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;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
}
.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(29,78,216,0.08); color: var(--blue); }
.proc-type-badge.eip { background: rgba(124,58,237,0.08); 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(4, 120, 87, 0.3); }
50% { box-shadow: 0 0 0 6px rgba(4, 120, 87, 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-light.html">Dashboard</a></li>
<li><a href="transaction-explorer-light.html">Transactions</a></li>
<li><a href="route-detail-light.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-light.html">Dashboard</a>
<span class="sep"></span>
<a href="#">order-service-eu</a>
<span class="sep"></span>
<span style="color:var(--amber)">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(29,78,216,0.06)', stroke: 'rgba(29,78,216,0.4)', text: '#1d4ed8' };
case 'processor': return { fill: 'rgba(4,120,87,0.06)', stroke: 'rgba(4,120,87,0.4)', text: '#047857' };
case 'eip': return { fill: 'rgba(124,58,237,0.06)', stroke: 'rgba(124,58,237,0.4)', text: '#7c3aed' };
case 'cross-route': return { fill: 'rgba(14,116,144,0.06)', stroke: 'rgba(14,116,144,0.4)', text: '#0e7490' };
default: return { fill: 'rgba(168,162,158,0.1)', stroke: 'rgba(168,162,158,0.4)', text: '#57534e' };
}
}
function getEdgeColor(type) {
switch(type) {
case 'flow': return '#a8a29e';
case 'branch': return '#b45309';
case 'cross-route': return '#0e7490';
case 'error': return '#be123c';
default: return '#a8a29e';
}
}
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="#a8a29e"/>
</marker>
<marker id="arrow-branch" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto">
<path d="M0,0 L8,3 L0,6" fill="#b45309"/>
</marker>
<marker id="arrow-cross" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto">
<path d="M0,0 L8,3 L0,6" fill="#0e7490"/>
</marker>
<marker id="arrow-exec" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto">
<path d="M0,0 L8,3 L0,6" fill="#047857"/>
</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="#b45309" 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="#1c1917" 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', '#047857');
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', '#047857');
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="#047857" opacity="0.9"/>
<text x="${node.x - 4}" y="${node.y + node.h/2 + 3.5}" text-anchor="middle" fill="#ffffff" 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(255,255,255,0.95)" stroke="rgba(4,120,87,0.3)" stroke-width="0.5" filter="url(#pill-shadow)"/>
<text x="${node.x + node.w/2}" y="${node.y - 7}" text-anchor="middle" fill="#047857" 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(180,83,9,0.7)' : 'rgba(180,83,9,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.08"/>
<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.08"/>
<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>