From 2d5cee3f5c4eecc27c2581f5c4be88ac5b20da6c Mon Sep 17 00:00:00 2001 From: adlsdztony Date: Thu, 5 Jun 2025 03:41:43 +0000 Subject: [PATCH] feat&fix: add brief task status retrieval and improve task status update mechanism --- monitor/main.py | 145 ++++++++++++++++++++++++++++++++++++++++ monitor/static/index.js | 123 ++++++++++++++++++++++++++++++++-- 2 files changed, 261 insertions(+), 7 deletions(-) diff --git a/monitor/main.py b/monitor/main.py index f38294b..395748f 100644 --- a/monitor/main.py +++ b/monitor/main.py @@ -151,6 +151,118 @@ def get_task_status(task_type, task_id): "result": result_content } +def get_task_status_brief(task_type, task_id): + """ + Get brief status info for a task, without detailed step data, for fast homepage loading. + """ + result_dir = os.path.join(RESULTS_BASE_PATH, task_type, task_id) + + if not os.path.exists(result_dir): + return { + "status": "Not Started", + "progress": 0, + "max_steps": MAX_STEPS, + "last_update": None + } + + traj_file = os.path.join(result_dir, "traj.jsonl") + log_file = os.path.join(result_dir, "runtime.log") + result_file = os.path.join(result_dir, "result.txt") + + if not os.path.exists(traj_file): + return { + "status": "Preparing", + "progress": 0, + "max_steps": MAX_STEPS, + "last_update": datetime.fromtimestamp(os.path.getmtime(result_dir)).strftime("%Y-%m-%d %H:%M:%S") + } + + # Get file line count and last line without reading the whole file + import subprocess + + # Use wc -l to get line count + try: + result = subprocess.run(['wc', '-l', traj_file], capture_output=True, text=True) + if result.returncode == 0: + step_count = int(result.stdout.strip().split()[0]) + else: + step_count = 0 + except: + step_count = 0 + + # Use tail -n 1 to get last line + last_step_data = None + if step_count > 0: + try: + result = subprocess.run(['tail', '-n', '1', traj_file], capture_output=True, text=True) + if result.returncode == 0 and result.stdout.strip(): + last_step_data = json.loads(result.stdout.strip()) + except: + pass + + if step_count == 0: + return { + "status": "Initializing", + "progress": 0, + "max_steps": MAX_STEPS, + "last_update": datetime.fromtimestamp(os.path.getmtime(traj_file)).strftime("%Y-%m-%d %H:%M:%S") + } + + # Set default status to "Running" + status = "Running" + + # Determine status from last step data + if last_step_data: + if last_step_data.get("done", False): + status = "Done" + elif last_step_data.get("Error", False): + status = "Error" + + # If step count reaches max, consider as done + if step_count >= MAX_STEPS: + status = "Done (Max Steps)" + + # Quickly check exit condition in log file (only last few lines) + if os.path.exists(log_file) and status == "Running": + try: + # Use tail to read last 2 lines of log file + result = subprocess.run(['tail', '-n', '2', log_file], capture_output=True, text=True) + if result.returncode == 0: + log_tail = result.stdout + if "message_exit: True" in log_tail: + status = "Done (Message Exit)" + except: + pass + + # If step count reaches max again (double check) + if step_count >= MAX_STEPS: + status = "Done (Max Steps)" + + # Get last update time + last_update = "None" + if last_step_data and "action_timestamp" in last_step_data: + try: + last_update = datetime.strptime(last_step_data["action_timestamp"], "%Y%m%d@%H%M%S").strftime("%Y-%m-%d %H:%M:%S") + except: + pass + + # Get result content if finished + result_content = None + if status.startswith("Done") and os.path.exists(result_file): + try: + with open(result_file, 'r') as f: + result_content = f.read().strip() + except: + result_content = "Result file not found" + + return { + "status": status, + "progress": step_count, + "max_steps": MAX_STEPS, + "last_update": last_update, + "result": result_content + } + def get_all_tasks_status(): task_list = load_task_list() result = {} @@ -176,6 +288,34 @@ def get_all_tasks_status(): return result +def get_all_tasks_status_brief(): + """ + Get brief status info for all tasks, without detailed step data, for fast homepage loading. + """ + task_list = load_task_list() + result = {} + + for task_type, task_ids in task_list.items(): + result[task_type] = [] + for task_id in task_ids: + task_info = get_task_info(task_type, task_id) + task_status = get_task_status_brief(task_type, task_id) + + if task_info: + result[task_type].append({ + "id": task_id, + "instruction": task_info.get("instruction", "No instruction provided"), + "status": task_status + }) + else: + result[task_type].append({ + "id": task_id, + "instruction": "No task info available", + "status": task_status + }) + + return result + @app.route('/') def index(): return render_template("index.html") @@ -199,6 +339,11 @@ def api_tasks(): """Task status API""" return jsonify(get_all_tasks_status()) +@app.route('/api/tasks/brief') +def api_tasks_brief(): + """Return brief status info for all tasks, without detailed step data, for fast homepage loading.""" + return jsonify(get_all_tasks_status_brief()) + @app.route('/task///screenshot/') def task_screenshot(task_type, task_id, filename): """Get task screenshot""" diff --git a/monitor/static/index.js b/monitor/static/index.js index f9be323..0413b4d 100644 --- a/monitor/static/index.js +++ b/monitor/static/index.js @@ -1,7 +1,6 @@ -// filepath: /home/adlsdztony/codes/OSWorld/monitor/static/index.js document.addEventListener('DOMContentLoaded', () => { fetchTasks(); - // 筛选功能绑定 + // Bind filter functionality document.getElementById('total-tasks').parentElement.addEventListener('click', () => setTaskFilter('all')); document.getElementById('active-tasks').parentElement.addEventListener('click', () => setTaskFilter('active')); document.getElementById('completed-tasks').parentElement.addEventListener('click', () => setTaskFilter('completed')); @@ -24,11 +23,118 @@ function refreshPage() { // Store in sessionStorage sessionStorage.setItem('expandedTaskTypes', JSON.stringify(expandedTaskTypes)); - fetchTasks(); + // Only fetch brief data for update to improve refresh speed + fetchTasksForRefresh(); +} + +function fetchTasksForRefresh() { + fetch('/api/tasks/brief') + .then(response => response.json()) + .then(data => { + // Update stored data + allTaskData = data; + // Only update statistics and task status, do not fully re-render + updateStatistics(data); + updateTaskStatus(data); + }) + .catch(error => console.error('Error refreshing tasks:', error)); +} + +// New function: only update task status, do not re-render the entire list +function updateTaskStatus(data) { + // Update the status display of each task + Object.entries(data).forEach(([taskType, tasks]) => { + tasks.forEach(task => { + // Find the corresponding task card + const taskCard = document.querySelector(`.task-card[data-task-id="${task.id}"][data-task-type="${taskType}"]`); + if (!taskCard) return; + + // Update status display + const statusElement = taskCard.querySelector('.task-status'); + if (statusElement) { + // Remove all status classes + statusElement.classList.remove('status-not-started', 'status-preparing', 'status-running', 'status-completed', 'status-error', 'status-unknown'); + + // Set new status class and icon + let statusClass = ''; + let statusIcon = ''; + + switch(task.status.status) { + case 'Not Started': + statusClass = 'status-not-started'; + statusIcon = 'fa-hourglass-start'; + break; + case 'Preparing': + case 'Initializing': + statusClass = 'status-preparing'; + statusIcon = 'fa-spinner fa-pulse'; + break; + case 'Running': + statusClass = 'status-running'; + statusIcon = 'fa-running'; + break; + case 'Done': + case 'Done (Message Exit)': + case 'Done (Max Steps)': + statusClass = 'status-completed'; + statusIcon = 'fa-check-circle'; + break; + case 'Error': + statusClass = 'status-error'; + statusIcon = 'fa-exclamation-circle'; + break; + default: + statusClass = 'status-unknown'; + statusIcon = 'fa-question-circle'; + break; + } + + statusElement.classList.add(statusClass); + statusElement.innerHTML = ` ${task.status.status}`; + } + + // Update progress bar + if (task.status.progress > 0) { + const progressText = taskCard.querySelector('.task-details div:first-child'); + if (progressText) { + progressText.innerHTML = ` Progress: ${task.status.progress}/${task.status.max_steps} step(s)`; + } + + const progressFill = taskCard.querySelector('.progress-fill'); + if (progressFill) { + const percentage = (task.status.progress / task.status.max_steps) * 100; + progressFill.style.width = `${percentage}%`; + } + + const progressPercentage = taskCard.querySelector('.progress-percentage'); + if (progressPercentage) { + const percentage = (task.status.progress / task.status.max_steps) * 100; + progressPercentage.textContent = `${Math.round(percentage)}%`; + } + } + + // Update last update time + const timestamp = taskCard.querySelector('.timestamp'); + if (timestamp && task.status.last_update) { + timestamp.innerHTML = ` Last Update: ${task.status.last_update}`; + } + + // Update result info + if (task.status.result) { + let resultDiv = taskCard.querySelector('.task-result'); + if (!resultDiv) { + resultDiv = document.createElement('div'); + resultDiv.className = 'task-result'; + taskCard.querySelector('.task-details').appendChild(resultDiv); + } + resultDiv.innerHTML = ` Result: ${task.status.result}`; + } + }); + }); } function fetchTasks() { - fetch('/api/tasks') + fetch('/api/tasks/brief') .then(response => response.json()) .then(data => { allTaskData = data; @@ -42,7 +148,7 @@ function setTaskFilter(filter) { currentFilter = filter; if (!allTaskData) return; renderTasks(allTaskData); - // 高亮选中卡片 + // Highlight selected card document.querySelectorAll('.stat-card').forEach(card => card.classList.remove('selected')); if (filter === 'all') { document.getElementById('total-tasks').parentElement.classList.add('selected'); @@ -55,7 +161,7 @@ function setTaskFilter(filter) { } } -// 更新统计信息 +// Update statistics info function updateStatistics(data) { let totalTasks = 0; let activeTasks = 0; @@ -80,7 +186,7 @@ function updateStatistics(data) { document.getElementById('completed-tasks').textContent = completedTasks; document.getElementById('error-tasks').textContent = errorTasks; - // 高亮显示当前选中的统计卡片 + // Highlight the currently selected statistics card document.querySelectorAll('.stat-card').forEach(card => card.classList.remove('selected')); if (currentFilter === 'all') { document.getElementById('total-tasks').parentElement.classList.add('selected'); @@ -176,6 +282,9 @@ function renderTasks(data) { tasks.forEach(task => { const taskCard = document.createElement('div'); taskCard.className = 'task-card'; + // Add data attributes for later updates + taskCard.setAttribute('data-task-id', task.id); + taskCard.setAttribute('data-task-type', taskType); const taskHeader = document.createElement('div'); taskHeader.className = 'task-header';