/** * DashboardCore - Core logic extracted from main.js for testability. * * This module contains the testable business logic: * - Authentication (login, logout, checkAuthStatus) * - API calls with auth headers * - WebSocket message handling * - Data management (hosts, tasks, schedules) * * The main.js file imports this module and adds DOM-specific functionality. */ /** * Core dashboard functionality that can be tested without DOM dependencies. */ export class DashboardCore { constructor(options = {}) { // Allow injection of dependencies for testing this.apiBase = options.apiBase || (typeof window !== 'undefined' ? window.location.origin : 'http://localhost'); this.storage = options.storage || (typeof localStorage !== 'undefined' ? localStorage : null); this.fetchFn = options.fetch || (typeof fetch !== 'undefined' ? fetch : null); this.WebSocketClass = options.WebSocket || (typeof WebSocket !== 'undefined' ? WebSocket : null); // Authentication state this.accessToken = this.storage?.getItem('accessToken') || null; this.currentUser = null; this.setupRequired = false; // Data state this.hosts = []; this.tasks = []; this.schedules = []; this.logs = []; this.alerts = []; this.alertsUnread = 0; // WebSocket this.ws = null; // Callbacks for UI updates (set by main.js) this.onHostsUpdated = null; this.onTasksUpdated = null; this.onSchedulesUpdated = null; this.onAlertsUpdated = null; this.onAuthStateChanged = null; this.onNotification = null; } // ===== AUTHENTICATION ===== getAuthHeaders() { const headers = { 'Content-Type': 'application/json' }; if (this.accessToken) { headers['Authorization'] = `Bearer ${this.accessToken}`; } return headers; } async checkAuthStatus() { try { const response = await this.fetchFn(`${this.apiBase}/api/auth/status`, { headers: this.getAuthHeaders() }); if (!response.ok) { return false; } const data = await response.json(); this.setupRequired = data.setup_required; if (data.setup_required) { return false; } if (data.authenticated && data.user) { this.currentUser = data.user; this.onAuthStateChanged?.({ authenticated: true, user: data.user }); return true; } return false; } catch (error) { console.error('Auth status check failed:', error); return false; } } async login(username, password) { try { const response = await this.fetchFn(`${this.apiBase}/api/auth/login/json`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password }) }); if (!response.ok) { const error = await response.json(); throw new Error(error.detail || 'Login failed'); } const data = await response.json(); this.accessToken = data.access_token; this.storage?.setItem('accessToken', data.access_token); await this.checkAuthStatus(); this.onNotification?.('Connexion réussie', 'success'); return true; } catch (error) { console.error('Login failed:', error); this.onNotification?.(error.message, 'error'); return false; } } logout() { this.accessToken = null; this.currentUser = null; this.storage?.removeItem('accessToken'); if (this.ws) { this.ws.close(); this.ws = null; } this.onAuthStateChanged?.({ authenticated: false, user: null }); this.onNotification?.('Déconnexion réussie', 'success'); } // ===== API CALLS ===== async apiCall(endpoint, options = {}) { const url = `${this.apiBase}${endpoint}`; const defaultOptions = { headers: this.getAuthHeaders() }; try { const response = await this.fetchFn(url, { ...defaultOptions, ...options }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } return await response.json(); } catch (error) { console.error(`API Error (${endpoint}):`, error); throw error; } } // ===== WEBSOCKET ===== connectWebSocket(wsUrl = null) { if (!this.WebSocketClass) { console.warn('WebSocket not available'); return; } const url = wsUrl || this._getDefaultWsUrl(); try { this.ws = new this.WebSocketClass(url); this.ws.onopen = () => { console.log('WebSocket connected'); }; this.ws.onmessage = (event) => { try { const data = JSON.parse(event.data); this.handleWebSocketMessage(data); } catch (e) { console.error('WebSocket message parse error:', e); } }; this.ws.onclose = () => { console.log('WebSocket closed'); }; this.ws.onerror = (error) => { console.error('WebSocket error:', error); }; } catch (error) { console.error('WebSocket connection failed:', error); } } _getDefaultWsUrl() { if (typeof window === 'undefined') { return 'ws://localhost/ws'; } const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; return `${protocol}//${window.location.host}/ws`; } handleWebSocketMessage(message) { const { type, data } = message; switch (type) { case 'host_updated': this.updateHostInList(data); break; case 'task_started': case 'task_updated': case 'task_completed': this.handleTaskUpdate(data); break; case 'schedule_completed': this.updateScheduleStatus(data); break; case 'alert_created': this.handleAlertCreated(data); break; case 'hosts_synced': this.onHostsUpdated?.(); break; default: // Unknown message type - ignore break; } } // ===== DATA MANAGEMENT ===== updateHostInList(hostData) { if (!hostData?.id) return; const idx = this.hosts.findIndex(h => h.id === hostData.id); if (idx >= 0) { this.hosts[idx] = { ...this.hosts[idx], ...hostData }; } else { this.hosts.push(hostData); } this.onHostsUpdated?.(); } handleTaskUpdate(taskData) { if (!taskData?.id) return; const idx = this.tasks.findIndex(t => t.id === taskData.id); if (idx >= 0) { this.tasks[idx] = { ...this.tasks[idx], ...taskData }; } else { this.tasks.unshift(taskData); } this.onTasksUpdated?.(); } updateScheduleStatus(scheduleData) { if (!scheduleData?.schedule_id) return; const idx = this.schedules.findIndex(s => s.id === scheduleData.schedule_id); if (idx >= 0) { this.schedules[idx].last_status = scheduleData.status; this.schedules[idx].last_run_at = scheduleData.completed_at; } this.onSchedulesUpdated?.(); } handleAlertCreated(alert) { if (!alert) return; this.alerts = [alert, ...this.alerts].slice(0, 200); this.alertsUnread++; this.onAlertsUpdated?.(); } // ===== UTILITY ===== escapeHtml(text) { if (!text) return ''; const div = typeof document !== 'undefined' ? document.createElement('div') : null; if (div) { div.textContent = text; return div.innerHTML; } // Fallback for non-DOM environments return String(text) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } } // Export for both ESM and CommonJS export default DashboardCore;