From 1d22447429e2ee0725b62a48d35e9e2dd4619ba8 Mon Sep 17 00:00:00 2001 From: Keshav Dayal Date: Sun, 25 Jan 2026 14:14:32 +0530 Subject: [PATCH 1/5] feat: resolving issue #8 #9 and #10 --- backend/.tes_instances | 15 ++++---- frontend/src/pages/SubmitTask.js | 64 ++++++++++++++++++++++---------- frontend/src/pages/Utilities.js | 4 +- 3 files changed, 54 insertions(+), 29 deletions(-) diff --git a/backend/.tes_instances b/backend/.tes_instances index 2bfea85..ddc87ef 100644 --- a/backend/.tes_instances +++ b/backend/.tes_instances @@ -3,14 +3,13 @@ # Lines starting with # are comments # ELIXIR Cloud TES Instances -Funnel/OpenPBS @ ELIXIR-CZ,https://funnel.cloud.e-infra.cz/ -Funnel/Slurm @ ELIXIR-FI,https://vm4816.kaj.pouta.csc.fi/ -TESK/Kubernetes @ ELIXIR-CZ (Prod),https://tesk-prod.cloud.e-infra.cz/ -TESK/Kubernetes @ ELIXIR-CZ (NA),https://tesk-na.cloud.e-infra.cz/ -TESK/Kubernetes @ ELIXIR-DE,https://tesk.elixir-cloud.bi.denbi.de/ -TESK/Kubernetes @ ELIXIR-GR,https://tesk-eu.hypatia-comp.athenarc.gr/ -TESK/OpenShift @ ELIXIR-FI,https://csc-tesk-noauth.rahtiapp.fi/ -TESK North America,https://tesk-na.cloud.e-infra.cz/ +Funnel/OpenPBS @ ELIXIR-CZ (Not Working),https://funnel.cloud.e-infra.cz/ +Funnel/Slurm @ ELIXIR-FI (DNS resolution Failure),https://vm4816.kaj.pouta.csc.fi/ +TESK/Kubernetes @ ELIXIR-CZ (Prod) (Auth Failure),https://tesk-prod.cloud.e-infra.cz/ +TESK/Kubernetes @ ELIXIR-CZ (NA) (403 Forbidden),https://tesk-na.cloud.e-infra.cz/ +TESK/Kubernetes @ ELIXIR-DE (DNS resolution Failure),https://tesk.elixir-cloud.bi.denbi.de/ +TESK/Kubernetes @ ELIXIR-GR (Working),https://tesk-eu.hypatia-comp.athenarc.gr/ +TESK/OpenShift @ ELIXIR-FI (Working),https://csc-tesk-noauth.rahtiapp.fi/ # Local Development Local TES,http://localhost:8080 diff --git a/frontend/src/pages/SubmitTask.js b/frontend/src/pages/SubmitTask.js index 162cbf5..53560dd 100644 --- a/frontend/src/pages/SubmitTask.js +++ b/frontend/src/pages/SubmitTask.js @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import styled from 'styled-components'; import { testConnection } from '../services/api'; @@ -201,6 +201,22 @@ const SubmitTask = () => { refresh: refreshInstances } = useInstances(); + // Issue #8: bug: set TES Instance for Demo task to healthy instance + + useEffect(() => { + if (instances.length > 0 && !formData.tes_instance) { + const elixirFiInstance = instances.find( + instance => instance.url && instance.url.includes('csc-tesk-noauth.rahtiapp.fi') + ); + + if (elixirFiInstance) { + setFormData(prev => ({ ...prev, tes_instance: elixirFiInstance.url })); + } else { + setFormData(prev => ({ ...prev, tes_instance: instances[0].url })); + } + } + }, [instances]); + const handleTestConnection = async () => { try { setTestingConnection(true); @@ -287,15 +303,16 @@ const SubmitTask = () => { const handleRunDemo = (demoType = 'basic') => { const demoData = getDemoTaskData(demoType); setFormData(demoData); - setError(null); - - const taskNames = { - basic: 'Basic Hello World', - python: 'Python Script', - fileops: 'File Operations' - }; + setError(null); + + // Issue #9: remove demo task pop-ups upon form population + // const taskNames = { + // basic: 'Basic Hello World', + // python: 'Python Script', + // fileops: 'File Operations' + // }; - alert(`${taskNames[demoType] || 'Demo'} task data loaded! Review the form and click "Submit Task" when ready.`); + // alert(`${taskNames[demoType] || 'Demo'} task data loaded! Review the form and click "Submit Task" when ready.`); }; const handleSubmit = async (e) => { @@ -330,11 +347,11 @@ const SubmitTask = () => { console.log('Task submission result:', result); - if (result && result.message) { - alert(`Success: ${result.message}`); - } else { - alert('Task submitted successfully!'); - } + // if (result && result.message) { + // alert(`Success: ${result.message}`); + // } else { + // alert('Task submitted successfully!'); + // } navigate('/tasks'); } catch (err) { console.error('Task submission error:', err); @@ -440,11 +457,20 @@ const SubmitTask = () => { required > - {instances.map((instance, index) => ( - - ))} + {instances + .sort((a, b) => { + // Prioritize ELIXIR-Fi instance at the top + const aIsElixirFi = a.url && a.url.includes('csc-tesk-noauth.rahtiapp.fi'); + const bIsElixirFi = b.url && b.url.includes('csc-tesk-noauth.rahtiapp.fi'); + if (aIsElixirFi && !bIsElixirFi) return -1; + if (!aIsElixirFi && bIsElixirFi) return 1; + return 0; + }) + .map((instance, index) => ( + + ))} diff --git a/frontend/src/pages/Utilities.js b/frontend/src/pages/Utilities.js index 52824bf..977bb3b 100644 --- a/frontend/src/pages/Utilities.js +++ b/frontend/src/pages/Utilities.js @@ -406,7 +406,7 @@ const Utilities = () => { )); const serviceInfo = response.data; - let successMessage = `✅ Connection Test Successful!\n\nInstance: ${instanceName}\nResponse Time: ${responseTime}ms`; + let successMessage = `Connection Test Successful!\n\nInstance: ${instanceName}\nResponse Time: ${responseTime}ms`; if (serviceInfo && serviceInfo.name) { successMessage += `\nService Name: ${serviceInfo.name}`; @@ -417,7 +417,7 @@ const Utilities = () => { alert(successMessage); } catch (error) { - let errorMessage = `❌ Connection Test Failed\n\nInstance: ${instanceName}\nURL: ${url}\n\n`; + let errorMessage = `Connection Test Failed\n\nInstance: ${instanceName}\nURL: ${url}\n\n`; let errorReason = ''; let errorCode = ''; let errorType = ''; From 993e9b1da8614c55431e16741e8d7c29c76cd976 Mon Sep 17 00:00:00 2001 From: Keshav Dayal Date: Sun, 25 Jan 2026 14:31:25 +0530 Subject: [PATCH 2/5] feat: suggested changes --- frontend/src/pages/SubmitTask.js | 31 ++++++++----------------------- 1 file changed, 8 insertions(+), 23 deletions(-) diff --git a/frontend/src/pages/SubmitTask.js b/frontend/src/pages/SubmitTask.js index 53560dd..01f7686 100644 --- a/frontend/src/pages/SubmitTask.js +++ b/frontend/src/pages/SubmitTask.js @@ -8,6 +8,8 @@ import ErrorMessage from '../components/common/ErrorMessage'; import useInstances from '../hooks/useInstances'; import { ArrowLeft, Play, Zap, RefreshCw } from 'lucide-react'; +const ELIXIR_FI_INSTANCE_SUBSTRING = 'csc-tesk-noauth.rahtiapp.fi'; + const PageContainer = styled.div` padding: 20px; background-color: #f8f9fa; @@ -201,12 +203,10 @@ const SubmitTask = () => { refresh: refreshInstances } = useInstances(); - // Issue #8: bug: set TES Instance for Demo task to healthy instance - useEffect(() => { if (instances.length > 0 && !formData.tes_instance) { const elixirFiInstance = instances.find( - instance => instance.url && instance.url.includes('csc-tesk-noauth.rahtiapp.fi') + instance => instance.url && instance.url.includes(ELIXIR_FI_INSTANCE_SUBSTRING) ); if (elixirFiInstance) { @@ -215,7 +215,7 @@ const SubmitTask = () => { setFormData(prev => ({ ...prev, tes_instance: instances[0].url })); } } - }, [instances]); + }, [instances, formData.tes_instance]); const handleTestConnection = async () => { try { @@ -304,15 +304,6 @@ const SubmitTask = () => { const demoData = getDemoTaskData(demoType); setFormData(demoData); setError(null); - - // Issue #9: remove demo task pop-ups upon form population - // const taskNames = { - // basic: 'Basic Hello World', - // python: 'Python Script', - // fileops: 'File Operations' - // }; - - // alert(`${taskNames[demoType] || 'Demo'} task data loaded! Review the form and click "Submit Task" when ready.`); }; const handleSubmit = async (e) => { @@ -346,12 +337,7 @@ const SubmitTask = () => { const result = await taskService.submitTask(submitData); console.log('Task submission result:', result); - - // if (result && result.message) { - // alert(`Success: ${result.message}`); - // } else { - // alert('Task submitted successfully!'); - // } + navigate('/tasks'); } catch (err) { console.error('Task submission error:', err); @@ -457,11 +443,10 @@ const SubmitTask = () => { required > - {instances + {[...instances] .sort((a, b) => { - // Prioritize ELIXIR-Fi instance at the top - const aIsElixirFi = a.url && a.url.includes('csc-tesk-noauth.rahtiapp.fi'); - const bIsElixirFi = b.url && b.url.includes('csc-tesk-noauth.rahtiapp.fi'); + const aIsElixirFi = a.url && a.url.includes(ELIXIR_FI_INSTANCE_SUBSTRING); + const bIsElixirFi = b.url && b.url.includes(ELIXIR_FI_INSTANCE_SUBSTRING); if (aIsElixirFi && !bIsElixirFi) return -1; if (!aIsElixirFi && bIsElixirFi) return 1; return 0; From a789ebdb4bd29165ead6ac62e2c431d935ed8b5c Mon Sep 17 00:00:00 2001 From: Keshav Dayal Date: Sun, 25 Jan 2026 14:41:30 +0530 Subject: [PATCH 3/5] feat: removing hard coded tes instance status --- backend/.tes_instances | 14 ++++----- backend/routes/instances.py | 21 +++++++++++++ frontend/src/hooks/useInstances.js | 8 ++++- frontend/src/pages/SubmitTask.js | 38 ++++++++++++++++++++---- frontend/src/services/instanceService.js | 38 ++++++++++++++++-------- 5 files changed, 93 insertions(+), 26 deletions(-) diff --git a/backend/.tes_instances b/backend/.tes_instances index ddc87ef..3396567 100644 --- a/backend/.tes_instances +++ b/backend/.tes_instances @@ -3,13 +3,13 @@ # Lines starting with # are comments # ELIXIR Cloud TES Instances -Funnel/OpenPBS @ ELIXIR-CZ (Not Working),https://funnel.cloud.e-infra.cz/ -Funnel/Slurm @ ELIXIR-FI (DNS resolution Failure),https://vm4816.kaj.pouta.csc.fi/ -TESK/Kubernetes @ ELIXIR-CZ (Prod) (Auth Failure),https://tesk-prod.cloud.e-infra.cz/ -TESK/Kubernetes @ ELIXIR-CZ (NA) (403 Forbidden),https://tesk-na.cloud.e-infra.cz/ -TESK/Kubernetes @ ELIXIR-DE (DNS resolution Failure),https://tesk.elixir-cloud.bi.denbi.de/ -TESK/Kubernetes @ ELIXIR-GR (Working),https://tesk-eu.hypatia-comp.athenarc.gr/ -TESK/OpenShift @ ELIXIR-FI (Working),https://csc-tesk-noauth.rahtiapp.fi/ +Funnel/OpenPBS @ ELIXIR-CZ,https://funnel.cloud.e-infra.cz/ +Funnel/Slurm @ ELIXIR-FI,https://vm4816.kaj.pouta.csc.fi/ +TESK/Kubernetes @ ELIXIR-CZ (Prod),https://tesk-prod.cloud.e-infra.cz/ +TESK/Kubernetes @ ELIXIR-CZ (NA),https://tesk-na.cloud.e-infra.cz/ +TESK/Kubernetes @ ELIXIR-DE,https://tesk.elixir-cloud.bi.denbi.de/ +TESK/Kubernetes @ ELIXIR-GR,https://tesk-eu.hypatia-comp.athenarc.gr/ +TESK/OpenShift @ ELIXIR-FI,https://csc-tesk-noauth.rahtiapp.fi/ # Local Development Local TES,http://localhost:8080 diff --git a/backend/routes/instances.py b/backend/routes/instances.py index 815c399..4b2247a 100644 --- a/backend/routes/instances.py +++ b/backend/routes/instances.py @@ -28,6 +28,27 @@ def get_healthy_instances_route(): print(f"❌ Error in get_healthy_instances: {str(e)}") return jsonify({'error': str(e)}), 500 +@instances_bp.route('/api/instances-with-status', methods=['GET']) +def get_instances_with_status(): + """Get all TES instances with their current health status""" + try: + from utils.tes_utils import load_tes_location_data + instances = load_tes_location_data() + + # Fetch status for all instances in parallel + with ThreadPoolExecutor(max_workers=8) as pool: + results = list(pool.map(fetch_tes_status, instances)) + + return jsonify({ + 'instances': results, + 'count': len(results), + 'last_updated': datetime.now(timezone.utc).isoformat() + }) + + except Exception as e: + print(f"Error in get_instances_with_status: {str(e)}") + return jsonify({'error': str(e)}), 500 + @instances_bp.route('/api/tes_locations', methods=['GET']) def tes_locations(): from utils.tes_utils import load_tes_location_data diff --git a/frontend/src/hooks/useInstances.js b/frontend/src/hooks/useInstances.js index ae9f8bd..de3d9d1 100644 --- a/frontend/src/hooks/useInstances.js +++ b/frontend/src/hooks/useInstances.js @@ -4,6 +4,7 @@ import instanceService from '../services/instanceService'; const useInstances = () => { const [state, setState] = useState({ instances: [], + allInstances: [], loading: true, error: null, lastUpdate: null @@ -15,7 +16,11 @@ const useInstances = () => { }; instanceService.addListener(handleUpdate); const initialState = instanceService.getHealthyInstances(); - setState(initialState); + const allInstancesState = instanceService.getAllInstancesWithStatus(); + setState({ + ...initialState, + allInstances: allInstancesState.instances + }); return () => { instanceService.removeListener(handleUpdate); }; @@ -26,6 +31,7 @@ const useInstances = () => { return { instances: state.instances, + allInstances: state.allInstances, loading: state.loading, error: state.error, lastUpdate: state.lastUpdate, diff --git a/frontend/src/pages/SubmitTask.js b/frontend/src/pages/SubmitTask.js index 01f7686..35871b7 100644 --- a/frontend/src/pages/SubmitTask.js +++ b/frontend/src/pages/SubmitTask.js @@ -198,11 +198,26 @@ const SubmitTask = () => { const { instances, + allInstances, loading: instancesLoading, error: instancesError, refresh: refreshInstances } = useInstances(); + // Helper function to get status badge + const getStatusBadge = (status) => { + switch(status) { + case 'healthy': + return '✓'; + case 'unhealthy': + return '✗'; + case 'unreachable': + return '⚠'; + default: + return '?'; + } + }; + useEffect(() => { if (instances.length > 0 && !formData.tes_instance) { const elixirFiInstance = instances.find( @@ -254,9 +269,14 @@ const SubmitTask = () => { }; const getDemoTaskData = (demoType = 'basic') => { - const defaultTesInstance = instances.length > 0 - ? instances[0].url - : 'https://csc-tesk-noauth.rahtiapp.fi/v1/tasks'; + let defaultTesInstance = 'https://csc-tesk-noauth.rahtiapp.fi/v1/tasks'; + + if (instances.length > 0) { + const elixirFiInstance = instances.find( + instance => instance.url && instance.url.includes(ELIXIR_FI_INSTANCE_SUBSTRING) + ); + defaultTesInstance = elixirFiInstance ? elixirFiInstance.url : instances[0].url; + } const demoTasks = { basic: { @@ -443,7 +463,8 @@ const SubmitTask = () => { required > - {[...instances] + {(allInstances.length > 0 ? allInstances : instances) + .slice() .sort((a, b) => { const aIsElixirFi = a.url && a.url.includes(ELIXIR_FI_INSTANCE_SUBSTRING); const bIsElixirFi = b.url && b.url.includes(ELIXIR_FI_INSTANCE_SUBSTRING); @@ -452,8 +473,13 @@ const SubmitTask = () => { return 0; }) .map((instance, index) => ( - ))} diff --git a/frontend/src/services/instanceService.js b/frontend/src/services/instanceService.js index 19b8dde..3f75910 100644 --- a/frontend/src/services/instanceService.js +++ b/frontend/src/services/instanceService.js @@ -3,6 +3,7 @@ import api from './api'; class InstanceService { constructor() { this.healthyInstances = []; + this.allInstancesWithStatus = []; this.loading = false; this.error = null; this.lastUpdate = null; @@ -24,6 +25,7 @@ class InstanceService { try { callback({ instances: this.healthyInstances, + allInstances: this.allInstancesWithStatus, loading: this.loading, error: this.error, lastUpdate: this.lastUpdate @@ -43,6 +45,15 @@ class InstanceService { }; } + getAllInstancesWithStatus() { + return { + instances: this.allInstancesWithStatus, + loading: this.loading, + error: this.error, + lastUpdate: this.lastUpdate + }; + } + async fetchHealthyInstances() { try { const isInitialLoad = this.healthyInstances.length === 0; @@ -50,24 +61,31 @@ class InstanceService { if (isInitialLoad) { this.loading = true; this.notifyListeners(); - console.log('🔄 Loading initial healthy TES instances...'); + console.log('🔄 Loading TES instances with status...'); } else { - console.log('🔄 Refreshing cached healthy TES instances...'); + console.log('🔄 Refreshing TES instances with status...'); } - const response = await api.get('/api/healthy-instances', { - timeout: 5000 + const response = await api.get('/api/instances-with-status', { + timeout: 10000 }); const data = response.data; - this.healthyInstances = data.instances || []; + const allInstances = data.instances || []; + + // Filter to only healthy instances, but keep status info + this.healthyInstances = allInstances.filter(inst => inst.status === 'healthy'); + + // Store all instances (including unhealthy) for dropdown display + this.allInstancesWithStatus = allInstances; + this.lastUpdate = data.last_updated ? new Date(data.last_updated) : new Date(); this.error = null; - console.log(`✅ Got ${this.healthyInstances.length} cached healthy TES instances (updated: ${this.lastUpdate.toLocaleTimeString()})`); + console.log(`✅ Got ${this.healthyInstances.length} healthy instances out of ${allInstances.length} total (updated: ${this.lastUpdate.toLocaleTimeString()})`); } catch (err) { - console.error('Error fetching healthy instances:', err); + console.error('Error fetching instances with status:', err); if (err.code === 'ECONNABORTED') { this.error = 'Connection timeout - using cached data'; @@ -84,11 +102,7 @@ class InstanceService { console.log(`Using ${this.healthyInstances.length} cached healthy instances during error`); } } finally { - if (this.healthyInstances.length === 0) { - this.loading = false; - } else { - this.loading = false; - } + this.loading = false; this.notifyListeners(); } } From cfccbaea318492234e809046f558c9d9d6b93a7e Mon Sep 17 00:00:00 2001 From: Keshav Dayal Date: Wed, 28 Jan 2026 00:39:15 +0530 Subject: [PATCH 4/5] feat: simplified status badges and removed the hardcoded dependencies --- frontend/src/pages/SubmitTask.js | 47 ++++++++++++-------------------- 1 file changed, 18 insertions(+), 29 deletions(-) diff --git a/frontend/src/pages/SubmitTask.js b/frontend/src/pages/SubmitTask.js index 35871b7..4c35d6e 100644 --- a/frontend/src/pages/SubmitTask.js +++ b/frontend/src/pages/SubmitTask.js @@ -8,8 +8,6 @@ import ErrorMessage from '../components/common/ErrorMessage'; import useInstances from '../hooks/useInstances'; import { ArrowLeft, Play, Zap, RefreshCw } from 'lucide-react'; -const ELIXIR_FI_INSTANCE_SUBSTRING = 'csc-tesk-noauth.rahtiapp.fi'; - const PageContainer = styled.div` padding: 20px; background-color: #f8f9fa; @@ -206,31 +204,23 @@ const SubmitTask = () => { // Helper function to get status badge const getStatusBadge = (status) => { - switch(status) { - case 'healthy': - return '✓'; - case 'unhealthy': - return '✗'; - case 'unreachable': - return '⚠'; - default: - return '?'; - } + return status === 'healthy' ? '✅' : '❌'; }; useEffect(() => { + // Auto-select first healthy instance if no instance is selected if (instances.length > 0 && !formData.tes_instance) { - const elixirFiInstance = instances.find( - instance => instance.url && instance.url.includes(ELIXIR_FI_INSTANCE_SUBSTRING) + // Try to find a healthy instance from allInstances (if available) + const healthyInstance = (allInstances.length > 0 ? allInstances : instances).find( + instance => instance.status === 'healthy' ); - if (elixirFiInstance) { - setFormData(prev => ({ ...prev, tes_instance: elixirFiInstance.url })); - } else { - setFormData(prev => ({ ...prev, tes_instance: instances[0].url })); + // Only set default if a healthy instance exists + if (healthyInstance) { + setFormData(prev => ({ ...prev, tes_instance: healthyInstance.url })); } } - }, [instances, formData.tes_instance]); + }, [instances, allInstances, formData.tes_instance]); const handleTestConnection = async () => { try { @@ -269,13 +259,15 @@ const SubmitTask = () => { }; const getDemoTaskData = (demoType = 'basic') => { - let defaultTesInstance = 'https://csc-tesk-noauth.rahtiapp.fi/v1/tasks'; + // Use first healthy instance as default for demos + let defaultTesInstance = ''; if (instances.length > 0) { - const elixirFiInstance = instances.find( - instance => instance.url && instance.url.includes(ELIXIR_FI_INSTANCE_SUBSTRING) + const healthyInstance = (allInstances.length > 0 ? allInstances : instances).find( + instance => instance.status === 'healthy' ); - defaultTesInstance = elixirFiInstance ? elixirFiInstance.url : instances[0].url; + // Only use an instance if it's healthy, otherwise leave empty + defaultTesInstance = healthyInstance ? healthyInstance.url : ''; } const demoTasks = { @@ -466,10 +458,9 @@ const SubmitTask = () => { {(allInstances.length > 0 ? allInstances : instances) .slice() .sort((a, b) => { - const aIsElixirFi = a.url && a.url.includes(ELIXIR_FI_INSTANCE_SUBSTRING); - const bIsElixirFi = b.url && b.url.includes(ELIXIR_FI_INSTANCE_SUBSTRING); - if (aIsElixirFi && !bIsElixirFi) return -1; - if (!aIsElixirFi && bIsElixirFi) return 1; + // Sort by status: healthy first, then others + if (a.status === 'healthy' && b.status !== 'healthy') return -1; + if (a.status !== 'healthy' && b.status === 'healthy') return 1; return 0; }) .map((instance, index) => ( @@ -478,8 +469,6 @@ const SubmitTask = () => { value={instance.url} > {getStatusBadge(instance.status)} {instance.name} - {instance.status === 'unhealthy' ? ' (Unhealthy)' : ''} - {instance.status === 'unreachable' ? ' (Unreachable)' : ''} ))} From a63c052717d1c0df5af0aaea807ef136247a14fe Mon Sep 17 00:00:00 2001 From: Keshav Dayal Date: Sat, 31 Jan 2026 16:48:28 +0530 Subject: [PATCH 5/5] feat: suggested changes by copilot are implemented --- backend/routes/tasks.py | 62 +++++++++++++++++++++++++++++--- backend/services/tes_service.py | 41 ++++++++++++++++++++- frontend/src/pages/SubmitTask.js | 34 +++++++++--------- 3 files changed, 116 insertions(+), 21 deletions(-) diff --git a/backend/routes/tasks.py b/backend/routes/tasks.py index ee5c77c..7c0af59 100644 --- a/backend/routes/tasks.py +++ b/backend/routes/tasks.py @@ -4,9 +4,13 @@ import json import time import requests +import shlex from services.task_service import get_submitted_tasks, add_task, update_single_task_status from utils.tes_utils import load_tes_instances from utils.auth_utils import get_instance_credentials +import logging + +logger = logging.getLogger(__name__) tasks_bp = Blueprint('tasks', __name__) @@ -36,9 +40,38 @@ def submit_task(): tes_name = inst['name'] break + + command_raw = data.get('command', '') + if isinstance(command_raw, list): + command = command_raw + elif command_raw: + + shell_operators = ['&&', '||', '|', '>', '<', '>>', '2>', '&', ';', '$(', '`'] + needs_shell = any(op in command_raw for op in shell_operators) + + if needs_shell: + + command = ["/bin/sh", "-c", command_raw] + print(f"🐚 Command contains shell operators, wrapping in shell: {command}") + else: + + try: + command = shlex.split(command_raw) + print(f"📝 Simple command parsed: {command}") + except ValueError as e: + logger.error(f"Command parsing error: {e}") + return jsonify({ + 'success': False, + 'error': f'Invalid command syntax: {str(e)}', + 'error_type': 'bad_request', + 'error_code': 'INVALID_COMMAND' + }), 400 + else: + command = ['echo', 'Hello World'] + executor = { "image": docker_image, - "command": data.get('command') if isinstance(data.get('command'), list) else (data.get('command', '').split() if data.get('command') else ['echo', 'Hello World']), + "command": command, "workdir": data.get('workdir', '/tmp') } @@ -100,13 +133,24 @@ def submit_task(): try: print(f" Trying service-info: {service_info_url}") - test_response = requests.get(service_info_url, timeout=10) - if test_response.status_code in [200, 403]: + test_response = requests.get(service_info_url, timeout=10) + + if test_response.status_code == 200: service_is_reachable = True working_endpoint = tasks_url print(f" ✅ Service reachable at {service_info_url} (status {test_response.status_code})") print(f" Will use tasks endpoint: {tasks_url}") break + elif test_response.status_code in [401, 403]: + # Authentication required - treat as unreachable since we can't use it + print(f" 🔐 Authentication required at {service_info_url} (status {test_response.status_code})") + connectivity_error_info = { + 'error_type': 'unauthorized', + 'error_code': 'UNAUTHORIZED', + 'message': 'Authentication required - TES instance requires credentials', + 'reason': 'This TES instance requires authentication. Please configure TESK_PROD_TOKEN or credentials in environment variables.' + } + continue else: print(f" ⚠️ Service returned status {test_response.status_code}") @@ -199,6 +243,7 @@ def submit_task(): tes_endpoint = working_endpoint print(f"🚀 Submitting task to {tes_endpoint}") + print(f"📦 Task payload: {json.dumps(tes_task, indent=2)}") credentials = get_instance_credentials(tes_name, tes_url) headers = { @@ -301,7 +346,7 @@ def submit_task(): else: error_type_map = { 400: {'error_type': 'bad_request', 'error_code': 'BAD_REQUEST', 'reason': 'The task specification is invalid or malformed'}, - 401: {'error_type': 'unauthorized', 'error_code': 'UNAUTHORIZED', 'reason': 'Authentication required or credentials are invalid'}, + 401: {'error_type': 'unauthorized', 'error_code': 'UNAUTHORIZED', 'reason': 'Authentication required. Please configure TESK_PROD_TOKEN or TESK_PROD_USER/TESK_PROD_PASSWORD environment variables for this instance.'}, 403: {'error_type': 'forbidden', 'error_code': 'FORBIDDEN', 'reason': 'You do not have permission to submit tasks to this instance'}, 404: {'error_type': 'not_found', 'error_code': 'NOT_FOUND', 'reason': 'The TES endpoint was not found. Check if the URL is correct.'}, 408: {'error_type': 'timeout', 'error_code': 'TIMEOUT', 'reason': 'The request timed out. The TES instance may be overloaded.'}, @@ -319,12 +364,21 @@ def submit_task(): }) error_msg = f'TES submission failed with status {response.status_code}' + print(f"❌ TES returned status {response.status_code}") + print(f"Response headers: {dict(response.headers)}") + print(f"Response body: {response.text[:500]}") + try: error_data = response.json() + print(f"Error data JSON: {error_data}") if error_data.get('message'): error_msg = error_data.get('message') elif error_data.get('error'): error_msg = error_data.get('error') + elif error_data.get('detail'): + error_msg = error_data.get('detail') + elif error_data.get('title'): + error_msg = error_data.get('title') else: error_msg = f'{error_msg}: {str(error_data)}' except: diff --git a/backend/services/tes_service.py b/backend/services/tes_service.py index c65f2a3..2f656c6 100644 --- a/backend/services/tes_service.py +++ b/backend/services/tes_service.py @@ -28,7 +28,46 @@ def fetch_tes_status(instance): r = requests.get(f"{tes_base_url}/ga4gh/tes/v1/service-info", timeout=5) latency_ms = int((time.time() - start_time) * 1000) - status = "healthy" if r.status_code == 200 else "unhealthy" + # First check service-info endpoint + if r.status_code in [401, 403]: + status = "unhealthy" # Authentication required but not available + elif r.status_code != 200: + status = "unhealthy" + else: + # Service-info is accessible, but we need to check if tasks endpoint is usable + # Try a HEAD/OPTIONS request to tasks endpoint to see if it requires auth + try: + instance_name = instance.get("name", "") + credentials = get_instance_credentials(instance_name, tes_base_url) + + # Test tasks endpoint with credentials (if available) + headers = {'Accept': 'application/json'} + auth = None + if credentials.get('token'): + headers['Authorization'] = f"Bearer {credentials['token']}" + elif credentials.get('user') and credentials.get('password'): + auth = (credentials['user'], credentials['password']) + + # Try to list tasks (with view=MINIMAL to reduce payload) + tasks_response = requests.get( + f"{tes_base_url}/ga4gh/tes/v1/tasks?view=MINIMAL", + headers=headers, + auth=auth, + timeout=5 + ) + + # If tasks endpoint returns 401/403, mark as unhealthy (auth required but not configured) + if tasks_response.status_code in [401, 403]: + print(f"⚠️ {instance.get('name')} tasks endpoint requires authentication (status {tasks_response.status_code})") + status = "unhealthy" + else: + # Tasks endpoint is accessible (200) or returns other non-auth error + status = "healthy" + except Exception as tasks_error: + print(f"⚠️ Could not check tasks endpoint for {instance.get('name')}: {tasks_error}") + # If we can't check tasks endpoint, assume healthy based on service-info + status = "healthy" + version = "" try: version = r.json().get("version", "") diff --git a/frontend/src/pages/SubmitTask.js b/frontend/src/pages/SubmitTask.js index 4c35d6e..c793ffe 100644 --- a/frontend/src/pages/SubmitTask.js +++ b/frontend/src/pages/SubmitTask.js @@ -220,7 +220,8 @@ const SubmitTask = () => { setFormData(prev => ({ ...prev, tes_instance: healthyInstance.url })); } } - }, [instances, allInstances, formData.tes_instance]); + + }, [instances, allInstances]); const handleTestConnection = async () => { try { @@ -274,38 +275,38 @@ const SubmitTask = () => { basic: { tes_instance: defaultTesInstance, task_name: 'Demo Hello World Task', - docker_image: 'ubuntu:20.04', - command: 'echo "Hello from TES Demo Task!" && echo "Current time: $(date)" && echo "System info: $(uname -a)" && echo "Task completed successfully"', + docker_image: 'alpine:latest', + command: 'echo "Hello from TES!" && date && uname -a', input_url: '', output_url: '', cpu_cores: '1', ram_gb: '1', - disk_gb: '5', - description: 'A simple demo task that prints system information and a hello message. Safe to run and completes quickly for testing purposes.' + disk_gb: '1', + description: 'Simple demo task using Alpine Linux (5MB). Note: If you see SYSTEM_ERROR, the TES instance may be experiencing infrastructure issues. Try a different instance or wait a few minutes.' }, python: { tes_instance: defaultTesInstance, task_name: 'Demo Python Script Task', - docker_image: 'python:3.9-slim', - command: 'python3 -c "import sys; import datetime; print(f\'Hello from Python {sys.version}\'); print(f\'Current time: {datetime.datetime.now()}\'); print(\'Demo task completed successfully!\')"', + docker_image: 'python:3.11-alpine', + command: 'python3 -c "import sys; import datetime; print(sys.version); print(datetime.datetime.now())"', input_url: '', output_url: '', cpu_cores: '1', ram_gb: '1', - disk_gb: '5', - description: 'A Python demo task that prints version info and timestamp using a Python container.' + disk_gb: '1', + description: 'Python demo using Alpine-based image (51MB). Faster than standard Python images.' }, fileops: { tes_instance: defaultTesInstance, task_name: 'Demo File Operations Task', - docker_image: 'ubuntu:20.04', - command: 'echo "Creating demo files..." && echo "Hello World" > /tmp/output.txt && echo "File contents:" && cat /tmp/output.txt && ls -la /tmp/', + docker_image: 'alpine:latest', + command: 'echo "Hello World" > /tmp/demo.txt && cat /tmp/demo.txt && ls -lh /tmp/demo.txt', input_url: '', output_url: '', cpu_cores: '1', ram_gb: '1', - disk_gb: '5', - description: 'A demo task that creates and reads files, demonstrating basic file operations within the container.' + disk_gb: '1', + description: 'Demonstrates file creation and reading in Alpine Linux.' } }; @@ -458,14 +459,15 @@ const SubmitTask = () => { {(allInstances.length > 0 ? allInstances : instances) .slice() .sort((a, b) => { - // Sort by status: healthy first, then others + // Sort by status: healthy (reachable without auth) first, then others + // Note: Backend marks instances requiring auth as 'unhealthy' if (a.status === 'healthy' && b.status !== 'healthy') return -1; if (a.status !== 'healthy' && b.status === 'healthy') return 1; return 0; }) - .map((instance, index) => ( + .map((instance) => (