// Configuration et état let config = null; let charts = new Map(); // Map des graphiques Chart.js let updateInterval = null; let currentTabIndex = 0; let totalTabs = 0; let consecutiveErrors = 0; const MAX_CONSECUTIVE_ERRORS = 3; // Variables pour les apps let appsRendered = false; let lastAppsTabId = null; // Variables pour le swipe let touchStartX = 0; let touchEndX = 0; let touchStartY = 0; let touchEndY = 0; const SWIPE_THRESHOLD = 50; // Minimum de pixels pour déclencher un swipe // Initialisation document.addEventListener('DOMContentLoaded', async () => { await loadConfig(); await loadData(); // Initialiser le swipe initSwipeGestures(); // Démarrer les mises à jour automatiques (toutes les 2 secondes) updateInterval = setInterval(loadData, 2000); }); // === Chargement configuration === async function loadConfig() { try { const response = await fetch('/api/config'); config = await response.json(); // Appliquer le thème document.documentElement.setAttribute('data-theme', config.theme || 'dark'); // Réinitialiser le flag de rendu des apps (la config peut avoir changé) appsRendered = false; // Les apps seront rendues dans loadData() après création des panes } catch (error) { console.error('Erreur chargement config:', error); showError('Impossible de charger la configuration'); } } // === Chargement données === async function loadData() { try { const response = await fetch('/api/lhm/data'); const data = await response.json(); if (data.error) { consecutiveErrors++; if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) { showError(data.error); } return; } // Réinitialiser le compteur d'erreurs en cas de succès consecutiveErrors = 0; // Cacher les états d'erreur/vide document.getElementById('error-state').style.display = 'none'; // Vérifier s'il y a des capteurs OU des apps configurés const hasSensors = data.tabs.some(tab => tab.sensors.length > 0); const hasApps = config && config.apps && config.apps.length > 0; if (!hasSensors && !hasApps) { document.getElementById('empty-state').style.display = 'block'; document.getElementById('tabs-nav').style.display = 'none'; document.getElementById('tabs-content').style.display = 'none'; return; } document.getElementById('empty-state').style.display = 'none'; // Render les onglets et données renderTabs(data.tabs); renderSensors(data.tabs); // Render les apps (dans le bon onglet ou section séparée) try { renderApps(); } catch (appError) { console.error('Erreur rendu apps:', appError); } // Mettre à jour le timestamp updateTimestamp(); } catch (error) { console.error('Erreur chargement données:', error); consecutiveErrors++; // N'afficher l'erreur que si plusieurs échecs consécutifs // et qu'il n'y a pas déjà du contenu affiché if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) { const tabsContent = document.getElementById('tabs-content'); if (!tabsContent || tabsContent.children.length === 0) { showError('Erreur de connexion à LibreHardwareMonitor'); } } } } // === Render onglets === function renderTabs(tabs) { const tabsNav = document.getElementById('tabs-nav'); // Mettre à jour le nombre total d'onglets totalTabs = tabs.length; // Si un seul onglet, on ne l'affiche pas mais on crée quand même le swipe indicator if (tabs.length === 1) { tabsNav.style.display = 'none'; removeSwipeIndicators(); return; } tabsNav.style.display = 'flex'; // Ne re-render que si nécessaire if (tabsNav.dataset.rendered !== 'true') { tabsNav.innerHTML = ''; tabs.forEach((tab, index) => { const button = document.createElement('button'); button.className = 'tab-button' + (index === 0 ? ' active' : ''); button.textContent = tab.name; button.dataset.tabId = tab.id; button.dataset.tabIndex = index; button.addEventListener('click', () => switchTab(tab.id)); tabsNav.appendChild(button); }); tabsNav.dataset.rendered = 'true'; // Créer les indicateurs de swipe createSwipeIndicators(tabs.length); } } function createSwipeIndicators(count) { // Supprimer les anciens indicateurs removeSwipeIndicators(); if (count <= 1) return; const container = document.createElement('div'); container.className = 'swipe-indicators'; container.id = 'swipe-indicators'; for (let i = 0; i < count; i++) { const dot = document.createElement('span'); dot.className = 'swipe-dot' + (i === 0 ? ' active' : ''); dot.dataset.index = i; dot.addEventListener('click', () => switchTabByIndex(i)); container.appendChild(dot); } // Ajouter après les tabs-nav const tabsNav = document.getElementById('tabs-nav'); tabsNav.parentNode.insertBefore(container, tabsNav.nextSibling); } function removeSwipeIndicators() { const existing = document.getElementById('swipe-indicators'); if (existing) { existing.remove(); } } function switchTab(tabId) { // Désactiver tous les tabs document.querySelectorAll('.tab-button').forEach(btn => { btn.classList.remove('active'); }); // Activer le bon const activeBtn = document.querySelector(`.tab-button[data-tab-id="${tabId}"]`); if (activeBtn) { activeBtn.classList.add('active'); // Mettre à jour l'index courant currentTabIndex = parseInt(activeBtn.dataset.tabIndex) || 0; } // Basculer les panes document.querySelectorAll('.tab-pane').forEach(pane => { pane.classList.remove('active'); }); const targetPane = document.getElementById(`pane-${tabId}`); if (targetPane) { targetPane.classList.add('active'); } // Mettre à jour les indicateurs updateSwipeIndicators(); } function switchTabByIndex(index) { const buttons = document.querySelectorAll('.tab-button'); if (index >= 0 && index < buttons.length) { const tabId = buttons[index].dataset.tabId; switchTab(tabId); } } function updateSwipeIndicators() { const indicators = document.querySelectorAll('.swipe-dot'); indicators.forEach((dot, index) => { dot.classList.toggle('active', index === currentTabIndex); }); } // === Render capteurs === function renderSensors(tabs) { const tabsContent = document.getElementById('tabs-content'); tabsContent.style.display = 'block'; // Créer les panes si nécessaire tabs.forEach((tab, index) => { let pane = document.getElementById(`pane-${tab.id}`); if (!pane) { pane = document.createElement('div'); pane.id = `pane-${tab.id}`; pane.className = 'tab-pane' + (index === 0 ? ' active' : ''); tabsContent.appendChild(pane); } // Créer le grid de capteurs let grid = pane.querySelector('.sensors-grid'); if (!grid) { grid = document.createElement('div'); grid.className = 'sensors-grid'; pane.appendChild(grid); } // Render chaque capteur tab.sensors.forEach(sensor => { console.log('--- Render sensor:', sensor.name, 'show_graph:', sensor.show_graph, 'viz_type:', sensor.viz_type); let card = grid.querySelector(`[data-sensor-id="${CSS.escape(sensor.id)}"]`); // Déterminer le viz_type à utiliser const vizType = sensor.show_graph ? (sensor.viz_type || 'line') : 'none'; // Vérifier si on doit recréer la carte let needsRecreation = false; if (card) { const currentVizType = card.dataset.vizType || 'none'; const currentShowTypeBadge = card.dataset.showTypeBadge !== 'false'; const newShowTypeBadge = sensor.show_type_badge !== false; const currentFontFamily = card.dataset.fontFamily || 'system'; const newFontFamily = sensor.font_family || 'system'; const currentFontBold = card.dataset.fontBold !== 'false'; const newFontBold = sensor.font_bold !== false; // Recréer si le type de visualisation a changé if (currentVizType !== vizType) { console.log(`⚠️ Changement de visualisation: ${currentVizType} → ${vizType}, recréation carte`); needsRecreation = true; // Supprimer l'ancien graphique du Map si existant if (charts.has(sensor.id)) { const oldChart = charts.get(sensor.id); // Détruire proprement si c'est un Chart.js if (oldChart && typeof oldChart.destroy === 'function') { oldChart.destroy(); } charts.delete(sensor.id); } } // Recréer si l'option badge a changé else if (currentShowTypeBadge !== newShowTypeBadge) { console.log(`⚠️ Changement badge type: ${currentShowTypeBadge} → ${newShowTypeBadge}, recréation carte`); needsRecreation = true; } // Recréer si la police a changé else if (currentFontFamily !== newFontFamily) { console.log(`⚠️ Changement police: ${currentFontFamily} → ${newFontFamily}, recréation carte`); needsRecreation = true; } // Recréer si le bold a changé else if (currentFontBold !== newFontBold) { console.log(`⚠️ Changement bold: ${currentFontBold} → ${newFontBold}, recréation carte`); needsRecreation = true; } // Vérifier aussi si le canvas manque else if (sensor.show_graph) { if (vizType === 'gauge' && !card.querySelector('.sensor-gauge')) { console.log('⚠️ Jauge manquante, recréation carte'); needsRecreation = true; } else if (vizType === 'line' && !card.querySelector('.sensor-chart')) { console.log('⚠️ Graphique manquant, recréation carte'); needsRecreation = true; } } } if (!card || needsRecreation) { console.log(needsRecreation ? 'Recréation carte...' : 'Carte n\'existe pas, création...'); // Supprimer l'ancienne si elle existe if (card) { card.remove(); } // Créer un objet sensor modifié pour l'affichage const sensorForDisplay = { ...sensor, viz_type: sensor.show_graph ? (sensor.viz_type || 'line') : 'none' }; // Créer la carte card = createSensorCard(sensorForDisplay); grid.appendChild(card); // Créer la visualisation si nécessaire if (sensor.show_graph) { console.log('show_graph = true, création visualisation...'); if (vizType === 'gauge') { console.log('→ Appel createGauge'); createGauge(sensorForDisplay); } else { console.log('→ Appel createChart'); createChart(sensorForDisplay); } } else { console.log('show_graph = false, pas de visualisation'); } } else { // Mettre à jour la valeur updateSensorCard(card, sensor); } // Mettre à jour la visualisation if (sensor.show_graph && charts.has(sensor.id)) { if (vizType === 'gauge') { updateGauge(sensor); } else { updateChart(sensor); } } }); }); } function createSensorCard(sensor) { console.log('=== createSensorCard ==='); console.log('Sensor:', sensor); console.log('show_graph:', sensor.show_graph); console.log('viz_type:', sensor.viz_type); const card = document.createElement('div'); card.className = 'sensor-card'; // Ajouter la classe de taille const cardSize = sensor.card_size || 'medium'; card.classList.add(`card-${cardSize}`); // Ajouter la classe de police const fontFamily = sensor.font_family || 'system'; card.classList.add(`font-family-${fontFamily}`); // Ajouter la classe bold/normal const fontBold = sensor.font_bold !== false; // true par défaut card.classList.add(fontBold ? 'font-bold' : 'font-normal'); // Ajouter la classe de taille de police const fontSize = sensor.font_size || 'small'; card.classList.add(`font-${fontSize}`); // Ajouter la classe pour masquer la valeur sur mobile si activé if (sensor.hide_value_mobile) { card.classList.add('hide-value-mobile'); } card.dataset.sensorId = sensor.id; card.dataset.vizType = sensor.show_graph ? (sensor.viz_type || 'line') : 'none'; card.dataset.showTypeBadge = sensor.show_type_badge !== false ? 'true' : 'false'; card.dataset.fontFamily = fontFamily; card.dataset.fontBold = fontBold ? 'true' : 'false'; const valueClass = getSensorValueClass(sensor); // Déterminer quel type de visualisation afficher let vizHtml = ''; if (sensor.show_graph && sensor.viz_type === 'gauge') { console.log('→ Création HTML jauge DIRECTE'); const gaugeSize = (sensor.gauge_options && sensor.gauge_options.size) || 'medium'; // Définir les dimensions selon la taille const gaugeSizes = { tiny: 60, xs: 90, small: 120, medium: 180, large: 240 }; const size = gaugeSizes[gaugeSize] || 180; // Canvas DIRECT avec dimensions explicites vizHtml = ``; } else if (sensor.show_graph && (sensor.viz_type === 'line' || !sensor.viz_type)) { console.log('→ Création HTML courbe DIRECTE'); const chartHeight = (sensor.chart_options && sensor.chart_options.height) || 'medium'; // Définir les hauteurs selon la taille const chartHeights = { tiny: 50, xs: 70, small: 90, medium: 120, large: 180 }; const height = chartHeights[chartHeight] || 120; // Canvas DIRECT avec dimensions explicites (width sera 100%) vizHtml = ``; } else { console.log('→ Pas de visualisation'); } // Badge de type (affiché par défaut, peut être masqué) const showTypeBadge = sensor.show_type_badge !== false; const typeBadgeHtml = showTypeBadge ? `
${sensor.type}
` : ''; card.innerHTML = `
${sensor.name}
${typeBadgeHtml}
${sensor.value}
${vizHtml} `; console.log('HTML carte créée avec canvas DIRECT'); return card; } function updateSensorCard(card, sensor) { const valueDiv = card.querySelector('[data-value]'); valueDiv.textContent = sensor.value; valueDiv.className = 'sensor-value ' + getSensorValueClass(sensor); } function getSensorValueClass(sensor) { let classes = [sensor.type]; // Ajouter des classes selon la valeur if (sensor.type === 'temperature') { const temp = parseFloat(sensor.value); if (temp < 40) classes.push('cold'); else if (temp > 70) classes.push('hot'); } else if (sensor.type === 'percentage') { const percent = parseFloat(sensor.value); if (percent > 90) classes.push('critical'); else if (percent > 70) classes.push('high'); } return classes.join(' '); } // === Graphiques Chart.js === function createChart(sensor) { console.log('=== createChart appelé ==='); console.log('Sensor:', sensor); const sensorId = sensor.id; const card = document.querySelector(`[data-sensor-id="${CSS.escape(sensorId)}"]`); console.log('Card trouvée:', card); if (!card) { console.error('ERREUR: Card non trouvée pour', sensorId); return; } const canvas = card.querySelector('.sensor-chart'); console.log('Canvas trouvé:', canvas); if (!canvas) { console.error('ERREUR: Canvas .sensor-chart non trouvé dans la carte'); return; } const ctx = canvas.getContext('2d'); console.log('Context 2D:', ctx); // Obtenir le type de capteur const sensorType = sensor.type || 'generic'; // Couleurs selon le type let borderColor, backgroundColor; switch(sensorType) { case 'temperature': borderColor = 'rgba(255, 152, 0, 1)'; // Orange backgroundColor = 'rgba(255, 152, 0, 0.1)'; break; case 'percentage': case 'load': borderColor = 'rgba(74, 158, 255, 1)'; // Bleu backgroundColor = 'rgba(74, 158, 255, 0.1)'; break; case 'fan': borderColor = 'rgba(76, 175, 80, 1)'; // Vert backgroundColor = 'rgba(76, 175, 80, 0.1)'; break; case 'power': borderColor = 'rgba(244, 67, 54, 1)'; // Rouge backgroundColor = 'rgba(244, 67, 54, 0.1)'; break; case 'voltage': borderColor = 'rgba(156, 39, 176, 1)'; // Violet backgroundColor = 'rgba(156, 39, 176, 0.1)'; break; case 'frequency': case 'clock': borderColor = 'rgba(255, 193, 7, 1)'; // Jaune backgroundColor = 'rgba(255, 193, 7, 0.1)'; break; default: borderColor = 'rgba(74, 158, 255, 1)'; backgroundColor = 'rgba(74, 158, 255, 0.1)'; } console.log('Création Chart.js...'); const chart = new Chart(ctx, { type: 'line', data: { labels: [], datasets: [{ data: [], borderColor: borderColor, backgroundColor: backgroundColor, borderWidth: 2, tension: 0.4, pointRadius: 0, fill: true }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false }, tooltip: { enabled: false } }, scales: { x: { display: false }, y: { display: true, grid: { color: 'rgba(255, 255, 255, 0.05)', drawBorder: false }, ticks: { color: 'rgba(255, 255, 255, 0.3)', font: { size: 10 } } } }, animation: { duration: 0 } } }); console.log('Chart créé:', chart); charts.set(sensorId, chart); console.log('Chart stocké dans Map'); // Charger l'historique initial console.log('Chargement historique initial...'); updateChart(sensor); } async function updateChart(sensor) { console.log('=== updateChart appelé ===', sensor.name); const sensorId = sensor.id; const chart = charts.get(sensorId); if (!chart) { console.error('ERREUR: Chart non trouvé dans Map pour', sensorId); return; } // Vérifier si le chart est dans un onglet actif const canvas = chart.canvas; if (canvas) { const pane = canvas.closest('.tab-pane'); if (pane && !pane.classList.contains('active')) { // Le chart est dans un onglet inactif, ne pas mettre à jour return; } } try { console.log('Fetch historique pour', sensorId); const response = await fetch(`/api/lhm/history/${encodeURIComponent(sensorId)}?count=60`); const data = await response.json(); console.log('Historique reçu:', data); if (data.values && data.values.length > 0) { console.log('→ Mise à jour chart avec', data.values.length, 'valeurs'); chart.data.labels = data.values.map((_, i) => i); chart.data.datasets[0].data = data.values; chart.update('none'); console.log('→ Chart mis à jour'); } else { console.warn('→ Pas de valeurs dans l\'historique'); } } catch (error) { console.error('Erreur mise à jour graphique:', sensorId, error); } } // === Jauges circulaires === function createGauge(sensor) { console.log('=== createGauge appelé ==='); console.log('Sensor:', sensor); const sensorId = sensor.id; const card = document.querySelector(`[data-sensor-id="${CSS.escape(sensorId)}"]`); console.log('Card trouvée:', card); if (!card) { console.error('ERREUR: Card non trouvée pour', sensorId); return; } const canvas = card.querySelector('.sensor-gauge'); console.log('Canvas trouvé:', canvas); if (!canvas) { console.error('ERREUR: Canvas .sensor-gauge non trouvé dans la carte'); console.log('HTML de la card:', card.innerHTML); return; } // Stocker les infos de la jauge charts.set(sensorId, { type: 'gauge', canvas: canvas, sensor: sensor }); console.log('Jauge stockée dans Map, dessin initial...'); // Dessiner la jauge initiale updateGauge(sensor); } function updateGauge(sensor) { console.log('=== updateGauge appelé ===', sensor.name); const sensorId = sensor.id; const gaugeInfo = charts.get(sensorId); if (!gaugeInfo || gaugeInfo.type !== 'gauge') { console.error('ERREUR: gaugeInfo non trouvé ou pas de type gauge'); return; } const canvas = gaugeInfo.canvas; if (!canvas) { console.error('ERREUR: canvas non trouvé'); return; } // Vérifier si le canvas est visible (dans un onglet actif) const pane = canvas.closest('.tab-pane'); if (pane && !pane.classList.contains('active')) { // Le canvas est dans un onglet inactif, ne pas dessiner return; } const ctx = canvas.getContext('2d'); console.log('Canvas:', canvas, 'offsetWidth:', canvas.offsetWidth); // Définir la taille - protection contre taille nulle ou trop petite const size = canvas.offsetWidth || canvas.width || 60; if (size < 20) { console.warn('Canvas trop petit, skip updateGauge'); return; } canvas.width = size; canvas.height = size; console.log('Taille canvas:', size); // Extraire la valeur numérique const valueStr = sensor.value.replace(/[^\d.,]/g, '').replace(',', '.'); let value = parseFloat(valueStr); if (isNaN(value)) value = 0; console.log('Valeur extraite:', value); // Déterminer min, max selon le type let min = 0, max = 100; const sensorType = sensor.type || 'generic'; switch(sensorType) { case 'temperature': min = 0; max = 100; // 0-100°C break; case 'percentage': case 'load': min = 0; max = 100; break; case 'fan': min = 0; max = 2000; // 0-2000 RPM break; case 'power': min = 0; max = 200; // 0-200W break; case 'voltage': min = 0; max = 15; // 0-15V break; case 'frequency': case 'clock': min = 0; max = 6000; // 0-6000 MHz break; default: min = 0; max = 100; } // Calculer le pourcentage const percentage = Math.min(Math.max((value - min) / (max - min), 0), 1); // Couleur selon le type et la valeur let color; if (sensorType === 'temperature') { if (value < 40) color = '#4a9eff'; // Bleu (froid) else if (value < 70) color = '#ff9800'; // Orange (tiède) else color = '#f44336'; // Rouge (chaud) } else if (sensorType === 'percentage' || sensorType === 'load') { if (value < 50) color = '#4caf50'; // Vert else if (value < 80) color = '#ff9800'; // Orange else color = '#f44336'; // Rouge } else if (sensorType === 'fan') { color = '#4caf50'; // Vert } else if (sensorType === 'power') { color = '#f44336'; // Rouge } else if (sensorType === 'voltage') { color = '#9c27b0'; // Violet } else if (sensorType === 'frequency' || sensorType === 'clock') { color = '#ffc107'; // Jaune } else { color = '#4a9eff'; // Bleu par défaut } // Effacer le canvas ctx.clearRect(0, 0, size, size); // Centre et rayon const centerX = size / 2; const centerY = size / 2; const radius = Math.max(size / 2 - 10, 5); // Minimum 5px de rayon const lineWidth = Math.max(size / 10, 2); // Minimum 2px d'épaisseur // Fond de la jauge (gris) ctx.beginPath(); ctx.arc(centerX, centerY, radius, 0.75 * Math.PI, 2.25 * Math.PI); ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)'; ctx.lineWidth = lineWidth; ctx.lineCap = 'round'; ctx.stroke(); // Arc de la valeur const startAngle = 0.75 * Math.PI; const endAngle = startAngle + (1.5 * Math.PI * percentage); ctx.beginPath(); ctx.arc(centerX, centerY, radius, startAngle, endAngle); ctx.strokeStyle = color; ctx.lineWidth = lineWidth; ctx.lineCap = 'round'; ctx.stroke(); // Récupérer les options personnalisées const gaugeOptions = sensor.gauge_options || {}; const showValue = gaugeOptions.show_value !== false; // true par défaut // Texte de la valeur au centre (si activé) if (showValue) { ctx.fillStyle = 'rgba(255, 255, 255, 0.9)'; ctx.font = `bold ${size / 4}px -apple-system, sans-serif`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(sensor.value, centerX, centerY); } } // === Applications === function renderApps() { const appsSection = document.getElementById('apps-section'); if (!config || !config.apps || config.apps.length === 0) { if (appsSection) appsSection.style.display = 'none'; cleanAppsFromTabs(); appsRendered = false; return; } const appsTabId = config.apps_tab || ''; // Si les apps ont déjà été rendues dans le même onglet, ne pas re-rendre if (appsRendered && lastAppsTabId === appsTabId) { return; } // Créer le HTML des boutons d'apps const appsHtml = config.apps.map((app, index) => ` `).join(''); // Nettoyer d'abord les apps existantes dans les onglets cleanAppsFromTabs(); // Si un onglet est spécifié, mettre les apps dans cet onglet if (appsTabId) { // Cacher la section séparée if (appsSection) appsSection.style.display = 'none'; // Trouver le pane cible const targetPane = document.getElementById(`pane-${appsTabId}`); if (targetPane) { // Créer la nouvelle grille d'apps const appsContainer = document.createElement('div'); appsContainer.className = 'apps-section-in-tab'; appsContainer.innerHTML = `

