Files
cameleer-server/examples/transaction-explorer-light.html

1310 lines
38 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>Cameleer — Transaction Explorer</title>
2026-03-13 10:52:43 +01:00
<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;
overflow-x: hidden;
}
/* Topographic background pattern */
body::before {
content: '';
position: fixed;
inset: 0;
background:
radial-gradient(ellipse 800px 400px at 20% 20%, rgba(180, 83, 9, 0.02), transparent),
radial-gradient(ellipse 600px 600px at 80% 80%, rgba(14, 116, 144, 0.015), transparent);
pointer-events: none;
z-index: 0;
}
/* Topo lines */
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); }
/* ─── Top Navigation ─── */
.topnav {
position: sticky;
top: 0;
z-index: 100;
background: rgba(255, 255, 255, 0.92);
backdrop-filter: blur(20px) saturate(1.2);
box-shadow: 0 1px 3px rgba(0,0,0,0.04);
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;
position: relative;
}
.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 Layout ─── */
.main {
position: relative;
z-index: 1;
max-width: 1440px;
margin: 0 auto;
padding: 24px;
}
/* ─── Page Header ─── */
.page-header {
margin-bottom: 24px;
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 16px;
}
.page-header h1 {
font-size: 24px;
font-weight: 700;
letter-spacing: -0.5px;
color: var(--text-primary);
}
.page-header .subtitle {
font-size: 13px;
color: var(--text-muted);
margin-top: 2px;
}
.live-indicator {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: var(--green);
font-family: var(--font-mono);
font-weight: 500;
}
.live-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--green);
animation: livePulse 2s ease-in-out infinite;
}
@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); }
}
/* ─── Stats Bar ─── */
.stats-bar {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 12px;
margin-bottom: 20px;
}
.stat-card {
background: var(--bg-surface);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-md);
padding: 16px 20px;
position: relative;
overflow: hidden;
transition: border-color 0.2s;
box-shadow: 0 1px 3px rgba(0,0,0,0.04);
}
.stat-card:hover { border-color: var(--border); }
.stat-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 2px;
}
.stat-card.amber::before { background: linear-gradient(90deg, var(--amber), transparent); }
.stat-card.cyan::before { background: linear-gradient(90deg, var(--cyan), transparent); }
.stat-card.rose::before { background: linear-gradient(90deg, var(--rose), transparent); }
.stat-card.green::before { background: linear-gradient(90deg, var(--green), transparent); }
.stat-card.blue::before { background: linear-gradient(90deg, var(--blue), transparent); }
.stat-card .stat-label {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.8px;
color: var(--text-muted);
margin-bottom: 8px;
}
.stat-card .stat-value {
font-family: var(--font-mono);
font-size: 26px;
font-weight: 600;
letter-spacing: -1px;
}
.stat-card.amber .stat-value { color: var(--amber); }
.stat-card.cyan .stat-value { color: var(--cyan); }
.stat-card.rose .stat-value { color: var(--rose); }
.stat-card.green .stat-value { color: var(--green); }
.stat-card.blue .stat-value { color: var(--blue); }
.stat-card .stat-change {
font-size: 11px;
font-family: var(--font-mono);
margin-top: 4px;
}
.stat-card .stat-change.up { color: var(--rose); }
.stat-card .stat-change.down { color: var(--green); }
.stat-card .stat-change.neutral { color: var(--text-muted); }
/* ─── Search & Filters ─── */
.filter-bar {
background: var(--bg-surface);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-lg);
padding: 16px 20px;
margin-bottom: 16px;
display: flex;
flex-direction: column;
gap: 12px;
box-shadow: 0 1px 3px rgba(0,0,0,0.04);
}
.filter-row {
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
}
.search-input-wrap {
flex: 1;
min-width: 300px;
position: relative;
}
.search-input-wrap svg {
position: absolute;
left: 14px;
top: 50%;
transform: translateY(-50%);
width: 16px;
height: 16px;
color: var(--text-muted);
}
.search-input-wrap input {
width: 100%;
background: var(--bg-base);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 10px 14px 10px 40px;
color: var(--text-primary);
font-family: var(--font-mono);
font-size: 13px;
outline: none;
transition: border-color 0.2s, box-shadow 0.2s;
}
.search-input-wrap input::placeholder { color: var(--text-muted); font-family: var(--font-body); }
.search-input-wrap input:focus {
border-color: var(--amber);
box-shadow: 0 0 0 3px var(--amber-glow);
}
.search-input-wrap .search-hint {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
font-family: var(--font-mono);
font-size: 10px;
padding: 3px 8px;
background: var(--bg-raised);
border: 1px solid var(--border);
border-radius: 4px;
color: var(--text-muted);
}
.filter-group {
display: flex;
align-items: center;
gap: 6px;
}
.filter-group label {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--text-muted);
white-space: nowrap;
}
.filter-select {
background: var(--bg-base);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 8px 32px 8px 12px;
color: var(--text-primary);
font-family: var(--font-body);
font-size: 13px;
outline: none;
cursor: pointer;
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg width='10' height='6' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%23a8a29e' fill='none' stroke-width='1.5' stroke-linecap='round'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 12px center;
transition: border-color 0.2s;
}
.filter-select:focus { border-color: var(--amber); }
.filter-chips {
display: flex;
gap: 6px;
flex-wrap: wrap;
}
.chip {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 5px 12px;
background: var(--bg-raised);
border: 1px solid var(--border);
border-radius: 99px;
font-size: 12px;
color: var(--text-secondary);
cursor: pointer;
transition: all 0.15s;
white-space: nowrap;
user-select: none;
}
.chip:hover { border-color: var(--text-muted); color: var(--text-primary); }
.chip.active { background: var(--amber-glow); border-color: var(--amber); color: var(--amber); }
.chip.active.green-chip { background: var(--green-glow); border-color: rgba(4,120,87,0.3); color: var(--green); }
.chip.active.rose-chip { background: var(--rose-glow); border-color: rgba(190,18,60,0.3); color: var(--rose); }
.chip.active.blue-chip { background: rgba(29,78,216,0.06); border-color: rgba(29,78,216,0.3); color: var(--blue); }
.chip .chip-count {
font-family: var(--font-mono);
font-size: 10px;
opacity: 0.7;
}
.date-input {
background: var(--bg-base);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 8px 12px;
color: var(--text-primary);
font-family: var(--font-mono);
font-size: 12px;
outline: none;
width: 160px;
transition: border-color 0.2s;
}
.date-input:focus { border-color: var(--amber); }
.duration-range {
display: flex;
align-items: center;
gap: 8px;
}
.duration-range input[type="range"] {
width: 100px;
accent-color: var(--amber);
cursor: pointer;
}
.duration-range .range-label {
font-family: var(--font-mono);
font-size: 11px;
color: var(--text-muted);
min-width: 50px;
}
.btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
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: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.15s;
white-space: nowrap;
}
.btn:hover { background: var(--bg-raised); color: var(--text-primary); border-color: var(--text-muted); }
.btn-primary {
background: var(--amber);
color: #ffffff;
border-color: var(--amber);
font-weight: 600;
}
.btn-primary:hover { background: #92400e; border-color: #92400e; color: #ffffff; }
.filter-tags {
display: flex;
gap: 6px;
flex-wrap: wrap;
align-items: center;
}
.filter-tag {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
background: var(--amber-glow);
border: 1px solid rgba(180,83,9,0.2);
border-radius: 99px;
font-size: 12px;
color: var(--amber);
font-family: var(--font-mono);
}
.filter-tag .remove {
cursor: pointer;
opacity: 0.5;
font-size: 14px;
line-height: 1;
}
.filter-tag .remove:hover { opacity: 1; }
.filter-tags .clear-all {
font-size: 11px;
color: var(--text-muted);
cursor: pointer;
padding: 4px 8px;
}
.filter-tags .clear-all:hover { color: var(--rose); }
/* ─── Results Table ─── */
.results-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
padding: 0 4px;
}
.results-count {
font-size: 12px;
color: var(--text-muted);
font-family: var(--font-mono);
}
.results-count strong { color: var(--text-secondary); }
.results-actions {
display: flex;
gap: 8px;
}
.table-wrap {
background: var(--bg-surface);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-lg);
overflow: hidden;
box-shadow: 0 1px 3px rgba(0,0,0,0.04);
}
table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
thead {
background: var(--bg-raised);
border-bottom: 1px solid var(--border);
}
thead th {
padding: 12px 16px;
text-align: left;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.8px;
color: var(--text-muted);
cursor: pointer;
user-select: none;
white-space: nowrap;
transition: color 0.15s;
position: relative;
}
thead th:hover { color: var(--text-secondary); }
thead th.sorted { color: var(--amber); }
thead th.sorted::after {
content: ' ▼';
font-size: 8px;
}
thead th.sorted.asc::after { content: ' ▲'; }
tbody tr {
border-bottom: 1px solid var(--border-subtle);
transition: background 0.1s;
cursor: pointer;
}
tbody tr:last-child { border-bottom: none; }
tbody tr:hover { background: var(--bg-raised); }
tbody td {
padding: 12px 16px;
vertical-align: middle;
white-space: nowrap;
}
.td-expand {
width: 32px;
text-align: center;
color: var(--text-muted);
font-size: 16px;
transition: transform 0.2s;
}
tr.expanded .td-expand { transform: rotate(90deg); color: var(--amber); }
.status-pill {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 3px 10px;
border-radius: 99px;
font-size: 11px;
font-weight: 600;
letter-spacing: 0.3px;
}
.status-pill.completed { background: var(--green-glow); color: var(--green); }
.status-pill.failed { background: var(--rose-glow); color: var(--rose); }
.status-pill.running { background: rgba(29,78,216,0.06); color: var(--blue); }
.status-pill .status-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: currentColor;
}
.status-pill.running .status-dot { animation: livePulse 1.5s ease-in-out infinite; }
.mono { font-family: var(--font-mono); font-size: 12px; }
.text-muted { color: var(--text-muted); }
.text-secondary { color: var(--text-secondary); }
.text-amber { color: var(--amber); }
.app-badge {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 2px 8px;
background: var(--bg-raised);
border: 1px solid var(--border-subtle);
border-radius: 4px;
font-size: 11px;
font-family: var(--font-mono);
color: var(--text-secondary);
}
.app-badge .app-dot {
width: 6px;
height: 6px;
border-radius: 50%;
}
.duration-bar {
display: flex;
align-items: center;
gap: 8px;
}
.duration-bar .bar {
width: 60px;
height: 4px;
background: var(--bg-base);
border-radius: 2px;
overflow: hidden;
}
.duration-bar .bar-fill {
height: 100%;
border-radius: 2px;
transition: width 0.3s;
}
.duration-bar .bar-fill.fast { background: var(--green); }
.duration-bar .bar-fill.medium { background: var(--amber); }
.duration-bar .bar-fill.slow { background: var(--rose); }
.correlation-id {
max-width: 140px;
overflow: hidden;
text-overflow: ellipsis;
}
/* ─── Expanded Row / Processor Tree ─── */
.detail-row { display: none; }
.detail-row.visible { display: table-row; }
.detail-row td {
padding: 0;
background: var(--bg-raised);
border-bottom: 1px solid var(--border);
}
.detail-content {
padding: 20px 24px;
display: flex;
gap: 24px;
}
.processor-tree {
flex: 1;
min-width: 0;
}
.processor-tree h4 {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.8px;
color: var(--text-muted);
margin-bottom: 12px;
}
.proc-node {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 8px 12px;
border-radius: var(--radius-sm);
margin-bottom: 2px;
transition: background 0.1s;
position: relative;
}
.proc-node:hover { background: var(--bg-surface); }
.proc-connector {
position: absolute;
left: 22px;
top: 28px;
bottom: -4px;
width: 1px;
background: var(--border);
}
.proc-node:last-child .proc-connector { display: none; }
.proc-icon {
width: 28px;
height: 28px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 700;
flex-shrink: 0;
z-index: 1;
font-family: var(--font-mono);
}
.proc-icon.endpoint { background: rgba(29,78,216,0.08); color: var(--blue); border: 1px solid rgba(29,78,216,0.2); }
.proc-icon.processor { background: var(--green-glow); color: var(--green); border: 1px solid rgba(4,120,87,0.2); }
.proc-icon.eip { background: rgba(124,58,237,0.07); color: var(--purple); border: 1px solid rgba(124,58,237,0.2); }
.proc-icon.error { background: var(--rose-glow); color: var(--rose); border: 1px solid rgba(190,18,60,0.2); }
.proc-info { flex: 1; min-width: 0; }
.proc-info .proc-type { font-size: 12px; font-weight: 600; color: var(--text-primary); }
.proc-info .proc-uri {
font-family: var(--font-mono);
font-size: 11px;
color: var(--text-muted);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.proc-timing {
font-family: var(--font-mono);
font-size: 11px;
color: var(--text-muted);
flex-shrink: 0;
text-align: right;
}
.proc-timing .proc-duration { font-weight: 600; color: var(--text-secondary); }
.detail-sidebar {
width: 280px;
flex-shrink: 0;
}
.detail-sidebar h4 {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.8px;
color: var(--text-muted);
margin-bottom: 12px;
}
.detail-kv {
display: grid;
grid-template-columns: auto 1fr;
gap: 4px 12px;
font-size: 12px;
}
.detail-kv dt { color: var(--text-muted); font-weight: 500; }
.detail-kv dd { font-family: var(--font-mono); color: var(--text-secondary); overflow: hidden; text-overflow: ellipsis; }
.detail-body-preview {
margin-top: 16px;
background: var(--bg-surface);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-sm);
padding: 12px;
font-family: var(--font-mono);
font-size: 11px;
color: var(--text-secondary);
max-height: 120px;
overflow: auto;
white-space: pre-wrap;
word-break: break-all;
}
.detail-body-preview .label {
font-family: var(--font-body);
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--text-muted);
display: block;
margin-bottom: 6px;
}
.error-preview {
margin-top: 12px;
background: var(--rose-glow);
border: 1px solid rgba(190,18,60,0.2);
border-radius: var(--radius-sm);
padding: 10px 12px;
font-family: var(--font-mono);
font-size: 11px;
color: var(--rose);
max-height: 80px;
overflow: auto;
}
/* ─── Pagination ─── */
.pagination {
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
margin-top: 20px;
}
.pagination .page-btn {
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
background: var(--bg-surface);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-sm);
color: var(--text-secondary);
font-family: var(--font-mono);
font-size: 13px;
cursor: pointer;
transition: all 0.15s;
}
.pagination .page-btn:hover { border-color: var(--border); background: var(--bg-raised); }
.pagination .page-btn.active { background: var(--amber-glow); border-color: var(--amber-dim); color: var(--amber); }
.pagination .page-btn.disabled { opacity: 0.3; cursor: default; }
.pagination .page-ellipsis {
color: var(--text-muted);
padding: 0 4px;
font-family: var(--font-mono);
}
/* ─── Animations ─── */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(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; }
.delay-5 { animation-delay: 0.25s; }
/* Scrollbar */
::-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); }
/* ─── Responsive ─── */
@media (max-width: 1200px) {
.stats-bar { grid-template-columns: repeat(3, 1fr); }
.detail-content { flex-direction: column; }
.detail-sidebar { width: 100%; }
}
@media (max-width: 768px) {
.stats-bar { grid-template-columns: 1fr 1fr; }
.filter-row { flex-direction: column; align-items: stretch; }
.search-input-wrap { min-width: unset; }
}
</style>
</head>
<body>
<!-- ═══ Top Navigation ═══ -->
<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>
cameleer
2026-03-13 10:52:43 +01:00
</div>
<ul class="nav-links">
<li><a href="dashboard-light.html">Dashboard</a></li>
<li><a href="transaction-explorer-light.html" class="active">Transactions</a></li>
<li><a href="route-detail-light.html">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 Content ═══ -->
<main class="main">
<!-- Page Header -->
<div class="page-header animate-in">
<div>
<h1>Transaction Explorer</h1>
<div class="subtitle">Search across 47 applications · 2.4M transactions today</div>
</div>
<div class="live-indicator">
<span class="live-dot"></span>
STREAMING
</div>
</div>
<!-- Stats Bar -->
<div class="stats-bar">
<div class="stat-card amber animate-in delay-1">
<div class="stat-label">Total Matches</div>
<div class="stat-value">12,847</div>
<div class="stat-change neutral">of 2.4M today</div>
</div>
<div class="stat-card cyan animate-in delay-2">
<div class="stat-label">Avg Duration</div>
<div class="stat-value">47ms</div>
<div class="stat-change down">↓ 12% vs yesterday</div>
</div>
<div class="stat-card rose animate-in delay-3">
<div class="stat-label">Failure Rate</div>
<div class="stat-value">0.34%</div>
<div class="stat-change up">↑ 0.08% vs yesterday</div>
</div>
<div class="stat-card green animate-in delay-4">
<div class="stat-label">P99 Latency</div>
<div class="stat-value">312ms</div>
<div class="stat-change down">↓ 23ms vs yesterday</div>
</div>
<div class="stat-card blue animate-in delay-5">
<div class="stat-label">Active Now</div>
<div class="stat-value">1,293</div>
<div class="stat-change neutral">exchanges in-flight</div>
</div>
</div>
<!-- Filters -->
<div class="filter-bar animate-in delay-3">
<!-- Row 1: Search -->
<div class="filter-row">
<div class="search-input-wrap">
<svg 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>
<input type="text" placeholder="Search by correlation ID, order ID, error message, route ID..." value="ORD-2024-88431">
<span class="search-hint">⌘K</span>
</div>
<button class="btn-primary btn">Search</button>
<button class="btn">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 21v-7m0 0V5a2 2 0 012-2h2.5l1 2H21l-3 6H9.5l-1-2H6v7z"/></svg>
Export
</button>
</div>
<!-- Row 2: Filters -->
<div class="filter-row">
<div class="filter-group">
<label>Status</label>
<div class="filter-chips">
<span class="chip active green-chip"><span class="status-dot" style="width:6px;height:6px;border-radius:50%;background:var(--green);display:inline-block"></span> Completed <span class="chip-count">12,804</span></span>
<span class="chip active rose-chip"><span class="status-dot" style="width:6px;height:6px;border-radius:50%;background:var(--rose);display:inline-block"></span> Failed <span class="chip-count">43</span></span>
<span class="chip blue-chip"><span class="status-dot" style="width:6px;height:6px;border-radius:50%;background:var(--blue);display:inline-block"></span> Running <span class="chip-count">0</span></span>
</div>
</div>
<div style="width:1px;height:24px;background:var(--border);margin:0 4px"></div>
<div class="filter-group">
<label>Date</label>
<input type="text" class="date-input" value="2026-03-09 00:00" placeholder="From">
<span class="text-muted" style="font-size:12px"></span>
<input type="text" class="date-input" value="2026-03-09 23:59" placeholder="To">
</div>
<div style="width:1px;height:24px;background:var(--border);margin:0 4px"></div>
<div class="filter-group">
<label>Duration</label>
<div class="duration-range">
<span class="range-label">0ms</span>
<input type="range" min="0" max="5000" value="5000">
<span class="range-label">≤ 5000ms</span>
</div>
</div>
</div>
<!-- Row 3: App + Route filters -->
<div class="filter-row">
<div class="filter-group">
<label>Application</label>
<select class="filter-select">
<option>All Applications (47)</option>
<option selected>order-service-eu</option>
<option>payment-gateway</option>
<option>inventory-sync</option>
<option>notification-hub</option>
<option>etl-pipeline-prod</option>
</select>
</div>
<div class="filter-group">
<label>Route</label>
<select class="filter-select">
<option>All Routes (1,283)</option>
<option>order-intake</option>
<option selected>content-based-routing</option>
<option>payment-processing</option>
<option>inventory-check</option>
</select>
</div>
<div class="filter-tags" style="margin-left:8px">
<span class="filter-tag">app:order-service-eu <span class="remove">×</span></span>
<span class="filter-tag">route:content-based-routing <span class="remove">×</span></span>
<span class="filter-tag">text:"ORD-2024-88431" <span class="remove">×</span></span>
<span class="clear-all">Clear all</span>
</div>
</div>
</div>
<!-- Results Header -->
<div class="results-header animate-in delay-4">
<span class="results-count">Showing <strong>125</strong> of <strong>12,847</strong> results · sorted by <strong>timestamp</strong> desc</span>
<div class="results-actions">
<button class="btn" style="font-size:12px;padding:6px 10px">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18M3 12h18M3 18h18"/></svg>
Columns
</button>
<button class="btn" style="font-size:12px;padding:6px 10px">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4m4-5l5 5 5-5m-5 5V3"/></svg>
CSV
</button>
</div>
</div>
<!-- Results Table -->
<div class="table-wrap animate-in delay-4">
<table>
<thead>
<tr>
<th style="width:32px"></th>
<th class="sorted">Timestamp</th>
<th>Status</th>
<th>Application</th>
<th>Route</th>
<th>Correlation ID</th>
<th>Duration</th>
<th>Processors</th>
</tr>
</thead>
<tbody id="results-body">
</tbody>
</table>
</div>
<!-- Pagination -->
<div class="pagination animate-in delay-5">
<div class="page-btn disabled"></div>
<div class="page-btn active">1</div>
<div class="page-btn">2</div>
<div class="page-btn">3</div>
<div class="page-btn">4</div>
<div class="page-btn">5</div>
<span class="page-ellipsis"></span>
<div class="page-btn">514</div>
<div class="page-btn"></div>
</div>
</main>
<script>
// ─── Mock Data Generator ───
const APPS = [
{ name: 'order-service-eu', color: '#1d4ed8' },
{ name: 'payment-gateway', color: '#b45309' },
{ name: 'inventory-sync', color: '#047857' },
{ name: 'notification-hub', color: '#7c3aed' },
{ name: 'etl-pipeline-prod', color: '#be123c' },
{ name: 'shipping-tracker', color: '#0e7490' },
{ name: 'customer-portal', color: '#c026d3' },
];
const ROUTES = [
'order-intake', 'content-based-routing', 'payment-processing',
'inventory-check', 'notification-dispatch', 'etl-transform',
'shipment-update', 'order-validation', 'fraud-check',
'email-sender', 'sms-gateway', 'audit-log',
];
const ERROR_MESSAGES = [
'Connection refused: payment-provider-api:8443',
'Timeout waiting for response from inventory-service (30000ms)',
'org.apache.camel.CamelExecutionException: MVEL expression failed',
'javax.jms.JMSException: Queue ORDERS.PRIORITY is full',
'HttpOperationFailedException: HTTP operation failed with status 503',
'com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: field "qty"',
];
const PROCESSOR_TREES = {
completed: [
{ type: 'From', icon: 'EP', iconClass: 'endpoint', uri: 'direct:order-intake', duration: 0 },
{ type: 'SetHeader', icon: 'PR', iconClass: 'processor', uri: 'X-Cameleer-CorrelationId', duration: 1 },
{ type: 'Log', icon: 'PR', iconClass: 'processor', uri: 'Received order: ${body}', duration: 0 },
{ type: 'Choice', icon: 'CB', iconClass: 'eip', uri: 'content-based-router', duration: 0 },
{ type: 'When', icon: 'CB', iconClass: 'eip', uri: '${header.priority} == "HIGH"', duration: 0 },
{ type: 'To', icon: 'EP', iconClass: 'endpoint', uri: 'direct:priority-processing', duration: 35 },
{ type: 'Marshal', icon: 'PR', iconClass: 'processor', uri: 'jackson', duration: 2 },
{ type: 'To', icon: 'EP', iconClass: 'endpoint', uri: 'jms:queue:ORDERS.COMPLETED', duration: 8 },
],
failed: [
{ type: 'From', icon: 'EP', iconClass: 'endpoint', uri: 'direct:payment-processing', duration: 0 },
{ type: 'SetHeader', icon: 'PR', iconClass: 'processor', uri: 'Authorization: Bearer ***', duration: 0 },
{ type: 'To', icon: 'EP', iconClass: 'endpoint', uri: 'https://api.payment-provider.com/v2/charge', duration: 3042 },
{ type: 'Exception', icon: '!!', iconClass: 'error', uri: 'HttpOperationFailedException: HTTP 503', duration: 0 },
]
};
function randomId() {
const hex = () => Math.random().toString(16).slice(2, 10);
return hex() + hex();
}
function generateTransactions(count) {
const txns = [];
const now = Date.now();
for (let i = 0; i < count; i++) {
const isFailed = Math.random() < 0.04;
const isRunning = !isFailed && Math.random() < 0.005;
const status = isRunning ? 'running' : (isFailed ? 'failed' : 'completed');
const app = APPS[Math.floor(Math.random() * APPS.length)];
const route = ROUTES[Math.floor(Math.random() * ROUTES.length)];
const duration = isFailed
? Math.floor(Math.random() * 30000) + 100
: Math.floor(Math.random() * 500) + 5;
const procCount = Math.floor(Math.random() * 12) + 3;
const orderId = `ORD-2024-${(88000 + Math.floor(Math.random() * 1000)).toString()}`;
const ts = new Date(now - i * (Math.random() * 2000 + 500));
txns.push({
id: randomId(),
timestamp: ts,
status,
app,
route,
correlationId: randomId(),
duration,
processorCount: procCount,
orderId,
errorMessage: isFailed ? ERROR_MESSAGES[Math.floor(Math.random() * ERROR_MESSAGES.length)] : null,
});
}
// Ensure our search target is in the list
txns[2].orderId = 'ORD-2024-88431';
txns[2].app = APPS[0];
txns[2].route = 'content-based-routing';
txns[2].status = 'completed';
txns[2].duration = 47;
txns[7].status = 'failed';
txns[7].duration = 3142;
txns[7].errorMessage = ERROR_MESSAGES[0];
return txns;
}
function formatTime(date) {
return date.toTimeString().slice(0, 12);
}
function durationClass(ms) {
if (ms < 100) return 'fast';
if (ms < 1000) return 'medium';
return 'slow';
}
function durationWidth(ms) {
return Math.min(100, (ms / 5000) * 100);
}
function renderTable(txns) {
const tbody = document.getElementById('results-body');
let html = '';
txns.forEach((txn, idx) => {
html += `
<tr onclick="toggleDetail(${idx})" data-idx="${idx}">
<td class="td-expand"></td>
<td class="mono">${formatTime(txn.timestamp)}</td>
<td>
<span class="status-pill ${txn.status}">
<span class="status-dot"></span>
${txn.status.charAt(0).toUpperCase() + txn.status.slice(1)}
</span>
</td>
<td>
<span class="app-badge">
<span class="app-dot" style="background:${txn.app.color}"></span>
${txn.app.name}
</span>
</td>
<td class="mono text-secondary">${txn.route}</td>
<td class="mono text-muted correlation-id" title="${txn.correlationId}">${txn.correlationId}</td>
<td>
<div class="duration-bar">
<span class="mono" style="color:${txn.duration > 1000 ? 'var(--rose)' : txn.duration > 100 ? 'var(--amber)' : 'var(--green)'}">${txn.duration.toLocaleString()}ms</span>
<div class="bar"><div class="bar-fill ${durationClass(txn.duration)}" style="width:${durationWidth(txn.duration)}%"></div></div>
</div>
</td>
<td class="mono text-muted">${txn.processorCount}</td>
</tr>
<tr class="detail-row" id="detail-${idx}">
<td colspan="8">
<div class="detail-content">
<div class="processor-tree">
<h4>Processor Execution Tree</h4>
${renderProcessorTree(txn)}
</div>
<div class="detail-sidebar">
<h4>Exchange Details</h4>
<dl class="detail-kv">
<dt>Exchange ID</dt><dd>${txn.id}</dd>
<dt>Correlation</dt><dd>${txn.correlationId}</dd>
<dt>Order ID</dt><dd>${txn.orderId}</dd>
<dt>Application</dt><dd>${txn.app.name}</dd>
<dt>Route</dt><dd>${txn.route}</dd>
<dt>Timestamp</dt><dd>${txn.timestamp.toISOString()}</dd>
<dt>Duration</dt><dd>${txn.duration}ms</dd>
<dt>Processors</dt><dd>${txn.processorCount}</dd>
</dl>
<div class="detail-body-preview">
<span class="label">Input Body</span>{"orderId":"${txn.orderId}","customer":"ACME Corp","items":[{"sku":"WDG-4401","qty":3,"price":29.99}],"priority":"HIGH","region":"EU-WEST"}</div>
${txn.errorMessage ? `<div class="error-preview">${txn.errorMessage}</div>` : ''}
</div>
</div>
</td>
</tr>
`;
});
tbody.innerHTML = html;
}
function renderProcessorTree(txn) {
const tree = txn.status === 'failed' ? PROCESSOR_TREES.failed : PROCESSOR_TREES.completed;
return tree.map(proc => `
<div class="proc-node">
<div class="proc-connector"></div>
<div class="proc-icon ${proc.iconClass}">${proc.icon}</div>
<div class="proc-info">
<div class="proc-type">${proc.type}</div>
<div class="proc-uri">${proc.uri}</div>
</div>
<div class="proc-timing">
<span class="proc-duration">${proc.duration}ms</span>
</div>
</div>
`).join('');
}
function toggleDetail(idx) {
const row = document.querySelector(`tr[data-idx="${idx}"]`);
const detail = document.getElementById(`detail-${idx}`);
row.classList.toggle('expanded');
detail.classList.toggle('visible');
}
// Init
const transactions = generateTransactions(25);
renderTable(transactions);
// Chip toggle
document.querySelectorAll('.chip').forEach(chip => {
chip.addEventListener('click', () => chip.classList.toggle('active'));
});
</script>
</body>
</html>