From 817f8b4ee7e12fabf5e4261cbc8e93eee97b0a60 Mon Sep 17 00:00:00 2001 From: Bruno Charest Date: Fri, 6 Mar 2026 09:31:08 -0500 Subject: [PATCH] feat: Implement Homelab Automation API v2, introducing a new dashboard, comprehensive backend models, and API routes. --- app/dashboard.html | 785 + app/dashboard_pro.js | 534 + app/factory.py | 8 + app/index.html | 2164 +- app/index.html.bak | 6128 ++++++ app/main.js | 18270 ++++++++-------- app/models/alert.py | 1 - app/models/app_setting.py | 1 - app/models/bootstrap_status.py | 1 - app/models/container_customization.py | 1 - app/models/database.py | 10 +- app/models/docker_alert.py | 1 - app/models/docker_container.py | 1 - app/models/docker_image.py | 1 - app/models/docker_volume.py | 1 - app/models/favorite_container.py | 1 - app/models/favorite_group.py | 1 - app/models/host.py | 1 - app/models/host_metrics.py | 1 - app/models/log.py | 1 - app/models/playbook_lint.py | 1 - app/models/schedule.py | 1 - app/models/schedule_run.py | 1 - app/models/task.py | 1 - app/models/terminal_command_log.py | 1 - app/models/terminal_session.py | 2 - app/models/user.py | 1 - app/routes/alerts.py | 57 +- app/routes/terminal.py | 1 - data/homelab.db | Bin 0 -> 487424 bytes debug_alerts.py | 38 + logs/tasks_logs/.metadata_cache.json | 2 +- ...i7.home_Vérification_de_santé_completed.md | 58 + ...nt.home_Vérification_de_santé_completed.md | 58 + ...nt.home_Vérification_de_santé_completed.md | 58 + ...nt.home_Vérification_de_santé_completed.md | 58 + tasks_logs/.metadata_cache.json | 2 +- 37 files changed, 18142 insertions(+), 10110 deletions(-) create mode 100644 app/dashboard.html create mode 100644 app/dashboard_pro.js create mode 100644 app/index.html.bak create mode 100644 data/homelab.db create mode 100644 debug_alerts.py create mode 100644 logs/tasks_logs/2026/03/05/task_200829_89e3b8_hp2.i7.home_Vérification_de_santé_completed.md create mode 100644 logs/tasks_logs/2026/03/05/task_200906_908950_jump.point.home_Vérification_de_santé_completed.md create mode 100644 logs/tasks_logs/2026/03/05/task_201037_5b6194_jump.point.home_Vérification_de_santé_completed.md create mode 100644 logs/tasks_logs/2026/03/05/task_201132_e47a19_jump.point.home_Vérification_de_santé_completed.md diff --git a/app/dashboard.html b/app/dashboard.html new file mode 100644 index 0000000..87b329a --- /dev/null +++ b/app/dashboard.html @@ -0,0 +1,785 @@ + + + + + + Homelab — Mission Control + + + + + + + + + + + + + + + + + +
+ +
+
+

Dashboard

+

Vue d'ensemble de votre infrastructure

+
+
+ Colonnes + + + +
+
+ +
+
+ + +
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+

Personnaliser

+ +
+

Activez ou désactivez les widgets affichés sur votre dashboard.

+
+
+ +
+
+ + +
+ + + \ No newline at end of file diff --git a/app/dashboard_pro.js b/app/dashboard_pro.js new file mode 100644 index 0000000..2783038 --- /dev/null +++ b/app/dashboard_pro.js @@ -0,0 +1,534 @@ +// ======================== 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(); +} + diff --git a/app/factory.py b/app/factory.py index a3660ae..866114b 100644 --- a/app/factory.py +++ b/app/factory.py @@ -101,6 +101,14 @@ def create_app() -> FastAPI: headers={"Service-Worker-Allowed": "/"}, ) + @app.get("/dashboard.html", response_class=HTMLResponse) + async def serve_dashboard(): + """Serve le nouveau dashboard Mission Control Pro.""" + dashboard_path = settings.base_dir / "dashboard.html" + if dashboard_path.exists(): + return dashboard_path.read_text(encoding='utf-8') + return HTMLResponse(content="Fichier dashboard.html non trouvé", status_code=404) + @app.get("/api", response_class=HTMLResponse) async def api_home(): """Page d'accueil de l'API.""" diff --git a/app/index.html b/app/index.html index bc85de1..89e38d2 100644 --- a/app/index.html +++ b/app/index.html @@ -8,29 +8,75 @@ - + - + + @@ -3824,7 +4188,37 @@
- + + +
+ +
@@ -3845,597 +4239,250 @@ if (loader) loader.style.display = 'none'; }, 10000); - - -
- - - - - + Mission Control +
+ +
+ +
+
+ + +
- -
-
-
-

