# CLAUDE.md — Project Context for AI Assistants ## What this project is A real-time web dashboard that tracks WiFi test execution progress. It compares a **target tests directory** (`.ini` files defining every test that must run) against a **results directory** (folders created when each test completes). The dashboard shows completion rates, elapsed time, estimated time remaining, and a filterable table of every test. --- ## How to run ### Backend (Python) ```powershell cd server .\.venv\Scripts\Activate.ps1 # activate venv python app.py # starts Flask on port 3001 ``` ### Frontend (dev) ```powershell cd dashboard npm run dev # Vite dev server; proxies /api → localhost:3001 ``` ### Frontend (production build) ```powershell cd dashboard npm run build # outputs to dashboard/dist/ # Flask serves dist/ automatically when app.py is running ``` --- ## Tech stack | Layer | Technology | |---|---| | Frontend | React 18 + Vite, TailwindCSS v4 (`@tailwindcss/vite`), TanStack React Query | | Backend | Python 3, Flask, flask-cors | | Database | SQLite via Python stdlib `sqlite3` (WAL mode, single persistent connection + RLock) | | File watching | `watchdog` Python library | | Real-time | Server-Sent Events (SSE) — one-way push from backend to browser | --- ## Key files ### Backend (`server/`) | File | Purpose | |---|---| | `app.py` | Flask app, all route handlers, `bootstrap()`, `__main__` | | `db_py.py` | SQLite schema init, all query helpers (`upsert_test`, `mark_completed`, `reset_by_file_id_and_device`, `get_config`, `set_config`, etc.) | | `scanner.py` | `full_scan()`, `scan_targets()`, `scan_results()`, `process_result_dir()`, `parse_deleted_result_dir_name()` | | `parser.py` | `parse_target_filename()`, `parse_result_filename()`, `parse_timestamp()`, `parse_elapsed_time()`, `parse_tput_rssi()` | | `watcher.py` | Two watchdog `Observer` instances — `_TargetHandler` and `_ResultsHandler`. `start_watching()` / `stop_watching()` | | `sse_py.py` | Per-client `Queue` registry, `stream_events()` generator, `broadcast(data)` | ### Frontend (`dashboard/src/`) | File | Purpose | |---|---| | `lib/api.js` | Thin `fetch` wrapper; all API calls go through `apiFetch('/path')` | | `hooks/useStats.js` | Fetches `/api/stats`; **owns the SSE `EventSource`**; invalidates `['stats']` and `['tests']` on `update` events | | `hooks/useTests.js` | Fetches `/api/tests` | | `hooks/useConfig.js` | Fetches/saves `/api/config` | | `components/ConfigModal.jsx` | Settings modal: directory paths (always editable) + avg time overrides | | `components/DirectoryBrowser.jsx` | Server-side folder picker using `/api/browse` | --- ## Data model ### How tests are identified - **Target files** live in subdirectories of the target directory. Format: `TC_WIFI_.ini`. Files named `GLOBAL.ini` are skipped. - **`test_id`**: segment matching `R\d+[A-Z0-9]+` (e.g., `R2COERXAX014`) - **Result directories**: top-level subdirectories of the results directory. A test is **completed** when a result directory name contains the same `test_id`. - **Result folder name example**: `COE_CGW453_R2COERXAX014_TPT3E_RSSI70_STA56_2GHZ_CH1_BW20_TCP_MIMOFD_SONFD_MESHFD_LPI_UL` ### Tags parsed from target filename `interference`, `device`, `test_point`, `rssi`, `station`, `band`, `channel`, `bandwidth`, `direction` `rotation` is parsed from the **parent folder name** (segment matching `ROT\d+`). ### Database tables **`tests`** — one row per target `.ini` file: - `id` (TEXT PK) — `parent_dir/base_filename` - `test_id`, `parent_dir`, `filename`, `interference`, `device`, `rotation`, `test_point`, `station`, `band`, `channel`, `bandwidth`, `rssi`, `direction` - `completed` (0/1), `completed_at` (ISO timestamp), `duration_seconds` (REAL) - `tput_results` (JSON array of `{station, tput, dlRssi, ulRssi}`) **`config`** — key/value store: - `target_dir`, `results_dir` - `avg_time_coe`, `avg_time_p2p`, `avg_time_p3p` (seconds as string; NULL = use calculated average) --- ## API routes | Method | Path | Description | |---|---|---| | GET | `/api/tests` | All tests; supports query filters (`completed`, `interference`, `device`, `rotation`, `testPoint`, `station`, `band`, `channel`, `bandwidth`, `rssi`, `direction`) | | GET | `/api/stats` | Aggregated stats: overall, per-device, timing with estimates | | GET | `/api/events` | SSE stream; sends `data: {"type":"update"}\n\n` on any directory change | | GET | `/api/config` | Current config values | | POST | `/api/config` | Update config; triggers full rescan + rewatcher if dirs changed | | POST | `/api/config/rescan` | Force full rescan without changing config | | GET | `/api/browse?path=...` | List subdirectories at a server path (omit `path` for drive roots) | --- ## Watcher architecture Two `watchdog.Observer` instances run in daemon threads: 1. **`_TargetHandler`** — watches `target_dir` recursively. Any add/delete/move of a `TC_WIFI_*.ini` file schedules a **debounced full scan** (1 second timer, cancels and restarts on rapid changes). 2. **`_ResultsHandler`** — watches `results_dir` recursively. Handles events surgically: - Directory created → `process_result_dir()` + broadcast - Directory deleted → `reset_by_file_id_and_device()` + broadcast - Directory moved in/out/renamed → appropriate reset/process + broadcast - File created/deleted inside a result dir → `process_result_dir()` + broadcast ### Critical Windows quirk When a directory is deleted (including Recycle Bin), `watchdog` calls `os.path.isdir()` at event-processing time. The directory is already gone, so it returns `False`, making `event.is_directory = False` even for directory events. **All handlers check `_is_direct_child_dir(path)` directly** — never gate on `event.is_directory` for delete/move cases. --- ## SSE implementation notes - `sse_py.py`: each connected client gets a `queue.Queue`. `broadcast()` calls `put_nowait(payload)` on all queues. `stream_events()` is a generator yielding `f"data: {payload}\n\n"` (real newlines — `\n` not `\\n`). - `useStats.js`: creates `new EventSource('/api/events')` once on mount. On `{"type":"update"}` message, invalidates React Query keys `['stats']` and `['tests']`. - Vite dev proxy forwards `/api/events` to Flask, keeping the SSE connection alive. --- ## Known issues / gotchas - `config.json` in `server/` is a legacy file used for a one-time migration to SQLite on first run. It is no longer needed once `dashboard.db` exists. - The backend entrypoint is `app.py`, not `main.py`. - Flask dev server (`app.run`) is used directly — no gunicorn/waitress configured yet. - Port is `3001` (configurable via `PORT` env var).