/** * HF Uploader Dashboard - JavaScript Application */ // ============================================================================ // API Client // ============================================================================ class APIClient { constructor(baseURL = '/api') { this.baseURL = baseURL; } async request(endpoint, options = {}) { const url = `${this.baseURL}${endpoint}`; const response = await fetch(url, { headers: { 'Content-Type': 'application/json', ...options.headers, }, ...options, }); if (!response.ok) { const error = await response.json().catch(() => ({ detail: response.statusText })); throw new Error(error.detail || `HTTP ${response.status}`); } return response.json(); } // Configuration endpoints getConfig() { return this.request('/config'); } updateConfig(data) { return this.request('/config', { method: 'POST', body: JSON.stringify(data), }); } // Queue endpoints getQueue(status = null) { const url = status ? `/queue?status=${status}` : '/queue'; return this.request(url); } getQueueStats() { return this.request('/queue/stats'); } addToQueue() { return this.request('/queue/add', { method: 'POST' }); } clearQueue() { return this.request('/queue/clear', { method: 'POST' }); } // Processing endpoints getProcessingState() { return this.request('/processing/state'); } startProcessing(maxFiles = 0) { return this.request('/processing/start', { method: 'POST', body: JSON.stringify({ max_files: maxFiles }), }); } getProcessedFiles() { return this.request('/processing/files'); } // Upload endpoints startUpload(fileIds, batchSize = 10) { return this.request('/upload/start', { method: 'POST', body: JSON.stringify({ file_ids: fileIds, batch_size: batchSize }), }); } getUploadStatus() { return this.request('/upload/status'); } retryUpload(itemId) { return this.request(`/upload/retry/${itemId}`, { method: 'POST' }); } // File endpoints previewFile(filename) { return this.request('/file/preview', { method: 'POST', body: JSON.stringify({ filename }), }); } // Error log endpoints getErrorLogs(limit = 50) { return this.request(`/errors?limit=${limit}`); } clearErrorLogs() { return this.request('/errors', { method: 'DELETE' }); } } // ============================================================================ // Notification System // ============================================================================ class Notifier { constructor() { this.toastEl = document.getElementById('toast'); this.timeout = null; } show(message, type = 'info', duration = 3000) { this.toastEl.textContent = message; this.toastEl.className = `toast show ${type}`; clearTimeout(this.timeout); this.timeout = setTimeout(() => { this.toastEl.classList.remove('show'); }, duration); } success(message) { this.show(message, 'success'); } error(message) { this.show(message, 'error', 5000); } warning(message) { this.show(message, 'warning', 4000); } info(message) { this.show(message, 'info'); } } // ============================================================================ // Dashboard Application // ============================================================================ class Dashboard { constructor() { this.api = new APIClient(); this.notifier = new Notifier(); this.currentSection = 'dashboard'; this.refreshInterval = null; this.init(); } init() { this.setupEventListeners(); this.loadInitialData(); this.startAutoRefresh(); } setupEventListeners() { // Navigation document.querySelectorAll('.nav-link').forEach(link => { link.addEventListener('click', (e) => { e.preventDefault(); const section = link.dataset.section; this.switchSection(section); }); }); // Dashboard buttons document.getElementById('btn-start-processing').addEventListener('click', () => this.startProcessing()); document.getElementById('btn-start-upload').addEventListener('click', () => this.startUpload()); // Queue buttons document.getElementById('btn-add-to-queue').addEventListener('click', () => this.addToQueue()); document.getElementById('btn-clear-queue').addEventListener('click', () => this.clearQueue()); document.getElementById('queue-filter').addEventListener('change', (e) => this.filterQueue(e.target.value)); // Configuration buttons document.getElementById('btn-save-config').addEventListener('click', () => this.saveConfig()); // Logs buttons document.getElementById('btn-clear-logs').addEventListener('click', () => this.clearLogs()); // Modal close document.querySelector('.modal-close').addEventListener('click', () => this.closeModal()); } switchSection(section) { // Hide all sections document.querySelectorAll('.section').forEach(s => s.classList.remove('active')); // Show selected section document.getElementById(section).classList.add('active'); // Update nav document.querySelectorAll('.nav-link').forEach(link => { link.classList.toggle('active', link.dataset.section === section); }); this.currentSection = section; // Load section-specific data if (section === 'queue') { this.loadQueue(); } else if (section === 'logs') { this.loadErrorLogs(); } } async loadInitialData() { try { await this.loadConfig(); await this.loadStats(); await this.loadProcessingState(); await this.loadUploadStatus(); } catch (error) { this.notifier.error(`Failed to load data: ${error.message}`); } } async loadConfig() { try { const config = await this.api.getConfig(); document.getElementById('hf-token').value = config.hf_token || ''; document.getElementById('source-all-repo').value = config.source_all_repo; document.getElementById('source-ato-repo').value = config.source_ato_repo; document.getElementById('target-repo').value = config.target_repo; document.getElementById('batch-size').value = config.upload_batch_size; document.getElementById('max-uploads').value = config.max_uploads_per_hour; } catch (error) { console.error('Failed to load config:', error); } } async loadStats() { try { const stats = await this.api.getQueueStats(); document.getElementById('stat-pending').textContent = stats.pending; document.getElementById('stat-uploaded').textContent = stats.completed; document.getElementById('stat-failed').textContent = stats.failed; const processed = await this.api.getProcessedFiles(); document.getElementById('stat-processed').textContent = processed.count; } catch (error) { console.error('Failed to load stats:', error); } } async loadProcessingState() { try { const state = await this.api.getProcessingState(); document.getElementById('processing-status').textContent = state.status; document.getElementById('matched-pairs').textContent = state.matched_pairs; if (state.total_files > 0) { const progress = (state.processed_files / state.total_files) * 100; document.getElementById('processing-progress').style.width = `${progress}%`; } } catch (error) { console.error('Failed to load processing state:', error); } } async loadUploadStatus() { try { const status = await this.api.getUploadStatus(); const rateLimitEl = document.getElementById('rate-limit-status'); const remainingEl = document.getElementById('remaining-uploads'); if (status.rate_limit.can_upload) { rateLimitEl.textContent = 'Ready'; rateLimitEl.style.color = 'var(--color-success)'; } else { rateLimitEl.textContent = 'Limited'; rateLimitEl.style.color = 'var(--color-error)'; } remainingEl.textContent = status.rate_limit.remaining_uploads; } catch (error) { console.error('Failed to load upload status:', error); } } async loadQueue(status = null) { try { const items = await this.api.getQueue(status); const tbody = document.getElementById('queue-tbody'); if (items.length === 0) { tbody.innerHTML = 'No files in queue'; return; } tbody.innerHTML = items.map(item => ` ${this.escapeHtml(item.file_name)} ${this.formatBytes(item.file_size)} ${item.status} ${item.retry_count}/${item.max_retries} ${item.status === 'failed' ? `` : ''} `).join(''); } catch (error) { this.notifier.error(`Failed to load queue: ${error.message}`); } } async filterQueue(status) { await this.loadQueue(status || null); } async loadErrorLogs() { try { const logs = await this.api.getErrorLogs(); const tbody = document.getElementById('logs-tbody'); if (logs.length === 0) { tbody.innerHTML = 'No errors logged'; return; } tbody.innerHTML = logs.map(log => ` ${this.escapeHtml(log.file_name)} ${log.error_code || 'N/A'} ${this.escapeHtml(log.error_message || '')} ${log.retryable ? 'Yes' : 'No'} ${new Date(log.created_at).toLocaleString()} `).join(''); } catch (error) { this.notifier.error(`Failed to load error logs: ${error.message}`); } } async startProcessing() { try { const btn = document.getElementById('btn-start-processing'); btn.disabled = true; this.notifier.info('Starting dataset processing...'); await this.api.startProcessing(0); this.notifier.success('Processing started'); // Refresh state periodically this.refreshInterval = setInterval(() => this.loadProcessingState(), 2000); } catch (error) { this.notifier.error(`Failed to start processing: ${error.message}`); document.getElementById('btn-start-processing').disabled = false; } } async startUpload() { try { const stats = await this.api.getQueueStats(); if (stats.pending === 0) { this.notifier.warning('No pending files to upload'); return; } const btn = document.getElementById('btn-start-upload'); btn.disabled = true; this.notifier.info('Starting upload...'); // Get all pending files const queue = await this.api.getQueue('pending'); const fileIds = queue.map(item => item.id); await this.api.startUpload(fileIds); this.notifier.success('Upload started'); // Refresh stats periodically this.refreshInterval = setInterval(() => this.loadStats(), 2000); } catch (error) { this.notifier.error(`Failed to start upload: ${error.message}`); document.getElementById('btn-start-upload').disabled = false; } } async addToQueue() { try { const btn = document.getElementById('btn-add-to-queue'); btn.disabled = true; const result = await this.api.addToQueue(); this.notifier.success(`Added ${result.added} files to queue`); await this.loadQueue(); await this.loadStats(); btn.disabled = false; } catch (error) { this.notifier.error(`Failed to add to queue: ${error.message}`); document.getElementById('btn-add-to-queue').disabled = false; } } async clearQueue() { if (!confirm('Are you sure you want to clear the entire queue?')) { return; } try { await this.api.clearQueue(); this.notifier.success('Queue cleared'); await this.loadQueue(); await this.loadStats(); } catch (error) { this.notifier.error(`Failed to clear queue: ${error.message}`); } } async saveConfig() { try { const config = { hf_token: document.getElementById('hf-token').value, source_all_repo: document.getElementById('source-all-repo').value, source_ato_repo: document.getElementById('source-ato-repo').value, target_repo: document.getElementById('target-repo').value, upload_batch_size: parseInt(document.getElementById('batch-size').value), max_uploads_per_hour: parseInt(document.getElementById('max-uploads').value), }; const btn = document.getElementById('btn-save-config'); btn.disabled = true; await this.api.updateConfig(config); this.notifier.success('Configuration saved'); btn.disabled = false; } catch (error) { this.notifier.error(`Failed to save configuration: ${error.message}`); document.getElementById('btn-save-config').disabled = false; } } async retryFile(itemId) { try { await this.api.retryUpload(itemId); this.notifier.success('Retry scheduled'); await this.loadQueue(); } catch (error) { this.notifier.error(`Failed to retry: ${error.message}`); } } async previewFile(filename) { try { const data = await this.api.previewFile(filename); const modal = document.getElementById('preview-modal'); const content = document.getElementById('preview-content'); content.textContent = JSON.stringify(data.content, null, 2); modal.classList.add('show'); } catch (error) { this.notifier.error(`Failed to preview file: ${error.message}`); } } closeModal() { document.getElementById('preview-modal').classList.remove('show'); } async clearLogs() { if (!confirm('Are you sure you want to clear all error logs?')) { return; } try { await this.api.clearErrorLogs(); this.notifier.success('Error logs cleared'); await this.loadErrorLogs(); } catch (error) { this.notifier.error(`Failed to clear logs: ${error.message}`); } } startAutoRefresh() { setInterval(() => { if (this.currentSection === 'dashboard') { this.loadStats(); this.loadProcessingState(); this.loadUploadStatus(); } }, 5000); } escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } formatBytes(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 Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i]; } } // ============================================================================ // Initialize Dashboard // ============================================================================ let dashboard; document.addEventListener('DOMContentLoaded', () => { dashboard = new Dashboard(); });