UI changes
This commit is contained in:
+90
-3
@@ -1,4 +1,5 @@
|
||||
import os
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
from flask import Flask, Response, jsonify, request, send_from_directory
|
||||
@@ -29,6 +30,7 @@ CORS(app)
|
||||
def get_tests_route():
|
||||
completed = request.args.get("completed")
|
||||
interference = request.args.get("interference")
|
||||
throttled = request.args.get("throttled")
|
||||
device = request.args.get("device")
|
||||
rotation = request.args.get("rotation")
|
||||
test_point = request.args.get("testPoint")
|
||||
@@ -46,6 +48,8 @@ def get_tests_route():
|
||||
tests = [t for t in tests if t.get("completed") == completed_value]
|
||||
if interference:
|
||||
tests = [t for t in tests if t.get("interference") == interference]
|
||||
if throttled:
|
||||
tests = [t for t in tests if t.get("throttled") == throttled]
|
||||
if device:
|
||||
tests = [t for t in tests if t.get("device") == device]
|
||||
if rotation:
|
||||
@@ -72,6 +76,7 @@ def get_tests_route():
|
||||
@app.get("/api/stats")
|
||||
def get_stats_route():
|
||||
tests = get_all_tests()
|
||||
types = ["COE", "P2P", "P3P"]
|
||||
|
||||
total_completed = sum(1 for t in tests if t.get("completed"))
|
||||
overall = {
|
||||
@@ -87,10 +92,21 @@ def get_stats_route():
|
||||
continue
|
||||
|
||||
if name not in device_map:
|
||||
device_map[name] = {"total": 0, "completed": 0}
|
||||
device_map[name] = {
|
||||
"total": 0,
|
||||
"completed": 0,
|
||||
"byType": {t: {"total": 0, "completed": 0} for t in types},
|
||||
}
|
||||
device_map[name]["total"] += 1
|
||||
|
||||
interference = test.get("interference")
|
||||
if interference in device_map[name]["byType"]:
|
||||
device_map[name]["byType"][interference]["total"] += 1
|
||||
|
||||
if test.get("completed"):
|
||||
device_map[name]["completed"] += 1
|
||||
if interference in device_map[name]["byType"]:
|
||||
device_map[name]["byType"][interference]["completed"] += 1
|
||||
|
||||
devices = []
|
||||
for name in sorted(device_map.keys()):
|
||||
@@ -101,6 +117,7 @@ def get_stats_route():
|
||||
"total": stats["total"],
|
||||
"completed": stats["completed"],
|
||||
"completionRate": (stats["completed"] / stats["total"]) if stats["total"] else 0,
|
||||
"byType": stats["byType"],
|
||||
}
|
||||
)
|
||||
|
||||
@@ -110,7 +127,6 @@ def get_stats_route():
|
||||
if test.get("completed") and duration is not None:
|
||||
elapsed_seconds += duration
|
||||
|
||||
types = ["COE", "P2P", "P3P"]
|
||||
by_type = {}
|
||||
estimate_possible = True
|
||||
estimated_remaining_seconds = 0
|
||||
@@ -234,6 +250,56 @@ def rescan_route():
|
||||
|
||||
@app.get("/api/browse")
|
||||
def browse_route():
|
||||
def _normalize_request_path(path):
|
||||
if not path:
|
||||
return path
|
||||
p = path.strip()
|
||||
if os.name == "nt":
|
||||
p = p.replace("/", "\\")
|
||||
return p
|
||||
|
||||
def _is_unc_host_only(path):
|
||||
if os.name != "nt" or not path:
|
||||
return False
|
||||
p = path.rstrip("\\")
|
||||
if not p.startswith("\\\\"):
|
||||
return False
|
||||
remainder = p[2:]
|
||||
return bool(remainder) and "\\" not in remainder
|
||||
|
||||
def _shares_for_unc_host(host):
|
||||
# Enumerate SMB shares with native Windows tooling for host-only UNC paths.
|
||||
proc = subprocess.run(
|
||||
["net", "view", f"\\\\{host}"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
check=False,
|
||||
)
|
||||
if proc.returncode != 0:
|
||||
raise RuntimeError(proc.stderr.strip() or proc.stdout.strip() or "Cannot query network host")
|
||||
|
||||
shares = []
|
||||
in_table = False
|
||||
for raw_line in proc.stdout.splitlines():
|
||||
line = raw_line.strip()
|
||||
if not line:
|
||||
continue
|
||||
if line.startswith("---"):
|
||||
in_table = True
|
||||
continue
|
||||
if not in_table:
|
||||
continue
|
||||
if line.lower().startswith("the command completed successfully"):
|
||||
break
|
||||
|
||||
first = line.split()[0]
|
||||
if first and first.lower() not in {"share", "name"}:
|
||||
shares.append(first)
|
||||
|
||||
unique = sorted(set(shares), key=str.lower)
|
||||
return [{"name": share, "path": f"\\\\{host}\\{share}"} for share in unique]
|
||||
|
||||
def _configured_roots():
|
||||
raw = os.getenv("BROWSE_ROOTS", "").strip()
|
||||
if not raw:
|
||||
@@ -260,8 +326,19 @@ def browse_route():
|
||||
continue
|
||||
return False
|
||||
|
||||
def _is_unc_host_allowed(host, roots):
|
||||
if not roots:
|
||||
return True
|
||||
host_prefix = os.path.normcase(f"\\\\{host}\\")
|
||||
host_exact = host_prefix.rstrip("\\")
|
||||
for root in roots:
|
||||
normalized_root = os.path.normcase(root)
|
||||
if normalized_root == host_exact or normalized_root.startswith(host_prefix):
|
||||
return True
|
||||
return False
|
||||
|
||||
roots = _configured_roots()
|
||||
req_path = request.args.get("path")
|
||||
req_path = _normalize_request_path(request.args.get("path"))
|
||||
|
||||
if not req_path:
|
||||
if roots:
|
||||
@@ -282,6 +359,16 @@ def browse_route():
|
||||
|
||||
return jsonify({"path": None, "parent": None, "dirs": dirs})
|
||||
|
||||
if _is_unc_host_only(req_path):
|
||||
host = req_path.rstrip("\\")[2:]
|
||||
if not _is_unc_host_allowed(host, roots):
|
||||
return jsonify({"error": "Path is outside allowed browse roots"}), 403
|
||||
try:
|
||||
dirs = _shares_for_unc_host(host)
|
||||
except Exception as exc:
|
||||
return jsonify({"error": f"Cannot list shares for host: {exc}"}), 403
|
||||
return jsonify({"path": f"\\\\{host}", "parent": None, "dirs": dirs})
|
||||
|
||||
if not os.path.exists(req_path):
|
||||
return jsonify({"error": "Path does not exist"}), 400
|
||||
if not os.path.isdir(req_path):
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
+5
-3
@@ -64,6 +64,7 @@ def _init_db():
|
||||
"dl_rssi_dbm REAL",
|
||||
"ul_rssi_dbm REAL",
|
||||
"tput_results TEXT",
|
||||
"throttled TEXT",
|
||||
]:
|
||||
try:
|
||||
_conn.execute(f"ALTER TABLE tests ADD COLUMN {col_def}")
|
||||
@@ -96,10 +97,10 @@ def upsert_test(test):
|
||||
"""
|
||||
INSERT INTO tests
|
||||
(id, test_id, parent_dir, filename, interference, device, rotation,
|
||||
test_point, station, band, channel, bandwidth, rssi, direction)
|
||||
test_point, station, band, channel, bandwidth, rssi, direction, throttled)
|
||||
VALUES
|
||||
(:id, :test_id, :parent_dir, :filename, :interference, :device, :rotation,
|
||||
:test_point, :station, :band, :channel, :bandwidth, :rssi, :direction)
|
||||
:test_point, :station, :band, :channel, :bandwidth, :rssi, :direction, :throttled)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
test_id = excluded.test_id,
|
||||
parent_dir = excluded.parent_dir,
|
||||
@@ -113,7 +114,8 @@ def upsert_test(test):
|
||||
channel = excluded.channel,
|
||||
bandwidth = excluded.bandwidth,
|
||||
rssi = excluded.rssi,
|
||||
direction = excluded.direction
|
||||
direction = excluded.direction,
|
||||
throttled = excluded.throttled
|
||||
""",
|
||||
test,
|
||||
)
|
||||
|
||||
@@ -11,6 +11,11 @@ def parse_target_filename(filename, parent_dir):
|
||||
test_identifier = f"{parent_dir}/{base_name}"
|
||||
interference = segments[0] if segments and segments[0] in INTERFERENCE_TYPES else None
|
||||
test_id = next((s for s in segments if re.match(r"^R\d+[A-Z0-9]+$", s, re.IGNORECASE)), None)
|
||||
throttled = None
|
||||
if interference == "P3P" and test_id:
|
||||
# P3P test_id carries throttle marker: TH = throttled, otherwise UT.
|
||||
throttled = "TH" if "TH" in test_id.upper() else "UT"
|
||||
|
||||
device = segments[1] if len(segments) > 1 else None
|
||||
test_point = next((s for s in segments if re.match(r"^TPT\w+$", s)), None)
|
||||
rssi = next((s for s in segments if re.match(r"^RSSI\d+$", s)), None)
|
||||
@@ -39,6 +44,7 @@ def parse_target_filename(filename, parent_dir):
|
||||
"bandwidth": bandwidth,
|
||||
"direction": direction,
|
||||
"rotation": rotation,
|
||||
"throttled": throttled,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -77,6 +77,7 @@ def scan_targets(target_dir):
|
||||
"bandwidth": parsed["bandwidth"],
|
||||
"rssi": parsed["rssi"],
|
||||
"direction": parsed["direction"],
|
||||
"throttled": parsed["throttled"],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user