@@ -50,7 +72,7 @@ import { computed } from 'vue'
const props = defineProps({ tests: Array })
const completedTests = computed(() =>
- (props.tests || []).filter(t => t.state === 'complete' && t.results)
+ (props.tests || []).filter(t => (t.state === 'complete' || t.state === 'error') && (t.results || t.error))
)
function resultColumns(t) {
@@ -63,22 +85,28 @@ function resultColumns(t) {
function formatVal(val, col) {
if (typeof val === 'object') {
- if (col.includes('Rate')) return val.max_rate_pps ?? '-'
- if (col.includes('Throughput')) return val.throughput_mbps ?? '-'
- if (col.includes('Min')) return val.min_ms ?? '-'
- if (col.includes('Avg')) return val.avg_ms ?? '-'
- if (col.includes('Max') && col.includes('ms')) return val.max_ms ?? '-'
- if (col.includes('Jitter')) return val.jitter_ms ?? '-'
+ if (col.includes('Rate')) return val.max_throughput_pps ?? val.max_rate_pps ?? '-'
+ if (col.includes('Throughput')) {
+ const pps = val.max_throughput_pps ?? val.max_rate_pps ?? 0
+ const fs = val.frame_size ?? 64
+ return pps ? ((pps * fs * 8) / 1_000_000).toFixed(2) : '-'
+ }
+ if (col.includes('Min')) return val.min_ms != null ? val.min_ms.toFixed(2) : '-'
+ if (col.includes('Avg')) return val.avg_ms != null ? val.avg_ms.toFixed(2) : '-'
+ if (col.includes('Max') && col.includes('ms')) return val.max_ms != null ? val.max_ms.toFixed(2) : '-'
+ if (col.includes('Jitter')) return val.jitter_ms != null ? val.jitter_ms.toFixed(2) : '-'
if (col.includes('Loss')) return val.loss_pct ?? '-'
- if (col.includes('Burst')) return val.max_burst ?? '-'
+ if (col.includes('Burst')) return val.max_burst_frames ?? val.max_burst ?? '-'
return JSON.stringify(val)
}
return val
}
function barHeight(t, val) {
- const v = typeof val === 'object' ? (val.max_rate_pps || val.avg_ms || val.loss_pct || val.max_burst || 0) : val
- return Math.min(100, Math.max(5, v / 100))
+ const v = typeof val === 'object' ? (val.max_throughput_pps || val.max_rate_pps || val.avg_ms || val.loss_pct || val.max_burst_frames || 0) : val
+ const allVals = Object.values(t.results).map(r => typeof r === 'object' ? (r.max_throughput_pps || r.max_rate_pps || r.avg_ms || r.loss_pct || r.max_burst_frames || 0) : r)
+ const maxVal = Math.max(...allVals, 1)
+ return Math.min(100, Math.max(5, (v / maxVal) * 100))
}
function exportJSON(t) {
@@ -123,4 +151,7 @@ td { font-size: 13px; padding: 4px 8px; }
.bar-item { flex: 1; display: flex; flex-direction: column; align-items: center; height: 100%; }
.bar-fill { width: 100%; background: var(--accent); border-radius: 3px 3px 0 0; min-height: 4px; transition: height 0.3s; margin-top: auto; }
.bar-label { font-size: 10px; color: var(--muted); margin-top: 4px; }
+.fl-section { margin-bottom: 12px; }
+.fl-title { font-size: 12px; font-weight: 600; color: var(--accent); margin-bottom: 4px; }
+.error-msg { color: var(--danger); font-size: 13px; padding: 8px 0; }
diff --git a/traffic-gen-ui/src/components/StatsMonitor.vue b/traffic-gen-ui/src/components/StatsMonitor.vue
index 52ba339..58f7e62 100644
--- a/traffic-gen-ui/src/components/StatsMonitor.vue
+++ b/traffic-gen-ui/src/components/StatsMonitor.vue
@@ -78,12 +78,79 @@ let timer = null
async function fetchStats() {
try {
if (selectedFlow.value) {
+ // Single flow: /flows/
/stats returns {flow_id, counters, rates}
+ // rates contains: tx_pps, rx_pps, tx_mbps, rx_mbps, loss_pct, tx_packets, tx_bytes, etc.
const s = await api.flowStats(selectedFlow.value)
- current.value = s
+ const rates = s.rates || {}
+ const counters = s.counters || {}
+ current.value = {
+ tx_pps: Math.round(rates.tx_pps || 0),
+ rx_pps: Math.round(rates.rx_pps || 0),
+ tx_mbps: rates.tx_mbps || 0,
+ rx_mbps: rates.rx_mbps || 0,
+ loss_pct: rates.loss_pct || 0,
+ avg_latency_ms: rates.latency ? rates.latency.avg_ms : null,
+ tx_packets: counters.tx_packets || 0,
+ tx_bytes: counters.tx_bytes || 0,
+ rx_packets: counters.rx_packets || 0,
+ rx_bytes: counters.rx_bytes || 0,
+ }
+ // Append to history for sparkline
+ history.value.push({ tx_pps: current.value.tx_pps, rx_pps: current.value.rx_pps })
+ if (history.value.length > 60) history.value = history.value.slice(-60)
} else {
+ // All flows: /stats/history returns {history: {flow_id: [samples]}}
const h = await api.statsHistory()
- if (h.current) current.value = h.current
- if (h.history) history.value = h.history.slice(-60)
+ const allHistory = h.history || {}
+ // Aggregate latest sample across all flows
+ let txPps = 0, rxPps = 0, txMbps = 0, rxMbps = 0
+ let txPkts = 0, txBytes = 0, rxPkts = 0, rxBytes = 0
+ let lossPcts = [], latencies = []
+
+ for (const [, samples] of Object.entries(allHistory)) {
+ if (!samples.length) continue
+ const latest = samples[samples.length - 1]
+ txPps += latest.tx_pps || 0
+ rxPps += latest.rx_pps || 0
+ txMbps += latest.tx_mbps || 0
+ rxMbps += latest.rx_mbps || 0
+ txPkts += latest.tx_packets || 0
+ txBytes += latest.tx_bytes || 0
+ rxPkts += latest.rx_packets || 0
+ rxBytes += latest.rx_bytes || 0
+ if (latest.loss_pct > 0) lossPcts.push(latest.loss_pct)
+ if (latest.latency && latest.latency.avg_ms) latencies.push(latest.latency.avg_ms)
+ }
+
+ current.value = {
+ tx_pps: Math.round(txPps),
+ rx_pps: Math.round(rxPps),
+ tx_mbps: txMbps,
+ rx_mbps: rxMbps,
+ loss_pct: txPkts > 0 ? Math.max(0, ((txPkts - rxPkts) / txPkts) * 100) : 0,
+ avg_latency_ms: latencies.length ? latencies.reduce((a, b) => a + b, 0) / latencies.length : null,
+ tx_packets: txPkts,
+ tx_bytes: txBytes,
+ rx_packets: rxPkts,
+ rx_bytes: rxBytes,
+ }
+
+ // Build aggregated sparkline from history samples
+ // Find max sample count across all flows
+ const flowIds = Object.keys(allHistory)
+ if (flowIds.length) {
+ const maxLen = Math.max(...flowIds.map(id => allHistory[id].length))
+ const sparkData = []
+ for (let i = Math.max(0, maxLen - 60); i < maxLen; i++) {
+ let sTx = 0, sRx = 0
+ for (const fid of flowIds) {
+ const s = allHistory[fid][i]
+ if (s) { sTx += s.tx_pps || 0; sRx += s.rx_pps || 0 }
+ }
+ sparkData.push({ tx_pps: Math.round(sTx), rx_pps: Math.round(sRx) })
+ }
+ history.value = sparkData
+ }
}
} catch (_) {}
}
diff --git a/traffic-gen-ui/src/components/StatusBar.vue b/traffic-gen-ui/src/components/StatusBar.vue
index b4857e4..c4614c8 100644
--- a/traffic-gen-ui/src/components/StatusBar.vue
+++ b/traffic-gen-ui/src/components/StatusBar.vue
@@ -2,25 +2,45 @@
- {{ connected ? 'Connected' : 'Offline' }}
+ {{ connected ? 'API Connected' : 'API Offline' }}
+
+
+ {{ (health.mode || 'sender').toUpperCase() }}
- Mode: {{ health.mode || 'sender' }}
+ Active Flows: {{ health.active_flows || 0 }}
- Flows: {{ health.active_flows || 0 }}
-
-
- Tests: {{ health.active_tests || 0 }}
+ Active Tests: {{ health.active_tests || 0 }}
diff --git a/traffic-gen-ui/src/components/TestBuilder.vue b/traffic-gen-ui/src/components/TestBuilder.vue
index 8db2f6b..536b1ec 100644
--- a/traffic-gen-ui/src/components/TestBuilder.vue
+++ b/traffic-gen-ui/src/components/TestBuilder.vue
@@ -13,13 +13,23 @@