From b5efb82172243a0d25f137fb352be10b23694419 Mon Sep 17 00:00:00 2001 From: adlsdztony Date: Sun, 1 Jun 2025 06:50:02 +0000 Subject: [PATCH] feat&fix: add task recording endpoint, enhance video player support, and improve mobile responsiveness --- monitor/main.py | 14 ++++ monitor/static/index.css | 51 ++++++++++-- monitor/static/task_detail.css | 129 ++++++++++++++++++++++++++++- monitor/templates/index.html | 2 +- monitor/templates/task_detail.html | 67 ++++++++++++++- 5 files changed, 255 insertions(+), 8 deletions(-) diff --git a/monitor/main.py b/monitor/main.py index 1d6bd96..f38294b 100644 --- a/monitor/main.py +++ b/monitor/main.py @@ -208,6 +208,20 @@ def task_screenshot(task_type, task_id, filename): else: return "Screenshot does not exist", 404 +@app.route('/task///recording') +def task_recording(task_type, task_id): + """Get task recording video""" + recording_path = os.path.join(RESULTS_BASE_PATH, task_type, task_id, "recording.mp4") + if os.path.exists(recording_path): + response = send_file(recording_path, mimetype='video/mp4') + # Add headers to improve mobile compatibility + response.headers['Accept-Ranges'] = 'bytes' + response.headers['Cache-Control'] = 'public, max-age=3600' + response.headers['X-Content-Type-Options'] = 'nosniff' + return response + else: + return "Recording does not exist", 404 + @app.route('/api/task//') def api_task_detail(task_type, task_id): """Task detail API""" diff --git a/monitor/static/index.css b/monitor/static/index.css index 1bdb589..3405705 100644 --- a/monitor/static/index.css +++ b/monitor/static/index.css @@ -33,7 +33,7 @@ h2 { color: #0056b3; margin-top: 32px; font-size: 1.6em; } } .stat-card { flex: 1; - min-width: 220px; + min-width: 150px; background: linear-gradient(135deg, #ffffff, #f8faff); padding: 20px; margin: 0 10px 20px; @@ -300,16 +300,57 @@ h2 { color: #0056b3; margin-top: 32px; font-size: 1.6em; } @media (max-width: 600px) { .fab { right: 18px; width: 52px; height: 52px; font-size: 1.3em; } .fab-refresh { bottom: 18px; } - h1 { font-size: 2em; } + h1 { font-size: 1.8em; } + .system-status { font-size: 0.4em; display: block; margin: 10px auto; width: fit-content; } .task-status { padding: 4px 10px; font-size: 0.85em; } - .dashboard-stats { flex-direction: column; } - .stat-card { margin: 0 0 15px; min-width: 100%; } + + /* Row layout for stat cards on mobile */ + .dashboard-stats { + flex-direction: row; + flex-wrap: wrap; + justify-content: center; + } + .stat-card { + flex: 0 0 22%; + margin: 0 1% 15px; + min-width: auto; + padding: 10px 5px; + } + .stat-card i { + font-size: 1.2em; + margin-bottom: 5px; + } + .stat-card span { + font-size: 1em; + margin-bottom: 2px; + } + .stat-label { + font-size: 0.7em; + } + + .task-type-header { flex-direction: column; align-items: flex-start; } + .task-type-stats { margin-top: 10px; } + .task-card { padding: 15px; } + .task-header { flex-direction: column; align-items: flex-start; } + .task-status { margin-top: 8px; } } @media (max-width: 700px) { - .main-container { padding: 20px; margin: 20px; border-radius: 10px; } + .main-container { padding: 20px; margin: 15px; border-radius: 10px; } .task-card { padding: 16px; } h1:after { width: 60px; } + .task-type-name { font-size: 1.1em; } + .task-stat { font-size: 0.8em; padding: 3px 8px; } +} + +/* Extra small devices */ +@media (max-width: 400px) { + .stat-card { + flex: 0 0 46%; + margin: 0 2% 10px; + } + h1 { font-size: 1.5em; } + .main-container { padding: 15px; margin: 10px; } } .stat-card.selected { diff --git a/monitor/static/task_detail.css b/monitor/static/task_detail.css index 06da1cb..fc1faf8 100644 --- a/monitor/static/task_detail.css +++ b/monitor/static/task_detail.css @@ -1,4 +1,21 @@ -body { font-family: 'Segoe UI', Arial, sans-serif; margin: 0; padding: 0; background: linear-gradient(135deg, #f4f6fa 0%, #e9f0f9 100%); } +body { font-family: 'Segoe UI', Arial, sans-serif; margin: 0; padding: 0; background: linepre { + background: linear-gradient(135deg, #f3f6fa, #edf1f7); + padding: 16px; + border-radius: 8px; + overflow-x: auto; + font-size: 1em; + box-shadow: inset 0 1px 3px rgba(0,0,0,0.05); + border-left: 3px solid rgba(0,123,255,0.3); + white-space: pre-wrap; + word-break: break-word; +} +.step-image { + max-width: 100%; + border: none; + border-radius: 8px; + box-shadow: 0 5px 15px rgba(0,0,0,0.08); + transition: all 0.3s; +}g, #f4f6fa 0%, #e9f0f9 100%); } .container { max-width: 950px; margin: 40px auto; background: #fff; border-radius: 14px; box-shadow: 0 10px 30px rgba(0,0,0,0.12); padding: 36px 44px; } h1 { font-size: 2.4em; margin-bottom: 14px; color: #1a237e; position: relative; } h1:after { content: ''; display: block; width: 70px; height: 4px; background: linear-gradient(90deg, #007bff, #00c6ff); margin: 12px 0 0; border-radius: 2px; } @@ -277,6 +294,82 @@ pre { left: auto; right: 10px; } + + .container { + margin: 20px; + padding: 20px; + max-width: calc(100% - 40px); + } + + h1 { + font-size: 1.8em; + } + + .task-info dl { + grid-template-columns: 1fr; + grid-gap: 8px; + } + + .task-info dt { + border-bottom: 1px solid #eee; + padding-bottom: 5px; + } + + .task-info dt:after { + display: none; + } + + pre { + font-size: 0.85em; + overflow-x: auto; + padding: 10px; + } + + .step-card { + padding: 15px; + } + + .step-header { + flex-direction: column; + align-items: flex-start; + } + + .step-time { + margin-top: 5px; + } + + .fab { + width: 50px; + height: 50px; + right: 20px; + bottom: 20px; + font-size: 1.3em; + } + + .back-link { + padding: 8px 16px; + font-size: 0.95em; + } +} + +/* Extra small devices */ +@media (max-width: 400px) { + .container { + margin: 10px; + padding: 15px; + } + + h1 { + font-size: 1.5em; + } + + h2 { + font-size: 1.3em; + } + + .step-card { + padding: 12px; + } } /* 进度条样式 */ @@ -305,3 +398,37 @@ pre { margin-top: 4px; font-weight: normal; } + +/* Video player styles */ +.video-player { + margin: 15px 0; + background: #f8f9fa; + border-radius: 8px; + overflow: hidden; + position: relative; + box-shadow: 0 4px 12px rgba(0,0,0,0.1); +} + +.video-player video { + display: block; + max-width: 100%; + border-radius: 6px; + background: #000; +} + +.video-status { + padding: 10px; + text-align: center; + color: #666; + font-style: italic; + background: rgba(0,0,0,0.03); + border-top: 1px solid rgba(0,0,0,0.05); +} + +.video-error { + color: #dc3545; +} + +.video-success { + color: #28a745; +} diff --git a/monitor/templates/index.html b/monitor/templates/index.html index 4388f64..92f2d5c 100644 --- a/monitor/templates/index.html +++ b/monitor/templates/index.html @@ -2,7 +2,7 @@ - + OSWorld Monitor diff --git a/monitor/templates/task_detail.html b/monitor/templates/task_detail.html index 8fcd26c..62349e6 100644 --- a/monitor/templates/task_detail.html +++ b/monitor/templates/task_detail.html @@ -2,7 +2,7 @@ - + Task Detail: {{ task_id }} @@ -61,6 +61,20 @@ {% endif %}
Result
{{ task_status.result }}
+ + {% if task_status.status.startswith('Done') %} +
Recording
+
+
+ +
Loading video...
+
+
+ {% endif %}
@@ -101,6 +115,57 @@ function refreshPage() { window.location.reload(); } + + // Handle video player with enhanced mobile support + document.addEventListener('DOMContentLoaded', function() { + const videoElement = document.getElementById('task-recording'); + const videoStatus = document.getElementById('video-status'); + + if (videoElement) { + // Function to update status on successful video load + function updateSuccessStatus() { + videoStatus.textContent = 'Recording available'; + videoStatus.className = 'video-status video-success'; + } + + // Multiple event listeners for different browser implementations + // The 'loadeddata' event might not fire on all mobile browsers + videoElement.addEventListener('loadeddata', updateSuccessStatus); + videoElement.addEventListener('loadedmetadata', updateSuccessStatus); + videoElement.addEventListener('canplay', updateSuccessStatus); + + // Error handling for video + videoElement.addEventListener('error', function(e) { + console.error('Video error:', e); + videoStatus.textContent = 'Recording not available'; + videoStatus.className = 'video-status video-error'; + }); + + // Timeout for slow connections or browser issues + setTimeout(function() { + // If video is playable but events didn't fire correctly + if (videoElement.readyState >= 2 && videoStatus.textContent === 'Loading video...') { + updateSuccessStatus(); + } + }, 2000); + + // Directly check video source availability with fetch API + fetch('/task/{{ task_type }}/{{ task_id }}/recording', {method: 'HEAD'}) + .then(function(response) { + if (response.ok && videoStatus.textContent === 'Loading video...') { + // If HEAD request succeeds but video events haven't fired + setTimeout(updateSuccessStatus, 500); + } + }) + .catch(function() { + // Network error or CORS issue + if (videoStatus.textContent === 'Loading video...') { + videoStatus.textContent = 'Recording check failed'; + videoStatus.className = 'video-status video-error'; + } + }); + } + }); \ No newline at end of file