implemented auto updates
This commit is contained in:
@@ -6,6 +6,8 @@ export function useStats(enabled = true) {
|
||||
queryKey: ['stats'],
|
||||
queryFn: getStats,
|
||||
enabled,
|
||||
refetchInterval: enabled ? 5000 : false,
|
||||
refetchIntervalInBackground: true,
|
||||
})
|
||||
|
||||
return {
|
||||
|
||||
@@ -7,5 +7,7 @@ export function useTests(filters = {}, enabled = true) {
|
||||
queryFn: () => getTests(filters),
|
||||
keepPreviousData: true,
|
||||
enabled,
|
||||
refetchInterval: enabled ? 3000 : false,
|
||||
refetchIntervalInBackground: true,
|
||||
})
|
||||
}
|
||||
|
||||
+36
-3
@@ -1,5 +1,6 @@
|
||||
import os
|
||||
import threading
|
||||
import atexit
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from functools import wraps
|
||||
from pathlib import Path
|
||||
@@ -24,6 +25,7 @@ from db_py import (
|
||||
clear_users,
|
||||
)
|
||||
from scanner import full_scan, is_scan_in_progress, resolve_runtime_path, scan_results_only
|
||||
from watcher import start_results_watchers, stop_results_watchers
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent
|
||||
load_dotenv(BASE_DIR / ".env")
|
||||
@@ -421,6 +423,8 @@ def set_config_route():
|
||||
return jsonify({"error": "Request body must be a JSON object"}), 400
|
||||
|
||||
rescan_required = False
|
||||
watcher_restart_required = False
|
||||
warnings = []
|
||||
|
||||
for key, value in updates.items():
|
||||
if key not in ALLOWED_KEYS:
|
||||
@@ -433,6 +437,19 @@ def set_config_route():
|
||||
|
||||
if key in {"target_dir", "results_dir", "results_dir_ref", "scan_exclusions"}:
|
||||
rescan_required = True
|
||||
if key in {"results_dir", "results_dir_ref", "smb_username", "smb_password", "smb_domain"}:
|
||||
watcher_restart_required = True
|
||||
|
||||
if watcher_restart_required:
|
||||
_apply_smb_env_from_config()
|
||||
results_dir = resolve_runtime_path(get_config("results_dir"))
|
||||
results_dir_ref = resolve_runtime_path(get_config("results_dir_ref"))
|
||||
try:
|
||||
start_results_watchers(results_dir, results_dir_ref)
|
||||
except Exception as exc:
|
||||
warning = f"Watcher restart failed: {exc}"
|
||||
warnings.append(warning)
|
||||
print(f"[server] {warning}")
|
||||
|
||||
if rescan_required:
|
||||
_apply_smb_env_from_config()
|
||||
@@ -443,16 +460,28 @@ def set_config_route():
|
||||
return jsonify({"error": "Directories not configured"}), 400
|
||||
|
||||
if is_scan_in_progress():
|
||||
return jsonify({"ok": True, "scanning": True, "testCount": None, "completedCount": None})
|
||||
return jsonify({
|
||||
"ok": True,
|
||||
"scanning": True,
|
||||
"testCount": None,
|
||||
"completedCount": None,
|
||||
"warnings": warnings,
|
||||
})
|
||||
|
||||
threading.Thread(
|
||||
target=full_scan,
|
||||
args=(target_dir, results_dir, results_dir_ref),
|
||||
daemon=True,
|
||||
).start()
|
||||
return jsonify({"ok": True, "scanning": True, "testCount": None, "completedCount": None})
|
||||
return jsonify({
|
||||
"ok": True,
|
||||
"scanning": True,
|
||||
"testCount": None,
|
||||
"completedCount": None,
|
||||
"warnings": warnings,
|
||||
})
|
||||
|
||||
return jsonify({"ok": True, "testCount": None, "completedCount": None})
|
||||
return jsonify({"ok": True, "testCount": None, "completedCount": None, "warnings": warnings})
|
||||
|
||||
|
||||
@app.post("/api/config/rescan")
|
||||
@@ -526,11 +555,15 @@ def bootstrap():
|
||||
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_results_watchers(results_dir, results_dir_ref)
|
||||
else:
|
||||
stop_results_watchers()
|
||||
print("[server] No directories configured -> open the dashboard settings to get started.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
atexit.register(stop_results_watchers)
|
||||
bootstrap()
|
||||
print(f"[server] Listening on http://0.0.0.0:{PORT}")
|
||||
app.run(host="0.0.0.0", port=PORT, threaded=True)
|
||||
|
||||
@@ -3,3 +3,4 @@ Flask-Cors>=4.0.1,<5.0.0
|
||||
smbprotocol>=1.13.0,<2.0.0
|
||||
PyJWT>=2.9.0,<3.0.0
|
||||
python-dotenv>=1.0.1,<2.0.0
|
||||
watchdog>=4.0.2,<5.0.0
|
||||
|
||||
+148
-21
@@ -1,29 +1,156 @@
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from watchdog.observers import Observer
|
||||
from watchdog.events import FileSystemEventHandler
|
||||
import threading
|
||||
|
||||
class TestResultsHandler(FileSystemEventHandler):
|
||||
def __init__(self, callback):
|
||||
from watchdog.events import FileSystemEventHandler
|
||||
from watchdog.observers.polling import PollingObserver
|
||||
|
||||
from db_py import (
|
||||
mark_tests_completed,
|
||||
reset_by_file_id_and_device,
|
||||
update_all_p2p_coe_pairs_sql,
|
||||
update_all_p3p_pairs_sql,
|
||||
)
|
||||
from scanner import parse_deleted_result_dir_name, process_result_dir, resolve_runtime_path
|
||||
|
||||
|
||||
_OBSERVER = None
|
||||
_OBSERVER_LOCK = threading.RLock()
|
||||
|
||||
|
||||
class ResultsDirEventHandler(FileSystemEventHandler):
|
||||
def __init__(self, results_root):
|
||||
super().__init__()
|
||||
self.callback = callback
|
||||
self.results_root = str(results_root)
|
||||
|
||||
def _is_direct_child(self, path_value):
|
||||
parent = os.path.dirname(path_value.rstrip("\\/"))
|
||||
return os.path.normcase(parent) == os.path.normcase(self.results_root.rstrip("\\/"))
|
||||
|
||||
def _mark_completed(self, dir_path):
|
||||
if not self._is_direct_child(dir_path):
|
||||
return
|
||||
|
||||
dir_name = os.path.basename(dir_path.rstrip("\\/"))
|
||||
completion = process_result_dir(self.results_root, dir_name)
|
||||
if not completion:
|
||||
return
|
||||
|
||||
mark_tests_completed([completion])
|
||||
coe_pairs = update_all_p2p_coe_pairs_sql()
|
||||
p3p_pairs = update_all_p3p_pairs_sql()
|
||||
print(
|
||||
"[watcher] Marked completed from new result dir "
|
||||
f"{dir_name} (coe_pair={coe_pairs}, p3p_pair={p3p_pairs})"
|
||||
)
|
||||
|
||||
def _mark_completed_from_file(self, file_path):
|
||||
# measurement.db arriving after directory creation should update that single test.
|
||||
if os.path.basename(file_path).lower() != "measurement.db":
|
||||
return
|
||||
|
||||
parent_dir = os.path.dirname(file_path.rstrip("\\/"))
|
||||
self._mark_completed(parent_dir)
|
||||
|
||||
def _clear_completed(self, dir_path):
|
||||
if not self._is_direct_child(dir_path):
|
||||
return
|
||||
|
||||
dir_name = os.path.basename(dir_path.rstrip("\\/"))
|
||||
test_id, device = parse_deleted_result_dir_name(dir_name)
|
||||
if not test_id:
|
||||
return
|
||||
|
||||
reset_by_file_id_and_device(test_id, device)
|
||||
coe_pairs = update_all_p2p_coe_pairs_sql()
|
||||
p3p_pairs = update_all_p3p_pairs_sql()
|
||||
print(
|
||||
"[watcher] Cleared completed from removed result dir "
|
||||
f"{dir_name} (coe_pair={coe_pairs}, p3p_pair={p3p_pairs})"
|
||||
)
|
||||
|
||||
def on_created(self, event):
|
||||
if event.is_directory:
|
||||
print(f"[watcher] Detected new test result directory: {event.src_path}")
|
||||
self.callback(event.src_path)
|
||||
self._mark_completed(event.src_path)
|
||||
return
|
||||
|
||||
self._mark_completed_from_file(event.src_path)
|
||||
|
||||
def on_deleted(self, event):
|
||||
if event.is_directory:
|
||||
self._clear_completed(event.src_path)
|
||||
|
||||
def on_moved(self, event):
|
||||
if event.is_directory:
|
||||
self._clear_completed(event.src_path)
|
||||
self._mark_completed(event.dest_path)
|
||||
return
|
||||
|
||||
self._mark_completed_from_file(event.dest_path)
|
||||
|
||||
|
||||
def start_watching_results_dir(results_dir, callback):
|
||||
event_handler = TestResultsHandler(callback)
|
||||
observer = Observer()
|
||||
observer.schedule(event_handler, results_dir, recursive=False)
|
||||
observer.start()
|
||||
print(f"[watcher] Started watching for new test results in: {results_dir}")
|
||||
try:
|
||||
while True:
|
||||
time.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
observer.stop()
|
||||
observer.join()
|
||||
def stop_results_watchers():
|
||||
global _OBSERVER
|
||||
|
||||
with _OBSERVER_LOCK:
|
||||
if _OBSERVER is None:
|
||||
return
|
||||
|
||||
try:
|
||||
_OBSERVER.stop()
|
||||
_OBSERVER.join(timeout=5)
|
||||
finally:
|
||||
_OBSERVER = None
|
||||
print("[watcher] Stopped results directory watcher")
|
||||
|
||||
|
||||
def start_results_watchers(results_dir, results_dir_ref):
|
||||
global _OBSERVER
|
||||
|
||||
with _OBSERVER_LOCK:
|
||||
stop_results_watchers()
|
||||
|
||||
resolved = []
|
||||
seen = set()
|
||||
for raw_dir in (results_dir, results_dir_ref):
|
||||
if not raw_dir:
|
||||
continue
|
||||
path = resolve_runtime_path(raw_dir)
|
||||
if not path:
|
||||
continue
|
||||
normalized = os.path.normcase(str(path).rstrip("\\/"))
|
||||
if normalized and normalized not in seen:
|
||||
seen.add(normalized)
|
||||
resolved.append(str(path))
|
||||
|
||||
if not resolved:
|
||||
print("[watcher] No results directories configured, watcher not started")
|
||||
return
|
||||
|
||||
observer = PollingObserver(timeout=1.0)
|
||||
|
||||
scheduled_count = 0
|
||||
for path in resolved:
|
||||
try:
|
||||
observer.schedule(ResultsDirEventHandler(path), path, recursive=False)
|
||||
scheduled_count += 1
|
||||
print(f"[watcher] Watching results directory: {path}")
|
||||
except Exception as exc:
|
||||
print(f"[watcher] Skipping watch path {path}: {exc}")
|
||||
|
||||
if scheduled_count == 0:
|
||||
print("[watcher] No valid watch paths available, watcher not started")
|
||||
return
|
||||
|
||||
try:
|
||||
observer.start()
|
||||
except Exception as exc:
|
||||
print(f"[watcher] Failed to start results watcher: {exc}")
|
||||
try:
|
||||
observer.stop()
|
||||
observer.join(timeout=5)
|
||||
except Exception:
|
||||
pass
|
||||
return
|
||||
|
||||
_OBSERVER = observer
|
||||
print("[watcher] Results directory watcher started")
|
||||
Reference in New Issue
Block a user