Files
PC-Monitor/static/js/admin.js
2026-03-24 07:17:48 +01:00

1128 lines
40 KiB
JavaScript
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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';
}
}