2026-03-13 10:52:43 +01:00
|
|
|
<!DOCTYPE html>
|
|
|
|
|
<html lang="en">
|
|
|
|
|
<head>
|
|
|
|
|
<meta charset="UTF-8">
|
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
2026-04-15 15:28:42 +02:00
|
|
|
<title>Cameleer — Dashboard</title>
|
2026-03-13 10:52:43 +01:00
|
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
|
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
|
|
|
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700&family=JetBrains+Mono:wght@300;400;500;600&display=swap" rel="stylesheet">
|
|
|
|
|
<style>
|
|
|
|
|
:root {
|
|
|
|
|
--bg-deep: #060a13;
|
|
|
|
|
--bg-base: #0a0e17;
|
|
|
|
|
--bg-surface: #111827;
|
|
|
|
|
--bg-raised: #1a2332;
|
|
|
|
|
--bg-hover: #1e2d3d;
|
|
|
|
|
--border: #1e2d3d;
|
|
|
|
|
--border-subtle: #152030;
|
|
|
|
|
--text-primary: #e2e8f0;
|
|
|
|
|
--text-secondary: #8b9cb6;
|
|
|
|
|
--text-muted: #4a5e7a;
|
|
|
|
|
--amber: #f0b429;
|
|
|
|
|
--amber-dim: #b8860b;
|
|
|
|
|
--amber-glow: rgba(240, 180, 41, 0.15);
|
|
|
|
|
--cyan: #22d3ee;
|
|
|
|
|
--cyan-dim: #0e7490;
|
|
|
|
|
--cyan-glow: rgba(34, 211, 238, 0.12);
|
|
|
|
|
--rose: #f43f5e;
|
|
|
|
|
--rose-dim: #9f1239;
|
|
|
|
|
--rose-glow: rgba(244, 63, 94, 0.12);
|
|
|
|
|
--blue: #3b82f6;
|
|
|
|
|
--green: #10b981;
|
|
|
|
|
--green-glow: rgba(16, 185, 129, 0.12);
|
|
|
|
|
--purple: #a855f7;
|
|
|
|
|
--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(240, 180, 41, 0.03), transparent),
|
|
|
|
|
radial-gradient(ellipse 600px 600px at 80% 80%, rgba(34, 211, 238, 0.02), 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='%23f0b429' stroke-width='1'/%3E%3Cpath d='M0 220 Q120 170 200 220 T400 220' fill='none' stroke='%23f0b429' stroke-width='0.5'/%3E%3Cpath d='M0 180 Q80 130 200 180 T400 180' fill='none' stroke='%23f0b429' stroke-width='0.5'/%3E%3Cpath d='M0 100 Q150 60 200 100 T400 100' fill='none' stroke='%2322d3ee' stroke-width='0.5'/%3E%3Cpath d='M0 300 Q100 260 200 300 T400 300' fill='none' stroke='%2322d3ee' 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(6, 10, 19, 0.85);
|
|
|
|
|
backdrop-filter: blur(20px) saturate(1.2);
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.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(16, 185, 129, 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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.hero-stat:hover { border-color: var(--border); }
|
|
|
|
|
|
|
|
|
|
.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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.card:hover { border-color: var(--border); }
|
|
|
|
|
|
|
|
|
|
.card-header {
|
|
|
|
|
padding: 16px 20px 12px;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.card-title {
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: var(--text-primary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.card-subtitle {
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
color: var(--text-muted);
|
|
|
|
|
font-family: var(--font-mono);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.card-body {
|
|
|
|
|
padding: 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-raised);
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.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-raised);
|
|
|
|
|
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-hover);
|
|
|
|
|
transform: translateY(-1px);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.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(16, 185, 129, 0.4); }
|
|
|
|
|
50% { box-shadow: 0 0 0 6px rgba(16, 185, 129, 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>
|
2026-04-15 15:28:42 +02:00
|
|
|
cameleer
|
2026-03-13 10:52:43 +01:00
|
|
|
</div>
|
|
|
|
|
<ul class="nav-links">
|
|
|
|
|
<li><a href="dashboard.html" class="active">Dashboard</a></li>
|
|
|
|
|
<li><a href="transaction-explorer.html">Transactions</a></li>
|
|
|
|
|
<li><a href="route-detail.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(240,180,41,0.05)"></div>
|
|
|
|
|
<div class="scale-cell" style="background:rgba(240,180,41,0.15)"></div>
|
|
|
|
|
<div class="scale-cell" style="background:rgba(240,180,41,0.3)"></div>
|
|
|
|
|
<div class="scale-cell" style="background:rgba(240,180,41,0.5)"></div>
|
|
|
|
|
<div class="scale-cell" style="background:rgba(240,180,41,0.8)"></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), '#f0b429');
|
|
|
|
|
sparkline('sparkline-success', randomSeries(24, 99.6, 0.2), '#10b981');
|
|
|
|
|
sparkline('sparkline-failed', randomSeries(24, 340, 80), '#f43f5e');
|
|
|
|
|
sparkline('sparkline-latency', randomSeries(24, 47, 12), '#22d3ee');
|
|
|
|
|
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], '#a855f7');
|
|
|
|
|
sparkline('sparkline-inflight', randomSeries(24, 1200, 400), '#3b82f6');
|
|
|
|
|
|
|
|
|
|
// ─── 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.15"/>
|
|
|
|
|
<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: '#f43f5e', fill: true },
|
|
|
|
|
{ label: 'threshold', data: Array(96).fill(0.5), color: '#f0b429', 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: '#f43f5e', fill: true },
|
|
|
|
|
{ label: 'p95', data: p95, color: '#f0b429', fill: true },
|
|
|
|
|
{ label: 'p50', data: p50, color: '#10b981', 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(240, 180, 41, ${intensity * 0.85})`;
|
|
|
|
|
|
|
|
|
|
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>
|