Compare commits

13 Commits

Author SHA1 Message Date
2d81f78fed theme 2026-01-05 15:41:31 +01:00
67e51a5348 update upload scripts 2026-01-05 14:49:54 +01:00
efb61eb283 update upload scripts 2026-01-05 13:33:00 +01:00
3572a99cc0 Update app/routes/jobs.py 2026-01-05 10:53:17 +00:00
586501b94a fix loading issue 2026-01-04 18:12:22 +01:00
f254e04fc5 fix loading issue 2026-01-04 18:01:23 +01:00
e33ce220e9 fix loading issue 2026-01-04 17:52:51 +01:00
ce37351722 fix loading issue 2026-01-04 16:36:57 +01:00
396a0555d9 update test arena 2026-01-04 16:15:37 +01:00
9d2f2b65ae fix log in issue 2025-12-28 06:11:06 +01:00
0c5f801854 fix log in issue 2025-12-28 06:05:27 +01:00
a9ad5bc2ad fix log in issue 2025-12-28 05:59:49 +01:00
6b254534f6 next_updates 2025-12-28 05:52:49 +01:00
33 changed files with 1681 additions and 5459 deletions

View File

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

View File

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

View File

@@ -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:** _______________

View File

@@ -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.*

View File

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

View File

@@ -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!**

View File

@@ -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
View File

@@ -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)**

View File

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

View File

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

View File

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

View File

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

View File

@@ -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
View File

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

View File

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

View File

@@ -2,6 +2,10 @@ from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager
import os
import threading
import time
import requests
import json
db = SQLAlchemy()
login_manager = LoginManager()
@@ -28,16 +32,45 @@ def create_app():
from app.routes.admin import admin_bp
from app.routes.dashboard import dashboard_bp
from app.routes.jobs import jobs_bp
from app.routes.api import api_bp
app.register_blueprint(auth_bp)
app.register_blueprint(admin_bp)
app.register_blueprint(dashboard_bp)
app.register_blueprint(jobs_bp)
app.register_blueprint(api_bp)
with app.app_context():
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
try:
from app.models import User
if not User.query.filter_by(username='admin').first():
admin = User(username='admin', is_admin=True)
admin.set_password('admin123')
@@ -47,4 +80,22 @@ def create_app():
# Admin user might already exist, rollback and continue
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

View File

@@ -37,11 +37,19 @@ class Job(db.Model):
reuse_results = db.Column(db.Boolean, default=False)
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):
icons = {
'in_progress': '🟠',
'passed': '🟢',
'failed': '🔴',
'aborted': ''
'waiting': '',
'in_progress': '🔄',
'passed': '',
'failed': '',
'aborted': '',
'error': '⚠️'
}
return icons.get(self.status, '')

120
app/routes/api.py Normal file
View 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

View File

@@ -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 app.models import Job
@@ -7,9 +7,26 @@ dashboard_bp = Blueprint('dashboard', __name__, url_prefix='/dashboard')
@dashboard_bp.route('/')
@login_required
def index():
if current_user.is_admin:
jobs = Job.query.order_by(Job.submitted_at.desc()).all()
else:
jobs = Job.query.filter_by(user_id=current_user.id).order_by(Job.submitted_at.desc()).all()
username_query = request.args.get('username')
job_id_query = request.args.get('job_id')
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)

View File

