1310 lines
38 KiB
HTML
1310 lines
38 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Cameleer3 — 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>
|
||
cameleer3
|
||
</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>1–25</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>
|