// ======================== NAVIGATION (must be defined FIRST) ======================== let currentPage = 'dashboard'; function navigateTo(pageName) { // Hide all pages document.querySelectorAll('.page-section').forEach(p => { p.classList.remove('active'); }); // Show target page const target = document.getElementById('page-' + pageName); if (target) { target.classList.add('active'); } // Update sidebar active state document.querySelectorAll('#sidebar-nav .nav-item[data-page]').forEach(item => { item.classList.toggle('active', item.dataset.page === pageName); }); // Update mobile nav active state document.querySelectorAll('.mobile-nav-link[data-page]').forEach(item => { item.classList.toggle('active', item.dataset.page === pageName); }); currentPage = pageName; window.location.hash = pageName === 'dashboard' ? '' : pageName; // Trigger page-specific data loading if (window.dashboard) { switch (pageName) { case 'hosts': if (typeof dashboard.renderHosts === 'function') dashboard.renderHosts(); break; case 'playbooks': if (typeof dashboard.renderPlaybooks === 'function') dashboard.renderPlaybooks(); break; case 'tasks': if (typeof dashboard.renderTasks === 'function') dashboard.renderTasks(); break; case 'schedules': if (typeof dashboard.renderSchedules === 'function') dashboard.renderSchedules(); break; case 'docker': if (window.dockerSection && typeof window.dockerSection.init === 'function') window.dockerSection.init(); break; case 'logs': if (typeof dashboard.renderLogs === 'function') dashboard.renderLogs(); break; case 'alerts': if (typeof dashboard.refreshAlerts === 'function') dashboard.refreshAlerts(); break; case 'configuration': if (typeof dashboard.loadMetricsCollectionSchedule === 'function') dashboard.loadMetricsCollectionSchedule(); break; case 'dashboard': renderAllWidgets(); break; } } } function toggleSidebar() { document.body.classList.toggle('sidebar-collapsed'); const icon = document.querySelector('#sidebar-toggle i'); const label = document.querySelector('#sidebar-toggle .nav-label'); if (document.body.classList.contains('sidebar-collapsed')) { if (icon) icon.className = 'fas fa-angles-right'; if (label) label.textContent = 'Déplier'; } else { if (icon) icon.className = 'fas fa-angles-left'; if (label) label.textContent = 'Réduire'; } localStorage.setItem('sidebarCollapsed', document.body.classList.contains('sidebar-collapsed')); } function openCmdPalette() { const palette = document.getElementById('cmd-palette'); if (palette) { palette.classList.add('open'); const input = document.getElementById('cmd-input'); if (input) { input.value = ''; input.focus(); } renderCmdResults(''); } } function closeCmdPalette() { const palette = document.getElementById('cmd-palette'); if (palette) palette.classList.remove('open'); } function renderCmdResults(query) { const container = document.getElementById('cmd-results'); if (!container) return; const q = (query || '').toLowerCase().trim(); // Navigation items const pages = [ { label: 'Dashboard', icon: 'fa-grip', page: 'dashboard' }, { label: 'Hosts', icon: 'fa-server', page: 'hosts' }, { label: 'Playbooks', icon: 'fa-book', page: 'playbooks' }, { label: 'Tâches', icon: 'fa-list-check', page: 'tasks' }, { label: 'Planificateur', icon: 'fa-calendar-alt', page: 'schedules' }, { label: 'Docker', icon: 'fa-docker fab', page: 'docker' }, { label: 'Logs', icon: 'fa-file-alt', page: 'logs' }, { label: 'Alertes', icon: 'fa-bell', page: 'alerts' }, { label: 'Paramètres', icon: 'fa-cog', page: 'configuration' }, ]; let results = pages; // Add hosts to search const hosts = window.dashboard ? window.dashboard.hosts || [] : []; hosts.forEach(h => { results.push({ label: h.name || h.hostname, icon: 'fa-server', page: 'hosts', sub: h.ansible_host || '' }); }); if (q) { results = results.filter(r => r.label.toLowerCase().includes(q) || (r.sub && r.sub.toLowerCase().includes(q))); } container.innerHTML = results.slice(0, 10).map(r => `
${r.label}
${r.sub ? `
${r.sub}
` : ''}
` ).join(''); } // Keyboard shortcut for command palette document.addEventListener('keydown', function(e) { if ((e.metaKey || e.ctrlKey) && e.key === 'k') { e.preventDefault(); openCmdPalette(); } if (e.key === 'Escape') { closeCmdPalette(); if (state.focusOpen) closeFocus(); } }); // Command palette search input document.addEventListener('DOMContentLoaded', function() { const cmdInput = document.getElementById('cmd-input'); if (cmdInput) { cmdInput.addEventListener('input', function() { renderCmdResults(this.value); }); cmdInput.addEventListener('keydown', function(e) { if (e.key === 'Enter') { const first = document.querySelector('.cmd-result'); if (first) first.click(); } }); } // Restore sidebar state if (localStorage.getItem('sidebarCollapsed') === 'true') { document.body.classList.add('sidebar-collapsed'); const icon = document.querySelector('#sidebar-toggle i'); const label = document.querySelector('#sidebar-toggle .nav-label'); if (icon) icon.className = 'fas fa-angles-right'; if (label) label.textContent = 'Déplier'; } }); async function fetchAPI(endpoint, options = {}) { const token = localStorage.getItem('accessToken'); const headers = { 'Content-Type': 'application/json' }; if (token) headers['Authorization'] = 'Bearer ' + token; try { const res = await fetch(location.origin + endpoint, { headers, ...options }); if (!res.ok) return null; return await res.json(); } catch (e) { console.error('fetchAPI error:', endpoint, e); return null; } } async function loadData() { if (window.dashboard && typeof window.dashboard.loadAllData === 'function') { await window.dashboard.loadAllData(); } renderAllWidgets(); updateSysMetrics(); } // ======================== STATE ======================== let state = { gridCols: parseInt(localStorage.getItem('mc_gridCols')) || 3, widgetOrder: JSON.parse(localStorage.getItem('mc_widgetOrder') || 'null'), hiddenWidgets: JSON.parse(localStorage.getItem('mc_hiddenWidgets') || '[]'), focusOpen: false, drawerOpen: false, // Expose getters that read from the live dashboard manager directly get hosts() { return window.dashboard ? window.dashboard.hosts || [] : []; }, get containers() { // Try docker section first, then dashboard if (window.dockerSection && window.dockerSection.containers) return window.dockerSection.containers; if (window.dashboard) return window.dashboard.containers || []; return []; }, get stats() { if (!window.dashboard) return {}; const d = window.dashboard; const hosts = d.hosts || []; const online = hosts.filter(h => h.status === 'online').length; const tasks = d.taskLogs || d.tasks || []; const totalTasks = d.taskLogsStats ? d.taskLogsStats.total || 0 : tasks.length; const completed = d.taskLogsStats ? d.taskLogsStats.completed || 0 : 0; const failed = d.taskLogsStats ? d.taskLogsStats.failed || 0 : 0; const successRate = totalTasks > 0 ? Math.round((completed / totalTasks) * 100) : 100; const uptime = hosts.length > 0 ? Math.round((online / hosts.length) * 100) : 0; return { hostsOnline: online, hostsTotal: hosts.length, tasksExecuted: totalTasks, successRate: successRate, uptime: uptime }; }, get executions() { if (!window.dashboard) return []; // Map adhoc logs to the format expected by renderConsole const logs = window.dashboard.adhocWidgetLogs || window.dashboard.adhocHistory || []; return logs.slice(0, 10).map(l => ({ cmd: l.command || l.playbook_name || l.name || '', host: l.target || l.host || '', status: (l.status === 'completed' || l.status === 'success') ? 'success' : (l.status === 'failed' ? 'error' : l.status || 'info'), type: l.source_type || 'Ad-hoc', ts: l.started_at ? new Date(l.started_at).getTime() : Date.now() })); }, get schedules() { if (!window.dashboard) return []; return (window.dashboard.schedules || []).map(s => ({ name: s.name || s.playbook_name || '', cron: s.cron_expression || s.cron || '', targets: s.target_hosts || s.targets || 'all', enabled: s.is_active !== false, next_run: s.next_run_at ? new Date(s.next_run_at).toLocaleString('fr-FR', { hour: '2-digit', minute: '2-digit', day: 'numeric', month: 'short' }) : '—', last_status: s.last_status || '—' })); }, get liveEvents() { if (!window.dashboard) return []; // Build live events from recent task logs const logs = window.dashboard.taskLogs || []; return logs.slice(0, 12).map(l => ({ message: l.playbook_name || l.name || l.command || 'Tâche', host: l.target || l.host || 'all', status: (l.status === 'completed' || l.status === 'success') ? 'success' : (l.status === 'failed' ? 'error' : (l.status === 'running' ? 'running' : 'info')), ts: l.started_at ? new Date(l.started_at).getTime() : (l.created_at ? new Date(l.created_at).getTime() : Date.now()) })); } }; // ======================== UTILITIES ======================== function timeAgo(ts) { const s = Math.floor((Date.now() - ts) / 1000); if (s < 60) return `${s}s`; if (s < 3600) return `${Math.floor(s/60)}m`; if (s < 86400) return `${Math.floor(s/3600)}h`; return `${Math.floor(s/86400)}j`; } function sparklineSVG(w=80, h=28, color='#22d3ee', points=20) { const data = Array.from({length:points}, () => Math.random()); const max = Math.max(...data, 0.01); const step = w / (points - 1); const pts = data.map((v, i) => `${i*step},${h - (v/max)*(h-4)}`).join(' '); return ``; } function progressColor(val) { if (val > 80) return 'bg-red-500'; if (val > 60) return 'bg-yellow-500'; return 'bg-cyan-400'; } function statusBadge(status) { const m = { success:'bg-emerald-500/20 text-emerald-400', error:'bg-red-500/20 text-red-400', warning:'bg-yellow-500/20 text-yellow-400', running:'bg-blue-500/20 text-blue-400', stopped:'bg-gray-500/20 text-gray-400', info:'bg-cyan-500/20 text-cyan-400' }; return m[status] || m.info; } function showToast(message, type='success', duration=3500) { const container = document.getElementById('toast-container'); const icons = { success:'fa-check-circle text-emerald-400', error:'fa-times-circle text-red-400', warning:'fa-exclamation-triangle text-yellow-400', info:'fa-info-circle text-cyan-400' }; const id = 'toast-' + Date.now(); const el = document.createElement('div'); el.className = 'toast relative'; el.id = id; el.innerHTML = `
${message}
`; container.appendChild(el); anime({ targets:`#${id}`, translateX:[80,0], opacity:[0,1], duration:400, easing:'easeOutCubic' }); anime({ targets:`#${id}-bar`, width:'0%', duration:duration, easing:'linear', complete:()=>{ anime({ targets:`#${id}`, translateX:80, opacity:0, duration:300, easing:'easeInCubic', complete:()=>el.remove() }); } }); } function updateSysMetrics() { const h = state.hosts || []; const online = h.filter(x => x.status === 'online'); const avgCpu = online.length ? Math.round(online.reduce((s,x)=>s+(x.cpu||0),0)/online.length) : 0; const avgRam = online.length ? Math.round(online.reduce((s,x)=>s+(x.ram||0),0)/online.length) : 0; document.getElementById('sys-cpu').textContent = avgCpu + '%'; document.getElementById('sys-ram').textContent = avgRam + '%'; document.getElementById('sys-net').textContent = online.length + '/' + h.length; } // ======================== WIDGET DEFINITIONS ======================== const WIDGETS = { 'stats-overview': { title:'Vue d\'ensemble', icon:'fa-chart-pie', color:'text-cyan-400', span:2, render:renderStatsWidget }, 'live-feed': { title:'Activité en direct', icon:'fa-bolt', color:'text-yellow-400', render:renderLiveFeed }, 'containers': { title:'Containers favoris', icon:'fa-docker fab', color:'text-blue-400', render:renderContainers }, 'hosts': { title:'Gestion des Hosts', icon:'fa-server', color:'text-emerald-400', span:2, render:renderHosts }, 'console': { title:'Console Ad-Hoc', icon:'fa-terminal', color:'text-violet-400', render:renderConsole }, 'scheduler': { title:'Planificateur', icon:'fa-calendar-alt', color:'text-pink-400', render:renderScheduler }, 'actions': { title:'Actions Rapides', icon:'fa-rocket', color:'text-orange-400', render:renderActions }, }; const DEFAULT_ORDER = ['stats-overview','live-feed','containers','hosts','console','scheduler','actions']; function getWidgetOrder() { return state.widgetOrder || DEFAULT_ORDER; } function renderAllWidgets() { const grid = document.getElementById('widget-grid'); grid.style.setProperty('--grid-cols', state.gridCols); grid.innerHTML = ''; const order = getWidgetOrder().filter(id => !state.hiddenWidgets.includes(id)); order.forEach((id, idx) => { const def = WIDGETS[id]; if (!def) return; const w = document.createElement('div'); w.className = `widget ${def.span ? 'span-2' : ''}`; w.id = `widget-${id}`; w.dataset.widgetId = id; w.draggable = true; w.innerHTML = `

