Files
2026-03-24 07:17:48 +01:00

1036 lines
34 KiB
JavaScript

// 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 = `<canvas class="sensor-gauge gauge-${gaugeSize}" width="${size}" height="${size}"></canvas>`;
} 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 = `<canvas class="sensor-chart chart-${chartHeight}" height="${height}"></canvas>`;
} 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 ? `<div class="sensor-type-badge">${sensor.type}</div>` : '';
card.innerHTML = `
<div class="sensor-header">
<div class="sensor-name">${sensor.name}</div>
${typeBadgeHtml}
</div>
<div class="sensor-value ${valueClass}" data-value>${sensor.value}</div>
${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) => `
<button class="app-button" data-app-index="${index}">
<div class="app-icon">${app.icon || '🚀'}</div>
<div class="app-name">${app.name}</div>
</button>
`).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 = `
<h3 class="apps-title-in-tab">🚀 Applications</h3>
<div class="apps-grid apps-grid-in-tab">${appsHtml}</div>
`;
// 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);
}