Files
test_dashboard/server/app.py
T

365 lines
12 KiB
Python
Raw Normal View History

2026-05-26 14:36:34 -04:00
import os
2026-05-28 12:34:01 -04:00
import threading
2026-05-26 14:36:34 -04:00
from pathlib import Path
from flask import Flask, Response, jsonify, request, send_from_directory
from flask_cors import CORS
from db_py import count_tests, del_config, get_all_tests, get_config, set_config
2026-05-28 12:34:01 -04:00
from scanner import full_scan, is_scan_in_progress, resolve_runtime_path, scan_results_only
2026-05-26 14:36:34 -04:00
from sse_py import broadcast, stream_events
from watcher import start_watching
PORT = int(os.getenv("PORT", "3001"))
ALLOWED_KEYS = {
"target_dir",
"results_dir",
2026-05-27 15:02:32 -04:00
"results_dir_ref",
2026-05-26 14:36:34 -04:00
"avg_time_coe",
"avg_time_p2p",
"avg_time_p3p",
2026-06-01 14:24:48 -04:00
"scan_exclusions",
2026-05-27 15:02:32 -04:00
"smb_username",
"smb_password",
"smb_domain",
2026-05-26 14:36:34 -04:00
}
BASE_DIR = Path(__file__).resolve().parent
DIST_DIR = BASE_DIR.parent / "dashboard" / "dist"
app = Flask(__name__, static_folder=str(DIST_DIR), static_url_path="")
CORS(app)
2026-05-27 15:02:32 -04:00
def _apply_smb_env_from_config():
mapping = {
"SMB_USERNAME": get_config("smb_username"),
"SMB_PASSWORD": get_config("smb_password"),
"SMB_DOMAIN": get_config("smb_domain"),
}
for env_key, value in mapping.items():
if value in (None, ""):
os.environ.pop(env_key, None)
else:
os.environ[env_key] = str(value)
2026-05-28 12:34:01 -04:00
def _start_full_scan_background(target_dir, results_dir, results_dir_ref, source_label):
def _job():
try:
full_scan(target_dir, results_dir, results_dir_ref)
2026-06-01 14:24:48 -04:00
start_watching(target_dir, results_dir, results_dir_ref)
2026-05-28 12:34:01 -04:00
tests = get_all_tests()
completed = len([t for t in tests if t.get("completed")])
print(f"[{source_label}] Scan complete -> {len(tests)} tests, {completed} completed")
except Exception as exc:
print(f"[{source_label}] background fullScan error: {exc}")
finally:
broadcast({"type": "update"})
worker = threading.Thread(target=_job, daemon=True)
worker.start()
def _start_results_scan_background(results_dir, results_dir_ref, source_label):
def _job():
try:
scan_results_only(results_dir, results_dir_ref)
tests = get_all_tests()
completed = len([t for t in tests if t.get("completed")])
print(f"[{source_label}] Results scan complete -> {len(tests)} tests, {completed} completed")
except Exception as exc:
print(f"[{source_label}] background rescan-results error: {exc}")
finally:
broadcast({"type": "update"})
worker = threading.Thread(target=_job, daemon=True)
worker.start()
2026-05-26 14:36:34 -04:00
@app.get("/api/tests")
def get_tests_route():
completed = request.args.get("completed")
interference = request.args.get("interference")
2026-05-27 10:29:26 -04:00
throttled = request.args.get("throttled")
2026-05-26 14:36:34 -04:00
device = request.args.get("device")
rotation = request.args.get("rotation")
test_point = request.args.get("testPoint")
station = request.args.get("station")
band = request.args.get("band")
channel = request.args.get("channel")
bandwidth = request.args.get("bandwidth")
rssi = request.args.get("rssi")
direction = request.args.get("direction")
tests = get_all_tests()
if completed is not None:
completed_value = 1 if completed == "true" else 0
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]
2026-05-27 10:29:26 -04:00
if throttled:
tests = [t for t in tests if t.get("throttled") == throttled]
2026-05-26 14:36:34 -04:00
if device:
tests = [t for t in tests if t.get("device") == device]
if rotation:
tests = [t for t in tests if t.get("rotation") == rotation]
if test_point:
tests = [t for t in tests if t.get("test_point") == test_point]
if station:
tests = [t for t in tests if t.get("station") == station]
if band:
tests = [t for t in tests if t.get("band") == band]
if channel:
tests = [t for t in tests if t.get("channel") == channel]
if bandwidth:
tests = [t for t in tests if t.get("bandwidth") == bandwidth]
if rssi:
tests = [t for t in tests if t.get("rssi") == rssi]
if direction:
tests = [t for t in tests if t.get("direction") == direction]
tests.sort(key=lambda item: ((item.get("interference") or ""), (item.get("test_id") or "")))
return jsonify(tests)
@app.get("/api/stats")
def get_stats_route():
tests = get_all_tests()
2026-05-27 10:29:26 -04:00
types = ["COE", "P2P", "P3P"]
2026-05-26 14:36:34 -04:00
total_completed = sum(1 for t in tests if t.get("completed"))
overall = {
"total": len(tests),
"completed": total_completed,
"completionRate": (total_completed / len(tests)) if tests else 0,
}
device_map = {}
for test in tests:
name = test.get("device")
if not name:
continue
if name not in device_map:
2026-05-27 10:29:26 -04:00
device_map[name] = {
"total": 0,
"completed": 0,
"byType": {t: {"total": 0, "completed": 0} for t in types},
}
2026-05-26 14:36:34 -04:00
device_map[name]["total"] += 1
2026-05-27 10:29:26 -04:00
interference = test.get("interference")
if interference in device_map[name]["byType"]:
device_map[name]["byType"][interference]["total"] += 1
2026-05-26 14:36:34 -04:00
if test.get("completed"):
device_map[name]["completed"] += 1
2026-05-27 10:29:26 -04:00
if interference in device_map[name]["byType"]:
device_map[name]["byType"][interference]["completed"] += 1
2026-05-26 14:36:34 -04:00
devices = []
for name in sorted(device_map.keys()):
stats = device_map[name]
devices.append(
{
"name": name,
"total": stats["total"],
"completed": stats["completed"],
"completionRate": (stats["completed"] / stats["total"]) if stats["total"] else 0,
2026-05-27 10:29:26 -04:00
"byType": stats["byType"],
2026-05-26 14:36:34 -04:00
}
)
elapsed_seconds = 0
for test in tests:
duration = test.get("duration_seconds")
if test.get("completed") and duration is not None:
elapsed_seconds += duration
by_type = {}
estimate_possible = True
estimated_remaining_seconds = 0
for test_type in types:
type_tests = [t for t in tests if t.get("interference") == test_type]
completed_tests = [t for t in type_tests if t.get("completed")]
with_duration = [t for t in completed_tests if t.get("duration_seconds") is not None]
remaining = len(type_tests) - len(completed_tests)
calc_avg = None
if with_duration:
calc_avg = sum(t.get("duration_seconds") for t in with_duration) / len(with_duration)
manual_value = get_config(f"avg_time_{test_type.lower()}")
avg_seconds = None
avg_source = None
if manual_value is not None:
avg_seconds = float(manual_value)
avg_source = "manual_override" if calc_avg is not None else "manual"
elif calc_avg is not None:
avg_seconds = calc_avg
avg_source = "calculated"
by_type[test_type] = {
"total": len(type_tests),
"completed": len(completed_tests),
"remaining": remaining,
"avgSeconds": avg_seconds,
"avgSource": avg_source,
}
if remaining > 0:
if avg_seconds is not None:
estimated_remaining_seconds += avg_seconds * remaining
else:
estimate_possible = False
return jsonify(
{
"overall": overall,
"devices": devices,
"timing": {
"elapsedSeconds": elapsed_seconds,
"estimatedRemainingSeconds": estimated_remaining_seconds if estimate_possible else None,
"byType": by_type,
},
}
)
2026-05-28 12:34:01 -04:00
@app.get("/api/scan-status")
def get_scan_status_route():
return jsonify({"scanning": is_scan_in_progress()})
2026-05-26 14:36:34 -04:00
@app.get("/api/config")
def get_config_route():
config = {}
for key in ALLOWED_KEYS:
config[key] = get_config(key)
return jsonify(config)
@app.post("/api/config")
def set_config_route():
updates = request.get_json(silent=True)
if not isinstance(updates, dict):
return jsonify({"error": "Request body must be a JSON object"}), 400
2026-06-01 14:24:48 -04:00
rescan_required = False
2026-05-26 14:36:34 -04:00
for key, value in updates.items():
if key not in ALLOWED_KEYS:
continue
if value in (None, ""):
del_config(key)
else:
set_config(key, str(value))
2026-06-01 14:24:48 -04:00
if key in {"target_dir", "results_dir", "results_dir_ref", "scan_exclusions"}:
rescan_required = True
2026-05-26 14:36:34 -04:00
2026-06-01 14:24:48 -04:00
if rescan_required:
2026-05-27 15:02:32 -04:00
_apply_smb_env_from_config()
2026-05-28 12:34:01 -04:00
target_dir = resolve_runtime_path(get_config("target_dir"))
results_dir = resolve_runtime_path(get_config("results_dir"))
results_dir_ref = resolve_runtime_path(get_config("results_dir_ref"))
if not target_dir or not results_dir:
return jsonify({"error": "Directories not configured"}), 400
if is_scan_in_progress():
return jsonify({"ok": True, "scanning": True, "testCount": None, "completedCount": None})
2026-05-26 14:36:34 -04:00
2026-05-28 12:34:01 -04:00
_start_full_scan_background(target_dir, results_dir, results_dir_ref, "config")
return jsonify({"ok": True, "scanning": True, "testCount": None, "completedCount": None})
2026-05-26 14:36:34 -04:00
2026-05-27 15:02:32 -04:00
broadcast({"type": "update"})
2026-05-26 14:36:34 -04:00
return jsonify({"ok": True, "testCount": None, "completedCount": None})
@app.post("/api/config/rescan")
def rescan_route():
2026-05-27 15:02:32 -04:00
_apply_smb_env_from_config()
2026-05-28 12:34:01 -04:00
target_dir = resolve_runtime_path(get_config("target_dir"))
results_dir = resolve_runtime_path(get_config("results_dir"))
results_dir_ref = resolve_runtime_path(get_config("results_dir_ref"))
2026-05-26 14:36:34 -04:00
if not target_dir or not results_dir:
return jsonify({"error": "Directories not configured"}), 400
2026-05-28 12:34:01 -04:00
if is_scan_in_progress():
return jsonify({"ok": True, "scanning": True, "testCount": None, "completedCount": None})
2026-05-26 14:36:34 -04:00
2026-05-28 12:34:01 -04:00
_start_full_scan_background(target_dir, results_dir, results_dir_ref, "config")
return jsonify({"ok": True, "scanning": True, "testCount": None, "completedCount": None})
2026-05-26 14:36:34 -04:00
2026-05-27 15:02:32 -04:00
@app.post("/api/config/rescan-results")
def rescan_results_route():
_apply_smb_env_from_config()
2026-05-28 12:34:01 -04:00
results_dir = resolve_runtime_path(get_config("results_dir"))
results_dir_ref = resolve_runtime_path(get_config("results_dir_ref"))
2026-05-27 15:02:32 -04:00
if not results_dir:
return jsonify({"error": "Results directory not configured"}), 400
2026-05-28 12:34:01 -04:00
if is_scan_in_progress():
return jsonify({"ok": True, "scanning": True, "testCount": None, "completedCount": None})
2026-05-27 15:02:32 -04:00
2026-05-28 12:34:01 -04:00
_start_results_scan_background(results_dir, results_dir_ref, "config")
return jsonify({"ok": True, "scanning": True, "testCount": None, "completedCount": None})
2026-05-27 15:02:32 -04:00
2026-05-26 14:36:34 -04:00
@app.get("/api/events")
def events_route():
headers = {
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"X-Accel-Buffering": "no",
}
return Response(stream_events(), mimetype="text/event-stream", headers=headers)
@app.get("/")
@app.get("/<path:path>")
def static_or_spa(path=""):
if not DIST_DIR.exists():
return jsonify({"error": "Frontend dist not found"}), 404
if path and (DIST_DIR / path).is_file():
return send_from_directory(DIST_DIR, path)
return send_from_directory(DIST_DIR, "index.html")
def bootstrap():
2026-05-27 15:02:32 -04:00
_apply_smb_env_from_config()
2026-05-28 12:34:01 -04:00
target_dir = resolve_runtime_path(get_config("target_dir"))
results_dir = resolve_runtime_path(get_config("results_dir"))
results_dir_ref = resolve_runtime_path(get_config("results_dir_ref"))
2026-05-26 14:36:34 -04:00
if target_dir and results_dir:
existing = count_tests()
if existing > 0:
print(f"[server] Resuming from DB -> {existing} tests already loaded.")
else:
print("[server] No cached data, scanning directories...")
2026-05-27 15:02:32 -04:00
full_scan(target_dir, results_dir, results_dir_ref)
2026-05-26 14:36:34 -04:00
tests = get_all_tests()
completed = len([t for t in tests if t.get("completed")])
print(f"[server] Scan complete -> {len(tests)} tests found, {completed} completed")
start_watching(target_dir, results_dir)
print("[server] Watching for changes.")
else:
print("[server] No directories configured -> open the dashboard settings to get started.")
if __name__ == "__main__":
bootstrap()
print(f"[server] Listening on http://0.0.0.0:{PORT}")
app.run(host="0.0.0.0", port=PORT, threaded=True)