Files
harmony/network_stress_test/templates/dashboard.html

383 lines
15 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Firewall Stress Test Dashboard</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'SF Mono', 'Fira Code', monospace; background: #0d1117; color: #c9d1d9; }
.header {
background: #161b22; border-bottom: 1px solid #30363d; padding: 16px 24px;
display: flex; justify-content: space-between; align-items: center;
}
.header h1 { font-size: 18px; color: #58a6ff; }
.status-badge {
padding: 4px 12px; border-radius: 12px; font-size: 12px; font-weight: 600;
text-transform: uppercase;
}
.status-active { background: #238636; color: #fff; }
.status-paused { background: #d29922; color: #000; }
.status-shutdown { background: #da3633; color: #fff; }
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; padding: 16px 24px; }
.grid-full { grid-column: 1 / -1; }
.card {
background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 16px;
}
.card h2 { font-size: 14px; color: #8b949e; margin-bottom: 12px; text-transform: uppercase; letter-spacing: 1px; }
.stat-row { display: flex; gap: 24px; flex-wrap: wrap; }
.stat { text-align: center; }
.stat .value { font-size: 28px; font-weight: 700; color: #58a6ff; }
.stat .label { font-size: 11px; color: #8b949e; margin-top: 4px; }
.progress-bar {
width: 100%; height: 6px; background: #30363d; border-radius: 3px; margin-top: 8px;
}
.progress-fill { height: 100%; background: #58a6ff; border-radius: 3px; transition: width 1s; }
.event-list { max-height: 400px; overflow-y: auto; }
.event-item {
padding: 8px 0; border-bottom: 1px solid #21262d; font-size: 13px;
display: flex; gap: 12px; align-items: center;
}
.event-time { color: #8b949e; flex-shrink: 0; width: 80px; }
.event-type {
padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 600;
flex-shrink: 0;
}
.event-type-reboot_start { background: #da363333; color: #f85149; }
.event-type-reboot_complete { background: #23863633; color: #3fb950; }
.event-type-port_down { background: #d2992233; color: #d29922; }
.event-type-port_up { background: #23863633; color: #3fb950; }
.event-type-reset_start { background: #58a6ff33; color: #58a6ff; }
.event-type-reset_complete { background: #58a6ff33; color: #58a6ff; }
.event-type-reset_settling { background: #58a6ff33; color: #58a6ff; }
.event-type-carp_mismatch { background: #da363333; color: #f85149; }
.event-type-error { background: #da363333; color: #f85149; }
.event-target { color: #c9d1d9; }
.event-details { color: #8b949e; font-size: 12px; }
.chart-container { position: relative; height: 250px; }
.fw-cards { display: flex; gap: 12px; flex-wrap: wrap; }
.fw-card {
flex: 1; min-width: 200px; background: #0d1117; border: 1px solid #30363d;
border-radius: 6px; padding: 12px;
}
.fw-card .ip { font-size: 14px; font-weight: 600; color: #c9d1d9; }
.fw-card .role { font-size: 11px; color: #8b949e; }
.fw-card .status-dot {
display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 6px;
}
.dot-up { background: #3fb950; }
.dot-down { background: #f85149; }
a.report-link {
display: inline-block; margin-top: 8px; padding: 8px 16px; background: #238636;
color: #fff; text-decoration: none; border-radius: 6px; font-size: 13px;
}
a.report-link:hover { background: #2ea043; }
@media (max-width: 768px) {
.grid { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<div class="header">
<h1>Firewall Stress Test</h1>
<div>
<span id="status-badge" class="status-badge status-active">Active</span>
<span id="elapsed" style="margin-left: 12px; color: #8b949e; font-size: 13px;">0h 0m</span>
</div>
</div>
<div class="grid">
<!-- Summary stats -->
<div class="card grid-full">
<h2>Overview</h2>
<div class="stat-row">
<div class="stat">
<div class="value" id="stat-reboots">0</div>
<div class="label">Reboots</div>
</div>
<div class="stat">
<div class="value" id="stat-flaps">0</div>
<div class="label">Port Flaps</div>
</div>
<div class="stat">
<div class="value" id="stat-resets">0</div>
<div class="label">State Resets</div>
</div>
<div class="stat">
<div class="value" id="stat-bandwidth">--</div>
<div class="label">Last Bandwidth (Mbps)</div>
</div>
<div class="stat">
<div class="value" id="stat-loss">--</div>
<div class="label">Last Loss (%)</div>
</div>
</div>
<div class="progress-bar" style="margin-top: 16px;">
<div class="progress-fill" id="progress-fill" style="width: 0%;"></div>
</div>
</div>
<!-- Bandwidth chart -->
<div class="card grid-full">
<h2>Bandwidth Over Time</h2>
<div class="chart-container">
<canvas id="bandwidth-chart"></canvas>
</div>
</div>
<!-- Firewall status -->
<div class="card">
<h2>Firewalls</h2>
<div class="fw-cards" id="firewall-cards">
<p style="color: #8b949e;">Loading...</p>
</div>
</div>
<!-- Event timeline -->
<div class="card">
<h2>Event Timeline</h2>
<div class="event-list" id="event-list">
<p style="color: #8b949e;">Waiting for events...</p>
</div>
</div>
<!-- Report -->
<div class="card grid-full">
<h2>Report</h2>
<p style="color: #8b949e; font-size: 13px;">Generate a full HTML report of the stress test results.</p>
<a href="/api/report" target="_blank" class="report-link">Generate Report</a>
</div>
</div>
<script>
// Bandwidth chart
const ctx = document.getElementById('bandwidth-chart').getContext('2d');
const bwChart = new Chart(ctx, {
type: 'line',
data: {
datasets: [{
label: 'Bandwidth (Mbps)',
data: [],
borderColor: '#58a6ff',
backgroundColor: '#58a6ff22',
fill: true,
tension: 0.3,
pointRadius: 2,
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
type: 'time',
time: { unit: 'minute', displayFormats: { minute: 'HH:mm' } },
grid: { color: '#21262d' },
ticks: { color: '#8b949e' },
},
y: {
grid: { color: '#21262d' },
ticks: { color: '#8b949e' },
title: { display: true, text: 'Mbps', color: '#8b949e' },
}
},
plugins: {
legend: { display: false },
},
animation: { duration: 300 },
}
});
let eventListCleared = false;
let firewallReboots = {};
function formatElapsed(secs) {
const h = Math.floor(secs / 3600);
const m = Math.floor((secs % 3600) / 60);
return `${h}h ${m}m`;
}
function formatTime(ts) {
const d = new Date(ts);
return d.toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' });
}
function addEvent(event) {
const list = document.getElementById('event-list');
if (!eventListCleared) {
list.innerHTML = '';
eventListCleared = true;
}
const item = document.createElement('div');
item.className = 'event-item';
const time = document.createElement('span');
time.className = 'event-time';
time.textContent = formatTime(event.timestamp);
const type_ = document.createElement('span');
type_.className = `event-type event-type-${event.event_type}`;
type_.textContent = event.event_type.replace(/_/g, ' ');
const target = document.createElement('span');
target.className = 'event-target';
target.textContent = event.target;
item.appendChild(time);
item.appendChild(type_);
item.appendChild(target);
if (event.details) {
const details = document.createElement('span');
details.className = 'event-details';
try {
const d = JSON.parse(event.details);
details.textContent = Object.entries(d).map(([k,v]) => `${k}=${v}`).join(', ');
} catch { details.textContent = event.details; }
item.appendChild(details);
}
list.insertBefore(item, list.firstChild);
// Keep max 200 events in DOM
while (list.children.length > 200) {
list.removeChild(list.lastChild);
}
}
// Track last-seen IDs to avoid duplicates
let lastEventId = 0;
let lastMetricId = 0;
let knownBwTimestamps = new Set();
// Poll status every 3s
async function pollStatus() {
try {
const resp = await fetch('/api/status');
const status = await resp.json();
const badge = document.getElementById('status-badge');
badge.textContent = status.state;
badge.className = `status-badge status-${status.state}`;
document.getElementById('elapsed').textContent = formatElapsed(status.elapsed_secs);
const pct = Math.min(100, (status.elapsed_secs / status.total_duration_secs) * 100);
document.getElementById('progress-fill').style.width = pct + '%';
document.getElementById('stat-reboots').textContent = status.total_reboots;
document.getElementById('stat-flaps').textContent = status.total_port_flaps;
document.getElementById('stat-resets').textContent = status.total_resets;
const container = document.getElementById('firewall-cards');
const allFws = [...status.firewalls_up, ...status.firewalls_down];
if (allFws.length > 0) {
container.innerHTML = '';
for (const ip of status.firewalls_up) {
container.innerHTML += `<div class="fw-card"><span class="status-dot dot-up"></span><span class="ip">${ip}</span><div class="role">UP</div></div>`;
}
for (const ip of status.firewalls_down) {
container.innerHTML += `<div class="fw-card"><span class="status-dot dot-down"></span><span class="ip">${ip}</span><div class="role">DOWN</div></div>`;
}
}
} catch {}
}
// Poll events every 3s
async function pollEvents() {
try {
const resp = await fetch('/api/events?limit=20');
const events = await resp.json();
// Events come newest-first; process oldest-first for correct ordering
const newEvents = events.filter(e => e.id > lastEventId).reverse();
newEvents.forEach(e => {
addEvent(e);
lastEventId = Math.max(lastEventId, e.id);
});
} catch {}
}
// Poll metrics every 3s
async function pollMetrics() {
try {
const resp = await fetch('/api/metrics?limit=20');
const metrics = await resp.json();
const newMetrics = metrics.filter(m => m.id > lastMetricId);
newMetrics.forEach(m => {
lastMetricId = Math.max(lastMetricId, m.id);
if (m.metric_type === 'iperf_bandwidth') {
document.getElementById('stat-bandwidth').textContent = m.value.toFixed(1);
const ts = m.timestamp;
if (!knownBwTimestamps.has(ts)) {
knownBwTimestamps.add(ts);
bwChart.data.datasets[0].data.push({ x: new Date(ts), y: m.value });
if (bwChart.data.datasets[0].data.length > 500) {
bwChart.data.datasets[0].data.shift();
}
}
}
if (m.metric_type === 'iperf_loss') {
document.getElementById('stat-loss').textContent = m.value.toFixed(2);
}
});
if (newMetrics.some(m => m.metric_type === 'iperf_bandwidth')) {
bwChart.update('quiet');
}
} catch {}
}
// Load initial data
async function loadInitialData() {
try {
const resp = await fetch('/api/events?limit=50');
const events = await resp.json();
events.reverse().forEach(e => {
addEvent(e);
lastEventId = Math.max(lastEventId, e.id);
});
} catch {}
try {
const resp = await fetch('/api/metrics?metric_type=iperf_bandwidth&limit=200');
const metrics = await resp.json();
metrics.reverse().forEach(m => {
lastMetricId = Math.max(lastMetricId, m.id);
knownBwTimestamps.add(m.timestamp);
bwChart.data.datasets[0].data.push({ x: new Date(m.timestamp), y: m.value });
});
bwChart.update();
} catch {}
// Also get latest loss value
try {
const resp = await fetch('/api/metrics?metric_type=iperf_loss&limit=1');
const metrics = await resp.json();
if (metrics.length > 0) {
document.getElementById('stat-loss').textContent = metrics[0].value.toFixed(2);
lastMetricId = Math.max(lastMetricId, metrics[0].id);
}
} catch {}
}
loadInitialData();
pollStatus();
setInterval(pollStatus, 3000);
setInterval(pollEvents, 3000);
setInterval(pollMetrics, 3000);
</script>
</body>
</html>