update upload scripts
This commit is contained in:
@@ -32,11 +32,13 @@ def create_app():
|
|||||||
from app.routes.admin import admin_bp
|
from app.routes.admin import admin_bp
|
||||||
from app.routes.dashboard import dashboard_bp
|
from app.routes.dashboard import dashboard_bp
|
||||||
from app.routes.jobs import jobs_bp
|
from app.routes.jobs import jobs_bp
|
||||||
|
from app.routes.api import api_bp
|
||||||
|
|
||||||
app.register_blueprint(auth_bp)
|
app.register_blueprint(auth_bp)
|
||||||
app.register_blueprint(admin_bp)
|
app.register_blueprint(admin_bp)
|
||||||
app.register_blueprint(dashboard_bp)
|
app.register_blueprint(dashboard_bp)
|
||||||
app.register_blueprint(jobs_bp)
|
app.register_blueprint(jobs_bp)
|
||||||
|
app.register_blueprint(api_bp)
|
||||||
|
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
db.create_all()
|
db.create_all()
|
||||||
|
|||||||
120
app/routes/api.py
Normal file
120
app/routes/api.py
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
from flask import Blueprint, request, jsonify
|
||||||
|
from app.models import User, Job
|
||||||
|
from app import db
|
||||||
|
import json
|
||||||
|
import requests
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
api_bp = Blueprint('api', __name__, url_prefix='/api')
|
||||||
|
|
||||||
|
@api_bp.route('/submit_job', methods=['POST'])
|
||||||
|
def submit_job():
|
||||||
|
data = request.get_json()
|
||||||
|
|
||||||
|
if not data:
|
||||||
|
return jsonify({'error': 'No JSON data provided'}), 400
|
||||||
|
|
||||||
|
username = data.get('username')
|
||||||
|
password = data.get('password')
|
||||||
|
branch_name = data.get('branch_name')
|
||||||
|
scenarios = data.get('scenarios')
|
||||||
|
|
||||||
|
# Validation
|
||||||
|
if not all([username, password, branch_name, scenarios]):
|
||||||
|
return jsonify({'error': 'Missing required fields: username, password, branch_name, scenarios'}), 400
|
||||||
|
|
||||||
|
if not isinstance(scenarios, list) or not scenarios:
|
||||||
|
return jsonify({'error': 'Scenarios must be a non-empty list'}), 400
|
||||||
|
|
||||||
|
# Authentication
|
||||||
|
user = User.query.filter_by(username=username).first()
|
||||||
|
if not user or not user.check_password(password):
|
||||||
|
return jsonify({'error': 'Invalid credentials'}), 401
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Create Job
|
||||||
|
job = Job(
|
||||||
|
user_id=user.id,
|
||||||
|
branch_name=branch_name,
|
||||||
|
scenarios=json.dumps(scenarios),
|
||||||
|
environment='staging', # Default
|
||||||
|
test_mode='simulator', # Default
|
||||||
|
status='waiting'
|
||||||
|
)
|
||||||
|
db.session.add(job)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# Prepare Remote Trigger
|
||||||
|
remote_queue_id = str(job.id)
|
||||||
|
remote_task_ids = {s: f"{job.id}_{i+1}" for i, s in enumerate(scenarios)}
|
||||||
|
|
||||||
|
job.remote_queue_id = remote_queue_id
|
||||||
|
job.remote_task_ids = json.dumps(remote_task_ids)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# Payload for Remote Queue
|
||||||
|
# Note: We don't have the scenario map here, so we assume scenario path is same as name or not needed if remote handles it.
|
||||||
|
# However, the existing logic uses a map. If the user provides just names, we might send names as paths.
|
||||||
|
# Let's assume for this API the user knows what they are doing or the remote accepts names.
|
||||||
|
# Based on jobs.py: {remote_task_ids[s]: scenario_map.get(s, s) for s in scenarios}
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"source": branch_name,
|
||||||
|
remote_queue_id: [
|
||||||
|
'staging',
|
||||||
|
{remote_task_ids[s]: s for s in scenarios} # Use scenario name as path/value
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Trigger Remote Queue
|
||||||
|
remote_url = "http://asf-server.duckdns.org:8080/api/queue"
|
||||||
|
try:
|
||||||
|
response = requests.post(remote_url, json=payload, timeout=10)
|
||||||
|
response.raise_for_status()
|
||||||
|
remote_triggered = True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ERROR] Failed to trigger remote queue from API: {e}")
|
||||||
|
remote_triggered = False
|
||||||
|
job.queue_log = f"[SYSTEM] Failed to trigger remote queue: {str(e)}"
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'job_id': job.id,
|
||||||
|
'status': job.status,
|
||||||
|
'remote_triggered': remote_triggered,
|
||||||
|
'message': 'Job submitted successfully'
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'error': f'Internal server error: {str(e)}'}), 500
|
||||||
|
|
||||||
|
@api_bp.route('/job/<int:job_id>', methods=['GET'])
|
||||||
|
def get_job_status(job_id):
|
||||||
|
try:
|
||||||
|
job = Job.query.get(job_id)
|
||||||
|
if not job:
|
||||||
|
return jsonify({'error': 'Job not found'}), 404
|
||||||
|
|
||||||
|
# Optional: Trigger internal status update to get latest data
|
||||||
|
# We need to import this function. It's in jobs.py but circular imports might be tricky.
|
||||||
|
# For now, let's rely on the background poller or just return what we have.
|
||||||
|
# If we really need it, we can import inside the function.
|
||||||
|
try:
|
||||||
|
from app.routes.jobs import update_job_status_internal
|
||||||
|
update_job_status_internal(job)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[WARNING] Failed to trigger internal status update: {e}")
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'job_id': job.id,
|
||||||
|
'status': job.status,
|
||||||
|
'branch_name': job.branch_name,
|
||||||
|
'scenarios': json.loads(job.scenarios) if job.scenarios else [],
|
||||||
|
'remote_results': json.loads(job.remote_results) if job.remote_results else {},
|
||||||
|
'created_at': job.submitted_at.isoformat() if job.submitted_at else None,
|
||||||
|
'completed_at': job.completed_at.isoformat() if job.completed_at else None,
|
||||||
|
'remote_queue_id': job.remote_queue_id
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'error': f'Internal server error: {str(e)}'}), 500
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
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;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ body {
|
|||||||
background: white;
|
background: white;
|
||||||
padding: 40px;
|
padding: 40px;
|
||||||
border-radius: 12px;
|
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%;
|
width: 100%;
|
||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
}
|
}
|
||||||
@@ -142,7 +142,7 @@ body {
|
|||||||
.navbar {
|
.navbar {
|
||||||
background: white;
|
background: white;
|
||||||
padding: 15px 30px;
|
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;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -193,7 +193,7 @@ body {
|
|||||||
background: white;
|
background: white;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 20px;
|
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;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -313,7 +313,7 @@ body {
|
|||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 30px;
|
padding: 30px;
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-header {
|
.admin-header {
|
||||||
@@ -376,7 +376,7 @@ body {
|
|||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: rgba(0,0,0,0.5);
|
background: rgba(0, 0, 0, 0.5);
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -422,7 +422,7 @@ body {
|
|||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.step-indicator {
|
.step-indicator {
|
||||||
@@ -519,7 +519,7 @@ body {
|
|||||||
border-color: var(--primary);
|
border-color: var(--primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.radio-item input[type="radio"]:checked + label {
|
.radio-item input[type="radio"]:checked+label {
|
||||||
color: var(--primary);
|
color: var(--primary);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
@@ -540,6 +540,7 @@ body {
|
|||||||
.empty-state h3 {
|
.empty-state h3 {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Context Menu */
|
/* Context Menu */
|
||||||
.context-menu {
|
.context-menu {
|
||||||
display: none;
|
display: none;
|
||||||
@@ -547,7 +548,7 @@ body {
|
|||||||
background: white;
|
background: white;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
min-width: 150px;
|
min-width: 150px;
|
||||||
}
|
}
|
||||||
@@ -615,8 +616,13 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
0% { transform: rotate(0deg); }
|
0% {
|
||||||
100% { transform: rotate(360deg); }
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn:disabled {
|
.btn:disabled {
|
||||||
@@ -624,6 +630,7 @@ body {
|
|||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Scenario Tree Styles */
|
/* Scenario Tree Styles */
|
||||||
.scenario-controls {
|
.scenario-controls {
|
||||||
margin: 20px 0;
|
margin: 20px 0;
|
||||||
@@ -713,7 +720,9 @@ body {
|
|||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.layer-checkbox, .stack-checkbox, .scenario-checkbox {
|
.layer-checkbox,
|
||||||
|
.stack-checkbox,
|
||||||
|
.scenario-checkbox {
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -772,7 +781,7 @@ input[type="checkbox"]:indeterminate::before {
|
|||||||
|
|
||||||
#queue-log {
|
#queue-log {
|
||||||
border: 1px solid #333;
|
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 {
|
#scenarioSearch:focus {
|
||||||
|
|||||||
40
test_api.py
Normal file
40
test_api.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import requests
|
||||||
|
import json
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
BASE_URL = "http://localhost:5000" # Adjust if running on a different port
|
||||||
|
API_URL = f"{BASE_URL}/api/submit_job"
|
||||||
|
|
||||||
|
# Test Data
|
||||||
|
payload = {
|
||||||
|
"username": "admin",
|
||||||
|
"password": "admin123", # Default password from __init__.py
|
||||||
|
"branch_name": "test_branch",
|
||||||
|
"scenarios": ["scenario1", "scenario2"]
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
print(f"Sending POST request to {API_URL}...")
|
||||||
|
response = requests.post(API_URL, json=payload)
|
||||||
|
|
||||||
|
print(f"Status Code: {response.status_code}")
|
||||||
|
print("Response JSON:")
|
||||||
|
print(json.dumps(response.json(), indent=2))
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
print("\nSUCCESS: Job submitted successfully.")
|
||||||
|
job_id = response.json().get('job_id')
|
||||||
|
|
||||||
|
# Test Status API
|
||||||
|
if job_id:
|
||||||
|
STATUS_URL = f"{BASE_URL}/api/job/{job_id}"
|
||||||
|
print(f"\nTesting Status API: {STATUS_URL}")
|
||||||
|
status_resp = requests.get(STATUS_URL)
|
||||||
|
print(f"Status Code: {status_resp.status_code}")
|
||||||
|
print("Status Response:")
|
||||||
|
print(json.dumps(status_resp.json(), indent=2))
|
||||||
|
else:
|
||||||
|
print("\nFAILURE: Job submission failed.")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\nERROR: Could not connect to server. Is it running? {e}")
|
||||||
Reference in New Issue
Block a user