@@ -5,6 +5,14 @@ from app import db
import json
import subprocess
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')
@@ -121,7 +129,7 @@ def submit_step1():
ssh_options = f"-p {ssh_port} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
# 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, '***')}")
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}")
# 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, '***')}")
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
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, '***')}")
scan_result = subprocess.run(scan_cmd, shell=True, capture_output=True, text=True, timeout=120)
@@ -262,7 +270,10 @@ def submit_step2():
try:
scenario_map = json.loads(scenario_map_json) if scenario_map_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')
return redirect(url_for('jobs.submit'))
@@ -270,7 +281,7 @@ def submit_step2():
flash('Please select at least one scenario', 'error')
return redirect(url_for('jobs.submit'))
return render_template('jobs/submit_step3.html',
return render_template('jobs/submit_review.html',
branch_name=branch_name,
scenarios=selected_scenarios,
scenario_map=scenario_map)
@@ -301,6 +312,8 @@ def submit_step2_validated():
except json.JSONDecodeError as 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')
return redirect(url_for('jobs.submit'))
@@ -318,73 +331,63 @@ def submit_step2_validated():
print(f"[ERROR] Step2 - Traceback: {traceback.format_exc()}")
flash(f'Error loading scenario selection: {str(e)}', 'error')
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'])
@login_required
def submit_final():
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')
test_mode = request.form.get('test_mode')
keep_devbenches = request.form.get('keep_devbenches') == 'on'
reuse_results = request.form.get('reuse_results') == 'on'
environment = request.form.get('environment', 'staging')
test_mode = request.form.get('test_mode', 'simulator')
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:
except json.JSONDecodeError as e:
print(f"[ERROR] JSON Decode Error in submit_final: {e}")
flash('Invalid scenario data', 'error')
return redirect(url_for('jobs.submit'))
# Create Job record first to get the ID
job = Job(
user_id=current_user.id,
branch_name=branch_name,
scenarios=json.dumps(scenarios), # Store as JSON string
scenarios=json.dumps(scenarios),
environment=environment,
test_mode=test_mode,
keep_devbenches=keep_devbenches,
reuse_results=reuse_results,
status='in_progress'
status='waiting'
)
db.session.add(job)
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')
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'),
'completed_at': job.completed_at.strftime('%Y-%m-%d %H:%M:%S') if job.completed_at else None,
'duration': job.duration,
'keep_devbenches': job.keep_devbenches,
'reuse_results': job.reuse_results,
'results_path': job.results_path
'remote_queue_id': job.remote_queue_id,
'remote_task_ids': job.remote_task_ids,
'remote_results': job.remote_results,
'queue_log': job.queue_log
})
@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:
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'
db.session.commit()
# TODO: Kill the running process
return jsonify({'success': True})
return jsonify({'success': True, 'message': 'Job aborted successfully'})
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
})

View File

