From 6b254534f6d8c2d7bd17ba8b0b3e390ccce24d01 Mon Sep 17 00:00:00 2001 From: mahmamdouh Date: Sun, 28 Dec 2025 05:52:49 +0100 Subject: [PATCH] next_updates --- app/models.py | 13 +- app/routes/jobs.py | 186 ++++++++---- app/static/css/style.css | 36 ++- app/templates/dashboard/index.html | 310 ++++++++++++-------- app/templates/jobs/submit.html | 291 +++++++++--------- app/templates/jobs/submit_review.html | 57 ++++ app/templates/jobs/submit_step2.html | 405 +++++++++++++------------- requirements.txt | 1 + 8 files changed, 766 insertions(+), 533 deletions(-) create mode 100644 app/templates/jobs/submit_review.html diff --git a/app/models.py b/app/models.py index e4d2887..a206e1d 100644 --- a/app/models.py +++ b/app/models.py @@ -37,11 +37,18 @@ 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': '🔴', + 'waiting': '⌛', + 'in_progress': '🔄', + 'passed': '✅', + 'failed': '❌', 'aborted': '⚫' } return icons.get(self.status, '⚪') diff --git a/app/routes/jobs.py b/app/routes/jobs.py index a063213..cbd1fd8 100644 --- a/app/routes/jobs.py +++ b/app/routes/jobs.py @@ -5,14 +5,15 @@ from app import db import json import subprocess import os +import requests +import random +import string + +def generate_remote_id(length=6): + return ''.join(random.choices(string.digits, k=length)) jobs_bp = Blueprint('jobs', __name__, url_prefix='/jobs') -@jobs_bp.route('/submit') -@login_required -def submit(): - return render_template('jobs/submit.html') - @jobs_bp.route('/debug/test-step2') @login_required def debug_test_step2(): @@ -270,7 +271,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) @@ -318,50 +319,14 @@ 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 [] @@ -370,22 +335,44 @@ def submit_final(): flash('Invalid scenario data', 'error') return redirect(url_for('jobs.submit')) + # Generate Remote IDs + remote_queue_id = generate_remote_id() + remote_task_ids = {s: generate_remote_id() for s in scenarios} + + # Prepare Remote Payload + # Format: {"source": "branch", "queue_id": ["staging", {"task_id": "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') + 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', # Start with waiting (sand watch) + remote_queue_id=remote_queue_id, + remote_task_ids=json.dumps(remote_task_ids) ) db.session.add(job) db.session.commit() - # TODO: Start test execution in background using scenario_map - flash('Test job submitted successfully', 'success') return redirect(url_for('dashboard.index')) @@ -409,23 +396,96 @@ 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('//abort', methods=['POST']) @login_required def abort_job(job_id): + return jsonify({'error': 'Abort functionality is not implemented yet'}), 400 + +@jobs_bp.route('//status') +@login_required +def get_job_status(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 - - if job.status == 'in_progress': - job.status = 'aborted' - db.session.commit() - # TODO: Kill the running process - return jsonify({'success': True}) - - return jsonify({'error': 'Job is not in progress'}), 400 + if job.status not in ['passed', 'failed', 'aborted']: + # Poll remote API + try: + # 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' + + # 2. Fetch Queue Logs if running + if remote_status == 'running': + 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 + + 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() + # User says: {"SCENARIO_NAME": ["PASS/FAIL", "link"]} + # We need to find the key that matches our scenario (case insensitive maybe?) + for key, val in r_data.items(): + if key.upper() == scenario.upper(): + results[scenario] = val + if val[0] == 'FAIL': + any_failed = True + break + else: + all_finished = False + else: + all_finished = False + else: + all_finished = False + else: + if results[scenario][0] == 'FAIL': + any_failed = True + + if all_finished and task_ids: + job.status = 'failed' if any_failed else 'passed' + job.completed_at = db.session.query(db.func.now()).scalar() + + job.remote_results = json.dumps(results) + db.session.commit() + + except Exception as e: + print(f"[ERROR] Status polling failed: {e}") + + return jsonify({ + 'status': job.status, + 'status_icon': job.get_status_icon(), + 'remote_results': job.remote_results, + 'queue_log': job.queue_log + }) diff --git a/app/static/css/style.css b/app/static/css/style.css index f8c5ed6..50ce4ca 100644 --- a/app/static/css/style.css +++ b/app/static/css/style.css @@ -623,8 +623,8 @@ body { background: #9ca3af; cursor: not-allowed; opacity: 0.6; -}/ -* Scenario Tree Styles */ +} +/* Scenario Tree Styles */ .scenario-controls { margin: 20px 0; padding: 15px; @@ -753,4 +753,34 @@ input[type="checkbox"]:indeterminate::before { #selectionCount { color: var(--primary); font-size: 14px; -} \ No newline at end of file +} + +/* Scenario Table & Log Styles */ +#scenarioTable { + margin-top: 10px; + font-size: 13px; +} + +#scenarioTable th { + padding: 10px; + background: #f8fafc; +} + +#scenarioTable td { + padding: 10px; +} + +#queue-log { + border: 1px solid #333; + box-shadow: inset 0 2px 10px rgba(0,0,0,0.5); +} + +#scenarioSearch:focus { + border-color: var(--primary); + box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1); +} + +.status-waiting { + background: #fef3c7; + color: #92400e; +} diff --git a/app/templates/dashboard/index.html b/app/templates/dashboard/index.html index 8db87c1..952d585 100644 --- a/app/templates/dashboard/index.html +++ b/app/templates/dashboard/index.html @@ -9,32 +9,33 @@

