|
|
|
|
|
const API_BASE_URL = window.location.origin; |
|
|
const REFRESH_INTERVAL = 10000; |
|
|
const MAX_LOGS = 100; |
|
|
|
|
|
|
|
|
let refreshInterval; |
|
|
let autoScrollEnabled = true; |
|
|
let currentFiles = []; |
|
|
let selectedFile = null; |
|
|
let apiConnected = false; |
|
|
|
|
|
|
|
|
const elements = { |
|
|
statusIndicator: document.getElementById('statusIndicator'), |
|
|
totalFiles: document.getElementById('totalFiles'), |
|
|
processedFiles: document.getElementById('processedFiles'), |
|
|
extractedCourses: document.getElementById('extractedCourses'), |
|
|
extractedVideos: document.getElementById('extractedVideos'), |
|
|
extractedFrames: document.getElementById('extractedFrames'), |
|
|
trackedCursors: document.getElementById('trackedCursors'), |
|
|
currentFile: document.getElementById('currentFile'), |
|
|
progressFill: document.getElementById('progressFill'), |
|
|
progressText: document.getElementById('progressText'), |
|
|
startIndex: document.getElementById('startIndex'), |
|
|
startProcessing: document.getElementById('startProcessing'), |
|
|
stopProcessing: document.getElementById('stopProcessing'), |
|
|
refreshBtn: document.getElementById('refreshBtn'), |
|
|
themeToggle: document.getElementById('themeToggle'), |
|
|
fileCount: document.getElementById('fileCount'), |
|
|
filesGrid: document.getElementById('filesGrid'), |
|
|
logsContainer: document.getElementById('logsContainer'), |
|
|
clearLogs: document.getElementById('clearLogs'), |
|
|
autoScroll: document.getElementById('autoScroll'), |
|
|
fileModal: document.getElementById('fileModal'), |
|
|
modalTitle: document.getElementById('modalTitle'), |
|
|
modalBody: document.getElementById('modalBody'), |
|
|
modalClose: document.getElementById('modalClose'), |
|
|
downloadFile: document.getElementById('downloadFile'), |
|
|
viewFrames: document.getElementById('viewFrames'), |
|
|
loadingOverlay: document.getElementById('loadingOverlay'), |
|
|
toastContainer: document.getElementById('toastContainer') |
|
|
}; |
|
|
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', function() { |
|
|
initializeTheme(); |
|
|
setupEventListeners(); |
|
|
startAutoRefresh(); |
|
|
fetchInitialData(); |
|
|
}); |
|
|
|
|
|
|
|
|
function initializeTheme() { |
|
|
const savedTheme = localStorage.getItem('theme') || 'light'; |
|
|
document.documentElement.setAttribute('data-theme', savedTheme); |
|
|
updateThemeIcon(savedTheme); |
|
|
} |
|
|
|
|
|
function toggleTheme() { |
|
|
const currentTheme = document.documentElement.getAttribute('data-theme'); |
|
|
const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; |
|
|
document.documentElement.setAttribute('data-theme', newTheme); |
|
|
localStorage.setItem('theme', newTheme); |
|
|
updateThemeIcon(newTheme); |
|
|
} |
|
|
|
|
|
function updateThemeIcon(theme) { |
|
|
const icon = elements.themeToggle.querySelector('i'); |
|
|
icon.className = theme === 'dark' ? 'fas fa-sun' : 'fas fa-moon'; |
|
|
} |
|
|
|
|
|
|
|
|
function setupEventListeners() { |
|
|
elements.themeToggle.addEventListener('click', toggleTheme); |
|
|
elements.refreshBtn.addEventListener('click', () => { |
|
|
showToast('Refreshing data...', 'info'); |
|
|
fetchAllData(); |
|
|
}); |
|
|
|
|
|
elements.startProcessing.addEventListener('click', startProcessing); |
|
|
elements.stopProcessing.addEventListener('click', stopProcessing); |
|
|
|
|
|
elements.clearLogs.addEventListener('click', clearLogs); |
|
|
elements.autoScroll.addEventListener('click', toggleAutoScroll); |
|
|
|
|
|
elements.modalClose.addEventListener('click', closeModal); |
|
|
elements.fileModal.addEventListener('click', (e) => { |
|
|
if (e.target === elements.fileModal) closeModal(); |
|
|
}); |
|
|
|
|
|
elements.downloadFile.addEventListener('click', downloadSelectedFile); |
|
|
elements.viewFrames.addEventListener('click', viewFrames); |
|
|
|
|
|
|
|
|
document.addEventListener('keydown', (e) => { |
|
|
if (e.key === 'Escape') closeModal(); |
|
|
if (e.key === 'F5') { |
|
|
e.preventDefault(); |
|
|
fetchAllData(); |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
async function apiRequest(endpoint, options = {}) { |
|
|
try { |
|
|
showLoading(); |
|
|
const response = await fetch(`${API_BASE_URL}${endpoint}`, { |
|
|
headers: { |
|
|
'Content-Type': 'application/json', |
|
|
...options.headers |
|
|
}, |
|
|
...options |
|
|
}); |
|
|
|
|
|
if (!response.ok) { |
|
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`); |
|
|
} |
|
|
|
|
|
apiConnected = true; |
|
|
return await response.json(); |
|
|
} catch (error) { |
|
|
console.error('API request failed:', error); |
|
|
apiConnected = false; |
|
|
|
|
|
|
|
|
if (error.name === 'TypeError' && error.message.includes('fetch')) { |
|
|
showToast('API server not running. Please start the cursor tracking API on port 8000.', 'warning'); |
|
|
} else { |
|
|
showToast(`API Error: ${error.message}`, 'error'); |
|
|
} |
|
|
throw error; |
|
|
} finally { |
|
|
hideLoading(); |
|
|
} |
|
|
} |
|
|
|
|
|
async function fetchStatus() { |
|
|
try { |
|
|
const data = await apiRequest('/status'); |
|
|
updateStatusDisplay(data.processing_status); |
|
|
return data; |
|
|
} catch (error) { |
|
|
|
|
|
const demoStatus = { |
|
|
is_running: false, |
|
|
total_files: 150, |
|
|
processed_files: 45, |
|
|
extracted_courses: 12, |
|
|
extracted_videos: 89, |
|
|
extracted_frames_count: 15420, |
|
|
tracked_cursors_count: 8934, |
|
|
current_file: null, |
|
|
logs: [ |
|
|
'[Demo Mode] API server not connected', |
|
|
'[Demo Mode] This is a demonstration of the UI', |
|
|
'[Demo Mode] Start the API server on port 8000 to see real data' |
|
|
] |
|
|
}; |
|
|
updateStatusDisplay(demoStatus); |
|
|
} |
|
|
} |
|
|
|
|
|
async function fetchCursorData() { |
|
|
try { |
|
|
const data = await apiRequest('/cursor-data'); |
|
|
currentFiles = data.files || []; |
|
|
updateFilesDisplay(currentFiles); |
|
|
return data; |
|
|
} catch (error) { |
|
|
|
|
|
const demoFiles = [ |
|
|
{ |
|
|
filename: 'course_1_video_1_mp4_cursor_data.json', |
|
|
size_bytes: 45678, |
|
|
modified_time: 'Sun Jul 13 19:30:15 2025' |
|
|
}, |
|
|
{ |
|
|
filename: 'course_2_video_3_mp4_cursor_data.json', |
|
|
size_bytes: 67890, |
|
|
modified_time: 'Sun Jul 13 18:45:22 2025' |
|
|
}, |
|
|
{ |
|
|
filename: 'course_3_video_2_mp4_cursor_data.json', |
|
|
size_bytes: 34567, |
|
|
modified_time: 'Sun Jul 13 17:20:10 2025' |
|
|
} |
|
|
]; |
|
|
currentFiles = demoFiles; |
|
|
updateFilesDisplay(demoFiles); |
|
|
} |
|
|
} |
|
|
|
|
|
async function fetchFileDetails(filename) { |
|
|
try { |
|
|
const data = await apiRequest(`/cursor-data/${filename}/summary`); |
|
|
return data; |
|
|
} catch (error) { |
|
|
showToast(`Failed to fetch details for ${filename}`, 'error'); |
|
|
return null; |
|
|
} |
|
|
} |
|
|
|
|
|
async function startProcessing() { |
|
|
try { |
|
|
const startIndex = parseInt(elements.startIndex.value) || 0; |
|
|
const data = await apiRequest('/start-processing', { |
|
|
method: 'POST', |
|
|
body: JSON.stringify({ start_index: startIndex }) |
|
|
}); |
|
|
|
|
|
showToast(data.message, data.status === 'started' ? 'success' : 'info'); |
|
|
|
|
|
if (data.status === 'started') { |
|
|
elements.startProcessing.disabled = true; |
|
|
elements.stopProcessing.disabled = false; |
|
|
} |
|
|
} catch (error) { |
|
|
showToast('Failed to start processing', 'error'); |
|
|
} |
|
|
} |
|
|
|
|
|
async function stopProcessing() { |
|
|
try { |
|
|
const data = await apiRequest('/stop-processing', { |
|
|
method: 'POST' |
|
|
}); |
|
|
|
|
|
showToast(data.message, 'info'); |
|
|
elements.startProcessing.disabled = false; |
|
|
elements.stopProcessing.disabled = true; |
|
|
} catch (error) { |
|
|
showToast('Failed to stop processing', 'error'); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function updateStatusDisplay(status) { |
|
|
|
|
|
const statusDot = elements.statusIndicator.querySelector('.status-dot'); |
|
|
const statusText = elements.statusIndicator.querySelector('.status-text'); |
|
|
|
|
|
if (status.is_running) { |
|
|
statusDot.className = 'status-dot running'; |
|
|
statusText.textContent = 'Processing'; |
|
|
elements.startProcessing.disabled = true; |
|
|
elements.stopProcessing.disabled = false; |
|
|
} else { |
|
|
statusDot.className = 'status-dot stopped'; |
|
|
statusText.textContent = 'Idle'; |
|
|
elements.startProcessing.disabled = false; |
|
|
elements.stopProcessing.disabled = true; |
|
|
} |
|
|
|
|
|
|
|
|
elements.totalFiles.textContent = status.total_files || 0; |
|
|
elements.processedFiles.textContent = status.processed_files || 0; |
|
|
elements.extractedCourses.textContent = status.extracted_courses || 0; |
|
|
elements.extractedVideos.textContent = status.extracted_videos || 0; |
|
|
elements.extractedFrames.textContent = status.extracted_frames_count || 0; |
|
|
elements.trackedCursors.textContent = status.tracked_cursors_count || 0; |
|
|
|
|
|
|
|
|
const currentFile = status.current_file || 'None'; |
|
|
elements.currentFile.textContent = currentFile; |
|
|
|
|
|
const progress = status.total_files > 0 ? |
|
|
Math.round((status.processed_files / status.total_files) * 100) : 0; |
|
|
elements.progressFill.style.width = `${progress}%`; |
|
|
elements.progressText.textContent = `${progress}%`; |
|
|
|
|
|
|
|
|
if (status.logs && status.logs.length > 0) { |
|
|
updateLogs(status.logs); |
|
|
} |
|
|
} |
|
|
|
|
|
function updateFilesDisplay(files) { |
|
|
elements.fileCount.textContent = `${files.length} files`; |
|
|
|
|
|
if (files.length === 0) { |
|
|
elements.filesGrid.innerHTML = ` |
|
|
<div class="no-files"> |
|
|
<i class="fas fa-folder-open" style="font-size: 3rem; color: var(--text-muted); margin-bottom: 1rem;"></i> |
|
|
<p style="color: var(--text-muted);">No cursor tracking files found yet.</p> |
|
|
<p style="color: var(--text-muted); font-size: 0.875rem;">Files will appear here after processing completes.</p> |
|
|
</div> |
|
|
`; |
|
|
return; |
|
|
} |
|
|
|
|
|
elements.filesGrid.innerHTML = files.map(file => ` |
|
|
<div class="file-card" onclick="openFileModal('${file.filename}')"> |
|
|
<div class="file-header"> |
|
|
<div class="file-name">${file.filename}</div> |
|
|
<div class="file-size">${formatFileSize(file.size_bytes)}</div> |
|
|
</div> |
|
|
<div class="file-stats"> |
|
|
<div class="file-stat"> |
|
|
<span class="file-stat-label">Modified:</span> |
|
|
<span class="file-stat-value">${formatDate(file.modified_time)}</span> |
|
|
</div> |
|
|
</div> |
|
|
<div class="file-actions"> |
|
|
<button class="btn btn-primary btn-sm" onclick="event.stopPropagation(); downloadFile('${file.filename}')"> |
|
|
<i class="fas fa-download"></i> |
|
|
Download |
|
|
</button> |
|
|
<button class="btn btn-secondary btn-sm" onclick="event.stopPropagation(); openFileModal('${file.filename}')"> |
|
|
<i class="fas fa-eye"></i> |
|
|
Details |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
`).join(''); |
|
|
} |
|
|
|
|
|
function updateLogs(logs) { |
|
|
const container = elements.logsContainer; |
|
|
|
|
|
|
|
|
if (logs.length > 0) { |
|
|
container.innerHTML = ''; |
|
|
} |
|
|
|
|
|
logs.slice(-MAX_LOGS).forEach(log => { |
|
|
const logEntry = document.createElement('div'); |
|
|
logEntry.className = 'log-entry'; |
|
|
|
|
|
|
|
|
let logType = ''; |
|
|
if (log.includes('❌') || log.includes('ERROR') || log.includes('Failed')) { |
|
|
logType = 'error'; |
|
|
} else if (log.includes('✅') || log.includes('SUCCESS') || log.includes('Successfully')) { |
|
|
logType = 'success'; |
|
|
} else if (log.includes('⚠️') || log.includes('WARN')) { |
|
|
logType = 'warning'; |
|
|
} |
|
|
|
|
|
if (logType) { |
|
|
logEntry.classList.add(logType); |
|
|
} |
|
|
|
|
|
|
|
|
const timestampMatch = log.match(/^\[([^\]]+)\]/); |
|
|
const timestamp = timestampMatch ? timestampMatch[1] : new Date().toLocaleTimeString(); |
|
|
const message = timestampMatch ? log.substring(timestampMatch[0].length).trim() : log; |
|
|
|
|
|
logEntry.innerHTML = ` |
|
|
<span class="log-time">[${timestamp}]</span> |
|
|
<span class="log-message">${escapeHtml(message)}</span> |
|
|
`; |
|
|
|
|
|
container.appendChild(logEntry); |
|
|
}); |
|
|
|
|
|
|
|
|
if (autoScrollEnabled) { |
|
|
container.scrollTop = container.scrollHeight; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
async function openFileModal(filename) { |
|
|
selectedFile = filename; |
|
|
elements.modalTitle.textContent = `File Details: ${filename}`; |
|
|
|
|
|
showModal(); |
|
|
|
|
|
const details = await fetchFileDetails(filename); |
|
|
if (details) { |
|
|
elements.modalBody.innerHTML = ` |
|
|
<div class="file-details"> |
|
|
<div class="detail-section"> |
|
|
<h4>File Information</h4> |
|
|
<div class="detail-grid"> |
|
|
<div class="detail-item"> |
|
|
<span class="detail-label">File Size:</span> |
|
|
<span class="detail-value">${formatFileSize(details.file_size_bytes)}</span> |
|
|
</div> |
|
|
<div class="detail-item"> |
|
|
<span class="detail-label">Modified:</span> |
|
|
<span class="detail-value">${details.modified_time}</span> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="detail-section"> |
|
|
<h4>Frame Statistics</h4> |
|
|
<div class="detail-grid"> |
|
|
<div class="detail-item"> |
|
|
<span class="detail-label">Total Frames:</span> |
|
|
<span class="detail-value">${details.total_frames}</span> |
|
|
</div> |
|
|
<div class="detail-item"> |
|
|
<span class="detail-label">Cursor Active:</span> |
|
|
<span class="detail-value">${details.cursor_active_frames}</span> |
|
|
</div> |
|
|
<div class="detail-item"> |
|
|
<span class="detail-label">Cursor Inactive:</span> |
|
|
<span class="detail-value">${details.cursor_inactive_frames}</span> |
|
|
</div> |
|
|
<div class="detail-item"> |
|
|
<span class="detail-label">Detection Rate:</span> |
|
|
<span class="detail-value">${(details.cursor_detection_rate * 100).toFixed(1)}%</span> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="detail-section"> |
|
|
<h4>Confidence Statistics</h4> |
|
|
<div class="detail-grid"> |
|
|
<div class="detail-item"> |
|
|
<span class="detail-label">Average:</span> |
|
|
<span class="detail-value">${details.confidence_stats.average.toFixed(3)}</span> |
|
|
</div> |
|
|
<div class="detail-item"> |
|
|
<span class="detail-label">Maximum:</span> |
|
|
<span class="detail-value">${details.confidence_stats.maximum.toFixed(3)}</span> |
|
|
</div> |
|
|
<div class="detail-item"> |
|
|
<span class="detail-label">Minimum:</span> |
|
|
<span class="detail-value">${details.confidence_stats.minimum.toFixed(3)}</span> |
|
|
</div> |
|
|
<div class="detail-item"> |
|
|
<span class="detail-label">Measurements:</span> |
|
|
<span class="detail-value">${details.confidence_stats.total_measurements}</span> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="detail-section"> |
|
|
<h4>Templates Used</h4> |
|
|
<div class="template-list"> |
|
|
${details.templates_used.length > 0 ? |
|
|
details.templates_used.map(template => `<span class="template-tag">${template}</span>`).join('') : |
|
|
'<span class="no-templates">No templates detected</span>' |
|
|
} |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<style> |
|
|
.file-details { font-size: 0.875rem; } |
|
|
.detail-section { margin-bottom: 1.5rem; } |
|
|
.detail-section h4 { |
|
|
margin-bottom: 0.75rem; |
|
|
color: var(--accent-primary); |
|
|
font-weight: 600; |
|
|
} |
|
|
.detail-grid { |
|
|
display: grid; |
|
|
grid-template-columns: 1fr 1fr; |
|
|
gap: 0.5rem; |
|
|
} |
|
|
.detail-item { |
|
|
display: flex; |
|
|
justify-content: space-between; |
|
|
padding: 0.5rem; |
|
|
background: var(--bg-secondary); |
|
|
border-radius: var(--radius); |
|
|
} |
|
|
.detail-label { color: var(--text-secondary); } |
|
|
.detail-value { font-weight: 500; } |
|
|
.template-list { |
|
|
display: flex; |
|
|
flex-wrap: wrap; |
|
|
gap: 0.5rem; |
|
|
} |
|
|
.template-tag { |
|
|
background: var(--accent-primary); |
|
|
color: white; |
|
|
padding: 0.25rem 0.5rem; |
|
|
border-radius: var(--radius); |
|
|
font-size: 0.75rem; |
|
|
} |
|
|
.no-templates { |
|
|
color: var(--text-muted); |
|
|
font-style: italic; |
|
|
} |
|
|
</style> |
|
|
`; |
|
|
} else { |
|
|
elements.modalBody.innerHTML = '<p>Failed to load file details.</p>'; |
|
|
} |
|
|
} |
|
|
|
|
|
function showModal() { |
|
|
elements.fileModal.classList.add('show'); |
|
|
document.body.style.overflow = 'hidden'; |
|
|
} |
|
|
|
|
|
function closeModal() { |
|
|
elements.fileModal.classList.remove('show'); |
|
|
document.body.style.overflow = ''; |
|
|
selectedFile = null; |
|
|
} |
|
|
|
|
|
|
|
|
async function downloadFile(filename) { |
|
|
try { |
|
|
const response = await fetch(`${API_BASE_URL}/cursor-data/${filename}`); |
|
|
if (!response.ok) throw new Error('Download failed'); |
|
|
|
|
|
const blob = await response.blob(); |
|
|
const url = window.URL.createObjectURL(blob); |
|
|
const a = document.createElement('a'); |
|
|
a.href = url; |
|
|
a.download = filename; |
|
|
document.body.appendChild(a); |
|
|
a.click(); |
|
|
document.body.removeChild(a); |
|
|
window.URL.revokeObjectURL(url); |
|
|
|
|
|
showToast(`Downloaded ${filename}`, 'success'); |
|
|
} catch (error) { |
|
|
showToast(`Failed to download ${filename}`, 'error'); |
|
|
} |
|
|
} |
|
|
|
|
|
function downloadSelectedFile() { |
|
|
if (selectedFile) { |
|
|
downloadFile(selectedFile); |
|
|
} |
|
|
} |
|
|
|
|
|
function viewFrames() { |
|
|
if (selectedFile) { |
|
|
showToast('Frame viewer feature coming soon!', 'info'); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function formatFileSize(bytes) { |
|
|
if (bytes === 0) return '0 B'; |
|
|
const k = 1024; |
|
|
const sizes = ['B', 'KB', 'MB', 'GB']; |
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k)); |
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; |
|
|
} |
|
|
|
|
|
function formatDate(dateString) { |
|
|
try { |
|
|
return new Date(dateString).toLocaleDateString(); |
|
|
} catch { |
|
|
return dateString; |
|
|
} |
|
|
} |
|
|
|
|
|
function escapeHtml(text) { |
|
|
const div = document.createElement('div'); |
|
|
div.textContent = text; |
|
|
return div.innerHTML; |
|
|
} |
|
|
|
|
|
|
|
|
function clearLogs() { |
|
|
elements.logsContainer.innerHTML = '<div class="log-entry"><span class="log-time">[' + |
|
|
new Date().toLocaleTimeString() + ']</span><span class="log-message">Logs cleared</span></div>'; |
|
|
showToast('Logs cleared', 'info'); |
|
|
} |
|
|
|
|
|
function toggleAutoScroll() { |
|
|
autoScrollEnabled = !autoScrollEnabled; |
|
|
elements.autoScroll.classList.toggle('active', autoScrollEnabled); |
|
|
|
|
|
if (autoScrollEnabled) { |
|
|
elements.logsContainer.scrollTop = elements.logsContainer.scrollHeight; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function showLoading() { |
|
|
elements.loadingOverlay.classList.add('show'); |
|
|
} |
|
|
|
|
|
function hideLoading() { |
|
|
elements.loadingOverlay.classList.remove('show'); |
|
|
} |
|
|
|
|
|
function showToast(message, type = 'info', duration = 5000) { |
|
|
const toast = document.createElement('div'); |
|
|
toast.className = `toast ${type}`; |
|
|
|
|
|
const icons = { |
|
|
success: 'fas fa-check-circle', |
|
|
error: 'fas fa-exclamation-circle', |
|
|
warning: 'fas fa-exclamation-triangle', |
|
|
info: 'fas fa-info-circle' |
|
|
}; |
|
|
|
|
|
toast.innerHTML = ` |
|
|
<i class="toast-icon ${icons[type]}"></i> |
|
|
<div class="toast-content"> |
|
|
<div class="toast-message">${escapeHtml(message)}</div> |
|
|
</div> |
|
|
<button class="toast-close"> |
|
|
<i class="fas fa-times"></i> |
|
|
</button> |
|
|
`; |
|
|
|
|
|
const closeBtn = toast.querySelector('.toast-close'); |
|
|
closeBtn.addEventListener('click', () => removeToast(toast)); |
|
|
|
|
|
elements.toastContainer.appendChild(toast); |
|
|
|
|
|
|
|
|
setTimeout(() => removeToast(toast), duration); |
|
|
} |
|
|
|
|
|
function removeToast(toast) { |
|
|
if (toast && toast.parentNode) { |
|
|
toast.style.animation = 'slideInRight 0.3s ease reverse'; |
|
|
setTimeout(() => { |
|
|
if (toast.parentNode) { |
|
|
toast.parentNode.removeChild(toast); |
|
|
} |
|
|
}, 300); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function startAutoRefresh() { |
|
|
fetchAllData(); |
|
|
refreshInterval = setInterval(fetchAllData, REFRESH_INTERVAL); |
|
|
} |
|
|
|
|
|
function stopAutoRefresh() { |
|
|
if (refreshInterval) { |
|
|
clearInterval(refreshInterval); |
|
|
refreshInterval = null; |
|
|
} |
|
|
} |
|
|
|
|
|
async function fetchInitialData() { |
|
|
await fetchAllData(); |
|
|
} |
|
|
|
|
|
async function fetchAllData() { |
|
|
try { |
|
|
await Promise.all([ |
|
|
fetchStatus(), |
|
|
fetchCursorData() |
|
|
]); |
|
|
} catch (error) { |
|
|
console.error('Failed to fetch data:', error); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
window.addEventListener('beforeunload', () => { |
|
|
stopAutoRefresh(); |
|
|
}); |
|
|
|
|
|
|