From f6bced78f3a9a4a751bdc2e6e9f319809bb59aad Mon Sep 17 00:00:00 2001 From: Mia Wu Date: Mon, 1 Jun 2026 15:07:49 -0400 Subject: [PATCH] remove live updates, fix save results --- dashboard/src/components/ConfigModal.jsx | 6 + dashboard/src/hooks/useStats.js | 34 +-- dashboard/src/lib/api.js | 1 - server/app.py | 70 ++---- server/dashboard.db | Bin 544768 -> 544768 bytes server/db_py.py | 13 ++ server/requirements.txt | 1 - server/scanner.py | 4 + server/sse_py.py | 32 --- server/watcher.py | 270 ----------------------- 10 files changed, 39 insertions(+), 392 deletions(-) delete mode 100644 server/sse_py.py delete mode 100644 server/watcher.py diff --git a/dashboard/src/components/ConfigModal.jsx b/dashboard/src/components/ConfigModal.jsx index 9a3b41e..53ffb50 100644 --- a/dashboard/src/components/ConfigModal.jsx +++ b/dashboard/src/components/ConfigModal.jsx @@ -1,4 +1,5 @@ import { useState, useEffect } from 'react' +import { useQueryClient } from '@tanstack/react-query' import { useConfig, useSaveConfig } from '../hooks/useConfig' import { apiFetch } from '../lib/api' @@ -9,6 +10,7 @@ function fmtSeconds(s) { } export default function ConfigModal({ onClose }) { + const queryClient = useQueryClient() const { data: config, isLoading } = useConfig() const { mutate: save, isPending } = useSaveConfig() @@ -81,6 +83,8 @@ export default function ConfigModal({ onClose }) { avg_time_p3p: form.avg_time_p3p ? String(parseFloat(form.avg_time_p3p) * 60) : null, }), }) + queryClient.invalidateQueries({ queryKey: ['config'] }) + queryClient.invalidateQueries({ queryKey: ['stats'] }) } catch (err) { setSaveError(err?.message ?? 'Failed to save') } finally { @@ -97,6 +101,8 @@ export default function ConfigModal({ onClose }) { if (data?.testCount !== null && data?.testCount !== undefined) { setScanResult({ testCount: data.testCount, completedCount: data.completedCount }) } + queryClient.invalidateQueries({ queryKey: ['stats'] }) + queryClient.invalidateQueries({ queryKey: ['tests'] }) } catch (err) { setSaveError(err?.message ?? 'Rescan failed') } finally { diff --git a/dashboard/src/hooks/useStats.js b/dashboard/src/hooks/useStats.js index e1b6dad..5837378 100644 --- a/dashboard/src/hooks/useStats.js +++ b/dashboard/src/hooks/useStats.js @@ -1,42 +1,14 @@ -import { useQuery, useQueryClient } from '@tanstack/react-query' -import { useEffect } from 'react' -import { getScanStatus, getStats } from '../lib/api' +import { useQuery } from '@tanstack/react-query' +import { getStats } from '../lib/api' export function useStats() { - const queryClient = useQueryClient() - - // Subscribe to SSE updates once; invalidate both stats and tests on any update - useEffect(() => { - const es = new EventSource('/api/events') - es.onmessage = (e) => { - try { - const data = JSON.parse(e.data) - if (data.type === 'update') { - queryClient.invalidateQueries({ queryKey: ['stats'] }) - queryClient.invalidateQueries({ queryKey: ['tests'] }) - } - } catch { /* ignore malformed */ } - } - return () => es.close() - }, [queryClient]) - - const scanStatusQuery = useQuery({ - queryKey: ['scanStatus'], - queryFn: getScanStatus, - refetchInterval: (query) => (query.state.data?.scanning ? 1000 : 5000), - }) - - const isScanning = scanStatusQuery.data?.scanning ?? true - const statsQuery = useQuery({ queryKey: ['stats'], queryFn: getStats, - enabled: !isScanning, - refetchInterval: isScanning ? false : 30_000, }) return { ...statsQuery, - isScanning, + isScanning: false, } } diff --git a/dashboard/src/lib/api.js b/dashboard/src/lib/api.js index 7fa1ada..e69b048 100644 --- a/dashboard/src/lib/api.js +++ b/dashboard/src/lib/api.js @@ -13,7 +13,6 @@ export async function apiFetch(path, options = {}) { } export const getStats = () => apiFetch('/stats') -export const getScanStatus = () => apiFetch('/scan-status') export const getTests = (params = {}) => { const qs = new URLSearchParams( Object.entries(params).filter(([, v]) => v !== '' && v !== undefined && v !== null) diff --git a/server/app.py b/server/app.py index b4a0049..e257163 100644 --- a/server/app.py +++ b/server/app.py @@ -1,14 +1,11 @@ import os -import threading from pathlib import Path -from flask import Flask, Response, jsonify, request, send_from_directory +from flask import Flask, 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 from scanner import full_scan, is_scan_in_progress, resolve_runtime_path, scan_results_only -from sse_py import broadcast, stream_events -from watcher import start_watching PORT = int(os.getenv("PORT", "3001")) ALLOWED_KEYS = { @@ -45,39 +42,6 @@ def _apply_smb_env_from_config(): os.environ[env_key] = str(value) -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) - start_watching(target_dir, results_dir, results_dir_ref) - 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() - - @app.get("/api/tests") def get_tests_route(): completed = request.args.get("completed") @@ -276,10 +240,11 @@ def set_config_route(): if is_scan_in_progress(): return jsonify({"ok": True, "scanning": True, "testCount": None, "completedCount": None}) - _start_full_scan_background(target_dir, results_dir, results_dir_ref, "config") - return jsonify({"ok": True, "scanning": True, "testCount": None, "completedCount": None}) + full_scan(target_dir, results_dir, results_dir_ref) + tests = get_all_tests() + completed = len([t for t in tests if t.get("completed")]) + return jsonify({"ok": True, "scanning": False, "testCount": len(tests), "completedCount": completed}) - broadcast({"type": "update"}) return jsonify({"ok": True, "testCount": None, "completedCount": None}) @@ -295,8 +260,10 @@ def rescan_route(): if is_scan_in_progress(): return jsonify({"ok": True, "scanning": True, "testCount": None, "completedCount": None}) - _start_full_scan_background(target_dir, results_dir, results_dir_ref, "config") - return jsonify({"ok": True, "scanning": True, "testCount": None, "completedCount": None}) + full_scan(target_dir, results_dir, results_dir_ref) + tests = get_all_tests() + completed = len([t for t in tests if t.get("completed")]) + return jsonify({"ok": True, "scanning": False, "testCount": len(tests), "completedCount": completed}) @app.post("/api/config/rescan-results") @@ -310,18 +277,10 @@ def rescan_results_route(): if is_scan_in_progress(): return jsonify({"ok": True, "scanning": True, "testCount": None, "completedCount": None}) - _start_results_scan_background(results_dir, results_dir_ref, "config") - return jsonify({"ok": True, "scanning": True, "testCount": None, "completedCount": None}) - - -@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) + scan_results_only(results_dir, results_dir_ref) + tests = get_all_tests() + completed = len([t for t in tests if t.get("completed")]) + return jsonify({"ok": True, "scanning": False, "testCount": len(tests), "completedCount": completed}) @app.get("/") @@ -351,9 +310,6 @@ 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_watching(target_dir, results_dir) - print("[server] Watching for changes.") else: print("[server] No directories configured -> open the dashboard settings to get started.") diff --git a/server/dashboard.db b/server/dashboard.db index 86b8ee158c07c5c3a8c53df6fa6a6e3dc64b89ce..bff3e1c45fc5e3ee8ecf9c479faae7be7d1cf87d 100644 GIT binary patch delta 27954 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 delta 13231 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 diff --git a/server/db_py.py b/server/db_py.py index 765d167..d956262 100644 --- a/server/db_py.py +++ b/server/db_py.py @@ -248,6 +248,19 @@ def reset_by_file_id_and_device(test_id, device): ) +def reset_all_results_state(): + with _tx(): + _conn.execute( + """ + UPDATE tests + SET completed = 0, + completed_at = NULL, + duration_seconds = NULL, + tput_results = NULL + """ + ) + + def clear_tests(): with _tx(): _conn.execute("DELETE FROM tests") diff --git a/server/requirements.txt b/server/requirements.txt index 3be0ebb..bad148b 100644 --- a/server/requirements.txt +++ b/server/requirements.txt @@ -1,4 +1,3 @@ Flask>=3.0.0,<4.0.0 Flask-Cors>=4.0.1,<5.0.0 -watchdog>=4.0.1,<5.0.0 smbprotocol>=1.13.0,<2.0.0 diff --git a/server/scanner.py b/server/scanner.py index f71639d..84cbf36 100644 --- a/server/scanner.py +++ b/server/scanner.py @@ -7,6 +7,7 @@ from db_py import ( clear_tests, get_config, mark_completed, + reset_all_results_state, update_all_p2p_coe_pairs_sql, update_all_p3p_pairs_sql, upsert_test, @@ -289,6 +290,9 @@ def scan_results_only(results_dir, results_dir_ref): print(f"[scanner] results-only scan dir : {results_dir}") print(f"[scanner] results-only scan dir ref : {results_dir_ref}") + # Rebuild completion state from current result folders so deletions are reflected. + reset_all_results_state() + scan_results(results_dir) scan_results(results_dir_ref) coe_pairs = update_all_p2p_coe_pairs_sql() diff --git a/server/sse_py.py b/server/sse_py.py deleted file mode 100644 index 02b7681..0000000 --- a/server/sse_py.py +++ /dev/null @@ -1,32 +0,0 @@ -import json -import queue -import threading - -_clients = set() -_clients_lock = threading.Lock() - - -def stream_events(): - q = queue.Queue() - with _clients_lock: - _clients.add(q) - - try: - yield 'data: {"type":"connected"}\n\n' - while True: - try: - payload = q.get(timeout=20) - yield f"data: {payload}\n\n" - except queue.Empty: - yield ": heartbeat\n\n" - finally: - with _clients_lock: - _clients.discard(q) - - -def broadcast(data): - payload = json.dumps(data) - with _clients_lock: - clients = list(_clients) - for q in clients: - q.put_nowait(payload) diff --git a/server/watcher.py b/server/watcher.py deleted file mode 100644 index 839d06a..0000000 --- a/server/watcher.py +++ /dev/null @@ -1,270 +0,0 @@ -import os -import threading - -from watchdog.events import FileSystemEventHandler -from watchdog.observers import Observer - -from db_py import reset_by_file_id_and_device -from scanner import ( - full_scan, - parse_deleted_result_dir_name, - process_result_dir, - _is_unc_path, - _normalize_input_path, -) -from sse_py import broadcast - -_target_observer = None -_results_observers = [] -_scan_timer = None -_scan_lock = threading.Lock() - - -def _path_exists(path): - """Check if path exists, handling UNC paths.""" - if _is_unc_path(path): - try: - import smbclient - return smbclient.path.isdir(path) - except Exception: - return False - return os.path.isdir(path) - - -def _normalize_watcher_path(path): - """Normalize path for watcher comparison, handling UNC paths.""" - normalized = _normalize_input_path(path) - if not _is_unc_path(normalized): - normalized = os.path.normcase(os.path.abspath(normalized)) - return normalized - - -def _schedule_full_scan(target_dir, results_dir, delay_seconds=1.0): - global _scan_timer - with _scan_lock: - if _scan_timer: - _scan_timer.cancel() - - def run_scan(): - try: - full_scan(target_dir, results_dir) - broadcast({"type": "update"}) - except Exception as exc: - print(f"[watcher] fullScan error: {exc}") - - _scan_timer = threading.Timer(delay_seconds, run_scan) - _scan_timer.daemon = True - _scan_timer.start() - - -class _TargetHandler(FileSystemEventHandler): - def __init__(self, target_dir, results_dir): - self.target_dir = os.path.normcase(os.path.abspath(target_dir)) - self.results_dir = results_dir - - def _is_target_test_file(self, path): - normalized = os.path.normcase(os.path.abspath(path)) - relative = os.path.relpath(normalized, self.target_dir) - parts = relative.split(os.sep) - if len(parts) != 2: - return False - - filename = parts[-1] - return filename.endswith(".ini") and not filename.startswith("GLOBAL") - - def on_created(self, event): - if not event.is_directory and self._is_target_test_file(event.src_path): - _schedule_full_scan(self.target_dir, self.results_dir) - - def on_deleted(self, event): - if not event.is_directory and self._is_target_test_file(event.src_path): - _schedule_full_scan(self.target_dir, self.results_dir) - - def on_moved(self, event): - if event.is_directory: - return - - # On Windows, create/delete in Explorer can show up as move/rename events. - if self._is_target_test_file(event.src_path) or self._is_target_test_file(event.dest_path): - _schedule_full_scan(self.target_dir, self.results_dir) - - -class _ResultsHandler(FileSystemEventHandler): - def __init__(self, results_dir): - self.results_dir = _normalize_watcher_path(results_dir) - - def _is_direct_child_dir(self, path): - try: - if _is_unc_path(path): - parent = _normalize_watcher_path(os.path.dirname(path)) - else: - parent = os.path.normcase(os.path.abspath(os.path.dirname(path))) - return parent == self.results_dir - except Exception: - return False - - def _is_file_under_result_child(self, path): - try: - if _is_unc_path(path): - parent_dir = _normalize_watcher_path(os.path.dirname(path)) - grandparent = _normalize_watcher_path(os.path.dirname(parent_dir)) - else: - parent_dir = os.path.normcase(os.path.abspath(os.path.dirname(path))) - grandparent = os.path.normcase(os.path.abspath(os.path.dirname(parent_dir))) - return grandparent == self.results_dir - except Exception: - return False - - def _result_child_name_for_file(self, path): - return os.path.basename(os.path.dirname(path)) - - def _result_child_name_for_dir(self, path): - return os.path.basename(path) - - def on_created(self, event): - # On Windows, is_directory may be False even for directories (timing issue), - # so check _is_direct_child_dir regardless of the flag. - if self._is_direct_child_dir(event.src_path): - dir_name = self._result_child_name_for_dir(event.src_path) - process_result_dir(self.results_dir, dir_name) - broadcast({"type": "update"}) - return - - if self._is_file_under_result_child(event.src_path): - dir_name = self._result_child_name_for_file(event.src_path) - process_result_dir(self.results_dir, dir_name) - broadcast({"type": "update"}) - - def on_deleted(self, event): - # On Windows, when a directory is deleted watchdog may report is_directory=False - # because os.path.isdir() returns False by the time the event is processed. - # Check _is_direct_child_dir first regardless of the is_directory flag. - if self._is_direct_child_dir(event.src_path): - dir_name = self._result_child_name_for_dir(event.src_path) - test_id, device = parse_deleted_result_dir_name(dir_name) - if test_id: - reset_by_file_id_and_device(test_id, device) - broadcast({"type": "update"}) - return - - if self._is_file_under_result_child(event.src_path): - dir_name = self._result_child_name_for_file(event.src_path) - process_result_dir(self.results_dir, dir_name) - broadcast({"type": "update"}) - - def on_moved(self, event): - src_in_root = self._is_direct_child_dir(event.src_path) - dst_in_root = self._is_direct_child_dir(event.dest_path) - - # Handle directory-level moves regardless of is_directory flag (Windows timing issue). - if src_in_root or dst_in_root: - # Result directory moved out (includes Recycle Bin delete on Windows). - if src_in_root and not dst_in_root: - old_name = self._result_child_name_for_dir(event.src_path) - test_id, device = parse_deleted_result_dir_name(old_name) - if test_id: - reset_by_file_id_and_device(test_id, device) - broadcast({"type": "update"}) - return - - # Result directory moved in. - if dst_in_root and not src_in_root: - new_name = self._result_child_name_for_dir(event.dest_path) - process_result_dir(self.results_dir, new_name) - broadcast({"type": "update"}) - return - - # Result directory renamed within root. - if src_in_root and dst_in_root: - old_name = self._result_child_name_for_dir(event.src_path) - new_name = self._result_child_name_for_dir(event.dest_path) - test_id, device = parse_deleted_result_dir_name(old_name) - if test_id: - reset_by_file_id_and_device(test_id, device) - process_result_dir(self.results_dir, new_name) - broadcast({"type": "update"}) - return - - src_file_in_result = self._is_file_under_result_child(event.src_path) - dst_file_in_result = self._is_file_under_result_child(event.dest_path) - - if src_file_in_result: - src_name = self._result_child_name_for_file(event.src_path) - process_result_dir(self.results_dir, src_name) - - if dst_file_in_result: - dst_name = self._result_child_name_for_file(event.dest_path) - if not src_file_in_result or src_name != dst_name: - process_result_dir(self.results_dir, dst_name) - - if src_file_in_result or dst_file_in_result: - broadcast({"type": "update"}) - - -def stop_watching(): - global _target_observer, _results_observers, _scan_timer - - if _scan_timer: - _scan_timer.cancel() - _scan_timer = None - - if _target_observer: - _target_observer.stop() - _target_observer.join(timeout=2) - _target_observer = None - - for obs in _results_observers: - try: - obs.stop() - obs.join(timeout=2) - except Exception as e: - print(f"[watcher] error stopping results observer: {e}") - _results_observers.clear() - - -def start_watching(target_dir, results_dir, results_dir_ref=None): - global _target_observer, _results_observers - - stop_watching() - if not target_dir or not results_dir: - print("[watcher] target_dir/results_dir not configured; watcher disabled.") - return - - if not _path_exists(target_dir): - print(f"[watcher] target_dir does not exist or is not accessible: {target_dir}") - print("[watcher] watcher disabled until valid paths are configured.") - return - - if not _path_exists(results_dir): - print(f"[watcher] results_dir does not exist or is not accessible: {results_dir}") - print("[watcher] watcher disabled until valid paths are configured.") - return - - try: - _target_observer = Observer() - _target_observer.schedule(_TargetHandler(target_dir, results_dir), target_dir, recursive=True) - _target_observer.daemon = True - _target_observer.start() - print(f"[watcher] target observer started for: {target_dir}") - - # Watch primary results directory - results_obs = Observer() - results_obs.schedule(_ResultsHandler(results_dir), results_dir, recursive=True) - results_obs.daemon = True - results_obs.start() - _results_observers.append(results_obs) - print(f"[watcher] results observer started for: {results_dir}") - - # Watch reference results directory if provided and different - if results_dir_ref and results_dir_ref != results_dir and _path_exists(results_dir_ref): - ref_obs = Observer() - ref_obs.schedule(_ResultsHandler(results_dir_ref), results_dir_ref, recursive=True) - ref_obs.daemon = True - ref_obs.start() - _results_observers.append(ref_obs) - print(f"[watcher] results observer started for reference: {results_dir_ref}") - elif results_dir_ref and results_dir_ref != results_dir: - print(f"[watcher] reference results_dir does not exist or is not accessible: {results_dir_ref}") - except Exception as exc: - print(f"[watcher] failed to start watchers: {exc}") - stop_watching()