${def.title}

`; grid.appendChild(w); }); // Render each widget order.filter(id => !state.hiddenWidgets.includes(id)).forEach(id => { WIDGETS[id]?.render(); }); setupDragDrop(); renderWidgetToggles(); animateWidgets(); } function renderWidget(id) { WIDGETS[id]?.render(); } function animateWidgets() { anime({ targets:'.widget', translateY:[30,0], opacity:[0,1], delay:anime.stagger(60, {start:100}), duration:600, easing:'easeOutCubic' }); } // ======================== WIDGET RENDERERS ======================== function renderStatsWidget() { const s = state.stats; const el = document.getElementById('wb-stats-overview'); if (!el) return; el.innerHTML = `
${sparklineSVG(64,24,'#22d3ee')}
${s.hostsOnline||0}
Hosts en ligne
${sparklineSVG(64,24,'#8b5cf6')}
${s.tasksExecuted||0}
Tâches exécutées
${sparklineSVG(64,24,'#10b981')}
${s.successRate||0}%
Taux de succès
${sparklineSVG(64,24,'#ec4899')}
${s.uptime||0}%
Disponibilité
`; } function renderLiveFeed() { const el = document.getElementById('wb-live-feed'); if (!el) return; const events = state.liveEvents.slice(0, 12); el.innerHTML = `
${events.length === 0 ? '

En attente d\'événements...

' : events.map(ev => `
${timeAgo(ev.ts)}
${ev.message}
${ev.host}
${ev.status}
`).join('')}
`; } function renderContainers() { const el = document.getElementById('wb-containers'); if (!el) return; const ctrs = (state.containers || []).slice(0, 6); el.innerHTML = `
${ctrs.map(c => `
${c.name}
${c.host||''} ${c.port?':'+c.port:''}
${c.status==='running'?``:``}
`).join('')}
`; } function renderHosts() { const el = document.getElementById('wb-hosts'); if (!el) return; const hosts = state.hosts.slice(0, 8); el.innerHTML = `
${hosts.map(h => ``).join('')}
HôteStatutCPURAMDisqueUptime
${h.name}
${h.status}
${h.cpu||0}%
${h.ram||0}%
${h.disk||0}%
${h.uptime||'—'}
`; } function renderConsole() { const el = document.getElementById('wb-console'); if (!el) return; const execs = (state.executions || []).slice(0, 6); el.innerHTML = `
${execs.map(e => `
${e.cmd||e.command||''}
${e.host||e.target||''} · ${e.type||'Ad-hoc'}
${e.ago||timeAgo(e.ts||Date.now())}
`).join('')}
`; } function renderScheduler() { const el = document.getElementById('wb-scheduler'); if (!el) return; const scheds = (state.schedules || []).slice(0, 5); el.innerHTML = `
${scheds.map(s => `
${s.name}
${s.cron||''} · ${s.targets||'all'}
${s.next_run||'—'}
${s.last_status||'—'}
`).join('')}
`; } function renderActions() { const el = document.getElementById('wb-actions'); if (!el) return; const actions = [ { label:'Mise à jour', icon:'fa-download', color:'from-cyan-500 to-blue-500', action:'update' }, { label:'Redémarrer', icon:'fa-rotate', color:'from-yellow-500 to-orange-500', action:'restart' }, { label:'Backup', icon:'fa-database', color:'from-emerald-500 to-teal-500', action:'backup' }, { label:'Santé', icon:'fa-heart-pulse', color:'from-pink-500 to-rose-500', action:'health' }, { label:'Sync Hosts', icon:'fa-arrows-rotate', color:'from-violet-500 to-purple-500', action:'sync' }, { label:'Nettoyage', icon:'fa-broom', color:'from-amber-500 to-yellow-500', action:'cleanup' }, ]; el.innerHTML = `
${actions.map(a => ``).join('')}
`; } async function containerAction(hostId, containerId, action) { showToast(`${action} du container en cours...`, 'info'); const res = await fetchAPI(`/api/docker/containers/${hostId}/${containerId}/${action}`); if (res) { showToast(`Container ${action} effectué`, 'success'); await loadData(); } else showToast(`Échec: ${action}`, 'error'); } async function triggerAction(action) { const msgs = { update:'Mise à jour déclenchée', restart:'Redémarrage en cours', backup:'Backup lancé', health:'Vérification santé lancée', sync:'Synchronisation des hosts...', cleanup:'Nettoyage Docker lancé' }; showToast(msgs[action] || 'Action exécutée', 'info'); let endpoint = null; if (action === 'sync') endpoint = '/api/hosts/sync'; if (action === 'health') endpoint = '/api/health/refresh'; if (action === 'cleanup') endpoint = '/api/docker/collect-all'; if (endpoint) { const res = await fetch(`${location.protocol}//${location.host}${endpoint}`, { method:'POST', headers:{ 'Content-Type':'application/json', ...(localStorage.getItem('accessToken')?{'Authorization':`Bearer ${localStorage.getItem('accessToken')}`}:{}) } }).catch(()=>null); if (res?.ok) showToast('Action terminée avec succès', 'success'); else showToast('Action simulée (API indisponible)', 'warning'); } else { setTimeout(() => showToast('Action simulée avec succès', 'success'), 1500); } } // ======================== DRAG & DROP ======================== function setupDragDrop() { const widgets = document.querySelectorAll('.widget'); let dragSrc = null; widgets.forEach(w => { w.addEventListener('dragstart', function(e) { dragSrc = this; this.classList.add('dragging'); e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', this.dataset.widgetId); }); w.addEventListener('dragend', function() { this.classList.remove('dragging'); }); w.addEventListener('dragover', function(e) { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; this.style.borderColor = 'rgba(139,92,246,.4)'; }); w.addEventListener('dragleave', function() { this.style.borderColor = ''; }); w.addEventListener('drop', function(e) { e.preventDefault(); this.style.borderColor = ''; if (dragSrc === this) return; const grid = document.getElementById('widget-grid'); const allW = [...grid.children]; const fromIdx = allW.indexOf(dragSrc); const toIdx = allW.indexOf(this); if (fromIdx < toIdx) this.after(dragSrc); else this.before(dragSrc); saveWidgetOrder(); }); }); } function saveWidgetOrder() { const order = [...document.querySelectorAll('.widget')].map(w => w.dataset.widgetId); state.widgetOrder = order; localStorage.setItem('mc_widgetOrder', JSON.stringify(order)); } // ======================== GRID COLUMNS ======================== function setGridCols(n) { state.gridCols = n; document.getElementById('widget-grid').style.setProperty('--grid-cols', n); document.querySelectorAll('.col-btn').forEach(b => b.classList.toggle('active', parseInt(b.dataset.cols) === n)); localStorage.setItem('mc_gridCols', n); } // ======================== FOCUS MODE ======================== function openFocus(widgetId) { state.focusOpen = true; const def = WIDGETS[widgetId]; if (!def) return; const el = document.getElementById('focus-content'); el.innerHTML = `

