From 2d81f78fed146a0d670efe1a096132a949a3132a Mon Sep 17 00:00:00 2001 From: mahmamdouh Date: Mon, 5 Jan 2026 15:41:31 +0100 Subject: [PATCH] theme --- app/static/css/style.css | 273 +++++++++++++++++++++++++++------------ doc/api.md | 142 +++++++++++++++----- doc/architecture.md | 81 +++++++----- 3 files changed, 349 insertions(+), 147 deletions(-) diff --git a/app/static/css/style.css b/app/static/css/style.css index 30569f1..84e5636 100644 --- a/app/static/css/style.css +++ b/app/static/css/style.css @@ -1,12 +1,26 @@ :root { - --primary: #2563eb; - --primary-dark: #1e40af; + --primary: #3b82f6; + /* Blue 500 */ + --primary-dark: #2563eb; + /* Blue 600 */ --success: #10b981; + /* Emerald 500 */ --danger: #ef4444; + /* Red 500 */ --warning: #f59e0b; - --dark: #1f2937; - --light: #f3f4f6; - --border: #e5e7eb; + /* Amber 500 */ + --dark: #f8fafc; + /* Slate 50 (Text) */ + --bg-dark: #0f172a; + /* Slate 900 (Body) */ + --bg-panel: #1e293b; + /* Slate 800 (Panels) */ + --bg-input: #334155; + /* Slate 700 (Inputs) */ + --border: #334155; + /* Slate 700 */ + --text-muted: #94a3b8; + /* Slate 400 */ } * { @@ -16,8 +30,9 @@ } body { - font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; - background: linear-gradient(135deg, #4f46e5 0%, #3730a3 100%); + font-family: 'Inter', 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background: var(--bg-dark); + color: var(--dark); min-height: 100vh; } @@ -36,12 +51,13 @@ body { } .login-box { - background: white; + background: var(--bg-panel); padding: 40px; - border-radius: 12px; - box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2); + border-radius: 16px; + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3); width: 100%; max-width: 400px; + border: 1px solid var(--border); } .logo { @@ -58,6 +74,7 @@ body { color: var(--dark); margin-top: 15px; font-size: 24px; + font-weight: 700; } .form-group { @@ -67,21 +84,25 @@ body { .form-group label { display: block; margin-bottom: 8px; - color: var(--dark); + color: var(--text-muted); font-weight: 500; } .form-control { width: 100%; padding: 12px; - border: 2px solid var(--border); + background: var(--bg-input); + border: 1px solid var(--border); border-radius: 8px; font-size: 14px; + color: var(--dark); + transition: all 0.2s; } .form-control:focus { outline: none; border-color: var(--primary); + box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2); } .btn { @@ -127,25 +148,26 @@ body { } .alert-success { - background: #d1fae5; - color: #065f46; - border: 1px solid #10b981; + background: rgba(16, 185, 129, 0.1); + color: #34d399; + border: 1px solid rgba(16, 185, 129, 0.2); } .alert-error { - background: #fee2e2; - color: #991b1b; - border: 1px solid #ef4444; + background: rgba(239, 68, 68, 0.1); + color: #f87171; + border: 1px solid rgba(239, 68, 68, 0.2); } /* Navbar */ .navbar { - background: white; + background: var(--bg-panel); padding: 15px 30px; - box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); display: flex; justify-content: space-between; align-items: center; + border-bottom: 1px solid var(--border); } .navbar-brand { @@ -161,6 +183,7 @@ body { .navbar-brand h2 { color: var(--dark); font-size: 20px; + font-weight: 700; } .navbar-menu { @@ -170,7 +193,7 @@ body { } .navbar-menu a { - color: var(--dark); + color: var(--text-muted); text-decoration: none; font-weight: 500; transition: color 0.3s; @@ -190,11 +213,12 @@ body { } .panel { - background: white; - border-radius: 12px; + background: var(--bg-panel); + border-radius: 16px; padding: 20px; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); overflow-y: auto; + border: 1px solid var(--border); } .panel-header { @@ -203,12 +227,13 @@ body { align-items: center; margin-bottom: 20px; padding-bottom: 15px; - border-bottom: 2px solid var(--border); + border-bottom: 1px solid var(--border); } .panel-header h3 { color: var(--dark); font-size: 18px; + font-weight: 600; } /* Job List */ @@ -220,10 +245,11 @@ body { .job-item { padding: 15px; - border: 2px solid var(--border); - border-radius: 8px; + background: var(--bg-input); + border: 1px solid var(--border); + border-radius: 12px; cursor: pointer; - transition: all 0.3s; + transition: all 0.2s; display: flex; align-items: center; gap: 12px; @@ -231,12 +257,12 @@ body { .job-item:hover { border-color: var(--primary); - background: var(--light); + transform: translateY(-2px); } .job-item.active { border-color: var(--primary); - background: #eff6ff; + background: rgba(59, 130, 246, 0.1); } .job-status-icon { @@ -247,10 +273,11 @@ body { color: var(--dark); font-size: 14px; margin-bottom: 4px; + font-weight: 600; } .job-info p { - color: #6b7280; + color: var(--text-muted); font-size: 12px; } @@ -272,11 +299,11 @@ body { .detail-label { font-weight: 600; - color: var(--dark); + color: var(--text-muted); } .detail-value { - color: #4b5563; + color: var(--dark); } .status-badge { @@ -288,32 +315,43 @@ body { } .status-in_progress { - background: #fef3c7; - color: #92400e; + background: rgba(245, 158, 11, 0.1); + color: #fbbf24; + border: 1px solid rgba(245, 158, 11, 0.2); } .status-passed { - background: #d1fae5; - color: #065f46; + background: rgba(16, 185, 129, 0.1); + color: #34d399; + border: 1px solid rgba(16, 185, 129, 0.2); } .status-failed { - background: #fee2e2; - color: #991b1b; + background: rgba(239, 68, 68, 0.1); + color: #f87171; + border: 1px solid rgba(239, 68, 68, 0.2); } .status-aborted { - background: #f3f4f6; - color: #374151; + background: rgba(148, 163, 184, 0.1); + color: #cbd5e1; + border: 1px solid rgba(148, 163, 184, 0.2); +} + +.status-waiting { + background: rgba(245, 158, 11, 0.1); + color: #fbbf24; + border: 1px solid rgba(245, 158, 11, 0.2); } /* Admin Dashboard */ .admin-container { - background: white; - border-radius: 12px; + background: var(--bg-panel); + border-radius: 16px; padding: 30px; margin-top: 20px; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); + border: 1px solid var(--border); } .admin-header { @@ -333,7 +371,7 @@ body { } .user-table th { - background: var(--light); + background: var(--bg-input); padding: 12px; text-align: left; font-weight: 600; @@ -344,10 +382,11 @@ body { .user-table td { padding: 12px; border-bottom: 1px solid var(--border); + color: var(--text-muted); } .user-table tr:hover { - background: var(--light); + background: rgba(255, 255, 255, 0.02); } .badge { @@ -359,13 +398,13 @@ body { } .badge-admin { - background: #dbeafe; - color: #1e40af; + background: rgba(59, 130, 246, 0.1); + color: #60a5fa; } .badge-user { - background: #f3f4f6; - color: #374151; + background: rgba(148, 163, 184, 0.1); + color: #cbd5e1; } /* Modal */ @@ -376,8 +415,9 @@ body { left: 0; width: 100%; height: 100%; - background: rgba(0, 0, 0, 0.5); + background: rgba(0, 0, 0, 0.7); z-index: 1000; + backdrop-filter: blur(4px); } .modal.active { @@ -387,11 +427,13 @@ body { } .modal-content { - background: white; + background: var(--bg-panel); padding: 30px; - border-radius: 12px; + border-radius: 16px; width: 90%; max-width: 500px; + border: 1px solid var(--border); + box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); } .modal-header { @@ -410,19 +452,24 @@ body { border: none; font-size: 24px; cursor: pointer; - color: #6b7280; + color: var(--text-muted); +} + +.close-btn:hover { + color: var(--dark); } /* Submit Form */ .submit-container { - background: white; - border-radius: 12px; + background: var(--bg-panel); + border-radius: 16px; padding: 40px; margin-top: 20px; max-width: 800px; margin-left: auto; margin-right: auto; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); + border: 1px solid var(--border); } .step-indicator { @@ -455,29 +502,33 @@ body { .step.active .step-number { background: var(--primary); color: white; + border-color: var(--primary); } .step.completed .step-number { background: var(--success); color: white; + border-color: var(--success); } .step-number { width: 30px; height: 30px; border-radius: 50%; - background: var(--border); - color: var(--dark); + background: var(--bg-input); + border: 2px solid var(--border); + color: var(--text-muted); display: inline-flex; align-items: center; justify-content: center; font-weight: 600; margin-bottom: 8px; + transition: all 0.3s; } .step-label { font-size: 12px; - color: #6b7280; + color: var(--text-muted); } .checkbox-group { @@ -491,11 +542,13 @@ body { display: flex; align-items: center; gap: 8px; + color: var(--dark); } .checkbox-item input[type="checkbox"] { width: 18px; height: 18px; + accent-color: var(--primary); } .radio-group { @@ -510,15 +563,22 @@ body { align-items: center; gap: 10px; padding: 12px; - border: 2px solid var(--border); + border: 1px solid var(--border); border-radius: 8px; cursor: pointer; + background: var(--bg-input); + transition: all 0.2s; } .radio-item:hover { border-color: var(--primary); } +.radio-item label { + color: var(--dark); + cursor: pointer; +} + .radio-item input[type="radio"]:checked+label { color: var(--primary); font-weight: 600; @@ -534,21 +594,22 @@ body { .empty-state { text-align: center; padding: 60px 20px; - color: #6b7280; + color: var(--text-muted); } .empty-state h3 { margin-bottom: 10px; + color: var(--dark); } /* Context Menu */ .context-menu { display: none; position: absolute; - background: white; + background: var(--bg-panel); border: 1px solid var(--border); border-radius: 8px; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); z-index: 1000; min-width: 150px; } @@ -560,10 +621,11 @@ body { align-items: center; gap: 8px; transition: background-color 0.2s; + color: var(--dark); } .context-menu-item:hover { - background: var(--light); + background: var(--bg-input); } .context-menu-item:first-child { @@ -587,28 +649,28 @@ body { } .branch-validation.success { - background: #d1fae5; - color: #065f46; - border: 1px solid #10b981; + background: rgba(16, 185, 129, 0.1); + color: #34d399; + border: 1px solid rgba(16, 185, 129, 0.2); } .branch-validation.error { - background: #fee2e2; - color: #991b1b; - border: 1px solid #ef4444; + background: rgba(239, 68, 68, 0.1); + color: #f87171; + border: 1px solid rgba(239, 68, 68, 0.2); } .branch-validation.loading { - background: #fef3c7; - color: #92400e; - border: 1px solid #f59e0b; + background: rgba(245, 158, 11, 0.1); + color: #fbbf24; + border: 1px solid rgba(245, 158, 11, 0.2); } .loading-spinner { display: inline-block; width: 16px; height: 16px; - border: 2px solid #f3f3f3; + border: 2px solid rgba(255, 255, 255, 0.1); border-top: 2px solid var(--primary); border-radius: 50%; animation: spin 1s linear infinite; @@ -626,27 +688,29 @@ body { } .btn:disabled { - background: #9ca3af; + background: var(--bg-input); cursor: not-allowed; opacity: 0.6; + color: var(--text-muted); } /* Scenario Tree Styles */ .scenario-controls { margin: 20px 0; padding: 15px; - background: var(--light); + background: var(--bg-input); border-radius: 8px; display: flex; align-items: center; gap: 10px; + border: 1px solid var(--border); } .scenario-tree { border: 1px solid var(--border); border-radius: 8px; padding: 20px; - background: white; + background: var(--bg-panel); margin: 20px 0; max-height: 500px; overflow-y: auto; @@ -673,6 +737,7 @@ body { padding: 8px 0; cursor: pointer; user-select: none; + color: var(--dark); } .tree-toggle { @@ -707,13 +772,13 @@ body { .stack-label { font-size: 14px; font-weight: 500; - color: #4b5563; + color: var(--text-muted); } .scenario-label { font-size: 13px; font-weight: 400; - color: #6b7280; + color: var(--text-muted); } .tree-children { @@ -726,6 +791,7 @@ body { width: 16px; height: 16px; cursor: pointer; + accent-color: var(--primary); } .layer-node:hover { @@ -768,28 +834,63 @@ input[type="checkbox"]:indeterminate::before { #scenarioTable { margin-top: 10px; font-size: 13px; + width: 100%; + border-collapse: collapse; } #scenarioTable th { padding: 10px; - background: #f8fafc; + background: var(--bg-input); + color: var(--dark); + text-align: left; + border-bottom: 1px solid var(--border); } #scenarioTable td { padding: 10px; + color: var(--text-muted); + border-bottom: 1px solid var(--border); } #queue-log { - border: 1px solid #333; - box-shadow: inset 0 2px 10px rgba(0, 0, 0, 0.5); + border: 1px solid var(--border); + box-shadow: inset 0 2px 10px rgba(0, 0, 0, 0.2); + background: #000; + color: #0f0; + padding: 10px; + font-family: monospace; + border-radius: 8px; +} + +#scenarioSearch { + background: var(--bg-input); + border: 1px solid var(--border); + color: var(--dark); + padding: 8px 12px; + border-radius: 6px; } #scenarioSearch:focus { border-color: var(--primary); box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1); + outline: none; } -.status-waiting { - background: #fef3c7; - color: #92400e; +/* Scrollbar Styling */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: var(--bg-dark); +} + +::-webkit-scrollbar-thumb { + background: var(--border); + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--text-muted); } \ No newline at end of file diff --git a/doc/api.md b/doc/api.md index ca8c032..12b22c8 100644 --- a/doc/api.md +++ b/doc/api.md @@ -1,48 +1,130 @@ # TestArena API Reference +TestArena provides a RESTful API for programmatic job submission and status monitoring. + ## Base URL -`http://:8080/api` + +All API endpoints are prefixed with `/api`. + +``` +http:///api +``` + +## Authentication + +The API uses Basic Authentication. You must provide your username and password in the request body for job submission. ## Endpoints -### 1. Submit a New Queue -`POST /queue` -Submits a new set of tasks to the execution queue. -**Payload:** +### 1. Submit Job + +Submit a new test job to the queue. + +- **URL**: `/submit_job` +- **Method**: `POST` +- **Content-Type**: `application/json` + +#### Request Body + +| Field | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| `username` | string | Yes | Your TestArena username | +| `password` | string | Yes | Your TestArena password | +| `branch_name` | string | Yes | Git branch name to test | +| `scenarios` | array | Yes | List of scenario names to execute | + +#### Example Request + +```bash +curl -X POST http://localhost:5000/api/submit_job \ + -H "Content-Type: application/json" \ + -d '{ + "username": "admin", + "password": "your_password", + "branch_name": "feature/new-driver", + "scenarios": ["adc_init_test", "gpio_init_test"] + }' +``` + +#### Success Response + +**Code**: `200 OK` + ```json { - "source": "branch_name", - "job_id": [ - "environment", - { - "task_id": "scenario_path" - } - ] + "success": true, + "job_id": 123, + "status": "waiting", + "remote_triggered": true, + "message": "Job submitted successfully" } ``` -### 2. Get Status -`GET /status/{id}` -Gets the status of a specific queue or task. +#### Error Response -### 3. Abort Queue or Task -`POST /abort/{id}` -Aborts a waiting or running queue or a single task. +**Code**: `401 Unauthorized` -### 4. Delete Queue -`DELETE /delete/{id}` -Permanently deletes a queue and its associated data. +```json +{ + "error": "Invalid credentials" +} +``` -## Local API (Web App) +**Code**: `400 Bad Request` -### Get Job Details -`GET /jobs/{job_id}` +```json +{ + "error": "Missing required fields: username, password, branch_name, scenarios" +} +``` -### Get Job Status (Triggers Update) -`GET /jobs/{job_id}/status` +--- -### Abort Job -`POST /jobs/{job_id}/abort` +### 2. Get Job Status -### Delete Job -`POST /jobs/{job_id}/delete` +Retrieve the status and details of a specific job. + +- **URL**: `/job/` +- **Method**: `GET` + +#### URL Parameters + +| Parameter | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| `job_id` | integer | Yes | The ID of the job to retrieve | + +#### Example Request + +```bash +curl http://localhost:5000/api/job/123 +``` + +#### Success Response + +**Code**: `200 OK` + +```json +{ + "job_id": 123, + "status": "in_progress", + "branch_name": "feature/new-driver", + "scenarios": ["adc_init_test", "gpio_init_test"], + "remote_results": { + "adc_init_test": ["PASS", "http://.../report.html"], + "gpio_init_test": ["FAIL", "http://.../report.html"] + }, + "created_at": "2023-10-27T10:00:00", + "completed_at": null, + "remote_queue_id": "123" +} +``` + +#### Error Response + +**Code**: `404 Not Found` + +```json +{ + "error": "Job not found" +} +``` diff --git a/doc/architecture.md b/doc/architecture.md index f507066..51e86ec 100644 --- a/doc/architecture.md +++ b/doc/architecture.md @@ -1,41 +1,60 @@ -# TestArena Architecture Documentation +# TestArena Architecture -## Overview -TestArena is a web-based test management and execution platform designed to orchestrate test scenarios across different environments. It consists of a Flask-based frontend/backend that communicates with a remote execution server. +TestArena is a web-based platform for managing and executing automated tests on remote hardware setups. It acts as a central hub for users to submit test jobs, monitor execution, and view results. -## System Components +## System Overview -### 1. Web Application (Flask) -- **Frontend**: Built with HTML, CSS (Vanilla), and JavaScript. Provides a dashboard for monitoring jobs, submitting new tests, and viewing logs. -- **Backend**: Flask server handling authentication, job management, and API requests. -- **Database**: PostgreSQL (in Docker) or SQLite. Managed via SQLAlchemy. Stores users and job history. +The system consists of three main layers: -### 2. Remote Execution Server -- Handles the actual execution of test scenarios. -- Exposes APIs for queueing, status polling, and aborting/deleting jobs. -- Provides access to execution logs and results. +1. **Web Frontend (Flask)**: + - Provides the user interface for job submission, dashboard monitoring, and administration. + - Handles user authentication and input validation. + - Exposes REST APIs for programmatic access. -## Key Workflows +2. **Backend Logic (Python/Flask)**: + - **Job Management**: Manages the lifecycle of test jobs (Waiting -> In Progress -> Passed/Failed). + - **Remote Execution**: Communicates with the remote test server via HTTP APIs to trigger queues and poll status. + - **Database**: Uses SQLite (via SQLAlchemy) to store user data, job history, and results. -### Job Submission -1. User selects a branch and validates it via SSH on the remote server. -2. User selects test scenarios to run. -3. User reviews and submits the job. -4. The web app creates a local `Job` record and sends a POST request to the remote `/api/queue` endpoint using the local Job ID as the `remote_queue_id`. +3. **Remote Test Server (External)**: + - Executes the actual test scenarios on hardware/simulators. + - Exposes APIs for queue management (`/api/queue`, `/api/status`, `/api/abort`). + - Hosts the test results and logs. -### Status Polling -1. A background thread in the Flask app polls the remote server every 20 seconds for all `waiting` or `in_progress` jobs. -2. The dashboard also polls the local API every 5 seconds when a job is being viewed to provide real-time updates. -3. Tab persistence is handled in the frontend to ensure the user's view (Scenarios vs Logs) is maintained during polling. +## Key Components -### Job Search (Global) -- Users can search for jobs by **Job ID** or **Username** globally. -- By default, non-admin users only see their own jobs. Searching allows them to see others' jobs if they have the ID or username. +### 1. Job Processing Flow -### Job Abort/Delete -- Users can abort running jobs or delete them entirely. These actions are synchronized with the remote server using the `POST /abort/{id}` and `DELETE /delete/{id}` APIs. +1. **Submission**: User submits a job via UI or API (`POST /api/submit_job`). +2. **Validation**: System validates credentials and branch existence (via SSH). +3. **Queueing**: A local `Job` record is created. The system sends a payload to the Remote Server's `/api/queue` endpoint. +4. **Monitoring**: A background thread in the Flask app polls the Remote Server every 20 seconds. + - Checks Queue Status (`/api/status/`). + - Checks Task Status (`/api/status/`). + - Updates local DB with progress and results. +5. **Completion**: + - When all tasks finish, the job is marked as `passed` or `failed`. + - **Cleanup**: An SSH command is automatically executed to clean up the remote workspace. + - **Timeout**: If a job runs > 1 hour, it is auto-aborted. -## Technology Stack -- **Backend**: Python, Flask, Flask-SQLAlchemy, Flask-Login, Requests. -- **Frontend**: HTML5, CSS3, JavaScript (ES6). -- **Remote Communication**: REST API, SSH (for branch validation and scenario scanning). +### 2. Database Schema + +- **Users**: `id`, `username`, `password_hash`, `is_admin` +- **Jobs**: `id`, `user_id`, `branch_name`, `scenarios`, `status`, `remote_queue_id`, `remote_results`, `queue_log` + +### 3. API Layer + +- **`POST /api/submit_job`**: Programmatic job submission. +- **`GET /api/job/`**: Status retrieval. + +## Deployment + +- **Containerization**: Docker & Docker Compose. +- **Web Server**: Gunicorn (WSGI) behind Caddy (Reverse Proxy/SSL). +- **Database**: SQLite (Persistent volume). + +## Security + +- **Authentication**: Flask-Login for UI, Basic Auth for API. +- **SSH**: Uses `sshpass` with strict host checking disabled (internal network) for remote operations. +- **Environment**: Secrets managed via `.env` file.