homelab_automation/app/dashboard_pro.js
Bruno Charest 817f8b4ee7
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
feat: Implement Homelab Automation API v2, introducing a new dashboard, comprehensive backend models, and API routes.
2026-03-06 09:31:08 -05:00

535 lines
32 KiB
JavaScript

// ======================== 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 =>
`<div class="cmd-result" onclick="navigateTo('${r.page}');closeCmdPalette()">
<i class="fas ${r.icon} text-gray-500 mr-3"></i>
<div><div class="text-sm">${r.label}</div>${r.sub ? `<div class="text-xs text-gray-500">${r.sub}</div>` : ''}</div>
</div>`
).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 `<svg class="sparkline" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}"><polyline fill="none" stroke="${color}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" points="${pts}"/><polyline fill="url(#sg)" stroke="none" points="0,${h} ${pts} ${w},${h}"/><defs><linearGradient id="sg" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="${color}" stop-opacity=".15"/><stop offset="1" stop-color="${color}" stop-opacity="0"/></linearGradient></defs></svg>`;
}
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 = `<i class="fas ${icons[type]||icons.info} text-lg mt-0.5"></i><div class="flex-1"><div class="text-sm font-medium">${message}</div></div><button onclick="this.closest('.toast').remove()" class="text-gray-500 hover:text-white ml-2"><i class="fas fa-xmark"></i></button><div class="toast-progress ${type==='error'?'bg-red-500':type==='warning'?'bg-yellow-500':'bg-cyan-400'}" id="${id}-bar" style="width:100%"></div>`;
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 = `<div class="widget-header" ondblclick="openFocus('${id}')"><div class="flex items-center gap-3"><i class="fas ${def.icon} ${def.color}"></i><h3 class="font-heading font-semibold text-sm">${def.title}</h3></div><div class="flex items-center gap-2"><button onclick="openFocus('${id}')" class="text-gray-600 hover:text-gray-300 text-xs" title="Agrandir"><i class="fas fa-expand"></i></button></div></div><div class="widget-body" id="wb-${id}"></div>`;
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 = `<div class="grid grid-cols-2 sm:grid-cols-4 gap-4">
<div class="kpi-card"><div class="flex items-center justify-between mb-2"><i class="fas fa-server text-cyan-400 text-lg"></i>${sparklineSVG(64,24,'#22d3ee')}</div><div class="kpi-value text-cyan-400">${s.hostsOnline||0}</div><div class="kpi-label">Hosts en ligne</div></div>
<div class="kpi-card"><div class="flex items-center justify-between mb-2"><i class="fas fa-list-check text-violet-400 text-lg"></i>${sparklineSVG(64,24,'#8b5cf6')}</div><div class="kpi-value text-violet-400">${s.tasksExecuted||0}</div><div class="kpi-label">Tâches exécutées</div></div>
<div class="kpi-card"><div class="flex items-center justify-between mb-2"><i class="fas fa-bullseye text-emerald-400 text-lg"></i>${sparklineSVG(64,24,'#10b981')}</div><div class="kpi-value text-emerald-400">${s.successRate||0}%</div><div class="kpi-label">Taux de succès</div></div>
<div class="kpi-card"><div class="flex items-center justify-between mb-2"><i class="fas fa-signal text-pink-400 text-lg"></i>${sparklineSVG(64,24,'#ec4899')}</div><div class="kpi-value text-pink-400">${s.uptime||0}%</div><div class="kpi-label">Disponibilité</div></div>
</div>`;
}
function renderLiveFeed() {
const el = document.getElementById('wb-live-feed');
if (!el) return;
const events = state.liveEvents.slice(0, 12);
el.innerHTML = `<div style="max-height:320px;overflow-y:auto">${events.length === 0 ? '<div class="text-center text-gray-600 py-8"><i class="fas fa-satellite-dish text-2xl mb-2"></i><p class="text-sm">En attente d\'événements...</p></div>' : events.map(ev => `<div class="feed-item"><div class="feed-time">${timeAgo(ev.ts)}</div><div class="flex-1"><div class="text-sm">${ev.message}</div><div class="text-xs text-gray-500 mt-0.5">${ev.host}</div></div><span class="feed-badge ${statusBadge(ev.status)}">${ev.status}</span></div>`).join('')}</div>`;
}
function renderContainers() {
const el = document.getElementById('wb-containers');
if (!el) return;
const ctrs = (state.containers || []).slice(0, 6);
el.innerHTML = `<div class="space-y-2">${ctrs.map(c => `<div class="flex items-center justify-between p-3 rounded-xl bg-white/[.02] border border-white/[.04] hover:border-white/10 transition-all group">
<div class="flex items-center gap-3"><span class="status-dot ${c.status==='running'?'online':'offline'}"></span><div><div class="text-sm font-medium">${c.name}</div><div class="text-xs text-gray-500">${c.host||''} ${c.port?':'+c.port:''}</div></div></div>
<div class="flex items-center gap-1.5 opacity-0 group-hover:opacity-100 transition-opacity">${c.status==='running'?`<button onclick="containerAction('${c.host_id||''}','${c.id}','restart')" class="w-7 h-7 rounded-lg bg-white/5 hover:bg-yellow-500/20 text-gray-400 hover:text-yellow-400 flex items-center justify-center text-xs" title="Redémarrer"><i class="fas fa-rotate"></i></button><button onclick="containerAction('${c.host_id||''}','${c.id}','stop')" class="w-7 h-7 rounded-lg bg-white/5 hover:bg-red-500/20 text-gray-400 hover:text-red-400 flex items-center justify-center text-xs" title="Arrêter"><i class="fas fa-stop"></i></button>`:`<button onclick="containerAction('${c.host_id||''}','${c.id}','start')" class="w-7 h-7 rounded-lg bg-white/5 hover:bg-emerald-500/20 text-gray-400 hover:text-emerald-400 flex items-center justify-center text-xs" title="Démarrer"><i class="fas fa-play"></i></button>`}</div>
</div>`).join('')}</div>`;
}
function renderHosts() {
const el = document.getElementById('wb-hosts');
if (!el) return;
const hosts = state.hosts.slice(0, 8);
el.innerHTML = `<div class="overflow-x-auto"><table class="w-full text-sm"><thead><tr class="text-xs text-gray-500 border-b border-white/5"><th class="text-left pb-2 font-medium">Hôte</th><th class="text-left pb-2 font-medium">Statut</th><th class="text-left pb-2 font-medium">CPU</th><th class="text-left pb-2 font-medium">RAM</th><th class="text-left pb-2 font-medium">Disque</th><th class="text-left pb-2 font-medium">Uptime</th></tr></thead><tbody>${hosts.map(h => `<tr class="border-b border-white/[.03] hover:bg-white/[.02] transition-colors"><td class="py-2.5"><div class="flex items-center gap-2"><span class="status-dot ${h.status==='online'?'online':'offline'}"></span><span class="font-medium">${h.name}</span></div></td><td><span class="feed-badge ${statusBadge(h.status==='online'?'success':'error')}">${h.status}</span></td><td><div class="flex items-center gap-2"><div class="progress-bar w-16"><div class="progress-fill ${progressColor(h.cpu||0)}" style="width:${h.cpu||0}%"></div></div><span class="text-xs text-gray-400">${h.cpu||0}%</span></div></td><td><div class="flex items-center gap-2"><div class="progress-bar w-16"><div class="progress-fill ${progressColor(h.ram||0)}" style="width:${h.ram||0}%"></div></div><span class="text-xs text-gray-400">${h.ram||0}%</span></div></td><td><div class="flex items-center gap-2"><div class="progress-bar w-16"><div class="progress-fill ${progressColor(h.disk||0)}" style="width:${h.disk||0}%"></div></div><span class="text-xs text-gray-400">${h.disk||0}%</span></div></td><td class="text-gray-400">${h.uptime||'—'}</td></tr>`).join('')}</tbody></table></div>`;
}
function renderConsole() {
const el = document.getElementById('wb-console');
if (!el) return;
const execs = (state.executions || []).slice(0, 6);
el.innerHTML = `<div class="space-y-2">${execs.map(e => `<div class="flex items-center justify-between p-2.5 rounded-lg bg-white/[.02] border border-white/[.04]"><div class="flex items-center gap-3"><span class="w-7 h-7 rounded-lg ${e.status==='success'?'bg-emerald-500/15 text-emerald-400':'bg-red-500/15 text-red-400'} flex items-center justify-center text-xs"><i class="fas ${e.status==='success'?'fa-check':'fa-xmark'}"></i></span><div><div class="text-sm font-mono">${e.cmd||e.command||''}</div><div class="text-xs text-gray-500">${e.host||e.target||''} · ${e.type||'Ad-hoc'}</div></div></div><span class="text-xs text-gray-500">${e.ago||timeAgo(e.ts||Date.now())}</span></div>`).join('')}</div>`;
}
function renderScheduler() {
const el = document.getElementById('wb-scheduler');
if (!el) return;
const scheds = (state.schedules || []).slice(0, 5);
el.innerHTML = `<div class="space-y-2">${scheds.map(s => `<div class="flex items-center justify-between p-3 rounded-xl bg-white/[.02] border border-white/[.04]"><div class="flex items-center gap-3"><span class="w-8 h-8 rounded-lg ${s.enabled?'bg-violet-500/15 text-violet-400':'bg-gray-500/15 text-gray-500'} flex items-center justify-center"><i class="fas ${s.enabled?'fa-clock':'fa-pause'}"></i></span><div><div class="text-sm font-medium">${s.name}</div><div class="text-xs text-gray-500">${s.cron||''} · ${s.targets||'all'}</div></div></div><div class="text-right"><div class="text-xs ${s.enabled?'text-cyan-400':'text-gray-600'}">${s.next_run||'—'}</div><span class="feed-badge mt-1 ${statusBadge(s.last_status||'info')}">${s.last_status||'—'}</span></div></div>`).join('')}</div>`;
}
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 = `<div class="grid grid-cols-2 gap-3">${actions.map(a => `<button onclick="triggerAction('${a.action}')" class="action-btn flex-col gap-2 py-4 hover:scale-[1.03] active:scale-100 group"><div class="w-10 h-10 rounded-xl bg-gradient-to-br ${a.color} flex items-center justify-center text-white shadow-lg group-hover:shadow-xl transition-shadow"><i class="fas ${a.icon}"></i></div><span class="text-xs">${a.label}</span></button>`).join('')}</div>`;
}
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 = `<div class="flex items-center justify-between mb-6"><div class="flex items-center gap-3"><i class="fas ${def.icon} ${def.color} text-xl"></i><h2 class="font-heading text-xl font-bold">${def.title}</h2></div><button onclick="closeFocus()" class="w-9 h-9 rounded-xl bg-white/5 hover:bg-white/10 flex items-center justify-center text-gray-400 hover:text-white transition-colors"><i class="fas fa-xmark"></i></button></div><div id="focus-body"></div>`;
// Re-render with more data
const focusBody = document.getElementById('focus-body');
if (widgetId === 'hosts') {
const hosts = state.hosts;
focusBody.innerHTML = `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">${hosts.map(h => `<div class="p-4 rounded-xl bg-white/[.03] border border-white/[.06]"><div class="flex items-center justify-between mb-3"><div class="flex items-center gap-2"><span class="status-dot ${h.status==='online'?'online':'offline'}"></span><span class="font-heading font-semibold">${h.name}</span></div><span class="text-xs text-gray-500">${h.os||''}</span></div><div class="space-y-2"><div class="flex items-center justify-between text-sm"><span class="text-gray-500">CPU</span><div class="flex items-center gap-2"><div class="progress-bar w-24"><div class="progress-fill ${progressColor(h.cpu||0)}" style="width:${h.cpu||0}%"></div></div><span>${h.cpu||0}%</span></div></div><div class="flex items-center justify-between text-sm"><span class="text-gray-500">RAM</span><div class="flex items-center gap-2"><div class="progress-bar w-24"><div class="progress-fill ${progressColor(h.ram||0)}" style="width:${h.ram||0}%"></div></div><span>${h.ram||0}%</span></div></div><div class="flex items-center justify-between text-sm"><span class="text-gray-500">Disque</span><div class="flex items-center gap-2"><div class="progress-bar w-24"><div class="progress-fill ${progressColor(h.disk||0)}" style="width:${h.disk||0}%"></div></div><span>${h.disk||0}%</span></div></div><div class="text-xs text-gray-500 mt-2">Uptime: ${h.uptime||'—'}</div></div></div>`).join('')}</div>`;
} else if (widgetId === 'stats-overview') {
focusBody.innerHTML = document.getElementById('wb-stats-overview')?.innerHTML || '';
focusBody.innerHTML += `<div class="mt-6 grid grid-cols-2 gap-4">${state.hosts.slice(0,4).map(h=>`<div class="p-4 rounded-xl bg-white/[.03] border border-white/[.06]"><div class="font-heading font-semibold mb-2">${h.name}</div><div class="flex gap-4">${sparklineSVG(120,40,'#22d3ee',30)} ${sparklineSVG(120,40,'#8b5cf6',30)}</div><div class="flex gap-4 mt-1 text-xs text-gray-500"><span>CPU 24h</span><span>RAM 24h</span></div></div>`).join('')}</div>`;
} else {
focusBody.innerHTML = document.getElementById(`wb-${widgetId}`)?.innerHTML || '<p class="text-gray-500">Contenu détaillé non disponible</p>';
}
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 `<label class="flex items-center justify-between p-3 rounded-xl hover:bg-white/[.03] cursor-pointer transition-colors mb-1"><div class="flex items-center gap-3"><i class="fas ${def.icon} ${def.color}"></i><span class="text-sm font-medium">${def.title}</span></div><input type="checkbox" ${checked?'checked':''} onchange="toggleWidget('${id}',this.checked)" class="w-4 h-4 accent-violet-500 rounded"></label>`;
}).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();
}