diff --git a/app/__init__.py b/app/__init__.py index 46d875a..83be759 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -32,11 +32,13 @@ 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() diff --git a/app/routes/api.py b/app/routes/api.py new file mode 100644 index 0000000..975d815 --- /dev/null +++ b/app/routes/api.py @@ -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/', 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 diff --git a/app/static/css/style.css b/app/static/css/style.css index 50ce4ca..30569f1 100644 --- a/app/static/css/style.css +++ b/app/static/css/style.css @@ -17,7 +17,7 @@ body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + background: linear-gradient(135deg, #4f46e5 0%, #3730a3 100%); min-height: 100vh; } @@ -39,7 +39,7 @@ body { background: white; padding: 40px; border-radius: 12px; - box-shadow: 0 10px 40px rgba(0,0,0,0.2); + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2); width: 100%; max-width: 400px; } @@ -142,7 +142,7 @@ body { .navbar { background: white; padding: 15px 30px; - box-shadow: 0 2px 10px rgba(0,0,0,0.1); + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); display: flex; justify-content: space-between; align-items: center; @@ -193,7 +193,7 @@ body { background: white; border-radius: 12px; padding: 20px; - box-shadow: 0 4px 20px rgba(0,0,0,0.1); + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); overflow-y: auto; } @@ -313,7 +313,7 @@ body { border-radius: 12px; padding: 30px; margin-top: 20px; - box-shadow: 0 4px 20px rgba(0,0,0,0.1); + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); } .admin-header { @@ -376,7 +376,7 @@ body { left: 0; width: 100%; height: 100%; - background: rgba(0,0,0,0.5); + background: rgba(0, 0, 0, 0.5); z-index: 1000; } @@ -422,7 +422,7 @@ body { max-width: 800px; margin-left: auto; margin-right: auto; - box-shadow: 0 4px 20px rgba(0,0,0,0.1); + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); } .step-indicator { @@ -519,7 +519,7 @@ body { border-color: var(--primary); } -.radio-item input[type="radio"]:checked + label { +.radio-item input[type="radio"]:checked+label { color: var(--primary); font-weight: 600; } @@ -540,6 +540,7 @@ body { .empty-state h3 { margin-bottom: 10px; } + /* Context Menu */ .context-menu { display: none; @@ -547,7 +548,7 @@ body { background: white; 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.15); z-index: 1000; min-width: 150px; } @@ -615,8 +616,13 @@ body { } @keyframes spin { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } } .btn:disabled { @@ -624,6 +630,7 @@ body { cursor: not-allowed; opacity: 0.6; } + /* Scenario Tree Styles */ .scenario-controls { margin: 20px 0; @@ -713,7 +720,9 @@ body { margin-top: 5px; } -.layer-checkbox, .stack-checkbox, .scenario-checkbox { +.layer-checkbox, +.stack-checkbox, +.scenario-checkbox { width: 16px; height: 16px; cursor: pointer; @@ -772,7 +781,7 @@ input[type="checkbox"]:indeterminate::before { #queue-log { border: 1px solid #333; - box-shadow: inset 0 2px 10px rgba(0,0,0,0.5); + box-shadow: inset 0 2px 10px rgba(0, 0, 0, 0.5); } #scenarioSearch:focus { @@ -783,4 +792,4 @@ input[type="checkbox"]:indeterminate::before { .status-waiting { background: #fef3c7; color: #92400e; -} +} \ No newline at end of file diff --git a/test_api.py b/test_api.py new file mode 100644 index 0000000..5e53a9a --- /dev/null +++ b/test_api.py @@ -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}")