Files
test_dashboard/IMPLEMENTATION_PLAN.md
2026-06-02 11:59:31 -04:00

18 KiB
Raw Permalink Blame History

Implementation Plan — Test Dashboard

Status as of June 2026: fully implemented and running. Backend is Python (Flask) with JWT authentication and role-based access control.

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 Notes
Frontend React + Vite Dev server proxies /apilocalhost:3001
Backend Python 3 + Flask Replaced original Node.js/Express plan
Auth JWT (PyJWT) Bearer token auth with admin and viewer roles
Database SQLite (via sqlite3 stdlib) WAL mode; single persistent connection with RLock
Frontend state TanStack React Query Query cache + manual invalidation after mutations
UI TailwindCSS v4 Via @tailwindcss/vite plugin

3. Project Structure

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
│   ├── package.json
│   └── src/
│       ├── App.jsx                   root layout, auth gate, modal settings entry
│       ├── 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
│       │   └── ConfigModal.jsx       settings modal (dirs + avg time overrides)
│       ├── hooks/
│       │   ├── useAuth.js            login session state + role checks
│       │   ├── useStats.js           fetches stats
│       │   ├── useTests.js           fetches test list
│       │   └── useConfig.js          fetches/saves config
│       └── lib/
│           └── api.js                fetch wrapper with JWT header injection
│
└── server/                          ← Python backend
  ├── app.py                        Flask app, JWT auth, role guards, bootstrap(), __main__
    ├── 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

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-142026-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
);

-- 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'))
);
-- Config keys:
--   target_dir             path to target tests directory
--   results_dir            path to results directory
--   results_dir_ref        optional reference 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
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)
GET /api/tests All tests; supports query filters (see below)
GET /api/stats Aggregated stats object
GET /api/scan-status Returns active scan state
GET /api/config Current directory paths and avg time overrides
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>.

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 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

Current implementation does not use SSE/watchdog.

  • 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.

8. Frontend Components

8.1 Layout

┌──────────────────────────────────────────────────────────────┐
│  Test Dashboard                                [⚙ 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│ ... │ ○      │
└──────────────────────────────────────────────────────────────┘

Auth gate: App shows a sign-in screen first. Dashboard renders only when authenticated. Settings button appears only for admin users.

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
TestTable Filtered table of tests with status and parsed tags
ConfigModal Admin settings modal for directories, avg overrides, and SMB credentials
StatusBadge Green checkmark or grey circle for completed/pending

8.3 Settings / Config

A gear icon opens ConfigModal with two sections:

  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.
  1. Avg Time Overrides — three number inputs (COE, P2P, P3P) in minutes. Each shows:
  • Manual override values converted to seconds and saved in config.
  1. SMB Credentials — domain, username, password for network share access.
  2. Action buttons:
  • Save All
  • Save Times
  • Save Results (/api/config/rescan-results)

All config values are POSTed to POST /api/config as key/value pairs and persisted in SQLite.


9. Vite Proxy Configuration

vite.config.js proxies all /api requests to the Python backend:

server: {
  host: true,          // expose on LAN so other devices can reach the dev server
  proxy: {
    '/api': 'http://localhost:3001'
  }
}

In production, Flask serves the built React dist/ as static files via send_from_directory. Build with npm run build inside dashboard/.


10. Implementation Status

Completed

  • Python Flask backend (app.py) with JWT auth and role guards
  • SQLite schema, WAL mode, thread-safe helpers (db_py.py)
  • Users table + user management APIs (admin-only)
  • Default user seeding from environment (DEFAULT_ADMIN_*, DEFAULT_VIEWER_*)
  • Tag parsing and scan logic (parser.py, scanner.py)
  • Full scan / rescan / results-only rescan flows
  • Frontend auth flow (login screen, JWT persistence, role-based UI)
  • Config modal (directories, avg overrides, SMB credentials)
  • Vite dev proxy + Flask static serving for production
  • Config migrated from config.json → SQLite on first run

Remaining / Future

  • Admin UI for user create/list (currently API-only)
  • Invalid test results
  • Watchdog automatic updates
  • Test detail view (click a row to see tput/RSSI breakdown)
  • Production deployment docs (service startup, backup, secrets handling)
  1. Build StatCard, CompletionBar, TimeDisplay, StatusBadge
  2. 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)

{
  "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).