Files
test_dashboard/CLAUDE.md
T
2026-05-26 14:36:34 -04:00

145 lines
6.6 KiB
Markdown

# 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_<tags>.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).