1036 lines
34 KiB
JavaScript
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);
|
|
}
|