/** * 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 = '