coe pair filter
This commit is contained in:
+18
-3
@@ -1,5 +1,4 @@
|
|||||||
import { useState, useMemo } from 'react'
|
import { useState, useMemo } from 'react'
|
||||||
import { useQueryClient } from '@tanstack/react-query'
|
|
||||||
import { Settings } from 'lucide-react'
|
import { Settings } from 'lucide-react'
|
||||||
import { useStats } from './hooks/useStats'
|
import { useStats } from './hooks/useStats'
|
||||||
import { useTests } from './hooks/useTests'
|
import { useTests } from './hooks/useTests'
|
||||||
@@ -11,8 +10,19 @@ import FilterPanel from './components/FilterPanel'
|
|||||||
import TestTable from './components/TestTable'
|
import TestTable from './components/TestTable'
|
||||||
import ConfigModal from './components/ConfigModal'
|
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() {
|
export default function App() {
|
||||||
const queryClient = useQueryClient()
|
|
||||||
const [showConfig, setShowConfig] = useState(false)
|
const [showConfig, setShowConfig] = useState(false)
|
||||||
const [filters, setFilters] = useState({})
|
const [filters, setFilters] = useState({})
|
||||||
|
|
||||||
@@ -32,6 +42,11 @@ export default function App() {
|
|||||||
for (const f of strFields) {
|
for (const f of strFields) {
|
||||||
if (filters[f] && t[f] !== filters[f]) return false
|
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
|
return true
|
||||||
})
|
})
|
||||||
}, [allTests, filters])
|
}, [allTests, filters])
|
||||||
@@ -125,7 +140,7 @@ export default function App() {
|
|||||||
<p className="text-slate-500 text-xs">
|
<p className="text-slate-500 text-xs">
|
||||||
Showing {filteredTests.length} of {allTests.length} tests
|
Showing {filteredTests.length} of {allTests.length} tests
|
||||||
</p>
|
</p>
|
||||||
<TestTable tests={filteredTests} isLoading={testsLoading} />
|
<TestTable tests={filteredTests} allTests={allTests} isLoading={testsLoading} />
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
const FILTER_FIELDS = [
|
const FILTER_FIELDS = [
|
||||||
{ key: 'completed', label: 'Status', options: [{ value: '', label: 'All' }, { value: 'true', label: 'Completed' }, { value: 'false', label: 'Pending' }] },
|
{ key: 'completed', label: 'Status', options: [{ value: '', label: 'All' }, { value: 'true', label: 'Completed' }, { value: 'false', label: 'Pending' }] },
|
||||||
{ key: 'interference', label: 'Type', options: [{ value: '', label: 'All' }, { value: 'COE', label: 'COE' }, { value: 'P2P', label: 'P2P' }, { value: 'P3P', label: 'P3P' }] },
|
{ key: 'interference', label: 'Type', options: [{ value: '', label: 'All' }, { value: 'COE', label: 'COE' }, { value: 'P2P', label: 'P2P' }, { value: 'P3P', label: 'P3P' }] },
|
||||||
|
{ key: 'coePair', label: 'COE Pairs', options: [{ value: '', label: 'All' }, { value: 'yes', label: 'Yes' }, { value: 'no', label: 'No' }] },
|
||||||
]
|
]
|
||||||
|
|
||||||
const DERIVED_FIELDS = [
|
const DERIVED_FIELDS = [
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState } from 'react'
|
import React, { useMemo, useState } from 'react'
|
||||||
import StatusBadge from './StatusBadge'
|
import StatusBadge from './StatusBadge'
|
||||||
|
|
||||||
const TYPE_COLORS = {
|
const TYPE_COLORS = {
|
||||||
@@ -7,6 +7,35 @@ const TYPE_COLORS = {
|
|||||||
P3P: 'bg-orange-900/50 text-orange-300',
|
P3P: 'bg-orange-900/50 text-orange-300',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parsePairArray(raw) {
|
||||||
|
if (!raw) return []
|
||||||
|
if (Array.isArray(raw)) return raw
|
||||||
|
if (typeof raw !== 'string') return []
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(raw)
|
||||||
|
return Array.isArray(parsed) ? parsed : []
|
||||||
|
} catch {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseTputRows(raw) {
|
||||||
|
if (!raw) return []
|
||||||
|
if (Array.isArray(raw)) return raw
|
||||||
|
if (typeof raw !== 'string') return []
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(raw)
|
||||||
|
return Array.isArray(parsed) ? parsed : []
|
||||||
|
} catch {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function testLookupKey(test) {
|
||||||
|
if (!test?.device || !test?.test_id) return null
|
||||||
|
return `${test.device}_${test.test_id}`
|
||||||
|
}
|
||||||
|
|
||||||
function fmtDuration(s) {
|
function fmtDuration(s) {
|
||||||
if (s == null) return null
|
if (s == null) return null
|
||||||
const h = Math.floor(s / 3600)
|
const h = Math.floor(s / 3600)
|
||||||
@@ -41,9 +70,21 @@ function SortTh({ colKey, label, sort, onSort }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TestTable({ tests = [], isLoading }) {
|
export default function TestTable({ tests = [], allTests = [], isLoading }) {
|
||||||
const [sort, setSort] = useState({ key: 'filename', dir: 1 })
|
const [sort, setSort] = useState({ key: 'filename', dir: 1 })
|
||||||
const [expanded, setExpanded] = useState(new Set())
|
const [expanded, setExpanded] = useState(new Set())
|
||||||
|
const [pairedExpanded, setPairedExpanded] = useState(new Set())
|
||||||
|
|
||||||
|
const sourceTests = allTests.length > 0 ? allTests : tests
|
||||||
|
|
||||||
|
const testsByLookupKey = useMemo(() => {
|
||||||
|
const map = new Map()
|
||||||
|
for (const t of sourceTests) {
|
||||||
|
const key = testLookupKey(t)
|
||||||
|
if (key) map.set(key, t)
|
||||||
|
}
|
||||||
|
return map
|
||||||
|
}, [sourceTests])
|
||||||
|
|
||||||
function toggleSort(key) {
|
function toggleSort(key) {
|
||||||
setSort(s => ({ key, dir: s.key === key ? -s.dir : 1 }))
|
setSort(s => ({ key, dir: s.key === key ? -s.dir : 1 }))
|
||||||
@@ -57,6 +98,15 @@ export default function TestTable({ tests = [], isLoading }) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function togglePairedExpand(parentId, pairKey) {
|
||||||
|
const key = `${parentId}::${pairKey}`
|
||||||
|
setPairedExpanded(prev => {
|
||||||
|
const next = new Set(prev)
|
||||||
|
next.has(key) ? next.delete(key) : next.add(key)
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const sorted = [...tests].sort((a, b) => {
|
const sorted = [...tests].sort((a, b) => {
|
||||||
let av = a[sort.key] ?? ''
|
let av = a[sort.key] ?? ''
|
||||||
let bv = b[sort.key] ?? ''
|
let bv = b[sort.key] ?? ''
|
||||||
@@ -142,6 +192,10 @@ export default function TestTable({ tests = [], isLoading }) {
|
|||||||
<tr className={`border-t border-slate-700/60 ${test.completed ? 'bg-emerald-950/10' : 'bg-slate-900/80'}`}>
|
<tr className={`border-t border-slate-700/60 ${test.completed ? 'bg-emerald-950/10' : 'bg-slate-900/80'}`}>
|
||||||
<td colSpan={3} className="px-6 py-4">
|
<td colSpan={3} className="px-6 py-4">
|
||||||
<div className="flex flex-wrap gap-3">
|
<div className="flex flex-wrap gap-3">
|
||||||
|
<TagPill label="COE Pairs" value={(() => {
|
||||||
|
const pairs = parsePairArray(test.coe_pair)
|
||||||
|
return pairs.length > 0 ? String(pairs.length) : null
|
||||||
|
})()} colorClass="bg-cyan-900/50 text-cyan-300" />
|
||||||
<TagPill label="File ID" value={test.test_id} />
|
<TagPill label="File ID" value={test.test_id} />
|
||||||
<TagPill label="Type" value={test.interference} colorClass={TYPE_COLORS[test.interference]} />
|
<TagPill label="Type" value={test.interference} colorClass={TYPE_COLORS[test.interference]} />
|
||||||
<TagPill label="Device" value={test.device} />
|
<TagPill label="Device" value={test.device} />
|
||||||
@@ -157,7 +211,95 @@ export default function TestTable({ tests = [], isLoading }) {
|
|||||||
<TagPill label="Elapsed Time" value={fmtDuration(test.duration_seconds)} />
|
<TagPill label="Elapsed Time" value={fmtDuration(test.duration_seconds)} />
|
||||||
</div>
|
</div>
|
||||||
{(() => {
|
{(() => {
|
||||||
const rows = test.tput_results ? JSON.parse(test.tput_results) : []
|
const pairs = parsePairArray(test.coe_pair)
|
||||||
|
if (pairs.length === 0) return null
|
||||||
|
return (
|
||||||
|
<div className="mt-3 pt-3 border-t border-slate-700/50">
|
||||||
|
<p className="text-[10px] uppercase tracking-wide text-slate-500 mb-2">Paired COE Tests</p>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
{pairs.map(pairKey => {
|
||||||
|
const pairTest = testsByLookupKey.get(pairKey)
|
||||||
|
const itemKey = `${test.id}-${pairKey}`
|
||||||
|
const open = pairedExpanded.has(`${test.id}::${pairKey}`)
|
||||||
|
|
||||||
|
if (!pairTest) {
|
||||||
|
return (
|
||||||
|
<div key={itemKey} className="text-xs font-medium px-2.5 py-2 rounded bg-slate-800 text-slate-400">
|
||||||
|
{pairKey}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={itemKey} className="border border-slate-700/70 rounded-md overflow-hidden">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => togglePairedExpand(test.id, pairKey)}
|
||||||
|
className="w-full px-2.5 py-2 bg-slate-900/70 hover:bg-slate-800 transition-colors flex items-center justify-between gap-2 text-left"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 min-w-0">
|
||||||
|
<span className="text-xs text-slate-500 select-none">{open ? '▾' : '▸'}</span>
|
||||||
|
<span className="text-xs text-slate-200 truncate">{pairTest.filename ?? pairKey}</span>
|
||||||
|
</div>
|
||||||
|
<StatusBadge completed={pairTest.completed} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{open && (
|
||||||
|
<div className="px-3 py-3 border-t border-slate-700/60 bg-slate-900/40">
|
||||||
|
<div className="flex flex-wrap gap-3">
|
||||||
|
<TagPill label="File ID" value={pairTest.test_id} />
|
||||||
|
<TagPill label="Type" value={pairTest.interference} colorClass={TYPE_COLORS[pairTest.interference]} />
|
||||||
|
<TagPill label="Device" value={pairTest.device} />
|
||||||
|
<TagPill label="Rotation" value={pairTest.rotation} />
|
||||||
|
<TagPill label="Test Point" value={pairTest.test_point} />
|
||||||
|
<TagPill label="RSSI" value={pairTest.rssi} />
|
||||||
|
<TagPill label="Station" value={pairTest.station} />
|
||||||
|
<TagPill label="Band" value={pairTest.band} />
|
||||||
|
<TagPill label="Channel" value={pairTest.channel} />
|
||||||
|
<TagPill label="Bandwidth" value={pairTest.bandwidth} />
|
||||||
|
<TagPill label="Throttle" value={pairTest.throttled} />
|
||||||
|
<TagPill label="Direction" value={pairTest.direction} />
|
||||||
|
<TagPill label="Elapsed Time" value={fmtDuration(pairTest.duration_seconds)} />
|
||||||
|
</div>
|
||||||
|
{(() => {
|
||||||
|
const rows = parseTputRows(pairTest.tput_results)
|
||||||
|
if (!pairTest.completed || rows.length === 0) return null
|
||||||
|
return (
|
||||||
|
<div className="mt-3 pt-3 border-t border-slate-700/50">
|
||||||
|
<table className="text-xs w-auto border-collapse">
|
||||||
|
<thead>
|
||||||
|
<tr className="text-slate-500 uppercase tracking-wide">
|
||||||
|
<th className="pr-6 pb-1 text-left font-medium">Station</th>
|
||||||
|
<th className="pr-6 pb-1 text-right font-medium">Throughput</th>
|
||||||
|
<th className="pr-6 pb-1 text-right font-medium">DL RSSI</th>
|
||||||
|
<th className="pb-1 text-right font-medium">UL RSSI</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{rows.map(r => (
|
||||||
|
<tr key={`${pairTest.id}-${r.station}`} className="text-slate-300">
|
||||||
|
<td className="pr-6 py-0.5 text-cyan-400 font-medium">STA{r.station}</td>
|
||||||
|
<td className="pr-6 py-0.5 text-right">{r.tput} Mbps</td>
|
||||||
|
<td className="pr-6 py-0.5 text-right">{r.dlRssi} dBm</td>
|
||||||
|
<td className="py-0.5 text-right">{r.ulRssi} dBm</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})()}
|
||||||
|
{(() => {
|
||||||
|
const rows = parseTputRows(test.tput_results)
|
||||||
if (!test.completed || rows.length === 0) return null
|
if (!test.completed || rows.length === 0) return null
|
||||||
return (
|
return (
|
||||||
<div className="mt-3 pt-3 border-t border-slate-700/50">
|
<div className="mt-3 pt-3 border-t border-slate-700/50">
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -65,6 +65,7 @@ def _init_db():
|
|||||||
"ul_rssi_dbm REAL",
|
"ul_rssi_dbm REAL",
|
||||||
"tput_results TEXT",
|
"tput_results TEXT",
|
||||||
"throttled TEXT",
|
"throttled TEXT",
|
||||||
|
"coe_pair TEXT",
|
||||||
]:
|
]:
|
||||||
try:
|
try:
|
||||||
_conn.execute(f"ALTER TABLE tests ADD COLUMN {col_def}")
|
_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):
|
def get_station_for_test(test_id, device):
|
||||||
with _lock:
|
with _lock:
|
||||||
row = _conn.execute(
|
row = _conn.execute(
|
||||||
|
|||||||
+43
-1
@@ -1,7 +1,7 @@
|
|||||||
import os
|
import os
|
||||||
import re
|
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 (
|
from parser import (
|
||||||
parse_elapsed_time,
|
parse_elapsed_time,
|
||||||
parse_result_filename,
|
parse_result_filename,
|
||||||
@@ -21,6 +21,48 @@ def full_scan(target_dir, results_dir):
|
|||||||
|
|
||||||
scan_targets(target_dir)
|
scan_targets(target_dir)
|
||||||
scan_results(results_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):
|
def scan_targets(target_dir):
|
||||||
|
|||||||
Reference in New Issue
Block a user