diff --git a/dashboard/src/App.jsx b/dashboard/src/App.jsx
index 1ce1e2c..c0aa07b 100644
--- a/dashboard/src/App.jsx
+++ b/dashboard/src/App.jsx
@@ -1,5 +1,4 @@
import { useState, useMemo } from 'react'
-import { useQueryClient } from '@tanstack/react-query'
import { Settings } from 'lucide-react'
import { useStats } from './hooks/useStats'
import { useTests } from './hooks/useTests'
@@ -11,8 +10,19 @@ import FilterPanel from './components/FilterPanel'
import TestTable from './components/TestTable'
import ConfigModal from './components/ConfigModal'
+function hasCoePairs(value) {
+ if (!value) return false
+ if (Array.isArray(value)) return value.length > 0
+ if (typeof value !== 'string') return false
+ try {
+ const parsed = JSON.parse(value)
+ return Array.isArray(parsed) && parsed.length > 0
+ } catch {
+ return false
+ }
+}
+
export default function App() {
- const queryClient = useQueryClient()
const [showConfig, setShowConfig] = useState(false)
const [filters, setFilters] = useState({})
@@ -32,6 +42,11 @@ export default function App() {
for (const f of strFields) {
if (filters[f] && t[f] !== filters[f]) return false
}
+ if (filters.coePair === 'yes' || filters.coePair === 'no') {
+ const paired = hasCoePairs(t.coe_pair)
+ if (filters.coePair === 'yes' && !paired) return false
+ if (filters.coePair === 'no' && paired) return false
+ }
return true
})
}, [allTests, filters])
@@ -125,7 +140,7 @@ export default function App() {
Showing {filteredTests.length} of {allTests.length} tests
-
|
+ {
+ const pairs = parsePairArray(test.coe_pair)
+ return pairs.length > 0 ? String(pairs.length) : null
+ })()} colorClass="bg-cyan-900/50 text-cyan-300" />
@@ -157,7 +211,95 @@ export default function TestTable({ tests = [], isLoading }) {
{(() => {
- const rows = test.tput_results ? JSON.parse(test.tput_results) : []
+ const pairs = parsePairArray(test.coe_pair)
+ if (pairs.length === 0) return null
+ return (
+
+ Paired COE Tests
+
+ {pairs.map(pairKey => {
+ const pairTest = testsByLookupKey.get(pairKey)
+ const itemKey = `${test.id}-${pairKey}`
+ const open = pairedExpanded.has(`${test.id}::${pairKey}`)
+
+ if (!pairTest) {
+ return (
+
+ {pairKey}
+
+ )
+ }
+
+ return (
+
+
+
+ {open && (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {(() => {
+ const rows = parseTputRows(pairTest.tput_results)
+ if (!pairTest.completed || rows.length === 0) return null
+ return (
+
+
+
+
+ | Station |
+ Throughput |
+ DL RSSI |
+ UL RSSI |
+
+
+
+ {rows.map(r => (
+
+ | STA{r.station} |
+ {r.tput} Mbps |
+ {r.dlRssi} dBm |
+ {r.ulRssi} dBm |
+
+ ))}
+
+
+
+ )
+ })()}
+
+ )}
+
+ )
+ })}
+
+
+ )
+ })()}
+ {(() => {
+ const rows = parseTputRows(test.tput_results)
if (!test.completed || rows.length === 0) return null
return (
diff --git a/server/dashboard.db b/server/dashboard.db
index ed90e1c..f95366c 100644
Binary files a/server/dashboard.db and b/server/dashboard.db differ
diff --git a/server/dashboard.db-shm b/server/dashboard.db-shm
index db8bdf8..59c5e99 100644
Binary files a/server/dashboard.db-shm and b/server/dashboard.db-shm differ
diff --git a/server/dashboard.db-wal b/server/dashboard.db-wal
index cea5806..c442712 100644
Binary files a/server/dashboard.db-wal and b/server/dashboard.db-wal differ
diff --git a/server/db_py.py b/server/db_py.py
index ae8a927..bd3d7b5 100644
--- a/server/db_py.py
+++ b/server/db_py.py
@@ -65,6 +65,7 @@ def _init_db():
"ul_rssi_dbm REAL",
"tput_results TEXT",
"throttled TEXT",
+ "coe_pair TEXT",
]:
try:
_conn.execute(f"ALTER TABLE tests ADD COLUMN {col_def}")
@@ -138,6 +139,19 @@ def mark_completed(test_id, device, completed_at, duration_seconds, tput_results
)
+def set_coe_pair(test_row_id, coe_pair):
+ json_value = json.dumps(coe_pair or [])
+ with _tx():
+ _conn.execute(
+ """
+ UPDATE tests
+ SET coe_pair = ?
+ WHERE id = ?
+ """,
+ (json_value, test_row_id),
+ )
+
+
def get_station_for_test(test_id, device):
with _lock:
row = _conn.execute(
diff --git a/server/scanner.py b/server/scanner.py
index fe5a6b9..76304ba 100644
--- a/server/scanner.py
+++ b/server/scanner.py
@@ -1,7 +1,7 @@
import os
import re
-from db_py import clear_tests, mark_completed, upsert_test
+from db_py import clear_tests, get_all_tests, mark_completed, set_coe_pair, upsert_test
from parser import (
parse_elapsed_time,
parse_result_filename,
@@ -21,6 +21,48 @@ def full_scan(target_dir, results_dir):
scan_targets(target_dir)
scan_results(results_dir)
+ update_p2p_coe_pairs()
+
+
+def update_p2p_coe_pairs():
+ pair_fields = [
+ "device",
+ "rotation",
+ "test_point",
+ "rssi",
+ "station",
+ "band",
+ "channel",
+ "bandwidth",
+ "direction",
+ ]
+
+ tests = get_all_tests()
+ coe_by_key = {}
+
+ for test in tests:
+ if test.get("interference") != "COE":
+ continue
+
+ key = tuple(test.get(field) for field in pair_fields)
+ device = test.get("device")
+ test_id = test.get("test_id")
+ if not device or not test_id:
+ continue
+
+ coe_by_key.setdefault(key, []).append(f"{device}_{test_id}")
+
+ updated = 0
+ for test in tests:
+ if test.get("interference") != "P2P":
+ continue
+
+ key = tuple(test.get(field) for field in pair_fields)
+ pairs = sorted(set(coe_by_key.get(key, [])))
+ set_coe_pair(test.get("id"), pairs)
+ updated += 1
+
+ print(f"[scanner] coe_pair updated for {updated} P2P test(s)")
def scan_targets(target_dir):
|