@@ -1,12 +1,26 @@
:root {
--primary: #2563eb;
--primary-dark: #1e40af;
--primary: #3b82f6;
/* Blue 500 */
--primary-dark: #2563eb;
/* Blue 600 */
--success: #10b981;
/* Emerald 500 */
--danger: #ef4444;
/* Red 500 */
--warning: #f59e0b;
--dark: #1f2937;
--light: #f3f4f6;
--border: #e5e7eb;
/* Amber 500 */
--dark: #f8fafc;
/* Slate 50 (Text) */
--bg-dark: #0f172a;
/* Slate 900 (Body) */
--bg-panel: #1e293b;
/* Slate 800 (Panels) */
--bg-input: #334155;
/* Slate 700 (Inputs) */
--border: #334155;
/* Slate 700 */
--text-muted: #94a3b8;
/* Slate 400 */
}
* {
@@ -16,8 +30,9 @@
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
font-family: 'Inter', 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: var(--bg-dark);
color: var(--dark);
min-height: 100vh;
}
@@ -36,12 +51,13 @@ body {
}
.login-box {
background: white;
background: var(--bg-panel);
padding: 40px;
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
border-radius: 16px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
width: 100%;
max-width: 400px;
border: 1px solid var(--border);
}
.logo {
@@ -58,6 +74,7 @@ body {
color: var(--dark);
margin-top: 15px;
font-size: 24px;
font-weight: 700;
}
.form-group {
@@ -67,21 +84,25 @@ body {
.form-group label {
display: block;
margin-bottom: 8px;
color: var(--dark);
color: var(--text-muted);
font-weight: 500;
}
.form-control {
width: 100%;
padding: 12px;
border: 2px solid var(--border);
background: var(--bg-input);
border: 1px solid var(--border);
border-radius: 8px;
font-size: 14px;
color: var(--dark);
transition: all 0.2s;
}
.form-control:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
}
.btn {
@@ -127,25 +148,26 @@ body {
}
.alert-success {
background: #d1fae5;
color: #065f46;
border: 1px solid #10b981;
background: rgba(16, 185, 129, 0.1);
color: #34d399;
border: 1px solid rgba(16, 185, 129, 0.2);
}
.alert-error {
background: #fee2e2;
color: #991b1b;
border: 1px solid #ef4444;
background: rgba(239, 68, 68, 0.1);
color: #f87171;
border: 1px solid rgba(239, 68, 68, 0.2);
}
/* Navbar */
.navbar {
background: white;
background: var(--bg-panel);
padding: 15px 30px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid var(--border);
}
.navbar-brand {
@@ -161,6 +183,7 @@ body {
.navbar-brand h2 {
color: var(--dark);
font-size: 20px;
font-weight: 700;
}
.navbar-menu {
@@ -170,7 +193,7 @@ body {
}
.navbar-menu a {
color: var(--dark);
color: var(--text-muted);
text-decoration: none;
font-weight: 500;
transition: color 0.3s;
@@ -190,11 +213,12 @@ body {
}
.panel {
background: white;
border-radius: 12px;
background: var(--bg-panel);
border-radius: 16px;
padding: 20px;
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
overflow-y: auto;
border: 1px solid var(--border);
}
.panel-header {
@@ -203,12 +227,13 @@ body {
align-items: center;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 2px solid var(--border);
border-bottom: 1px solid var(--border);
}
.panel-header h3 {
color: var(--dark);
font-size: 18px;
font-weight: 600;
}
/* Job List */
@@ -220,10 +245,11 @@ body {
.job-item {
padding: 15px;
border: 2px solid var(--border);
border-radius: 8px;
background: var(--bg-input);
border: 1px solid var(--border);
border-radius: 12px;
cursor: pointer;
transition: all 0.3s;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 12px;
@@ -231,12 +257,12 @@ body {
.job-item:hover {
border-color: var(--primary);
background: var(--light);
transform: translateY(-2px);
}
.job-item.active {
border-color: var(--primary);
background: #eff6ff;
background: rgba(59, 130, 246, 0.1);
}
.job-status-icon {
@@ -247,10 +273,11 @@ body {
color: var(--dark);
font-size: 14px;
margin-bottom: 4px;
font-weight: 600;
}
.job-info p {
color: #6b7280;
color: var(--text-muted);
font-size: 12px;
}
@@ -272,11 +299,11 @@ body {
.detail-label {
font-weight: 600;
color: var(--dark);
color: var(--text-muted);
}
.detail-value {
color: #4b5563;
color: var(--dark);
}
.status-badge {
@@ -288,32 +315,43 @@ body {
}
.status-in_progress {
background: #fef3c7;
color: #92400e;
background: rgba(245, 158, 11, 0.1);
color: #fbbf24;
border: 1px solid rgba(245, 158, 11, 0.2);
}
.status-passed {
background: #d1fae5;
color: #065f46;
background: rgba(16, 185, 129, 0.1);
color: #34d399;
border: 1px solid rgba(16, 185, 129, 0.2);
}
.status-failed {
background: #fee2e2;
color: #991b1b;
background: rgba(239, 68, 68, 0.1);
color: #f87171;
border: 1px solid rgba(239, 68, 68, 0.2);
}
.status-aborted {
background: #f3f4f6;
color: #374151;
background: rgba(148, 163, 184, 0.1);
color: #cbd5e1;
border: 1px solid rgba(148, 163, 184, 0.2);
}
.status-waiting {
background: rgba(245, 158, 11, 0.1);
color: #fbbf24;
border: 1px solid rgba(245, 158, 11, 0.2);
}
/* Admin Dashboard */
.admin-container {
background: white;
border-radius: 12px;
background: var(--bg-panel);
border-radius: 16px;
padding: 30px;
margin-top: 20px;
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
border: 1px solid var(--border);
}
.admin-header {
@@ -333,7 +371,7 @@ body {
}
.user-table th {
background: var(--light);
background: var(--bg-input);
padding: 12px;
text-align: left;
font-weight: 600;
@@ -344,10 +382,11 @@ body {
.user-table td {
padding: 12px;
border-bottom: 1px solid var(--border);
color: var(--text-muted);
}
.user-table tr:hover {
background: var(--light);
background: rgba(255, 255, 255, 0.02);
}
.badge {
@@ -359,13 +398,13 @@ body {
}
.badge-admin {
background: #dbeafe;
color: #1e40af;
background: rgba(59, 130, 246, 0.1);
color: #60a5fa;
}
.badge-user {
background: #f3f4f6;
color: #374151;
background: rgba(148, 163, 184, 0.1);
color: #cbd5e1;
}
/* Modal */
@@ -376,8 +415,9 @@ body {
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
background: rgba(0, 0, 0, 0.7);
z-index: 1000;
backdrop-filter: blur(4px);
}
.modal.active {
@@ -387,11 +427,13 @@ body {
}
.modal-content {
background: white;
background: var(--bg-panel);
padding: 30px;
border-radius: 12px;
border-radius: 16px;
width: 90%;
max-width: 500px;
border: 1px solid var(--border);
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
}
.modal-header {
@@ -410,19 +452,24 @@ body {
border: none;
font-size: 24px;
cursor: pointer;
color: #6b7280;
color: var(--text-muted);
}
.close-btn:hover {
color: var(--dark);
}
/* Submit Form */
.submit-container {
background: white;
border-radius: 12px;
background: var(--bg-panel);
border-radius: 16px;
padding: 40px;
margin-top: 20px;
max-width: 800px;
margin-left: auto;
margin-right: auto;
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
border: 1px solid var(--border);
}
.step-indicator {
@@ -455,29 +502,33 @@ body {
.step.active .step-number {
background: var(--primary);
color: white;
border-color: var(--primary);
}
.step.completed .step-number {
background: var(--success);
color: white;
border-color: var(--success);
}
.step-number {
width: 30px;
height: 30px;
border-radius: 50%;
background: var(--border);
color: var(--dark);
background: var(--bg-input);
border: 2px solid var(--border);
color: var(--text-muted);
display: inline-flex;
align-items: center;
justify-content: center;
font-weight: 600;
margin-bottom: 8px;
transition: all 0.3s;
}
.step-label {
font-size: 12px;
color: #6b7280;
color: var(--text-muted);
}
.checkbox-group {
@@ -491,11 +542,13 @@ body {
display: flex;
align-items: center;
gap: 8px;
color: var(--dark);
}
.checkbox-item input[type="checkbox"] {
width: 18px;
height: 18px;
accent-color: var(--primary);
}
.radio-group {
@@ -510,16 +563,23 @@ body {
align-items: center;
gap: 10px;
padding: 12px;
border: 2px solid var(--border);
border: 1px solid var(--border);
border-radius: 8px;
cursor: pointer;
background: var(--bg-input);
transition: all 0.2s;
}
.radio-item:hover {
border-color: var(--primary);
}
.radio-item input[type="radio"]:checked + label {
.radio-item label {
color: var(--dark);
cursor: pointer;
}
.radio-item input[type="radio"]:checked+label {
color: var(--primary);
font-weight: 600;
}
@@ -534,20 +594,22 @@ body {
.empty-state {
text-align: center;
padding: 60px 20px;
color: #6b7280;
color: var(--text-muted);
}
.empty-state h3 {
margin-bottom: 10px;
color: var(--dark);
}
/* Context Menu */
.context-menu {
display: none;
position: absolute;
background: white;
background: var(--bg-panel);
border: 1px solid var(--border);
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
z-index: 1000;
min-width: 150px;
}
@@ -559,10 +621,11 @@ body {
align-items: center;
gap: 8px;
transition: background-color 0.2s;
color: var(--dark);
}
.context-menu-item:hover {
background: var(--light);
background: var(--bg-input);
}
.context-menu-item:first-child {
@@ -586,28 +649,28 @@ body {
}
.branch-validation.success {
background: #d1fae5;
color: #065f46;
border: 1px solid #10b981;
background: rgba(16, 185, 129, 0.1);
color: #34d399;
border: 1px solid rgba(16, 185, 129, 0.2);
}
.branch-validation.error {
background: #fee2e2;
color: #991b1b;
border: 1px solid #ef4444;
background: rgba(239, 68, 68, 0.1);
color: #f87171;
border: 1px solid rgba(239, 68, 68, 0.2);
}
.branch-validation.loading {
background: #fef3c7;
color: #92400e;
border: 1px solid #f59e0b;
background: rgba(245, 158, 11, 0.1);
color: #fbbf24;
border: 1px solid rgba(245, 158, 11, 0.2);
}
.loading-spinner {
display: inline-block;
width: 16px;
height: 16px;
border: 2px solid #f3f3f3;
border: 2px solid rgba(255, 255, 255, 0.1);
border-top: 2px solid var(--primary);
border-radius: 50%;
animation: spin 1s linear infinite;
@@ -615,31 +678,39 @@ body {
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.btn:disabled {
background: #9ca3af;
background: var(--bg-input);
cursor: not-allowed;
opacity: 0.6;
}/
* Scenario Tree Styles */
color: var(--text-muted);
}
/* Scenario Tree Styles */
.scenario-controls {
margin: 20px 0;
padding: 15px;
background: var(--light);
background: var(--bg-input);
border-radius: 8px;
display: flex;
align-items: center;
gap: 10px;
border: 1px solid var(--border);
}
.scenario-tree {
border: 1px solid var(--border);
border-radius: 8px;
padding: 20px;
background: white;
background: var(--bg-panel);
margin: 20px 0;
max-height: 500px;
overflow-y: auto;
@@ -666,6 +737,7 @@ body {
padding: 8px 0;
cursor: pointer;
user-select: none;
color: var(--dark);
}
.tree-toggle {
@@ -700,23 +772,26 @@ body {
.stack-label {
font-size: 14px;
font-weight: 500;
color: #4b5563;
color: var(--text-muted);
}
.scenario-label {
font-size: 13px;
font-weight: 400;
color: #6b7280;
color: var(--text-muted);
}
.tree-children {
margin-top: 5px;
}
.layer-checkbox, .stack-checkbox, .scenario-checkbox {
.layer-checkbox,
.stack-checkbox,
.scenario-checkbox {
width: 16px;
height: 16px;
cursor: pointer;
accent-color: var(--primary);
}
.layer-node:hover {
@@ -754,3 +829,68 @@ input[type="checkbox"]:indeterminate::before {
color: var(--primary);
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);
}

View File

@@ -31,12 +31,15 @@
<span class="badge badge-user">User</span>
{% endif %}
</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>
<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 %}
<form method="POST" action="{{ url_for('admin.delete_user', user_id=user.id) }}" style="display: inline;">
<button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('Delete user {{ user.username }}?')">Delete</button>
<form method="POST" action="{{ url_for('admin.delete_user', user_id=user.id) }}"
style="display: inline;">
<button type="submit" class="btn btn-sm btn-danger"
onclick="return confirm('Delete user {{ user.username }}?')">Delete</button>
</form>
{% endif %}
</td>
@@ -92,25 +95,25 @@
</div>
<script>
function openModal(modalId) {
document.getElementById(modalId).classList.add('active');
}
function closeModal(modalId) {
document.getElementById(modalId).classList.remove('active');
}
function openResetPasswordModal(userId, username) {
document.getElementById('resetUsername').textContent = username;
document.getElementById('resetPasswordForm').action = `/admin/users/${userId}/reset-password`;
openModal('resetPasswordModal');
}
// Close modal when clicking outside
window.onclick = function(event) {
if (event.target.classList.contains('modal')) {
event.target.classList.remove('active');
function openModal(modalId) {
document.getElementById(modalId).classList.add('active');
}
function closeModal(modalId) {
document.getElementById(modalId).classList.remove('active');
}
function openResetPasswordModal(userId, username) {
document.getElementById('resetUsername').textContent = username;
document.getElementById('resetPasswordForm').action = `/admin/users/${userId}/reset-password`;
openModal('resetPasswordModal');
}
// Close modal when clicking outside
window.onclick = function (event) {
if (event.target.classList.contains('modal')) {
event.target.classList.remove('active');
}
}
}
</script>
{% endblock %}

View File

@@ -1,11 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<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') }}">
</head>
<body>
{% if current_user.is_authenticated %}
<nav class="navbar">
@@ -27,11 +30,11 @@
<div class="container">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
@@ -39,4 +42,5 @@
{% block scripts %}{% endblock %}
</body>
</html>

View File

@@ -4,35 +4,68 @@
{% block content %}
<div class="dashboard-container">
<div class="panel">
<div class="panel sidebar-panel">
<div class="panel-header">
<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 class="job-list">
{% if 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-status-icon">{{ job.get_status_icon() }}</div>
<div class="job-info">
<h4>Job #{{ job.id }} - {{ job.branch_name }}</h4>
<p>{{ job.submitted_at.strftime('%Y-%m-%d %H:%M') }} by {{ job.submitter.username }}</p>
</div>
{% 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-status-icon">{{ job.get_status_icon() }}</div>
<div class="job-info">
<h4>Job #{{ job.id }} - {{ job.branch_name }}</h4>
<p>{{ job.submitted_at.strftime('%Y-%m-%d %H:%M') }} by {{ job.submitter.username }}</p>
</div>
{% endfor %}
</div>
{% endfor %}
{% else %}
<div class="empty-state">
<h3>No jobs yet</h3>
<p>Submit your first test job to get started</p>
</div>
<div class="empty-state">
<h3>No jobs found</h3>
<p>Try a different search or submit a new job</p>
</div>
{% endif %}
</div>
</div>
<div class="panel">
<div class="panel details-panel">
<div class="panel-header">
<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 id="job-details-container">
@@ -46,64 +79,128 @@
<!-- 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>
Abort Job
</div>
<div class="context-menu-item" onclick="deleteJobFromContext()">
<span class="context-menu-icon">🗑️</span>
Delete Job
</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>
let contextJobId = null;
let contextJobId = null;
let activeJobId = null;
let pollingInterval = null;
let currentActiveTab = 'scenarios';
function showContextMenu(event, jobId, status) {
event.preventDefault();
// Only show context menu for in_progress jobs
if (status !== 'in_progress') {
return;
function toggleSearch() {
const panel = document.getElementById('search-panel');
panel.style.display = panel.style.display === 'none' ? 'block' : 'none';
}
contextJobId = jobId;
const contextMenu = document.getElementById('contextMenu');
function showContextMenu(event, jobId, status) {
event.preventDefault();
contextJobId = jobId;
const contextMenu = document.getElementById('contextMenu');
contextMenu.style.display = 'block';
contextMenu.style.left = event.pageX + 'px';
contextMenu.style.top = event.pageY + 'px';
const abortItem = document.getElementById('ctx-abort');
if (status === 'in_progress' || status === 'waiting') {
abortItem.style.display = 'flex';
} else {
abortItem.style.display = 'none';
}
// Hide context menu when clicking elsewhere
document.addEventListener('click', hideContextMenu);
}
contextMenu.style.display = 'block';
contextMenu.style.left = event.pageX + 'px';
contextMenu.style.top = event.pageY + 'px';
function hideContextMenu() {
document.getElementById('contextMenu').style.display = 'none';
document.removeEventListener('click', hideContextMenu);
contextJobId = null;
}
function abortJobFromContext() {
if (contextJobId) {
abortJob(contextJobId);
document.addEventListener('click', hideContextMenu);
}
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
fetch(`/jobs/${jobId}`)
.then(response => response.json())
.then(job => {
const container = document.getElementById('job-details-container');
const scenarios = JSON.parse(job.scenarios || '[]');
function hideContextMenu() {
document.getElementById('contextMenu').style.display = 'none';
document.removeEventListener('click', hideContextMenu);
contextJobId = null;
}
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-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 class="detail-row">
<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>
</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) {
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);
<div class="tabs">
<div class="tab ${currentActiveTab === 'scenarios' ? 'active' : ''}" onclick="switchTab('scenarios')">Scenario Summary</div>
<div class="tab ${currentActiveTab === 'logs' ? 'active' : ''}" onclick="switchTab('logs')">Queue Logging</div>
</div>
<div id="scenarios-tab" class="tab-content ${currentActiveTab === 'scenarios' ? 'active' : ''}">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
<h4 style="color: var(--dark);">Scenarios</h4>
<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>
{% endblock %}

View File

@@ -17,14 +17,6 @@
</div>
<div class="step">
<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>
</div>
@@ -33,7 +25,7 @@
<div class="form-group">
<label for="branch_name">Git Branch Name</label>
<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;">
Enter the branch name to analyze available test scenarios
</small>
@@ -47,151 +39,152 @@
<div class="form-actions">
<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="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>
</form>
</div>
<script>
let validatedBranch = null;
let organizedData = {};
let scenarioMap = {};
let validatedBranch = null;
let organizedData = {};
let scenarioMap = {};
document.getElementById('branchForm').addEventListener('submit', function(e) {
e.preventDefault();
validateBranch();
});
document.getElementById('branchForm').addEventListener('submit', function (e) {
e.preventDefault();
validateBranch();
});
function validateBranch() {
const branchName = document.getElementById('branch_name').value.trim();
function validateBranch() {
const branchName = document.getElementById('branch_name').value.trim();
if (!branchName) {
alert('Please enter a branch name');
return;
if (!branchName) {
alert('Please enter a branch name');
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
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);
}
function proceedToStep2() {
if (!validatedBranch || Object.keys(organizedData).length === 0) {
alert('Please validate the branch first');
return;
}
})
.catch(error => {
// Network error
validation.className = 'branch-validation error';
message.textContent = 'Network error. Please try again.';
// 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;
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>
{% endblock %}

View 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 %}

View File

@@ -17,14 +17,6 @@
</div>
<div class="step">
<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>
</div>
@@ -35,74 +27,85 @@
<form method="POST" action="{{ url_for('jobs.submit_step2') }}" id="scenarioForm">
<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">
<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="deselectAll()" style="background: var(--danger); color: white;">Deselect All</button>
<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="deselectAll()"
style="background: var(--danger); color: white;">Deselect All</button>
<span id="selectionCount" style="margin-left: 20px; font-weight: 600;">0 scenarios selected</span>
</div>
<div class="scenario-tree">
{% if organized_data %}
{% for layer_name, layer_data in organized_data.items() %}
<div class="tree-layer">
<div class="tree-node layer-node" onclick="toggleLayer('{{ layer_name }}')">
<span class="tree-toggle" id="toggle-{{ layer_name }}"></span>
<input type="checkbox" class="layer-checkbox" id="layer-{{ layer_name }}" onchange="toggleLayerSelection('{{ layer_name }}')">
<label for="layer-{{ layer_name }}" class="tree-label layer-label">{{ layer_name.replace('_', ' ').title() }}</label>
</div>
{% for layer_name, layer_data in organized_data.items() %}
<div class="tree-layer">
<div class="tree-node layer-node" onclick="toggleLayer('{{ layer_name }}')">
<span class="tree-toggle" id="toggle-{{ layer_name }}"></span>
<input type="checkbox" class="layer-checkbox" id="layer-{{ layer_name }}"
onchange="toggleLayerSelection('{{ layer_name }}')">
<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;">
{% if layer_data %}
{% for stack_name, scenarios in layer_data.items() %}
<div class="tree-stack">
<div class="tree-node stack-node" onclick="toggleStack('{{ layer_name }}', '{{ stack_name }}')">
<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 }}')">
<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 }}" style="display: none;">
{% if layer_data %}
{% for stack_name, scenarios in layer_data.items() %}
<div class="tree-stack">
<div class="tree-node stack-node" onclick="toggleStack('{{ layer_name }}', '{{ stack_name }}')">
<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 }}')">
<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;">
{% if scenarios %}
{% for scenario in scenarios %}
<div class="tree-scenario">
<div class="tree-node scenario-node">
<span class="tree-spacer"></span>
<input type="checkbox" class="scenario-checkbox" id="scenario-{{ scenario }}" value="{{ scenario }}" onchange="updateSelectionCount()" data-layer="{{ layer_name }}" data-stack="{{ stack_name }}">
<label for="scenario-{{ scenario }}" class="tree-label scenario-label">{{ scenario }}</label>
</div>
</div>
{% endfor %}
{% else %}
<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 class="tree-children" id="children-{{ layer_name }}-{{ stack_name }}"
style="display: none;">
{% if scenarios %}
{% for scenario in scenarios %}
<div class="tree-scenario">
<div class="tree-node scenario-node">
<span class="tree-spacer"></span>
<input type="checkbox" class="scenario-checkbox" id="scenario-{{ scenario }}"
value="{{ scenario }}" onchange="updateSelectionCount()"
data-layer="{{ layer_name }}" data-stack="{{ stack_name }}">
<label for="scenario-{{ scenario }}" class="tree-label scenario-label">{{ scenario
}}</label>
</div>
</div>
{% endfor %}
{% else %}
<div class="tree-stack">
<div class="tree-node stack-node">
{% else %}
<div class="tree-scenario">
<div class="tree-node scenario-node">
<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>
{% endif %}
{% endif %}
</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>
{% endfor %}
</div>
{% endfor %}
{% else %}
<div class="tree-layer">
<div class="tree-node layer-node">
<span class="tree-label layer-label" style="color: #9ca3af;">No scenarios available</span>
</div>
<div class="tree-layer">
<div class="tree-node layer-node">
<span class="tree-label layer-label" style="color: #9ca3af;">No scenarios available</span>
</div>
</div>
{% endif %}
</div>
@@ -114,155 +117,155 @@
</div>
<script>
let selectedScenarios = new Set();
let selectedScenarios = new Set();
function toggleLayer(layerName) {
const children = document.getElementById(`children-${layerName}`);
const toggle = document.getElementById(`toggle-${layerName}`);
function toggleLayer(layerName) {
const children = document.getElementById(`children-${layerName}`);
const toggle = document.getElementById(`toggle-${layerName}`);
if (children.style.display === 'none') {
children.style.display = 'block';
toggle.textContent = '▼';
} else {
children.style.display = 'none';
toggle.textContent = '▶';
if (children.style.display === 'none') {
children.style.display = 'block';
toggle.textContent = '▼';
} else {
children.style.display = 'none';
toggle.textContent = '▶';
}
}
}
function toggleStack(layerName, stackName) {
const children = document.getElementById(`children-${layerName}-${stackName}`);
const toggle = document.getElementById(`toggle-${layerName}-${stackName}`);
function toggleStack(layerName, stackName) {
const children = document.getElementById(`children-${layerName}-${stackName}`);
const toggle = document.getElementById(`toggle-${layerName}-${stackName}`);
if (children.style.display === 'none') {
children.style.display = 'block';
toggle.textContent = '▼';
} else {
children.style.display = 'none';
toggle.textContent = '▶';
if (children.style.display === 'none') {
children.style.display = 'block';
toggle.textContent = '▼';
} else {
children.style.display = 'none';
toggle.textContent = '▶';
}
}
}
function toggleLayerSelection(layerName) {
const layerCheckbox = document.getElementById(`layer-${layerName}`);
const stackCheckboxes = document.querySelectorAll(`input[id^="stack-${layerName}-"]`);
const scenarioCheckboxes = document.querySelectorAll(`input[data-layer="${layerName}"]`);
function toggleLayerSelection(layerName) {
const layerCheckbox = document.getElementById(`layer-${layerName}`);
const stackCheckboxes = document.querySelectorAll(`input[id^="stack-${layerName}-"]`);
const scenarioCheckboxes = document.querySelectorAll(`input[data-layer="${layerName}"]`);
// Update all stacks and scenarios in this layer
stackCheckboxes.forEach(checkbox => {
checkbox.checked = layerCheckbox.checked;
checkbox.indeterminate = false;
});
// Update all stacks and scenarios in this layer
stackCheckboxes.forEach(checkbox => {
checkbox.checked = layerCheckbox.checked;
checkbox.indeterminate = false;
});
scenarioCheckboxes.forEach(checkbox => {
checkbox.checked = layerCheckbox.checked;
if (layerCheckbox.checked) {
scenarioCheckboxes.forEach(checkbox => {
checkbox.checked = 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);
} 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) {
const stackCheckbox = document.getElementById(`stack-${layerName}-${stackName}`);
const scenarioCheckboxes = document.querySelectorAll(`input[data-layer="${layerName}"][data-stack="${stackName}"]`);
// Update UI
const count = selectedScenarios.size;
document.getElementById('selectionCount').textContent = `${count} scenario${count !== 1 ? 's' : ''} selected`;
document.getElementById('nextBtn').disabled = count === 0;
// 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;
// Update hidden input
document.getElementById('selectedScenariosInput').value = JSON.stringify(Array.from(selectedScenarios));
}
}
function updateSelectionCount() {
// Update selected scenarios set
selectedScenarios.clear();
document.querySelectorAll('.scenario-checkbox:checked').forEach(checkbox => {
selectedScenarios.add(checkbox.value);
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();
});
// 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>
{% endblock %}

130
doc/api.md Normal file
View 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
View 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
View 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.

View File

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

View File

@@ -7,3 +7,4 @@ gunicorn==21.2.0
python-dotenv==1.0.0
Werkzeug==3.0.1
WTForms==3.1.1
requests==2.31.0

40
test_api.py Normal file
View 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}")