Files
cameleer-server/examples/transaction-explorer-light.html
hsiegeln cb3ebfea7c
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Failing after 18s
CI / docker (push) Has been skipped
CI / deploy (push) Has been skipped
CI / deploy-feature (push) Has been skipped
chore: rename cameleer3 to cameleer
Rename Java packages from com.cameleer3 to com.cameleer, module
directories from cameleer3-* to cameleer-*, and all references
throughout workflows, Dockerfiles, docs, migrations, and pom.xml.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 15:28:42 +02:00

1310 lines
38 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>Cameleer — Transaction Explorer</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;
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
</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>