1128 lines
40 KiB
JavaScript
1128 lines
40 KiB
JavaScript
// 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 = `
|
||
<div class="plugin-card-header">
|
||
<span class="plugin-icon">${icon}</span>
|
||
<span class="plugin-name">${plugin.name}</span>
|
||
</div>
|
||
<div class="plugin-description">${plugin.description}</div>
|
||
<div class="plugin-meta">
|
||
Port par défaut: <code>${plugin.default_port}</code> |
|
||
<a href="${plugin.website}" target="_blank">Site web ↗</a>
|
||
</div>
|
||
`;
|
||
|
||
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 = `
|
||
<h4>â„¹ï¸ Configuration LibreHardwareMonitor</h4>
|
||
<p>Pour utiliser ce plugin :</p>
|
||
<ul>
|
||
<li>Téléchargez <a href="https://github.com/LibreHardwareMonitor/LibreHardwareMonitor/releases" target="_blank">LibreHardwareMonitor</a></li>
|
||
<li>Lancez-le en administrateur</li>
|
||
<li>Allez dans <code>Options → Remote Web Server → Run</code></li>
|
||
<li>Le serveur écoute par défaut sur le port <code>8085</code></li>
|
||
</ul>
|
||
`;
|
||
} else if (pluginId === 'hwinfo') {
|
||
container.innerHTML = `
|
||
<h4>â„¹ï¸ Configuration HWiNFO</h4>
|
||
<p>Pour utiliser ce plugin, vous avez besoin de 2 logiciels :</p>
|
||
<ul>
|
||
<li>Téléchargez <a href="https://www.hwinfo.com/download/" target="_blank">HWiNFO</a></li>
|
||
<li>Dans HWiNFO : <code>Settings → General → Enable Shared Memory Support</code></li>
|
||
<li>Téléchargez <a href="https://www.hwinfo.com/files/RemoteSensorMonitor/Remote.Sensor.Monitor.v.2.1.0.zip" target="_blank">Remote Sensor Monitor</a></li>
|
||
<li>Lancez Remote Sensor Monitor (port par défaut: <code>55555</code>)</li>
|
||
</ul>
|
||
<p><strong>Note :</strong> La version gratuite de HWiNFO limite le Shared Memory à 12 heures. Après, il faut réactiver manuellement.</p>
|
||
`;
|
||
}
|
||
}
|
||
|
||
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 = '<option value="">-- Section séparée (en bas) --</option>';
|
||
|
||
// 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 = `
|
||
<input type="checkbox"
|
||
class="sensor-checkbox"
|
||
data-sensor-id="${node.id}"
|
||
${isSelected ? 'checked' : ''}>
|
||
<div class="sensor-info">
|
||
<div class="sensor-name">${node.name}</div>
|
||
<div class="sensor-value">
|
||
${node.value}
|
||
<span class="sensor-type">${node.type}</span>
|
||
</div>
|
||
</div>
|
||
<div class="sensor-options">
|
||
<select class="sensor-tab-select" data-sensor-id="${node.id}" ${!isSelected ? 'disabled' : ''}>
|
||
${config.tabs.map(tab =>
|
||
`<option value="${tab.id}" ${sensorConfig.tabId === tab.id ? 'selected' : ''}>${tab.name}</option>`
|
||
).join('')}
|
||
</select>
|
||
<select class="sensor-viz-select" data-sensor-id="${node.id}" ${!isSelected ? 'disabled' : ''}>
|
||
<option value="none" ${!sensorConfig.showGraph ? 'selected' : ''}>📊 Aucun</option>
|
||
<option value="line" ${sensorConfig.showGraph && (!sensorConfig.viz_type || sensorConfig.viz_type === 'line') ? 'selected' : ''}>📈 Courbe</option>
|
||
<option value="gauge" ${sensorConfig.showGraph && sensorConfig.viz_type === 'gauge' ? 'selected' : ''}>🎯 Jauge</option>
|
||
</select>
|
||
<button class="sensor-config-btn" data-sensor-id="${node.id}" ${!isSelected ? 'disabled' : ''} title="Options d'affichage">âš™ï¸</button>
|
||
</div>
|
||
`;
|
||
|
||
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 = `
|
||
<span class="group-toggle">â–¼</span>
|
||
<span class="group-icon">${icon}</span>
|
||
<span class="group-name">${node.name}</span>
|
||
<span class="group-count">${sensorCount} capteur${sensorCount > 1 ? 's' : ''}</span>
|
||
`;
|
||
|
||
// 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 = `
|
||
<span class="tab-handle">☰</span>
|
||
<input type="text" value="${tab.name}" placeholder="Nom de l'onglet" data-tab-id="${tab.id}">
|
||
<span class="tab-sensors-count">${tab.sensors.length} capteurs</span>
|
||
${config.tabs.length > 1 ? `<button class="btn btn-danger" onclick="removeTab('${tab.id}')">🗑ï¸</button>` : ''}
|
||
`;
|
||
|
||
// 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 = `
|
||
<input type="text" class="app-icon-input" value="${app.icon || '🚀'}" placeholder="🚀" data-app-index="${index}" data-field="icon">
|
||
<input type="text" value="${app.name}" placeholder="Nom de l'application" data-app-index="${index}" data-field="name">
|
||
<input type="text" value="${app.path}" placeholder="Chemin complet (C:\\...\\app.exe)" data-app-index="${index}" data-field="path">
|
||
<button class="btn btn-danger" onclick="removeApp(${index})">🗑ï¸</button>
|
||
`;
|
||
|
||
// 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 = `
|
||
<h3>📑 ${tab.name} <span style="font-weight: normal; font-size: 12px; color: var(--text-secondary);">(${tab.sensors.length} capteurs)</span></h3>
|
||
<div class="order-sensors-list" data-tab-id="${tab.id}" data-tab-index="${tabIndex}">
|
||
${tab.sensors.length === 0 ?
|
||
'<div class="order-empty-message">Aucun capteur dans cet onglet</div>' :
|
||
tab.sensors.map((sensor, index) => `
|
||
<div class="order-sensor-item" data-sensor-id="${sensor.id}" data-index="${index}">
|
||
<span class="order-sensor-number">${index + 1}</span>
|
||
<span class="order-sensor-handle">☰</span>
|
||
<div class="order-sensor-info">
|
||
<div class="order-sensor-name">${sensor.name || sensor.id}</div>
|
||
<div class="order-sensor-details">
|
||
<span>${sensor.type || 'generic'}</span>
|
||
<span>${sensor.viz_type === 'gauge' ? '🎯 Jauge' : sensor.viz_type === 'line' ? '📈 Courbe' : '📊 Valeur'}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`).join('')
|
||
}
|
||
</div>
|
||
`;
|
||
|
||
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';
|
||
}
|
||
}
|