Test Jobs

+ New Job - +
{% if jobs %} - {% for job in jobs %} -
-
{{ job.get_status_icon() }}
-
-

Job #{{ job.id }} - {{ job.branch_name }}

-

{{ job.submitted_at.strftime('%Y-%m-%d %H:%M') }} by {{ job.submitter.username }}

-
+ {% for job in jobs %} +
+
{{ job.get_status_icon() }}
+
+

Job #{{ job.id }} - {{ job.branch_name }}

+

{{ job.submitted_at.strftime('%Y-%m-%d %H:%M') }} by {{ job.submitter.username }}

- {% endfor %} +
+ {% endfor %} {% else %} -
-

No jobs yet

-

Submit your first test job to get started

-
+
+

No jobs yet

+

Submit your first test job to get started

+
{% endif %}
- +

Job Details

- +

Select a job

@@ -53,61 +54,90 @@
-{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/app/templates/jobs/submit.html b/app/templates/jobs/submit.html index a9924e6..dd6b7d8 100644 --- a/app/templates/jobs/submit.html +++ b/app/templates/jobs/submit.html @@ -5,7 +5,7 @@ {% block content %}

Submit New Test Job

- +
1
@@ -17,181 +17,174 @@
3
-
Environment
-
-
-
4
-
Test Mode
-
-
-
5
Review
- +
- + Enter the branch name to analyze available test scenarios - +
Validating branch...
- +
Cancel - +
-{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/app/templates/jobs/submit_review.html b/app/templates/jobs/submit_review.html new file mode 100644 index 0000000..dc149a4 --- /dev/null +++ b/app/templates/jobs/submit_review.html @@ -0,0 +1,57 @@ +{% extends "base.html" %} + +{% block title %}Review & Submit - ASF TestArena{% endblock %} + +{% block content %} +
+

Review & Submit

+ +
+
+
+
Branch
+
+
+
+
Scenarios
+
+
+
3
+
Review
+
+
+ +
+
+
Branch:
+
{{ branch_name }}
+
+
+
Scenarios:
+
+ {{ scenarios|length }} scenarios selected +
    + {% for scenario in scenarios %} +
  • {{ scenario }}
  • + {% endfor %} +
+
+
+
+ +
+ + + + + + + + +
+ + +
+
+
+{% endblock %} diff --git a/app/templates/jobs/submit_step2.html b/app/templates/jobs/submit_step2.html index 029a891..9c940d8 100644 --- a/app/templates/jobs/submit_step2.html +++ b/app/templates/jobs/submit_step2.html @@ -5,7 +5,7 @@ {% block content %}

Select Test Scenarios

- +
@@ -17,95 +17,98 @@
3
-
Environment
-
-
-
4
-
Test Mode
-
-
-
5
Review
- +
Branch: {{ branch_name }}
- +
- +
- - + + 0 scenarios selected
- +
{% if organized_data %} - {% for layer_name, layer_data in organized_data.items() %} -
-
- - - -
- -