6.6 KiB
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)
cd server
.\.venv\Scripts\Activate.ps1 # activate venv
python app.py # starts Flask on port 3001
Frontend (dev)
cd dashboard
npm run dev # Vite dev server; proxies /api → localhost:3001
Frontend (production build)
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 namedGLOBAL.iniare skipped. test_id: segment matchingR\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_filenametest_id,parent_dir,filename,interference,device,rotation,test_point,station,band,channel,bandwidth,rssi,directioncompleted(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_diravg_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:
-
_TargetHandler— watchestarget_dirrecursively. Any add/delete/move of aTC_WIFI_*.inifile schedules a debounced full scan (1 second timer, cancels and restarts on rapid changes). -
_ResultsHandler— watchesresults_dirrecursively. 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
- Directory created →
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 aqueue.Queue.broadcast()callsput_nowait(payload)on all queues.stream_events()is a generator yieldingf"data: {payload}\n\n"(real newlines —\nnot\\n).useStats.js: createsnew EventSource('/api/events')once on mount. On{"type":"update"}message, invalidates React Query keys['stats']and['tests'].- Vite dev proxy forwards
/api/eventsto Flask, keeping the SSE connection alive.
Known issues / gotchas
config.jsoninserver/is a legacy file used for a one-time migration to SQLite on first run. It is no longer needed oncedashboard.dbexists.- The backend entrypoint is
app.py, notmain.py. - Flask dev server (
app.run) is used directly — no gunicorn/waitress configured yet. - Port is
3001(configurable viaPORTenv var).