- 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.
19 KiB
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
-- 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
{
"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 fromconfigtable (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
nullto 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:
{
"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
pathdoes not exist or is not a directory, returns400. parentisnullwhen 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, andchangeevents - 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
updateevent 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:
- 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
- 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
- The calculated average from completed tests (if any), labelled
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:
// 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
- Initialize
server/package.json; installexpress,better-sqlite3,chokidar,dotenv - Implement
db.js— create schema, upsert helpers - Implement
parser.js— regex-based tag extraction from filenames and folder names - Implement
scanner.js— walk target dir to build test list; walk results dir to mark completions - Implement
watcher.js— chokidar watchers for both directories; call scanner on change - Implement
sse.js— maintain SSE client set; broadcast on update - Wire up Express routes and start server
Phase 2 — Frontend Core
- Update
vite.config.jswith API proxy - Install
@tanstack/react-query,axios,tailwindcss,lucide-react - Build
api.js— base fetch helpers + SSE subscription hook - Build
useTestsanduseStatshooks - Build
StatCard,CompletionBar,TimeDisplay,StatusBadge - Build
App.jsxlayout with stats row
Phase 3 — Test List & Filters
- Build
FilterPanelwith controlled filter state - Build
TestTablewith all columns + sort - Wire filters to
GET /api/testsquery params - Add
ConfigModalfor directory path configuration
Phase 4 — Real-time & Polish
- Connect SSE stream in
useTests/useStatsto auto-invalidate queries - Add manual refresh button
- Add loading skeletons and error states
- Mobile-responsive layout adjustments
- Test on secondary device over local network
Phase 5 — SSO (Future)
- Add authentication middleware to the Express server (e.g.,
passport.jswith an OAuth/OIDC provider) - Protect all
/apiroutes and the static frontend behind the auth middleware - Add session management (
express-session+ a session store)
11. Key Dependencies
Server (server/package.json)
{
"dependencies": {
"better-sqlite3": "^9.x",
"chokidar": "^4.x",
"cors": "^2.x",
"dotenv": "^16.x",
"express": "^4.x"
}
}
Client (dashboard/package.json additions)
{
"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). |