# 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`). |