2026-05-20 11:52:18 -04:00
# Implementation Plan — Test Dashboard
2026-06-02 11:59:31 -04:00
> **Status as of June 2026**: fully implemented and running. Backend is Python (Flask) with JWT authentication and role-based access control.
2026-05-26 14:36:34 -04:00
2026-05-20 11:52:18 -04:00
## 1. Overview
A web dashboard that monitors test execution progress by comparing a **target tests directory ** (what should run) against a **results directory ** (what has run). The backend runs on one machine with access to both directories; the frontend is accessible from any device on the network.
---
## 2. Tech Stack
2026-05-26 14:36:34 -04:00
| Layer | Technology | Notes |
2026-05-20 11:52:18 -04:00
|---|---|---|
2026-05-26 14:36:34 -04:00
| Frontend | React + Vite | Dev server proxies `/api` → `localhost:3001` |
| Backend | Python 3 + Flask | Replaced original Node.js/Express plan |
2026-06-02 11:59:31 -04:00
| Auth | JWT (PyJWT) | Bearer token auth with `admin` and `viewer` roles |
2026-05-26 14:36:34 -04:00
| Database | SQLite (via `sqlite3` stdlib) | WAL mode; single persistent connection with RLock |
2026-06-02 11:59:31 -04:00
| Frontend state | TanStack React Query | Query cache + manual invalidation after mutations |
2026-05-26 14:36:34 -04:00
| UI | TailwindCSS v4 | Via `@tailwindcss/vite` plugin |
2026-05-20 11:52:18 -04:00
---
## 3. Project Structure
```
2026-05-26 14:36:34 -04:00
test_house_dashboard/
├── DESIGNPLAN.md
├── IMPLEMENTATION_PLAN.md
├── CLAUDE.md ← project context for AI assistants
│
├── dashboard/ ← React frontend
│ ├── vite.config.js – proxy /api → localhost:3001
│ ├── index.html
2026-05-20 11:52:18 -04:00
│ ├── package.json
2026-05-26 14:36:34 -04:00
│ └── src/
2026-06-02 11:59:31 -04:00
│ ├── App.jsx – root layout, auth gate, modal settings entry
2026-05-26 14:36:34 -04:00
│ ├── main.jsx
│ ├── components/
│ │ ├── StatCard.jsx – metric card (label / value / sub)
│ │ ├── CompletionBar.jsx – progress bar
│ │ ├── TestTable.jsx – filterable test list table
│ │ ├── FilterPanel.jsx – dropdown tag filters
│ │ ├── TimeDisplay.jsx – elapsed / estimated time display
│ │ ├── StatusBadge.jsx – completed / pending indicator
2026-06-02 11:59:31 -04:00
│ │ └── ConfigModal.jsx – settings modal (dirs + avg time overrides)
2026-05-26 14:36:34 -04:00
│ ├── hooks/
2026-06-02 11:59:31 -04:00
│ │ ├── useAuth.js – login session state + role checks
│ │ ├── useStats.js – fetches stats
2026-05-26 14:36:34 -04:00
│ │ ├── useTests.js – fetches test list
│ │ └── useConfig.js – fetches/saves config
│ └── lib/
2026-06-02 11:59:31 -04:00
│ └── api.js – fetch wrapper with JWT header injection
2026-05-20 11:52:18 -04:00
│
2026-05-26 14:36:34 -04:00
└── server/ ← Python backend
2026-06-02 11:59:31 -04:00
├── app.py – Flask app, JWT auth, role guards, bootstrap(), __main__
2026-05-26 14:36:34 -04:00
├── db_py.py – SQLite schema, query helpers, config CRUD
├── scanner.py – full_scan(), scan_targets(), scan_results(), process_result_dir()
├── parser.py – parse_target_filename(), parse_result_filename(), etc.
├── requirements.txt
├── dashboard.db – SQLite database (auto-created)
└── .venv/ – Python virtual environment
2026-05-20 11:52:18 -04:00
```
---
## 4. File ID & Tag Parsing
### 4.1 File ID & Completion Matching
A **test is completed ** when its **file ID ** appears in any result directory name.
**File ID ** = the test case code segment extracted from the filename — the segment matching the pattern `R\d+[A-Z0-9]+` (e.g., `R2COERXAC003` , `R5P2PRXAX012` ).
**Target files ** live inside subdirectories of the target directory. All target files use the prefix `TC_WIFI_` and extension `.ini` . Each parent directory also contains a `GLOBAL.ini` file that must be **skipped ** by the scanner:
```
TP_WIFI_ATT_CDR_GRP1_ROT1_CGW452_WNC_REGRESSION/
GLOBAL.ini ← skip
TC_WIFI_COE_CGW452_R2COERXAC003_TPT3E_RSSI70_STA4_2GHZ_CH1_BW20_TCP_MIMOFD_SONFD_MESHFD_LPI_UL.ini
↑
2026-05-21 14:53:08 -04:00
test_id = R2COERXAC003
2026-05-20 11:52:18 -04:00
```
Scanner filter: include only files where `filename.startsWith('TC_WIFI_') && filename.endsWith('.ini')` .
**Result directories ** live as top-level subdirectories of the results directory. The directory name contains the same file ID:
```
COE_CGW452_R2COERXAC003_TPT3E_RSSI70_STA4_2GHZ_CH1_BW20_TCP_MIMOFD_SONFD_MESHFD_LPI_UL/
↑
2026-05-21 14:53:08 -04:00
test_id = R2COERXAC003
COE_CGW452_R2COERXAC003_..._2026-05-16-07-30-14 ← target log file (contains test_id in name)
2026-05-20 11:52:18 -04:00
other_file.txt ← ignored
...other files
```
2026-05-21 14:53:08 -04:00
A test is **completed ** when a result directory name contains the target's `test_id` .
2026-05-20 11:52:18 -04:00
2026-05-21 14:53:08 -04:00
**Duration per test ** : the result directory may contain multiple `.txt` files. Only the `.txt` file(s) whose name includes the `test_id` are scanned. All other `.txt` files are ignored.
2026-05-20 11:52:18 -04:00
2026-05-21 14:53:08 -04:00
Filter: `filename.endsWith('.txt') && filename.includes(test_id)`
2026-05-20 11:52:18 -04:00
Scan the matched file for the line:
```
[2026-05-16 09:21:02,105 INFO] Elapsed time : 1:51:00.660866
```
Regex: `/\[.*?INFO\]\s+Elapsed time\s*:\s*([\d]+:[\d]{2}:[\d]{2}\.[\d]+)/`
The captured group (`1:51:00.660866` ) is parsed as `H:MM:SS.microseconds` and converted to total seconds stored in `duration_seconds` . If no matching line is found, `duration_seconds` is left `NULL` and excluded from the elapsed sum and avg calculations.
2026-05-21 14:53:08 -04:00
**Re-runs ** : if multiple `.txt` files match the `test_id` filter (re-run logs), use the one with the **latest ** timestamp in its filename for both `completed_at` and `duration_seconds` .
2026-05-20 11:52:18 -04:00
### 4.2 Tag Parsing
Tags are parsed from two sources:
**From the parent folder name ** (e.g., `TP_WIFI_ATT_CDR_GRP1_ROT1_CGW452_WNC_REGRESSION` ):
| Tag | Pattern | Example |
|---|---|---|
| Rotation | `ROT\d+` | `ROT1` |
| Device | `CGW\d+` | `CGW452` |
**From the target filename ** (after stripping `TC_WIFI_` prefix and `.ini` extension):
```
COE_CGW452_R2COERXAC003_TPT3E_RSSI70_STA4_2GHZ_CH1_BW20_..._UL
↑
first segment = interference type
```
| Tag | Extraction | Example |
|---|---|---|
| **Interference ** | First segment — fixed enum: `COE` , `P2P` , `P3P` | `COE` |
| Device | Regex `CGW\d+` | `CGW452` |
| File ID | Regex `R\d+[A-Z0-9]+` | `R2COERXAC003` |
| Test Point | Regex `TPT\w+` | `TPT3E` |
| RSSI | Regex `RSSI\d+` | `RSSI70` |
| Station | Regex `STA\d+` | `STA4` |
| Band | Regex `\dGHZ` | `2GHZ` |
| Channel | Regex `CH\d+` | `CH1` |
| Bandwidth | Regex `BW\d+` | `BW20` |
| Direction | Last segment — fixed enum: `UL` , `DL` , `BI` | `UL` |
**Completion Timestamp ** : extracted from the timestamped result file name inside the matching result directory (e.g., `..._2026-05-16-07-30-14` → `2026-05-16 07:30:14` ).
**Interference type ** is also validated against the result directory name's first segment (`COE_...` , `P2P_...` , `P3P_...` ) as a cross-check.
---
## 5. Database Schema
``` sql
-- Stores one row per target test file
CREATE TABLE tests (
id TEXT PRIMARY KEY , -- full derived ID (filename without prefix/ext)
2026-05-21 14:53:08 -04:00
test_id TEXT NOT NULL , -- short test case code, e.g. R2COERXAC003
2026-05-20 11:52:18 -04:00
parent_dir TEXT NOT NULL , -- parent folder name (TP_WIFI_...)
filename TEXT NOT NULL , -- original .ini filename
completed INTEGER NOT NULL DEFAULT 0 , -- 0 or 1
completed_at TEXT , -- ISO timestamp from result file name
duration_seconds REAL , -- per-test duration in seconds (from result file or fs timestamps)
-- parsed tags
interference TEXT , -- COE | P2P | P3P
device TEXT , -- CGW452 | CGW453
rotation TEXT ,
test_point TEXT ,
station TEXT ,
band TEXT ,
channel TEXT ,
bandwidth TEXT ,
rssi TEXT ,
direction TEXT ,
created_at TEXT NOT NULL DEFAULT ( datetime ( ' now ' ) )
) ;
-- Stores configuration: directory paths AND manual avg time overrides
CREATE TABLE config (
key TEXT PRIMARY KEY ,
value TEXT NOT NULL
) ;
2026-06-02 11:59:31 -04:00
-- Stores dashboard users
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT ,
username TEXT NOT NULL UNIQUE ,
password_hash TEXT NOT NULL ,
role TEXT NOT NULL , -- admin | viewer
is_active INTEGER NOT NULL DEFAULT 1 ,
created_at TEXT NOT NULL DEFAULT ( datetime ( ' now ' ) )
) ;
2026-05-20 11:52:18 -04:00
-- Config keys:
-- target_dir – path to target tests directory
-- results_dir – path to results directory
2026-06-02 11:59:31 -04:00
-- results_dir_ref – optional reference results directory
2026-05-20 11:52:18 -04:00
-- avg_time_coe – manual avg seconds per COE test (NULL = use calculated)
-- avg_time_p2p – manual avg seconds per P2P test (NULL = use calculated)
-- avg_time_p3p – manual avg seconds per P3P test (NULL = use calculated)
```
---
## 6. Backend API
### Endpoints
| Method | Path | Description |
|---|---|---|
2026-06-02 11:59:31 -04:00
| `POST` | `/api/auth/login` | Login with username/password; returns JWT + user role |
| `GET` | `/api/auth/me` | Resolve current authenticated user from JWT |
| `GET` | `/api/users` | Admin-only list of users |
| `POST` | `/api/users` | Admin-only create user (`admin` / `viewer` ) |
2026-05-20 11:52:18 -04:00
| `GET` | `/api/tests` | All tests; supports query filters (see below) |
| `GET` | `/api/stats` | Aggregated stats object |
2026-06-02 11:59:31 -04:00
| `GET` | `/api/scan-status` | Returns active scan state |
2026-05-20 11:52:18 -04:00
| `GET` | `/api/config` | Current directory paths and avg time overrides |
2026-06-02 11:59:31 -04:00
| `POST` | `/api/config` | Save config and optionally trigger full re-scan |
| `POST` | `/api/config/rescan` | Force full re-scan using saved config |
| `POST` | `/api/config/rescan-results` | Re-process only results directories |
Auth/role rules:
- All dashboard data routes require a valid JWT.
- `/api/config*` and `/api/users*` are admin-only.
- Frontend stores JWT in localStorage key `dashboard_jwt` and sends `Authorization: Bearer <token>` .
2026-05-20 11:52:18 -04:00
### `GET /api/tests` Query Parameters
```
?completed=true|false
&interference=COE|P2P|P3P
&device=CGW452
&rotation=ROT1
&testPoint=TPT3E
&station=STA4
&band=2GHZ
&channel=CH1
&bandwidth=BW20
&rssi=RSSI70
&direction=UL
```
### `GET /api/stats` Response Shape
``` json
{
"overall" : {
"total" : 200 ,
"completed" : 120 ,
"completionRate" : 0.60
} ,
"cgw452" : {
"total" : 100 ,
"completed" : 70 ,
"completionRate" : 0.70
} ,
"cgw453" : {
"total" : 100 ,
"completed" : 50 ,
"completionRate" : 0.50
} ,
"timing" : {
"elapsedSeconds" : 172800 ,
"estimatedRemainingSeconds" : 115200 ,
"byType" : {
"COE" : {
"avgSeconds" : 450 ,
"avgSource" : "calculated" ,
"remaining" : 80
} ,
"P2P" : {
"avgSeconds" : 300 ,
"avgSource" : "manual" ,
"remaining" : 50
} ,
"P3P" : {
"avgSeconds" : 600 ,
"avgSource" : "calculated" ,
"remaining" : 30
}
}
}
}
```
**Timing logic ** :
- `elapsedSeconds` = `SUM(duration_seconds)` for all completed tests (sum of individual test durations, not wall-clock)
- For each interference type (COE, P2P, P3P):
- `avgSeconds` = `AVG(duration_seconds) WHERE interference = type AND completed = 1`
- If no completed tests exist for that type, `avgSeconds` = value from `config` table (`avg_time_coe` / `avg_time_p2p` / `avg_time_p3p` ); `avgSource` = `"manual"`
- If a manual override is set in config even when calculated data exists, the manual value takes precedence; `avgSource` = `"manual_override"`
- `estimatedRemainingSeconds` = `(avgCOE × COE_remaining) + (avgP2P × P2P_remaining) + (avgP3P × P3P_remaining)`
- If any type has remaining tests but no avg time (calculated or manual), that type contributes `null` to the sum and the UI flags it as **input required **
## 7. Real-time Updates
2026-06-02 11:59:31 -04:00
Current implementation does not use SSE/watchdog.
2026-05-26 14:36:34 -04:00
2026-06-02 11:59:31 -04:00
- Stats/tests/config are fetched via React Query.
- Mutations (`saveConfig` , `Save Times` , `Save Results` ) manually invalidate affected query keys.
- Full scans/rescans run only when triggered by settings actions.
2026-05-20 11:52:18 -04:00
---
## 8. Frontend Components
### 8.1 Layout
```
┌──────────────────────────────────────────────────────────────┐
2026-06-02 11:59:31 -04:00
│ Test Dashboard [⚙ Settings] │
2026-05-20 11:52:18 -04:00
├──────────┬──────────┬──────────┬────────────┬───────────────┤
│ Overall │ CGW452 │ CGW453 │ Time │ Est. Remaining │
│ 60% │ 70% │ 50% │ Elapsed │ │
│ 120/200 │ 70/100 │ 50/100 │ 48h 0m │ 32h 0m │
├──────────┴──────────┴──────────┴────────────┴───────────────┤
│ ⚠ No P2P results yet — avg time required for estimate │
│ COE avg: 7m 30s (calc) P2P avg: [___] min P3P avg: 10m │
├──────────────────────────────────────────────────────────────┤
│ [Completed ▾] [Interference ▾] [Device ▾] [Band ▾] ... │
├──────────────────────────────────────────────────────────────┤
│ File ID │ Type │ Device │ Rotation │ TP │ ... │ Status │
│ R2COERXAC003│ COE │ CGW452 │ ROT1 │ TPT3E│ ... │ ✓ │
│ R5P2PRXAX012│ P2P │ CGW452 │ ROT1 │ TPT1C│ ... │ ○ │
└──────────────────────────────────────────────────────────────┘
```
2026-06-02 11:59:31 -04:00
**Auth gate ** : App shows a sign-in screen first. Dashboard renders only when authenticated. Settings button appears only for admin users.
2026-05-20 11:52:18 -04:00
### 8.2 Component Breakdown
| Component | Responsibility |
|---|---|
| `StatCard` | Displays a metric (label, value, sub-value); used for completion rate + counts |
| `CompletionBar` | Progress bar with percentage label |
| `TimeDisplay` | Formats seconds into `Xh Ym` ; shows elapsed and estimated remaining |
| `FilterPanel` | Dropdown filters for each tag including Interference; maintains filter state |
2026-06-02 11:59:31 -04:00
| `TestTable` | Filtered table of tests with status and parsed tags |
| `ConfigModal` | Admin settings modal for directories, avg overrides, and SMB credentials |
2026-05-20 11:52:18 -04:00
| `StatusBadge` | Green checkmark or grey circle for completed/pending |
### 8.3 Settings / Config
A gear icon opens `ConfigModal` with two sections:
2026-06-02 11:59:31 -04:00
1. **Directories ** — target/results paths plus optional reference results path and exclusion rules.
- On Save All, backend updates config and triggers full re-scan when directory/exclusion fields changed.
2026-05-20 11:52:18 -04:00
2. **Avg Time Overrides ** — three number inputs (COE, P2P, P3P) in minutes. Each shows:
2026-06-02 11:59:31 -04:00
- Manual override values converted to seconds and saved in config.
3. **SMB Credentials ** — domain, username, password for network share access.
4. **Action buttons ** :
- Save All
- Save Times
- Save Results (`/api/config/rescan-results` )
2026-05-20 11:52:18 -04:00
All config values are POSTed to `POST /api/config` as key/value pairs and persisted in SQLite.
---
## 9. Vite Proxy Configuration
2026-05-26 14:36:34 -04:00
`vite.config.js` proxies all `/api` requests to the Python backend:
2026-05-20 11:52:18 -04:00
``` js
server : {
2026-05-26 14:36:34 -04:00
host : true , // expose on LAN so other devices can reach the dev server
2026-05-20 11:52:18 -04:00
proxy : {
'/api' : 'http://localhost:3001'
}
}
```
2026-05-26 14:36:34 -04:00
In production, Flask serves the built React `dist/` as static files via `send_from_directory` . Build with `npm run build` inside `dashboard/` .
2026-05-20 11:52:18 -04:00
---
2026-05-26 14:36:34 -04:00
## 10. Implementation Status
### Completed
2026-06-02 11:59:31 -04:00
- [x] Python Flask backend (`app.py` ) with JWT auth and role guards
2026-05-26 14:36:34 -04:00
- [x] SQLite schema, WAL mode, thread-safe helpers (`db_py.py` )
2026-06-02 11:59:31 -04:00
- [x] Users table + user management APIs (admin-only)
- [x] Default user seeding from environment (`DEFAULT_ADMIN_*` , `DEFAULT_VIEWER_*` )
- [x] Tag parsing and scan logic (`parser.py` , `scanner.py` )
- [x] Full scan / rescan / results-only rescan flows
- [x] Frontend auth flow (login screen, JWT persistence, role-based UI)
- [x] Config modal (directories, avg overrides, SMB credentials)
2026-05-26 14:36:34 -04:00
- [x] Vite dev proxy + Flask static serving for production
- [x] Config migrated from `config.json` → SQLite on first run
### Remaining / Future
2026-06-02 11:59:31 -04:00
- [ ] Admin UI for user create/list (currently API-only)
- [ ] Invalid test results
- [ ] Watchdog automatic updates
2026-05-26 14:36:34 -04:00
- [ ] Test detail view (click a row to see tput/RSSI breakdown)
2026-06-02 11:59:31 -04:00
- [ ] Production deployment docs (service startup, backup, secrets handling)
2026-05-20 11:52:18 -04:00
5. Build `StatCard` , `CompletionBar` , `TimeDisplay` , `StatusBadge`
6. Build `App.jsx` layout with stats row
### Phase 3 — Test List & Filters
1. Build `FilterPanel` with controlled filter state
2. Build `TestTable` with all columns + sort
3. Wire filters to `GET /api/tests` query params
4. Add `ConfigModal` for directory path configuration
### Phase 4 — Real-time & Polish
1. Connect SSE stream in `useTests` / `useStats` to auto-invalidate queries
2. Add manual refresh button
3. Add loading skeletons and error states
4. Mobile-responsive layout adjustments
5. Test on secondary device over local network
### Phase 5 — SSO (Future)
- Add authentication middleware to the Express server (e.g., `passport.js` with an OAuth/OIDC provider)
- Protect all `/api` routes and the static frontend behind the auth middleware
- Add session management (`express-session` + a session store)
---
## 11. Key Dependencies
### Server (`server/package.json`)
``` json
{
"dependencies" : {
"better-sqlite3" : "^9.x" ,
"chokidar" : "^4.x" ,
"cors" : "^2.x" ,
"dotenv" : "^16.x" ,
"express" : "^4.x"
}
}
```
### Client (`dashboard/package.json` additions)
``` json
{
"dependencies" : {
"@tanstack/react-query" : "^5.x" ,
"axios" : "^1.x" ,
"lucide-react" : "^0.x" ,
"tailwindcss" : "^4.x"
}
}
```
---
## 12. Resolved Design Decisions
| # | Decision |
|---|---|
| 1 | Target prefix is always `TC_WIFI_` , extension always `.ini` . `GLOBAL.ini` present in each parent directory is skipped by the scanner. |
| 2 | Interference types are a fixed enum: `COE` , `P2P` , `P3P` . |
| 3 | Duration parsed from result `.txt` log line `[... INFO] Elapsed time : H:MM:SS.ffffff` . Tests with no parseable line excluded from avg/sum. |
| 4 | If multiple result log files exist in a result directory (re-run), use the file with the **latest ** timestamp in its name. |
| 5 | Backend runs on port **3001 ** . |
| 6 | When a result directory is deleted, the matching test is reset to pending (`completed=0` , `completed_at=NULL` , `duration_seconds=NULL` ). |