- Automation Dashboard -

-

- Gérez votre homelab avec puissance et élégance. Surveillance, automatisation et contrôle en temps réel. -

- -
- - -
-
- - -
-
-
12
-
Hosts En Ligne
-
-
-
48
-
Tâches Exécutées
-
-
-
98.5%
-
Taux de Succès
-
-
-
99.9%
-
Disponibilité
-
-
-
-
- - -
-
-

Tableau de Bord

- - -
- -
- -
-
-

- - Console Ad-Hoc -

-
- -
-
- - - - - -
-
-
0
-
Succès
-
-
-
0
-
Échecs
-
-
-
0
-
Total
-
-
- - -
-
- - Dernières exécutions - -
-
-

- Chargement... -

-
- -
-
- - -
-

Actions Rapides

- -
- - - - -
-
-
-
-
-
-

- - Containers favoris -

-
- - - - -
-
-
-
- - - -
-
-
-

- Chargement... -

-
-
-
- - -
-
-
-

- Planificateur -

- Voir tout → -
- - -
-
-
0
-
Actifs
-
-
-
--
-
Prochaine
-
-
-
0
-
Échecs 24h
-
-
- - -
-

Chargement...

-
- - -
-
- -
-
-
-

Gestion des Hosts

-
- -
- - -
-
-
- -
- -
-
-
-
-
+
+ +
+
+

Dashboard

+

Vue d'ensemble de votre infrastructure

+
+
+ Colonnes + + + +
+ +
+
+ +
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+

Personnaliser

+ +
+

Activez ou désactivez les widgets affichés sur votre dashboard.

+
+
+ +
+
+ + +
+
-
-
-
-

- Gestion des Hosts -

-

Gérez et surveillez tous vos serveurs depuis un seul endroit

+
+
+
+

Inventaire des Hosts

+

+ hôtes + + 0 actifs +

- -
-
-

Inventaire des Hosts

-
- -
- - -
-
+
+ + +
+
+ + +
+ + + + +
+ +
+
+ +
+ +
-
- + +
+
+ + + +
+
-
+ +
+ +
+
-
-
- -
-

- Gestion des Playbooks -

-

Gérez vos scripts d'automatisation Ansible depuis un seul endroit

+
+
+
+

Playbooks

+

+ modules +

- -
- -
- -
-
- - -
- - 0 playbooks - -
- - -
- - -
+
+ + +
+
+ +
+
+ +
+ +
- -
- Catégorie: - - - - - - -
- - -
- -
- -

Chargement des playbooks...

+ +
+
+ + + +
-
+ +
+ +
+
-
-
-

Gestion des Tâches

+
+
+
+

File d'exécution

+

+ tâches +

+
+
+ + +
+
-
+

Tâches

0
@@ -4560,28 +4607,35 @@
- -
-
+
-
-
- -
-

- Planificateur -

-

Planifiez et orchestrez vos playbooks - Exécutions automatiques

+
+
+
+

Planificateur

+

+ Automation orchestrée +

+
+ + +
+
@@ -4698,22 +4752,29 @@
-
-
-
-
+ +
-
-
-
-

- Centre d'Alertes -

-

Tous les messages reçus (toasts) avec statut lu/non-lu, date et catégorie

+
+
+
+

Centre d'Alertes

+

+ nouvelles notifications +

+
+ + +
+
@@ -4733,21 +4794,28 @@
-
-
-
+ +
-
-
- -
-

- Docker Hosts -

-

Surveillance et gestion des containers Docker

+
+
+
+

Docker Infrastructure

+

+ Ordonnancement de containers +

+
+ + +
+
@@ -4803,9 +4871,8 @@

Chargement des hosts Docker...

-
-
-
+ + @@ -4870,35 +4937,33 @@
-

+                
Chargement...
-
-
- -
+
+
+
+
-
- -

- Containers -

-
-

Tous les containers de vos Docker hosts

-
-
- - +

Gestion des Containers

+

+ instances actives +

+
+ Actualisé: — + +
+
@@ -5059,7 +5124,7 @@
-
+
@@ -5181,14 +5246,15 @@
-
-
-
-

- Configuration -

-

Paramètres et outils d'administration

+
+
+
+

Configuration

+

+ Paramètres système et administration +

+
@@ -5242,20 +5308,28 @@
-
-
-
+ +
-
-
-
-

- Logs Système -

-

Consultez l'historique des opérations et événements système

+
+
+
+

Logs Système

+

+ Historique des événements +

+
+ + +
+
@@ -5290,9 +5364,10 @@
-
-
-
+ + + + @@ -5349,7 +5424,7 @@