feat&fix: add brief task status retrieval and improve task status update mechanism
This commit is contained in:
145
monitor/main.py
145
monitor/main.py
@@ -151,6 +151,118 @@ def get_task_status(task_type, task_id):
|
|||||||
"result": result_content
|
"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():
|
def get_all_tasks_status():
|
||||||
task_list = load_task_list()
|
task_list = load_task_list()
|
||||||
result = {}
|
result = {}
|
||||||
@@ -176,6 +288,34 @@ def get_all_tasks_status():
|
|||||||
|
|
||||||
return result
|
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('/')
|
@app.route('/')
|
||||||
def index():
|
def index():
|
||||||
return render_template("index.html")
|
return render_template("index.html")
|
||||||
@@ -199,6 +339,11 @@ def api_tasks():
|
|||||||
"""Task status API"""
|
"""Task status API"""
|
||||||
return jsonify(get_all_tasks_status())
|
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/<task_type>/<task_id>/screenshot/<path:filename>')
|
@app.route('/task/<task_type>/<task_id>/screenshot/<path:filename>')
|
||||||
def task_screenshot(task_type, task_id, filename):
|
def task_screenshot(task_type, task_id, filename):
|
||||||
"""Get task screenshot"""
|
"""Get task screenshot"""
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
// filepath: /home/adlsdztony/codes/OSWorld/monitor/static/index.js
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
fetchTasks();
|
fetchTasks();
|
||||||
// 筛选功能绑定
|
// Bind filter functionality
|
||||||
document.getElementById('total-tasks').parentElement.addEventListener('click', () => setTaskFilter('all'));
|
document.getElementById('total-tasks').parentElement.addEventListener('click', () => setTaskFilter('all'));
|
||||||
document.getElementById('active-tasks').parentElement.addEventListener('click', () => setTaskFilter('active'));
|
document.getElementById('active-tasks').parentElement.addEventListener('click', () => setTaskFilter('active'));
|
||||||
document.getElementById('completed-tasks').parentElement.addEventListener('click', () => setTaskFilter('completed'));
|
document.getElementById('completed-tasks').parentElement.addEventListener('click', () => setTaskFilter('completed'));
|
||||||
@@ -24,11 +23,118 @@ function refreshPage() {
|
|||||||
// Store in sessionStorage
|
// Store in sessionStorage
|
||||||
sessionStorage.setItem('expandedTaskTypes', JSON.stringify(expandedTaskTypes));
|
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 = `<i class="fas ${statusIcon}"></i> ${task.status.status}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update progress bar
|
||||||
|
if (task.status.progress > 0) {
|
||||||
|
const progressText = taskCard.querySelector('.task-details div:first-child');
|
||||||
|
if (progressText) {
|
||||||
|
progressText.innerHTML = `<i class="fas fa-chart-line"></i> 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 = `<i class="far fa-clock"></i> 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 = `<strong><i class="fas fa-flag-checkered"></i> Result:</strong> ${task.status.result}`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchTasks() {
|
function fetchTasks() {
|
||||||
fetch('/api/tasks')
|
fetch('/api/tasks/brief')
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
allTaskData = data;
|
allTaskData = data;
|
||||||
@@ -42,7 +148,7 @@ function setTaskFilter(filter) {
|
|||||||
currentFilter = filter;
|
currentFilter = filter;
|
||||||
if (!allTaskData) return;
|
if (!allTaskData) return;
|
||||||
renderTasks(allTaskData);
|
renderTasks(allTaskData);
|
||||||
// 高亮选中卡片
|
// Highlight selected card
|
||||||
document.querySelectorAll('.stat-card').forEach(card => card.classList.remove('selected'));
|
document.querySelectorAll('.stat-card').forEach(card => card.classList.remove('selected'));
|
||||||
if (filter === 'all') {
|
if (filter === 'all') {
|
||||||
document.getElementById('total-tasks').parentElement.classList.add('selected');
|
document.getElementById('total-tasks').parentElement.classList.add('selected');
|
||||||
@@ -55,7 +161,7 @@ function setTaskFilter(filter) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新统计信息
|
// Update statistics info
|
||||||
function updateStatistics(data) {
|
function updateStatistics(data) {
|
||||||
let totalTasks = 0;
|
let totalTasks = 0;
|
||||||
let activeTasks = 0;
|
let activeTasks = 0;
|
||||||
@@ -80,7 +186,7 @@ function updateStatistics(data) {
|
|||||||
document.getElementById('completed-tasks').textContent = completedTasks;
|
document.getElementById('completed-tasks').textContent = completedTasks;
|
||||||
document.getElementById('error-tasks').textContent = errorTasks;
|
document.getElementById('error-tasks').textContent = errorTasks;
|
||||||
|
|
||||||
// 高亮显示当前选中的统计卡片
|
// Highlight the currently selected statistics card
|
||||||
document.querySelectorAll('.stat-card').forEach(card => card.classList.remove('selected'));
|
document.querySelectorAll('.stat-card').forEach(card => card.classList.remove('selected'));
|
||||||
if (currentFilter === 'all') {
|
if (currentFilter === 'all') {
|
||||||
document.getElementById('total-tasks').parentElement.classList.add('selected');
|
document.getElementById('total-tasks').parentElement.classList.add('selected');
|
||||||
@@ -176,6 +282,9 @@ function renderTasks(data) {
|
|||||||
tasks.forEach(task => {
|
tasks.forEach(task => {
|
||||||
const taskCard = document.createElement('div');
|
const taskCard = document.createElement('div');
|
||||||
taskCard.className = 'task-card';
|
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');
|
const taskHeader = document.createElement('div');
|
||||||
taskHeader.className = 'task-header';
|
taskHeader.className = 'task-header';
|
||||||
|
|||||||
Reference in New Issue
Block a user