🚀 Applications

${appsHtml}
`; // Ajouter à la fin du pane targetPane.appendChild(appsContainer); // Ajouter les event listeners appsContainer.querySelectorAll('.app-button').forEach(btn => { btn.addEventListener('click', () => launchApp(parseInt(btn.dataset.appIndex))); }); appsRendered = true; lastAppsTabId = appsTabId; } else { // L'onglet cible n'existe pas encore, afficher dans la section séparée console.warn(`Onglet apps "${appsTabId}" non trouvé, affichage en section séparée`); renderAppsInSection(appsHtml); appsRendered = true; lastAppsTabId = ''; } } else { // Afficher dans la section séparée (comportement par défaut) renderAppsInSection(appsHtml); appsRendered = true; lastAppsTabId = ''; } } function renderAppsInSection(appsHtml) { const appsSection = document.getElementById('apps-section'); const appsGrid = document.getElementById('apps-grid'); if (appsSection && appsGrid) { appsSection.style.display = 'block'; appsGrid.innerHTML = appsHtml; // Ajouter les event listeners appsGrid.querySelectorAll('.app-button').forEach(btn => { btn.addEventListener('click', () => launchApp(parseInt(btn.dataset.appIndex))); }); } } // Nettoyer les apps des onglets (appelé lors du re-render) function cleanAppsFromTabs() { document.querySelectorAll('.apps-section-in-tab').forEach(el => el.remove()); } async function launchApp(appId) { try { const response = await fetch(`/api/apps/launch/${appId}`, { method: 'POST' }); const result = await response.json(); if (!result.success) { alert('Erreur: ' + result.error); } } catch (error) { console.error('Erreur lancement app:', error); alert('Erreur de lancement'); } } // === Utilitaires === function updateTimestamp() { const now = new Date(); const timeStr = now.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit', second: '2-digit' }); document.getElementById('last-update').textContent = `🔄 ${timeStr}`; } function showError(message) { const errorState = document.getElementById('error-state'); const errorMessage = document.getElementById('error-message'); errorMessage.textContent = message; errorState.style.display = 'block'; document.getElementById('empty-state').style.display = 'none'; document.getElementById('tabs-nav').style.display = 'none'; document.getElementById('tabs-content').style.display = 'none'; } // Nettoyage à la fermeture window.addEventListener('beforeunload', () => { if (updateInterval) { clearInterval(updateInterval); } charts.forEach((chart, key) => { // Chart.js a une méthode destroy, les jauges n'en ont pas if (chart && typeof chart.destroy === 'function') { chart.destroy(); } }); }); // === Swipe Gestures === function initSwipeGestures() { const tabsContent = document.getElementById('tabs-content'); if (!tabsContent) return; // Touch events tabsContent.addEventListener('touchstart', handleTouchStart, { passive: true }); tabsContent.addEventListener('touchend', handleTouchEnd, { passive: true }); // Optionnel: support souris pour test sur desktop tabsContent.addEventListener('mousedown', handleMouseDown); tabsContent.addEventListener('mouseup', handleMouseUp); } function handleTouchStart(e) { touchStartX = e.changedTouches[0].screenX; touchStartY = e.changedTouches[0].screenY; } function handleTouchEnd(e) { touchEndX = e.changedTouches[0].screenX; touchEndY = e.changedTouches[0].screenY; handleSwipe(); } function handleMouseDown(e) { touchStartX = e.screenX; touchStartY = e.screenY; } function handleMouseUp(e) { touchEndX = e.screenX; touchEndY = e.screenY; handleSwipe(); } function handleSwipe() { const deltaX = touchEndX - touchStartX; const deltaY = touchEndY - touchStartY; // Vérifier que c'est bien un swipe horizontal (pas vertical) if (Math.abs(deltaX) < SWIPE_THRESHOLD) return; if (Math.abs(deltaY) > Math.abs(deltaX)) return; // Scroll vertical, ignorer if (deltaX > 0) { // Swipe vers la droite → onglet précédent navigateToPreviousTab(); } else { // Swipe vers la gauche → onglet suivant navigateToNextTab(); } } function navigateToNextTab() { if (totalTabs <= 1) return; const nextIndex = (currentTabIndex + 1) % totalTabs; switchTabByIndex(nextIndex); // Feedback visuel showSwipeFeedback('next'); } function navigateToPreviousTab() { if (totalTabs <= 1) return; const prevIndex = (currentTabIndex - 1 + totalTabs) % totalTabs; switchTabByIndex(prevIndex); // Feedback visuel showSwipeFeedback('prev'); } function showSwipeFeedback(direction) { const content = document.getElementById('tabs-content'); content.classList.add('swipe-' + direction); setTimeout(() => { content.classList.remove('swipe-' + direction); }, 200); } // ==================== Plexamp Widget ==================== let plexampEnabled = false; let plexampInterval = null; async function initPlexamp() { try { const response = await fetch('/api/plexamp/config'); const config = await response.json(); plexampEnabled = config.enabled && config.has_token; const widget = document.getElementById('plexamp-widget'); if (plexampEnabled && widget) { widget.style.display = 'flex'; setupPlexampControls(); updatePlexampStatus(); // Polling toutes les 2 secondes plexampInterval = setInterval(updatePlexampStatus, 2000); } } catch (error) { console.error('Erreur init Plexamp:', error); } } async function updatePlexampStatus() { try { const response = await fetch('/api/plexamp/status'); const data = await response.json(); const widget = document.getElementById('plexamp-widget'); const cover = document.getElementById('plexamp-cover'); const noMusic = document.getElementById('plexamp-no-music'); const title = document.getElementById('plexamp-title'); const artist = document.getElementById('plexamp-artist'); const album = document.getElementById('plexamp-album'); const progressFill = document.getElementById('plexamp-progress-fill'); const timeCurrent = document.getElementById('plexamp-time-current'); const timeTotal = document.getElementById('plexamp-time-total'); const playPauseBtn = document.getElementById('plexamp-playpause'); if (!data.enabled || data.state === 'stopped' || !data.title) { // Pas de lecture cover.style.display = 'none'; noMusic.style.display = 'flex'; title.textContent = '-'; artist.textContent = '-'; album.textContent = '-'; progressFill.style.width = '0%'; timeCurrent.textContent = '0:00'; timeTotal.textContent = '0:00'; playPauseBtn.textContent = '▶️'; return; } // Afficher les infos cover.style.display = 'block'; noMusic.style.display = 'none'; if (data.artwork_url) { cover.src = data.artwork_url; } title.textContent = data.title || '-'; artist.textContent = data.artist || '-'; album.textContent = data.album || '-'; // Progression progressFill.style.width = `${data.progress || 0}%`; timeCurrent.textContent = formatTime(data.time || 0); timeTotal.textContent = formatTime(data.duration || 0); // Bouton play/pause playPauseBtn.textContent = data.playing ? '⏸️' : '▶️'; } catch (error) { console.error('Erreur update Plexamp:', error); } } function formatTime(ms) { const seconds = Math.floor(ms / 1000); const mins = Math.floor(seconds / 60); const secs = seconds % 60; return `${mins}:${secs.toString().padStart(2, '0')}`; } function setupPlexampControls() { const prevBtn = document.getElementById('plexamp-prev'); const playPauseBtn = document.getElementById('plexamp-playpause'); const nextBtn = document.getElementById('plexamp-next'); if (prevBtn) { prevBtn.addEventListener('click', () => sendPlexampCommand('prev')); } if (playPauseBtn) { playPauseBtn.addEventListener('click', () => sendPlexampCommand('playpause')); } if (nextBtn) { nextBtn.addEventListener('click', () => sendPlexampCommand('next')); } } async function sendPlexampCommand(command) { try { await fetch(`/api/plexamp/${command}`, { method: 'POST' }); // Mettre à jour immédiatement setTimeout(updatePlexampStatus, 300); } catch (error) { console.error(`Erreur commande Plexamp ${command}:`, error); } } // Initialiser Plexamp au chargement document.addEventListener('DOMContentLoaded', () => { // Attendre un peu que le reste soit chargé setTimeout(initPlexamp, 500); });