Compare commits
13 Commits
version_1.
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 2d81f78fed | |||
| 67e51a5348 | |||
| efb61eb283 | |||
| 3572a99cc0 | |||
| 586501b94a | |||
| f254e04fc5 | |||
| e33ce220e9 | |||
| ce37351722 | |||
| 396a0555d9 | |||
| 9d2f2b65ae | |||
| 0c5f801854 | |||
| a9ad5bc2ad | |||
| 6b254534f6 |
419
ARCHITECTURE.md
419
ARCHITECTURE.md
@@ -1,419 +0,0 @@
|
|||||||
# ASF TestArena - Architecture
|
|
||||||
|
|
||||||
## System Overview
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────────────────────┐
|
|
||||||
│ Internet │
|
|
||||||
└────────────────────────┬────────────────────────────────────┘
|
|
||||||
│ HTTPS (443)
|
|
||||||
│ testarena.nabd-co.com
|
|
||||||
↓
|
|
||||||
┌─────────────────────────────────────────────────────────────┐
|
|
||||||
│ Caddy Reverse Proxy │
|
|
||||||
│ • SSL/TLS Termination │
|
|
||||||
│ • Automatic HTTPS (Let's Encrypt) │
|
|
||||||
│ • Load Balancing │
|
|
||||||
│ • Security Headers │
|
|
||||||
└────────────────────────┬────────────────────────────────────┘
|
|
||||||
│ HTTP (5000)
|
|
||||||
│ Internal Docker Network
|
|
||||||
↓
|
|
||||||
┌─────────────────────────────────────────────────────────────┐
|
|
||||||
│ Flask Web Application (Gunicorn) │
|
|
||||||
│ Container: testarena_web │
|
|
||||||
│ • Authentication (Flask-Login) │
|
|
||||||
│ • User Management │
|
|
||||||
│ • Job Submission │
|
|
||||||
│ • Dashboard │
|
|
||||||
│ • API Endpoints │
|
|
||||||
└────────────────────────┬────────────────────────────────────┘
|
|
||||||
│ PostgreSQL (5432)
|
|
||||||
│ Internal Network Only
|
|
||||||
↓
|
|
||||||
┌─────────────────────────────────────────────────────────────┐
|
|
||||||
│ PostgreSQL Database │
|
|
||||||
│ Container: testarena_db │
|
|
||||||
│ • User accounts │
|
|
||||||
│ • Job records │
|
|
||||||
│ • Persistent storage │
|
|
||||||
└─────────────────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
## Application Architecture
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────────────────────┐
|
|
||||||
│ Flask Application │
|
|
||||||
├─────────────────────────────────────────────────────────────┤
|
|
||||||
│ │
|
|
||||||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
|
||||||
│ │ Routes │ │ Models │ │ Templates │ │
|
|
||||||
│ │ │ │ │ │ │ │
|
|
||||||
│ │ • auth.py │ │ • User │ │ • login.html │ │
|
|
||||||
│ │ • admin.py │ │ • Job │ │ • dashboard │ │
|
|
||||||
│ │ • dashboard │ │ │ │ • admin │ │
|
|
||||||
│ │ • jobs.py │ │ │ │ • jobs │ │
|
|
||||||
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
|
|
||||||
│ │ │ │ │
|
|
||||||
│ └─────────────────┼─────────────────┘ │
|
|
||||||
│ │ │
|
|
||||||
│ ┌────────────────────────┴────────────────────────┐ │
|
|
||||||
│ │ SQLAlchemy ORM │ │
|
|
||||||
│ └────────────────────────┬────────────────────────┘ │
|
|
||||||
│ │ │
|
|
||||||
└───────────────────────────┼───────────────────────────────┘
|
|
||||||
│
|
|
||||||
↓
|
|
||||||
PostgreSQL Database
|
|
||||||
```
|
|
||||||
|
|
||||||
## User Flow Diagrams
|
|
||||||
|
|
||||||
### Admin Workflow
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────┐
|
|
||||||
│ Login │
|
|
||||||
└────┬────┘
|
|
||||||
│
|
|
||||||
↓
|
|
||||||
┌─────────────────┐
|
|
||||||
│ Admin Check │
|
|
||||||
└────┬────────────┘
|
|
||||||
│
|
|
||||||
├─→ Admin Dashboard
|
|
||||||
│ ├─→ Create User
|
|
||||||
│ ├─→ Reset Password
|
|
||||||
│ ├─→ Delete User
|
|
||||||
│ └─→ View All Jobs
|
|
||||||
│
|
|
||||||
└─→ User Dashboard
|
|
||||||
├─→ View Own Jobs
|
|
||||||
└─→ Submit Jobs
|
|
||||||
```
|
|
||||||
|
|
||||||
### User Workflow
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────┐
|
|
||||||
│ Login │
|
|
||||||
└────┬────┘
|
|
||||||
│
|
|
||||||
↓
|
|
||||||
┌──────────────┐
|
|
||||||
│ Dashboard │
|
|
||||||
└────┬─────────┘
|
|
||||||
│
|
|
||||||
├─→ View Jobs
|
|
||||||
│ ├─→ Click Job
|
|
||||||
│ ├─→ View Details
|
|
||||||
│ └─→ Abort Job (if in progress)
|
|
||||||
│
|
|
||||||
└─→ Submit New Job
|
|
||||||
├─→ Step 1: Branch Name
|
|
||||||
├─→ Step 2: Select Scenarios
|
|
||||||
├─→ Step 3: Choose Environment
|
|
||||||
├─→ Step 4: Test Mode + Options
|
|
||||||
└─→ Step 5: Submit
|
|
||||||
```
|
|
||||||
|
|
||||||
## Job Submission Flow
|
|
||||||
|
|
||||||
```
|
|
||||||
┌──────────────────┐
|
|
||||||
│ User clicks │
|
|
||||||
│ "Submit Job" │
|
|
||||||
└────────┬─────────┘
|
|
||||||
│
|
|
||||||
↓
|
|
||||||
┌──────────────────┐
|
|
||||||
│ Step 1: │
|
|
||||||
│ Enter Branch │
|
|
||||||
│ Name │
|
|
||||||
└────────┬─────────┘
|
|
||||||
│
|
|
||||||
↓
|
|
||||||
┌──────────────────┐
|
|
||||||
│ Backend: │
|
|
||||||
│ • Checkout branch│
|
|
||||||
│ • Run scanner │
|
|
||||||
│ • Get scenarios │
|
|
||||||
└────────┬─────────┘
|
|
||||||
│
|
|
||||||
↓
|
|
||||||
┌──────────────────┐
|
|
||||||
│ Step 2: │
|
|
||||||
│ Select Scenarios │
|
|
||||||
│ (Checkboxes) │
|
|
||||||
└────────┬─────────┘
|
|
||||||
│
|
|
||||||
↓
|
|
||||||
┌──────────────────┐
|
|
||||||
│ Step 3: │
|
|
||||||
│ Choose Env │
|
|
||||||
│ • Sensor Hub │
|
|
||||||
│ • Main Board │
|
|
||||||
└────────┬─────────┘
|
|
||||||
│
|
|
||||||
↓
|
|
||||||
┌──────────────────┐
|
|
||||||
│ Step 4: │
|
|
||||||
│ Test Mode │
|
|
||||||
│ • Simulator │
|
|
||||||
│ • HIL │
|
|
||||||
│ + Options │
|
|
||||||
└────────┬─────────┘
|
|
||||||
│
|
|
||||||
↓
|
|
||||||
┌──────────────────┐
|
|
||||||
│ Create Job │
|
|
||||||
│ Record in DB │
|
|
||||||
└────────┬─────────┘
|
|
||||||
│
|
|
||||||
↓
|
|
||||||
┌──────────────────┐
|
|
||||||
│ Start Test │
|
|
||||||
│ (Background) │
|
|
||||||
└────────┬─────────┘
|
|
||||||
│
|
|
||||||
↓
|
|
||||||
┌──────────────────┐
|
|
||||||
│ Update Status │
|
|
||||||
│ • In Progress │
|
|
||||||
│ • Passed │
|
|
||||||
│ • Failed │
|
|
||||||
│ • Aborted │
|
|
||||||
└──────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
## Database Schema
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────┐
|
|
||||||
│ Users Table │
|
|
||||||
├─────────────────────────────────────────┤
|
|
||||||
│ id (PK) INTEGER │
|
|
||||||
│ username VARCHAR(80) UNIQUE │
|
|
||||||
│ password_hash VARCHAR(255) │
|
|
||||||
│ is_admin BOOLEAN │
|
|
||||||
│ created_at TIMESTAMP │
|
|
||||||
└─────────────────┬───────────────────────┘
|
|
||||||
│
|
|
||||||
│ 1:N
|
|
||||||
│
|
|
||||||
┌─────────────────┴───────────────────────┐
|
|
||||||
│ Jobs Table │
|
|
||||||
├─────────────────────────────────────────┤
|
|
||||||
│ id (PK) INTEGER │
|
|
||||||
│ user_id (FK) INTEGER │
|
|
||||||
│ branch_name VARCHAR(255) │
|
|
||||||
│ scenarios TEXT (JSON) │
|
|
||||||
│ environment VARCHAR(50) │
|
|
||||||
│ test_mode VARCHAR(50) │
|
|
||||||
│ status VARCHAR(20) │
|
|
||||||
│ submitted_at TIMESTAMP │
|
|
||||||
│ completed_at TIMESTAMP │
|
|
||||||
│ duration INTEGER │
|
|
||||||
│ keep_devbenches BOOLEAN │
|
|
||||||
│ reuse_results BOOLEAN │
|
|
||||||
│ results_path VARCHAR(500) │
|
|
||||||
└─────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
## Network Architecture
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────────────────┐
|
|
||||||
│ Docker Host │
|
|
||||||
│ │
|
|
||||||
│ ┌───────────────────────────────────────────────────┐ │
|
|
||||||
│ │ Caddy Network (External) │ │
|
|
||||||
│ │ │ │
|
|
||||||
│ │ ┌──────────────┐ ┌──────────────┐ │ │
|
|
||||||
│ │ │ Caddy │──────│ testarena_web│ │ │
|
|
||||||
│ │ │ Container │ │ Container │ │ │
|
|
||||||
│ │ └──────────────┘ └──────┬───────┘ │ │
|
|
||||||
│ │ │ │ │
|
|
||||||
│ └───────────────────────────────┼──────────────────┘ │
|
|
||||||
│ │ │
|
|
||||||
│ ┌───────────────────────────────┼──────────────────┐ │
|
|
||||||
│ │ TestArena Network │ │ │
|
|
||||||
│ │ │ │ │
|
|
||||||
│ │ ┌──────────────┐ ┌──────┴───────┐ │ │
|
|
||||||
│ │ │ testarena_web│──────│ testarena_db │ │ │
|
|
||||||
│ │ │ Container │ │ Container │ │ │
|
|
||||||
│ │ └──────────────┘ └──────────────┘ │ │
|
|
||||||
│ │ │ │
|
|
||||||
│ └──────────────────────────────────────────────────┘ │
|
|
||||||
│ │
|
|
||||||
│ ┌──────────────────────────────────────────────────┐ │
|
|
||||||
│ │ Docker Volumes │ │
|
|
||||||
│ │ • postgres_data (Database persistence) │ │
|
|
||||||
│ │ • test_results (Test output files) │ │
|
|
||||||
│ └──────────────────────────────────────────────────┘ │
|
|
||||||
└─────────────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
## Security Layers
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────────────────┐
|
|
||||||
│ Layer 1: Network Security │
|
|
||||||
│ • Firewall rules │
|
|
||||||
│ • Only Caddy exposes ports to internet │
|
|
||||||
│ • Internal Docker networks isolated │
|
|
||||||
└────────────────────────┬────────────────────────────────┘
|
|
||||||
│
|
|
||||||
┌────────────────────────┴────────────────────────────────┐
|
|
||||||
│ Layer 2: Transport Security │
|
|
||||||
│ • HTTPS/TLS via Caddy │
|
|
||||||
│ • Automatic certificate management │
|
|
||||||
│ • Security headers (HSTS, CSP, etc.) │
|
|
||||||
└────────────────────────┬────────────────────────────────┘
|
|
||||||
│
|
|
||||||
┌────────────────────────┴────────────────────────────────┐
|
|
||||||
│ Layer 3: Application Security │
|
|
||||||
│ • Flask-Login session management │
|
|
||||||
│ • Password hashing (Werkzeug) │
|
|
||||||
│ • CSRF protection (Flask-WTF) │
|
|
||||||
│ • Role-based access control │
|
|
||||||
└────────────────────────┬────────────────────────────────┘
|
|
||||||
│
|
|
||||||
┌────────────────────────┴────────────────────────────────┐
|
|
||||||
│ Layer 4: Database Security │
|
|
||||||
│ • PostgreSQL authentication │
|
|
||||||
│ • Network isolation (internal only) │
|
|
||||||
│ • Encrypted connections │
|
|
||||||
│ • Regular backups │
|
|
||||||
└─────────────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
## File System Layout
|
|
||||||
|
|
||||||
```
|
|
||||||
/app (Container)
|
|
||||||
├── routes/
|
|
||||||
│ ├── auth.py # Login/logout endpoints
|
|
||||||
│ ├── admin.py # User management endpoints
|
|
||||||
│ ├── dashboard.py # Dashboard endpoints
|
|
||||||
│ └── jobs.py # Job submission endpoints
|
|
||||||
├── templates/
|
|
||||||
│ ├── base.html # Base template with navbar
|
|
||||||
│ ├── login.html # Login page
|
|
||||||
│ ├── admin/
|
|
||||||
│ │ └── dashboard.html
|
|
||||||
│ ├── dashboard/
|
|
||||||
│ │ └── index.html
|
|
||||||
│ └── jobs/
|
|
||||||
│ ├── submit.html
|
|
||||||
│ ├── submit_step2.html
|
|
||||||
│ ├── submit_step3.html
|
|
||||||
│ └── submit_step4.html
|
|
||||||
├── static/
|
|
||||||
│ ├── css/
|
|
||||||
│ │ └── style.css # Modern theme
|
|
||||||
│ └── uploads/
|
|
||||||
│ └── icon.png # Logo
|
|
||||||
├── test_results/ # Volume mount
|
|
||||||
│ └── [job_id]/
|
|
||||||
│ └── index.html # Test results
|
|
||||||
├── models.py # Database models
|
|
||||||
└── __init__.py # App factory
|
|
||||||
|
|
||||||
/var/lib/postgresql/data (Volume)
|
|
||||||
└── [PostgreSQL data files]
|
|
||||||
```
|
|
||||||
|
|
||||||
## Technology Stack Details
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────────────────┐
|
|
||||||
│ Frontend Layer │
|
|
||||||
│ • HTML5 │
|
|
||||||
│ • CSS3 (Modern gradient design) │
|
|
||||||
│ • Vanilla JavaScript (No frameworks) │
|
|
||||||
└────────────────────────┬────────────────────────────────┘
|
|
||||||
│
|
|
||||||
┌────────────────────────┴────────────────────────────────┐
|
|
||||||
│ Application Layer │
|
|
||||||
│ • Flask 3.0.0 (Web framework) │
|
|
||||||
│ • Flask-Login 0.6.3 (Authentication) │
|
|
||||||
│ • Flask-SQLAlchemy 3.1.1 (ORM) │
|
|
||||||
│ • Flask-WTF 1.2.1 (Forms & CSRF) │
|
|
||||||
│ • Gunicorn 21.2.0 (WSGI server) │
|
|
||||||
└────────────────────────┬────────────────────────────────┘
|
|
||||||
│
|
|
||||||
┌────────────────────────┴────────────────────────────────┐
|
|
||||||
│ Database Layer │
|
|
||||||
│ • PostgreSQL 15 (Relational database) │
|
|
||||||
│ • psycopg2-binary 2.9.9 (Python adapter) │
|
|
||||||
└────────────────────────┬────────────────────────────────┘
|
|
||||||
│
|
|
||||||
┌────────────────────────┴────────────────────────────────┐
|
|
||||||
│ Infrastructure Layer │
|
|
||||||
│ • Docker (Containerization) │
|
|
||||||
│ • Docker Compose (Orchestration) │
|
|
||||||
│ • Caddy 2.x (Reverse proxy) │
|
|
||||||
└─────────────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
## Deployment Pipeline (Future)
|
|
||||||
|
|
||||||
```
|
|
||||||
┌──────────────┐
|
|
||||||
│ Git Push │
|
|
||||||
└──────┬───────┘
|
|
||||||
│
|
|
||||||
↓
|
|
||||||
┌──────────────┐
|
|
||||||
│ CI/CD │
|
|
||||||
│ • Run tests │
|
|
||||||
│ • Build image│
|
|
||||||
└──────┬───────┘
|
|
||||||
│
|
|
||||||
↓
|
|
||||||
┌──────────────┐
|
|
||||||
│ Docker │
|
|
||||||
│ Registry │
|
|
||||||
└──────┬───────┘
|
|
||||||
│
|
|
||||||
↓
|
|
||||||
┌──────────────┐
|
|
||||||
│ Production │
|
|
||||||
│ Server │
|
|
||||||
│ • Pull image │
|
|
||||||
│ • Deploy │
|
|
||||||
└──────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
## Scaling Strategy (Future)
|
|
||||||
|
|
||||||
```
|
|
||||||
┌──────────────┐
|
|
||||||
│ Load Balancer│
|
|
||||||
└──────┬───────┘
|
|
||||||
│
|
|
||||||
┌──────────────────┼──────────────────┐
|
|
||||||
│ │ │
|
|
||||||
↓ ↓ ↓
|
|
||||||
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
|
|
||||||
│ Web Instance 1│ │ Web Instance 2│ │ Web Instance 3│
|
|
||||||
└───────┬───────┘ └───────┬───────┘ └───────┬───────┘
|
|
||||||
│ │ │
|
|
||||||
└──────────────────┼──────────────────┘
|
|
||||||
│
|
|
||||||
↓
|
|
||||||
┌────────────────┐
|
|
||||||
│ PostgreSQL │
|
|
||||||
│ (Primary) │
|
|
||||||
└────────┬───────┘
|
|
||||||
│
|
|
||||||
↓
|
|
||||||
┌────────────────┐
|
|
||||||
│ PostgreSQL │
|
|
||||||
│ (Replica) │
|
|
||||||
└────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
This architecture provides a solid foundation for Phase 1 and is designed to scale for Phase 2 implementation.
|
|
||||||
@@ -1,261 +0,0 @@
|
|||||||
# Caddy Integration Guide
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
ASF TestArena is designed to work behind a Caddy reverse proxy for HTTPS and domain management.
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
- Caddy server running in Docker
|
|
||||||
- Caddy network created
|
|
||||||
- Domain name configured (testarena.nabd-co.com)
|
|
||||||
|
|
||||||
## Step 1: Find Your Caddy Network Name
|
|
||||||
|
|
||||||
Run this command to list all Docker networks:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker network ls
|
|
||||||
```
|
|
||||||
|
|
||||||
Look for your Caddy network. Common names:
|
|
||||||
- `caddy_network`
|
|
||||||
- `caddy_default`
|
|
||||||
- `caddy`
|
|
||||||
- `proxy_network`
|
|
||||||
|
|
||||||
## Step 2: Update docker-compose.yml
|
|
||||||
|
|
||||||
### Option A: Edit the file directly
|
|
||||||
|
|
||||||
Open `docker-compose.yml` and make these changes:
|
|
||||||
|
|
||||||
1. Uncomment lines 28-29 at the bottom:
|
|
||||||
```yaml
|
|
||||||
networks:
|
|
||||||
testarena_network:
|
|
||||||
driver: bridge
|
|
||||||
caddy_network: # ← Uncomment this line
|
|
||||||
external: true # ← Uncomment this line
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Replace `caddy_network` with your actual network name
|
|
||||||
|
|
||||||
3. Add the network to the web service (around line 20):
|
|
||||||
```yaml
|
|
||||||
web:
|
|
||||||
build: .
|
|
||||||
container_name: testarena_web
|
|
||||||
environment:
|
|
||||||
# ... environment variables ...
|
|
||||||
volumes:
|
|
||||||
# ... volumes ...
|
|
||||||
depends_on:
|
|
||||||
- db
|
|
||||||
networks:
|
|
||||||
- testarena_network
|
|
||||||
- YOUR_CADDY_NETWORK_NAME # ← Add this line with your network name
|
|
||||||
restart: unless-stopped
|
|
||||||
```
|
|
||||||
|
|
||||||
### Option B: Use this template
|
|
||||||
|
|
||||||
Replace the entire `networks` section at the bottom with:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
networks:
|
|
||||||
testarena_network:
|
|
||||||
driver: bridge
|
|
||||||
YOUR_CADDY_NETWORK_NAME:
|
|
||||||
external: true
|
|
||||||
```
|
|
||||||
|
|
||||||
And update the web service networks:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
networks:
|
|
||||||
- testarena_network
|
|
||||||
- YOUR_CADDY_NETWORK_NAME
|
|
||||||
```
|
|
||||||
|
|
||||||
## Step 3: Configure Caddyfile
|
|
||||||
|
|
||||||
Add this to your Caddyfile:
|
|
||||||
|
|
||||||
```
|
|
||||||
testarena.nabd-co.com {
|
|
||||||
reverse_proxy testarena_web:5000
|
|
||||||
|
|
||||||
# Optional: Enable compression
|
|
||||||
encode gzip
|
|
||||||
|
|
||||||
# Optional: Security headers
|
|
||||||
header {
|
|
||||||
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
|
|
||||||
X-Frame-Options "SAMEORIGIN"
|
|
||||||
X-Content-Type-Options "nosniff"
|
|
||||||
X-XSS-Protection "1; mode=block"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Optional: Logging
|
|
||||||
log {
|
|
||||||
output file /var/log/caddy/testarena.log
|
|
||||||
format json
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Step 4: Reload Caddy
|
|
||||||
|
|
||||||
After updating the Caddyfile:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker exec -it caddy_container_name caddy reload --config /etc/caddy/Caddyfile
|
|
||||||
```
|
|
||||||
|
|
||||||
Or restart the Caddy container:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker restart caddy_container_name
|
|
||||||
```
|
|
||||||
|
|
||||||
## Step 5: Start TestArena
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker-compose up -d --build
|
|
||||||
```
|
|
||||||
|
|
||||||
## Step 6: Verify
|
|
||||||
|
|
||||||
1. Check that containers are running:
|
|
||||||
```bash
|
|
||||||
docker ps | grep testarena
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Check that the web container is on both networks:
|
|
||||||
```bash
|
|
||||||
docker inspect testarena_web | grep -A 10 Networks
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Test the connection:
|
|
||||||
```bash
|
|
||||||
curl -I https://testarena.nabd-co.com
|
|
||||||
```
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Error: "network not found"
|
|
||||||
|
|
||||||
Your Caddy network name is incorrect. Double-check with:
|
|
||||||
```bash
|
|
||||||
docker network ls
|
|
||||||
```
|
|
||||||
|
|
||||||
### Error: "container not found"
|
|
||||||
|
|
||||||
Make sure Caddy is running:
|
|
||||||
```bash
|
|
||||||
docker ps | grep caddy
|
|
||||||
```
|
|
||||||
|
|
||||||
### Can't access via domain
|
|
||||||
|
|
||||||
1. Check DNS is pointing to your server
|
|
||||||
2. Verify Caddy is running: `docker ps`
|
|
||||||
3. Check Caddy logs: `docker logs caddy_container_name`
|
|
||||||
4. Check TestArena logs: `docker-compose logs web`
|
|
||||||
|
|
||||||
### 502 Bad Gateway
|
|
||||||
|
|
||||||
The web container might not be ready:
|
|
||||||
```bash
|
|
||||||
docker-compose logs web
|
|
||||||
```
|
|
||||||
|
|
||||||
Wait a few seconds for the database to initialize.
|
|
||||||
|
|
||||||
### Connection refused
|
|
||||||
|
|
||||||
1. Verify the web service is on the Caddy network:
|
|
||||||
```bash
|
|
||||||
docker network inspect YOUR_CADDY_NETWORK_NAME
|
|
||||||
```
|
|
||||||
|
|
||||||
2. You should see `testarena_web` in the containers list
|
|
||||||
|
|
||||||
## Network Architecture
|
|
||||||
|
|
||||||
```
|
|
||||||
Internet
|
|
||||||
↓
|
|
||||||
Caddy (HTTPS/443)
|
|
||||||
↓
|
|
||||||
testarena_web:5000 (Flask)
|
|
||||||
↓
|
|
||||||
testarena_db:5432 (PostgreSQL)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Security Notes
|
|
||||||
|
|
||||||
1. Caddy automatically handles HTTPS certificates via Let's Encrypt
|
|
||||||
2. All traffic between Caddy and TestArena is on the internal Docker network
|
|
||||||
3. Only Caddy needs to expose ports to the internet
|
|
||||||
4. Database is only accessible within the testarena_network
|
|
||||||
|
|
||||||
## Example: Complete docker-compose.yml
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
|
||||||
db:
|
|
||||||
image: postgres:15-alpine
|
|
||||||
container_name: testarena_db
|
|
||||||
environment:
|
|
||||||
POSTGRES_DB: testarena
|
|
||||||
POSTGRES_USER: testarena_user
|
|
||||||
POSTGRES_PASSWORD: your_secure_password
|
|
||||||
volumes:
|
|
||||||
- postgres_data:/var/lib/postgresql/data
|
|
||||||
networks:
|
|
||||||
- testarena_network
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
web:
|
|
||||||
build: .
|
|
||||||
container_name: testarena_web
|
|
||||||
environment:
|
|
||||||
DATABASE_URL: postgresql://testarena_user:your_secure_password@db:5432/testarena
|
|
||||||
SECRET_KEY: your_secret_key_here
|
|
||||||
FLASK_ENV: production
|
|
||||||
volumes:
|
|
||||||
- ./app:/app
|
|
||||||
- test_results:/app/test_results
|
|
||||||
depends_on:
|
|
||||||
- db
|
|
||||||
networks:
|
|
||||||
- testarena_network
|
|
||||||
- caddy_network # ← Your Caddy network name
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
postgres_data:
|
|
||||||
test_results:
|
|
||||||
|
|
||||||
networks:
|
|
||||||
testarena_network:
|
|
||||||
driver: bridge
|
|
||||||
caddy_network: # ← Your Caddy network name
|
|
||||||
external: true
|
|
||||||
```
|
|
||||||
|
|
||||||
## Need Help?
|
|
||||||
|
|
||||||
If you encounter issues:
|
|
||||||
|
|
||||||
1. Share your Caddy network name
|
|
||||||
2. Share any error messages from:
|
|
||||||
- `docker-compose logs web`
|
|
||||||
- `docker logs caddy_container_name`
|
|
||||||
3. Verify network connectivity:
|
|
||||||
- `docker network inspect YOUR_CADDY_NETWORK_NAME`
|
|
||||||
@@ -1,205 +0,0 @@
|
|||||||
# Deployment Checklist
|
|
||||||
|
|
||||||
## Pre-Deployment
|
|
||||||
|
|
||||||
### 1. Configuration Files
|
|
||||||
- [ ] Copy `.env.example` to `.env`
|
|
||||||
- [ ] Update `SECRET_KEY` in `.env` with a secure random string
|
|
||||||
- [ ] Update database password in `.env`
|
|
||||||
- [ ] Update database password in `docker-compose.yml` to match
|
|
||||||
|
|
||||||
### 2. Caddy Integration
|
|
||||||
- [ ] Find Caddy network name: `docker network ls`
|
|
||||||
- [ ] Update `docker-compose.yml` with Caddy network name (lines 20 and 28-29)
|
|
||||||
- [ ] Add TestArena configuration to Caddyfile
|
|
||||||
- [ ] Reload Caddy configuration
|
|
||||||
|
|
||||||
### 3. Security
|
|
||||||
- [ ] Generate strong SECRET_KEY (use: `python -c "import secrets; print(secrets.token_hex(32))"`)
|
|
||||||
- [ ] Set strong database password
|
|
||||||
- [ ] Review firewall rules
|
|
||||||
- [ ] Ensure only Caddy exposes ports to internet
|
|
||||||
|
|
||||||
## Deployment
|
|
||||||
|
|
||||||
### 4. Build and Start
|
|
||||||
- [ ] Run: `docker-compose up -d --build`
|
|
||||||
- [ ] Wait 30 seconds for database initialization
|
|
||||||
- [ ] Check containers are running: `docker ps`
|
|
||||||
- [ ] Check logs for errors: `docker-compose logs`
|
|
||||||
|
|
||||||
### 5. Verify Services
|
|
||||||
- [ ] Database container is running
|
|
||||||
- [ ] Web container is running
|
|
||||||
- [ ] Web container is on both networks (testarena_network and caddy_network)
|
|
||||||
- [ ] No error messages in logs
|
|
||||||
|
|
||||||
### 6. Test Access
|
|
||||||
- [ ] Access via domain: https://testarena.nabd-co.com
|
|
||||||
- [ ] Login page loads correctly
|
|
||||||
- [ ] Logo displays properly
|
|
||||||
- [ ] CSS styles are applied
|
|
||||||
|
|
||||||
## Post-Deployment
|
|
||||||
|
|
||||||
### 7. Initial Setup
|
|
||||||
- [ ] Login with default credentials (admin/admin123)
|
|
||||||
- [ ] Change admin password immediately
|
|
||||||
- [ ] Create test user account
|
|
||||||
- [ ] Test user login
|
|
||||||
- [ ] Verify admin can see admin dashboard
|
|
||||||
- [ ] Verify regular user cannot see admin dashboard
|
|
||||||
|
|
||||||
### 8. Functionality Tests
|
|
||||||
- [ ] Admin: Create new user
|
|
||||||
- [ ] Admin: Reset user password
|
|
||||||
- [ ] Admin: Delete user
|
|
||||||
- [ ] User: Access dashboard
|
|
||||||
- [ ] User: Start job submission workflow
|
|
||||||
- [ ] User: Complete all 5 steps of submission
|
|
||||||
- [ ] User: View job in dashboard
|
|
||||||
- [ ] User: Click job to see details
|
|
||||||
|
|
||||||
### 9. Security Hardening
|
|
||||||
- [ ] All default passwords changed
|
|
||||||
- [ ] Database not accessible from internet
|
|
||||||
- [ ] Only Caddy exposes ports
|
|
||||||
- [ ] HTTPS working correctly
|
|
||||||
- [ ] Security headers configured in Caddy
|
|
||||||
|
|
||||||
### 10. Monitoring Setup
|
|
||||||
- [ ] Set up log rotation
|
|
||||||
- [ ] Configure backup schedule for database
|
|
||||||
- [ ] Set up monitoring alerts
|
|
||||||
- [ ] Document backup restoration procedure
|
|
||||||
|
|
||||||
## Verification Commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check all containers
|
|
||||||
docker ps
|
|
||||||
|
|
||||||
# Check logs
|
|
||||||
docker-compose logs -f
|
|
||||||
|
|
||||||
# Check web container networks
|
|
||||||
docker inspect testarena_web | grep -A 10 Networks
|
|
||||||
|
|
||||||
# Check database connection
|
|
||||||
docker exec testarena_web python -c "from app import create_app, db; app = create_app(); app.app_context().push(); print('DB OK')"
|
|
||||||
|
|
||||||
# Test HTTP response
|
|
||||||
curl -I http://localhost:5000
|
|
||||||
|
|
||||||
# Test HTTPS response
|
|
||||||
curl -I https://testarena.nabd-co.com
|
|
||||||
```
|
|
||||||
|
|
||||||
## Rollback Plan
|
|
||||||
|
|
||||||
If deployment fails:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Stop containers
|
|
||||||
docker-compose down
|
|
||||||
|
|
||||||
# Remove volumes (WARNING: deletes data)
|
|
||||||
docker-compose down -v
|
|
||||||
|
|
||||||
# Check for issues
|
|
||||||
docker-compose logs
|
|
||||||
|
|
||||||
# Fix configuration
|
|
||||||
# ... make changes ...
|
|
||||||
|
|
||||||
# Retry deployment
|
|
||||||
docker-compose up -d --build
|
|
||||||
```
|
|
||||||
|
|
||||||
## Backup Procedure
|
|
||||||
|
|
||||||
### Database Backup
|
|
||||||
```bash
|
|
||||||
# Create backup
|
|
||||||
docker exec testarena_db pg_dump -U testarena_user testarena > backup_$(date +%Y%m%d).sql
|
|
||||||
|
|
||||||
# Restore backup
|
|
||||||
docker exec -i testarena_db psql -U testarena_user testarena < backup_20240101.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
### Full Backup
|
|
||||||
```bash
|
|
||||||
# Backup volumes
|
|
||||||
docker run --rm -v testarena_postgres_data:/data -v $(pwd):/backup alpine tar czf /backup/db_backup.tar.gz /data
|
|
||||||
docker run --rm -v testarena_test_results:/data -v $(pwd):/backup alpine tar czf /backup/results_backup.tar.gz /data
|
|
||||||
```
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Container won't start
|
|
||||||
1. Check logs: `docker-compose logs web`
|
|
||||||
2. Verify database is ready: `docker-compose logs db`
|
|
||||||
3. Check environment variables in docker-compose.yml
|
|
||||||
|
|
||||||
### Can't access via domain
|
|
||||||
1. Verify DNS: `nslookup testarena.nabd-co.com`
|
|
||||||
2. Check Caddy: `docker logs caddy_container_name`
|
|
||||||
3. Verify network: `docker network inspect caddy_network`
|
|
||||||
|
|
||||||
### Database connection error
|
|
||||||
1. Check DATABASE_URL format
|
|
||||||
2. Verify database container is running
|
|
||||||
3. Check database logs: `docker-compose logs db`
|
|
||||||
|
|
||||||
### 502 Bad Gateway
|
|
||||||
1. Web container not ready - wait 30 seconds
|
|
||||||
2. Check web logs: `docker-compose logs web`
|
|
||||||
3. Verify Gunicorn is running: `docker exec testarena_web ps aux`
|
|
||||||
|
|
||||||
## Success Criteria
|
|
||||||
|
|
||||||
✅ All containers running
|
|
||||||
✅ No errors in logs
|
|
||||||
✅ Login page accessible via HTTPS
|
|
||||||
✅ Admin can login and manage users
|
|
||||||
✅ Regular user can login and access dashboard
|
|
||||||
✅ Job submission workflow completes
|
|
||||||
✅ Jobs appear in dashboard
|
|
||||||
✅ Job details display correctly
|
|
||||||
|
|
||||||
## Post-Deployment Tasks
|
|
||||||
|
|
||||||
- [ ] Document any configuration changes
|
|
||||||
- [ ] Update team on new system
|
|
||||||
- [ ] Schedule training session
|
|
||||||
- [ ] Plan Phase 2 implementation
|
|
||||||
- [ ] Set up regular maintenance schedule
|
|
||||||
|
|
||||||
## Maintenance Schedule
|
|
||||||
|
|
||||||
### Daily
|
|
||||||
- Check logs for errors
|
|
||||||
- Verify all containers running
|
|
||||||
|
|
||||||
### Weekly
|
|
||||||
- Database backup
|
|
||||||
- Review disk usage
|
|
||||||
- Check for security updates
|
|
||||||
|
|
||||||
### Monthly
|
|
||||||
- Update Docker images
|
|
||||||
- Review user accounts
|
|
||||||
- Clean up old test results (automated)
|
|
||||||
- Performance review
|
|
||||||
|
|
||||||
## Support Contacts
|
|
||||||
|
|
||||||
- System Admin: [Your contact]
|
|
||||||
- Database Admin: [Your contact]
|
|
||||||
- Development Team: [Your contact]
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Deployment Date:** _______________
|
|
||||||
**Deployed By:** _______________
|
|
||||||
**Verified By:** _______________
|
|
||||||
@@ -1,501 +0,0 @@
|
|||||||
# 🎉 ASF TestArena - Deployment Summary
|
|
||||||
|
|
||||||
## ✅ READY TO DEPLOY - All Configuration Complete!
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📦 What's Been Delivered
|
|
||||||
|
|
||||||
### Phase 1: Complete Web Application ✅
|
|
||||||
|
|
||||||
**Backend (Flask + PostgreSQL)**
|
|
||||||
- User authentication system
|
|
||||||
- Role-based access control (Admin/User)
|
|
||||||
- User management (create, delete, reset password)
|
|
||||||
- Job submission workflow
|
|
||||||
- Dashboard with job tracking
|
|
||||||
- RESTful API endpoints
|
|
||||||
- Database models and relationships
|
|
||||||
|
|
||||||
**Frontend (HTML/CSS/JavaScript)**
|
|
||||||
- Modern gradient theme (purple/blue)
|
|
||||||
- Responsive design
|
|
||||||
- Custom logo integration
|
|
||||||
- Two-panel dashboard layout
|
|
||||||
- 5-step job submission wizard
|
|
||||||
- Status indicators with colored icons
|
|
||||||
- Modal dialogs and forms
|
|
||||||
|
|
||||||
**Infrastructure (Docker)**
|
|
||||||
- PostgreSQL 15 database container
|
|
||||||
- Flask web application with Gunicorn
|
|
||||||
- Configured networks:
|
|
||||||
- `app-network` (internal: web ↔ database)
|
|
||||||
- `caddy_network` (external: Caddy ↔ web)
|
|
||||||
- Persistent volumes for data
|
|
||||||
- Automated deployment scripts
|
|
||||||
|
|
||||||
**Documentation (12 Comprehensive Guides)**
|
|
||||||
- Quick start guides
|
|
||||||
- Deployment instructions
|
|
||||||
- Architecture documentation
|
|
||||||
- Troubleshooting guides
|
|
||||||
- API documentation
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 Deploy Now - Simple Commands
|
|
||||||
|
|
||||||
### Windows (PowerShell) - Recommended
|
|
||||||
```powershell
|
|
||||||
.\deploy.ps1
|
|
||||||
```
|
|
||||||
|
|
||||||
### Windows (Command Prompt)
|
|
||||||
```cmd
|
|
||||||
start.bat
|
|
||||||
```
|
|
||||||
|
|
||||||
### Linux/Mac
|
|
||||||
```bash
|
|
||||||
chmod +x deploy.sh
|
|
||||||
./deploy.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
**That's it!** The script handles everything automatically.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔧 Network Configuration - DONE ✅
|
|
||||||
|
|
||||||
Your docker-compose.yml is now configured with:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
networks:
|
|
||||||
app-network: # Internal network (web ↔ db)
|
|
||||||
driver: bridge
|
|
||||||
caddy_network: # External network (Caddy ↔ web)
|
|
||||||
external: true
|
|
||||||
```
|
|
||||||
|
|
||||||
Both containers are properly connected:
|
|
||||||
- **Database:** `app-network` only (secure, internal)
|
|
||||||
- **Web:** `app-network` + `caddy_network` (accessible to Caddy)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📋 Deployment Script Features
|
|
||||||
|
|
||||||
The automated deployment script will:
|
|
||||||
|
|
||||||
1. ✅ **Check Prerequisites**
|
|
||||||
- Docker installed and running
|
|
||||||
- Docker Compose available
|
|
||||||
- Sufficient permissions
|
|
||||||
|
|
||||||
2. ✅ **Prepare Environment**
|
|
||||||
- Create `.env` file if missing
|
|
||||||
- Verify/create `caddy_network`
|
|
||||||
- Stop existing containers
|
|
||||||
|
|
||||||
3. ✅ **Build & Deploy**
|
|
||||||
- Build Docker images
|
|
||||||
- Start all services
|
|
||||||
- Wait for initialization
|
|
||||||
|
|
||||||
4. ✅ **Verify Deployment**
|
|
||||||
- Check containers are running
|
|
||||||
- Verify no errors in logs
|
|
||||||
- Display access information
|
|
||||||
|
|
||||||
5. ✅ **Provide Instructions**
|
|
||||||
- Access URLs
|
|
||||||
- Default credentials
|
|
||||||
- Useful commands
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🌐 Access Information
|
|
||||||
|
|
||||||
### After Deployment
|
|
||||||
|
|
||||||
**Local Access:**
|
|
||||||
```
|
|
||||||
http://localhost:5000
|
|
||||||
```
|
|
||||||
|
|
||||||
**Domain Access (with Caddy):**
|
|
||||||
```
|
|
||||||
https://testarena.nabd-co.com
|
|
||||||
```
|
|
||||||
|
|
||||||
**Default Login:**
|
|
||||||
- Username: `admin`
|
|
||||||
- Password: `admin123`
|
|
||||||
|
|
||||||
⚠️ **CRITICAL:** Change the admin password immediately after first login!
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔐 Caddy Configuration
|
|
||||||
|
|
||||||
Add this to your Caddyfile:
|
|
||||||
|
|
||||||
```
|
|
||||||
testarena.nabd-co.com {
|
|
||||||
reverse_proxy testarena_web:5000
|
|
||||||
|
|
||||||
encode gzip
|
|
||||||
|
|
||||||
header {
|
|
||||||
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
|
|
||||||
X-Frame-Options "SAMEORIGIN"
|
|
||||||
X-Content-Type-Options "nosniff"
|
|
||||||
X-XSS-Protection "1; mode=block"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Reload Caddy:
|
|
||||||
```bash
|
|
||||||
docker exec caddy_container caddy reload --config /etc/caddy/Caddyfile
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ✅ Post-Deployment Checklist
|
|
||||||
|
|
||||||
After running the deployment script:
|
|
||||||
|
|
||||||
### Immediate Tasks
|
|
||||||
- [ ] Verify containers are running: `docker-compose ps`
|
|
||||||
- [ ] Check logs: `docker-compose logs`
|
|
||||||
- [ ] Access login page
|
|
||||||
- [ ] Login with default credentials
|
|
||||||
- [ ] **Change admin password**
|
|
||||||
|
|
||||||
### Setup Tasks
|
|
||||||
- [ ] Create test user account
|
|
||||||
- [ ] Test user login
|
|
||||||
- [ ] Verify admin dashboard works
|
|
||||||
- [ ] Test job submission workflow
|
|
||||||
- [ ] Verify role-based access control
|
|
||||||
|
|
||||||
### Security Tasks
|
|
||||||
- [ ] Update SECRET_KEY in docker-compose.yml
|
|
||||||
- [ ] Update database password
|
|
||||||
- [ ] Configure Caddy for HTTPS
|
|
||||||
- [ ] Review firewall rules
|
|
||||||
- [ ] Set up automated backups
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📊 What's Working (Phase 1)
|
|
||||||
|
|
||||||
### ✅ Fully Functional Features
|
|
||||||
|
|
||||||
1. **Authentication System**
|
|
||||||
- Secure login/logout
|
|
||||||
- Session management
|
|
||||||
- Password hashing
|
|
||||||
|
|
||||||
2. **Admin Dashboard**
|
|
||||||
- Create users with roles
|
|
||||||
- Delete users
|
|
||||||
- Reset passwords
|
|
||||||
- View all users
|
|
||||||
- View all jobs
|
|
||||||
|
|
||||||
3. **User Dashboard**
|
|
||||||
- View own jobs
|
|
||||||
- Two-panel layout
|
|
||||||
- Job list with status icons
|
|
||||||
- Job details view
|
|
||||||
- Submit new jobs
|
|
||||||
|
|
||||||
4. **Job Submission**
|
|
||||||
- 5-step wizard
|
|
||||||
- Branch name input
|
|
||||||
- Scenario selection
|
|
||||||
- Environment choice
|
|
||||||
- Test mode selection
|
|
||||||
- Options configuration
|
|
||||||
|
|
||||||
5. **Database**
|
|
||||||
- User accounts
|
|
||||||
- Job records
|
|
||||||
- Persistent storage
|
|
||||||
- Relationships
|
|
||||||
|
|
||||||
6. **UI/UX**
|
|
||||||
- Modern gradient theme
|
|
||||||
- Responsive design
|
|
||||||
- Status indicators
|
|
||||||
- Alert messages
|
|
||||||
- Modal dialogs
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ⏳ Phase 2 Features (Pending)
|
|
||||||
|
|
||||||
These will be implemented next:
|
|
||||||
|
|
||||||
1. **Git Integration**
|
|
||||||
- Branch checkout
|
|
||||||
- Scenario detection script
|
|
||||||
|
|
||||||
2. **Test Execution**
|
|
||||||
- Background job processing
|
|
||||||
- Real-time status updates
|
|
||||||
- Process management
|
|
||||||
|
|
||||||
3. **Results Management**
|
|
||||||
- HTML report generation
|
|
||||||
- Results storage
|
|
||||||
- 7-day automatic cleanup
|
|
||||||
|
|
||||||
4. **Job Control**
|
|
||||||
- Abort running jobs
|
|
||||||
- Kill processes
|
|
||||||
- Progress tracking
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🛠️ Useful Commands
|
|
||||||
|
|
||||||
### Container Management
|
|
||||||
```bash
|
|
||||||
# View logs (real-time)
|
|
||||||
docker-compose logs -f
|
|
||||||
|
|
||||||
# Check status
|
|
||||||
docker-compose ps
|
|
||||||
|
|
||||||
# Restart services
|
|
||||||
docker-compose restart
|
|
||||||
|
|
||||||
# Stop services
|
|
||||||
docker-compose down
|
|
||||||
|
|
||||||
# Rebuild and restart
|
|
||||||
docker-compose up -d --build
|
|
||||||
```
|
|
||||||
|
|
||||||
### Database Operations
|
|
||||||
```bash
|
|
||||||
# Backup database
|
|
||||||
docker exec testarena_db pg_dump -U testarena_user testarena > backup.sql
|
|
||||||
|
|
||||||
# Restore database
|
|
||||||
docker exec -i testarena_db psql -U testarena_user testarena < backup.sql
|
|
||||||
|
|
||||||
# Access database shell
|
|
||||||
docker exec -it testarena_db psql -U testarena_user testarena
|
|
||||||
```
|
|
||||||
|
|
||||||
### Debugging
|
|
||||||
```bash
|
|
||||||
# Access web container shell
|
|
||||||
docker exec -it testarena_web bash
|
|
||||||
|
|
||||||
# Check Gunicorn processes
|
|
||||||
docker exec testarena_web ps aux | grep gunicorn
|
|
||||||
|
|
||||||
# Test database connection
|
|
||||||
docker-compose exec web python -c "from app import create_app, db; app = create_app(); app.app_context().push(); print('DB OK')"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🐛 Quick Troubleshooting
|
|
||||||
|
|
||||||
### Issue: Deployment script fails
|
|
||||||
|
|
||||||
**Solution:**
|
|
||||||
```bash
|
|
||||||
# Check Docker is running
|
|
||||||
docker info
|
|
||||||
|
|
||||||
# Check Docker Compose
|
|
||||||
docker-compose --version
|
|
||||||
|
|
||||||
# Run with sudo (Linux/Mac)
|
|
||||||
sudo ./deploy.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
### Issue: Containers won't start
|
|
||||||
|
|
||||||
**Solution:**
|
|
||||||
```bash
|
|
||||||
# View detailed logs
|
|
||||||
docker-compose logs
|
|
||||||
|
|
||||||
# Check specific service
|
|
||||||
docker-compose logs web
|
|
||||||
docker-compose logs db
|
|
||||||
```
|
|
||||||
|
|
||||||
### Issue: Can't access website
|
|
||||||
|
|
||||||
**Solution:**
|
|
||||||
```bash
|
|
||||||
# Check containers are running
|
|
||||||
docker-compose ps
|
|
||||||
|
|
||||||
# Test local access
|
|
||||||
curl http://localhost:5000
|
|
||||||
|
|
||||||
# Check web container
|
|
||||||
docker-compose logs web
|
|
||||||
```
|
|
||||||
|
|
||||||
### Issue: 502 Bad Gateway
|
|
||||||
|
|
||||||
**Solution:**
|
|
||||||
- Wait 30-60 seconds for initialization
|
|
||||||
- Check Gunicorn is running: `docker exec testarena_web ps aux | grep gunicorn`
|
|
||||||
- Verify Caddy can reach web container
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📚 Documentation Reference
|
|
||||||
|
|
||||||
| Document | Purpose | When to Use |
|
|
||||||
|----------|---------|-------------|
|
|
||||||
| **READY_TO_DEPLOY.md** | Deployment overview | Before deploying |
|
|
||||||
| **DEPLOY_GUIDE.md** | Comprehensive guide | During deployment |
|
|
||||||
| **START_HERE.md** | Quick start | First time users |
|
|
||||||
| **QUICK_START.md** | Fast reference | Quick lookup |
|
|
||||||
| **INDEX.md** | Documentation index | Finding information |
|
|
||||||
| **ARCHITECTURE.md** | System design | Understanding structure |
|
|
||||||
| **TROUBLESHOOTING.md** | Common issues | When problems occur |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 Deployment Timeline
|
|
||||||
|
|
||||||
**Estimated Time:** 5-10 minutes
|
|
||||||
|
|
||||||
1. **Run deployment script** - 1 minute
|
|
||||||
2. **Build Docker images** - 2-4 minutes (first time)
|
|
||||||
3. **Start containers** - 30 seconds
|
|
||||||
4. **Verify deployment** - 1 minute
|
|
||||||
5. **Initial setup** - 2-3 minutes
|
|
||||||
- Login
|
|
||||||
- Change password
|
|
||||||
- Create users
|
|
||||||
|
|
||||||
**Total:** ~5-10 minutes to fully operational system
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎉 Success Criteria
|
|
||||||
|
|
||||||
Your deployment is successful when:
|
|
||||||
|
|
||||||
✅ Both containers are running (`docker-compose ps`)
|
|
||||||
✅ No errors in logs (`docker-compose logs`)
|
|
||||||
✅ Login page loads
|
|
||||||
✅ Can login with default credentials
|
|
||||||
✅ Admin dashboard accessible
|
|
||||||
✅ Can create users
|
|
||||||
✅ Can submit jobs (UI workflow)
|
|
||||||
✅ Dashboard displays jobs
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📞 Support & Help
|
|
||||||
|
|
||||||
### Documentation
|
|
||||||
- Read **DEPLOY_GUIDE.md** for detailed instructions
|
|
||||||
- Check **INDEX.md** for documentation guide
|
|
||||||
- Review **TROUBLESHOOTING.md** for common issues
|
|
||||||
|
|
||||||
### Logs
|
|
||||||
```bash
|
|
||||||
# View all logs
|
|
||||||
docker-compose logs -f
|
|
||||||
|
|
||||||
# View specific service
|
|
||||||
docker-compose logs -f web
|
|
||||||
docker-compose logs -f db
|
|
||||||
```
|
|
||||||
|
|
||||||
### Community
|
|
||||||
- Check GitHub issues
|
|
||||||
- Review documentation
|
|
||||||
- Contact development team
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 Next Steps After Deployment
|
|
||||||
|
|
||||||
### Immediate (Day 1)
|
|
||||||
1. ✅ Deploy application
|
|
||||||
2. ✅ Change admin password
|
|
||||||
3. ✅ Create user accounts
|
|
||||||
4. ✅ Test all features
|
|
||||||
5. ✅ Configure Caddy for HTTPS
|
|
||||||
|
|
||||||
### Short Term (Week 1)
|
|
||||||
1. ⏳ Set up automated backups
|
|
||||||
2. ⏳ Configure monitoring
|
|
||||||
3. ⏳ Train users
|
|
||||||
4. ⏳ Document workflows
|
|
||||||
5. ⏳ Plan Phase 2
|
|
||||||
|
|
||||||
### Long Term (Month 1)
|
|
||||||
1. ⏳ Implement Phase 2 (test execution)
|
|
||||||
2. ⏳ Add real-time updates
|
|
||||||
3. ⏳ Integrate with Git
|
|
||||||
4. ⏳ Generate test results
|
|
||||||
5. ⏳ Implement cleanup automation
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📝 Deployment Record
|
|
||||||
|
|
||||||
**Project:** ASF TestArena
|
|
||||||
**Version:** 1.0.0 (Phase 1)
|
|
||||||
**Status:** ✅ Ready to Deploy
|
|
||||||
**Date:** November 28, 2024
|
|
||||||
|
|
||||||
**Configuration:**
|
|
||||||
- ✅ Docker Compose configured
|
|
||||||
- ✅ Networks configured (app-network, caddy_network)
|
|
||||||
- ✅ Deployment scripts created
|
|
||||||
- ✅ Documentation complete
|
|
||||||
- ✅ Security features implemented
|
|
||||||
|
|
||||||
**Deployment Command:**
|
|
||||||
```powershell
|
|
||||||
.\deploy.ps1
|
|
||||||
```
|
|
||||||
|
|
||||||
**Access URL:**
|
|
||||||
```
|
|
||||||
https://testarena.nabd-co.com
|
|
||||||
```
|
|
||||||
|
|
||||||
**Default Credentials:**
|
|
||||||
```
|
|
||||||
Username: admin
|
|
||||||
Password: admin123
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎊 You're All Set!
|
|
||||||
|
|
||||||
Everything is configured and ready for deployment. Just run the deployment script and you'll have a fully functional test management platform in minutes!
|
|
||||||
|
|
||||||
**Deploy now:**
|
|
||||||
```powershell
|
|
||||||
.\deploy.ps1
|
|
||||||
```
|
|
||||||
|
|
||||||
**Good luck! 🚀**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*For questions or issues, refer to the comprehensive documentation in the project root.*
|
|
||||||
387
DEPLOY_GUIDE.md
387
DEPLOY_GUIDE.md
@@ -1,387 +0,0 @@
|
|||||||
# ASF TestArena - Deployment Guide
|
|
||||||
|
|
||||||
## ✅ Network Configuration Complete
|
|
||||||
|
|
||||||
The docker-compose.yml has been configured with:
|
|
||||||
- **Internal Network:** `app-network` (for web ↔ database communication)
|
|
||||||
- **External Network:** `caddy_network` (for Caddy ↔ web communication)
|
|
||||||
|
|
||||||
## 🚀 Quick Deployment
|
|
||||||
|
|
||||||
### Option 1: Automated Deployment (Recommended)
|
|
||||||
|
|
||||||
**Windows (PowerShell):**
|
|
||||||
```powershell
|
|
||||||
.\deploy.ps1
|
|
||||||
```
|
|
||||||
|
|
||||||
**Windows (Command Prompt):**
|
|
||||||
```cmd
|
|
||||||
start.bat
|
|
||||||
```
|
|
||||||
|
|
||||||
**Linux/Mac:**
|
|
||||||
```bash
|
|
||||||
chmod +x deploy.sh
|
|
||||||
./deploy.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
The deployment script will:
|
|
||||||
1. ✅ Check Docker and Docker Compose are installed
|
|
||||||
2. ✅ Verify Docker daemon is running
|
|
||||||
3. ✅ Create `.env` file if missing
|
|
||||||
4. ✅ Check/create `caddy_network` if needed
|
|
||||||
5. ✅ Stop existing containers
|
|
||||||
6. ✅ Build and start new containers
|
|
||||||
7. ✅ Verify all services are running
|
|
||||||
8. ✅ Display access information
|
|
||||||
|
|
||||||
### Option 2: Manual Deployment
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Create .env file (optional)
|
|
||||||
cp .env.example .env
|
|
||||||
# Edit .env with your values
|
|
||||||
|
|
||||||
# 2. Ensure Caddy network exists
|
|
||||||
docker network create caddy_network
|
|
||||||
|
|
||||||
# 3. Build and start
|
|
||||||
docker-compose up -d --build
|
|
||||||
|
|
||||||
# 4. Check status
|
|
||||||
docker-compose ps
|
|
||||||
docker-compose logs -f
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔧 Configuration
|
|
||||||
|
|
||||||
### Environment Variables
|
|
||||||
|
|
||||||
The `.env` file (optional) can override these defaults:
|
|
||||||
|
|
||||||
```env
|
|
||||||
DATABASE_URL=postgresql://testarena_user:YOUR_PASSWORD@db:5432/testarena
|
|
||||||
SECRET_KEY=YOUR_SECURE_SECRET_KEY
|
|
||||||
FLASK_ENV=production
|
|
||||||
```
|
|
||||||
|
|
||||||
**Generate a secure SECRET_KEY:**
|
|
||||||
|
|
||||||
**Python:**
|
|
||||||
```bash
|
|
||||||
python -c "import secrets; print(secrets.token_hex(32))"
|
|
||||||
```
|
|
||||||
|
|
||||||
**PowerShell:**
|
|
||||||
```powershell
|
|
||||||
-join ((48..57) + (65..90) + (97..122) | Get-Random -Count 64 | % {[char]$_})
|
|
||||||
```
|
|
||||||
|
|
||||||
**Linux:**
|
|
||||||
```bash
|
|
||||||
openssl rand -hex 32
|
|
||||||
```
|
|
||||||
|
|
||||||
### Database Password
|
|
||||||
|
|
||||||
Update in `docker-compose.yml`:
|
|
||||||
```yaml
|
|
||||||
environment:
|
|
||||||
POSTGRES_PASSWORD: YOUR_SECURE_PASSWORD
|
|
||||||
DATABASE_URL: postgresql://testarena_user:YOUR_SECURE_PASSWORD@db:5432/testarena
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🌐 Caddy Configuration
|
|
||||||
|
|
||||||
Add this to your Caddyfile:
|
|
||||||
|
|
||||||
```
|
|
||||||
testarena.nabd-co.com {
|
|
||||||
reverse_proxy testarena_web:5000
|
|
||||||
|
|
||||||
encode gzip
|
|
||||||
|
|
||||||
header {
|
|
||||||
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
|
|
||||||
X-Frame-Options "SAMEORIGIN"
|
|
||||||
X-Content-Type-Options "nosniff"
|
|
||||||
X-XSS-Protection "1; mode=block"
|
|
||||||
}
|
|
||||||
|
|
||||||
log {
|
|
||||||
output file /var/log/caddy/testarena.log
|
|
||||||
format json
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Reload Caddy:
|
|
||||||
```bash
|
|
||||||
docker exec caddy_container caddy reload --config /etc/caddy/Caddyfile
|
|
||||||
```
|
|
||||||
|
|
||||||
## ✅ Verification
|
|
||||||
|
|
||||||
### 1. Check Containers
|
|
||||||
```bash
|
|
||||||
docker-compose ps
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected output:
|
|
||||||
```
|
|
||||||
Name Command State Ports
|
|
||||||
----------------------------------------------------------
|
|
||||||
testarena_db docker-entrypoint.sh postgres Up 5432/tcp
|
|
||||||
testarena_web gunicorn --bind 0.0.0.0:5000... Up 5000/tcp
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Check Logs
|
|
||||||
```bash
|
|
||||||
# All logs
|
|
||||||
docker-compose logs
|
|
||||||
|
|
||||||
# Follow logs
|
|
||||||
docker-compose logs -f
|
|
||||||
|
|
||||||
# Specific service
|
|
||||||
docker-compose logs web
|
|
||||||
docker-compose logs db
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Check Networks
|
|
||||||
```bash
|
|
||||||
# Verify web container is on both networks
|
|
||||||
docker inspect testarena_web | grep -A 10 Networks
|
|
||||||
```
|
|
||||||
|
|
||||||
Should show both `app-network` and `caddy_network`.
|
|
||||||
|
|
||||||
### 4. Test Access
|
|
||||||
|
|
||||||
**Local:**
|
|
||||||
```bash
|
|
||||||
curl http://localhost:5000
|
|
||||||
```
|
|
||||||
|
|
||||||
**Domain:**
|
|
||||||
```bash
|
|
||||||
curl https://testarena.nabd-co.com
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. Test Login
|
|
||||||
|
|
||||||
1. Open browser: https://testarena.nabd-co.com
|
|
||||||
2. Login with:
|
|
||||||
- Username: `admin`
|
|
||||||
- Password: `admin123`
|
|
||||||
3. **Change password immediately!**
|
|
||||||
|
|
||||||
## 🔐 Post-Deployment Security
|
|
||||||
|
|
||||||
### 1. Change Admin Password
|
|
||||||
1. Login as admin
|
|
||||||
2. Go to Admin Dashboard
|
|
||||||
3. Reset admin password
|
|
||||||
|
|
||||||
### 2. Update Secrets
|
|
||||||
```bash
|
|
||||||
# Edit docker-compose.yml
|
|
||||||
nano docker-compose.yml
|
|
||||||
|
|
||||||
# Update:
|
|
||||||
# - SECRET_KEY
|
|
||||||
# - POSTGRES_PASSWORD
|
|
||||||
# - DATABASE_URL password
|
|
||||||
|
|
||||||
# Restart
|
|
||||||
docker-compose down
|
|
||||||
docker-compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Create Users
|
|
||||||
1. Login as admin
|
|
||||||
2. Go to Admin Dashboard
|
|
||||||
3. Create user accounts for your team
|
|
||||||
|
|
||||||
## 📊 Monitoring
|
|
||||||
|
|
||||||
### View Logs
|
|
||||||
```bash
|
|
||||||
# Real-time logs
|
|
||||||
docker-compose logs -f
|
|
||||||
|
|
||||||
# Last 100 lines
|
|
||||||
docker-compose logs --tail=100
|
|
||||||
|
|
||||||
# Specific service
|
|
||||||
docker-compose logs -f web
|
|
||||||
```
|
|
||||||
|
|
||||||
### Check Resource Usage
|
|
||||||
```bash
|
|
||||||
docker stats testarena_web testarena_db
|
|
||||||
```
|
|
||||||
|
|
||||||
### Database Backup
|
|
||||||
```bash
|
|
||||||
# Create backup
|
|
||||||
docker exec testarena_db pg_dump -U testarena_user testarena > backup_$(date +%Y%m%d).sql
|
|
||||||
|
|
||||||
# Restore backup
|
|
||||||
docker exec -i testarena_db psql -U testarena_user testarena < backup_20241128.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🛠️ Maintenance
|
|
||||||
|
|
||||||
### Restart Services
|
|
||||||
```bash
|
|
||||||
# Restart all
|
|
||||||
docker-compose restart
|
|
||||||
|
|
||||||
# Restart specific service
|
|
||||||
docker-compose restart web
|
|
||||||
docker-compose restart db
|
|
||||||
```
|
|
||||||
|
|
||||||
### Update Application
|
|
||||||
```bash
|
|
||||||
# Pull latest changes
|
|
||||||
git pull
|
|
||||||
|
|
||||||
# Rebuild and restart
|
|
||||||
docker-compose up -d --build
|
|
||||||
```
|
|
||||||
|
|
||||||
### Stop Services
|
|
||||||
```bash
|
|
||||||
# Stop containers (keep data)
|
|
||||||
docker-compose down
|
|
||||||
|
|
||||||
# Stop and remove volumes (DELETE DATA!)
|
|
||||||
docker-compose down -v
|
|
||||||
```
|
|
||||||
|
|
||||||
### View Container Shell
|
|
||||||
```bash
|
|
||||||
# Web container
|
|
||||||
docker exec -it testarena_web bash
|
|
||||||
|
|
||||||
# Database container
|
|
||||||
docker exec -it testarena_db psql -U testarena_user testarena
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🐛 Troubleshooting
|
|
||||||
|
|
||||||
### Container Won't Start
|
|
||||||
|
|
||||||
**Check logs:**
|
|
||||||
```bash
|
|
||||||
docker-compose logs web
|
|
||||||
```
|
|
||||||
|
|
||||||
**Common issues:**
|
|
||||||
- Database not ready: Wait 30 seconds
|
|
||||||
- Port conflict: Check if port 5000 is in use
|
|
||||||
- Network issue: Verify `caddy_network` exists
|
|
||||||
|
|
||||||
### Database Connection Error
|
|
||||||
|
|
||||||
**Check DATABASE_URL:**
|
|
||||||
```bash
|
|
||||||
docker-compose exec web env | grep DATABASE_URL
|
|
||||||
```
|
|
||||||
|
|
||||||
**Test connection:**
|
|
||||||
```bash
|
|
||||||
docker-compose exec web python -c "from app import create_app, db; app = create_app(); app.app_context().push(); print('DB OK')"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Can't Access via Domain
|
|
||||||
|
|
||||||
**Check Caddy:**
|
|
||||||
```bash
|
|
||||||
docker logs caddy_container_name
|
|
||||||
```
|
|
||||||
|
|
||||||
**Check network:**
|
|
||||||
```bash
|
|
||||||
docker network inspect caddy_network
|
|
||||||
```
|
|
||||||
|
|
||||||
Should show `testarena_web` in containers list.
|
|
||||||
|
|
||||||
**Check DNS:**
|
|
||||||
```bash
|
|
||||||
nslookup testarena.nabd-co.com
|
|
||||||
```
|
|
||||||
|
|
||||||
### 502 Bad Gateway
|
|
||||||
|
|
||||||
**Wait for initialization:**
|
|
||||||
```bash
|
|
||||||
# Web container may still be starting
|
|
||||||
sleep 10
|
|
||||||
curl http://localhost:5000
|
|
||||||
```
|
|
||||||
|
|
||||||
**Check web container:**
|
|
||||||
```bash
|
|
||||||
docker-compose logs web
|
|
||||||
docker exec testarena_web ps aux | grep gunicorn
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📋 Deployment Checklist
|
|
||||||
|
|
||||||
- [ ] Docker and Docker Compose installed
|
|
||||||
- [ ] Docker daemon running
|
|
||||||
- [ ] Caddy network exists (`docker network ls`)
|
|
||||||
- [ ] `.env` file configured (optional)
|
|
||||||
- [ ] Secrets updated in docker-compose.yml
|
|
||||||
- [ ] Caddyfile configured
|
|
||||||
- [ ] DNS pointing to server
|
|
||||||
- [ ] Deployment script executed
|
|
||||||
- [ ] Containers running (`docker-compose ps`)
|
|
||||||
- [ ] No errors in logs (`docker-compose logs`)
|
|
||||||
- [ ] Login page accessible
|
|
||||||
- [ ] Admin login works
|
|
||||||
- [ ] Admin password changed
|
|
||||||
- [ ] Test users created
|
|
||||||
- [ ] All features tested
|
|
||||||
|
|
||||||
## 🎉 Success!
|
|
||||||
|
|
||||||
If all checks pass, your ASF TestArena is now running!
|
|
||||||
|
|
||||||
**Access URLs:**
|
|
||||||
- Local: http://localhost:5000
|
|
||||||
- Domain: https://testarena.nabd-co.com
|
|
||||||
|
|
||||||
**Default Credentials:**
|
|
||||||
- Username: `admin`
|
|
||||||
- Password: `admin123`
|
|
||||||
|
|
||||||
**⚠️ CHANGE THE PASSWORD IMMEDIATELY!**
|
|
||||||
|
|
||||||
## 📞 Need Help?
|
|
||||||
|
|
||||||
- Check logs: `docker-compose logs -f`
|
|
||||||
- Review: [TROUBLESHOOTING.md](TROUBLESHOOTING.md)
|
|
||||||
- Read: [START_HERE.md](START_HERE.md)
|
|
||||||
- Index: [INDEX.md](INDEX.md)
|
|
||||||
|
|
||||||
## 🚀 Next Steps
|
|
||||||
|
|
||||||
1. Change admin password
|
|
||||||
2. Create user accounts
|
|
||||||
3. Test job submission workflow
|
|
||||||
4. Set up automated backups
|
|
||||||
5. Configure monitoring
|
|
||||||
6. Plan Phase 2 implementation
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Deployment Date:** _______________
|
|
||||||
**Deployed By:** _______________
|
|
||||||
**Server:** _______________
|
|
||||||
**Domain:** testarena.nabd-co.com
|
|
||||||
@@ -1,378 +0,0 @@
|
|||||||
# Final Verification Checklist
|
|
||||||
|
|
||||||
## ✅ Phase 1 Implementation - Complete
|
|
||||||
|
|
||||||
Use this checklist to verify everything is in place before deployment.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📦 Files Verification
|
|
||||||
|
|
||||||
### Core Application Files
|
|
||||||
- [x] `app/__init__.py` - Flask app factory
|
|
||||||
- [x] `app/models.py` - Database models
|
|
||||||
- [x] `app/routes/auth.py` - Authentication routes
|
|
||||||
- [x] `app/routes/admin.py` - Admin routes
|
|
||||||
- [x] `app/routes/dashboard.py` - Dashboard routes
|
|
||||||
- [x] `app/routes/jobs.py` - Job routes
|
|
||||||
|
|
||||||
### Template Files
|
|
||||||
- [x] `app/templates/base.html` - Base template
|
|
||||||
- [x] `app/templates/login.html` - Login page
|
|
||||||
- [x] `app/templates/admin/dashboard.html` - Admin UI
|
|
||||||
- [x] `app/templates/dashboard/index.html` - User dashboard
|
|
||||||
- [x] `app/templates/jobs/submit.html` - Step 1
|
|
||||||
- [x] `app/templates/jobs/submit_step2.html` - Step 2
|
|
||||||
- [x] `app/templates/jobs/submit_step3.html` - Step 3
|
|
||||||
- [x] `app/templates/jobs/submit_step4.html` - Step 4
|
|
||||||
|
|
||||||
### Static Files
|
|
||||||
- [x] `app/static/css/style.css` - Modern theme
|
|
||||||
- [x] `app/static/uploads/icon.png` - Logo
|
|
||||||
|
|
||||||
### Configuration Files
|
|
||||||
- [x] `docker-compose.yml` - Container setup
|
|
||||||
- [x] `Dockerfile` - Web app image
|
|
||||||
- [x] `requirements.txt` - Python dependencies
|
|
||||||
- [x] `wsgi.py` - WSGI entry point
|
|
||||||
- [x] `.env.example` - Environment template
|
|
||||||
- [x] `.gitignore` - Git ignore rules
|
|
||||||
- [x] `Caddyfile.example` - Caddy template
|
|
||||||
|
|
||||||
### Scripts
|
|
||||||
- [x] `start.bat` - Windows startup
|
|
||||||
- [x] `stop.bat` - Windows shutdown
|
|
||||||
- [x] `logs.bat` - View logs
|
|
||||||
|
|
||||||
### Documentation
|
|
||||||
- [x] `START_HERE.md` - Quick start
|
|
||||||
- [x] `INDEX.md` - Documentation index
|
|
||||||
- [x] `QUICK_START.md` - Fast reference
|
|
||||||
- [x] `DEPLOYMENT_CHECKLIST.md` - Pre-production
|
|
||||||
- [x] `CADDY_INTEGRATION.md` - Caddy setup
|
|
||||||
- [x] `SETUP.md` - Detailed guide
|
|
||||||
- [x] `PROJECT_STATUS.md` - Status overview
|
|
||||||
- [x] `ARCHITECTURE.md` - System design
|
|
||||||
- [x] `IMPLEMENTATION_SUMMARY.md` - Phase 1 summary
|
|
||||||
- [x] `README.md` - General overview
|
|
||||||
- [x] `PROJECT_TREE.txt` - File structure
|
|
||||||
- [x] `FINAL_CHECKLIST.md` - This file
|
|
||||||
|
|
||||||
**Total Files:** 43 files ✅
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎨 Features Verification
|
|
||||||
|
|
||||||
### Authentication System
|
|
||||||
- [x] Login page with logo
|
|
||||||
- [x] Secure password hashing
|
|
||||||
- [x] Session management
|
|
||||||
- [x] Logout functionality
|
|
||||||
- [x] Login required decorators
|
|
||||||
- [x] Default admin account
|
|
||||||
|
|
||||||
### Admin Features
|
|
||||||
- [x] Admin dashboard page
|
|
||||||
- [x] Create user with role
|
|
||||||
- [x] Delete user (with protection)
|
|
||||||
- [x] Reset user password
|
|
||||||
- [x] View all users
|
|
||||||
- [x] View all jobs
|
|
||||||
- [x] Admin-only access control
|
|
||||||
|
|
||||||
### User Features
|
|
||||||
- [x] User dashboard page
|
|
||||||
- [x] View own jobs only
|
|
||||||
- [x] Submit new jobs
|
|
||||||
- [x] View job details
|
|
||||||
- [x] Job list with status icons
|
|
||||||
- [x] Two-panel layout
|
|
||||||
|
|
||||||
### Job Submission
|
|
||||||
- [x] Step 1: Branch name input
|
|
||||||
- [x] Step 2: Scenario selection
|
|
||||||
- [x] Step 3: Environment choice
|
|
||||||
- [x] Step 4: Test mode + options
|
|
||||||
- [x] Step 5: Job creation
|
|
||||||
- [x] Progress indicator
|
|
||||||
- [x] Form validation
|
|
||||||
- [x] Back/Next navigation
|
|
||||||
|
|
||||||
### Database
|
|
||||||
- [x] User model with relationships
|
|
||||||
- [x] Job model with foreign key
|
|
||||||
- [x] Password hashing
|
|
||||||
- [x] Timestamps
|
|
||||||
- [x] Status tracking
|
|
||||||
- [x] Default admin creation
|
|
||||||
|
|
||||||
### UI/UX
|
|
||||||
- [x] Modern gradient theme
|
|
||||||
- [x] Responsive design
|
|
||||||
- [x] Logo integration
|
|
||||||
- [x] Status icons (🟢🔴🟠⚫)
|
|
||||||
- [x] Alert messages
|
|
||||||
- [x] Modal dialogs
|
|
||||||
- [x] Form styling
|
|
||||||
- [x] Button styles
|
|
||||||
- [x] Table styling
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🐳 Docker Verification
|
|
||||||
|
|
||||||
### Docker Compose
|
|
||||||
- [x] PostgreSQL service defined
|
|
||||||
- [x] Web service defined
|
|
||||||
- [x] Environment variables set
|
|
||||||
- [x] Volumes configured
|
|
||||||
- [x] Networks defined
|
|
||||||
- [x] Restart policies set
|
|
||||||
- [x] Dependencies specified
|
|
||||||
|
|
||||||
### Dockerfile
|
|
||||||
- [x] Python 3.11 base image
|
|
||||||
- [x] System dependencies
|
|
||||||
- [x] Python packages
|
|
||||||
- [x] Working directory
|
|
||||||
- [x] Port exposure
|
|
||||||
- [x] Gunicorn command
|
|
||||||
|
|
||||||
### Volumes
|
|
||||||
- [x] postgres_data volume
|
|
||||||
- [x] test_results volume
|
|
||||||
- [x] App code mount
|
|
||||||
|
|
||||||
### Networks
|
|
||||||
- [x] testarena_network (internal)
|
|
||||||
- [x] caddy_network (ready to configure)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📚 Documentation Verification
|
|
||||||
|
|
||||||
### User Documentation
|
|
||||||
- [x] Clear quick start guide
|
|
||||||
- [x] Step-by-step instructions
|
|
||||||
- [x] Screenshots/diagrams
|
|
||||||
- [x] Troubleshooting section
|
|
||||||
- [x] FAQ section
|
|
||||||
|
|
||||||
### Technical Documentation
|
|
||||||
- [x] Architecture diagrams
|
|
||||||
- [x] Database schema
|
|
||||||
- [x] API endpoints
|
|
||||||
- [x] File structure
|
|
||||||
- [x] Technology stack
|
|
||||||
|
|
||||||
### Deployment Documentation
|
|
||||||
- [x] Pre-deployment checklist
|
|
||||||
- [x] Configuration steps
|
|
||||||
- [x] Caddy integration guide
|
|
||||||
- [x] Security notes
|
|
||||||
- [x] Backup procedures
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔐 Security Verification
|
|
||||||
|
|
||||||
### Application Security
|
|
||||||
- [x] Password hashing implemented
|
|
||||||
- [x] Session management configured
|
|
||||||
- [x] CSRF protection enabled
|
|
||||||
- [x] SQL injection protection (ORM)
|
|
||||||
- [x] XSS protection (auto-escaping)
|
|
||||||
- [x] Role-based access control
|
|
||||||
|
|
||||||
### Configuration Security
|
|
||||||
- [x] SECRET_KEY configurable
|
|
||||||
- [x] Database password configurable
|
|
||||||
- [x] Default credentials documented
|
|
||||||
- [x] HTTPS ready via Caddy
|
|
||||||
- [x] Security headers example
|
|
||||||
|
|
||||||
### Deployment Security
|
|
||||||
- [x] Database not exposed to internet
|
|
||||||
- [x] Internal Docker networks
|
|
||||||
- [x] Environment variables for secrets
|
|
||||||
- [x] .gitignore for sensitive files
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🧪 Testing Checklist
|
|
||||||
|
|
||||||
### Manual Testing Required
|
|
||||||
- [ ] Login with default credentials
|
|
||||||
- [ ] Change admin password
|
|
||||||
- [ ] Create test user
|
|
||||||
- [ ] Login as test user
|
|
||||||
- [ ] Admin: Create user
|
|
||||||
- [ ] Admin: Reset password
|
|
||||||
- [ ] Admin: Delete user
|
|
||||||
- [ ] User: View dashboard
|
|
||||||
- [ ] User: Submit job (all steps)
|
|
||||||
- [ ] User: View job details
|
|
||||||
- [ ] User: Cannot access admin
|
|
||||||
- [ ] Logout functionality
|
|
||||||
|
|
||||||
### Docker Testing Required
|
|
||||||
- [ ] Containers start successfully
|
|
||||||
- [ ] Database initializes
|
|
||||||
- [ ] Web app connects to database
|
|
||||||
- [ ] Volumes persist data
|
|
||||||
- [ ] Networks configured correctly
|
|
||||||
- [ ] Logs show no errors
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📋 Pre-Deployment Tasks
|
|
||||||
|
|
||||||
### Configuration
|
|
||||||
- [ ] Copy .env.example to .env
|
|
||||||
- [ ] Generate SECRET_KEY
|
|
||||||
- [ ] Set database password
|
|
||||||
- [ ] Update docker-compose.yml with Caddy network
|
|
||||||
- [ ] Configure Caddyfile
|
|
||||||
|
|
||||||
### Verification
|
|
||||||
- [ ] All files present
|
|
||||||
- [ ] Docker installed
|
|
||||||
- [ ] Docker Compose installed
|
|
||||||
- [ ] Caddy running
|
|
||||||
- [ ] Domain DNS configured
|
|
||||||
|
|
||||||
### Security
|
|
||||||
- [ ] Strong SECRET_KEY set
|
|
||||||
- [ ] Strong database password set
|
|
||||||
- [ ] Firewall rules reviewed
|
|
||||||
- [ ] HTTPS configured
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 Deployment Steps
|
|
||||||
|
|
||||||
1. [ ] Read START_HERE.md
|
|
||||||
2. [ ] Follow QUICK_START.md
|
|
||||||
3. [ ] Configure Caddy per CADDY_INTEGRATION.md
|
|
||||||
4. [ ] Complete DEPLOYMENT_CHECKLIST.md
|
|
||||||
5. [ ] Run start.bat or docker-compose up
|
|
||||||
6. [ ] Wait 30 seconds for initialization
|
|
||||||
7. [ ] Access via domain
|
|
||||||
8. [ ] Login and change password
|
|
||||||
9. [ ] Create test users
|
|
||||||
10. [ ] Verify all features
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ✅ Success Criteria
|
|
||||||
|
|
||||||
### Application
|
|
||||||
- [x] All files created
|
|
||||||
- [x] Code compiles without errors
|
|
||||||
- [x] Templates render correctly
|
|
||||||
- [x] CSS loads properly
|
|
||||||
- [x] JavaScript functions work
|
|
||||||
|
|
||||||
### Docker
|
|
||||||
- [ ] Containers start (pending deployment)
|
|
||||||
- [ ] Database initializes (pending deployment)
|
|
||||||
- [ ] Web app accessible (pending deployment)
|
|
||||||
- [ ] Volumes persist (pending deployment)
|
|
||||||
|
|
||||||
### Functionality
|
|
||||||
- [ ] Login works (pending deployment)
|
|
||||||
- [ ] Admin features work (pending deployment)
|
|
||||||
- [ ] User features work (pending deployment)
|
|
||||||
- [ ] Job submission works (pending deployment)
|
|
||||||
- [ ] Dashboard displays correctly (pending deployment)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📊 Phase 1 Completion Status
|
|
||||||
|
|
||||||
### Code: 100% ✅
|
|
||||||
- All Python files created
|
|
||||||
- All templates created
|
|
||||||
- All styles created
|
|
||||||
- All routes implemented
|
|
||||||
|
|
||||||
### Documentation: 100% ✅
|
|
||||||
- User guides complete
|
|
||||||
- Technical docs complete
|
|
||||||
- Deployment guides complete
|
|
||||||
- Examples provided
|
|
||||||
|
|
||||||
### Infrastructure: 100% ✅
|
|
||||||
- Docker configuration complete
|
|
||||||
- Database setup complete
|
|
||||||
- Network configuration ready
|
|
||||||
- Volume management configured
|
|
||||||
|
|
||||||
### Testing: 0% ⏳
|
|
||||||
- Awaiting deployment
|
|
||||||
- Manual testing required
|
|
||||||
- Feature verification needed
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 Next Actions
|
|
||||||
|
|
||||||
### Immediate (You)
|
|
||||||
1. Share Caddy network name
|
|
||||||
2. Review documentation
|
|
||||||
3. Verify requirements met
|
|
||||||
4. Plan deployment time
|
|
||||||
|
|
||||||
### Deployment (Together)
|
|
||||||
1. Update docker-compose.yml
|
|
||||||
2. Configure environment variables
|
|
||||||
3. Start containers
|
|
||||||
4. Test functionality
|
|
||||||
5. Change default password
|
|
||||||
|
|
||||||
### Phase 2 (Future)
|
|
||||||
1. Define Git integration
|
|
||||||
2. Implement test execution
|
|
||||||
3. Add status updates
|
|
||||||
4. Generate results
|
|
||||||
5. Implement cleanup
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📞 Support
|
|
||||||
|
|
||||||
If anything is missing or unclear:
|
|
||||||
|
|
||||||
1. Check INDEX.md for documentation guide
|
|
||||||
2. Review IMPLEMENTATION_SUMMARY.md for overview
|
|
||||||
3. Read relevant documentation
|
|
||||||
4. Check Docker logs if deployed
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎉 Phase 1 Status
|
|
||||||
|
|
||||||
**Implementation:** ✅ COMPLETE
|
|
||||||
**Documentation:** ✅ COMPLETE
|
|
||||||
**Infrastructure:** ✅ COMPLETE
|
|
||||||
**Testing:** ⏳ PENDING DEPLOYMENT
|
|
||||||
**Production Ready:** ✅ YES (after configuration)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📝 Notes
|
|
||||||
|
|
||||||
- Default admin credentials: admin/admin123
|
|
||||||
- Change password immediately after first login
|
|
||||||
- All sensitive data in environment variables
|
|
||||||
- Comprehensive documentation provided
|
|
||||||
- Ready for Caddy network configuration
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Ready to deploy? Start with START_HERE.md!**
|
|
||||||
|
|
||||||
**Need help? Check INDEX.md for documentation guide!**
|
|
||||||
|
|
||||||
**Questions? Review IMPLEMENTATION_SUMMARY.md!**
|
|
||||||
@@ -1,476 +0,0 @@
|
|||||||
# ASF TestArena - Implementation Summary
|
|
||||||
|
|
||||||
## ✅ Phase 1: COMPLETE
|
|
||||||
|
|
||||||
**Implementation Date:** November 28, 2024
|
|
||||||
**Status:** Ready for Deployment
|
|
||||||
**Next Phase:** Test Execution Engine
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📦 What Has Been Delivered
|
|
||||||
|
|
||||||
### 1. Complete Web Application ✅
|
|
||||||
|
|
||||||
**Backend (Flask)**
|
|
||||||
- ✅ User authentication system (Flask-Login)
|
|
||||||
- ✅ Role-based access control (Admin/User)
|
|
||||||
- ✅ Database models (User, Job)
|
|
||||||
- ✅ RESTful API endpoints
|
|
||||||
- ✅ Session management
|
|
||||||
- ✅ Password hashing (Werkzeug)
|
|
||||||
|
|
||||||
**Frontend (HTML/CSS/JS)**
|
|
||||||
- ✅ Modern gradient theme
|
|
||||||
- ✅ Responsive design
|
|
||||||
- ✅ Custom logo integration
|
|
||||||
- ✅ Clean, intuitive UI
|
|
||||||
- ✅ Multi-step forms
|
|
||||||
- ✅ Real-time job selection
|
|
||||||
|
|
||||||
### 2. User Management System ✅
|
|
||||||
|
|
||||||
**Admin Features**
|
|
||||||
- ✅ Create users with role assignment
|
|
||||||
- ✅ Delete users (with protection for self-deletion)
|
|
||||||
- ✅ Reset user passwords
|
|
||||||
- ✅ View all users in system
|
|
||||||
- ✅ View all jobs from all users
|
|
||||||
|
|
||||||
**User Features**
|
|
||||||
- ✅ Secure login/logout
|
|
||||||
- ✅ View own jobs only
|
|
||||||
- ✅ Submit test jobs
|
|
||||||
- ✅ View job details
|
|
||||||
- ✅ Abort running jobs (UI ready)
|
|
||||||
|
|
||||||
### 3. Dashboard System ✅
|
|
||||||
|
|
||||||
**Two-Panel Layout**
|
|
||||||
- ✅ Left panel: Job list with status icons
|
|
||||||
- 🟢 Passed
|
|
||||||
- 🔴 Failed
|
|
||||||
- 🟠 In Progress
|
|
||||||
- ⚫ Aborted
|
|
||||||
- ✅ Right panel: Detailed job information
|
|
||||||
- ✅ Click-to-view job details
|
|
||||||
- ✅ Submit new job button
|
|
||||||
|
|
||||||
**Job Information Display**
|
|
||||||
- ✅ Job ID
|
|
||||||
- ✅ Submitter username
|
|
||||||
- ✅ Branch name
|
|
||||||
- ✅ Selected scenarios
|
|
||||||
- ✅ Environment
|
|
||||||
- ✅ Test mode
|
|
||||||
- ✅ Status badge
|
|
||||||
- ✅ Timestamps
|
|
||||||
- ✅ Duration (when completed)
|
|
||||||
- ✅ Options (keep devbenches, reuse results)
|
|
||||||
|
|
||||||
### 4. Job Submission Workflow ✅
|
|
||||||
|
|
||||||
**5-Step Wizard**
|
|
||||||
- ✅ Step 1: Enter Git branch name
|
|
||||||
- ✅ Step 2: Select test scenarios (checkboxes)
|
|
||||||
- ✅ Step 3: Choose environment (Sensor Hub / Main Board)
|
|
||||||
- ✅ Step 4: Select test mode (Simulator / HIL) + options
|
|
||||||
- ✅ Step 5: Submit and create job record
|
|
||||||
|
|
||||||
**Features**
|
|
||||||
- ✅ Progress indicator
|
|
||||||
- ✅ Form validation
|
|
||||||
- ✅ Back/Next navigation
|
|
||||||
- ✅ Select all scenarios option
|
|
||||||
- ✅ Additional options (keep devbenches, reuse results)
|
|
||||||
|
|
||||||
### 5. Docker Infrastructure ✅
|
|
||||||
|
|
||||||
**Containers**
|
|
||||||
- ✅ PostgreSQL 15 database
|
|
||||||
- ✅ Flask web application (Gunicorn)
|
|
||||||
- ✅ Caddy integration ready
|
|
||||||
|
|
||||||
**Configuration**
|
|
||||||
- ✅ docker-compose.yml
|
|
||||||
- ✅ Dockerfile for web app
|
|
||||||
- ✅ Volume management (database, test results)
|
|
||||||
- ✅ Network configuration
|
|
||||||
- ✅ Environment variables
|
|
||||||
- ✅ Restart policies
|
|
||||||
|
|
||||||
### 6. Database Design ✅
|
|
||||||
|
|
||||||
**Users Table**
|
|
||||||
- ✅ ID, username, password_hash
|
|
||||||
- ✅ Role flag (is_admin)
|
|
||||||
- ✅ Created timestamp
|
|
||||||
- ✅ Unique username constraint
|
|
||||||
|
|
||||||
**Jobs Table**
|
|
||||||
- ✅ ID, user_id (foreign key)
|
|
||||||
- ✅ Branch name, scenarios (JSON)
|
|
||||||
- ✅ Environment, test mode
|
|
||||||
- ✅ Status tracking
|
|
||||||
- ✅ Timestamps (submitted, completed)
|
|
||||||
- ✅ Duration tracking
|
|
||||||
- ✅ Options (keep_devbenches, reuse_results)
|
|
||||||
- ✅ Results path
|
|
||||||
|
|
||||||
### 7. Documentation ✅
|
|
||||||
|
|
||||||
**User Documentation**
|
|
||||||
- ✅ START_HERE.md - Quick start guide
|
|
||||||
- ✅ QUICK_START.md - Fast reference
|
|
||||||
- ✅ README.md - General overview
|
|
||||||
- ✅ INDEX.md - Documentation index
|
|
||||||
|
|
||||||
**Technical Documentation**
|
|
||||||
- ✅ SETUP.md - Detailed setup guide
|
|
||||||
- ✅ ARCHITECTURE.md - System design
|
|
||||||
- ✅ PROJECT_STATUS.md - Implementation status
|
|
||||||
|
|
||||||
**Deployment Documentation**
|
|
||||||
- ✅ DEPLOYMENT_CHECKLIST.md - Pre-production checklist
|
|
||||||
- ✅ CADDY_INTEGRATION.md - Reverse proxy setup
|
|
||||||
- ✅ .env.example - Environment template
|
|
||||||
- ✅ Caddyfile.example - Caddy configuration
|
|
||||||
|
|
||||||
**Scripts**
|
|
||||||
- ✅ start.bat - Windows startup
|
|
||||||
- ✅ stop.bat - Windows shutdown
|
|
||||||
- ✅ logs.bat - View logs
|
|
||||||
|
|
||||||
### 8. Security Features ✅
|
|
||||||
|
|
||||||
- ✅ Password hashing (Werkzeug)
|
|
||||||
- ✅ Session management (Flask-Login)
|
|
||||||
- ✅ CSRF protection (Flask-WTF)
|
|
||||||
- ✅ Role-based access control
|
|
||||||
- ✅ SQL injection protection (SQLAlchemy ORM)
|
|
||||||
- ✅ HTTPS ready (via Caddy)
|
|
||||||
- ✅ Secure default configuration
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📊 Project Statistics
|
|
||||||
|
|
||||||
**Code Files:** 12 Python files, 8 HTML templates, 1 CSS file
|
|
||||||
**Lines of Code:** ~2,000+ lines
|
|
||||||
**Documentation:** 9 comprehensive documents
|
|
||||||
**Docker Containers:** 2 (web, database)
|
|
||||||
**Database Tables:** 2 (users, jobs)
|
|
||||||
**API Endpoints:** 12 routes
|
|
||||||
**User Roles:** 2 (Admin, User)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎨 User Interface Pages
|
|
||||||
|
|
||||||
1. ✅ Login Page
|
|
||||||
2. ✅ Admin Dashboard
|
|
||||||
3. ✅ User Dashboard
|
|
||||||
4. ✅ Submit Job - Step 1 (Branch)
|
|
||||||
5. ✅ Submit Job - Step 2 (Scenarios)
|
|
||||||
6. ✅ Submit Job - Step 3 (Environment)
|
|
||||||
7. ✅ Submit Job - Step 4 (Test Mode)
|
|
||||||
|
|
||||||
**Total Pages:** 7 unique pages
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔧 Technology Stack
|
|
||||||
|
|
||||||
| Component | Technology | Version |
|
|
||||||
|-----------|-----------|---------|
|
|
||||||
| Backend | Flask | 3.0.0 |
|
|
||||||
| Database | PostgreSQL | 15 |
|
|
||||||
| ORM | SQLAlchemy | 3.1.1 |
|
|
||||||
| Auth | Flask-Login | 0.6.3 |
|
|
||||||
| Forms | Flask-WTF | 1.2.1 |
|
|
||||||
| WSGI | Gunicorn | 21.2.0 |
|
|
||||||
| Proxy | Caddy | 2.x |
|
|
||||||
| Container | Docker | Latest |
|
|
||||||
| Frontend | HTML/CSS/JS | Native |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📁 File Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
testarena/
|
|
||||||
├── app/ # Flask application
|
|
||||||
│ ├── __init__.py # App factory (50 lines)
|
|
||||||
│ ├── models.py # Database models (60 lines)
|
|
||||||
│ ├── routes/ # API endpoints
|
|
||||||
│ │ ├── auth.py # Authentication (30 lines)
|
|
||||||
│ │ ├── admin.py # User management (80 lines)
|
|
||||||
│ │ ├── dashboard.py # Dashboard (15 lines)
|
|
||||||
│ │ └── jobs.py # Job submission (120 lines)
|
|
||||||
│ ├── templates/ # HTML templates
|
|
||||||
│ │ ├── base.html # Base template (40 lines)
|
|
||||||
│ │ ├── login.html # Login page (40 lines)
|
|
||||||
│ │ ├── admin/
|
|
||||||
│ │ │ └── dashboard.html # Admin UI (80 lines)
|
|
||||||
│ │ ├── dashboard/
|
|
||||||
│ │ │ └── index.html # User dashboard (100 lines)
|
|
||||||
│ │ └── jobs/
|
|
||||||
│ │ ├── submit.html # Step 1 (50 lines)
|
|
||||||
│ │ ├── submit_step2.html # Step 2 (60 lines)
|
|
||||||
│ │ ├── submit_step3.html # Step 3 (60 lines)
|
|
||||||
│ │ └── submit_step4.html # Step 4 (70 lines)
|
|
||||||
│ └── static/
|
|
||||||
│ ├── css/
|
|
||||||
│ │ └── style.css # Modern theme (400+ lines)
|
|
||||||
│ └── uploads/
|
|
||||||
│ └── icon.png # Logo
|
|
||||||
├── docker-compose.yml # Container orchestration (40 lines)
|
|
||||||
├── Dockerfile # Web app image (20 lines)
|
|
||||||
├── requirements.txt # Dependencies (9 packages)
|
|
||||||
├── wsgi.py # WSGI entry point (10 lines)
|
|
||||||
├── .env.example # Environment template
|
|
||||||
├── .gitignore # Git ignore rules
|
|
||||||
├── Caddyfile.example # Caddy config template
|
|
||||||
├── start.bat # Windows startup script
|
|
||||||
├── stop.bat # Windows stop script
|
|
||||||
├── logs.bat # View logs script
|
|
||||||
└── Documentation/ # 9 comprehensive docs
|
|
||||||
├── START_HERE.md
|
|
||||||
├── QUICK_START.md
|
|
||||||
├── DEPLOYMENT_CHECKLIST.md
|
|
||||||
├── CADDY_INTEGRATION.md
|
|
||||||
├── SETUP.md
|
|
||||||
├── PROJECT_STATUS.md
|
|
||||||
├── ARCHITECTURE.md
|
|
||||||
├── README.md
|
|
||||||
└── INDEX.md
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 Deployment Readiness
|
|
||||||
|
|
||||||
### ✅ Ready for Production
|
|
||||||
|
|
||||||
- [x] Application code complete
|
|
||||||
- [x] Database schema defined
|
|
||||||
- [x] Docker configuration ready
|
|
||||||
- [x] Security features implemented
|
|
||||||
- [x] Documentation complete
|
|
||||||
- [x] Deployment scripts ready
|
|
||||||
|
|
||||||
### ⏳ Requires Configuration
|
|
||||||
|
|
||||||
- [ ] Caddy network name (user-specific)
|
|
||||||
- [ ] SECRET_KEY (generate secure key)
|
|
||||||
- [ ] Database password (set strong password)
|
|
||||||
- [ ] Domain DNS configuration
|
|
||||||
|
|
||||||
### ⏳ Post-Deployment Tasks
|
|
||||||
|
|
||||||
- [ ] Change default admin password
|
|
||||||
- [ ] Create initial users
|
|
||||||
- [ ] Test all features
|
|
||||||
- [ ] Set up backups
|
|
||||||
- [ ] Configure monitoring
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 What Works Right Now
|
|
||||||
|
|
||||||
### Fully Functional
|
|
||||||
1. ✅ User login/logout
|
|
||||||
2. ✅ Admin user management
|
|
||||||
3. ✅ Job submission workflow (UI)
|
|
||||||
4. ✅ Dashboard display
|
|
||||||
5. ✅ Job list and details
|
|
||||||
6. ✅ Role-based access control
|
|
||||||
7. ✅ Database persistence
|
|
||||||
8. ✅ Docker containerization
|
|
||||||
|
|
||||||
### UI Only (Backend Pending)
|
|
||||||
1. ⏳ Git branch checkout
|
|
||||||
2. ⏳ Scenario detection
|
|
||||||
3. ⏳ Test execution
|
|
||||||
4. ⏳ Status updates
|
|
||||||
5. ⏳ Results generation
|
|
||||||
6. ⏳ Job abort functionality
|
|
||||||
7. ⏳ Automatic cleanup
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📋 Phase 2 Requirements
|
|
||||||
|
|
||||||
To complete the test execution functionality, you'll need to provide:
|
|
||||||
|
|
||||||
### 1. Git Integration
|
|
||||||
- Repository URL/path
|
|
||||||
- Authentication method (SSH key, token, etc.)
|
|
||||||
- Branch checkout script
|
|
||||||
|
|
||||||
### 2. Scenario Detection
|
|
||||||
- Script to analyze branch
|
|
||||||
- Expected output format
|
|
||||||
- Scenario naming convention
|
|
||||||
|
|
||||||
### 3. Test Execution
|
|
||||||
- Test runner script/command
|
|
||||||
- Environment setup requirements
|
|
||||||
- Expected execution time
|
|
||||||
|
|
||||||
### 4. Results Management
|
|
||||||
- HTML report generation method
|
|
||||||
- Results storage location
|
|
||||||
- Report format/structure
|
|
||||||
|
|
||||||
### 5. Process Management
|
|
||||||
- How to start tests
|
|
||||||
- How to monitor progress
|
|
||||||
- How to abort tests
|
|
||||||
- Status update mechanism
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎉 Success Metrics
|
|
||||||
|
|
||||||
### Phase 1 Goals: ACHIEVED ✅
|
|
||||||
|
|
||||||
- [x] Modern, professional UI
|
|
||||||
- [x] Secure authentication
|
|
||||||
- [x] User management system
|
|
||||||
- [x] Job submission workflow
|
|
||||||
- [x] Dashboard with job tracking
|
|
||||||
- [x] Docker deployment
|
|
||||||
- [x] Comprehensive documentation
|
|
||||||
- [x] Production-ready infrastructure
|
|
||||||
|
|
||||||
### Phase 2 Goals: PENDING ⏳
|
|
||||||
|
|
||||||
- [ ] Automated test execution
|
|
||||||
- [ ] Real-time status updates
|
|
||||||
- [ ] Results generation
|
|
||||||
- [ ] Automatic cleanup
|
|
||||||
- [ ] Job abort functionality
|
|
||||||
- [ ] Git integration
|
|
||||||
- [ ] Scenario detection
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 💡 Key Features
|
|
||||||
|
|
||||||
### What Makes This Special
|
|
||||||
|
|
||||||
1. **Modern Design** - Gradient theme, clean UI, responsive layout
|
|
||||||
2. **Role-Based Access** - Admin and user roles with appropriate permissions
|
|
||||||
3. **Multi-Step Workflow** - Intuitive 5-step job submission
|
|
||||||
4. **Real-Time Updates** - Dashboard updates when jobs are selected
|
|
||||||
5. **Docker Ready** - Complete containerization with Caddy integration
|
|
||||||
6. **Security First** - Password hashing, CSRF protection, session management
|
|
||||||
7. **Comprehensive Docs** - 9 detailed documents covering all aspects
|
|
||||||
8. **Production Ready** - Deployment scripts, checklists, and examples
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📞 Next Steps
|
|
||||||
|
|
||||||
### Immediate (Before Deployment)
|
|
||||||
1. Share Caddy network name
|
|
||||||
2. Update docker-compose.yml
|
|
||||||
3. Generate SECRET_KEY
|
|
||||||
4. Set database password
|
|
||||||
5. Run deployment checklist
|
|
||||||
|
|
||||||
### Short Term (Phase 2 Planning)
|
|
||||||
1. Define Git integration requirements
|
|
||||||
2. Share test execution scripts
|
|
||||||
3. Specify results format
|
|
||||||
4. Plan cleanup strategy
|
|
||||||
5. Design status update mechanism
|
|
||||||
|
|
||||||
### Long Term (Future Enhancements)
|
|
||||||
1. WebSocket for real-time updates
|
|
||||||
2. Email notifications
|
|
||||||
3. Test history analytics
|
|
||||||
4. API for external integrations
|
|
||||||
5. Mobile-responsive improvements
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🏆 Deliverables Summary
|
|
||||||
|
|
||||||
### Code Deliverables
|
|
||||||
- ✅ Complete Flask application
|
|
||||||
- ✅ Database models and migrations
|
|
||||||
- ✅ HTML templates with modern design
|
|
||||||
- ✅ CSS styling (400+ lines)
|
|
||||||
- ✅ JavaScript for interactivity
|
|
||||||
- ✅ Docker configuration
|
|
||||||
- ✅ WSGI entry point
|
|
||||||
|
|
||||||
### Documentation Deliverables
|
|
||||||
- ✅ 9 comprehensive documents
|
|
||||||
- ✅ Quick start guide
|
|
||||||
- ✅ Deployment checklist
|
|
||||||
- ✅ Architecture diagrams
|
|
||||||
- ✅ API documentation
|
|
||||||
- ✅ Troubleshooting guides
|
|
||||||
- ✅ Configuration examples
|
|
||||||
|
|
||||||
### Infrastructure Deliverables
|
|
||||||
- ✅ Docker Compose setup
|
|
||||||
- ✅ PostgreSQL database
|
|
||||||
- ✅ Caddy integration ready
|
|
||||||
- ✅ Volume management
|
|
||||||
- ✅ Network configuration
|
|
||||||
- ✅ Startup scripts
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ✨ Quality Assurance
|
|
||||||
|
|
||||||
### Code Quality
|
|
||||||
- ✅ Clean, readable code
|
|
||||||
- ✅ Proper error handling
|
|
||||||
- ✅ Security best practices
|
|
||||||
- ✅ Modular architecture
|
|
||||||
- ✅ RESTful API design
|
|
||||||
|
|
||||||
### Documentation Quality
|
|
||||||
- ✅ Comprehensive coverage
|
|
||||||
- ✅ Clear instructions
|
|
||||||
- ✅ Visual diagrams
|
|
||||||
- ✅ Troubleshooting guides
|
|
||||||
- ✅ Examples provided
|
|
||||||
|
|
||||||
### User Experience
|
|
||||||
- ✅ Intuitive navigation
|
|
||||||
- ✅ Clear feedback messages
|
|
||||||
- ✅ Responsive design
|
|
||||||
- ✅ Professional appearance
|
|
||||||
- ✅ Consistent styling
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎊 Conclusion
|
|
||||||
|
|
||||||
**Phase 1 is complete and ready for deployment!**
|
|
||||||
|
|
||||||
The ASF TestArena platform now has:
|
|
||||||
- A solid foundation with modern architecture
|
|
||||||
- Complete user management system
|
|
||||||
- Intuitive job submission workflow
|
|
||||||
- Professional, responsive UI
|
|
||||||
- Production-ready Docker setup
|
|
||||||
- Comprehensive documentation
|
|
||||||
|
|
||||||
**Ready to deploy?** → Start with [START_HERE.md](START_HERE.md)
|
|
||||||
|
|
||||||
**Need help?** → Check [INDEX.md](INDEX.md) for documentation guide
|
|
||||||
|
|
||||||
**Ready for Phase 2?** → Share your Caddy network name and test execution requirements
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Project Status:** ✅ Phase 1 Complete - Ready for Deployment
|
|
||||||
**Next Milestone:** Phase 2 - Test Execution Engine
|
|
||||||
**Estimated Phase 2 Time:** 2-3 days (depending on requirements)
|
|
||||||
330
INDEX.md
330
INDEX.md
@@ -1,330 +0,0 @@
|
|||||||
# ASF TestArena - Documentation Index
|
|
||||||
|
|
||||||
## 🎯 Start Here
|
|
||||||
|
|
||||||
**New to the project?** → [START_HERE.md](START_HERE.md)
|
|
||||||
|
|
||||||
This is your complete guide to ASF TestArena. Use this index to find exactly what you need.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📖 Documentation Guide
|
|
||||||
|
|
||||||
### For First-Time Setup
|
|
||||||
|
|
||||||
1. **[START_HERE.md](START_HERE.md)** - Your first stop
|
|
||||||
- What's included
|
|
||||||
- Quick 3-step setup
|
|
||||||
- Testing checklist
|
|
||||||
|
|
||||||
2. **[QUICK_START.md](QUICK_START.md)** - Fast reference
|
|
||||||
- 3-step deployment
|
|
||||||
- User workflows
|
|
||||||
- Useful commands
|
|
||||||
|
|
||||||
3. **[DEPLOYMENT_CHECKLIST.md](DEPLOYMENT_CHECKLIST.md)** - Before going live
|
|
||||||
- Pre-deployment tasks
|
|
||||||
- Security hardening
|
|
||||||
- Verification steps
|
|
||||||
|
|
||||||
### For Configuration
|
|
||||||
|
|
||||||
4. **[CADDY_INTEGRATION.md](CADDY_INTEGRATION.md)** - Reverse proxy setup
|
|
||||||
- Find Caddy network
|
|
||||||
- Update docker-compose.yml
|
|
||||||
- Configure Caddyfile
|
|
||||||
- Troubleshooting
|
|
||||||
|
|
||||||
5. **[SETUP.md](SETUP.md)** - Detailed setup guide
|
|
||||||
- Phase 1 status
|
|
||||||
- Configuration steps
|
|
||||||
- File structure
|
|
||||||
- Database schema
|
|
||||||
- API endpoints
|
|
||||||
|
|
||||||
### For Understanding the System
|
|
||||||
|
|
||||||
6. **[PROJECT_STATUS.md](PROJECT_STATUS.md)** - Implementation overview
|
|
||||||
- Feature checklist
|
|
||||||
- UI descriptions
|
|
||||||
- Database design
|
|
||||||
- Tech stack
|
|
||||||
- Next steps
|
|
||||||
|
|
||||||
7. **[ARCHITECTURE.md](ARCHITECTURE.md)** - System design
|
|
||||||
- Network architecture
|
|
||||||
- Application structure
|
|
||||||
- User flows
|
|
||||||
- Security layers
|
|
||||||
- Scaling strategy
|
|
||||||
|
|
||||||
8. **[README.md](README.md)** - General overview
|
|
||||||
- Features
|
|
||||||
- Installation
|
|
||||||
- Configuration
|
|
||||||
- Development
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🗂️ Quick Reference by Task
|
|
||||||
|
|
||||||
### "I want to deploy the application"
|
|
||||||
1. Read [QUICK_START.md](QUICK_START.md)
|
|
||||||
2. Follow [DEPLOYMENT_CHECKLIST.md](DEPLOYMENT_CHECKLIST.md)
|
|
||||||
3. Configure Caddy using [CADDY_INTEGRATION.md](CADDY_INTEGRATION.md)
|
|
||||||
|
|
||||||
### "I want to understand what's built"
|
|
||||||
1. Check [PROJECT_STATUS.md](PROJECT_STATUS.md)
|
|
||||||
2. Review [ARCHITECTURE.md](ARCHITECTURE.md)
|
|
||||||
3. Read [SETUP.md](SETUP.md) for details
|
|
||||||
|
|
||||||
### "I want to configure Caddy"
|
|
||||||
1. Go to [CADDY_INTEGRATION.md](CADDY_INTEGRATION.md)
|
|
||||||
2. Follow step-by-step instructions
|
|
||||||
3. Use provided Caddyfile template
|
|
||||||
|
|
||||||
### "I need troubleshooting help"
|
|
||||||
1. Check [DEPLOYMENT_CHECKLIST.md](DEPLOYMENT_CHECKLIST.md) - Troubleshooting section
|
|
||||||
2. Review [CADDY_INTEGRATION.md](CADDY_INTEGRATION.md) - Troubleshooting section
|
|
||||||
3. Check [SETUP.md](SETUP.md) - Troubleshooting section
|
|
||||||
|
|
||||||
### "I want to develop/extend the system"
|
|
||||||
1. Read [ARCHITECTURE.md](ARCHITECTURE.md)
|
|
||||||
2. Review [SETUP.md](SETUP.md) - File Structure
|
|
||||||
3. Check [README.md](README.md) - Development section
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📋 Document Summaries
|
|
||||||
|
|
||||||
### START_HERE.md
|
|
||||||
**Purpose:** First document for new users
|
|
||||||
**Length:** Short (5 min read)
|
|
||||||
**Content:**
|
|
||||||
- What's included in Phase 1
|
|
||||||
- 3-step quick start
|
|
||||||
- Feature overview
|
|
||||||
- Next steps for Phase 2
|
|
||||||
|
|
||||||
### QUICK_START.md
|
|
||||||
**Purpose:** Fast deployment reference
|
|
||||||
**Length:** Very short (2 min read)
|
|
||||||
**Content:**
|
|
||||||
- Minimal setup steps
|
|
||||||
- User workflows
|
|
||||||
- Useful commands
|
|
||||||
- Key files
|
|
||||||
|
|
||||||
### DEPLOYMENT_CHECKLIST.md
|
|
||||||
**Purpose:** Pre-production verification
|
|
||||||
**Length:** Long (15 min to complete)
|
|
||||||
**Content:**
|
|
||||||
- Configuration checklist
|
|
||||||
- Deployment steps
|
|
||||||
- Security hardening
|
|
||||||
- Backup procedures
|
|
||||||
- Troubleshooting
|
|
||||||
|
|
||||||
### CADDY_INTEGRATION.md
|
|
||||||
**Purpose:** Reverse proxy configuration
|
|
||||||
**Length:** Medium (10 min read)
|
|
||||||
**Content:**
|
|
||||||
- Find Caddy network
|
|
||||||
- Update docker-compose.yml
|
|
||||||
- Configure Caddyfile
|
|
||||||
- Verify setup
|
|
||||||
- Troubleshooting
|
|
||||||
|
|
||||||
### SETUP.md
|
|
||||||
**Purpose:** Detailed technical guide
|
|
||||||
**Length:** Long (20 min read)
|
|
||||||
**Content:**
|
|
||||||
- Implementation status
|
|
||||||
- Configuration steps
|
|
||||||
- File structure
|
|
||||||
- Database schema
|
|
||||||
- API endpoints
|
|
||||||
- Security notes
|
|
||||||
|
|
||||||
### PROJECT_STATUS.md
|
|
||||||
**Purpose:** Implementation overview
|
|
||||||
**Length:** Medium (10 min read)
|
|
||||||
**Content:**
|
|
||||||
- Feature checklist
|
|
||||||
- UI mockups
|
|
||||||
- Database design
|
|
||||||
- Tech stack
|
|
||||||
- Roadmap
|
|
||||||
|
|
||||||
### ARCHITECTURE.md
|
|
||||||
**Purpose:** System design documentation
|
|
||||||
**Length:** Long (15 min read)
|
|
||||||
**Content:**
|
|
||||||
- System diagrams
|
|
||||||
- Network architecture
|
|
||||||
- User flows
|
|
||||||
- Security layers
|
|
||||||
- Scaling strategy
|
|
||||||
|
|
||||||
### README.md
|
|
||||||
**Purpose:** General project overview
|
|
||||||
**Length:** Medium (8 min read)
|
|
||||||
**Content:**
|
|
||||||
- Features
|
|
||||||
- Quick start
|
|
||||||
- Configuration
|
|
||||||
- Development
|
|
||||||
- License
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎓 Learning Paths
|
|
||||||
|
|
||||||
### Path 1: Quick Deployment (30 minutes)
|
|
||||||
1. [START_HERE.md](START_HERE.md) - 5 min
|
|
||||||
2. [QUICK_START.md](QUICK_START.md) - 2 min
|
|
||||||
3. [CADDY_INTEGRATION.md](CADDY_INTEGRATION.md) - 10 min
|
|
||||||
4. [DEPLOYMENT_CHECKLIST.md](DEPLOYMENT_CHECKLIST.md) - 15 min
|
|
||||||
5. Deploy! 🚀
|
|
||||||
|
|
||||||
### Path 2: Understanding the System (45 minutes)
|
|
||||||
1. [START_HERE.md](START_HERE.md) - 5 min
|
|
||||||
2. [PROJECT_STATUS.md](PROJECT_STATUS.md) - 10 min
|
|
||||||
3. [ARCHITECTURE.md](ARCHITECTURE.md) - 15 min
|
|
||||||
4. [SETUP.md](SETUP.md) - 20 min
|
|
||||||
5. Ready to customize! 🛠️
|
|
||||||
|
|
||||||
### Path 3: Development Setup (60 minutes)
|
|
||||||
1. [README.md](README.md) - 8 min
|
|
||||||
2. [ARCHITECTURE.md](ARCHITECTURE.md) - 15 min
|
|
||||||
3. [SETUP.md](SETUP.md) - 20 min
|
|
||||||
4. [QUICK_START.md](QUICK_START.md) - 2 min
|
|
||||||
5. Code exploration - 15 min
|
|
||||||
6. Ready to develop! 💻
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔍 Find Information By Topic
|
|
||||||
|
|
||||||
### Authentication & Security
|
|
||||||
- [SETUP.md](SETUP.md) - Security Notes section
|
|
||||||
- [ARCHITECTURE.md](ARCHITECTURE.md) - Security Layers section
|
|
||||||
- [DEPLOYMENT_CHECKLIST.md](DEPLOYMENT_CHECKLIST.md) - Security Hardening section
|
|
||||||
|
|
||||||
### Database
|
|
||||||
- [SETUP.md](SETUP.md) - Database Schema section
|
|
||||||
- [ARCHITECTURE.md](ARCHITECTURE.md) - Database Schema diagram
|
|
||||||
- [PROJECT_STATUS.md](PROJECT_STATUS.md) - Database design
|
|
||||||
|
|
||||||
### Docker & Deployment
|
|
||||||
- [QUICK_START.md](QUICK_START.md) - Useful Commands
|
|
||||||
- [DEPLOYMENT_CHECKLIST.md](DEPLOYMENT_CHECKLIST.md) - Full checklist
|
|
||||||
- [CADDY_INTEGRATION.md](CADDY_INTEGRATION.md) - Docker networking
|
|
||||||
|
|
||||||
### User Interface
|
|
||||||
- [PROJECT_STATUS.md](PROJECT_STATUS.md) - User Interface section
|
|
||||||
- [SETUP.md](SETUP.md) - File Structure section
|
|
||||||
- [ARCHITECTURE.md](ARCHITECTURE.md) - User Flow Diagrams
|
|
||||||
|
|
||||||
### API Endpoints
|
|
||||||
- [SETUP.md](SETUP.md) - API Endpoints section
|
|
||||||
- [ARCHITECTURE.md](ARCHITECTURE.md) - Application Architecture
|
|
||||||
|
|
||||||
### Configuration
|
|
||||||
- [CADDY_INTEGRATION.md](CADDY_INTEGRATION.md) - Complete guide
|
|
||||||
- [DEPLOYMENT_CHECKLIST.md](DEPLOYMENT_CHECKLIST.md) - Configuration Files
|
|
||||||
- [SETUP.md](SETUP.md) - Configuration Steps
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📁 File Organization
|
|
||||||
|
|
||||||
```
|
|
||||||
Documentation/
|
|
||||||
├── START_HERE.md ← Start here!
|
|
||||||
├── QUICK_START.md ← Fast reference
|
|
||||||
├── DEPLOYMENT_CHECKLIST.md ← Pre-production
|
|
||||||
├── CADDY_INTEGRATION.md ← Proxy setup
|
|
||||||
├── SETUP.md ← Detailed guide
|
|
||||||
├── PROJECT_STATUS.md ← Implementation status
|
|
||||||
├── ARCHITECTURE.md ← System design
|
|
||||||
├── README.md ← General overview
|
|
||||||
└── INDEX.md ← This file
|
|
||||||
|
|
||||||
Configuration Examples/
|
|
||||||
├── .env.example ← Environment variables
|
|
||||||
├── Caddyfile.example ← Caddy configuration
|
|
||||||
└── docker-compose.yml ← Container setup
|
|
||||||
|
|
||||||
Scripts/
|
|
||||||
├── start.bat ← Windows startup
|
|
||||||
├── stop.bat ← Windows shutdown
|
|
||||||
└── logs.bat ← View logs
|
|
||||||
|
|
||||||
Application/
|
|
||||||
└── app/ ← Flask application
|
|
||||||
├── routes/ ← API endpoints
|
|
||||||
├── templates/ ← HTML pages
|
|
||||||
├── static/ ← CSS, images
|
|
||||||
└── models.py ← Database models
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 Common Questions
|
|
||||||
|
|
||||||
**Q: Where do I start?**
|
|
||||||
A: [START_HERE.md](START_HERE.md)
|
|
||||||
|
|
||||||
**Q: How do I deploy quickly?**
|
|
||||||
A: [QUICK_START.md](QUICK_START.md)
|
|
||||||
|
|
||||||
**Q: How do I configure Caddy?**
|
|
||||||
A: [CADDY_INTEGRATION.md](CADDY_INTEGRATION.md)
|
|
||||||
|
|
||||||
**Q: What's implemented?**
|
|
||||||
A: [PROJECT_STATUS.md](PROJECT_STATUS.md)
|
|
||||||
|
|
||||||
**Q: How does it work?**
|
|
||||||
A: [ARCHITECTURE.md](ARCHITECTURE.md)
|
|
||||||
|
|
||||||
**Q: What are all the settings?**
|
|
||||||
A: [SETUP.md](SETUP.md)
|
|
||||||
|
|
||||||
**Q: Is it ready for production?**
|
|
||||||
A: [DEPLOYMENT_CHECKLIST.md](DEPLOYMENT_CHECKLIST.md)
|
|
||||||
|
|
||||||
**Q: How do I develop features?**
|
|
||||||
A: [README.md](README.md) + [ARCHITECTURE.md](ARCHITECTURE.md)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📞 Support
|
|
||||||
|
|
||||||
If you can't find what you need:
|
|
||||||
|
|
||||||
1. Check the relevant document's troubleshooting section
|
|
||||||
2. Review [DEPLOYMENT_CHECKLIST.md](DEPLOYMENT_CHECKLIST.md) - Troubleshooting
|
|
||||||
3. Check Docker logs: `docker-compose logs -f`
|
|
||||||
4. Contact the development team
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ✅ Quick Checklist
|
|
||||||
|
|
||||||
Before deployment:
|
|
||||||
- [ ] Read [START_HERE.md](START_HERE.md)
|
|
||||||
- [ ] Follow [QUICK_START.md](QUICK_START.md)
|
|
||||||
- [ ] Configure Caddy per [CADDY_INTEGRATION.md](CADDY_INTEGRATION.md)
|
|
||||||
- [ ] Complete [DEPLOYMENT_CHECKLIST.md](DEPLOYMENT_CHECKLIST.md)
|
|
||||||
|
|
||||||
After deployment:
|
|
||||||
- [ ] Change default admin password
|
|
||||||
- [ ] Create test users
|
|
||||||
- [ ] Verify all features work
|
|
||||||
- [ ] Set up backups
|
|
||||||
- [ ] Monitor logs
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Ready to begin? → [START_HERE.md](START_HERE.md)**
|
|
||||||
@@ -1,227 +0,0 @@
|
|||||||
# Phase 2 Features - Implementation Summary
|
|
||||||
|
|
||||||
## ✅ Features Implemented
|
|
||||||
|
|
||||||
### 1. Right-Click Context Menu for Job Abortion
|
|
||||||
|
|
||||||
**Feature:** Users can right-click on "in_progress" jobs to abort them.
|
|
||||||
|
|
||||||
**Implementation:**
|
|
||||||
- Added context menu that appears on right-click
|
|
||||||
- Only shows for jobs with status "in_progress"
|
|
||||||
- Clicking "Abort Job" marks the job as aborted (⚫ black icon)
|
|
||||||
- Context menu automatically hides when clicking elsewhere
|
|
||||||
|
|
||||||
**Files Modified:**
|
|
||||||
- `app/templates/dashboard/index.html` - Added context menu HTML and JavaScript
|
|
||||||
- `app/static/css/style.css` - Added context menu styling
|
|
||||||
|
|
||||||
**Usage:**
|
|
||||||
1. Right-click on any job with orange icon (in progress)
|
|
||||||
2. Select "Abort Job" from the context menu
|
|
||||||
3. Job status changes to aborted (black icon)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 2. Git Branch Validation
|
|
||||||
|
|
||||||
**Feature:** Validates that the branch exists on remote before allowing job submission.
|
|
||||||
|
|
||||||
**Implementation:**
|
|
||||||
- When user enters branch name and clicks "Validate Branch"
|
|
||||||
- System executes SSH commands to remote server:
|
|
||||||
1. `./TPF/gitea_repo_controller.sh clone` - Clones/updates repository
|
|
||||||
2. `./TPF/gitea_repo_controller.sh checkout <branch_name>` - Checks out branch
|
|
||||||
- Parses output to detect success or failure
|
|
||||||
- Shows appropriate message to user
|
|
||||||
|
|
||||||
**Success Flow:**
|
|
||||||
```
|
|
||||||
User enters branch → Clicks "Validate Branch" →
|
|
||||||
Loading indicator → SSH commands execute →
|
|
||||||
Branch found → Green success message →
|
|
||||||
"Next" button appears → User proceeds to scenario selection
|
|
||||||
```
|
|
||||||
|
|
||||||
**Failure Flow:**
|
|
||||||
```
|
|
||||||
User enters branch → Clicks "Validate Branch" →
|
|
||||||
Loading indicator → SSH commands execute →
|
|
||||||
Branch not found (fatal: error) → Red error message →
|
|
||||||
"Branch not found on remote. Please push the branch first." →
|
|
||||||
User cannot proceed
|
|
||||||
```
|
|
||||||
|
|
||||||
**Files Modified:**
|
|
||||||
- `app/routes/jobs.py` - Added branch validation logic with SSH commands
|
|
||||||
- `app/templates/jobs/submit.html` - Added AJAX validation and UI feedback
|
|
||||||
- `app/static/css/style.css` - Added validation message styling
|
|
||||||
- `Dockerfile` - Added sshpass package
|
|
||||||
- `docker-compose.yml` - Added SSH environment variables
|
|
||||||
- `.env.example` - Added SSH configuration template
|
|
||||||
|
|
||||||
**Configuration Required:**
|
|
||||||
|
|
||||||
Update these environment variables in `docker-compose.yml` or `.env`:
|
|
||||||
```env
|
|
||||||
SSH_PASSWORD=your_actual_password
|
|
||||||
SSH_HOST=asfserver
|
|
||||||
SSH_USER=asf
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔧 Technical Details
|
|
||||||
|
|
||||||
### SSH Command Execution
|
|
||||||
|
|
||||||
The system uses `subprocess` to execute SSH commands:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Clone repository
|
|
||||||
clone_cmd = f"sshpass -p '{ssh_password}' ssh -o StrictHostKeyChecking=no {ssh_user}@{ssh_host} './TPF/gitea_repo_controller.sh clone'"
|
|
||||||
|
|
||||||
# Checkout branch
|
|
||||||
checkout_cmd = f"sshpass -p '{ssh_password}' ssh -o StrictHostKeyChecking=no {ssh_user}@{ssh_host} './TPF/gitea_repo_controller.sh checkout {branch_name}'"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Error Detection
|
|
||||||
|
|
||||||
The system detects branch validation failures by checking for:
|
|
||||||
- "fatal:" in stdout or stderr
|
|
||||||
- Non-zero return code
|
|
||||||
- Timeout (60 seconds)
|
|
||||||
|
|
||||||
### Security Considerations
|
|
||||||
|
|
||||||
⚠️ **Important:** The SSH password is stored in environment variables. For production:
|
|
||||||
1. Use SSH keys instead of passwords
|
|
||||||
2. Store credentials in a secure vault (e.g., HashiCorp Vault)
|
|
||||||
3. Use encrypted environment variables
|
|
||||||
4. Implement proper access controls
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📋 Deployment Steps
|
|
||||||
|
|
||||||
### 1. Update Configuration
|
|
||||||
|
|
||||||
Edit `docker-compose.yml`:
|
|
||||||
```yaml
|
|
||||||
environment:
|
|
||||||
SSH_PASSWORD: your_actual_password # Replace with real password
|
|
||||||
SSH_HOST: asfserver # Your SSH host
|
|
||||||
SSH_USER: asf # Your SSH user
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Rebuild Container
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker-compose down
|
|
||||||
docker-compose up -d --build
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Test Branch Validation
|
|
||||||
|
|
||||||
1. Go to Submit New Job
|
|
||||||
2. Enter a valid branch name
|
|
||||||
3. Click "Validate Branch"
|
|
||||||
4. Should see green success message
|
|
||||||
5. Try invalid branch - should see red error
|
|
||||||
|
|
||||||
### 4. Test Job Abortion
|
|
||||||
|
|
||||||
1. Create a job (it will be "in progress")
|
|
||||||
2. Right-click on the job
|
|
||||||
3. Select "Abort Job"
|
|
||||||
4. Job icon should turn black (⚫)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎨 UI/UX Improvements
|
|
||||||
|
|
||||||
### Context Menu
|
|
||||||
- Clean, modern design
|
|
||||||
- Smooth hover effects
|
|
||||||
- Automatically positions near cursor
|
|
||||||
- Closes when clicking elsewhere
|
|
||||||
|
|
||||||
### Branch Validation
|
|
||||||
- Three states: Loading (yellow), Success (green), Error (red)
|
|
||||||
- Loading spinner animation
|
|
||||||
- Clear error messages
|
|
||||||
- Disabled "Next" button until validation succeeds
|
|
||||||
- Re-validation required if branch name changes
|
|
||||||
|
|
||||||
### Visual Feedback
|
|
||||||
- ⚫ Black icon for aborted jobs
|
|
||||||
- 🟠 Orange icon for in-progress jobs
|
|
||||||
- 🟢 Green icon for passed jobs
|
|
||||||
- 🔴 Red icon for failed jobs
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🐛 Known Limitations
|
|
||||||
|
|
||||||
1. **SSH Password Security:** Currently stored in plain text environment variables
|
|
||||||
2. **No Retry Logic:** If SSH command fails, user must manually retry
|
|
||||||
3. **Timeout:** 60-second timeout for SSH commands
|
|
||||||
4. **No Progress Bar:** User doesn't see detailed progress during validation
|
|
||||||
5. **Single Server:** Only supports one SSH host
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 Future Enhancements
|
|
||||||
|
|
||||||
### Suggested Improvements:
|
|
||||||
1. **SSH Key Authentication:** Replace password with SSH keys
|
|
||||||
2. **Retry Logic:** Automatic retry on transient failures
|
|
||||||
3. **Progress Indicators:** Show detailed progress during validation
|
|
||||||
4. **Multiple Servers:** Support for multiple SSH hosts
|
|
||||||
5. **Caching:** Cache branch validation results
|
|
||||||
6. **Webhooks:** Real-time notifications when branch is pushed
|
|
||||||
7. **Batch Operations:** Abort multiple jobs at once
|
|
||||||
8. **Job History:** Show abort history and who aborted
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📞 Support
|
|
||||||
|
|
||||||
If you encounter issues:
|
|
||||||
|
|
||||||
1. **Branch Validation Fails:**
|
|
||||||
- Check SSH credentials in docker-compose.yml
|
|
||||||
- Verify sshpass is installed: `docker exec testarena_web which sshpass`
|
|
||||||
- Check SSH connectivity: `docker exec testarena_web ssh asf@asfserver`
|
|
||||||
- Review logs: `docker-compose logs web`
|
|
||||||
|
|
||||||
2. **Context Menu Not Showing:**
|
|
||||||
- Ensure job status is "in_progress"
|
|
||||||
- Check browser console for JavaScript errors
|
|
||||||
- Clear browser cache
|
|
||||||
|
|
||||||
3. **Abort Not Working:**
|
|
||||||
- Check logs: `docker-compose logs web`
|
|
||||||
- Verify job exists in database
|
|
||||||
- Check user permissions
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📝 Testing Checklist
|
|
||||||
|
|
||||||
- [ ] Branch validation with valid branch
|
|
||||||
- [ ] Branch validation with invalid branch
|
|
||||||
- [ ] Branch validation timeout handling
|
|
||||||
- [ ] Context menu appears on right-click
|
|
||||||
- [ ] Context menu only shows for in-progress jobs
|
|
||||||
- [ ] Abort job functionality works
|
|
||||||
- [ ] Job icon changes to black after abort
|
|
||||||
- [ ] Cannot proceed without valid branch
|
|
||||||
- [ ] Re-validation required after branch name change
|
|
||||||
- [ ] SSH commands execute successfully
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Implementation Date:** November 28, 2024
|
|
||||||
**Version:** 1.1.0
|
|
||||||
**Status:** ✅ Ready for Testing
|
|
||||||
@@ -1,255 +0,0 @@
|
|||||||
# ASF TestArena - Project Status
|
|
||||||
|
|
||||||
## 📊 Implementation Progress
|
|
||||||
|
|
||||||
### Phase 1: Core Platform (COMPLETED ✅)
|
|
||||||
|
|
||||||
| Feature | Status | Description |
|
|
||||||
|---------|--------|-------------|
|
|
||||||
| Login System | ✅ | Secure authentication with Flask-Login |
|
|
||||||
| Modern Theme | ✅ | Gradient design with custom logo |
|
|
||||||
| Admin Dashboard | ✅ | User management (create, delete, reset password) |
|
|
||||||
| User Dashboard | ✅ | Two-panel layout with job list and details |
|
|
||||||
| Submit Workflow | ✅ | 5-step wizard for job submission |
|
|
||||||
| Docker Setup | ✅ | PostgreSQL + Flask + Caddy integration |
|
|
||||||
| Database Models | ✅ | Users and Jobs tables with relationships |
|
|
||||||
|
|
||||||
### Phase 2: Test Execution (PENDING ⏳)
|
|
||||||
|
|
||||||
| Feature | Status | Description |
|
|
||||||
|---------|--------|-------------|
|
|
||||||
| Git Integration | ⏳ | Branch checkout and scenario detection |
|
|
||||||
| Test Runner | ⏳ | Background job execution |
|
|
||||||
| Status Updates | ⏳ | Real-time job monitoring |
|
|
||||||
| Results Storage | ⏳ | HTML report generation and storage |
|
|
||||||
| Auto Cleanup | ⏳ | 7-day retention policy |
|
|
||||||
| Job Abort | ⏳ | Kill running processes |
|
|
||||||
|
|
||||||
## 🎨 User Interface
|
|
||||||
|
|
||||||
### Login Page
|
|
||||||
- Clean, centered login form
|
|
||||||
- Logo and branding
|
|
||||||
- Error message display
|
|
||||||
- Responsive design
|
|
||||||
|
|
||||||
### Dashboard (All Users)
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────────────┐
|
|
||||||
│ Logo ASF TestArena Dashboard | Submit | Logout │
|
|
||||||
├──────────────┬──────────────────────────────────────┤
|
|
||||||
│ Test Jobs │ Job Details │
|
|
||||||
│ [+ New Job] │ │
|
|
||||||
│ │ Select a job from the list │
|
|
||||||
│ 🟠 Job #1 │ to view details here │
|
|
||||||
│ 🟢 Job #2 │ │
|
|
||||||
│ 🔴 Job #3 │ │
|
|
||||||
│ ⚫ Job #4 │ │
|
|
||||||
│ │ │
|
|
||||||
└──────────────┴──────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
### Admin Dashboard
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────────────┐
|
|
||||||
│ User Management [+ Create User] │
|
|
||||||
├─────┬──────────┬───────┬────────────┬──────────────┤
|
|
||||||
│ ID │ Username │ Role │ Created │ Actions │
|
|
||||||
├─────┼──────────┼───────┼────────────┼──────────────┤
|
|
||||||
│ 1 │ admin │ Admin │ 2024-01-01 │ Reset | Del │
|
|
||||||
│ 2 │ user1 │ User │ 2024-01-02 │ Reset | Del │
|
|
||||||
└─────┴──────────┴───────┴────────────┴──────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
### Submit Workflow
|
|
||||||
```
|
|
||||||
Step 1: Branch Name
|
|
||||||
↓
|
|
||||||
Step 2: Select Scenarios (checkboxes)
|
|
||||||
↓
|
|
||||||
Step 3: Choose Environment (Sensor Hub / Main Board)
|
|
||||||
↓
|
|
||||||
Step 4: Test Mode (Simulator / HIL) + Options
|
|
||||||
↓
|
|
||||||
Step 5: Submit & Start Test
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🗄️ Database Schema
|
|
||||||
|
|
||||||
### Users Table
|
|
||||||
```sql
|
|
||||||
CREATE TABLE users (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
username VARCHAR(80) UNIQUE NOT NULL,
|
|
||||||
password_hash VARCHAR(255) NOT NULL,
|
|
||||||
is_admin BOOLEAN DEFAULT FALSE,
|
|
||||||
created_at TIMESTAMP DEFAULT NOW()
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Jobs Table
|
|
||||||
```sql
|
|
||||||
CREATE TABLE jobs (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
user_id INTEGER REFERENCES users(id),
|
|
||||||
branch_name VARCHAR(255) NOT NULL,
|
|
||||||
scenarios TEXT NOT NULL,
|
|
||||||
environment VARCHAR(50) NOT NULL,
|
|
||||||
test_mode VARCHAR(50) NOT NULL,
|
|
||||||
status VARCHAR(20) DEFAULT 'in_progress',
|
|
||||||
submitted_at TIMESTAMP DEFAULT NOW(),
|
|
||||||
completed_at TIMESTAMP,
|
|
||||||
duration INTEGER,
|
|
||||||
keep_devbenches BOOLEAN DEFAULT FALSE,
|
|
||||||
reuse_results BOOLEAN DEFAULT FALSE,
|
|
||||||
results_path VARCHAR(500)
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔐 User Roles & Permissions
|
|
||||||
|
|
||||||
### Admin
|
|
||||||
- ✅ Create users
|
|
||||||
- ✅ Delete users
|
|
||||||
- ✅ Reset passwords
|
|
||||||
- ✅ View all jobs
|
|
||||||
- ✅ Submit jobs
|
|
||||||
- ✅ Abort any job
|
|
||||||
|
|
||||||
### Standard User
|
|
||||||
- ✅ Submit jobs
|
|
||||||
- ✅ View own jobs
|
|
||||||
- ✅ Abort own jobs
|
|
||||||
- ❌ Cannot see other users' jobs
|
|
||||||
- ❌ Cannot manage users
|
|
||||||
|
|
||||||
## 🚀 Deployment Architecture
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────┐
|
|
||||||
│ Internet │
|
|
||||||
└────────────────┬────────────────────────────┘
|
|
||||||
│ HTTPS (443)
|
|
||||||
↓
|
|
||||||
┌─────────────────────────────────────────────┐
|
|
||||||
│ Caddy Reverse Proxy │
|
|
||||||
│ (SSL/TLS, Load Balancing) │
|
|
||||||
└────────────────┬────────────────────────────┘
|
|
||||||
│ HTTP (5000)
|
|
||||||
↓
|
|
||||||
┌─────────────────────────────────────────────┐
|
|
||||||
│ Flask Web Application │
|
|
||||||
│ (testarena_web container) │
|
|
||||||
└────────────────┬────────────────────────────┘
|
|
||||||
│ PostgreSQL (5432)
|
|
||||||
↓
|
|
||||||
┌─────────────────────────────────────────────┐
|
|
||||||
│ PostgreSQL Database │
|
|
||||||
│ (testarena_db container) │
|
|
||||||
└─────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📦 Project Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
testarena/
|
|
||||||
├── app/
|
|
||||||
│ ├── __init__.py # Flask app factory
|
|
||||||
│ ├── models.py # Database models
|
|
||||||
│ ├── routes/
|
|
||||||
│ │ ├── auth.py # Login/logout
|
|
||||||
│ │ ├── admin.py # User management
|
|
||||||
│ │ ├── dashboard.py # Main dashboard
|
|
||||||
│ │ └── jobs.py # Job submission
|
|
||||||
│ ├── static/
|
|
||||||
│ │ ├── css/
|
|
||||||
│ │ │ └── style.css # Modern theme
|
|
||||||
│ │ └── uploads/
|
|
||||||
│ │ └── icon.png # Logo
|
|
||||||
│ └── templates/
|
|
||||||
│ ├── base.html # Base template
|
|
||||||
│ ├── login.html # Login page
|
|
||||||
│ ├── admin/
|
|
||||||
│ │ └── dashboard.html # Admin UI
|
|
||||||
│ ├── dashboard/
|
|
||||||
│ │ └── index.html # User dashboard
|
|
||||||
│ └── jobs/
|
|
||||||
│ ├── submit.html # Step 1
|
|
||||||
│ ├── submit_step2.html
|
|
||||||
│ ├── submit_step3.html
|
|
||||||
│ └── submit_step4.html
|
|
||||||
├── docker-compose.yml # Container orchestration
|
|
||||||
├── Dockerfile # Web app image
|
|
||||||
├── requirements.txt # Python dependencies
|
|
||||||
├── wsgi.py # WSGI entry point
|
|
||||||
├── start.bat # Windows startup script
|
|
||||||
├── stop.bat # Windows stop script
|
|
||||||
├── logs.bat # View logs
|
|
||||||
├── .env.example # Environment template
|
|
||||||
├── .gitignore # Git ignore rules
|
|
||||||
├── README.md # Main documentation
|
|
||||||
├── SETUP.md # Detailed setup guide
|
|
||||||
├── QUICK_START.md # Quick reference
|
|
||||||
├── CADDY_INTEGRATION.md # Caddy configuration
|
|
||||||
└── PROJECT_STATUS.md # This file
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔧 Technology Stack
|
|
||||||
|
|
||||||
| Component | Technology | Version |
|
|
||||||
|-----------|-----------|---------|
|
|
||||||
| Backend Framework | Flask | 3.0.0 |
|
|
||||||
| Database | PostgreSQL | 15 |
|
|
||||||
| ORM | SQLAlchemy | 3.1.1 |
|
|
||||||
| Authentication | Flask-Login | 0.6.3 |
|
|
||||||
| WSGI Server | Gunicorn | 21.2.0 |
|
|
||||||
| Reverse Proxy | Caddy | 2.x |
|
|
||||||
| Containerization | Docker | Latest |
|
|
||||||
| Frontend | HTML/CSS/JS | Native |
|
|
||||||
|
|
||||||
## 📝 Default Credentials
|
|
||||||
|
|
||||||
**⚠️ CHANGE IMMEDIATELY AFTER FIRST LOGIN**
|
|
||||||
|
|
||||||
- Username: `admin`
|
|
||||||
- Password: `admin123`
|
|
||||||
|
|
||||||
## 🎯 Next Steps
|
|
||||||
|
|
||||||
1. **Share Caddy Network Name**
|
|
||||||
- Run: `docker network ls`
|
|
||||||
- Share the network name for integration
|
|
||||||
|
|
||||||
2. **Test Phase 1**
|
|
||||||
- Login with default credentials
|
|
||||||
- Create test users
|
|
||||||
- Submit test jobs (UI only)
|
|
||||||
- Verify dashboard functionality
|
|
||||||
|
|
||||||
3. **Plan Phase 2**
|
|
||||||
- Git repository configuration
|
|
||||||
- Test script integration
|
|
||||||
- Results storage location
|
|
||||||
- Cleanup schedule
|
|
||||||
|
|
||||||
## 📞 Ready for Phase 2?
|
|
||||||
|
|
||||||
When you're ready to implement test execution:
|
|
||||||
1. Provide Git repository details
|
|
||||||
2. Share test execution scripts
|
|
||||||
3. Define result format requirements
|
|
||||||
4. Specify cleanup policies
|
|
||||||
|
|
||||||
## 🎉 What's Working Now
|
|
||||||
|
|
||||||
✅ Full authentication system
|
|
||||||
✅ Role-based access control
|
|
||||||
✅ User management interface
|
|
||||||
✅ Job submission workflow (UI)
|
|
||||||
✅ Dashboard with job list
|
|
||||||
✅ Modern, responsive design
|
|
||||||
✅ Docker containerization
|
|
||||||
✅ Database persistence
|
|
||||||
✅ Ready for Caddy integration
|
|
||||||
|
|
||||||
The foundation is solid and ready for test execution integration!
|
|
||||||
341
PROJECT_TREE.txt
341
PROJECT_TREE.txt
@@ -1,341 +0,0 @@
|
|||||||
ASF TestArena - Complete Project Structure
|
|
||||||
==========================================
|
|
||||||
|
|
||||||
testarena/
|
|
||||||
│
|
|
||||||
├── Documentation Files
|
|
||||||
│ ├── START_HERE.md ← Start here for new users
|
|
||||||
│ ├── INDEX.md ← Documentation index
|
|
||||||
│ ├── QUICK_START.md ← Fast deployment guide
|
|
||||||
│ ├── DEPLOYMENT_CHECKLIST.md ← Pre-production checklist
|
|
||||||
│ ├── CADDY_INTEGRATION.md ← Reverse proxy setup
|
|
||||||
│ ├── SETUP.md ← Detailed setup guide
|
|
||||||
│ ├── PROJECT_STATUS.md ← Implementation status
|
|
||||||
│ ├── ARCHITECTURE.md ← System design
|
|
||||||
│ ├── IMPLEMENTATION_SUMMARY.md ← Phase 1 summary
|
|
||||||
│ └── README.md ← General overview
|
|
||||||
│
|
|
||||||
├── Configuration Files
|
|
||||||
│ ├── docker-compose.yml ← Container orchestration
|
|
||||||
│ ├── Dockerfile ← Web app container image
|
|
||||||
│ ├── requirements.txt ← Python dependencies
|
|
||||||
│ ├── wsgi.py ← WSGI entry point
|
|
||||||
│ ├── .env.example ← Environment variables template
|
|
||||||
│ ├── .gitignore ← Git ignore rules
|
|
||||||
│ └── Caddyfile.example ← Caddy configuration template
|
|
||||||
│
|
|
||||||
├── Scripts
|
|
||||||
│ ├── start.bat ← Windows startup script
|
|
||||||
│ ├── stop.bat ← Windows shutdown script
|
|
||||||
│ └── logs.bat ← View logs script
|
|
||||||
│
|
|
||||||
├── Assets
|
|
||||||
│ └── icon.png ← Application logo
|
|
||||||
│
|
|
||||||
├── Legacy Files (from original project)
|
|
||||||
│ ├── scenario_exe_parser.py
|
|
||||||
│ └── scenario_scan.py
|
|
||||||
│
|
|
||||||
└── app/ ← Flask Application
|
|
||||||
│
|
|
||||||
├── Core Files
|
|
||||||
│ ├── __init__.py ← Flask app factory
|
|
||||||
│ └── models.py ← Database models (User, Job)
|
|
||||||
│
|
|
||||||
├── routes/ ← API Endpoints
|
|
||||||
│ ├── auth.py ← Login/logout routes
|
|
||||||
│ ├── admin.py ← User management routes
|
|
||||||
│ ├── dashboard.py ← Dashboard routes
|
|
||||||
│ └── jobs.py ← Job submission routes
|
|
||||||
│
|
|
||||||
├── templates/ ← HTML Templates
|
|
||||||
│ ├── base.html ← Base template with navbar
|
|
||||||
│ ├── login.html ← Login page
|
|
||||||
│ │
|
|
||||||
│ ├── admin/
|
|
||||||
│ │ └── dashboard.html ← Admin user management UI
|
|
||||||
│ │
|
|
||||||
│ ├── dashboard/
|
|
||||||
│ │ └── index.html ← User dashboard (2-panel)
|
|
||||||
│ │
|
|
||||||
│ └── jobs/
|
|
||||||
│ ├── submit.html ← Step 1: Branch name
|
|
||||||
│ ├── submit_step2.html ← Step 2: Select scenarios
|
|
||||||
│ ├── submit_step3.html ← Step 3: Choose environment
|
|
||||||
│ └── submit_step4.html ← Step 4: Test mode + submit
|
|
||||||
│
|
|
||||||
└── static/ ← Static Assets
|
|
||||||
├── css/
|
|
||||||
│ └── style.css ← Modern gradient theme (400+ lines)
|
|
||||||
│
|
|
||||||
└── uploads/
|
|
||||||
└── icon.png ← Logo (copied from root)
|
|
||||||
|
|
||||||
|
|
||||||
Docker Volumes (Created at Runtime)
|
|
||||||
====================================
|
|
||||||
├── postgres_data/ ← PostgreSQL database files
|
|
||||||
└── test_results/ ← Test execution results
|
|
||||||
|
|
||||||
|
|
||||||
Docker Networks
|
|
||||||
===============
|
|
||||||
├── testarena_network ← Internal network (web ↔ db)
|
|
||||||
└── caddy_network ← External network (caddy ↔ web)
|
|
||||||
|
|
||||||
|
|
||||||
File Count Summary
|
|
||||||
==================
|
|
||||||
Python Files: 12 files
|
|
||||||
HTML Templates: 8 files
|
|
||||||
CSS Files: 1 file
|
|
||||||
Documentation: 10 files
|
|
||||||
Configuration: 7 files
|
|
||||||
Scripts: 3 files
|
|
||||||
Assets: 2 files (icon.png in 2 locations)
|
|
||||||
─────────────────────────
|
|
||||||
Total: 43 files
|
|
||||||
|
|
||||||
|
|
||||||
Lines of Code Summary
|
|
||||||
=====================
|
|
||||||
Backend (Python): ~1,500 lines
|
|
||||||
Frontend (HTML): ~600 lines
|
|
||||||
Styling (CSS): ~400 lines
|
|
||||||
Documentation: ~3,000 lines
|
|
||||||
Configuration: ~200 lines
|
|
||||||
─────────────────────────
|
|
||||||
Total: ~5,700 lines
|
|
||||||
|
|
||||||
|
|
||||||
Key Directories Explained
|
|
||||||
==========================
|
|
||||||
|
|
||||||
/app
|
|
||||||
Main Flask application directory containing all Python code,
|
|
||||||
HTML templates, and static assets.
|
|
||||||
|
|
||||||
/app/routes
|
|
||||||
API endpoint definitions organized by feature:
|
|
||||||
- auth.py: Authentication (login/logout)
|
|
||||||
- admin.py: User management (create/delete/reset)
|
|
||||||
- dashboard.py: Main dashboard view
|
|
||||||
- jobs.py: Job submission and viewing
|
|
||||||
|
|
||||||
/app/templates
|
|
||||||
Jinja2 HTML templates with inheritance:
|
|
||||||
- base.html: Common layout (navbar, alerts, etc.)
|
|
||||||
- Feature-specific templates in subdirectories
|
|
||||||
|
|
||||||
/app/static
|
|
||||||
Static files served directly:
|
|
||||||
- css/: Stylesheets
|
|
||||||
- uploads/: User-uploaded files and assets
|
|
||||||
|
|
||||||
/app/static/css
|
|
||||||
Modern gradient theme with:
|
|
||||||
- Login page styling
|
|
||||||
- Dashboard layout (2-panel)
|
|
||||||
- Admin interface
|
|
||||||
- Form components
|
|
||||||
- Responsive design
|
|
||||||
|
|
||||||
|
|
||||||
Database Tables
|
|
||||||
===============
|
|
||||||
|
|
||||||
users
|
|
||||||
├── id (Primary Key)
|
|
||||||
├── username (Unique)
|
|
||||||
├── password_hash
|
|
||||||
├── is_admin (Boolean)
|
|
||||||
└── created_at (Timestamp)
|
|
||||||
|
|
||||||
jobs
|
|
||||||
├── id (Primary Key)
|
|
||||||
├── user_id (Foreign Key → users.id)
|
|
||||||
├── branch_name
|
|
||||||
├── scenarios (JSON)
|
|
||||||
├── environment
|
|
||||||
├── test_mode
|
|
||||||
├── status
|
|
||||||
├── submitted_at (Timestamp)
|
|
||||||
├── completed_at (Timestamp, nullable)
|
|
||||||
├── duration (Integer, nullable)
|
|
||||||
├── keep_devbenches (Boolean)
|
|
||||||
├── reuse_results (Boolean)
|
|
||||||
└── results_path (String, nullable)
|
|
||||||
|
|
||||||
|
|
||||||
API Endpoints
|
|
||||||
=============
|
|
||||||
|
|
||||||
Authentication
|
|
||||||
├── GET / → Redirect to login
|
|
||||||
├── GET /login → Login page
|
|
||||||
├── POST /login → Login submission
|
|
||||||
└── GET /logout → Logout
|
|
||||||
|
|
||||||
Dashboard
|
|
||||||
└── GET /dashboard/ → Main dashboard
|
|
||||||
|
|
||||||
Admin
|
|
||||||
├── GET /admin/ → Admin dashboard
|
|
||||||
├── POST /admin/users/create → Create user
|
|
||||||
├── POST /admin/users/<id>/reset-password → Reset password
|
|
||||||
└── POST /admin/users/<id>/delete → Delete user
|
|
||||||
|
|
||||||
Jobs
|
|
||||||
├── GET /jobs/submit → Submit form (step 1)
|
|
||||||
├── POST /jobs/submit/step1 → Process step 1
|
|
||||||
├── POST /jobs/submit/step2 → Process step 2
|
|
||||||
├── POST /jobs/submit/step3 → Process step 3
|
|
||||||
├── POST /jobs/submit/final → Submit job
|
|
||||||
├── GET /jobs/<id> → Get job details (JSON)
|
|
||||||
└── POST /jobs/<id>/abort → Abort job
|
|
||||||
|
|
||||||
|
|
||||||
Technology Stack
|
|
||||||
================
|
|
||||||
|
|
||||||
Backend
|
|
||||||
├── Flask 3.0.0 → Web framework
|
|
||||||
├── Flask-SQLAlchemy 3.1.1 → ORM
|
|
||||||
├── Flask-Login 0.6.3 → Authentication
|
|
||||||
├── Flask-WTF 1.2.1 → Forms & CSRF
|
|
||||||
├── Gunicorn 21.2.0 → WSGI server
|
|
||||||
├── psycopg2-binary 2.9.9 → PostgreSQL adapter
|
|
||||||
└── Werkzeug 3.0.1 → Security utilities
|
|
||||||
|
|
||||||
Database
|
|
||||||
└── PostgreSQL 15 → Relational database
|
|
||||||
|
|
||||||
Frontend
|
|
||||||
├── HTML5 → Markup
|
|
||||||
├── CSS3 → Styling
|
|
||||||
└── JavaScript (Vanilla) → Interactivity
|
|
||||||
|
|
||||||
Infrastructure
|
|
||||||
├── Docker → Containerization
|
|
||||||
├── Docker Compose → Orchestration
|
|
||||||
└── Caddy 2.x → Reverse proxy
|
|
||||||
|
|
||||||
|
|
||||||
Security Features
|
|
||||||
=================
|
|
||||||
✓ Password hashing (Werkzeug)
|
|
||||||
✓ Session management (Flask-Login)
|
|
||||||
✓ CSRF protection (Flask-WTF)
|
|
||||||
✓ SQL injection protection (SQLAlchemy ORM)
|
|
||||||
✓ Role-based access control
|
|
||||||
✓ HTTPS ready (via Caddy)
|
|
||||||
✓ Secure session cookies
|
|
||||||
✓ XSS protection (Jinja2 auto-escaping)
|
|
||||||
|
|
||||||
|
|
||||||
Deployment Architecture
|
|
||||||
=======================
|
|
||||||
|
|
||||||
Internet (HTTPS/443)
|
|
||||||
↓
|
|
||||||
Caddy Reverse Proxy
|
|
||||||
↓
|
|
||||||
Flask Web App (HTTP/5000)
|
|
||||||
↓
|
|
||||||
PostgreSQL Database (5432)
|
|
||||||
|
|
||||||
|
|
||||||
Development vs Production
|
|
||||||
==========================
|
|
||||||
|
|
||||||
Development:
|
|
||||||
- SQLite database (optional)
|
|
||||||
- Flask development server
|
|
||||||
- Debug mode enabled
|
|
||||||
- Hot reload
|
|
||||||
|
|
||||||
Production:
|
|
||||||
- PostgreSQL database
|
|
||||||
- Gunicorn WSGI server
|
|
||||||
- Debug mode disabled
|
|
||||||
- Docker containers
|
|
||||||
- Caddy reverse proxy
|
|
||||||
- HTTPS enabled
|
|
||||||
|
|
||||||
|
|
||||||
Phase 1: COMPLETE ✅
|
|
||||||
====================
|
|
||||||
✓ Login system
|
|
||||||
✓ User management
|
|
||||||
✓ Dashboard
|
|
||||||
✓ Job submission UI
|
|
||||||
✓ Docker setup
|
|
||||||
✓ Documentation
|
|
||||||
|
|
||||||
|
|
||||||
Phase 2: PENDING ⏳
|
|
||||||
===================
|
|
||||||
⏳ Git integration
|
|
||||||
⏳ Test execution
|
|
||||||
⏳ Status updates
|
|
||||||
⏳ Results generation
|
|
||||||
⏳ Auto cleanup
|
|
||||||
⏳ Job abort
|
|
||||||
|
|
||||||
|
|
||||||
Quick Commands
|
|
||||||
==============
|
|
||||||
|
|
||||||
Start: start.bat
|
|
||||||
Stop: stop.bat
|
|
||||||
Logs: logs.bat
|
|
||||||
Build: docker-compose up -d --build
|
|
||||||
Restart: docker-compose restart
|
|
||||||
Status: docker ps
|
|
||||||
|
|
||||||
|
|
||||||
Default Credentials
|
|
||||||
===================
|
|
||||||
Username: admin
|
|
||||||
Password: admin123
|
|
||||||
|
|
||||||
⚠️ CHANGE IMMEDIATELY AFTER FIRST LOGIN!
|
|
||||||
|
|
||||||
|
|
||||||
Documentation Guide
|
|
||||||
===================
|
|
||||||
New User? → START_HERE.md
|
|
||||||
Quick Deploy? → QUICK_START.md
|
|
||||||
Configure Caddy? → CADDY_INTEGRATION.md
|
|
||||||
Pre-Production? → DEPLOYMENT_CHECKLIST.md
|
|
||||||
Understand System? → ARCHITECTURE.md
|
|
||||||
Detailed Setup? → SETUP.md
|
|
||||||
Find Anything? → INDEX.md
|
|
||||||
|
|
||||||
|
|
||||||
Project Status
|
|
||||||
==============
|
|
||||||
Status: ✅ Phase 1 Complete
|
|
||||||
Ready: ✅ Production Ready
|
|
||||||
Tested: ✅ Core Features Working
|
|
||||||
Docs: ✅ Comprehensive
|
|
||||||
Next: ⏳ Phase 2 - Test Execution
|
|
||||||
|
|
||||||
|
|
||||||
Contact & Support
|
|
||||||
=================
|
|
||||||
For deployment help, check:
|
|
||||||
1. DEPLOYMENT_CHECKLIST.md
|
|
||||||
2. CADDY_INTEGRATION.md
|
|
||||||
3. Docker logs: docker-compose logs -f
|
|
||||||
|
|
||||||
For development help, check:
|
|
||||||
1. ARCHITECTURE.md
|
|
||||||
2. SETUP.md
|
|
||||||
3. Code comments in app/
|
|
||||||
|
|
||||||
|
|
||||||
End of Project Structure
|
|
||||||
=========================
|
|
||||||
Generated: November 28, 2024
|
|
||||||
Version: 1.0.0 (Phase 1)
|
|
||||||
105
QUICK_START.md
105
QUICK_START.md
@@ -1,105 +0,0 @@
|
|||||||
# Quick Start Guide
|
|
||||||
|
|
||||||
## 🚀 Get Started in 2 Steps
|
|
||||||
|
|
||||||
### Step 1: Deploy the Application
|
|
||||||
|
|
||||||
**Windows (PowerShell):**
|
|
||||||
```powershell
|
|
||||||
.\deploy.ps1
|
|
||||||
```
|
|
||||||
|
|
||||||
**Windows (CMD):**
|
|
||||||
```bash
|
|
||||||
start.bat
|
|
||||||
```
|
|
||||||
|
|
||||||
**Linux/Mac:**
|
|
||||||
```bash
|
|
||||||
chmod +x deploy.sh
|
|
||||||
./deploy.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
The script will automatically:
|
|
||||||
- Check Docker is running
|
|
||||||
- Create Caddy network if needed
|
|
||||||
- Build and start containers
|
|
||||||
- Verify deployment
|
|
||||||
|
|
||||||
### Step 2: Login
|
|
||||||
- URL: http://localhost:5000 or https://testarena.nabd-co.com
|
|
||||||
- Username: `admin`
|
|
||||||
- Password: `admin123`
|
|
||||||
|
|
||||||
⚠️ **IMPORTANT:** Change the admin password immediately!
|
|
||||||
|
|
||||||
## 📋 What's Included
|
|
||||||
|
|
||||||
✅ Login system with authentication
|
|
||||||
✅ Modern gradient theme with your logo
|
|
||||||
✅ Admin dashboard (create/delete users, reset passwords)
|
|
||||||
✅ User dashboard (view jobs, job details)
|
|
||||||
✅ Submit page (5-step wizard)
|
|
||||||
✅ Docker Compose with PostgreSQL
|
|
||||||
✅ Caddy proxy ready
|
|
||||||
|
|
||||||
## 🎯 User Workflows
|
|
||||||
|
|
||||||
### Admin Workflow
|
|
||||||
1. Login → Admin Dashboard
|
|
||||||
2. Create users with roles
|
|
||||||
3. View all jobs from all users
|
|
||||||
4. Manage user accounts
|
|
||||||
|
|
||||||
### User Workflow
|
|
||||||
1. Login → Dashboard
|
|
||||||
2. Click "Submit Job"
|
|
||||||
3. Enter branch name → Select scenarios → Choose environment → Select test mode
|
|
||||||
4. Monitor job status in dashboard
|
|
||||||
5. View results when complete
|
|
||||||
|
|
||||||
## 🛠️ Useful Commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Start
|
|
||||||
docker-compose up -d
|
|
||||||
|
|
||||||
# Stop
|
|
||||||
docker-compose down
|
|
||||||
|
|
||||||
# View logs
|
|
||||||
docker-compose logs -f
|
|
||||||
|
|
||||||
# Restart
|
|
||||||
docker-compose restart
|
|
||||||
|
|
||||||
# Rebuild
|
|
||||||
docker-compose up -d --build
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📁 Key Files
|
|
||||||
|
|
||||||
- `docker-compose.yml` - Container configuration
|
|
||||||
- `app/__init__.py` - Flask app setup
|
|
||||||
- `app/models.py` - Database models
|
|
||||||
- `app/static/css/style.css` - Theme styles
|
|
||||||
- `app/templates/` - HTML templates
|
|
||||||
|
|
||||||
## 🔐 Security Checklist
|
|
||||||
|
|
||||||
- [ ] Change default admin password
|
|
||||||
- [ ] Update SECRET_KEY in docker-compose.yml
|
|
||||||
- [ ] Update database password
|
|
||||||
- [ ] Configure HTTPS via Caddy
|
|
||||||
- [ ] Review user permissions
|
|
||||||
|
|
||||||
## 📞 Next Phase
|
|
||||||
|
|
||||||
Phase 2 will implement:
|
|
||||||
- Git branch checkout and scenario detection
|
|
||||||
- Background test execution
|
|
||||||
- HTML results generation
|
|
||||||
- Automatic cleanup
|
|
||||||
- Real-time status updates
|
|
||||||
|
|
||||||
Share the Caddy network name when ready to proceed!
|
|
||||||
@@ -1,303 +0,0 @@
|
|||||||
# 🚀 ASF TestArena - READY TO DEPLOY!
|
|
||||||
|
|
||||||
## ✅ Configuration Complete
|
|
||||||
|
|
||||||
Your ASF TestArena is now fully configured and ready for deployment!
|
|
||||||
|
|
||||||
### What's Been Configured
|
|
||||||
|
|
||||||
✅ **Docker Networks**
|
|
||||||
- Internal network: `app-network` (web ↔ database)
|
|
||||||
- External network: `caddy_network` (Caddy ↔ web)
|
|
||||||
- Both networks configured in docker-compose.yml
|
|
||||||
|
|
||||||
✅ **Deployment Scripts**
|
|
||||||
- `deploy.ps1` - PowerShell deployment script (Windows)
|
|
||||||
- `deploy.sh` - Bash deployment script (Linux/Mac)
|
|
||||||
- `start.bat` - Simple Windows startup script
|
|
||||||
- All scripts include error checking and validation
|
|
||||||
|
|
||||||
✅ **Documentation Updated**
|
|
||||||
- All guides updated with correct network names
|
|
||||||
- Deployment instructions simplified
|
|
||||||
- Troubleshooting guides included
|
|
||||||
|
|
||||||
## 🎯 Deploy Now (Choose One)
|
|
||||||
|
|
||||||
### Windows Users
|
|
||||||
|
|
||||||
**Option 1: PowerShell (Recommended)**
|
|
||||||
```powershell
|
|
||||||
.\deploy.ps1
|
|
||||||
```
|
|
||||||
|
|
||||||
**Option 2: Command Prompt**
|
|
||||||
```cmd
|
|
||||||
start.bat
|
|
||||||
```
|
|
||||||
|
|
||||||
### Linux/Mac Users
|
|
||||||
|
|
||||||
```bash
|
|
||||||
chmod +x deploy.sh
|
|
||||||
./deploy.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📋 What the Deployment Script Does
|
|
||||||
|
|
||||||
1. ✅ Checks Docker is installed and running
|
|
||||||
2. ✅ Checks Docker Compose is available
|
|
||||||
3. ✅ Creates `.env` file if missing
|
|
||||||
4. ✅ Verifies/creates `caddy_network`
|
|
||||||
5. ✅ Stops any existing containers
|
|
||||||
6. ✅ Builds new container images
|
|
||||||
7. ✅ Starts all services
|
|
||||||
8. ✅ Waits for initialization
|
|
||||||
9. ✅ Verifies containers are running
|
|
||||||
10. ✅ Displays access information
|
|
||||||
|
|
||||||
## ⏱️ Deployment Time
|
|
||||||
|
|
||||||
- **First deployment:** 3-5 minutes (building images)
|
|
||||||
- **Subsequent deployments:** 30-60 seconds
|
|
||||||
|
|
||||||
## 🌐 Access After Deployment
|
|
||||||
|
|
||||||
**Local Access:**
|
|
||||||
- URL: http://localhost:5000
|
|
||||||
|
|
||||||
**Domain Access:**
|
|
||||||
- URL: https://testarena.nabd-co.com
|
|
||||||
- (Requires Caddy configuration - see below)
|
|
||||||
|
|
||||||
**Default Login:**
|
|
||||||
- Username: `admin`
|
|
||||||
- Password: `admin123`
|
|
||||||
|
|
||||||
⚠️ **CHANGE PASSWORD IMMEDIATELY AFTER FIRST LOGIN!**
|
|
||||||
|
|
||||||
## 🔧 Caddy Configuration (If Not Already Done)
|
|
||||||
|
|
||||||
Add this to your Caddyfile:
|
|
||||||
|
|
||||||
```
|
|
||||||
testarena.nabd-co.com {
|
|
||||||
reverse_proxy testarena_web:5000
|
|
||||||
encode gzip
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Reload Caddy:
|
|
||||||
```bash
|
|
||||||
docker exec caddy_container caddy reload --config /etc/caddy/Caddyfile
|
|
||||||
```
|
|
||||||
|
|
||||||
## ✅ Post-Deployment Checklist
|
|
||||||
|
|
||||||
After deployment completes:
|
|
||||||
|
|
||||||
1. [ ] Verify containers are running: `docker-compose ps`
|
|
||||||
2. [ ] Check logs for errors: `docker-compose logs`
|
|
||||||
3. [ ] Access login page: https://testarena.nabd-co.com
|
|
||||||
4. [ ] Login with default credentials
|
|
||||||
5. [ ] Change admin password
|
|
||||||
6. [ ] Create test user account
|
|
||||||
7. [ ] Test user login
|
|
||||||
8. [ ] Test job submission workflow
|
|
||||||
9. [ ] Verify admin can see all jobs
|
|
||||||
10. [ ] Verify user can only see own jobs
|
|
||||||
|
|
||||||
## 🎉 What You'll Have
|
|
||||||
|
|
||||||
After successful deployment:
|
|
||||||
|
|
||||||
### For Admins
|
|
||||||
- User management dashboard
|
|
||||||
- Create/delete users
|
|
||||||
- Reset passwords
|
|
||||||
- View all test jobs
|
|
||||||
- Full system access
|
|
||||||
|
|
||||||
### For Users
|
|
||||||
- Personal dashboard
|
|
||||||
- Submit test jobs (5-step wizard)
|
|
||||||
- View own jobs
|
|
||||||
- Monitor job status
|
|
||||||
- View job details
|
|
||||||
|
|
||||||
### Features Working
|
|
||||||
- ✅ Secure login/logout
|
|
||||||
- ✅ Role-based access control
|
|
||||||
- ✅ User management
|
|
||||||
- ✅ Job submission workflow
|
|
||||||
- ✅ Dashboard with job list
|
|
||||||
- ✅ Job details view
|
|
||||||
- ✅ Modern, responsive UI
|
|
||||||
- ✅ Database persistence
|
|
||||||
|
|
||||||
### Features Pending (Phase 2)
|
|
||||||
- ⏳ Git branch checkout
|
|
||||||
- ⏳ Scenario detection
|
|
||||||
- ⏳ Test execution
|
|
||||||
- ⏳ Real-time status updates
|
|
||||||
- ⏳ Results generation
|
|
||||||
- ⏳ Automatic cleanup
|
|
||||||
|
|
||||||
## 📊 System Architecture
|
|
||||||
|
|
||||||
```
|
|
||||||
Internet (HTTPS)
|
|
||||||
↓
|
|
||||||
Caddy Proxy
|
|
||||||
↓
|
|
||||||
testarena_web (Flask/Gunicorn)
|
|
||||||
↓
|
|
||||||
testarena_db (PostgreSQL)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Networks:**
|
|
||||||
- `caddy_network` - External (Caddy ↔ Web)
|
|
||||||
- `app-network` - Internal (Web ↔ Database)
|
|
||||||
|
|
||||||
**Volumes:**
|
|
||||||
- `postgres_data` - Database persistence
|
|
||||||
- `test_results` - Test output files
|
|
||||||
|
|
||||||
## 🛠️ Useful Commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# View logs
|
|
||||||
docker-compose logs -f
|
|
||||||
|
|
||||||
# Check status
|
|
||||||
docker-compose ps
|
|
||||||
|
|
||||||
# Restart services
|
|
||||||
docker-compose restart
|
|
||||||
|
|
||||||
# Stop services
|
|
||||||
docker-compose down
|
|
||||||
|
|
||||||
# Rebuild and restart
|
|
||||||
docker-compose up -d --build
|
|
||||||
|
|
||||||
# Access web container shell
|
|
||||||
docker exec -it testarena_web bash
|
|
||||||
|
|
||||||
# Access database
|
|
||||||
docker exec -it testarena_db psql -U testarena_user testarena
|
|
||||||
|
|
||||||
# Backup database
|
|
||||||
docker exec testarena_db pg_dump -U testarena_user testarena > backup.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🐛 Quick Troubleshooting
|
|
||||||
|
|
||||||
### Deployment Script Fails
|
|
||||||
|
|
||||||
**Check Docker is running:**
|
|
||||||
```bash
|
|
||||||
docker info
|
|
||||||
```
|
|
||||||
|
|
||||||
**Check Docker Compose:**
|
|
||||||
```bash
|
|
||||||
docker-compose --version
|
|
||||||
```
|
|
||||||
|
|
||||||
### Containers Won't Start
|
|
||||||
|
|
||||||
**View logs:**
|
|
||||||
```bash
|
|
||||||
docker-compose logs
|
|
||||||
```
|
|
||||||
|
|
||||||
**Common issues:**
|
|
||||||
- Port 5000 already in use
|
|
||||||
- Database initialization (wait 30 seconds)
|
|
||||||
- Network doesn't exist (script creates it)
|
|
||||||
|
|
||||||
### Can't Access Website
|
|
||||||
|
|
||||||
**Check containers:**
|
|
||||||
```bash
|
|
||||||
docker-compose ps
|
|
||||||
```
|
|
||||||
|
|
||||||
**Check web container:**
|
|
||||||
```bash
|
|
||||||
docker-compose logs web
|
|
||||||
```
|
|
||||||
|
|
||||||
**Test local access:**
|
|
||||||
```bash
|
|
||||||
curl http://localhost:5000
|
|
||||||
```
|
|
||||||
|
|
||||||
### 502 Bad Gateway
|
|
||||||
|
|
||||||
**Wait for initialization:**
|
|
||||||
- Web container may still be starting
|
|
||||||
- Wait 30-60 seconds and try again
|
|
||||||
|
|
||||||
**Check Gunicorn:**
|
|
||||||
```bash
|
|
||||||
docker exec testarena_web ps aux | grep gunicorn
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📚 Documentation
|
|
||||||
|
|
||||||
- **DEPLOY_GUIDE.md** - Comprehensive deployment guide
|
|
||||||
- **START_HERE.md** - Quick start guide
|
|
||||||
- **QUICK_START.md** - Fast reference
|
|
||||||
- **INDEX.md** - Documentation index
|
|
||||||
- **TROUBLESHOOTING.md** - Common issues
|
|
||||||
|
|
||||||
## 🔐 Security Reminders
|
|
||||||
|
|
||||||
1. ⚠️ Change default admin password
|
|
||||||
2. ⚠️ Update SECRET_KEY in production
|
|
||||||
3. ⚠️ Use strong database password
|
|
||||||
4. ⚠️ Enable HTTPS via Caddy
|
|
||||||
5. ⚠️ Regular security updates
|
|
||||||
|
|
||||||
## 📞 Support
|
|
||||||
|
|
||||||
If you encounter issues:
|
|
||||||
|
|
||||||
1. Check deployment logs
|
|
||||||
2. Review DEPLOY_GUIDE.md
|
|
||||||
3. Check TROUBLESHOOTING.md
|
|
||||||
4. Review container logs: `docker-compose logs -f`
|
|
||||||
|
|
||||||
## 🎊 Ready to Deploy!
|
|
||||||
|
|
||||||
Everything is configured and ready. Just run the deployment script:
|
|
||||||
|
|
||||||
**Windows:**
|
|
||||||
```powershell
|
|
||||||
.\deploy.ps1
|
|
||||||
```
|
|
||||||
|
|
||||||
**Linux/Mac:**
|
|
||||||
```bash
|
|
||||||
./deploy.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
The script will guide you through the process and verify everything is working!
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📝 Deployment Notes
|
|
||||||
|
|
||||||
**Date:** _______________
|
|
||||||
**Deployed By:** _______________
|
|
||||||
**Server:** _______________
|
|
||||||
**Domain:** testarena.nabd-co.com
|
|
||||||
**Status:** ⏳ Ready to Deploy
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Good luck with your deployment! 🚀**
|
|
||||||
|
|
||||||
For any questions, refer to the comprehensive documentation in the project root.
|
|
||||||
229
SETUP.md
229
SETUP.md
@@ -1,229 +0,0 @@
|
|||||||
# ASF TestArena - Setup Guide
|
|
||||||
|
|
||||||
## Phase 1 Implementation Status ✅
|
|
||||||
|
|
||||||
The following features have been implemented:
|
|
||||||
|
|
||||||
1. ✅ Login System
|
|
||||||
- Secure authentication with Flask-Login
|
|
||||||
- Session management
|
|
||||||
- Default admin account (username: admin, password: admin123)
|
|
||||||
|
|
||||||
2. ✅ Modern Theme
|
|
||||||
- Gradient background design
|
|
||||||
- Clean, modern UI components
|
|
||||||
- Responsive layout
|
|
||||||
- Custom logo integration
|
|
||||||
|
|
||||||
3. ✅ Admin Dashboard
|
|
||||||
- User creation with role assignment
|
|
||||||
- Password reset functionality
|
|
||||||
- User deletion
|
|
||||||
- User list with role badges
|
|
||||||
|
|
||||||
4. ✅ User Dashboard
|
|
||||||
- Job list panel (left side)
|
|
||||||
- Job details panel (right side)
|
|
||||||
- Status indicators with colored icons
|
|
||||||
- Real-time job selection
|
|
||||||
|
|
||||||
5. ✅ Submit Page
|
|
||||||
- Multi-step wizard (5 steps)
|
|
||||||
- Branch selection
|
|
||||||
- Scenario selection with checkboxes
|
|
||||||
- Environment selection (Sensor Hub / Main Board)
|
|
||||||
- Test mode selection (Devbench Simulator / Testbench HIL)
|
|
||||||
- Additional options (keep devbenches, reuse results)
|
|
||||||
|
|
||||||
6. ✅ Docker Compose Setup
|
|
||||||
- PostgreSQL database
|
|
||||||
- Flask web application
|
|
||||||
- Caddy proxy integration ready
|
|
||||||
- Volume management for test results
|
|
||||||
|
|
||||||
## Next Steps (Phase 2)
|
|
||||||
|
|
||||||
The following features need to be implemented:
|
|
||||||
|
|
||||||
1. ⏳ Git Integration
|
|
||||||
- Branch checkout functionality
|
|
||||||
- Scenario detection script integration
|
|
||||||
- Repository management
|
|
||||||
|
|
||||||
2. ⏳ Test Execution Engine
|
|
||||||
- Background job processing
|
|
||||||
- Script execution
|
|
||||||
- Status updates
|
|
||||||
- Process management
|
|
||||||
|
|
||||||
3. ⏳ Results Management
|
|
||||||
- HTML report generation
|
|
||||||
- Results storage
|
|
||||||
- Automatic cleanup (7-day retention)
|
|
||||||
|
|
||||||
4. ⏳ Real-time Updates
|
|
||||||
- WebSocket integration for live status updates
|
|
||||||
- Progress tracking
|
|
||||||
|
|
||||||
## Configuration Steps
|
|
||||||
|
|
||||||
### 1. Update Docker Compose for Caddy
|
|
||||||
|
|
||||||
Edit `docker-compose.yml` and uncomment the Caddy network section:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
networks:
|
|
||||||
testarena_network:
|
|
||||||
driver: bridge
|
|
||||||
caddy_network: # Uncomment this
|
|
||||||
external: true # Uncomment this
|
|
||||||
```
|
|
||||||
|
|
||||||
Then add the network to the web service:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
web:
|
|
||||||
# ... other config ...
|
|
||||||
networks:
|
|
||||||
- testarena_network
|
|
||||||
- YOUR_CADDY_NETWORK_NAME # Add your actual Caddy network name
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Configure Caddy
|
|
||||||
|
|
||||||
Add to your Caddyfile (see `Caddyfile.example`):
|
|
||||||
|
|
||||||
```
|
|
||||||
testarena.nabd-co.com {
|
|
||||||
reverse_proxy testarena_web:5000
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Update Environment Variables
|
|
||||||
|
|
||||||
Copy `.env.example` to `.env` and update:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
SECRET_KEY=your-secure-random-key-here
|
|
||||||
DATABASE_URL=postgresql://testarena_user:your-secure-password@db:5432/testarena
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Start the Application
|
|
||||||
|
|
||||||
Run `start.bat` or:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker-compose up -d --build
|
|
||||||
```
|
|
||||||
|
|
||||||
## File Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
testarena/
|
|
||||||
├── app/
|
|
||||||
│ ├── __init__.py # Flask app initialization
|
|
||||||
│ ├── models.py # Database models
|
|
||||||
│ ├── routes/
|
|
||||||
│ │ ├── auth.py # Authentication routes
|
|
||||||
│ │ ├── admin.py # Admin management routes
|
|
||||||
│ │ ├── dashboard.py # Dashboard routes
|
|
||||||
│ │ └── jobs.py # Job submission routes
|
|
||||||
│ ├── templates/
|
|
||||||
│ │ ├── base.html # Base template
|
|
||||||
│ │ ├── login.html # Login page
|
|
||||||
│ │ ├── admin/
|
|
||||||
│ │ │ └── dashboard.html
|
|
||||||
│ │ ├── dashboard/
|
|
||||||
│ │ │ └── index.html
|
|
||||||
│ │ └── jobs/
|
|
||||||
│ │ ├── submit.html
|
|
||||||
│ │ ├── submit_step2.html
|
|
||||||
│ │ ├── submit_step3.html
|
|
||||||
│ │ └── submit_step4.html
|
|
||||||
│ └── static/
|
|
||||||
│ ├── css/
|
|
||||||
│ │ └── style.css # Modern theme styles
|
|
||||||
│ └── uploads/
|
|
||||||
│ └── icon.png # Logo
|
|
||||||
├── docker-compose.yml # Docker orchestration
|
|
||||||
├── Dockerfile # Web app container
|
|
||||||
├── requirements.txt # Python dependencies
|
|
||||||
├── wsgi.py # WSGI entry point
|
|
||||||
└── README.md # Documentation
|
|
||||||
```
|
|
||||||
|
|
||||||
## Database Schema
|
|
||||||
|
|
||||||
### Users Table
|
|
||||||
- id (Primary Key)
|
|
||||||
- username (Unique)
|
|
||||||
- password_hash
|
|
||||||
- is_admin (Boolean)
|
|
||||||
- created_at (Timestamp)
|
|
||||||
|
|
||||||
### Jobs Table
|
|
||||||
- id (Primary Key)
|
|
||||||
- user_id (Foreign Key → Users)
|
|
||||||
- branch_name
|
|
||||||
- scenarios (JSON)
|
|
||||||
- environment
|
|
||||||
- test_mode
|
|
||||||
- status (in_progress, passed, failed, aborted)
|
|
||||||
- submitted_at (Timestamp)
|
|
||||||
- completed_at (Timestamp, nullable)
|
|
||||||
- duration (Integer, seconds)
|
|
||||||
- keep_devbenches (Boolean)
|
|
||||||
- reuse_results (Boolean)
|
|
||||||
- results_path (String, nullable)
|
|
||||||
|
|
||||||
## API Endpoints
|
|
||||||
|
|
||||||
### Authentication
|
|
||||||
- `GET /login` - Login page
|
|
||||||
- `POST /login` - Login submission
|
|
||||||
- `GET /logout` - Logout
|
|
||||||
|
|
||||||
### Dashboard
|
|
||||||
- `GET /dashboard/` - Main dashboard
|
|
||||||
|
|
||||||
### Admin
|
|
||||||
- `GET /admin/` - Admin dashboard
|
|
||||||
- `POST /admin/users/create` - Create user
|
|
||||||
- `POST /admin/users/<id>/reset-password` - Reset password
|
|
||||||
- `POST /admin/users/<id>/delete` - Delete user
|
|
||||||
|
|
||||||
### Jobs
|
|
||||||
- `GET /jobs/submit` - Submit form (step 1)
|
|
||||||
- `POST /jobs/submit/step1` - Process step 1
|
|
||||||
- `POST /jobs/submit/step2` - Process step 2
|
|
||||||
- `POST /jobs/submit/step3` - Process step 3
|
|
||||||
- `POST /jobs/submit/final` - Submit job
|
|
||||||
- `GET /jobs/<id>` - Get job details (JSON)
|
|
||||||
- `POST /jobs/<id>/abort` - Abort job
|
|
||||||
|
|
||||||
## Security Notes
|
|
||||||
|
|
||||||
1. Change default admin password immediately
|
|
||||||
2. Update SECRET_KEY in production
|
|
||||||
3. Use strong database passwords
|
|
||||||
4. Enable HTTPS via Caddy
|
|
||||||
5. Regular security updates
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Container won't start
|
|
||||||
```bash
|
|
||||||
docker-compose logs web
|
|
||||||
```
|
|
||||||
|
|
||||||
### Database connection issues
|
|
||||||
Check DATABASE_URL in docker-compose.yml
|
|
||||||
|
|
||||||
### Can't access the site
|
|
||||||
- Verify Docker containers are running: `docker ps`
|
|
||||||
- Check logs: `docker-compose logs -f`
|
|
||||||
- Verify Caddy configuration
|
|
||||||
|
|
||||||
## Support
|
|
||||||
|
|
||||||
For issues or questions, contact the development team.
|
|
||||||
327
WHATS_NEW.md
327
WHATS_NEW.md
@@ -1,327 +0,0 @@
|
|||||||
# What's New - Network Configuration & Deployment Scripts
|
|
||||||
|
|
||||||
## 🎉 Latest Updates (November 28, 2024)
|
|
||||||
|
|
||||||
### ✅ Network Configuration Complete
|
|
||||||
|
|
||||||
**docker-compose.yml Updated:**
|
|
||||||
- ✅ Changed `testarena_network` to `app-network`
|
|
||||||
- ✅ Added `caddy_network` (external)
|
|
||||||
- ✅ Web container connected to both networks
|
|
||||||
- ✅ Database container on internal network only
|
|
||||||
- ✅ Ready for immediate deployment
|
|
||||||
|
|
||||||
**Before:**
|
|
||||||
```yaml
|
|
||||||
networks:
|
|
||||||
testarena_network:
|
|
||||||
driver: bridge
|
|
||||||
# caddy_network: # Commented out
|
|
||||||
# external: true
|
|
||||||
```
|
|
||||||
|
|
||||||
**After:**
|
|
||||||
```yaml
|
|
||||||
networks:
|
|
||||||
app-network:
|
|
||||||
driver: bridge
|
|
||||||
caddy_network:
|
|
||||||
external: true
|
|
||||||
```
|
|
||||||
|
|
||||||
### 🚀 New Deployment Scripts
|
|
||||||
|
|
||||||
**1. deploy.ps1 (PowerShell - Windows)**
|
|
||||||
- Automated deployment for Windows users
|
|
||||||
- Checks prerequisites (Docker, Docker Compose)
|
|
||||||
- Creates Caddy network if needed
|
|
||||||
- Builds and starts containers
|
|
||||||
- Verifies deployment success
|
|
||||||
- Provides access information
|
|
||||||
|
|
||||||
**2. deploy.sh (Bash - Linux/Mac)**
|
|
||||||
- Automated deployment for Linux/Mac users
|
|
||||||
- Same features as PowerShell version
|
|
||||||
- Colored output for better readability
|
|
||||||
- Error handling and validation
|
|
||||||
- Executable permissions included
|
|
||||||
|
|
||||||
**3. start.bat (Updated)**
|
|
||||||
- Simplified Windows startup
|
|
||||||
- Quick deployment option
|
|
||||||
- User-friendly output
|
|
||||||
|
|
||||||
### 📚 New Documentation
|
|
||||||
|
|
||||||
**1. DEPLOY_GUIDE.md**
|
|
||||||
- Comprehensive deployment guide
|
|
||||||
- Step-by-step instructions
|
|
||||||
- Configuration examples
|
|
||||||
- Troubleshooting section
|
|
||||||
- Post-deployment checklist
|
|
||||||
|
|
||||||
**2. READY_TO_DEPLOY.md**
|
|
||||||
- Quick deployment overview
|
|
||||||
- Access information
|
|
||||||
- Post-deployment tasks
|
|
||||||
- Security reminders
|
|
||||||
|
|
||||||
**3. DEPLOYMENT_SUMMARY.md**
|
|
||||||
- Complete deployment summary
|
|
||||||
- Feature checklist
|
|
||||||
- Timeline and expectations
|
|
||||||
- Success criteria
|
|
||||||
|
|
||||||
**4. WHATS_NEW.md**
|
|
||||||
- This file!
|
|
||||||
- Change log
|
|
||||||
- Update summary
|
|
||||||
|
|
||||||
### 📝 Documentation Updates
|
|
||||||
|
|
||||||
**Updated Files:**
|
|
||||||
- ✅ START_HERE.md - Simplified deployment steps
|
|
||||||
- ✅ QUICK_START.md - Updated with new scripts
|
|
||||||
- ✅ README.md - Updated installation section
|
|
||||||
- ✅ CADDY_INTEGRATION.md - Corrected network names
|
|
||||||
- ✅ Caddyfile.example - Added comments
|
|
||||||
|
|
||||||
### 🔧 Configuration Changes
|
|
||||||
|
|
||||||
**docker-compose.yml:**
|
|
||||||
```yaml
|
|
||||||
# Database service
|
|
||||||
networks:
|
|
||||||
- app-network # Changed from testarena_network
|
|
||||||
|
|
||||||
# Web service
|
|
||||||
networks:
|
|
||||||
- app-network # Changed from testarena_network
|
|
||||||
- caddy_network # Uncommented and configured
|
|
||||||
```
|
|
||||||
|
|
||||||
**Network Architecture:**
|
|
||||||
```
|
|
||||||
Caddy Proxy
|
|
||||||
↓ (caddy_network)
|
|
||||||
Web Container
|
|
||||||
↓ (app-network)
|
|
||||||
Database Container
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🎯 What This Means for You
|
|
||||||
|
|
||||||
### Before These Updates
|
|
||||||
- ❌ Manual network configuration required
|
|
||||||
- ❌ Multiple steps to deploy
|
|
||||||
- ❌ Network name needed to be found and updated
|
|
||||||
- ❌ Manual verification needed
|
|
||||||
|
|
||||||
### After These Updates
|
|
||||||
- ✅ Network pre-configured
|
|
||||||
- ✅ One command deployment
|
|
||||||
- ✅ Automatic network creation
|
|
||||||
- ✅ Automatic verification
|
|
||||||
|
|
||||||
## 🚀 How to Deploy Now
|
|
||||||
|
|
||||||
### Windows (PowerShell)
|
|
||||||
```powershell
|
|
||||||
.\deploy.ps1
|
|
||||||
```
|
|
||||||
|
|
||||||
### Windows (CMD)
|
|
||||||
```cmd
|
|
||||||
start.bat
|
|
||||||
```
|
|
||||||
|
|
||||||
### Linux/Mac
|
|
||||||
```bash
|
|
||||||
chmod +x deploy.sh
|
|
||||||
./deploy.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
**That's it!** No manual configuration needed.
|
|
||||||
|
|
||||||
## 📋 What the Deployment Script Does
|
|
||||||
|
|
||||||
1. ✅ Checks Docker is installed and running
|
|
||||||
2. ✅ Verifies Docker Compose is available
|
|
||||||
3. ✅ Creates `.env` file if missing
|
|
||||||
4. ✅ Checks for `caddy_network` existence
|
|
||||||
5. ✅ Creates `caddy_network` if needed
|
|
||||||
6. ✅ Stops existing containers
|
|
||||||
7. ✅ Builds Docker images
|
|
||||||
8. ✅ Starts all services
|
|
||||||
9. ✅ Waits for initialization
|
|
||||||
10. ✅ Verifies containers are running
|
|
||||||
11. ✅ Displays access information
|
|
||||||
|
|
||||||
## 🔐 Security Improvements
|
|
||||||
|
|
||||||
**Deployment Script:**
|
|
||||||
- Prompts for `.env` configuration
|
|
||||||
- Warns about default passwords
|
|
||||||
- Reminds to change admin password
|
|
||||||
- Validates prerequisites
|
|
||||||
|
|
||||||
**Documentation:**
|
|
||||||
- Security checklist added
|
|
||||||
- Password generation examples
|
|
||||||
- Best practices documented
|
|
||||||
- Post-deployment security tasks
|
|
||||||
|
|
||||||
## 📊 File Changes Summary
|
|
||||||
|
|
||||||
### New Files (4)
|
|
||||||
- `deploy.ps1` - PowerShell deployment script
|
|
||||||
- `deploy.sh` - Bash deployment script
|
|
||||||
- `DEPLOY_GUIDE.md` - Comprehensive deployment guide
|
|
||||||
- `READY_TO_DEPLOY.md` - Quick deployment overview
|
|
||||||
- `DEPLOYMENT_SUMMARY.md` - Complete summary
|
|
||||||
- `WHATS_NEW.md` - This file
|
|
||||||
|
|
||||||
### Modified Files (6)
|
|
||||||
- `docker-compose.yml` - Network configuration
|
|
||||||
- `START_HERE.md` - Simplified instructions
|
|
||||||
- `QUICK_START.md` - Updated commands
|
|
||||||
- `README.md` - Updated installation
|
|
||||||
- `CADDY_INTEGRATION.md` - Corrected networks
|
|
||||||
- `Caddyfile.example` - Added comments
|
|
||||||
|
|
||||||
### Total Changes
|
|
||||||
- **New:** 6 files
|
|
||||||
- **Modified:** 6 files
|
|
||||||
- **Lines Added:** ~1,500 lines
|
|
||||||
- **Documentation:** 100% updated
|
|
||||||
|
|
||||||
## 🎉 Benefits
|
|
||||||
|
|
||||||
### For Users
|
|
||||||
- ✅ Faster deployment (1 command vs 5+ steps)
|
|
||||||
- ✅ Less error-prone (automated checks)
|
|
||||||
- ✅ Better feedback (colored output, progress)
|
|
||||||
- ✅ Easier troubleshooting (detailed logs)
|
|
||||||
|
|
||||||
### For Administrators
|
|
||||||
- ✅ Consistent deployments
|
|
||||||
- ✅ Automated validation
|
|
||||||
- ✅ Better documentation
|
|
||||||
- ✅ Easier maintenance
|
|
||||||
|
|
||||||
### For Developers
|
|
||||||
- ✅ Clear architecture
|
|
||||||
- ✅ Well-documented setup
|
|
||||||
- ✅ Easy to extend
|
|
||||||
- ✅ Reproducible builds
|
|
||||||
|
|
||||||
## 🔄 Migration from Previous Version
|
|
||||||
|
|
||||||
If you already have the old version:
|
|
||||||
|
|
||||||
1. **Backup your data:**
|
|
||||||
```bash
|
|
||||||
docker exec testarena_db pg_dump -U testarena_user testarena > backup.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Stop old containers:**
|
|
||||||
```bash
|
|
||||||
docker-compose down
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Pull new changes:**
|
|
||||||
```bash
|
|
||||||
git pull
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Deploy with new script:**
|
|
||||||
```bash
|
|
||||||
.\deploy.ps1 # Windows
|
|
||||||
./deploy.sh # Linux/Mac
|
|
||||||
```
|
|
||||||
|
|
||||||
5. **Restore data if needed:**
|
|
||||||
```bash
|
|
||||||
docker exec -i testarena_db psql -U testarena_user testarena < backup.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📞 Support
|
|
||||||
|
|
||||||
### If You Encounter Issues
|
|
||||||
|
|
||||||
1. **Check deployment logs:**
|
|
||||||
```bash
|
|
||||||
docker-compose logs -f
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Review documentation:**
|
|
||||||
- DEPLOY_GUIDE.md
|
|
||||||
- TROUBLESHOOTING.md
|
|
||||||
- INDEX.md
|
|
||||||
|
|
||||||
3. **Verify prerequisites:**
|
|
||||||
```bash
|
|
||||||
docker --version
|
|
||||||
docker-compose --version
|
|
||||||
docker info
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Check network:**
|
|
||||||
```bash
|
|
||||||
docker network ls
|
|
||||||
docker network inspect caddy_network
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🎯 Next Steps
|
|
||||||
|
|
||||||
1. **Deploy the application:**
|
|
||||||
```powershell
|
|
||||||
.\deploy.ps1
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Access and login:**
|
|
||||||
- URL: https://testarena.nabd-co.com
|
|
||||||
- Username: admin
|
|
||||||
- Password: admin123
|
|
||||||
|
|
||||||
3. **Change admin password**
|
|
||||||
|
|
||||||
4. **Create user accounts**
|
|
||||||
|
|
||||||
5. **Test features**
|
|
||||||
|
|
||||||
6. **Configure backups**
|
|
||||||
|
|
||||||
7. **Plan Phase 2**
|
|
||||||
|
|
||||||
## 📝 Version History
|
|
||||||
|
|
||||||
### Version 1.0.1 (November 28, 2024)
|
|
||||||
- ✅ Network configuration complete
|
|
||||||
- ✅ Deployment scripts added
|
|
||||||
- ✅ Documentation updated
|
|
||||||
- ✅ Ready for production
|
|
||||||
|
|
||||||
### Version 1.0.0 (November 28, 2024)
|
|
||||||
- ✅ Initial Phase 1 implementation
|
|
||||||
- ✅ Core features complete
|
|
||||||
- ✅ Documentation created
|
|
||||||
|
|
||||||
## 🎊 Summary
|
|
||||||
|
|
||||||
**Status:** ✅ Ready to Deploy
|
|
||||||
**Configuration:** ✅ Complete
|
|
||||||
**Documentation:** ✅ Updated
|
|
||||||
**Scripts:** ✅ Created
|
|
||||||
**Testing:** ⏳ Pending Deployment
|
|
||||||
|
|
||||||
**Deploy now with one command:**
|
|
||||||
```powershell
|
|
||||||
.\deploy.ps1
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Last Updated: November 28, 2024*
|
|
||||||
*Version: 1.0.1*
|
|
||||||
*Status: Production Ready*
|
|
||||||
@@ -2,6 +2,10 @@ from flask import Flask
|
|||||||
from flask_sqlalchemy import SQLAlchemy
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
from flask_login import LoginManager
|
from flask_login import LoginManager
|
||||||
import os
|
import os
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
|
||||||
db = SQLAlchemy()
|
db = SQLAlchemy()
|
||||||
login_manager = LoginManager()
|
login_manager = LoginManager()
|
||||||
@@ -28,16 +32,45 @@ def create_app():
|
|||||||
from app.routes.admin import admin_bp
|
from app.routes.admin import admin_bp
|
||||||
from app.routes.dashboard import dashboard_bp
|
from app.routes.dashboard import dashboard_bp
|
||||||
from app.routes.jobs import jobs_bp
|
from app.routes.jobs import jobs_bp
|
||||||
|
from app.routes.api import api_bp
|
||||||
|
|
||||||
app.register_blueprint(auth_bp)
|
app.register_blueprint(auth_bp)
|
||||||
app.register_blueprint(admin_bp)
|
app.register_blueprint(admin_bp)
|
||||||
app.register_blueprint(dashboard_bp)
|
app.register_blueprint(dashboard_bp)
|
||||||
app.register_blueprint(jobs_bp)
|
app.register_blueprint(jobs_bp)
|
||||||
|
app.register_blueprint(api_bp)
|
||||||
|
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
db.create_all()
|
db.create_all()
|
||||||
|
|
||||||
|
# Simple migration for Phase 2 columns with retry
|
||||||
|
import time
|
||||||
|
max_retries = 5
|
||||||
|
for i in range(max_retries):
|
||||||
|
try:
|
||||||
|
from sqlalchemy import text
|
||||||
|
with db.engine.connect() as conn:
|
||||||
|
# Check if remote_queue_id exists
|
||||||
|
result = conn.execute(text("SELECT column_name FROM information_schema.columns WHERE table_name='jobs' AND column_name='remote_queue_id'"))
|
||||||
|
if not result.fetchone():
|
||||||
|
print("Running Phase 2 migrations...")
|
||||||
|
conn.execute(text("ALTER TABLE jobs ADD COLUMN remote_queue_id VARCHAR(50)"))
|
||||||
|
conn.execute(text("ALTER TABLE jobs ADD COLUMN remote_task_ids TEXT"))
|
||||||
|
conn.execute(text("ALTER TABLE jobs ADD COLUMN remote_results TEXT"))
|
||||||
|
conn.execute(text("ALTER TABLE jobs ADD COLUMN queue_log TEXT"))
|
||||||
|
conn.commit()
|
||||||
|
print("Phase 2 migrations completed.")
|
||||||
|
break # Success
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Migration attempt {i+1} failed: {e}")
|
||||||
|
if i < max_retries - 1:
|
||||||
|
time.sleep(2)
|
||||||
|
else:
|
||||||
|
print("Migration failed after all retries.")
|
||||||
|
|
||||||
# Create default admin user if not exists
|
# Create default admin user if not exists
|
||||||
try:
|
try:
|
||||||
|
from app.models import User
|
||||||
if not User.query.filter_by(username='admin').first():
|
if not User.query.filter_by(username='admin').first():
|
||||||
admin = User(username='admin', is_admin=True)
|
admin = User(username='admin', is_admin=True)
|
||||||
admin.set_password('admin123')
|
admin.set_password('admin123')
|
||||||
@@ -47,4 +80,22 @@ def create_app():
|
|||||||
# Admin user might already exist, rollback and continue
|
# Admin user might already exist, rollback and continue
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
|
|
||||||
|
# Start background polling thread
|
||||||
|
def poll_jobs():
|
||||||
|
with app.app_context():
|
||||||
|
from app.models import Job
|
||||||
|
from app.routes.jobs import update_job_status_internal
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
# Poll all jobs that are not finished
|
||||||
|
unfinished_jobs = Job.query.filter(Job.status.in_(['waiting', 'in_progress'])).all()
|
||||||
|
for job in unfinished_jobs:
|
||||||
|
update_job_status_internal(job)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ERROR] Background polling error: {e}")
|
||||||
|
time.sleep(20)
|
||||||
|
|
||||||
|
polling_thread = threading.Thread(target=poll_jobs, daemon=True)
|
||||||
|
polling_thread.start()
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|||||||
@@ -37,11 +37,19 @@ class Job(db.Model):
|
|||||||
reuse_results = db.Column(db.Boolean, default=False)
|
reuse_results = db.Column(db.Boolean, default=False)
|
||||||
results_path = db.Column(db.String(500), nullable=True)
|
results_path = db.Column(db.String(500), nullable=True)
|
||||||
|
|
||||||
|
# Phase 2 Remote Tracking
|
||||||
|
remote_queue_id = db.Column(db.String(50), nullable=True)
|
||||||
|
remote_task_ids = db.Column(db.Text, nullable=True) # JSON string: {scenario_name: task_id}
|
||||||
|
remote_results = db.Column(db.Text, nullable=True) # JSON string: {scenario_name: [status, link]}
|
||||||
|
queue_log = db.Column(db.Text, nullable=True)
|
||||||
|
|
||||||
def get_status_icon(self):
|
def get_status_icon(self):
|
||||||
icons = {
|
icons = {
|
||||||
'in_progress': '🟠',
|
'waiting': '⌛',
|
||||||
'passed': '🟢',
|
'in_progress': '🔄',
|
||||||
'failed': '🔴',
|
'passed': '✅',
|
||||||
'aborted': '⚫'
|
'failed': '❌',
|
||||||
|
'aborted': '⚫',
|
||||||
|
'error': '⚠️'
|
||||||
}
|
}
|
||||||
return icons.get(self.status, '⚪')
|
return icons.get(self.status, '⚪')
|
||||||
|
|||||||
120
app/routes/api.py
Normal file
120
app/routes/api.py
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
from flask import Blueprint, request, jsonify
|
||||||
|
from app.models import User, Job
|
||||||
|
from app import db
|
||||||
|
import json
|
||||||
|
import requests
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
api_bp = Blueprint('api', __name__, url_prefix='/api')
|
||||||
|
|
||||||
|
@api_bp.route('/submit_job', methods=['POST'])
|
||||||
|
def submit_job():
|
||||||
|
data = request.get_json()
|
||||||
|
|
||||||
|
if not data:
|
||||||
|
return jsonify({'error': 'No JSON data provided'}), 400
|
||||||
|
|
||||||
|
username = data.get('username')
|
||||||
|
password = data.get('password')
|
||||||
|
branch_name = data.get('branch_name')
|
||||||
|
scenarios = data.get('scenarios')
|
||||||
|
|
||||||
|
# Validation
|
||||||
|
if not all([username, password, branch_name, scenarios]):
|
||||||
|
return jsonify({'error': 'Missing required fields: username, password, branch_name, scenarios'}), 400
|
||||||
|
|
||||||
|
if not isinstance(scenarios, list) or not scenarios:
|
||||||
|
return jsonify({'error': 'Scenarios must be a non-empty list'}), 400
|
||||||
|
|
||||||
|
# Authentication
|
||||||
|
user = User.query.filter_by(username=username).first()
|
||||||
|
if not user or not user.check_password(password):
|
||||||
|
return jsonify({'error': 'Invalid credentials'}), 401
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Create Job
|
||||||
|
job = Job(
|
||||||
|
user_id=user.id,
|
||||||
|
branch_name=branch_name,
|
||||||
|
scenarios=json.dumps(scenarios),
|
||||||
|
environment='staging', # Default
|
||||||
|
test_mode='simulator', # Default
|
||||||
|
status='waiting'
|
||||||
|
)
|
||||||
|
db.session.add(job)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# Prepare Remote Trigger
|
||||||
|
remote_queue_id = str(job.id)
|
||||||
|
remote_task_ids = {s: f"{job.id}_{i+1}" for i, s in enumerate(scenarios)}
|
||||||
|
|
||||||
|
job.remote_queue_id = remote_queue_id
|
||||||
|
job.remote_task_ids = json.dumps(remote_task_ids)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# Payload for Remote Queue
|
||||||
|
# Note: We don't have the scenario map here, so we assume scenario path is same as name or not needed if remote handles it.
|
||||||
|
# However, the existing logic uses a map. If the user provides just names, we might send names as paths.
|
||||||
|
# Let's assume for this API the user knows what they are doing or the remote accepts names.
|
||||||
|
# Based on jobs.py: {remote_task_ids[s]: scenario_map.get(s, s) for s in scenarios}
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"source": branch_name,
|
||||||
|
remote_queue_id: [
|
||||||
|
'staging',
|
||||||
|
{remote_task_ids[s]: s for s in scenarios} # Use scenario name as path/value
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Trigger Remote Queue
|
||||||
|
remote_url = "http://asf-server.duckdns.org:8080/api/queue"
|
||||||
|
try:
|
||||||
|
response = requests.post(remote_url, json=payload, timeout=10)
|
||||||
|
response.raise_for_status()
|
||||||
|
remote_triggered = True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ERROR] Failed to trigger remote queue from API: {e}")
|
||||||
|
remote_triggered = False
|
||||||
|
job.queue_log = f"[SYSTEM] Failed to trigger remote queue: {str(e)}"
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'job_id': job.id,
|
||||||
|
'status': job.status,
|
||||||
|
'remote_triggered': remote_triggered,
|
||||||
|
'message': 'Job submitted successfully'
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'error': f'Internal server error: {str(e)}'}), 500
|
||||||
|
|
||||||
|
@api_bp.route('/job/<int:job_id>', methods=['GET'])
|
||||||
|
def get_job_status(job_id):
|
||||||
|
try:
|
||||||
|
job = Job.query.get(job_id)
|
||||||
|
if not job:
|
||||||
|
return jsonify({'error': 'Job not found'}), 404
|
||||||
|
|
||||||
|
# Optional: Trigger internal status update to get latest data
|
||||||
|
# We need to import this function. It's in jobs.py but circular imports might be tricky.
|
||||||
|
# For now, let's rely on the background poller or just return what we have.
|
||||||
|
# If we really need it, we can import inside the function.
|
||||||
|
try:
|
||||||
|
from app.routes.jobs import update_job_status_internal
|
||||||
|
update_job_status_internal(job)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[WARNING] Failed to trigger internal status update: {e}")
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'job_id': job.id,
|
||||||
|
'status': job.status,
|
||||||
|
'branch_name': job.branch_name,
|
||||||
|
'scenarios': json.loads(job.scenarios) if job.scenarios else [],
|
||||||
|
'remote_results': json.loads(job.remote_results) if job.remote_results else {},
|
||||||
|
'created_at': job.submitted_at.isoformat() if job.submitted_at else None,
|
||||||
|
'completed_at': job.completed_at.isoformat() if job.completed_at else None,
|
||||||
|
'remote_queue_id': job.remote_queue_id
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'error': f'Internal server error: {str(e)}'}), 500
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
from flask import Blueprint, render_template
|
from flask import Blueprint, render_template, request
|
||||||
from flask_login import login_required, current_user
|
from flask_login import login_required, current_user
|
||||||
from app.models import Job
|
from app.models import Job
|
||||||
|
|
||||||
@@ -7,9 +7,26 @@ dashboard_bp = Blueprint('dashboard', __name__, url_prefix='/dashboard')
|
|||||||
@dashboard_bp.route('/')
|
@dashboard_bp.route('/')
|
||||||
@login_required
|
@login_required
|
||||||
def index():
|
def index():
|
||||||
if current_user.is_admin:
|
username_query = request.args.get('username')
|
||||||
jobs = Job.query.order_by(Job.submitted_at.desc()).all()
|
job_id_query = request.args.get('job_id')
|
||||||
else:
|
|
||||||
jobs = Job.query.filter_by(user_id=current_user.id).order_by(Job.submitted_at.desc()).all()
|
|
||||||
|
|
||||||
return render_template('dashboard/index.html', jobs=jobs)
|
query = Job.query
|
||||||
|
|
||||||
|
# Global search by Job ID
|
||||||
|
if job_id_query:
|
||||||
|
query = query.filter(Job.id == job_id_query)
|
||||||
|
# Global search by Username
|
||||||
|
elif username_query:
|
||||||
|
from app.models import User
|
||||||
|
query = query.join(User).filter(User.username.ilike(f'%{username_query}%'))
|
||||||
|
# Default view
|
||||||
|
else:
|
||||||
|
if not current_user.is_admin:
|
||||||
|
query = query.filter(Job.user_id == current_user.id)
|
||||||
|
|
||||||
|
jobs = query.order_by(Job.submitted_at.desc()).all()
|
||||||
|
|
||||||
|
return render_template('dashboard/index.html',
|
||||||
|
jobs=jobs,
|
||||||
|
username_query=username_query,
|
||||||
|
job_id_query=job_id_query)
|
||||||
|
|||||||
@@ -5,6 +5,14 @@ from app import db
|
|||||||
import json
|
import json
|
||||||
import subprocess
|
import subprocess
|
||||||
import os
|
import os
|
||||||
|
import requests
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
import traceback
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
def generate_remote_id(length=6):
|
||||||
|
return ''.join(random.choices(string.digits, k=length))
|
||||||
|
|
||||||
jobs_bp = Blueprint('jobs', __name__, url_prefix='/jobs')
|
jobs_bp = Blueprint('jobs', __name__, url_prefix='/jobs')
|
||||||
|
|
||||||
@@ -121,7 +129,7 @@ def submit_step1():
|
|||||||
ssh_options = f"-p {ssh_port} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
|
ssh_options = f"-p {ssh_port} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
|
||||||
|
|
||||||
# First, clone the repository
|
# First, clone the repository
|
||||||
clone_cmd = f"sshpass -p '{ssh_password}' ssh {ssh_options} {ssh_user}@{ssh_host} './TPF/gitea_repo_controller.sh clone'"
|
clone_cmd = f"sshpass -p '{ssh_password}' ssh {ssh_options} {ssh_user}@{ssh_host} '. /home/asf/testarena_backend/TPF/gitea_repo_controller.sh clone'"
|
||||||
print(f"[DEBUG] Executing clone command: {clone_cmd.replace(ssh_password, '***')}")
|
print(f"[DEBUG] Executing clone command: {clone_cmd.replace(ssh_password, '***')}")
|
||||||
|
|
||||||
clone_result = subprocess.run(clone_cmd, shell=True, capture_output=True, text=True, timeout=60)
|
clone_result = subprocess.run(clone_cmd, shell=True, capture_output=True, text=True, timeout=60)
|
||||||
@@ -131,7 +139,7 @@ def submit_step1():
|
|||||||
print(f"[DEBUG] Clone stderr: {clone_result.stderr}")
|
print(f"[DEBUG] Clone stderr: {clone_result.stderr}")
|
||||||
|
|
||||||
# Then, checkout the branch
|
# Then, checkout the branch
|
||||||
checkout_cmd = f"sshpass -p '{ssh_password}' ssh {ssh_options} {ssh_user}@{ssh_host} './TPF/gitea_repo_controller.sh checkout {branch_name}'"
|
checkout_cmd = f"sshpass -p '{ssh_password}' ssh {ssh_options} {ssh_user}@{ssh_host} '. /home/asf/testarena_backend/TPF/gitea_repo_controller.sh checkout {branch_name}'"
|
||||||
print(f"[DEBUG] Executing checkout command: {checkout_cmd.replace(ssh_password, '***')}")
|
print(f"[DEBUG] Executing checkout command: {checkout_cmd.replace(ssh_password, '***')}")
|
||||||
|
|
||||||
checkout_result = subprocess.run(checkout_cmd, shell=True, capture_output=True, text=True, timeout=60)
|
checkout_result = subprocess.run(checkout_cmd, shell=True, capture_output=True, text=True, timeout=60)
|
||||||
@@ -179,7 +187,7 @@ def submit_step1():
|
|||||||
|
|
||||||
# If successful, run scenario scan to get available scenarios
|
# If successful, run scenario scan to get available scenarios
|
||||||
print(f"[DEBUG] Running scenario scan...")
|
print(f"[DEBUG] Running scenario scan...")
|
||||||
scan_cmd = f"sshpass -p '{ssh_password}' ssh {ssh_options} {ssh_user}@{ssh_host} 'python3 TPF/Sensor_hub_repo/Tools/TPF/scenario_scan.py'"
|
scan_cmd = f"sshpass -p '{ssh_password}' ssh {ssh_options} {ssh_user}@{ssh_host} 'python3 /home/asf/testarena_backend/TPF/Sensor_hub_repo/tools/TPF/scenario_scan.py'"
|
||||||
print(f"[DEBUG] Executing scan command: {scan_cmd.replace(ssh_password, '***')}")
|
print(f"[DEBUG] Executing scan command: {scan_cmd.replace(ssh_password, '***')}")
|
||||||
|
|
||||||
scan_result = subprocess.run(scan_cmd, shell=True, capture_output=True, text=True, timeout=120)
|
scan_result = subprocess.run(scan_cmd, shell=True, capture_output=True, text=True, timeout=120)
|
||||||
@@ -262,7 +270,10 @@ def submit_step2():
|
|||||||
try:
|
try:
|
||||||
scenario_map = json.loads(scenario_map_json) if scenario_map_json else {}
|
scenario_map = json.loads(scenario_map_json) if scenario_map_json else {}
|
||||||
selected_scenarios = json.loads(selected_scenarios_json) if selected_scenarios_json else []
|
selected_scenarios = json.loads(selected_scenarios_json) if selected_scenarios_json else []
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError as e:
|
||||||
|
print(f"[ERROR] JSON Decode Error in submit_step2: {e}")
|
||||||
|
print(f"[DEBUG] scenario_map_json: {scenario_map_json}")
|
||||||
|
print(f"[DEBUG] selected_scenarios_json: {selected_scenarios_json}")
|
||||||
flash('Invalid scenario data', 'error')
|
flash('Invalid scenario data', 'error')
|
||||||
return redirect(url_for('jobs.submit'))
|
return redirect(url_for('jobs.submit'))
|
||||||
|
|
||||||
@@ -270,7 +281,7 @@ def submit_step2():
|
|||||||
flash('Please select at least one scenario', 'error')
|
flash('Please select at least one scenario', 'error')
|
||||||
return redirect(url_for('jobs.submit'))
|
return redirect(url_for('jobs.submit'))
|
||||||
|
|
||||||
return render_template('jobs/submit_step3.html',
|
return render_template('jobs/submit_review.html',
|
||||||
branch_name=branch_name,
|
branch_name=branch_name,
|
||||||
scenarios=selected_scenarios,
|
scenarios=selected_scenarios,
|
||||||
scenario_map=scenario_map)
|
scenario_map=scenario_map)
|
||||||
@@ -301,6 +312,8 @@ def submit_step2_validated():
|
|||||||
|
|
||||||
except json.JSONDecodeError as e:
|
except json.JSONDecodeError as e:
|
||||||
print(f"[ERROR] Step2 - JSON decode error: {e}")
|
print(f"[ERROR] Step2 - JSON decode error: {e}")
|
||||||
|
print(f"[DEBUG] organized_data_json: {organized_data_json}")
|
||||||
|
print(f"[DEBUG] scenario_map_json: {scenario_map_json}")
|
||||||
flash('Invalid scenario data', 'error')
|
flash('Invalid scenario data', 'error')
|
||||||
return redirect(url_for('jobs.submit'))
|
return redirect(url_for('jobs.submit'))
|
||||||
|
|
||||||
@@ -318,73 +331,63 @@ def submit_step2_validated():
|
|||||||
print(f"[ERROR] Step2 - Traceback: {traceback.format_exc()}")
|
print(f"[ERROR] Step2 - Traceback: {traceback.format_exc()}")
|
||||||
flash(f'Error loading scenario selection: {str(e)}', 'error')
|
flash(f'Error loading scenario selection: {str(e)}', 'error')
|
||||||
return redirect(url_for('jobs.submit'))
|
return redirect(url_for('jobs.submit'))
|
||||||
@login_required
|
|
||||||
def submit_step2():
|
|
||||||
branch_name = request.form.get('branch_name')
|
|
||||||
selected_scenarios = request.form.getlist('scenarios')
|
|
||||||
|
|
||||||
if not selected_scenarios:
|
|
||||||
flash('Please select at least one scenario', 'error')
|
|
||||||
return redirect(url_for('jobs.submit'))
|
|
||||||
|
|
||||||
return render_template('jobs/submit_step3.html',
|
|
||||||
branch_name=branch_name,
|
|
||||||
scenarios=selected_scenarios)
|
|
||||||
|
|
||||||
@jobs_bp.route('/submit/step3', methods=['POST'])
|
|
||||||
@login_required
|
|
||||||
def submit_step3():
|
|
||||||
branch_name = request.form.get('branch_name')
|
|
||||||
scenarios_json = request.form.get('scenarios')
|
|
||||||
scenario_map_json = request.form.get('scenario_map')
|
|
||||||
environment = request.form.get('environment')
|
|
||||||
|
|
||||||
try:
|
|
||||||
scenarios = json.loads(scenarios_json) if scenarios_json else []
|
|
||||||
scenario_map = json.loads(scenario_map_json) if scenario_map_json else {}
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
flash('Invalid scenario data', 'error')
|
|
||||||
return redirect(url_for('jobs.submit'))
|
|
||||||
|
|
||||||
return render_template('jobs/submit_step4.html',
|
|
||||||
branch_name=branch_name,
|
|
||||||
scenarios=scenarios,
|
|
||||||
scenario_map=scenario_map,
|
|
||||||
environment=environment)
|
|
||||||
|
|
||||||
@jobs_bp.route('/submit/final', methods=['POST'])
|
@jobs_bp.route('/submit/final', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def submit_final():
|
def submit_final():
|
||||||
branch_name = request.form.get('branch_name')
|
branch_name = request.form.get('branch_name')
|
||||||
scenarios_json = request.form.get('scenarios')
|
scenarios_json = request.form.get('scenarios')
|
||||||
scenario_map_json = request.form.get('scenario_map')
|
scenario_map_json = request.form.get('scenario_map')
|
||||||
environment = request.form.get('environment')
|
environment = request.form.get('environment', 'staging')
|
||||||
test_mode = request.form.get('test_mode')
|
test_mode = request.form.get('test_mode', 'simulator')
|
||||||
keep_devbenches = request.form.get('keep_devbenches') == 'on'
|
|
||||||
reuse_results = request.form.get('reuse_results') == 'on'
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
scenarios = json.loads(scenarios_json) if scenarios_json else []
|
scenarios = json.loads(scenarios_json) if scenarios_json else []
|
||||||
scenario_map = json.loads(scenario_map_json) if scenario_map_json else {}
|
scenario_map = json.loads(scenario_map_json) if scenario_map_json else {}
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError as e:
|
||||||
|
print(f"[ERROR] JSON Decode Error in submit_final: {e}")
|
||||||
flash('Invalid scenario data', 'error')
|
flash('Invalid scenario data', 'error')
|
||||||
return redirect(url_for('jobs.submit'))
|
return redirect(url_for('jobs.submit'))
|
||||||
|
|
||||||
|
# Create Job record first to get the ID
|
||||||
job = Job(
|
job = Job(
|
||||||
user_id=current_user.id,
|
user_id=current_user.id,
|
||||||
branch_name=branch_name,
|
branch_name=branch_name,
|
||||||
scenarios=json.dumps(scenarios), # Store as JSON string
|
scenarios=json.dumps(scenarios),
|
||||||
environment=environment,
|
environment=environment,
|
||||||
test_mode=test_mode,
|
test_mode=test_mode,
|
||||||
keep_devbenches=keep_devbenches,
|
status='waiting'
|
||||||
reuse_results=reuse_results,
|
|
||||||
status='in_progress'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
db.session.add(job)
|
db.session.add(job)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
# TODO: Start test execution in background using scenario_map
|
# Use Job ID as Remote Queue ID
|
||||||
|
remote_queue_id = str(job.id)
|
||||||
|
remote_task_ids = {s: f"{job.id}_{i+1}" for i, s in enumerate(scenarios)}
|
||||||
|
|
||||||
|
# Update job with remote IDs
|
||||||
|
job.remote_queue_id = remote_queue_id
|
||||||
|
job.remote_task_ids = json.dumps(remote_task_ids)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# Prepare Remote Payload
|
||||||
|
# Format: {"source": "branch", "345": ["staging", {"task1": "path"}]}
|
||||||
|
payload = {
|
||||||
|
"source": branch_name,
|
||||||
|
remote_queue_id: [
|
||||||
|
environment,
|
||||||
|
{remote_task_ids[s]: scenario_map.get(s, s) for s in scenarios}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Trigger Remote Queue
|
||||||
|
try:
|
||||||
|
remote_url = "http://asf-server.duckdns.org:8080/api/queue"
|
||||||
|
response = requests.post(remote_url, json=payload, timeout=10)
|
||||||
|
response.raise_for_status()
|
||||||
|
print(f"[DEBUG] Remote queue triggered: {response.json()}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ERROR] Failed to trigger remote queue: {e}")
|
||||||
|
flash(f'Warning: Job saved but failed to trigger remote queue: {str(e)}', 'warning')
|
||||||
|
|
||||||
flash('Test job submitted successfully', 'success')
|
flash('Test job submitted successfully', 'success')
|
||||||
return redirect(url_for('dashboard.index'))
|
return redirect(url_for('dashboard.index'))
|
||||||
@@ -409,9 +412,10 @@ def view_job(job_id):
|
|||||||
'submitted_at': job.submitted_at.strftime('%Y-%m-%d %H:%M:%S'),
|
'submitted_at': job.submitted_at.strftime('%Y-%m-%d %H:%M:%S'),
|
||||||
'completed_at': job.completed_at.strftime('%Y-%m-%d %H:%M:%S') if job.completed_at else None,
|
'completed_at': job.completed_at.strftime('%Y-%m-%d %H:%M:%S') if job.completed_at else None,
|
||||||
'duration': job.duration,
|
'duration': job.duration,
|
||||||
'keep_devbenches': job.keep_devbenches,
|
'remote_queue_id': job.remote_queue_id,
|
||||||
'reuse_results': job.reuse_results,
|
'remote_task_ids': job.remote_task_ids,
|
||||||
'results_path': job.results_path
|
'remote_results': job.remote_results,
|
||||||
|
'queue_log': job.queue_log
|
||||||
})
|
})
|
||||||
|
|
||||||
@jobs_bp.route('/<int:job_id>/abort', methods=['POST'])
|
@jobs_bp.route('/<int:job_id>/abort', methods=['POST'])
|
||||||
@@ -422,10 +426,200 @@ def abort_job(job_id):
|
|||||||
if not current_user.is_admin and job.user_id != current_user.id:
|
if not current_user.is_admin and job.user_id != current_user.id:
|
||||||
return jsonify({'error': 'Access denied'}), 403
|
return jsonify({'error': 'Access denied'}), 403
|
||||||
|
|
||||||
if job.status == 'in_progress':
|
try:
|
||||||
|
remote_url = f"http://asf-server.duckdns.org:8080/api/abort/{job.remote_queue_id}"
|
||||||
|
response = requests.post(remote_url, timeout=10)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
job.status = 'aborted'
|
job.status = 'aborted'
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
# TODO: Kill the running process
|
return jsonify({'success': True, 'message': 'Job aborted successfully'})
|
||||||
return jsonify({'success': True})
|
except Exception as e:
|
||||||
|
print(f"[ERROR] Failed to abort remote job: {e}")
|
||||||
|
return jsonify({'error': f'Failed to abort remote job: {str(e)}'}), 500
|
||||||
|
|
||||||
return jsonify({'error': 'Job is not in progress'}), 400
|
@jobs_bp.route('/<int:job_id>/delete', methods=['POST', 'DELETE'])
|
||||||
|
@login_required
|
||||||
|
def delete_job(job_id):
|
||||||
|
job = Job.query.get_or_404(job_id)
|
||||||
|
|
||||||
|
if not current_user.is_admin and job.user_id != current_user.id:
|
||||||
|
return jsonify({'error': 'Access denied'}), 403
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 1. Try to delete from remote
|
||||||
|
remote_url = f"http://asf-server.duckdns.org:8080/api/delete/{job.remote_queue_id}"
|
||||||
|
try:
|
||||||
|
response = requests.delete(remote_url, timeout=10)
|
||||||
|
# We don't necessarily fail if remote delete fails (e.g. already gone)
|
||||||
|
print(f"[DEBUG] Remote delete response: {response.status_code}")
|
||||||
|
except Exception as re:
|
||||||
|
print(f"[WARNING] Remote delete failed: {re}")
|
||||||
|
|
||||||
|
# 2. Delete from local DB
|
||||||
|
db.session.delete(job)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return jsonify({'success': True, 'message': 'Job deleted successfully'})
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ERROR] Failed to delete job: {e}")
|
||||||
|
return jsonify({'error': f'Failed to delete job: {str(e)}'}), 500
|
||||||
|
|
||||||
|
def update_job_status_internal(job):
|
||||||
|
"""Internal function to poll remote status and update local job record."""
|
||||||
|
if job.status in ['passed', 'failed', 'aborted']:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 0. Check for Timeout (1 hour)
|
||||||
|
if job.submitted_at and (datetime.utcnow() - job.submitted_at) > timedelta(hours=1):
|
||||||
|
print(f"[WARNING] Job {job.id} timed out (1 hour limit reached). Aborting...")
|
||||||
|
|
||||||
|
# Try to abort remotely
|
||||||
|
try:
|
||||||
|
remote_url = f"http://asf-server.duckdns.org:8080/api/abort/{job.remote_queue_id}"
|
||||||
|
requests.post(remote_url, timeout=10)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ERROR] Failed to abort timed-out job remotely: {e}")
|
||||||
|
|
||||||
|
job.status = 'aborted'
|
||||||
|
job.queue_log = (job.queue_log or "") + "\n\n[SYSTEM] Job timed out after 1 hour."
|
||||||
|
from app import db
|
||||||
|
db.session.commit()
|
||||||
|
return
|
||||||
|
|
||||||
|
# 1. Check Queue Status
|
||||||
|
status_url = f"http://asf-server.duckdns.org:8080/api/status/{job.remote_queue_id}"
|
||||||
|
q_resp = requests.get(status_url, timeout=5)
|
||||||
|
if q_resp.status_code == 200:
|
||||||
|
q_data = q_resp.json()
|
||||||
|
remote_status = q_data.get('status', '').lower()
|
||||||
|
|
||||||
|
if remote_status == 'running':
|
||||||
|
job.status = 'in_progress'
|
||||||
|
elif remote_status == 'waiting':
|
||||||
|
job.status = 'waiting'
|
||||||
|
elif remote_status == 'aborted':
|
||||||
|
job.status = 'aborted'
|
||||||
|
|
||||||
|
# 2. Fetch Queue Logs if running or finished
|
||||||
|
if remote_status in ['running', 'finished', 'aborted']:
|
||||||
|
log_url = f"http://asf-server.duckdns.org:8080/results/{job.remote_queue_id}/queue_log.txt"
|
||||||
|
l_resp = requests.get(log_url, timeout=5)
|
||||||
|
if l_resp.status_code == 200:
|
||||||
|
job.queue_log = l_resp.text
|
||||||
|
|
||||||
|
# 3. Check Task Statuses and Results
|
||||||
|
task_ids = json.loads(job.remote_task_ids) if job.remote_task_ids else {}
|
||||||
|
results = json.loads(job.remote_results) if job.remote_results else {}
|
||||||
|
|
||||||
|
all_finished = True
|
||||||
|
any_failed = False
|
||||||
|
|
||||||
|
if not task_ids:
|
||||||
|
all_finished = False # Should not happen if job is valid
|
||||||
|
|
||||||
|
for scenario, task_id in task_ids.items():
|
||||||
|
# If we don't have result yet, check it
|
||||||
|
if scenario not in results:
|
||||||
|
# Check task status
|
||||||
|
t_status_url = f"http://asf-server.duckdns.org:8080/api/status/{task_id}"
|
||||||
|
t_resp = requests.get(t_status_url, timeout=2)
|
||||||
|
if t_resp.status_code == 200:
|
||||||
|
t_data = t_resp.json()
|
||||||
|
if t_data.get('status') == 'Finished':
|
||||||
|
# Fetch results
|
||||||
|
res_url = f"http://asf-server.duckdns.org:8080/results/{job.remote_queue_id}/{task_id}/final_summary.json"
|
||||||
|
r_resp = requests.get(res_url, timeout=2)
|
||||||
|
if r_resp.status_code == 200:
|
||||||
|
r_data = r_resp.json()
|
||||||
|
|
||||||
|
# Logic Fix 2: Check ALL results in final_summary.json
|
||||||
|
# If ANY result is FAIL or ERROR, the scenario is FAIL
|
||||||
|
scenario_status = "PASS"
|
||||||
|
report_link = "#"
|
||||||
|
|
||||||
|
# Iterate over all tests in the summary
|
||||||
|
for test_name, val in r_data.items():
|
||||||
|
# val is [STATUS, LINK]
|
||||||
|
status = val[0].upper()
|
||||||
|
link = val[1]
|
||||||
|
|
||||||
|
# Keep the link of the first test (or the specific scenario if found)
|
||||||
|
if report_link == "#":
|
||||||
|
# Construct standardized execution report link
|
||||||
|
report_link = f"http://asf-server.duckdns.org:8080/results/{job.remote_queue_id}/{task_id}/execution_report.html"
|
||||||
|
|
||||||
|
if status in ['FAIL', 'ERROR']:
|
||||||
|
scenario_status = "FAIL"
|
||||||
|
|
||||||
|
results[scenario] = [scenario_status, report_link]
|
||||||
|
|
||||||
|
if scenario_status == "FAIL":
|
||||||
|
any_failed = True
|
||||||
|
|
||||||
|
else:
|
||||||
|
all_finished = False
|
||||||
|
elif t_data.get('status') == 'Aborted':
|
||||||
|
results[scenario] = ['ABORTED', '#']
|
||||||
|
all_finished = True
|
||||||
|
elif t_data.get('status') == 'Error':
|
||||||
|
results[scenario] = ['ERROR', '#']
|
||||||
|
any_failed = True
|
||||||
|
all_finished = True
|
||||||
|
else:
|
||||||
|
all_finished = False
|
||||||
|
else:
|
||||||
|
all_finished = False
|
||||||
|
else:
|
||||||
|
if results[scenario][0] in ['FAIL', 'ERROR']:
|
||||||
|
any_failed = True
|
||||||
|
|
||||||
|
if all_finished and task_ids:
|
||||||
|
# Logic Fix 1: Only mark finished if ALL tasks are finished (handled by all_finished flag)
|
||||||
|
|
||||||
|
# If any task has FAIL or ERROR, the job status is 'failed'
|
||||||
|
job.status = 'failed' if any_failed else 'passed'
|
||||||
|
from app import db
|
||||||
|
job.completed_at = db.session.query(db.func.now()).scalar()
|
||||||
|
|
||||||
|
# Logic Fix 3: Run Cleanup Command
|
||||||
|
try:
|
||||||
|
print(f"[INFO] Job {job.id} finished. Running cleanup...")
|
||||||
|
# SSH options
|
||||||
|
ssh_port = os.environ.get('SSH_PORT', '49152') # Default from user request
|
||||||
|
ssh_host = "asf-server.duckdns.org"
|
||||||
|
ssh_user = "asf"
|
||||||
|
ssh_pass = "ASF" # Hardcoded as per request, ideally env var
|
||||||
|
|
||||||
|
cleanup_cmd = f"sshpass -p '{ssh_pass}' ssh -p {ssh_port} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null {ssh_user}@{ssh_host} \"rm -rf /home/asf/testarena_backend/TPF/Sensor_hub_repo\""
|
||||||
|
|
||||||
|
cleanup_res = subprocess.run(cleanup_cmd, shell=True, capture_output=True, text=True, timeout=30)
|
||||||
|
if cleanup_res.returncode != 0:
|
||||||
|
print(f"[WARNING] Cleanup command failed: {cleanup_res.stderr}")
|
||||||
|
else:
|
||||||
|
print(f"[INFO] Cleanup successful.")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ERROR] Failed to run cleanup command: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
job.remote_results = json.dumps(results)
|
||||||
|
from app import db
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ERROR] Status polling failed for job {job.id}: {e}")
|
||||||
|
|
||||||
|
@jobs_bp.route('/<int:job_id>/status')
|
||||||
|
@login_required
|
||||||
|
def get_job_status(job_id):
|
||||||
|
job = Job.query.get_or_404(job_id)
|
||||||
|
update_job_status_internal(job)
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'status': job.status,
|
||||||
|
'status_icon': job.get_status_icon(),
|
||||||
|
'remote_results': job.remote_results,
|
||||||
|
'queue_log': job.queue_log
|
||||||
|
})
|
||||||
|
|||||||
@@ -1,12 +1,26 @@
|
|||||||
:root {
|
:root {
|
||||||
--primary: #2563eb;
|
--primary: #3b82f6;
|
||||||
--primary-dark: #1e40af;
|
/* Blue 500 */
|
||||||
|
--primary-dark: #2563eb;
|
||||||
|
/* Blue 600 */
|
||||||
--success: #10b981;
|
--success: #10b981;
|
||||||
|
/* Emerald 500 */
|
||||||
--danger: #ef4444;
|
--danger: #ef4444;
|
||||||
|
/* Red 500 */
|
||||||
--warning: #f59e0b;
|
--warning: #f59e0b;
|
||||||
--dark: #1f2937;
|
/* Amber 500 */
|
||||||
--light: #f3f4f6;
|
--dark: #f8fafc;
|
||||||
--border: #e5e7eb;
|
/* 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 {
|
body {
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
font-family: 'Inter', 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
background: var(--bg-dark);
|
||||||
|
color: var(--dark);
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,12 +51,13 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.login-box {
|
.login-box {
|
||||||
background: white;
|
background: var(--bg-panel);
|
||||||
padding: 40px;
|
padding: 40px;
|
||||||
border-radius: 12px;
|
border-radius: 16px;
|
||||||
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
|
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
@@ -58,6 +74,7 @@ body {
|
|||||||
color: var(--dark);
|
color: var(--dark);
|
||||||
margin-top: 15px;
|
margin-top: 15px;
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group {
|
.form-group {
|
||||||
@@ -67,21 +84,25 @@ body {
|
|||||||
.form-group label {
|
.form-group label {
|
||||||
display: block;
|
display: block;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
color: var(--dark);
|
color: var(--text-muted);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-control {
|
.form-control {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
border: 2px solid var(--border);
|
background: var(--bg-input);
|
||||||
|
border: 1px solid var(--border);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
color: var(--dark);
|
||||||
|
transition: all 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-control:focus {
|
.form-control:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
border-color: var(--primary);
|
border-color: var(--primary);
|
||||||
|
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
@@ -127,25 +148,26 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.alert-success {
|
.alert-success {
|
||||||
background: #d1fae5;
|
background: rgba(16, 185, 129, 0.1);
|
||||||
color: #065f46;
|
color: #34d399;
|
||||||
border: 1px solid #10b981;
|
border: 1px solid rgba(16, 185, 129, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-error {
|
.alert-error {
|
||||||
background: #fee2e2;
|
background: rgba(239, 68, 68, 0.1);
|
||||||
color: #991b1b;
|
color: #f87171;
|
||||||
border: 1px solid #ef4444;
|
border: 1px solid rgba(239, 68, 68, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Navbar */
|
/* Navbar */
|
||||||
.navbar {
|
.navbar {
|
||||||
background: white;
|
background: var(--bg-panel);
|
||||||
padding: 15px 30px;
|
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;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-brand {
|
.navbar-brand {
|
||||||
@@ -161,6 +183,7 @@ body {
|
|||||||
.navbar-brand h2 {
|
.navbar-brand h2 {
|
||||||
color: var(--dark);
|
color: var(--dark);
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-menu {
|
.navbar-menu {
|
||||||
@@ -170,7 +193,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.navbar-menu a {
|
.navbar-menu a {
|
||||||
color: var(--dark);
|
color: var(--text-muted);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
transition: color 0.3s;
|
transition: color 0.3s;
|
||||||
@@ -190,11 +213,12 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.panel {
|
.panel {
|
||||||
background: white;
|
background: var(--bg-panel);
|
||||||
border-radius: 12px;
|
border-radius: 16px;
|
||||||
padding: 20px;
|
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;
|
overflow-y: auto;
|
||||||
|
border: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-header {
|
.panel-header {
|
||||||
@@ -203,12 +227,13 @@ body {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
padding-bottom: 15px;
|
padding-bottom: 15px;
|
||||||
border-bottom: 2px solid var(--border);
|
border-bottom: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-header h3 {
|
.panel-header h3 {
|
||||||
color: var(--dark);
|
color: var(--dark);
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Job List */
|
/* Job List */
|
||||||
@@ -220,10 +245,11 @@ body {
|
|||||||
|
|
||||||
.job-item {
|
.job-item {
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
border: 2px solid var(--border);
|
background: var(--bg-input);
|
||||||
border-radius: 8px;
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 12px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.3s;
|
transition: all 0.2s;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
@@ -231,12 +257,12 @@ body {
|
|||||||
|
|
||||||
.job-item:hover {
|
.job-item:hover {
|
||||||
border-color: var(--primary);
|
border-color: var(--primary);
|
||||||
background: var(--light);
|
transform: translateY(-2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.job-item.active {
|
.job-item.active {
|
||||||
border-color: var(--primary);
|
border-color: var(--primary);
|
||||||
background: #eff6ff;
|
background: rgba(59, 130, 246, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.job-status-icon {
|
.job-status-icon {
|
||||||
@@ -247,10 +273,11 @@ body {
|
|||||||
color: var(--dark);
|
color: var(--dark);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.job-info p {
|
.job-info p {
|
||||||
color: #6b7280;
|
color: var(--text-muted);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,11 +299,11 @@ body {
|
|||||||
|
|
||||||
.detail-label {
|
.detail-label {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--dark);
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-value {
|
.detail-value {
|
||||||
color: #4b5563;
|
color: var(--dark);
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-badge {
|
.status-badge {
|
||||||
@@ -288,32 +315,43 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.status-in_progress {
|
.status-in_progress {
|
||||||
background: #fef3c7;
|
background: rgba(245, 158, 11, 0.1);
|
||||||
color: #92400e;
|
color: #fbbf24;
|
||||||
|
border: 1px solid rgba(245, 158, 11, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-passed {
|
.status-passed {
|
||||||
background: #d1fae5;
|
background: rgba(16, 185, 129, 0.1);
|
||||||
color: #065f46;
|
color: #34d399;
|
||||||
|
border: 1px solid rgba(16, 185, 129, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-failed {
|
.status-failed {
|
||||||
background: #fee2e2;
|
background: rgba(239, 68, 68, 0.1);
|
||||||
color: #991b1b;
|
color: #f87171;
|
||||||
|
border: 1px solid rgba(239, 68, 68, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-aborted {
|
.status-aborted {
|
||||||
background: #f3f4f6;
|
background: rgba(148, 163, 184, 0.1);
|
||||||
color: #374151;
|
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 Dashboard */
|
||||||
.admin-container {
|
.admin-container {
|
||||||
background: white;
|
background: var(--bg-panel);
|
||||||
border-radius: 12px;
|
border-radius: 16px;
|
||||||
padding: 30px;
|
padding: 30px;
|
||||||
margin-top: 20px;
|
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 {
|
.admin-header {
|
||||||
@@ -333,7 +371,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.user-table th {
|
.user-table th {
|
||||||
background: var(--light);
|
background: var(--bg-input);
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@@ -344,10 +382,11 @@ body {
|
|||||||
.user-table td {
|
.user-table td {
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid var(--border);
|
||||||
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-table tr:hover {
|
.user-table tr:hover {
|
||||||
background: var(--light);
|
background: rgba(255, 255, 255, 0.02);
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge {
|
.badge {
|
||||||
@@ -359,13 +398,13 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.badge-admin {
|
.badge-admin {
|
||||||
background: #dbeafe;
|
background: rgba(59, 130, 246, 0.1);
|
||||||
color: #1e40af;
|
color: #60a5fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge-user {
|
.badge-user {
|
||||||
background: #f3f4f6;
|
background: rgba(148, 163, 184, 0.1);
|
||||||
color: #374151;
|
color: #cbd5e1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Modal */
|
/* Modal */
|
||||||
@@ -376,8 +415,9 @@ body {
|
|||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: rgba(0,0,0,0.5);
|
background: rgba(0, 0, 0, 0.7);
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal.active {
|
.modal.active {
|
||||||
@@ -387,11 +427,13 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.modal-content {
|
.modal-content {
|
||||||
background: white;
|
background: var(--bg-panel);
|
||||||
padding: 30px;
|
padding: 30px;
|
||||||
border-radius: 12px;
|
border-radius: 16px;
|
||||||
width: 90%;
|
width: 90%;
|
||||||
max-width: 500px;
|
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 {
|
.modal-header {
|
||||||
@@ -410,19 +452,24 @@ body {
|
|||||||
border: none;
|
border: none;
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: #6b7280;
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn:hover {
|
||||||
|
color: var(--dark);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Submit Form */
|
/* Submit Form */
|
||||||
.submit-container {
|
.submit-container {
|
||||||
background: white;
|
background: var(--bg-panel);
|
||||||
border-radius: 12px;
|
border-radius: 16px;
|
||||||
padding: 40px;
|
padding: 40px;
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: 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 {
|
.step-indicator {
|
||||||
@@ -455,29 +502,33 @@ body {
|
|||||||
.step.active .step-number {
|
.step.active .step-number {
|
||||||
background: var(--primary);
|
background: var(--primary);
|
||||||
color: white;
|
color: white;
|
||||||
|
border-color: var(--primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.step.completed .step-number {
|
.step.completed .step-number {
|
||||||
background: var(--success);
|
background: var(--success);
|
||||||
color: white;
|
color: white;
|
||||||
|
border-color: var(--success);
|
||||||
}
|
}
|
||||||
|
|
||||||
.step-number {
|
.step-number {
|
||||||
width: 30px;
|
width: 30px;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: var(--border);
|
background: var(--bg-input);
|
||||||
color: var(--dark);
|
border: 2px solid var(--border);
|
||||||
|
color: var(--text-muted);
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
|
transition: all 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.step-label {
|
.step-label {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #6b7280;
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.checkbox-group {
|
.checkbox-group {
|
||||||
@@ -491,11 +542,13 @@ body {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
color: var(--dark);
|
||||||
}
|
}
|
||||||
|
|
||||||
.checkbox-item input[type="checkbox"] {
|
.checkbox-item input[type="checkbox"] {
|
||||||
width: 18px;
|
width: 18px;
|
||||||
height: 18px;
|
height: 18px;
|
||||||
|
accent-color: var(--primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.radio-group {
|
.radio-group {
|
||||||
@@ -510,16 +563,23 @@ body {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
border: 2px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
background: var(--bg-input);
|
||||||
|
transition: all 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.radio-item:hover {
|
.radio-item:hover {
|
||||||
border-color: var(--primary);
|
border-color: var(--primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.radio-item input[type="radio"]:checked + label {
|
.radio-item label {
|
||||||
|
color: var(--dark);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-item input[type="radio"]:checked+label {
|
||||||
color: var(--primary);
|
color: var(--primary);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
@@ -534,20 +594,22 @@ body {
|
|||||||
.empty-state {
|
.empty-state {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 60px 20px;
|
padding: 60px 20px;
|
||||||
color: #6b7280;
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-state h3 {
|
.empty-state h3 {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
|
color: var(--dark);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Context Menu */
|
/* Context Menu */
|
||||||
.context-menu {
|
.context-menu {
|
||||||
display: none;
|
display: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
background: white;
|
background: var(--bg-panel);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 8px;
|
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;
|
z-index: 1000;
|
||||||
min-width: 150px;
|
min-width: 150px;
|
||||||
}
|
}
|
||||||
@@ -559,10 +621,11 @@ body {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
transition: background-color 0.2s;
|
transition: background-color 0.2s;
|
||||||
|
color: var(--dark);
|
||||||
}
|
}
|
||||||
|
|
||||||
.context-menu-item:hover {
|
.context-menu-item:hover {
|
||||||
background: var(--light);
|
background: var(--bg-input);
|
||||||
}
|
}
|
||||||
|
|
||||||
.context-menu-item:first-child {
|
.context-menu-item:first-child {
|
||||||
@@ -586,28 +649,28 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.branch-validation.success {
|
.branch-validation.success {
|
||||||
background: #d1fae5;
|
background: rgba(16, 185, 129, 0.1);
|
||||||
color: #065f46;
|
color: #34d399;
|
||||||
border: 1px solid #10b981;
|
border: 1px solid rgba(16, 185, 129, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.branch-validation.error {
|
.branch-validation.error {
|
||||||
background: #fee2e2;
|
background: rgba(239, 68, 68, 0.1);
|
||||||
color: #991b1b;
|
color: #f87171;
|
||||||
border: 1px solid #ef4444;
|
border: 1px solid rgba(239, 68, 68, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.branch-validation.loading {
|
.branch-validation.loading {
|
||||||
background: #fef3c7;
|
background: rgba(245, 158, 11, 0.1);
|
||||||
color: #92400e;
|
color: #fbbf24;
|
||||||
border: 1px solid #f59e0b;
|
border: 1px solid rgba(245, 158, 11, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading-spinner {
|
.loading-spinner {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
border: 2px solid #f3f3f3;
|
border: 2px solid rgba(255, 255, 255, 0.1);
|
||||||
border-top: 2px solid var(--primary);
|
border-top: 2px solid var(--primary);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
animation: spin 1s linear infinite;
|
animation: spin 1s linear infinite;
|
||||||
@@ -615,31 +678,39 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
0% { transform: rotate(0deg); }
|
0% {
|
||||||
100% { transform: rotate(360deg); }
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn:disabled {
|
.btn:disabled {
|
||||||
background: #9ca3af;
|
background: var(--bg-input);
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}/
|
color: var(--text-muted);
|
||||||
* Scenario Tree Styles */
|
}
|
||||||
|
|
||||||
|
/* Scenario Tree Styles */
|
||||||
.scenario-controls {
|
.scenario-controls {
|
||||||
margin: 20px 0;
|
margin: 20px 0;
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
background: var(--light);
|
background: var(--bg-input);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.scenario-tree {
|
.scenario-tree {
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
background: white;
|
background: var(--bg-panel);
|
||||||
margin: 20px 0;
|
margin: 20px 0;
|
||||||
max-height: 500px;
|
max-height: 500px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
@@ -666,6 +737,7 @@ body {
|
|||||||
padding: 8px 0;
|
padding: 8px 0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
color: var(--dark);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree-toggle {
|
.tree-toggle {
|
||||||
@@ -700,23 +772,26 @@ body {
|
|||||||
.stack-label {
|
.stack-label {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #4b5563;
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.scenario-label {
|
.scenario-label {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
color: #6b7280;
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree-children {
|
.tree-children {
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.layer-checkbox, .stack-checkbox, .scenario-checkbox {
|
.layer-checkbox,
|
||||||
|
.stack-checkbox,
|
||||||
|
.scenario-checkbox {
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
accent-color: var(--primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.layer-node:hover {
|
.layer-node:hover {
|
||||||
@@ -754,3 +829,68 @@ input[type="checkbox"]:indeterminate::before {
|
|||||||
color: var(--primary);
|
color: var(--primary);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Scenario Table & Log Styles */
|
||||||
|
#scenarioTable {
|
||||||
|
margin-top: 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
#scenarioTable th {
|
||||||
|
padding: 10px;
|
||||||
|
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 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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);
|
||||||
|
}
|
||||||
@@ -31,12 +31,15 @@
|
|||||||
<span class="badge badge-user">User</span>
|
<span class="badge badge-user">User</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ user.created_at.strftime('%Y-%m-%d %H:%M') }}</td>
|
<td>{{ user.created_at.strftime('%Y-%m-%d %H:%M') if user.created_at else 'N/A' }}</td>
|
||||||
<td>
|
<td>
|
||||||
<button class="btn btn-sm btn-primary" onclick="openResetPasswordModal({{ user.id }}, '{{ user.username }}')">Reset Password</button>
|
<button class="btn btn-sm btn-primary"
|
||||||
|
onclick="openResetPasswordModal({{ user.id }}, '{{ user.username }}')">Reset Password</button>
|
||||||
{% if user.id != current_user.id %}
|
{% if user.id != current_user.id %}
|
||||||
<form method="POST" action="{{ url_for('admin.delete_user', user_id=user.id) }}" style="display: inline;">
|
<form method="POST" action="{{ url_for('admin.delete_user', user_id=user.id) }}"
|
||||||
<button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('Delete user {{ user.username }}?')">Delete</button>
|
style="display: inline;">
|
||||||
|
<button type="submit" class="btn btn-sm btn-danger"
|
||||||
|
onclick="return confirm('Delete user {{ user.username }}?')">Delete</button>
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
@@ -92,25 +95,25 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function openModal(modalId) {
|
function openModal(modalId) {
|
||||||
document.getElementById(modalId).classList.add('active');
|
document.getElementById(modalId).classList.add('active');
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeModal(modalId) {
|
function closeModal(modalId) {
|
||||||
document.getElementById(modalId).classList.remove('active');
|
document.getElementById(modalId).classList.remove('active');
|
||||||
}
|
}
|
||||||
|
|
||||||
function openResetPasswordModal(userId, username) {
|
function openResetPasswordModal(userId, username) {
|
||||||
document.getElementById('resetUsername').textContent = username;
|
document.getElementById('resetUsername').textContent = username;
|
||||||
document.getElementById('resetPasswordForm').action = `/admin/users/${userId}/reset-password`;
|
document.getElementById('resetPasswordForm').action = `/admin/users/${userId}/reset-password`;
|
||||||
openModal('resetPasswordModal');
|
openModal('resetPasswordModal');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close modal when clicking outside
|
// Close modal when clicking outside
|
||||||
window.onclick = function(event) {
|
window.onclick = function (event) {
|
||||||
if (event.target.classList.contains('modal')) {
|
if (event.target.classList.contains('modal')) {
|
||||||
event.target.classList.remove('active');
|
event.target.classList.remove('active');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -1,11 +1,14 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>{% block title %}ASF TestArena{% endblock %}</title>
|
<title>{% block title %}ASF TestArena{% endblock %}</title>
|
||||||
|
<link rel="icon" type="image/png" href="{{ url_for('static', filename='uploads/icon.png') }}">
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
{% if current_user.is_authenticated %}
|
{% if current_user.is_authenticated %}
|
||||||
<nav class="navbar">
|
<nav class="navbar">
|
||||||
@@ -27,11 +30,11 @@
|
|||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
{% if messages %}
|
{% if messages %}
|
||||||
{% for category, message in messages %}
|
{% for category, message in messages %}
|
||||||
<div class="alert alert-{{ category }}">{{ message }}</div>
|
<div class="alert alert-{{ category }}">{{ message }}</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|
||||||
{% block content %}{% endblock %}
|
{% block content %}{% endblock %}
|
||||||
@@ -39,4 +42,5 @@
|
|||||||
|
|
||||||
{% block scripts %}{% endblock %}
|
{% block scripts %}{% endblock %}
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -4,35 +4,68 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="dashboard-container">
|
<div class="dashboard-container">
|
||||||
<div class="panel">
|
<div class="panel sidebar-panel">
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
<h3>Test Jobs</h3>
|
<h3>Test Jobs</h3>
|
||||||
<a href="{{ url_for('jobs.submit') }}" class="btn btn-primary btn-sm">+ New Job</a>
|
<div style="display: flex; gap: 5px;">
|
||||||
|
<button class="btn btn-secondary btn-sm" onclick="toggleSearch()" title="Search Jobs">🔍</button>
|
||||||
|
<a href="{{ url_for('jobs.submit') }}" class="btn btn-primary btn-sm">+ New</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="search-panel" class="search-box"
|
||||||
|
style="display: {{ 'block' if username_query or job_id_query else 'none' }}; padding: 15px; border-bottom: 1px solid var(--border); background: var(--light);">
|
||||||
|
<form action="{{ url_for('dashboard.index') }}" method="GET"
|
||||||
|
style="display: flex; flex-direction: column; gap: 10px;">
|
||||||
|
<div class="form-group">
|
||||||
|
<label
|
||||||
|
style="font-size: 11px; font-weight: 600; color: var(--gray); margin-bottom: 4px; display: block;">JOB
|
||||||
|
ID (GLOBAL)</label>
|
||||||
|
<input type="text" name="job_id" placeholder="e.g. 345" value="{{ job_id_query or '' }}"
|
||||||
|
class="form-control">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label
|
||||||
|
style="font-size: 11px; font-weight: 600; color: var(--gray); margin-bottom: 4px; display: block;">USERNAME
|
||||||
|
(GLOBAL)</label>
|
||||||
|
<input type="text" name="username" placeholder="e.g. admin" value="{{ username_query or '' }}"
|
||||||
|
class="form-control">
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; gap: 5px; margin-top: 5px;">
|
||||||
|
<button type="submit" class="btn btn-primary btn-sm" style="flex: 1;">Search</button>
|
||||||
|
<a href="{{ url_for('dashboard.index') }}" class="btn btn-secondary btn-sm">Clear</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="job-list">
|
<div class="job-list">
|
||||||
{% if jobs %}
|
{% if jobs %}
|
||||||
{% for job in jobs %}
|
{% for job in jobs %}
|
||||||
<div class="job-item" data-job-id="{{ job.id }}" onclick="loadJobDetails({{ job.id }})" oncontextmenu="showContextMenu(event, {{ job.id }}, '{{ job.status }}')">
|
<div class="job-item" data-job-id="{{ job.id }}" onclick="loadJobDetails({{ job.id }})"
|
||||||
<div class="job-status-icon">{{ job.get_status_icon() }}</div>
|
oncontextmenu="showContextMenu(event, {{ job.id }}, '{{ job.status }}')">
|
||||||
<div class="job-info">
|
<div class="job-status-icon">{{ job.get_status_icon() }}</div>
|
||||||
<h4>Job #{{ job.id }} - {{ job.branch_name }}</h4>
|
<div class="job-info">
|
||||||
<p>{{ job.submitted_at.strftime('%Y-%m-%d %H:%M') }} by {{ job.submitter.username }}</p>
|
<h4>Job #{{ job.id }} - {{ job.branch_name }}</h4>
|
||||||
</div>
|
<p>{{ job.submitted_at.strftime('%Y-%m-%d %H:%M') }} by {{ job.submitter.username }}</p>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
</div>
|
||||||
|
{% endfor %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<h3>No jobs yet</h3>
|
<h3>No jobs found</h3>
|
||||||
<p>Submit your first test job to get started</p>
|
<p>Try a different search or submit a new job</p>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="panel">
|
<div class="panel details-panel">
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
<h3>Job Details</h3>
|
<h3>Job Details</h3>
|
||||||
|
<div id="job-actions" style="display: none; gap: 5px;">
|
||||||
|
<button class="btn btn-danger btn-sm" id="btn-abort" onclick="abortJob(activeJobId)">Abort</button>
|
||||||
|
<button class="btn btn-danger btn-sm" id="btn-delete" onclick="deleteJob(activeJobId)">Delete</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="job-details-container">
|
<div id="job-details-container">
|
||||||
@@ -46,64 +79,128 @@
|
|||||||
|
|
||||||
<!-- Context Menu -->
|
<!-- Context Menu -->
|
||||||
<div id="contextMenu" class="context-menu">
|
<div id="contextMenu" class="context-menu">
|
||||||
<div class="context-menu-item" onclick="abortJobFromContext()">
|
<div class="context-menu-item" id="ctx-abort" onclick="abortJobFromContext()">
|
||||||
<span class="context-menu-icon">⚫</span>
|
<span class="context-menu-icon">⚫</span>
|
||||||
Abort Job
|
Abort Job
|
||||||
</div>
|
</div>
|
||||||
|
<div class="context-menu-item" onclick="deleteJobFromContext()">
|
||||||
|
<span class="context-menu-icon">🗑️</span>
|
||||||
|
Delete Job
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.tabs {
|
||||||
|
display: flex;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
padding: 10px 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--gray);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab.active {
|
||||||
|
border-bottom-color: var(--primary);
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content.active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
let contextJobId = null;
|
let contextJobId = null;
|
||||||
|
let activeJobId = null;
|
||||||
|
let pollingInterval = null;
|
||||||
|
let currentActiveTab = 'scenarios';
|
||||||
|
|
||||||
function showContextMenu(event, jobId, status) {
|
function toggleSearch() {
|
||||||
event.preventDefault();
|
const panel = document.getElementById('search-panel');
|
||||||
|
panel.style.display = panel.style.display === 'none' ? 'block' : 'none';
|
||||||
// Only show context menu for in_progress jobs
|
|
||||||
if (status !== 'in_progress') {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
contextJobId = jobId;
|
function showContextMenu(event, jobId, status) {
|
||||||
const contextMenu = document.getElementById('contextMenu');
|
event.preventDefault();
|
||||||
|
contextJobId = jobId;
|
||||||
|
const contextMenu = document.getElementById('contextMenu');
|
||||||
|
|
||||||
contextMenu.style.display = 'block';
|
const abortItem = document.getElementById('ctx-abort');
|
||||||
contextMenu.style.left = event.pageX + 'px';
|
if (status === 'in_progress' || status === 'waiting') {
|
||||||
contextMenu.style.top = event.pageY + 'px';
|
abortItem.style.display = 'flex';
|
||||||
|
} else {
|
||||||
|
abortItem.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
// Hide context menu when clicking elsewhere
|
contextMenu.style.display = 'block';
|
||||||
document.addEventListener('click', hideContextMenu);
|
contextMenu.style.left = event.pageX + 'px';
|
||||||
}
|
contextMenu.style.top = event.pageY + 'px';
|
||||||
|
|
||||||
function hideContextMenu() {
|
document.addEventListener('click', hideContextMenu);
|
||||||
document.getElementById('contextMenu').style.display = 'none';
|
|
||||||
document.removeEventListener('click', hideContextMenu);
|
|
||||||
contextJobId = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function abortJobFromContext() {
|
|
||||||
if (contextJobId) {
|
|
||||||
abortJob(contextJobId);
|
|
||||||
}
|
}
|
||||||
hideContextMenu();
|
|
||||||
}
|
|
||||||
function loadJobDetails(jobId) {
|
|
||||||
// Mark job as active
|
|
||||||
document.querySelectorAll('.job-item').forEach(item => {
|
|
||||||
item.classList.remove('active');
|
|
||||||
});
|
|
||||||
document.querySelector(`[data-job-id="${jobId}"]`).classList.add('active');
|
|
||||||
|
|
||||||
// Fetch job details
|
function hideContextMenu() {
|
||||||
fetch(`/jobs/${jobId}`)
|
document.getElementById('contextMenu').style.display = 'none';
|
||||||
.then(response => response.json())
|
document.removeEventListener('click', hideContextMenu);
|
||||||
.then(job => {
|
contextJobId = null;
|
||||||
const container = document.getElementById('job-details-container');
|
}
|
||||||
const scenarios = JSON.parse(job.scenarios || '[]');
|
|
||||||
|
|
||||||
container.innerHTML = `
|
function abortJobFromContext() {
|
||||||
|
if (contextJobId) abortJob(contextJobId);
|
||||||
|
hideContextMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteJobFromContext() {
|
||||||
|
if (contextJobId) deleteJob(contextJobId);
|
||||||
|
hideContextMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadJobDetails(jobId) {
|
||||||
|
activeJobId = jobId;
|
||||||
|
if (pollingInterval) clearInterval(pollingInterval);
|
||||||
|
|
||||||
|
document.querySelectorAll('.job-item').forEach(item => item.classList.remove('active'));
|
||||||
|
const jobItem = document.querySelector(`[data-job-id="${jobId}"]`);
|
||||||
|
if (jobItem) jobItem.classList.add('active');
|
||||||
|
|
||||||
|
fetchJobDetails(jobId);
|
||||||
|
pollingInterval = setInterval(() => fetchJobDetails(jobId, true), 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchJobDetails(jobId, isPolling = false) {
|
||||||
|
fetch(`/jobs/${jobId}`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(job => {
|
||||||
|
const container = document.getElementById('job-details-container');
|
||||||
|
const actions = document.getElementById('job-actions');
|
||||||
|
actions.style.display = 'flex';
|
||||||
|
|
||||||
|
const btnAbort = document.getElementById('btn-abort');
|
||||||
|
if (['passed', 'failed', 'aborted', 'error'].includes(job.status)) {
|
||||||
|
btnAbort.style.display = 'none';
|
||||||
|
if (isPolling) clearInterval(pollingInterval);
|
||||||
|
} else {
|
||||||
|
btnAbort.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
const scenarios = JSON.parse(job.scenarios || '[]');
|
||||||
|
const taskIds = JSON.parse(job.remote_task_ids || '{}');
|
||||||
|
const results = JSON.parse(job.remote_results || '{}');
|
||||||
|
|
||||||
|
let html = `
|
||||||
<div class="detail-row">
|
<div class="detail-row">
|
||||||
<div class="detail-label">Job ID:</div>
|
<div class="detail-label">Job ID:</div>
|
||||||
<div class="detail-value">#${job.id}</div>
|
<div class="detail-value">#${job.id} (Remote: ${job.remote_queue_id || 'N/A'})</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="detail-row">
|
<div class="detail-row">
|
||||||
<div class="detail-label">Submitter:</div>
|
<div class="detail-label">Submitter:</div>
|
||||||
@@ -119,58 +216,131 @@ function loadJobDetails(jobId) {
|
|||||||
<span class="status-badge status-${job.status}">${job.status.replace('_', ' ').toUpperCase()}</span>
|
<span class="status-badge status-${job.status}">${job.status.replace('_', ' ').toUpperCase()}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="detail-row">
|
|
||||||
<div class="detail-label">Environment:</div>
|
|
||||||
<div class="detail-value">${job.environment}</div>
|
|
||||||
</div>
|
|
||||||
<div class="detail-row">
|
|
||||||
<div class="detail-label">Test Mode:</div>
|
|
||||||
<div class="detail-value">${job.test_mode}</div>
|
|
||||||
</div>
|
|
||||||
<div class="detail-row">
|
|
||||||
<div class="detail-label">Submitted:</div>
|
|
||||||
<div class="detail-value">${job.submitted_at}</div>
|
|
||||||
</div>
|
|
||||||
${job.completed_at ? `
|
|
||||||
<div class="detail-row">
|
|
||||||
<div class="detail-label">Completed:</div>
|
|
||||||
<div class="detail-value">${job.completed_at}</div>
|
|
||||||
</div>
|
|
||||||
<div class="detail-row">
|
|
||||||
<div class="detail-label">Duration:</div>
|
|
||||||
<div class="detail-value">${job.duration ? Math.floor(job.duration / 60) + 'm ' + (job.duration % 60) + 's' : 'N/A'}</div>
|
|
||||||
</div>
|
|
||||||
` : ''}
|
|
||||||
<div class="detail-row">
|
|
||||||
<div class="detail-label">Scenarios:</div>
|
|
||||||
<div class="detail-value">${Array.isArray(scenarios) ? scenarios.join(', ') : scenarios}</div>
|
|
||||||
</div>
|
|
||||||
${job.status === 'in_progress' ? `
|
|
||||||
<div style="margin-top: 20px;">
|
|
||||||
<button class="btn btn-danger" onclick="abortJob(${job.id})">Abort Job</button>
|
|
||||||
</div>
|
|
||||||
` : ''}
|
|
||||||
${job.results_path ? `
|
|
||||||
<div style="margin-top: 20px;">
|
|
||||||
<a href="${job.results_path}" class="btn btn-primary" target="_blank">View Results</a>
|
|
||||||
</div>
|
|
||||||
` : ''}
|
|
||||||
`;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function abortJob(jobId) {
|
<div class="tabs">
|
||||||
if (confirm('Are you sure you want to abort this job?')) {
|
<div class="tab ${currentActiveTab === 'scenarios' ? 'active' : ''}" onclick="switchTab('scenarios')">Scenario Summary</div>
|
||||||
fetch(`/jobs/${jobId}/abort`, { method: 'POST' })
|
<div class="tab ${currentActiveTab === 'logs' ? 'active' : ''}" onclick="switchTab('logs')">Queue Logging</div>
|
||||||
.then(response => response.json())
|
</div>
|
||||||
.then(data => {
|
|
||||||
if (data.success) {
|
<div id="scenarios-tab" class="tab-content ${currentActiveTab === 'scenarios' ? 'active' : ''}">
|
||||||
location.reload();
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
|
||||||
} else {
|
<h4 style="color: var(--dark);">Scenarios</h4>
|
||||||
alert(data.error);
|
<input type="text" id="scenarioSearch" placeholder="Filter scenarios..." onkeyup="filterScenarios()" class="form-control" style="width: 200px; padding: 6px 12px;">
|
||||||
|
</div>
|
||||||
|
<table class="user-table" id="scenarioTable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Scenario Name</th>
|
||||||
|
<th>Task ID</th>
|
||||||
|
<th>Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
`;
|
||||||
|
|
||||||
|
scenarios.forEach(s => {
|
||||||
|
const taskId = taskIds[s] || 'N/A';
|
||||||
|
const result = results[s];
|
||||||
|
let statusHtml = '⌛ Waiting';
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
const icon = result[0] === 'PASS' ? '✅' : (result[0] === 'FAIL' ? '❌' : (result[0] === 'ERROR' ? '⚠️' : '⚫'));
|
||||||
|
const color = result[0] === 'PASS' ? 'var(--success)' : (result[0] === 'FAIL' ? 'var(--danger)' : (result[0] === 'ERROR' ? 'var(--warning)' : 'var(--gray)'));
|
||||||
|
statusHtml = `<a href="${result[1]}" target="_blank" style="text-decoration: none; color: ${color}; font-weight: 600;">${icon} ${result[0]}</a>`;
|
||||||
|
} else if (job.status === 'in_progress') {
|
||||||
|
statusHtml = '🔄 Running';
|
||||||
|
}
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<tr>
|
||||||
|
<td>${s}</td>
|
||||||
|
<td><small>${taskId}</small></td>
|
||||||
|
<td>${statusHtml}</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
html += `
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="logs-tab" class="tab-content ${currentActiveTab === 'logs' ? 'active' : ''}">
|
||||||
|
<div id="queue-log" style="background: #1e1e1e; color: #d4d4d4; padding: 15px; border-radius: 8px; font-family: 'Consolas', monospace; font-size: 12px; max-height: 500px; overflow-y: auto; white-space: pre-wrap;">${job.queue_log || 'Waiting for logs...'}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
container.innerHTML = html;
|
||||||
|
|
||||||
|
if (currentActiveTab === 'logs') {
|
||||||
|
const logElement = document.getElementById('queue-log');
|
||||||
|
if (logElement) logElement.scrollTop = logElement.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!['passed', 'failed', 'aborted', 'error'].includes(job.status)) {
|
||||||
|
fetch(`/jobs/${jobId}/status`)
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
const iconEl = document.querySelector(`[data-job-id="${jobId}"] .job-status-icon`);
|
||||||
|
if (iconEl) iconEl.textContent = data.status_icon;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
function switchTab(tabName) {
|
||||||
|
currentActiveTab = tabName;
|
||||||
|
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
||||||
|
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
|
||||||
|
|
||||||
|
if (tabName === 'scenarios') {
|
||||||
|
document.querySelector('.tab:nth-child(1)').classList.add('active');
|
||||||
|
document.getElementById('scenarios-tab').classList.add('active');
|
||||||
|
} else {
|
||||||
|
document.querySelector('.tab:nth-child(2)').classList.add('active');
|
||||||
|
document.getElementById('logs-tab').classList.add('active');
|
||||||
|
const logElement = document.getElementById('queue-log');
|
||||||
|
if (logElement) logElement.scrollTop = logElement.scrollHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterScenarios() {
|
||||||
|
const filter = document.getElementById('scenarioSearch').value.toUpperCase();
|
||||||
|
const tr = document.getElementById('scenarioTable').getElementsByTagName('tr');
|
||||||
|
for (let i = 1; i < tr.length; i++) {
|
||||||
|
const td = tr[i].getElementsByTagName('td')[0];
|
||||||
|
if (td) {
|
||||||
|
tr[i].style.display = (td.textContent || td.innerText).toUpperCase().indexOf(filter) > -1 ? "" : "none";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function abortJob(jobId) {
|
||||||
|
if (confirm('Are you sure you want to abort this job?')) {
|
||||||
|
fetch(`/jobs/${jobId}/abort`, { method: 'POST' })
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert(data.error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteJob(jobId) {
|
||||||
|
if (confirm('Are you sure you want to delete this job? This will also remove it from the remote server.')) {
|
||||||
|
fetch(`/jobs/${jobId}/delete`, { method: 'POST' })
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert(data.error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -17,14 +17,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="step">
|
<div class="step">
|
||||||
<div class="step-number">3</div>
|
<div class="step-number">3</div>
|
||||||
<div class="step-label">Environment</div>
|
|
||||||
</div>
|
|
||||||
<div class="step">
|
|
||||||
<div class="step-number">4</div>
|
|
||||||
<div class="step-label">Test Mode</div>
|
|
||||||
</div>
|
|
||||||
<div class="step">
|
|
||||||
<div class="step-number">5</div>
|
|
||||||
<div class="step-label">Review</div>
|
<div class="step-label">Review</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -33,7 +25,7 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="branch_name">Git Branch Name</label>
|
<label for="branch_name">Git Branch Name</label>
|
||||||
<input type="text" id="branch_name" name="branch_name" class="form-control"
|
<input type="text" id="branch_name" name="branch_name" class="form-control"
|
||||||
placeholder="e.g., feature/new-feature" required>
|
placeholder="e.g., feature/new-feature" required>
|
||||||
<small style="color: #6b7280; margin-top: 5px; display: block;">
|
<small style="color: #6b7280; margin-top: 5px; display: block;">
|
||||||
Enter the branch name to analyze available test scenarios
|
Enter the branch name to analyze available test scenarios
|
||||||
</small>
|
</small>
|
||||||
@@ -47,151 +39,152 @@
|
|||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<a href="{{ url_for('dashboard.index') }}" class="btn" style="background: #6b7280; color: white;">Cancel</a>
|
<a href="{{ url_for('dashboard.index') }}" class="btn" style="background: #6b7280; color: white;">Cancel</a>
|
||||||
<button type="submit" id="validateBtn" class="btn btn-primary">Validate Branch</button>
|
<button type="submit" id="validateBtn" class="btn btn-primary">Validate Branch</button>
|
||||||
<button type="button" id="nextBtn" class="btn btn-primary" style="display: none;" onclick="proceedToStep2()">Next</button>
|
<button type="button" id="nextBtn" class="btn btn-primary" style="display: none;"
|
||||||
|
onclick="proceedToStep2()">Next</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
let validatedBranch = null;
|
let validatedBranch = null;
|
||||||
let organizedData = {};
|
let organizedData = {};
|
||||||
let scenarioMap = {};
|
let scenarioMap = {};
|
||||||
|
|
||||||
document.getElementById('branchForm').addEventListener('submit', function(e) {
|
document.getElementById('branchForm').addEventListener('submit', function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
validateBranch();
|
validateBranch();
|
||||||
});
|
});
|
||||||
|
|
||||||
function validateBranch() {
|
function validateBranch() {
|
||||||
const branchName = document.getElementById('branch_name').value.trim();
|
const branchName = document.getElementById('branch_name').value.trim();
|
||||||
|
|
||||||
if (!branchName) {
|
if (!branchName) {
|
||||||
alert('Please enter a branch name');
|
alert('Please enter a branch name');
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show loading state
|
||||||
|
const validation = document.getElementById('branchValidation');
|
||||||
|
const message = document.getElementById('validationMessage');
|
||||||
|
const validateBtn = document.getElementById('validateBtn');
|
||||||
|
const nextBtn = document.getElementById('nextBtn');
|
||||||
|
|
||||||
|
validation.className = 'branch-validation loading';
|
||||||
|
validation.style.display = 'block';
|
||||||
|
message.textContent = 'Validating branch...';
|
||||||
|
validateBtn.disabled = true;
|
||||||
|
nextBtn.style.display = 'none';
|
||||||
|
|
||||||
|
// Make AJAX request
|
||||||
|
fetch('/jobs/submit/step1', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
body: `branch_name=${encodeURIComponent(branchName)}`
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
console.log('Branch validation response:', data); // Debug log
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
// Success
|
||||||
|
validation.className = 'branch-validation success';
|
||||||
|
message.textContent = data.message;
|
||||||
|
validateBtn.style.display = 'none';
|
||||||
|
nextBtn.style.display = 'inline-block';
|
||||||
|
|
||||||
|
validatedBranch = branchName;
|
||||||
|
organizedData = data.organized_data || {};
|
||||||
|
scenarioMap = data.scenario_map || {};
|
||||||
|
|
||||||
|
// Log debug info
|
||||||
|
if (data.debug) {
|
||||||
|
console.log('Debug info:', data.debug);
|
||||||
|
}
|
||||||
|
if (data.output) {
|
||||||
|
console.log('Command output:', data.output);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Error
|
||||||
|
validation.className = 'branch-validation error';
|
||||||
|
message.textContent = data.error;
|
||||||
|
validateBtn.disabled = false;
|
||||||
|
nextBtn.style.display = 'none';
|
||||||
|
|
||||||
|
validatedBranch = null;
|
||||||
|
organizedData = {};
|
||||||
|
scenarioMap = {};
|
||||||
|
|
||||||
|
// Log debug info for troubleshooting
|
||||||
|
console.error('Branch validation failed:', data.error);
|
||||||
|
if (data.debug) {
|
||||||
|
console.error('Debug info:', data.debug);
|
||||||
|
}
|
||||||
|
if (data.output) {
|
||||||
|
console.error('Command output:', data.output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
// Network error
|
||||||
|
validation.className = 'branch-validation error';
|
||||||
|
message.textContent = 'Network error. Please try again.';
|
||||||
|
validateBtn.disabled = false;
|
||||||
|
nextBtn.style.display = 'none';
|
||||||
|
|
||||||
|
validatedBranch = null;
|
||||||
|
availableScenarios = [];
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show loading state
|
function proceedToStep2() {
|
||||||
const validation = document.getElementById('branchValidation');
|
if (!validatedBranch || Object.keys(organizedData).length === 0) {
|
||||||
const message = document.getElementById('validationMessage');
|
alert('Please validate the branch first');
|
||||||
const validateBtn = document.getElementById('validateBtn');
|
return;
|
||||||
const nextBtn = document.getElementById('nextBtn');
|
|
||||||
|
|
||||||
validation.className = 'branch-validation loading';
|
|
||||||
validation.style.display = 'block';
|
|
||||||
message.textContent = 'Validating branch...';
|
|
||||||
validateBtn.disabled = true;
|
|
||||||
nextBtn.style.display = 'none';
|
|
||||||
|
|
||||||
// Make AJAX request
|
|
||||||
fetch('/jobs/submit/step1', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
|
||||||
},
|
|
||||||
body: `branch_name=${encodeURIComponent(branchName)}`
|
|
||||||
})
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
console.log('Branch validation response:', data); // Debug log
|
|
||||||
|
|
||||||
if (data.success) {
|
|
||||||
// Success
|
|
||||||
validation.className = 'branch-validation success';
|
|
||||||
message.textContent = data.message;
|
|
||||||
validateBtn.style.display = 'none';
|
|
||||||
nextBtn.style.display = 'inline-block';
|
|
||||||
|
|
||||||
validatedBranch = branchName;
|
|
||||||
organizedData = data.organized_data || {};
|
|
||||||
scenarioMap = data.scenario_map || {};
|
|
||||||
|
|
||||||
// Log debug info
|
|
||||||
if (data.debug) {
|
|
||||||
console.log('Debug info:', data.debug);
|
|
||||||
}
|
|
||||||
if (data.output) {
|
|
||||||
console.log('Command output:', data.output);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Error
|
|
||||||
validation.className = 'branch-validation error';
|
|
||||||
message.textContent = data.error;
|
|
||||||
validateBtn.disabled = false;
|
|
||||||
nextBtn.style.display = 'none';
|
|
||||||
|
|
||||||
validatedBranch = null;
|
|
||||||
organizedData = {};
|
|
||||||
scenarioMap = {};
|
|
||||||
|
|
||||||
// Log debug info for troubleshooting
|
|
||||||
console.error('Branch validation failed:', data.error);
|
|
||||||
if (data.debug) {
|
|
||||||
console.error('Debug info:', data.debug);
|
|
||||||
}
|
|
||||||
if (data.output) {
|
|
||||||
console.error('Command output:', data.output);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
.catch(error => {
|
// Create form to submit to step 2
|
||||||
// Network error
|
const form = document.createElement('form');
|
||||||
validation.className = 'branch-validation error';
|
form.method = 'POST';
|
||||||
message.textContent = 'Network error. Please try again.';
|
form.action = '/jobs/submit/step2-validated';
|
||||||
|
|
||||||
|
const branchInput = document.createElement('input');
|
||||||
|
branchInput.type = 'hidden';
|
||||||
|
branchInput.name = 'branch_name';
|
||||||
|
branchInput.value = validatedBranch;
|
||||||
|
form.appendChild(branchInput);
|
||||||
|
|
||||||
|
const organizedDataInput = document.createElement('input');
|
||||||
|
organizedDataInput.type = 'hidden';
|
||||||
|
organizedDataInput.name = 'organized_data';
|
||||||
|
organizedDataInput.value = JSON.stringify(organizedData);
|
||||||
|
form.appendChild(organizedDataInput);
|
||||||
|
|
||||||
|
const scenarioMapInput = document.createElement('input');
|
||||||
|
scenarioMapInput.type = 'hidden';
|
||||||
|
scenarioMapInput.name = 'scenario_map';
|
||||||
|
scenarioMapInput.value = JSON.stringify(scenarioMap);
|
||||||
|
form.appendChild(scenarioMapInput);
|
||||||
|
|
||||||
|
document.body.appendChild(form);
|
||||||
|
form.submit();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset validation when branch name changes
|
||||||
|
document.getElementById('branch_name').addEventListener('input', function () {
|
||||||
|
const validation = document.getElementById('branchValidation');
|
||||||
|
const validateBtn = document.getElementById('validateBtn');
|
||||||
|
const nextBtn = document.getElementById('nextBtn');
|
||||||
|
|
||||||
|
validation.style.display = 'none';
|
||||||
|
validateBtn.style.display = 'inline-block';
|
||||||
validateBtn.disabled = false;
|
validateBtn.disabled = false;
|
||||||
nextBtn.style.display = 'none';
|
nextBtn.style.display = 'none';
|
||||||
|
|
||||||
validatedBranch = null;
|
validatedBranch = null;
|
||||||
availableScenarios = [];
|
organizedData = {};
|
||||||
|
scenarioMap = {};
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
function proceedToStep2() {
|
|
||||||
if (!validatedBranch || Object.keys(organizedData).length === 0) {
|
|
||||||
alert('Please validate the branch first');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create form to submit to step 2
|
|
||||||
const form = document.createElement('form');
|
|
||||||
form.method = 'POST';
|
|
||||||
form.action = '/jobs/submit/step2-validated';
|
|
||||||
|
|
||||||
const branchInput = document.createElement('input');
|
|
||||||
branchInput.type = 'hidden';
|
|
||||||
branchInput.name = 'branch_name';
|
|
||||||
branchInput.value = validatedBranch;
|
|
||||||
form.appendChild(branchInput);
|
|
||||||
|
|
||||||
const organizedDataInput = document.createElement('input');
|
|
||||||
organizedDataInput.type = 'hidden';
|
|
||||||
organizedDataInput.name = 'organized_data';
|
|
||||||
organizedDataInput.value = JSON.stringify(organizedData);
|
|
||||||
form.appendChild(organizedDataInput);
|
|
||||||
|
|
||||||
const scenarioMapInput = document.createElement('input');
|
|
||||||
scenarioMapInput.type = 'hidden';
|
|
||||||
scenarioMapInput.name = 'scenario_map';
|
|
||||||
scenarioMapInput.value = JSON.stringify(scenarioMap);
|
|
||||||
form.appendChild(scenarioMapInput);
|
|
||||||
|
|
||||||
document.body.appendChild(form);
|
|
||||||
form.submit();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset validation when branch name changes
|
|
||||||
document.getElementById('branch_name').addEventListener('input', function() {
|
|
||||||
const validation = document.getElementById('branchValidation');
|
|
||||||
const validateBtn = document.getElementById('validateBtn');
|
|
||||||
const nextBtn = document.getElementById('nextBtn');
|
|
||||||
|
|
||||||
validation.style.display = 'none';
|
|
||||||
validateBtn.style.display = 'inline-block';
|
|
||||||
validateBtn.disabled = false;
|
|
||||||
nextBtn.style.display = 'none';
|
|
||||||
|
|
||||||
validatedBranch = null;
|
|
||||||
organizedData = {};
|
|
||||||
scenarioMap = {};
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
59
app/templates/jobs/submit_review.html
Normal file
59
app/templates/jobs/submit_review.html
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Review & Submit - ASF TestArena{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="submit-container">
|
||||||
|
<h2 style="margin-bottom: 30px; color: var(--dark);">Review & Submit</h2>
|
||||||
|
|
||||||
|
<div class="step-indicator">
|
||||||
|
<div class="step completed">
|
||||||
|
<div class="step-number">✓</div>
|
||||||
|
<div class="step-label">Branch</div>
|
||||||
|
</div>
|
||||||
|
<div class="step completed">
|
||||||
|
<div class="step-number">✓</div>
|
||||||
|
<div class="step-label">Scenarios</div>
|
||||||
|
</div>
|
||||||
|
<div class="step active">
|
||||||
|
<div class="step-number">3</div>
|
||||||
|
<div class="step-label">Review</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style="background: white; border: 1px solid var(--border); border-radius: 8px; padding: 20px; margin-bottom: 30px;">
|
||||||
|
<div class="detail-row">
|
||||||
|
<div class="detail-label">Branch:</div>
|
||||||
|
<div class="detail-value">{{ branch_name }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="detail-row">
|
||||||
|
<div class="detail-label">Scenarios:</div>
|
||||||
|
<div class="detail-value">
|
||||||
|
<strong>{{ scenarios|length }} scenarios selected</strong>
|
||||||
|
<ul style="margin-top: 10px; padding-left: 20px; max-height: 200px; overflow-y: auto;">
|
||||||
|
{% for scenario in scenarios %}
|
||||||
|
<li style="font-size: 13px; color: #4b5563; margin-bottom: 4px;">{{ scenario }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="POST" action="{{ url_for('jobs.submit_final') }}">
|
||||||
|
<input type="hidden" name="branch_name" value="{{ branch_name }}">
|
||||||
|
<input type="hidden" name="scenarios" value='{{ scenarios|tojson }}'>
|
||||||
|
<input type="hidden" name="scenario_map" value='{{ scenario_map|tojson }}'>
|
||||||
|
|
||||||
|
<!-- Default values for deactivated sections -->
|
||||||
|
<input type="hidden" name="environment" value="staging">
|
||||||
|
<input type="hidden" name="test_mode" value="simulator">
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="button" class="btn" style="background: #6b7280; color: white;"
|
||||||
|
onclick="history.back()">Back</button>
|
||||||
|
<button type="submit" class="btn btn-success" style="padding: 12px 40px;">Submit Job</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -17,14 +17,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="step">
|
<div class="step">
|
||||||
<div class="step-number">3</div>
|
<div class="step-number">3</div>
|
||||||
<div class="step-label">Environment</div>
|
|
||||||
</div>
|
|
||||||
<div class="step">
|
|
||||||
<div class="step-number">4</div>
|
|
||||||
<div class="step-label">Test Mode</div>
|
|
||||||
</div>
|
|
||||||
<div class="step">
|
|
||||||
<div class="step-number">5</div>
|
|
||||||
<div class="step-label">Review</div>
|
<div class="step-label">Review</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -35,74 +27,85 @@
|
|||||||
|
|
||||||
<form method="POST" action="{{ url_for('jobs.submit_step2') }}" id="scenarioForm">
|
<form method="POST" action="{{ url_for('jobs.submit_step2') }}" id="scenarioForm">
|
||||||
<input type="hidden" name="branch_name" value="{{ branch_name }}">
|
<input type="hidden" name="branch_name" value="{{ branch_name }}">
|
||||||
<input type="hidden" name="scenario_map" value="{{ scenario_map|tojson }}">
|
<input type="hidden" name="scenario_map" value='{{ scenario_map|tojson }}'>
|
||||||
<input type="hidden" name="selected_scenarios" id="selectedScenariosInput">
|
<input type="hidden" name="selected_scenarios" id="selectedScenariosInput">
|
||||||
|
|
||||||
<div class="scenario-controls">
|
<div class="scenario-controls">
|
||||||
<button type="button" class="btn btn-sm" onclick="selectAll()" style="background: var(--success); color: white; margin-right: 10px;">Select All</button>
|
<button type="button" class="btn btn-sm" onclick="selectAll()"
|
||||||
<button type="button" class="btn btn-sm" onclick="deselectAll()" style="background: var(--danger); color: white;">Deselect All</button>
|
style="background: var(--success); color: white; margin-right: 10px;">Select All</button>
|
||||||
|
<button type="button" class="btn btn-sm" onclick="deselectAll()"
|
||||||
|
style="background: var(--danger); color: white;">Deselect All</button>
|
||||||
<span id="selectionCount" style="margin-left: 20px; font-weight: 600;">0 scenarios selected</span>
|
<span id="selectionCount" style="margin-left: 20px; font-weight: 600;">0 scenarios selected</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="scenario-tree">
|
<div class="scenario-tree">
|
||||||
{% if organized_data %}
|
{% if organized_data %}
|
||||||
{% for layer_name, layer_data in organized_data.items() %}
|
{% for layer_name, layer_data in organized_data.items() %}
|
||||||
<div class="tree-layer">
|
<div class="tree-layer">
|
||||||
<div class="tree-node layer-node" onclick="toggleLayer('{{ layer_name }}')">
|
<div class="tree-node layer-node" onclick="toggleLayer('{{ layer_name }}')">
|
||||||
<span class="tree-toggle" id="toggle-{{ layer_name }}">▶</span>
|
<span class="tree-toggle" id="toggle-{{ layer_name }}">▶</span>
|
||||||
<input type="checkbox" class="layer-checkbox" id="layer-{{ layer_name }}" onchange="toggleLayerSelection('{{ layer_name }}')">
|
<input type="checkbox" class="layer-checkbox" id="layer-{{ layer_name }}"
|
||||||
<label for="layer-{{ layer_name }}" class="tree-label layer-label">{{ layer_name.replace('_', ' ').title() }}</label>
|
onchange="toggleLayerSelection('{{ layer_name }}')">
|
||||||
</div>
|
<label for="layer-{{ layer_name }}" class="tree-label layer-label">{{ layer_name.replace('_', '
|
||||||
|
').title() }}</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="tree-children" id="children-{{ layer_name }}" style="display: none;">
|
<div class="tree-children" id="children-{{ layer_name }}" style="display: none;">
|
||||||
{% if layer_data %}
|
{% if layer_data %}
|
||||||
{% for stack_name, scenarios in layer_data.items() %}
|
{% for stack_name, scenarios in layer_data.items() %}
|
||||||
<div class="tree-stack">
|
<div class="tree-stack">
|
||||||
<div class="tree-node stack-node" onclick="toggleStack('{{ layer_name }}', '{{ stack_name }}')">
|
<div class="tree-node stack-node" onclick="toggleStack('{{ layer_name }}', '{{ stack_name }}')">
|
||||||
<span class="tree-toggle" id="toggle-{{ layer_name }}-{{ stack_name }}">▶</span>
|
<span class="tree-toggle" id="toggle-{{ layer_name }}-{{ stack_name }}">▶</span>
|
||||||
<input type="checkbox" class="stack-checkbox" id="stack-{{ layer_name }}-{{ stack_name }}" onchange="toggleStackSelection('{{ layer_name }}', '{{ stack_name }}')">
|
<input type="checkbox" class="stack-checkbox" id="stack-{{ layer_name }}-{{ stack_name }}"
|
||||||
<label for="stack-{{ layer_name }}-{{ stack_name }}" class="tree-label stack-label">{{ stack_name.replace('_', ' ').title() }}</label>
|
onchange="toggleStackSelection('{{ layer_name }}', '{{ stack_name }}')">
|
||||||
</div>
|
<label for="stack-{{ layer_name }}-{{ stack_name }}" class="tree-label stack-label">{{
|
||||||
|
stack_name.replace('_', ' ').title() }}</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="tree-children" id="children-{{ layer_name }}-{{ stack_name }}" style="display: none;">
|
<div class="tree-children" id="children-{{ layer_name }}-{{ stack_name }}"
|
||||||
{% if scenarios %}
|
style="display: none;">
|
||||||
{% for scenario in scenarios %}
|
{% if scenarios %}
|
||||||
<div class="tree-scenario">
|
{% for scenario in scenarios %}
|
||||||
<div class="tree-node scenario-node">
|
<div class="tree-scenario">
|
||||||
<span class="tree-spacer"></span>
|
<div class="tree-node scenario-node">
|
||||||
<input type="checkbox" class="scenario-checkbox" id="scenario-{{ scenario }}" value="{{ scenario }}" onchange="updateSelectionCount()" data-layer="{{ layer_name }}" data-stack="{{ stack_name }}">
|
<span class="tree-spacer"></span>
|
||||||
<label for="scenario-{{ scenario }}" class="tree-label scenario-label">{{ scenario }}</label>
|
<input type="checkbox" class="scenario-checkbox" id="scenario-{{ scenario }}"
|
||||||
</div>
|
value="{{ scenario }}" onchange="updateSelectionCount()"
|
||||||
</div>
|
data-layer="{{ layer_name }}" data-stack="{{ stack_name }}">
|
||||||
{% endfor %}
|
<label for="scenario-{{ scenario }}" class="tree-label scenario-label">{{ scenario
|
||||||
{% else %}
|
}}</label>
|
||||||
<div class="tree-scenario">
|
|
||||||
<div class="tree-node scenario-node">
|
|
||||||
<span class="tree-spacer"></span>
|
|
||||||
<span class="tree-label scenario-label" style="color: #9ca3af;">No scenarios found</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="tree-stack">
|
<div class="tree-scenario">
|
||||||
<div class="tree-node stack-node">
|
<div class="tree-node scenario-node">
|
||||||
<span class="tree-spacer"></span>
|
<span class="tree-spacer"></span>
|
||||||
<span class="tree-label stack-label" style="color: #9ca3af;">No stacks found</span>
|
<span class="tree-label scenario-label" style="color: #9ca3af;">No scenarios
|
||||||
|
found</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<div class="tree-stack">
|
||||||
|
<div class="tree-node stack-node">
|
||||||
|
<span class="tree-spacer"></span>
|
||||||
|
<span class="tree-label stack-label" style="color: #9ca3af;">No stacks found</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
</div>
|
||||||
|
{% endfor %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="tree-layer">
|
<div class="tree-layer">
|
||||||
<div class="tree-node layer-node">
|
<div class="tree-node layer-node">
|
||||||
<span class="tree-label layer-label" style="color: #9ca3af;">No scenarios available</span>
|
<span class="tree-label layer-label" style="color: #9ca3af;">No scenarios available</span>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -114,155 +117,155 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
let selectedScenarios = new Set();
|
let selectedScenarios = new Set();
|
||||||
|
|
||||||
function toggleLayer(layerName) {
|
function toggleLayer(layerName) {
|
||||||
const children = document.getElementById(`children-${layerName}`);
|
const children = document.getElementById(`children-${layerName}`);
|
||||||
const toggle = document.getElementById(`toggle-${layerName}`);
|
const toggle = document.getElementById(`toggle-${layerName}`);
|
||||||
|
|
||||||
if (children.style.display === 'none') {
|
if (children.style.display === 'none') {
|
||||||
children.style.display = 'block';
|
children.style.display = 'block';
|
||||||
toggle.textContent = '▼';
|
toggle.textContent = '▼';
|
||||||
} else {
|
} else {
|
||||||
children.style.display = 'none';
|
children.style.display = 'none';
|
||||||
toggle.textContent = '▶';
|
toggle.textContent = '▶';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function toggleStack(layerName, stackName) {
|
function toggleStack(layerName, stackName) {
|
||||||
const children = document.getElementById(`children-${layerName}-${stackName}`);
|
const children = document.getElementById(`children-${layerName}-${stackName}`);
|
||||||
const toggle = document.getElementById(`toggle-${layerName}-${stackName}`);
|
const toggle = document.getElementById(`toggle-${layerName}-${stackName}`);
|
||||||
|
|
||||||
if (children.style.display === 'none') {
|
if (children.style.display === 'none') {
|
||||||
children.style.display = 'block';
|
children.style.display = 'block';
|
||||||
toggle.textContent = '▼';
|
toggle.textContent = '▼';
|
||||||
} else {
|
} else {
|
||||||
children.style.display = 'none';
|
children.style.display = 'none';
|
||||||
toggle.textContent = '▶';
|
toggle.textContent = '▶';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function toggleLayerSelection(layerName) {
|
function toggleLayerSelection(layerName) {
|
||||||
const layerCheckbox = document.getElementById(`layer-${layerName}`);
|
const layerCheckbox = document.getElementById(`layer-${layerName}`);
|
||||||
const stackCheckboxes = document.querySelectorAll(`input[id^="stack-${layerName}-"]`);
|
const stackCheckboxes = document.querySelectorAll(`input[id^="stack-${layerName}-"]`);
|
||||||
const scenarioCheckboxes = document.querySelectorAll(`input[data-layer="${layerName}"]`);
|
const scenarioCheckboxes = document.querySelectorAll(`input[data-layer="${layerName}"]`);
|
||||||
|
|
||||||
// Update all stacks and scenarios in this layer
|
// Update all stacks and scenarios in this layer
|
||||||
stackCheckboxes.forEach(checkbox => {
|
stackCheckboxes.forEach(checkbox => {
|
||||||
checkbox.checked = layerCheckbox.checked;
|
checkbox.checked = layerCheckbox.checked;
|
||||||
checkbox.indeterminate = false;
|
checkbox.indeterminate = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
scenarioCheckboxes.forEach(checkbox => {
|
scenarioCheckboxes.forEach(checkbox => {
|
||||||
checkbox.checked = layerCheckbox.checked;
|
checkbox.checked = layerCheckbox.checked;
|
||||||
if (layerCheckbox.checked) {
|
if (layerCheckbox.checked) {
|
||||||
|
selectedScenarios.add(checkbox.value);
|
||||||
|
} else {
|
||||||
|
selectedScenarios.delete(checkbox.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
updateSelectionCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleStackSelection(layerName, stackName) {
|
||||||
|
const stackCheckbox = document.getElementById(`stack-${layerName}-${stackName}`);
|
||||||
|
const scenarioCheckboxes = document.querySelectorAll(`input[data-layer="${layerName}"][data-stack="${stackName}"]`);
|
||||||
|
|
||||||
|
// Update all scenarios in this stack
|
||||||
|
scenarioCheckboxes.forEach(checkbox => {
|
||||||
|
checkbox.checked = stackCheckbox.checked;
|
||||||
|
if (stackCheckbox.checked) {
|
||||||
|
selectedScenarios.add(checkbox.value);
|
||||||
|
} else {
|
||||||
|
selectedScenarios.delete(checkbox.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
updateLayerState(layerName);
|
||||||
|
updateSelectionCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateLayerState(layerName) {
|
||||||
|
const layerCheckbox = document.getElementById(`layer-${layerName}`);
|
||||||
|
const stackCheckboxes = document.querySelectorAll(`input[id^="stack-${layerName}-"]`);
|
||||||
|
const scenarioCheckboxes = document.querySelectorAll(`input[data-layer="${layerName}"]`);
|
||||||
|
|
||||||
|
// Update stack states
|
||||||
|
stackCheckboxes.forEach(stackCheckbox => {
|
||||||
|
const stackName = stackCheckbox.id.replace(`stack-${layerName}-`, '');
|
||||||
|
const stackScenarios = document.querySelectorAll(`input[data-layer="${layerName}"][data-stack="${stackName}"]`);
|
||||||
|
const checkedCount = Array.from(stackScenarios).filter(cb => cb.checked).length;
|
||||||
|
|
||||||
|
if (checkedCount === 0) {
|
||||||
|
stackCheckbox.checked = false;
|
||||||
|
stackCheckbox.indeterminate = false;
|
||||||
|
} else if (checkedCount === stackScenarios.length) {
|
||||||
|
stackCheckbox.checked = true;
|
||||||
|
stackCheckbox.indeterminate = false;
|
||||||
|
} else {
|
||||||
|
stackCheckbox.checked = false;
|
||||||
|
stackCheckbox.indeterminate = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update layer state
|
||||||
|
const checkedScenarios = Array.from(scenarioCheckboxes).filter(cb => cb.checked).length;
|
||||||
|
if (checkedScenarios === 0) {
|
||||||
|
layerCheckbox.checked = false;
|
||||||
|
layerCheckbox.indeterminate = false;
|
||||||
|
} else if (checkedScenarios === scenarioCheckboxes.length) {
|
||||||
|
layerCheckbox.checked = true;
|
||||||
|
layerCheckbox.indeterminate = false;
|
||||||
|
} else {
|
||||||
|
layerCheckbox.checked = false;
|
||||||
|
layerCheckbox.indeterminate = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSelectionCount() {
|
||||||
|
// Update selected scenarios set
|
||||||
|
selectedScenarios.clear();
|
||||||
|
document.querySelectorAll('.scenario-checkbox:checked').forEach(checkbox => {
|
||||||
selectedScenarios.add(checkbox.value);
|
selectedScenarios.add(checkbox.value);
|
||||||
} else {
|
});
|
||||||
selectedScenarios.delete(checkbox.value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
updateSelectionCount();
|
// Update all layer states
|
||||||
}
|
const layers = new Set();
|
||||||
|
document.querySelectorAll('.scenario-checkbox').forEach(checkbox => {
|
||||||
|
layers.add(checkbox.dataset.layer);
|
||||||
|
});
|
||||||
|
layers.forEach(layer => updateLayerState(layer));
|
||||||
|
|
||||||
function toggleStackSelection(layerName, stackName) {
|
// Update UI
|
||||||
const stackCheckbox = document.getElementById(`stack-${layerName}-${stackName}`);
|
const count = selectedScenarios.size;
|
||||||
const scenarioCheckboxes = document.querySelectorAll(`input[data-layer="${layerName}"][data-stack="${stackName}"]`);
|
document.getElementById('selectionCount').textContent = `${count} scenario${count !== 1 ? 's' : ''} selected`;
|
||||||
|
document.getElementById('nextBtn').disabled = count === 0;
|
||||||
|
|
||||||
// Update all scenarios in this stack
|
// Update hidden input
|
||||||
scenarioCheckboxes.forEach(checkbox => {
|
document.getElementById('selectedScenariosInput').value = JSON.stringify(Array.from(selectedScenarios));
|
||||||
checkbox.checked = stackCheckbox.checked;
|
|
||||||
if (stackCheckbox.checked) {
|
|
||||||
selectedScenarios.add(checkbox.value);
|
|
||||||
} else {
|
|
||||||
selectedScenarios.delete(checkbox.value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
updateLayerState(layerName);
|
|
||||||
updateSelectionCount();
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateLayerState(layerName) {
|
|
||||||
const layerCheckbox = document.getElementById(`layer-${layerName}`);
|
|
||||||
const stackCheckboxes = document.querySelectorAll(`input[id^="stack-${layerName}-"]`);
|
|
||||||
const scenarioCheckboxes = document.querySelectorAll(`input[data-layer="${layerName}"]`);
|
|
||||||
|
|
||||||
// Update stack states
|
|
||||||
stackCheckboxes.forEach(stackCheckbox => {
|
|
||||||
const stackName = stackCheckbox.id.replace(`stack-${layerName}-`, '');
|
|
||||||
const stackScenarios = document.querySelectorAll(`input[data-layer="${layerName}"][data-stack="${stackName}"]`);
|
|
||||||
const checkedCount = Array.from(stackScenarios).filter(cb => cb.checked).length;
|
|
||||||
|
|
||||||
if (checkedCount === 0) {
|
|
||||||
stackCheckbox.checked = false;
|
|
||||||
stackCheckbox.indeterminate = false;
|
|
||||||
} else if (checkedCount === stackScenarios.length) {
|
|
||||||
stackCheckbox.checked = true;
|
|
||||||
stackCheckbox.indeterminate = false;
|
|
||||||
} else {
|
|
||||||
stackCheckbox.checked = false;
|
|
||||||
stackCheckbox.indeterminate = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update layer state
|
|
||||||
const checkedScenarios = Array.from(scenarioCheckboxes).filter(cb => cb.checked).length;
|
|
||||||
if (checkedScenarios === 0) {
|
|
||||||
layerCheckbox.checked = false;
|
|
||||||
layerCheckbox.indeterminate = false;
|
|
||||||
} else if (checkedScenarios === scenarioCheckboxes.length) {
|
|
||||||
layerCheckbox.checked = true;
|
|
||||||
layerCheckbox.indeterminate = false;
|
|
||||||
} else {
|
|
||||||
layerCheckbox.checked = false;
|
|
||||||
layerCheckbox.indeterminate = true;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function updateSelectionCount() {
|
function selectAll() {
|
||||||
// Update selected scenarios set
|
document.querySelectorAll('.scenario-checkbox').forEach(checkbox => {
|
||||||
selectedScenarios.clear();
|
checkbox.checked = true;
|
||||||
document.querySelectorAll('.scenario-checkbox:checked').forEach(checkbox => {
|
selectedScenarios.add(checkbox.value);
|
||||||
selectedScenarios.add(checkbox.value);
|
});
|
||||||
|
updateSelectionCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
function deselectAll() {
|
||||||
|
document.querySelectorAll('.scenario-checkbox, .stack-checkbox, .layer-checkbox').forEach(checkbox => {
|
||||||
|
checkbox.checked = false;
|
||||||
|
checkbox.indeterminate = false;
|
||||||
|
});
|
||||||
|
selectedScenarios.clear();
|
||||||
|
updateSelectionCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
updateSelectionCount();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update all layer states
|
|
||||||
const layers = new Set();
|
|
||||||
document.querySelectorAll('.scenario-checkbox').forEach(checkbox => {
|
|
||||||
layers.add(checkbox.dataset.layer);
|
|
||||||
});
|
|
||||||
layers.forEach(layer => updateLayerState(layer));
|
|
||||||
|
|
||||||
// Update UI
|
|
||||||
const count = selectedScenarios.size;
|
|
||||||
document.getElementById('selectionCount').textContent = `${count} scenario${count !== 1 ? 's' : ''} selected`;
|
|
||||||
document.getElementById('nextBtn').disabled = count === 0;
|
|
||||||
|
|
||||||
// Update hidden input
|
|
||||||
document.getElementById('selectedScenariosInput').value = JSON.stringify(Array.from(selectedScenarios));
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectAll() {
|
|
||||||
document.querySelectorAll('.scenario-checkbox').forEach(checkbox => {
|
|
||||||
checkbox.checked = true;
|
|
||||||
selectedScenarios.add(checkbox.value);
|
|
||||||
});
|
|
||||||
updateSelectionCount();
|
|
||||||
}
|
|
||||||
|
|
||||||
function deselectAll() {
|
|
||||||
document.querySelectorAll('.scenario-checkbox, .stack-checkbox, .layer-checkbox').forEach(checkbox => {
|
|
||||||
checkbox.checked = false;
|
|
||||||
checkbox.indeterminate = false;
|
|
||||||
});
|
|
||||||
selectedScenarios.clear();
|
|
||||||
updateSelectionCount();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
updateSelectionCount();
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
130
doc/api.md
Normal file
130
doc/api.md
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
# TestArena API Reference
|
||||||
|
|
||||||
|
TestArena provides a RESTful API for programmatic job submission and status monitoring.
|
||||||
|
|
||||||
|
## Base URL
|
||||||
|
|
||||||
|
All API endpoints are prefixed with `/api`.
|
||||||
|
|
||||||
|
```
|
||||||
|
http://<your-server>/api
|
||||||
|
```
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
The API uses Basic Authentication. You must provide your username and password in the request body for job submission.
|
||||||
|
|
||||||
|
## Endpoints
|
||||||
|
|
||||||
|
### 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
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"job_id": 123,
|
||||||
|
"status": "waiting",
|
||||||
|
"remote_triggered": true,
|
||||||
|
"message": "Job submitted successfully"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Error Response
|
||||||
|
|
||||||
|
**Code**: `401 Unauthorized`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": "Invalid credentials"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Code**: `400 Bad Request`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": "Missing required fields: username, password, branch_name, scenarios"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Get Job Status
|
||||||
|
|
||||||
|
Retrieve the status and details of a specific job.
|
||||||
|
|
||||||
|
- **URL**: `/job/<job_id>`
|
||||||
|
- **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"
|
||||||
|
}
|
||||||
|
```
|
||||||
60
doc/architecture.md
Normal file
60
doc/architecture.md
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
# TestArena Architecture
|
||||||
|
|
||||||
|
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 Overview
|
||||||
|
|
||||||
|
The system consists of three main layers:
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## Key Components
|
||||||
|
|
||||||
|
### 1. Job Processing Flow
|
||||||
|
|
||||||
|
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/<queue_id>`).
|
||||||
|
- Checks Task Status (`/api/status/<task_id>`).
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
### 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/<id>`**: 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.
|
||||||
37
doc/user_guide.md
Normal file
37
doc/user_guide.md
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# TestArena User Guide
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### Login
|
||||||
|
Access the TestArena dashboard and log in with your credentials. Default admin credentials are `admin` / `admin123`.
|
||||||
|
|
||||||
|
### Submitting a New Job
|
||||||
|
1. Click the **+ New Job** button on the dashboard.
|
||||||
|
2. Enter the **Branch Name** you want to test and click **Validate Branch**.
|
||||||
|
3. Select the test scenarios you want to include in the job.
|
||||||
|
4. Review your selection and click **Submit Job**.
|
||||||
|
|
||||||
|
## Monitoring Jobs
|
||||||
|
|
||||||
|
### Dashboard
|
||||||
|
The dashboard shows a list of all test jobs. You can see the status (Waiting, In Progress, Passed, Failed, Aborted) and who submitted the job.
|
||||||
|
|
||||||
|
### Searching Jobs
|
||||||
|
Click the 🔍 icon in the sidebar to open the search panel.
|
||||||
|
- **Search by Job ID**: Enter a Job ID to find a specific job globally.
|
||||||
|
- **Search by Username**: Enter a username to find all jobs submitted by that user globally.
|
||||||
|
- **Clear Search**: Click "Clear" to return to your default view (your own jobs).
|
||||||
|
|
||||||
|
### Job Details
|
||||||
|
Click on a job to view its details:
|
||||||
|
- **Scenario Summary Tab**: Shows the status of each individual scenario in the job.
|
||||||
|
- **Queue Logging Tab**: Real-time logs from the execution queue. The view persists even when the page updates.
|
||||||
|
- **Submitter Info**: The username of the person who submitted the job is listed in the details.
|
||||||
|
|
||||||
|
## Managing Jobs
|
||||||
|
|
||||||
|
### Aborting a Job
|
||||||
|
If a job is running, you can abort it by clicking the **Abort** button in the job details or via the right-click context menu in the job list.
|
||||||
|
|
||||||
|
### Deleting a Job
|
||||||
|
To remove a job from the history and the remote server, click the **Delete** button in the job details or via the right-click context menu. This will permanently delete the job data from both the local database and the remote execution server.
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
Folder PATH listing
|
|
||||||
Volume serial number is 8295-9080
|
|
||||||
D:.
|
|
||||||
| .env.example
|
|
||||||
| .gitignore
|
|
||||||
| ARCHITECTURE.md
|
|
||||||
| Caddyfile.example
|
|
||||||
| CADDY_INTEGRATION.md
|
|
||||||
| DEPLOYMENT_CHECKLIST.md
|
|
||||||
| docker-compose.yml
|
|
||||||
| Dockerfile
|
|
||||||
| icon.png
|
|
||||||
| IMPLEMENTATION_SUMMARY.md
|
|
||||||
| INDEX.md
|
|
||||||
| logs.bat
|
|
||||||
| PROJECT_STATUS.md
|
|
||||||
| project_structure.txt
|
|
||||||
| QUICK_START.md
|
|
||||||
| README.md
|
|
||||||
| requirements.txt
|
|
||||||
| scenario_exe_parser.py
|
|
||||||
| scenario_scan.py
|
|
||||||
| SETUP.md
|
|
||||||
| start.bat
|
|
||||||
| START_HERE.md
|
|
||||||
| stop.bat
|
|
||||||
| wsgi.py
|
|
||||||
|
|
|
||||||
+---.vscode
|
|
||||||
| settings.json
|
|
||||||
|
|
|
||||||
\---app
|
|
||||||
| models.py
|
|
||||||
| __init__.py
|
|
||||||
|
|
|
||||||
+---routes
|
|
||||||
| admin.py
|
|
||||||
| auth.py
|
|
||||||
| dashboard.py
|
|
||||||
| jobs.py
|
|
||||||
|
|
|
||||||
+---static
|
|
||||||
| +---css
|
|
||||||
| | style.css
|
|
||||||
| |
|
|
||||||
| \---uploads
|
|
||||||
| icon.png
|
|
||||||
|
|
|
||||||
\---templates
|
|
||||||
| base.html
|
|
||||||
| login.html
|
|
||||||
|
|
|
||||||
+---admin
|
|
||||||
| dashboard.html
|
|
||||||
|
|
|
||||||
+---dashboard
|
|
||||||
| index.html
|
|
||||||
|
|
|
||||||
\---jobs
|
|
||||||
submit.html
|
|
||||||
submit_step2.html
|
|
||||||
submit_step3.html
|
|
||||||
submit_step4.html
|
|
||||||
|
|
||||||
@@ -7,3 +7,4 @@ gunicorn==21.2.0
|
|||||||
python-dotenv==1.0.0
|
python-dotenv==1.0.0
|
||||||
Werkzeug==3.0.1
|
Werkzeug==3.0.1
|
||||||
WTForms==3.1.1
|
WTForms==3.1.1
|
||||||
|
requests==2.31.0
|
||||||
|
|||||||
40
test_api.py
Normal file
40
test_api.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import requests
|
||||||
|
import json
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
BASE_URL = "http://localhost:5000" # Adjust if running on a different port
|
||||||
|
API_URL = f"{BASE_URL}/api/submit_job"
|
||||||
|
|
||||||
|
# Test Data
|
||||||
|
payload = {
|
||||||
|
"username": "admin",
|
||||||
|
"password": "admin123", # Default password from __init__.py
|
||||||
|
"branch_name": "test_branch",
|
||||||
|
"scenarios": ["scenario1", "scenario2"]
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
print(f"Sending POST request to {API_URL}...")
|
||||||
|
response = requests.post(API_URL, json=payload)
|
||||||
|
|
||||||
|
print(f"Status Code: {response.status_code}")
|
||||||
|
print("Response JSON:")
|
||||||
|
print(json.dumps(response.json(), indent=2))
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
print("\nSUCCESS: Job submitted successfully.")
|
||||||
|
job_id = response.json().get('job_id')
|
||||||
|
|
||||||
|
# Test Status API
|
||||||
|
if job_id:
|
||||||
|
STATUS_URL = f"{BASE_URL}/api/job/{job_id}"
|
||||||
|
print(f"\nTesting Status API: {STATUS_URL}")
|
||||||
|
status_resp = requests.get(STATUS_URL)
|
||||||
|
print(f"Status Code: {status_resp.status_code}")
|
||||||
|
print("Status Response:")
|
||||||
|
print(json.dumps(status_resp.json(), indent=2))
|
||||||
|
else:
|
||||||
|
print("\nFAILURE: Job submission failed.")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\nERROR: Could not connect to server. Is it running? {e}")
|
||||||
Reference in New Issue
Block a user