homelab_automation/app/dashboard_core.js
Bruno Charest ecefbc8611
Some checks failed
Tests / Backend Tests (Python) (3.10) (push) Has been cancelled
Tests / Backend Tests (Python) (3.11) (push) Has been cancelled
Tests / Backend Tests (Python) (3.12) (push) Has been cancelled
Tests / Frontend Tests (JS) (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / All Tests Passed (push) Has been cancelled
Clean up test files and debug artifacts, add node_modules to gitignore, export DashboardManager for testing, and enhance pytest configuration with comprehensive test markers and settings
2025-12-15 08:15:49 -05:00

288 lines
7.6 KiB
JavaScript

/**
* 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, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
}
// Export for both ESM and CommonJS
export default DashboardCore;