// Configuration globale
let config = {
theme: 'dark',
tabs: [],
apps: [],
active_plugin: 'librehardwaremonitor',
plugins: {}
};
let allSensors = {};
let availablePlugins = [];
// Initialisation
document.addEventListener('DOMContentLoaded', async () => {
// Charger les plugins disponibles
await loadPlugins();
// Charger la configuration
await loadConfig();
// Charger les capteurs
await loadSensors();
// Charger la config Plexamp
await loadPlexampConfig();
// Appliquer le thème
applyTheme(config.theme);
// Setup event listeners
setupEventListeners();
// Afficher les onglets et apps
renderTabs();
renderApps();
});
// === Gestion des sections ===
function setupEventListeners() {
// Navigation entre sections
document.querySelectorAll('.section-tab').forEach(tab => {
tab.addEventListener('click', () => {
const section = tab.dataset.section;
switchSection(section);
});
});
// Bouton sauvegarder
document.getElementById('save-btn').addEventListener('click', saveConfig);
// Bouton ajouter onglet
document.getElementById('add-tab-btn').addEventListener('click', addTab);
// Bouton ajouter app
document.getElementById('add-app-btn').addEventListener('click', addApp);
// Sélecteur de thème
document.querySelectorAll('input[name="theme"]').forEach(radio => {
radio.addEventListener('change', (e) => {
config.theme = e.target.value;
applyTheme(config.theme);
});
});
// Configuration du plugin (host/port)
const pluginHost = document.getElementById('plugin-host');
const pluginPort = document.getElementById('plugin-port');
if (pluginHost) {
pluginHost.addEventListener('change', updatePluginConfig);
}
if (pluginPort) {
pluginPort.addEventListener('change', updatePluginConfig);
}
// Bouton tester connexion
const testBtn = document.getElementById('test-connection-btn');
if (testBtn) {
testBtn.addEventListener('click', testPluginConnection);
}
}
function switchSection(sectionName) {
// Désactiver tous les tabs et sections
document.querySelectorAll('.section-tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.config-section').forEach(s => s.classList.remove('active'));
// Activer le bon
document.querySelector(`.section-tab[data-section="${sectionName}"]`).classList.add('active');
document.getElementById(`section-${sectionName}`).classList.add('active');
// Rafraîchir la section ordre quand on y accède
if (sectionName === 'order') {
renderOrderSection();
}
}
// === Gestion des Plugins ===
async function loadPlugins() {
try {
const response = await fetch('/api/plugins');
availablePlugins = await response.json();
renderPluginSelector();
} catch (error) {
console.error('Erreur chargement plugins:', error);
}
}
function renderPluginSelector() {
const container = document.getElementById('plugin-selector');
if (!container) return;
container.innerHTML = '';
availablePlugins.forEach(plugin => {
const card = document.createElement('div');
card.className = 'plugin-card' + (plugin.active ? ' active' : '');
card.dataset.pluginId = plugin.id;
const icon = plugin.id === 'librehardwaremonitor' ? '🖥ï¸' : '📊';
card.innerHTML = `
${plugin.description}
`;
card.addEventListener('click', () => selectPlugin(plugin.id));
container.appendChild(card);
});
// Mettre à jour l'affichage de la config
updatePluginConfigDisplay();
updatePluginInfo();
}
function selectPlugin(pluginId) {
const currentPlugin = config.active_plugin;
if (pluginId === currentPlugin) return;
// Confirmer le changement (les capteurs seront différents)
if (config.tabs.some(t => t.sensors.length > 0)) {
if (!confirm('Changer de plugin va afficher des capteurs différents. Les capteurs configurés pour ce plugin seront conservés si vous revenez. Continuer ?')) {
return;
}
}
// Mettre à jour visuellement
document.querySelectorAll('.plugin-card').forEach(card => {
card.classList.toggle('active', card.dataset.pluginId === pluginId);
});
// Mettre à jour la config
config.active_plugin = pluginId;
// Mettre à jour l'affichage
updatePluginConfigDisplay();
updatePluginInfo();
// Sauvegarder et recharger
switchPluginOnServer(pluginId);
}
async function switchPluginOnServer(pluginId) {
try {
const response = await fetch('/api/plugins/switch', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ plugin_id: pluginId })
});
const result = await response.json();
if (result.success) {
showNotification('Plugin changé, rechargement des capteurs...', 'success');
// Recharger la config et les capteurs
await loadConfig();
await loadSensors();
renderTabs();
} else {
showNotification('Erreur: ' + result.error, 'error');
}
} catch (error) {
console.error('Erreur changement plugin:', error);
showNotification('Erreur de connexion', 'error');
}
}
function updatePluginConfigDisplay() {
const pluginId = config.active_plugin || 'librehardwaremonitor';
const pluginConfig = config.plugins?.[pluginId] || {};
const hostInput = document.getElementById('plugin-host');
const portInput = document.getElementById('plugin-port');
if (hostInput) {
hostInput.value = pluginConfig.host || '127.0.0.1';
}
if (portInput) {
// Trouver le port par défaut du plugin
const plugin = availablePlugins.find(p => p.id === pluginId);
portInput.value = pluginConfig.port || plugin?.default_port || 8085;
}
}
function updatePluginConfig() {
const pluginId = config.active_plugin || 'librehardwaremonitor';
const host = document.getElementById('plugin-host').value || '127.0.0.1';
const port = parseInt(document.getElementById('plugin-port').value) || 8085;
if (!config.plugins) config.plugins = {};
config.plugins[pluginId] = { host, port };
}
function updatePluginInfo() {
const container = document.getElementById('plugin-info');
if (!container) return;
const pluginId = config.active_plugin || 'librehardwaremonitor';
if (pluginId === 'librehardwaremonitor') {
container.innerHTML = `
â„¹ï¸ Configuration LibreHardwareMonitor
Pour utiliser ce plugin :
- Téléchargez LibreHardwareMonitor
- Lancez-le en administrateur
- Allez dans
Options → Remote Web Server → Run
- Le serveur écoute par défaut sur le port
8085
`;
} else if (pluginId === 'hwinfo') {
container.innerHTML = `
â„¹ï¸ Configuration HWiNFO
Pour utiliser ce plugin, vous avez besoin de 2 logiciels :
- Téléchargez HWiNFO
- Dans HWiNFO :
Settings → General → Enable Shared Memory Support
- Téléchargez Remote Sensor Monitor
- Lancez Remote Sensor Monitor (port par défaut:
55555)
Note : La version gratuite de HWiNFO limite le Shared Memory à 12 heures. Après, il faut réactiver manuellement.
`;
}
}
async function testPluginConnection() {
const statusEl = document.getElementById('connection-status');
const pluginId = config.active_plugin || 'librehardwaremonitor';
const host = document.getElementById('plugin-host').value || '127.0.0.1';
const port = parseInt(document.getElementById('plugin-port').value) || 8085;
statusEl.className = 'loading';
statusEl.textContent = 'â³ Test en cours...';
try {
const response = await fetch('/api/plugins/test', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
plugin_id: pluginId,
config: { host, port }
})
});
const result = await response.json();
if (result.success) {
statusEl.className = 'success';
statusEl.textContent = `✅ ${result.message}`;
// Mettre à jour la config si le test réussit
updatePluginConfig();
} else {
statusEl.className = 'error';
statusEl.textContent = `⌠${result.message}`;
}
} catch (error) {
statusEl.className = 'error';
statusEl.textContent = '⌠Erreur de connexion au serveur';
}
}
// === Chargement config ===
async function loadConfig() {
try {
const response = await fetch('/api/config');
config = await response.json();
// Assurer qu'il y a au moins un onglet
if (!config.tabs || config.tabs.length === 0) {
config.tabs = [{
id: 'tab1',
name: 'Général',
sensors: []
}];
}
// Valeurs par défaut pour les plugins
if (!config.active_plugin) {
config.active_plugin = 'librehardwaremonitor';
}
if (!config.plugins) {
config.plugins = {};
}
// Sélectionner le thème
const themeRadio = document.querySelector(`input[name="theme"][value="${config.theme}"]`);
if (themeRadio) {
themeRadio.checked = true;
}
// Remplir le sélecteur d'onglet pour les apps
updateAppsTabSelector();
// Mettre à jour l'affichage des plugins
renderPluginSelector();
} catch (error) {
console.error('Erreur chargement config:', error);
showNotification('Erreur de chargement', 'error');
}
}
function updateAppsTabSelector() {
const select = document.getElementById('apps-tab-select');
if (!select) return;
// Sauvegarder la valeur actuelle
const currentValue = config.apps_tab || '';
// Garder la première option
select.innerHTML = '';
// Ajouter les onglets
config.tabs.forEach(tab => {
const option = document.createElement('option');
option.value = tab.id;
option.textContent = tab.name;
select.appendChild(option);
});
// Sélectionner la valeur actuelle
select.value = currentValue;
// Utiliser onchange au lieu de addEventListener pour éviter les doublons
select.onchange = (e) => {
config.apps_tab = e.target.value;
showNotification('Onglet des apps modifié (pensez à sauvegarder)', 'success');
};
}
// === Chargement capteurs ===
async function loadSensors() {
const loadingDiv = document.getElementById('sensors-loading');
const listDiv = document.getElementById('sensors-list');
try {
const response = await fetch('/api/lhm/hierarchy');
allSensors = await response.json();
loadingDiv.style.display = 'none';
listDiv.style.display = 'block';
renderSensorsHierarchical();
} catch (error) {
console.error('Erreur chargement capteurs:', error);
loadingDiv.innerHTML = 'âš ï¸ Erreur: Impossible de charger les capteurs. Vérifiez que LibreHardwareMonitor est lancé.';
}
}
function renderSensorsHierarchical() {
const listDiv = document.getElementById('sensors-list');
listDiv.innerHTML = '';
// Créer un Set des capteurs déjà sélectionnés
const selectedSensors = new Map();
config.tabs.forEach(tab => {
tab.sensors.forEach(sensor => {
selectedSensors.set(sensor.id, {
tabId: tab.id,
showGraph: sensor.show_graph,
viz_type: sensor.viz_type // Utiliser snake_case pour cohérence
});
});
});
// Fonction récursive pour afficher l'arbre
function renderNode(node, parentDiv, level = 0) {
// Ignorer le nœud racine "Sensor"
if (level === 0 && node.name === "Sensor") {
node.children.forEach(child => renderNode(child, parentDiv, 0));
return;
}
// Si c'est un capteur (a une valeur et un ID)
if (node.value && node.id) {
const isSelected = selectedSensors.has(node.id);
const sensorConfig = selectedSensors.get(node.id) || {};
const item = document.createElement('div');
item.className = 'sensor-item';
item.style.marginLeft = `${level * 15}px`;
item.innerHTML = `
${node.name}
${node.value}
${node.type}
`;
parentDiv.appendChild(item);
}
// Si c'est un groupe (a des enfants)
else if (node.children && node.children.length > 0) {
const groupDiv = document.createElement('div');
groupDiv.className = 'sensor-group';
groupDiv.setAttribute('data-level', level);
// Header du groupe (repliable)
const header = document.createElement('div');
header.className = 'group-header';
header.style.marginLeft = `${level * 15}px`;
// Compter les capteurs dans ce groupe et ses enfants
function countSensors(n) {
let count = 0;
if (n.value && n.id) count = 1;
if (n.children) {
n.children.forEach(child => {
count += countSensors(child);
});
}
return count;
}
const sensorCount = countSensors(node);
// Icône selon le niveau
let icon = '📦';
if (level === 1) {
if (node.name.includes('CPU') || node.name.includes('Core')) icon = '🔧';
else if (node.name.includes('GPU') || node.name.includes('NVIDIA') || node.name.includes('AMD') || node.name.includes('Radeon') || node.name.includes('GeForce')) icon = '🎮';
else if (node.name.includes('Memory') || node.name.includes('RAM')) icon = '💾';
else if (node.name.includes('Storage') || node.name.includes('Disk')) icon = '💿';
} else if (level >= 2) {
if (node.name.includes('Temperature')) icon = '🌡ï¸';
else if (node.name.includes('Voltage')) icon = 'âš¡';
else if (node.name.includes('Clock') || node.name.includes('Frequency')) icon = '🔄';
else if (node.name.includes('Load') || node.name.includes('Usage')) icon = '📊';
else if (node.name.includes('Fan')) icon = '🌀';
else if (node.name.includes('Power')) icon = '🔋';
else icon = 'ðŸ“';
}
header.innerHTML = `
â–¼
${icon}
${node.name}
${sensorCount} capteur${sensorCount > 1 ? 's' : ''}
`;
// Container pour les enfants
const childrenDiv = document.createElement('div');
childrenDiv.className = 'group-children';
// Rendre les enfants
node.children.forEach(child => {
renderNode(child, childrenDiv, level + 1);
});
// Toggle pour replier/déplier
header.addEventListener('click', () => {
const isExpanded = childrenDiv.style.display !== 'none';
childrenDiv.style.display = isExpanded ? 'none' : 'block';
header.querySelector('.group-toggle').textContent = isExpanded ? 'â–¶' : 'â–¼';
});
groupDiv.appendChild(header);
groupDiv.appendChild(childrenDiv);
parentDiv.appendChild(groupDiv);
}
}
// Rendre tous les nœuds racine
allSensors.forEach(node => {
renderNode(node, listDiv);
});
// Event listeners pour les checkboxes
document.querySelectorAll('.sensor-checkbox').forEach(checkbox => {
checkbox.addEventListener('change', handleSensorToggle);
});
document.querySelectorAll('.sensor-tab-select').forEach(select => {
select.addEventListener('change', handleSensorTabChange);
});
document.querySelectorAll('.sensor-viz-select').forEach(select => {
select.addEventListener('change', handleSensorVizChange);
});
// Utiliser la délégation d'événements pour les boutons de config
// (car ils sont créés dynamiquement quand on déplie les groupes)
const sensorsContainer = document.getElementById('sensors-list');
if (sensorsContainer) {
sensorsContainer.addEventListener('click', (e) => {
if (e.target.classList.contains('sensor-config-btn')) {
openSensorConfig(e);
}
});
}
}
// Garder l'ancienne fonction pour la compatibilité avec findSensor
function findSensor(sensorId) {
function searchTree(node) {
if (node.id === sensorId) {
return {
id: node.id,
name: node.name,
type: node.type,
value: node.value
};
}
if (node.children) {
for (const child of node.children) {
const found = searchTree(child);
if (found) return found;
}
}
return null;
}
for (const rootNode of allSensors) {
const found = searchTree(rootNode);
if (found) return found;
}
return null;
}
function handleSensorToggle(e) {
const sensorId = e.target.dataset.sensorId;
const isChecked = e.target.checked;
const sensorItem = e.target.closest('.sensor-item');
const tabSelect = sensorItem.querySelector('.sensor-tab-select');
const vizSelect = sensorItem.querySelector('.sensor-viz-select');
const configBtn = sensorItem.querySelector('.sensor-config-btn');
tabSelect.disabled = !isChecked;
vizSelect.disabled = !isChecked;
// Le bouton config est actif dès que le capteur est coché
// (on peut configurer la taille même sans graphique)
if (configBtn) {
configBtn.disabled = !isChecked;
}
if (isChecked) {
// Ajouter le capteur au premier onglet par défaut
const sensor = findSensor(sensorId);
// Vérifier que le capteur existe
if (!sensor) {
console.error('Capteur non trouvé:', sensorId);
console.log('allSensors:', allSensors);
e.target.checked = false;
tabSelect.disabled = true;
vizSelect.disabled = true;
if (configBtn) configBtn.disabled = true;
return;
}
const firstTab = config.tabs[0];
if (!firstTab.sensors.find(s => s.id === sensorId)) {
firstTab.sensors.push({
id: sensorId,
name: sensor.name,
type: sensor.type,
show_graph: false,
viz_type: 'none'
});
}
tabSelect.value = firstTab.id;
} else {
// Retirer le capteur de tous les onglets
config.tabs.forEach(tab => {
tab.sensors = tab.sensors.filter(s => s.id !== sensorId);
});
}
}
function handleSensorTabChange(e) {
const sensorId = e.target.dataset.sensorId;
const newTabId = e.target.value;
// Retirer le capteur de tous les onglets
config.tabs.forEach(tab => {
tab.sensors = tab.sensors.filter(s => s.id !== sensorId);
});
// Ajouter au nouvel onglet
const sensor = findSensor(sensorId);
const targetTab = config.tabs.find(t => t.id === newTabId);
if (targetTab) {
targetTab.sensors.push({
id: sensorId,
name: sensor.name,
type: sensor.type,
show_graph: false
});
}
}
function handleSensorVizChange(e) {
const sensorId = e.target.dataset.sensorId;
const vizType = e.target.value;
// Trouver et mettre à jour
config.tabs.forEach(tab => {
const sensor = tab.sensors.find(s => s.id === sensorId);
if (sensor) {
if (vizType === 'none') {
sensor.show_graph = false;
sensor.viz_type = 'none';
} else {
sensor.show_graph = true;
sensor.viz_type = vizType; // 'line' ou 'gauge'
}
}
});
// Le bouton config reste toujours actif si le capteur est coché
// (on peut configurer la taille même sans graphique)
}
// === Configuration Modal ===
let currentConfigSensorId = null;
function openSensorConfig(e) {
const sensorId = e.target.dataset.sensorId;
currentConfigSensorId = sensorId;
// Trouver le capteur dans la config
let sensorConfig = null;
config.tabs.forEach(tab => {
const sensor = tab.sensors.find(s => s.id === sensorId);
if (sensor) sensorConfig = sensor;
});
if (!sensorConfig) return;
// Remplir la modal avec les valeurs actuelles
document.getElementById('config-card-size').value = sensorConfig.card_size || 'medium';
document.getElementById('config-font-family').value = sensorConfig.font_family || 'system';
document.getElementById('config-font-bold').checked = sensorConfig.font_bold !== false; // true par défaut
document.getElementById('config-font-size').value = sensorConfig.font_size || 'small';
document.getElementById('config-hide-value-mobile').checked = sensorConfig.hide_value_mobile || false;
document.getElementById('config-show-type-badge').checked = sensorConfig.show_type_badge !== false; // true par défaut
// Afficher les options selon le type de visualisation
const gaugeOptions = document.getElementById('gauge-options-group');
const chartOptions = document.getElementById('chart-options-group');
if (sensorConfig.viz_type === 'gauge') {
gaugeOptions.style.display = 'block';
chartOptions.style.display = 'none';
const gaugeOpts = sensorConfig.gauge_options || {};
document.getElementById('config-gauge-show-value').checked = gaugeOpts.show_value !== false;
document.getElementById('config-gauge-size').value = gaugeOpts.size || 'medium';
document.getElementById('config-gauge-style').value = gaugeOpts.style || 'arc';
} else if (sensorConfig.viz_type === 'line') {
gaugeOptions.style.display = 'none';
chartOptions.style.display = 'block';
const chartOpts = sensorConfig.chart_options || {};
document.getElementById('config-chart-height').value = chartOpts.height || 'medium';
} else {
gaugeOptions.style.display = 'none';
chartOptions.style.display = 'none';
}
// Afficher la modal
document.getElementById('sensor-config-modal').style.display = 'flex';
}
function closeSensorConfig() {
document.getElementById('sensor-config-modal').style.display = 'none';
currentConfigSensorId = null;
}
function saveSensorConfig() {
if (!currentConfigSensorId) return;
// Trouver le capteur dans la config
config.tabs.forEach(tab => {
const sensor = tab.sensors.find(s => s.id === currentConfigSensorId);
if (sensor) {
// Sauvegarder la taille de la carte
sensor.card_size = document.getElementById('config-card-size').value;
sensor.font_family = document.getElementById('config-font-family').value;
sensor.font_bold = document.getElementById('config-font-bold').checked;
sensor.font_size = document.getElementById('config-font-size').value;
sensor.hide_value_mobile = document.getElementById('config-hide-value-mobile').checked;
sensor.show_type_badge = document.getElementById('config-show-type-badge').checked;
// Sauvegarder les options selon le type
if (sensor.viz_type === 'gauge') {
sensor.gauge_options = {
show_value: document.getElementById('config-gauge-show-value').checked,
size: document.getElementById('config-gauge-size').value,
style: document.getElementById('config-gauge-style').value
};
} else if (sensor.viz_type === 'line') {
sensor.chart_options = {
height: document.getElementById('config-chart-height').value
};
}
}
});
closeSensorConfig();
showNotification('Configuration mise à jour', 'success');
}
// Event listeners pour la modal
document.querySelector('.modal-close').addEventListener('click', closeSensorConfig);
document.querySelector('.modal-cancel').addEventListener('click', closeSensorConfig);
document.querySelector('.modal-save').addEventListener('click', saveSensorConfig);
// Fermer la modal en cliquant en dehors
document.getElementById('sensor-config-modal').addEventListener('click', (e) => {
if (e.target.id === 'sensor-config-modal') {
closeSensorConfig();
}
});
// === Gestion des onglets ===
function renderTabs() {
const tabsList = document.getElementById('tabs-list');
tabsList.innerHTML = '';
config.tabs.forEach((tab, index) => {
const item = document.createElement('div');
item.className = 'tab-item';
item.innerHTML = `
☰
${tab.sensors.length} capteurs
${config.tabs.length > 1 ? `` : ''}
`;
// Event listener pour le nom
item.querySelector('input').addEventListener('input', (e) => {
const tabToUpdate = config.tabs.find(t => t.id === tab.id);
if (tabToUpdate) {
tabToUpdate.name = e.target.value;
// Mettre à jour le sélecteur d'onglet des apps
updateAppsTabSelector();
}
});
tabsList.appendChild(item);
});
// Mettre à jour le sélecteur d'onglet des apps
updateAppsTabSelector();
}
function addTab() {
const newId = 'tab' + Date.now(); // Utiliser timestamp pour ID unique
config.tabs.push({
id: newId,
name: `Onglet ${config.tabs.length + 1}`,
sensors: []
});
renderTabs();
renderSensors(); // Re-render pour ajouter le nouvel onglet aux selects
}
function removeTab(tabId) {
if (config.tabs.length === 1) {
showNotification('Impossible de supprimer le dernier onglet', 'error');
return;
}
// Si c'était l'onglet des apps, réinitialiser
if (config.apps_tab === tabId) {
config.apps_tab = '';
}
config.tabs = config.tabs.filter(t => t.id !== tabId);
renderTabs();
renderSensors();
}
// === Gestion des applications ===
function renderApps() {
const appsList = document.getElementById('apps-list');
appsList.innerHTML = '';
config.apps.forEach((app, index) => {
const item = document.createElement('div');
item.className = 'app-item';
item.innerHTML = `
`;
// Event listeners
item.querySelectorAll('input').forEach(input => {
input.addEventListener('input', (e) => {
const idx = parseInt(e.target.dataset.appIndex);
const field = e.target.dataset.field;
config.apps[idx][field] = e.target.value;
});
});
appsList.appendChild(item);
});
}
function addApp() {
config.apps.push({
name: 'Nouvelle App',
path: '',
icon: '🚀'
});
renderApps();
}
function removeApp(index) {
config.apps.splice(index, 1);
renderApps();
}
// === Sauvegarde ===
async function saveConfig() {
try {
const response = await fetch('/api/config', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(config)
});
const result = await response.json();
if (result.success) {
showNotification('✅ Configuration sauvegardée !');
} else {
showNotification('⌠Erreur de sauvegarde', 'error');
}
} catch (error) {
console.error('Erreur sauvegarde:', error);
showNotification('⌠Erreur de sauvegarde', 'error');
}
}
// === Thème ===
function applyTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
}
// === Notifications ===
function showNotification(message, type = 'success') {
const notification = document.getElementById('notification');
notification.textContent = message;
notification.className = `notification ${type}`;
notification.classList.add('show');
setTimeout(() => {
notification.classList.remove('show');
}, 3000);
}
// === Section Ordre des capteurs ===
let sortableInstances = [];
function renderOrderSection() {
const container = document.getElementById('order-tabs-container');
if (!container) return;
container.innerHTML = '';
// Détruire les anciennes instances Sortable
sortableInstances.forEach(instance => instance.destroy());
sortableInstances = [];
// Créer une section pour chaque onglet
config.tabs.forEach((tab, tabIndex) => {
const section = document.createElement('div');
section.className = 'order-tab-section';
section.innerHTML = `
📑 ${tab.name} (${tab.sensors.length} capteurs)
${tab.sensors.length === 0 ?
'
Aucun capteur dans cet onglet
' :
tab.sensors.map((sensor, index) => `
${index + 1}
☰
${sensor.name || sensor.id}
${sensor.type || 'generic'}
${sensor.viz_type === 'gauge' ? '🎯 Jauge' : sensor.viz_type === 'line' ? '📈 Courbe' : '📊 Valeur'}
`).join('')
}
`;
container.appendChild(section);
// Initialiser Sortable sur la liste si elle a des capteurs
if (tab.sensors.length > 0) {
const listEl = section.querySelector('.order-sensors-list');
const sortable = new Sortable(listEl, {
animation: 150,
handle: '.order-sensor-handle',
ghostClass: 'sortable-ghost',
chosenClass: 'sortable-chosen',
dragClass: 'sortable-drag',
onEnd: function(evt) {
handleSensorReorder(evt);
}
});
sortableInstances.push(sortable);
}
});
}
function handleSensorReorder(evt) {
const listEl = evt.from;
const tabId = listEl.dataset.tabId;
const tabIndex = parseInt(listEl.dataset.tabIndex);
// Récupérer le nouvel ordre des IDs
const newOrder = Array.from(listEl.querySelectorAll('.order-sensor-item'))
.map(item => item.dataset.sensorId);
// Réorganiser les capteurs dans la config
const tab = config.tabs[tabIndex];
const reorderedSensors = [];
newOrder.forEach(sensorId => {
const sensor = tab.sensors.find(s => s.id === sensorId);
if (sensor) {
reorderedSensors.push(sensor);
}
});
// Mettre à jour la config
config.tabs[tabIndex].sensors = reorderedSensors;
// Mettre à jour les numéros affichés
updateOrderNumbers(listEl);
console.log(`Ordre mis à jour pour ${tab.name}:`, newOrder);
showNotification('Ordre modifié (pensez à sauvegarder)', 'success');
}
function updateOrderNumbers(listEl) {
const items = listEl.querySelectorAll('.order-sensor-item');
items.forEach((item, index) => {
const numberEl = item.querySelector('.order-sensor-number');
if (numberEl) {
numberEl.textContent = index + 1;
}
item.dataset.index = index;
});
}
// ==================== Plexamp ====================
async function loadPlexampConfig() {
try {
const response = await fetch('/api/plexamp/config');
const data = await response.json();
document.getElementById('plexamp-enabled').checked = data.enabled;
document.getElementById('plexamp-host').value = data.host || '192.168.1.235';
document.getElementById('plexamp-port').value = data.port || 32400;
// Le token n'est pas renvoyé pour des raisons de sécurité
if (data.has_token) {
document.getElementById('plexamp-token').placeholder = '••••••••••••••••';
}
// Setup event listeners Plexamp
setupPlexampListeners();
} catch (error) {
console.error('Erreur chargement config Plexamp:', error);
}
}
function setupPlexampListeners() {
const testBtn = document.getElementById('plexamp-test-btn');
const saveBtn = document.getElementById('plexamp-save-btn');
if (testBtn) {
testBtn.addEventListener('click', testPlexampConnection);
}
if (saveBtn) {
saveBtn.addEventListener('click', savePlexampConfig);
}
}
async function testPlexampConnection() {
const statusEl = document.getElementById('plexamp-status');
statusEl.textContent = '⏳ Test en cours...';
statusEl.className = '';
const config = {
host: document.getElementById('plexamp-host').value,
port: parseInt(document.getElementById('plexamp-port').value),
token: document.getElementById('plexamp-token').value
};
// Si le token est vide, on ne peut pas tester
if (!config.token) {
statusEl.textContent = '⚠️ Entrez un token';
statusEl.className = 'status-warning';
return;
}
try {
const response = await fetch('/api/plexamp/test', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(config)
});
const result = await response.json();
if (result.success) {
statusEl.textContent = '✅ ' + result.message;
statusEl.className = 'status-success';
} else {
statusEl.textContent = '❌ ' + result.message;
statusEl.className = 'status-error';
}
} catch (error) {
statusEl.textContent = '❌ Erreur: ' + error.message;
statusEl.className = 'status-error';
}
}
async function savePlexampConfig() {
const statusEl = document.getElementById('plexamp-status');
const config = {
enabled: document.getElementById('plexamp-enabled').checked,
host: document.getElementById('plexamp-host').value,
port: parseInt(document.getElementById('plexamp-port').value),
token: document.getElementById('plexamp-token').value
};
try {
const response = await fetch('/api/plexamp/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(config)
});
const result = await response.json();
if (result.success) {
statusEl.textContent = '✅ Sauvegardé';
statusEl.className = 'status-success';
showNotification('Configuration Plexamp sauvegardée', 'success');
} else {
statusEl.textContent = '❌ ' + result.error;
statusEl.className = 'status-error';
}
} catch (error) {
statusEl.textContent = '❌ Erreur: ' + error.message;
statusEl.className = 'status-error';
}
}