${def.title}

`; // Re-render with more data const focusBody = document.getElementById('focus-body'); if (widgetId === 'hosts') { const hosts = state.hosts; focusBody.innerHTML = `
${hosts.map(h => `
${h.name}
${h.os||''}
CPU
${h.cpu||0}%
RAM
${h.ram||0}%
Disque
${h.disk||0}%
Uptime: ${h.uptime||'—'}
`).join('')}
`; } else if (widgetId === 'stats-overview') { focusBody.innerHTML = document.getElementById('wb-stats-overview')?.innerHTML || ''; focusBody.innerHTML += `
${state.hosts.slice(0,4).map(h=>`
${h.name}
${sparklineSVG(120,40,'#22d3ee',30)} ${sparklineSVG(120,40,'#8b5cf6',30)}
CPU 24hRAM 24h
`).join('')}
`; } else { focusBody.innerHTML = document.getElementById(`wb-${widgetId}`)?.innerHTML || '

Contenu détaillé non disponible

'; } document.getElementById('focus-overlay').classList.add('open'); anime({ targets:'#focus-content', scale:[.9,1], opacity:[0,1], duration:300, easing:'easeOutCubic' }); } function closeFocus() { state.focusOpen = false; anime({ targets:'#focus-content', scale:.9, opacity:0, duration:200, easing:'easeInCubic', complete:()=>document.getElementById('focus-overlay').classList.remove('open') }); } // ======================== CUSTOMIZE DRAWER ======================== function openCustomizeDrawer() { state.drawerOpen = true; document.getElementById('customize-drawer').classList.add('open'); document.getElementById('drawer-overlay').classList.add('open'); anime({ targets:'#customize-drawer', translateX:[360,0], duration:350, easing:'easeOutCubic' }); } function closeCustomizeDrawer() { state.drawerOpen = false; anime({ targets:'#customize-drawer', translateX:360, duration:250, easing:'easeInCubic', complete:()=>{ document.getElementById('customize-drawer').classList.remove('open'); document.getElementById('drawer-overlay').classList.remove('open'); } }); } function renderWidgetToggles() { const el = document.getElementById('widget-toggles'); if (!el) return; el.innerHTML = Object.entries(WIDGETS).map(([id, def]) => { const checked = !state.hiddenWidgets.includes(id); return ``; }).join(''); } function toggleWidget(id, show) { if (show) state.hiddenWidgets = state.hiddenWidgets.filter(w => w !== id); else if (!state.hiddenWidgets.includes(id)) state.hiddenWidgets.push(id); localStorage.setItem('mc_hiddenWidgets', JSON.stringify(state.hiddenWidgets)); renderAllWidgets(); } function resetLayout() { state.widgetOrder = null; state.hiddenWidgets = []; state.gridCols = 3; localStorage.removeItem('mc_widgetOrder'); localStorage.removeItem('mc_hiddenWidgets'); localStorage.setItem('mc_gridCols', '3'); document.querySelectorAll('.col-btn').forEach(b => b.classList.toggle('active', parseInt(b.dataset.cols) === 3)); renderAllWidgets(); showToast('Disposition réinitialisée', 'success'); closeCustomizeDrawer(); }