Files
test_dashboard/IMPLEMENTATION_PLAN.md
T
Mia.Wu a67815c61a feat: refactor parsing and scanning logic for test files
- Updated `parseFilename` to `parseTargetFilename` and modified its return structure to include `test_id` instead of `file_id`.
- Introduced `parseResultFilename` to extract `test_id` and `device` from result file names.
- Enhanced `fullScan` to separately handle target and results directories, improving clarity and functionality.
- Updated database interactions to use `test_id` instead of `file_id` across various modules.
- Added a new `/rescan` endpoint to trigger a full scan of target and results directories.
- Improved logging and error handling throughout the scanning process.
- Introduced `parseTputRssi` to extract throughput and RSSI data from log files.
2026-05-21 14:53:08 -04:00

466 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Implementation Plan — Test Dashboard
## 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
| Layer | Technology | Rationale |
|---|---|---|
| Frontend | React + Vite (existing) | Already scaffolded |
| Backend | Node.js + Express | Lightweight API + static file serving |
| Real-time | Server-Sent Events (SSE) | Simpler than WebSocket for one-way push |
| Directory watching | chokidar | Cross-platform file system watcher |
| Database | SQLite (via `better-sqlite3`) | Embedded, no separate process, fast reads |
| Frontend state | React Query (TanStack Query) | Cache, refetch, and SSE invalidation |
| UI | TailwindCSS + shadcn/ui | Rapid, consistent component styling |
---
## 3. Project Structure
```
Projects/
├── dashboard/ ← React frontend (existing)
│ ├── src/
│ │ ├── components/
│ │ │ ├── StatCard.jsx metric display card
│ │ │ ├── CompletionBar.jsx progress bar with percentage
│ │ │ ├── TestTable.jsx filterable test list
│ │ │ ├── FilterPanel.jsx tag filter controls
│ │ │ └── TimeDisplay.jsx elapsed / estimated time
│ │ ├── hooks/
│ │ │ ├── useTests.js fetch + SSE subscription
│ │ │ └── useStats.js derived stats from test data
│ │ ├── lib/
│ │ │ └── api.js axios/fetch base client
│ │ ├── App.jsx
│ │ └── main.jsx
│ ├── package.json
│ └── vite.config.js proxy /api → backend port
└── server/ ← NEW: Node.js backend
├── index.js Express app entry point
├── db.js SQLite schema + query helpers
├── watcher.js chokidar setup + change handlers
├── parser.js filename/foldername → tags
├── scanner.js full directory scan on startup
├── sse.js SSE client registry + broadcast
├── routes/
│ ├── tests.js GET /api/tests, GET /api/tests/:id
│ ├── stats.js GET /api/stats
│ ├── config.js GET/POST /api/config (directory paths)
│ ├── browse.js GET /api/browse (server-side directory browser)
│ └── events.js GET /api/events (SSE stream)
├── package.json
└── .env TARGET_DIR, RESULTS_DIR, PORT=3001
```
---
## 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
test_id = R2COERXAC003
```
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/
test_id = R2COERXAC003
COE_CGW452_R2COERXAC003_..._2026-05-16-07-30-14 ← target log file (contains test_id in name)
other_file.txt ← ignored
...other files
```
A test is **completed** when a result directory name contains the target's `test_id`.
**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.
Filter: `filename.endsWith('.txt') && filename.includes(test_id)`
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.
**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`.
### 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)
test_id TEXT NOT NULL, -- short test case code, e.g. R2COERXAC003
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
);
-- Config keys:
-- target_dir path to target tests directory
-- results_dir path to results directory
-- 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 |
|---|---|---|
| `GET` | `/api/tests` | All tests; supports query filters (see below) |
| `GET` | `/api/stats` | Aggregated stats object |
| `GET` | `/api/events` | SSE stream — pushes `update` events on directory change |
| `GET` | `/api/config` | Current directory paths and avg time overrides |
| `POST` | `/api/config` | Set `targetDir`, `resultsDir`, avg time overrides; triggers re-scan |
| `GET` | `/api/browse` | List subdirectories at a given server path (directory picker) |
### `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**
### `GET /api/browse` — Directory Browser
Allows the frontend to navigate the server's local filesystem so users can pick directories without typing paths manually.
**Query parameters**:
- `path` (optional) — absolute path to list. If omitted or empty, returns the filesystem roots (e.g., `C:\`, `D:\` on Windows).
**Response**:
```json
{
"path": "C:\\Tests",
"parent": "C:\\",
"dirs": [
{ "name": "Target", "path": "C:\\Tests\\Target" },
{ "name": "Results", "path": "C:\\Tests\\Results" }
]
}
```
- Only **directories** are returned (no files).
- Hidden directories (names starting with `.`) are excluded.
- If `path` does not exist or is not a directory, returns `400`.
- `parent` is `null` when already at a filesystem root.
---
## 7. Real-time Updates
- The backend registers an SSE endpoint at `GET /api/events`
- chokidar watches both the target and results directories for `add`, `unlink`, and `change` events
- On any change, the backend re-scans the affected path, updates SQLite, and broadcasts an SSE event: `data: {"type":"update"}`
- The React frontend subscribes to the SSE stream; on receiving an `update` event it invalidates and refetches stats and test list via React Query
**`unlink` handling for result directories**: when chokidar detects that a result directory has been deleted, the corresponding test row is reset — `completed = 0`, `completed_at = NULL`, `duration_seconds = NULL`. This ensures the dashboard always reflects the actual state of the filesystem.
---
## 8. Frontend Components
### 8.1 Layout
```
┌──────────────────────────────────────────────────────────────┐
│ Test Dashboard [⟳ Refresh] [⚙ Settings] │
├──────────┬──────────┬──────────┬────────────┬───────────────┤
│ 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│ ... │ ○ │
└──────────────────────────────────────────────────────────────┘
```
**Inline avg-time input**: When a type has remaining tests but zero completed, a warning banner appears with inline input fields for the missing avg time(s). Submitting saves to `POST /api/config` and immediately recalculates the estimate.
### 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 |
| `AvgTimeWarning` | Banner shown when a type has remaining tests but no avg time; contains inline inputs |
| `FilterPanel` | Dropdown filters for each tag including Interference; maintains filter state |
| `TestTable` | Virtualized (react-window) table of tests; columns sortable; File ID as primary ID column |
| `ConfigModal` | Form for directory paths (with picker button) + manual avg time overrides per type |
| `DirectoryBrowser` | Inline folder picker inside `ConfigModal`; navigates the server filesystem via `/api/browse` |
| `StatusBadge` | Green checkmark or grey circle for completed/pending |
### 8.3 Settings / Config
A gear icon opens `ConfigModal` with two sections:
1. **Directories** — target and results directory paths. Each path field has a **Browse** button that opens the `DirectoryBrowser`:
- Starts at the filesystem root (lists available drives on Windows)
- Displays current path as a clickable breadcrumb
- Lists subdirectories; clicking one navigates into it
- **Select This Folder** button confirms the selection and populates the path field
- **Cancel** closes the browser without changing the field
- On save, triggers a full re-scan and re-watch
2. **Avg Time Overrides** — three number inputs (COE, P2P, P3P) in minutes. Each shows:
- The calculated average from completed tests (if any), labelled `Auto: 7m 30s`
- A manual override field; when filled, it takes precedence over the calculated value
- Clear button to remove the override and revert to calculated
- If no completed tests exist for a type, the field is highlighted with a required indicator
All config values are POSTed to `POST /api/config` as key/value pairs and persisted in SQLite.
---
## 9. Vite Proxy Configuration
`vite.config.js` is updated to proxy all `/api` requests to the backend, so the React dev server and production build do not need CORS configuration:
```js
// vite.config.js
server: {
proxy: {
'/api': 'http://localhost:3001'
}
}
```
In production, Express serves the built React `dist/` as static files.
---
## 10. Implementation Phases
### Phase 1 — Backend Foundation
1. Initialize `server/package.json`; install `express`, `better-sqlite3`, `chokidar`, `dotenv`
2. Implement `db.js` — create schema, upsert helpers
3. Implement `parser.js` — regex-based tag extraction from filenames and folder names
4. Implement `scanner.js` — walk target dir to build test list; walk results dir to mark completions
5. Implement `watcher.js` — chokidar watchers for both directories; call scanner on change
6. Implement `sse.js` — maintain SSE client set; broadcast on update
7. Wire up Express routes and start server
### Phase 2 — Frontend Core
1. Update `vite.config.js` with API proxy
2. Install `@tanstack/react-query`, `axios`, `tailwindcss`, `lucide-react`
3. Build `api.js` — base fetch helpers + SSE subscription hook
4. Build `useTests` and `useStats` hooks
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`). |