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
288 lines
7.6 KiB
JavaScript
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, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|
|
}
|
|
|
|
// Export for both ESM and CommonJS
|
|
export default DashboardCore;
|