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

1129 lines
33 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 — Dashboard</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,650;0,9..40,700;0,9..40,800&family=JetBrains+Mono:wght@300;400;500;600&display=swap" rel="stylesheet">
<style>
:root {
--bg-deep: #f7f5f2;
--bg-base: #efecea;
--bg-surface: #ffffff;
--bg-raised: #f3f1ee;
--bg-hover: #eae7e3;
--border: #d4cfc8;
--border-subtle: #e4e0db;
--text-primary: #1c1917;
--text-secondary: #57534e;
--text-muted: #a8a29e;
--amber: #b45309;
--amber-dim: #92400e;
--amber-glow: rgba(180, 83, 9, 0.07);
--cyan: #0e7490;
--cyan-dim: #155e75;
--cyan-glow: rgba(14, 116, 144, 0.06);
--rose: #be123c;
--rose-dim: #9f1239;
--rose-glow: rgba(190, 18, 60, 0.05);
--green: #047857;
--green-glow: rgba(4, 120, 87, 0.06);
--blue: #1d4ed8;
--purple: #7c3aed;
--font-body: 'DM Sans', system-ui, sans-serif;
--font-mono: 'JetBrains Mono', 'Fira Code', monospace;
--radius-sm: 6px;
--radius-md: 10px;
--radius-lg: 14px;
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
background: var(--bg-deep);
color: var(--text-primary);
font-family: var(--font-body);
font-size: 14px;
line-height: 1.5;
min-height: 100vh;
}
body::before {
content: '';
position: fixed;
inset: 0;
background:
radial-gradient(ellipse 800px 400px at 20% 20%, rgba(180, 83, 9, 0.02), transparent),
radial-gradient(ellipse 600px 600px at 80% 80%, rgba(14, 116, 144, 0.015), transparent);
pointer-events: none;
z-index: 0;
}
body::after {
content: '';
position: fixed;
inset: 0;
opacity: 0.025;
background-image: url("data:image/svg+xml,%3Csvg width='400' height='400' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 200 Q100 150 200 200 T400 200' fill='none' stroke='%23b45309' stroke-width='1' opacity='0.018'/%3E%3Cpath d='M0 220 Q120 170 200 220 T400 220' fill='none' stroke='%23b45309' stroke-width='0.5' opacity='0.018'/%3E%3Cpath d='M0 180 Q80 130 200 180 T400 180' fill='none' stroke='%23b45309' stroke-width='0.5' opacity='0.018'/%3E%3Cpath d='M0 100 Q150 60 200 100 T400 100' fill='none' stroke='%230e7490' stroke-width='0.5' opacity='0.012'/%3E%3Cpath d='M0 300 Q100 260 200 300 T400 300' fill='none' stroke='%230e7490' stroke-width='0.5' opacity='0.012'/%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), 0 0 0 1px rgba(0,0,0,0.02);
padding: 0 24px;
display: flex;
align-items: center;
height: 56px;
gap: 32px;
}
.topnav .logo {
font-family: var(--font-mono);
font-weight: 600;
font-size: 16px;
color: var(--amber);
letter-spacing: -0.5px;
display: flex;
align-items: center;
gap: 10px;
flex-shrink: 0;
}
.topnav .logo svg { width: 22px; height: 22px; }
.topnav .nav-links {
display: flex;
gap: 4px;
list-style: none;
}
.topnav .nav-links a {
padding: 8px 16px;
border-radius: var(--radius-sm);
font-size: 13px;
font-weight: 500;
color: var(--text-secondary);
transition: all 0.15s;
}
.topnav .nav-links a:hover { color: var(--text-primary); background: var(--bg-raised); }
.topnav .nav-links a.active { color: var(--amber); background: var(--amber-glow); }
.topnav .nav-right {
margin-left: auto;
display: flex;
align-items: center;
gap: 16px;
}
.env-badge {
font-family: var(--font-mono);
font-size: 11px;
padding: 4px 10px;
border-radius: 99px;
background: var(--green-glow);
color: var(--green);
border: 1px solid rgba(4, 120, 87, 0.2);
font-weight: 500;
}
.cluster-count {
font-size: 12px;
color: var(--text-muted);
font-family: var(--font-mono);
}
/* ─── Main 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: 800;
letter-spacing: -0.5px;
}
.page-header .subtitle {
font-size: 13px;
color: var(--text-muted);
margin-top: 2px;
}
.time-range-selector {
display: flex;
gap: 4px;
background: var(--bg-surface);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-sm);
padding: 3px;
}
.time-range-selector .range-btn {
padding: 6px 14px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
color: var(--text-muted);
cursor: pointer;
transition: all 0.15s;
border: none;
background: none;
font-family: var(--font-body);
}
.time-range-selector .range-btn:hover { color: var(--text-secondary); }
.time-range-selector .range-btn.active { background: var(--amber-glow); color: var(--amber); }
/* ─── Hero Stats ─── */
.hero-stats {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 12px;
margin-bottom: 24px;
}
.hero-stat {
background: var(--bg-surface);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-md);
padding: 18px 20px;
position: relative;
overflow: hidden;
transition: border-color 0.2s, box-shadow 0.2s;
}
.hero-stat:hover { border-color: var(--border); box-shadow: 0 2px 8px rgba(0,0,0,0.06); }
.hero-stat::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 2px;
}
.hero-stat .label {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.8px;
color: var(--text-muted);
margin-bottom: 8px;
}
.hero-stat .value {
font-family: var(--font-mono);
font-size: 28px;
font-weight: 600;
letter-spacing: -1px;
}
.hero-stat .sparkline {
margin-top: 10px;
height: 24px;
}
.hero-stat .sparkline svg { width: 100%; height: 100%; }
/* Color variants */
.hero-stat.amber::before { background: linear-gradient(90deg, var(--amber), transparent); }
.hero-stat.amber .value { color: var(--amber); }
.hero-stat.cyan::before { background: linear-gradient(90deg, var(--cyan), transparent); }
.hero-stat.cyan .value { color: var(--cyan); }
.hero-stat.rose::before { background: linear-gradient(90deg, var(--rose), transparent); }
.hero-stat.rose .value { color: var(--rose); }
.hero-stat.green::before { background: linear-gradient(90deg, var(--green), transparent); }
.hero-stat.green .value { color: var(--green); }
.hero-stat.blue::before { background: linear-gradient(90deg, var(--blue), transparent); }
.hero-stat.blue .value { color: var(--blue); }
.hero-stat.purple::before { background: linear-gradient(90deg, var(--purple), transparent); }
.hero-stat.purple .value { color: var(--purple); }
/* ─── Grid Layout ─── */
.dashboard-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin-bottom: 16px;
}
.dashboard-grid.triple {
grid-template-columns: 1fr 1fr 1fr;
}
.card {
background: var(--bg-surface);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-lg);
overflow: hidden;
transition: border-color 0.2s, box-shadow 0.2s;
box-shadow: 0 1px 3px rgba(0,0,0,0.04), 0 1px 2px rgba(0,0,0,0.02);
}
.card:hover { border-color: var(--border); box-shadow: 0 2px 8px rgba(0,0,0,0.06); }
.card-header {
padding: 16px 20px 12px;
display: flex;
align-items: center;
justify-content: space-between;
}
.card-title {
font-size: 13px;
font-weight: 650;
color: var(--text-primary);
}
.card-subtitle {
font-size: 11px;
color: var(--text-muted);
font-family: var(--font-mono);
}
.card-body {
padding: 0 20px 20px;
}
.card.full-width {
grid-column: 1 / -1;
}
/* ─── Chart Container ─── */
.chart-area {
width: 100%;
position: relative;
}
.chart-area canvas {
width: 100% !important;
height: 200px !important;
}
/* SVG Charts */
.svg-chart {
width: 100%;
overflow: visible;
}
.svg-chart text {
font-family: var(--font-mono);
fill: var(--text-muted);
font-size: 10px;
}
.svg-chart .grid-line {
stroke: var(--border-subtle);
stroke-width: 0.5;
}
.svg-chart .axis-line {
stroke: var(--border);
stroke-width: 1;
}
.chart-legend {
display: flex;
gap: 16px;
margin-top: 12px;
}
.chart-legend .legend-item {
display: flex;
align-items: center;
gap: 6px;
font-size: 11px;
color: var(--text-muted);
}
.chart-legend .legend-dot {
width: 8px;
height: 8px;
border-radius: 2px;
}
/* ─── Heatmap ─── */
.heatmap {
display: grid;
gap: 2px;
}
.heatmap-row {
display: flex;
align-items: center;
gap: 2px;
}
.heatmap-label {
width: 32px;
font-family: var(--font-mono);
font-size: 10px;
color: var(--text-muted);
text-align: right;
padding-right: 6px;
flex-shrink: 0;
}
.heatmap-cell {
flex: 1;
aspect-ratio: 1.8;
min-height: 16px;
border-radius: 3px;
transition: transform 0.1s, box-shadow 0.1s;
cursor: pointer;
position: relative;
}
.heatmap-cell:hover {
transform: scale(1.3);
z-index: 2;
box-shadow: 0 0 0 2px var(--bg-deep), 0 0 0 3px var(--amber-dim);
}
.heatmap-cell .tooltip {
display: none;
position: absolute;
bottom: calc(100% + 8px);
left: 50%;
transform: translateX(-50%);
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 6px 10px;
font-size: 11px;
font-family: var(--font-mono);
color: var(--text-primary);
white-space: nowrap;
z-index: 10;
pointer-events: none;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
}
.heatmap-cell:hover .tooltip { display: block; }
.heatmap-day-labels {
display: flex;
gap: 2px;
margin-left: 38px;
margin-bottom: 4px;
}
.heatmap-day-labels span {
flex: 1;
text-align: center;
font-family: var(--font-mono);
font-size: 9px;
color: var(--text-muted);
}
.heatmap-scale {
display: flex;
align-items: center;
gap: 4px;
margin-top: 10px;
justify-content: flex-end;
}
.heatmap-scale .scale-label {
font-size: 10px;
font-family: var(--font-mono);
color: var(--text-muted);
}
.heatmap-scale .scale-cell {
width: 14px;
height: 14px;
border-radius: 2px;
}
/* ─── Application Health Grid ─── */
.app-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
gap: 6px;
}
.app-tile {
background: var(--bg-surface);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-sm);
padding: 10px 12px;
cursor: pointer;
transition: all 0.15s;
position: relative;
overflow: hidden;
}
.app-tile:hover {
border-color: var(--border);
background: var(--bg-surface);
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
}
.app-tile::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 3px;
}
.app-tile.healthy::before { background: var(--green); }
.app-tile.warning::before { background: var(--amber); }
.app-tile.critical::before { background: var(--rose); }
.app-tile .app-name {
font-size: 11px;
font-weight: 600;
color: var(--text-primary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-bottom: 4px;
}
.app-tile .app-stats {
display: flex;
justify-content: space-between;
align-items: baseline;
}
.app-tile .app-routes {
font-family: var(--font-mono);
font-size: 10px;
color: var(--text-muted);
}
.app-tile .app-rate {
font-family: var(--font-mono);
font-size: 10px;
font-weight: 600;
}
.app-tile.healthy .app-rate { color: var(--green); }
.app-tile.warning .app-rate { color: var(--amber); }
.app-tile.critical .app-rate { color: var(--rose); }
/* ─── Top Lists ─── */
.top-list {
list-style: none;
}
.top-list li {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 0;
border-bottom: 1px solid var(--border-subtle);
font-size: 13px;
}
.top-list li:last-child { border-bottom: none; }
.top-list .rank {
font-family: var(--font-mono);
font-size: 11px;
font-weight: 600;
color: var(--text-muted);
width: 20px;
text-align: right;
flex-shrink: 0;
}
.top-list .route-info {
flex: 1;
min-width: 0;
}
.top-list .route-name {
font-family: var(--font-mono);
font-size: 12px;
color: var(--text-primary);
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.top-list .route-app {
font-size: 10px;
color: var(--text-muted);
}
.top-list .route-metric {
font-family: var(--font-mono);
font-size: 12px;
font-weight: 600;
flex-shrink: 0;
}
.top-list .route-bar {
width: 80px;
height: 4px;
background: var(--bg-base);
border-radius: 2px;
overflow: hidden;
flex-shrink: 0;
}
.top-list .route-bar-fill {
height: 100%;
border-radius: 2px;
}
/* ─── Animations ─── */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes livePulse {
0%, 100% { box-shadow: 0 0 0 0 rgba(4, 120, 87, 0.3); }
50% { box-shadow: 0 0 0 6px rgba(4, 120, 87, 0); }
}
.animate-in { animation: fadeIn 0.3s ease-out both; }
.delay-1 { animation-delay: 0.05s; }
.delay-2 { animation-delay: 0.1s; }
.delay-3 { animation-delay: 0.15s; }
.delay-4 { animation-delay: 0.2s; }
.delay-5 { animation-delay: 0.25s; }
.delay-6 { animation-delay: 0.3s; }
::-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); }
.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;
}
@media (max-width: 1200px) {
.hero-stats { grid-template-columns: repeat(3, 1fr); }
.dashboard-grid { grid-template-columns: 1fr; }
.dashboard-grid.triple { grid-template-columns: 1fr; }
}
@media (max-width: 768px) {
.hero-stats { grid-template-columns: 1fr 1fr; }
}
</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" class="active">Dashboard</a></li>
<li><a href="transaction-explorer-light.html">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">
<div class="page-header animate-in">
<div>
<h1>Fleet Overview</h1>
<div class="subtitle">Real-time observability across 47 applications</div>
</div>
<div style="display:flex;gap:16px;align-items:center">
<div class="live-indicator">
<span class="live-dot"></span>
LIVE
</div>
<div class="time-range-selector">
<button class="range-btn">1h</button>
<button class="range-btn">6h</button>
<button class="range-btn active">24h</button>
<button class="range-btn">7d</button>
<button class="range-btn">30d</button>
</div>
</div>
</div>
<!-- Hero Stats -->
<div class="hero-stats">
<div class="hero-stat amber animate-in delay-1">
<div class="label">Total Exchanges</div>
<div class="value">2.4M</div>
<div class="sparkline" id="sparkline-vol"></div>
</div>
<div class="hero-stat green animate-in delay-2">
<div class="label">Success Rate</div>
<div class="value">99.66%</div>
<div class="sparkline" id="sparkline-success"></div>
</div>
<div class="hero-stat rose animate-in delay-3">
<div class="label">Failed</div>
<div class="value">8,147</div>
<div class="sparkline" id="sparkline-failed"></div>
</div>
<div class="hero-stat cyan animate-in delay-4">
<div class="label">Avg Latency</div>
<div class="value">47ms</div>
<div class="sparkline" id="sparkline-latency"></div>
</div>
<div class="hero-stat purple animate-in delay-5">
<div class="label">Active Routes</div>
<div class="value">1,283</div>
<div class="sparkline" id="sparkline-routes"></div>
</div>
<div class="hero-stat blue animate-in delay-6">
<div class="label">In-Flight</div>
<div class="value">1,293</div>
<div class="sparkline" id="sparkline-inflight"></div>
</div>
</div>
<!-- Charts Row 1: Failure Rate Trend + Latency Percentiles -->
<div class="dashboard-grid animate-in delay-3">
<div class="card">
<div class="card-header">
<div>
<div class="card-title">Failure Rate Trend</div>
<div class="card-subtitle">Last 24 hours · 15-min intervals</div>
</div>
</div>
<div class="card-body">
<div class="chart-area">
<svg class="svg-chart" viewBox="0 0 600 200" id="chart-failure"></svg>
</div>
<div class="chart-legend">
<div class="legend-item"><div class="legend-dot" style="background:var(--rose)"></div> Failure %</div>
<div class="legend-item"><div class="legend-dot" style="background:var(--amber)"></div> Threshold</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<div>
<div class="card-title">Latency Percentiles</div>
<div class="card-subtitle">Last 24 hours · p50 / p95 / p99</div>
</div>
</div>
<div class="card-body">
<div class="chart-area">
<svg class="svg-chart" viewBox="0 0 600 200" id="chart-latency"></svg>
</div>
<div class="chart-legend">
<div class="legend-item"><div class="legend-dot" style="background:var(--green)"></div> p50</div>
<div class="legend-item"><div class="legend-dot" style="background:var(--amber)"></div> p95</div>
<div class="legend-item"><div class="legend-dot" style="background:var(--rose)"></div> p99</div>
</div>
</div>
</div>
</div>
<!-- Volume Heatmap -->
<div class="dashboard-grid" style="grid-template-columns:1fr">
<div class="card animate-in delay-4">
<div class="card-header">
<div>
<div class="card-title">Exchange Volume Heatmap</div>
<div class="card-subtitle">Last 7 days · hourly buckets</div>
</div>
</div>
<div class="card-body">
<div class="heatmap-day-labels" id="heatmap-days"></div>
<div class="heatmap" id="heatmap"></div>
<div class="heatmap-scale">
<span class="scale-label">Less</span>
<div class="scale-cell" style="background:rgba(180,83,9,0.04)"></div>
<div class="scale-cell" style="background:rgba(180,83,9,0.15)"></div>
<div class="scale-cell" style="background:rgba(180,83,9,0.3)"></div>
<div class="scale-cell" style="background:rgba(180,83,9,0.5)"></div>
<div class="scale-cell" style="background:rgba(180,83,9,0.65)"></div>
<span class="scale-label">More</span>
</div>
</div>
</div>
</div>
<!-- Charts Row 2: Top Failing + Slowest Routes -->
<div class="dashboard-grid animate-in delay-5">
<div class="card">
<div class="card-header">
<div>
<div class="card-title">Top Failing Routes</div>
<div class="card-subtitle">By failure count · last 24h</div>
</div>
</div>
<div class="card-body">
<ul class="top-list" id="top-failing"></ul>
</div>
</div>
<div class="card">
<div class="card-header">
<div>
<div class="card-title">Slowest Routes</div>
<div class="card-subtitle">By p95 latency · last 24h</div>
</div>
</div>
<div class="card-body">
<ul class="top-list" id="top-slowest"></ul>
</div>
</div>
</div>
<!-- Application Health Grid -->
<div class="dashboard-grid" style="grid-template-columns:1fr">
<div class="card animate-in delay-6">
<div class="card-header">
<div>
<div class="card-title">Application Health</div>
<div class="card-subtitle">47 applications · sorted by failure rate</div>
</div>
</div>
<div class="card-body">
<div class="app-grid" id="app-grid"></div>
</div>
</div>
</div>
</main>
<script>
// ─── Sparkline Generator ───
function sparkline(containerId, data, color) {
const el = document.getElementById(containerId);
const w = 200, h = 24;
const max = Math.max(...data);
const min = Math.min(...data);
const range = max - min || 1;
const step = w / (data.length - 1);
const points = data.map((v, i) => `${i * step},${h - ((v - min) / range) * (h - 2) - 1}`);
const gradId = containerId + '-grad';
el.innerHTML = `
<svg viewBox="0 0 ${w} ${h}" preserveAspectRatio="none">
<defs>
<linearGradient id="${gradId}" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="${color}" stop-opacity="0.3"/>
<stop offset="100%" stop-color="${color}" stop-opacity="0"/>
</linearGradient>
</defs>
<polygon points="0,${h} ${points.join(' ')} ${w},${h}" fill="url(#${gradId})"/>
<polyline points="${points.join(' ')}" fill="none" stroke="${color}" stroke-width="1.5" stroke-linejoin="round"/>
</svg>
`;
}
function randomSeries(len, base, variance) {
const data = [];
let v = base;
for (let i = 0; i < len; i++) {
v += (Math.random() - 0.5) * variance;
v = Math.max(base * 0.3, v);
data.push(v);
}
return data;
}
sparkline('sparkline-vol', randomSeries(24, 100000, 20000), '#b45309');
sparkline('sparkline-success', randomSeries(24, 99.6, 0.2), '#047857');
sparkline('sparkline-failed', randomSeries(24, 340, 80), '#be123c');
sparkline('sparkline-latency', randomSeries(24, 47, 12), '#0e7490');
sparkline('sparkline-routes', [1280,1280,1281,1281,1282,1282,1283,1283,1283,1283,1283,1283,1283,1283,1283,1283,1283,1283,1283,1283,1283,1283,1283,1283], '#7c3aed');
sparkline('sparkline-inflight', randomSeries(24, 1200, 400), '#1d4ed8');
// ─── SVG Line Chart: Failure Rate ───
function drawLineChart(svgId, datasets, yLabels, gridLines) {
const svg = document.getElementById(svgId);
const W = 600, H = 200;
const pad = { top: 10, right: 15, bottom: 25, left: 45 };
const cw = W - pad.left - pad.right;
const ch = H - pad.top - pad.bottom;
let html = '';
// Grid lines
gridLines.forEach(val => {
const y = pad.top + ch - (val / gridLines[gridLines.length - 1]) * ch;
html += `<line class="grid-line" x1="${pad.left}" y1="${y}" x2="${W - pad.right}" y2="${y}"/>`;
html += `<text x="${pad.left - 8}" y="${y + 3}" text-anchor="end" font-size="10">${val}${yLabels.suffix || ''}</text>`;
});
// X-axis labels
const xLabels = ['00:00', '04:00', '08:00', '12:00', '16:00', '20:00', '24:00'];
xLabels.forEach((label, i) => {
const x = pad.left + (i / (xLabels.length - 1)) * cw;
html += `<text x="${x}" y="${H - 4}" text-anchor="middle" font-size="10">${label}</text>`;
});
// Data lines
datasets.forEach(ds => {
const points = ds.data.map((v, i) => {
const x = pad.left + (i / (ds.data.length - 1)) * cw;
const y = pad.top + ch - (v / gridLines[gridLines.length - 1]) * ch;
return `${x},${y}`;
});
// Area fill
if (ds.fill) {
const gradId = svgId + '-' + ds.label;
html += `<defs><linearGradient id="${gradId}" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="${ds.color}" stop-opacity="0.08"/>
<stop offset="100%" stop-color="${ds.color}" stop-opacity="0"/>
</linearGradient></defs>`;
html += `<polygon points="${pad.left},${pad.top + ch} ${points.join(' ')} ${W - pad.right},${pad.top + ch}" fill="url(#${gradId})"/>`;
}
// Line
html += `<polyline points="${points.join(' ')}" fill="none" stroke="${ds.color}" stroke-width="${ds.width || 1.5}" stroke-linejoin="round" ${ds.dash ? `stroke-dasharray="${ds.dash}"` : ''}/>`;
});
svg.innerHTML = html;
}
// Failure rate data
const failureData = randomSeries(96, 0.35, 0.12).map(v => Math.max(0, v));
drawLineChart('chart-failure', [
{ label: 'failure', data: failureData, color: '#be123c', fill: true },
{ label: 'threshold', data: Array(96).fill(0.5), color: '#b45309', width: 1, dash: '4,4' }
], { suffix: '%' }, [0, 0.25, 0.5, 0.75, 1.0]);
// Latency percentile data
const p50 = randomSeries(96, 25, 8);
const p95 = randomSeries(96, 120, 30);
const p99 = randomSeries(96, 310, 60);
drawLineChart('chart-latency', [
{ label: 'p99', data: p99, color: '#be123c', fill: true },
{ label: 'p95', data: p95, color: '#b45309', fill: true },
{ label: 'p50', data: p50, color: '#047857', fill: true },
], { suffix: 'ms' }, [0, 100, 200, 300, 400, 500]);
// ─── Volume Heatmap ───
(function() {
const container = document.getElementById('heatmap');
const daysEl = document.getElementById('heatmap-days');
const days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
const hours = Array.from({length: 24}, (_, i) => i);
// Day labels
days.forEach(d => {
const span = document.createElement('span');
span.textContent = d;
daysEl.appendChild(span);
});
// Generate volume data (business hours = more traffic)
hours.forEach(hour => {
const row = document.createElement('div');
row.className = 'heatmap-row';
const label = document.createElement('div');
label.className = 'heatmap-label';
label.textContent = hour.toString().padStart(2, '0');
row.appendChild(label);
days.forEach(day => {
const cell = document.createElement('div');
cell.className = 'heatmap-cell';
// Simulate realistic traffic: business hours (8-18) on weekdays get more
const isWeekday = ['Mon','Tue','Wed','Thu','Fri'].includes(day);
const isBusinessHour = hour >= 8 && hour <= 18;
const isNight = hour >= 0 && hour <= 5;
let intensity;
if (isWeekday && isBusinessHour) {
intensity = 0.5 + Math.random() * 0.5;
} else if (isWeekday && !isNight) {
intensity = 0.2 + Math.random() * 0.3;
} else if (isNight) {
intensity = 0.02 + Math.random() * 0.08;
} else {
intensity = 0.05 + Math.random() * 0.2;
}
const volume = Math.floor(intensity * 200000);
cell.style.background = `rgba(180, 83, 9, ${intensity * 0.65})`;
const tooltip = document.createElement('div');
tooltip.className = 'tooltip';
tooltip.textContent = `${day} ${hour}:00 — ${volume.toLocaleString()} txns`;
cell.appendChild(tooltip);
row.appendChild(cell);
});
container.appendChild(row);
});
})();
// ─── Top Failing Routes ───
(function() {
const list = document.getElementById('top-failing');
const routes = [
{ name: 'payment-processing', app: 'payment-gateway', failures: 2841, total: 89420 },
{ name: 'inventory-check', app: 'inventory-sync', failures: 1203, total: 156000 },
{ name: 'email-sender', app: 'notification-hub', failures: 987, total: 234000 },
{ name: 'fraud-check', app: 'order-service-eu', failures: 812, total: 89420 },
{ name: 'shipment-update', app: 'shipping-tracker', failures: 504, total: 78000 },
{ name: 'etl-transform-legacy', app: 'etl-pipeline-prod', failures: 398, total: 45000 },
{ name: 'sms-gateway', app: 'notification-hub', failures: 287, total: 67000 },
{ name: 'order-validation', app: 'order-service-eu', failures: 115, total: 156000 },
];
const maxFailures = routes[0].failures;
routes.forEach((r, i) => {
const pct = ((r.failures / r.total) * 100).toFixed(2);
const barWidth = (r.failures / maxFailures) * 100;
list.innerHTML += `
<li>
<span class="rank">${i + 1}</span>
<div class="route-info">
<div class="route-name">${r.name}</div>
<div class="route-app">${r.app}</div>
</div>
<div class="route-bar"><div class="route-bar-fill" style="width:${barWidth}%;background:var(--rose)"></div></div>
<span class="route-metric" style="color:var(--rose)">${r.failures.toLocaleString()}</span>
</li>`;
});
})();
// ─── Top Slowest Routes ───
(function() {
const list = document.getElementById('top-slowest');
const routes = [
{ name: 'etl-transform-legacy', app: 'etl-pipeline-prod', p95: 4823 },
{ name: 'payment-processing', app: 'payment-gateway', p95: 2140 },
{ name: 'report-generation', app: 'analytics-engine', p95: 1870 },
{ name: 'fraud-check', app: 'order-service-eu', p95: 892 },
{ name: 'inventory-batch-sync', app: 'inventory-sync', p95: 756 },
{ name: 'pdf-invoice-gen', app: 'billing-service', p95: 645 },
{ name: 'email-template-render', app: 'notification-hub', p95: 412 },
{ name: 'shipment-tracking-poll', app: 'shipping-tracker', p95: 387 },
];
const maxP95 = routes[0].p95;
routes.forEach((r, i) => {
const barWidth = (r.p95 / maxP95) * 100;
list.innerHTML += `
<li>
<span class="rank">${i + 1}</span>
<div class="route-info">
<div class="route-name">${r.name}</div>
<div class="route-app">${r.app}</div>
</div>
<div class="route-bar"><div class="route-bar-fill" style="width:${barWidth}%;background:var(--amber)"></div></div>
<span class="route-metric" style="color:var(--amber)">${r.p95.toLocaleString()}ms</span>
</li>`;
});
})();
// ─── Application Health Grid ───
(function() {
const grid = document.getElementById('app-grid');
const apps = [
'order-service-eu', 'order-service-us', 'order-service-ap',
'payment-gateway', 'payment-refunds', 'payment-reconciliation',
'inventory-sync', 'inventory-warehouse-eu', 'inventory-warehouse-us',
'notification-hub', 'notification-sms', 'notification-push',
'etl-pipeline-prod', 'etl-pipeline-staging', 'etl-archive',
'shipping-tracker', 'shipping-labels', 'shipping-returns',
'customer-portal', 'customer-auth', 'customer-preferences',
'analytics-engine', 'analytics-realtime', 'analytics-batch',
'billing-service', 'billing-invoices', 'billing-subscriptions',
'fraud-detection', 'fraud-rules-engine', 'fraud-ml-scoring',
'api-gateway-eu', 'api-gateway-us', 'api-gateway-ap',
'search-service', 'search-indexer', 'search-autocomplete',
'cms-content', 'cms-media', 'cms-translations',
'monitoring-agent', 'monitoring-alerts', 'monitoring-dashboards',
'cache-service', 'cache-invalidator',
'queue-manager', 'message-router', 'dead-letter-handler',
];
apps.forEach(name => {
const routes = Math.floor(Math.random() * 40) + 5;
const failRate = Math.random() * 5;
let status = 'healthy';
if (failRate > 3) status = 'critical';
else if (failRate > 1) status = 'warning';
// Force some specific statuses for realism
if (name === 'payment-gateway') { status = 'warning'; }
if (name === 'etl-pipeline-prod') { status = 'critical'; }
grid.innerHTML += `
<div class="app-tile ${status}" title="${name}: ${routes} routes, ${failRate.toFixed(2)}% fail rate">
<div class="app-name">${name}</div>
<div class="app-stats">
<span class="app-routes">${routes} routes</span>
<span class="app-rate">${failRate.toFixed(2)}%</span>
</div>
</div>`;
});
})();
// Time range selector
document.querySelectorAll('.range-btn').forEach(btn => {
btn.addEventListener('click', function() {
document.querySelectorAll('.range-btn').forEach(b => b.classList.remove('active'));
this.classList.add('active');
});
});
</script>
</body>
</html>