1157 lines
39 KiB
JavaScript
1157 lines
39 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);
|
||
}
|
||
|
||
// ==================== 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);
|
||
});
|