184 lines
11 KiB
JavaScript
184 lines
11 KiB
JavaScript
(() => {
|
|
'use strict';
|
|
const $ = id => document.getElementById(id);
|
|
const page = document.body.dataset.page;
|
|
const state = {csrf: null, logged: false, baseline: {}};
|
|
const fields = [...document.querySelectorAll('[data-setting]')];
|
|
let polling = null;
|
|
const fetchJSON = async (url, options = {}) => {
|
|
const headers = { ...(options.headers || {}) };
|
|
if (options.body) headers['Content-Type'] = 'application/json';
|
|
if (options.method && options.method !== 'GET' && state.csrf) headers['X-Alfred-CSRF'] = state.csrf;
|
|
const response = await fetch(url, {...options, credentials: 'same-origin', cache:'no-store', headers});
|
|
let data;
|
|
try { data = await response.json(); } catch { data = {}; }
|
|
if (response.status === 401 && url !== '/api/control/login') { showLogin(); throw new Error('Session expirée. Reconnecte-toi.'); }
|
|
if (!response.ok) throw new Error(data.error || 'Erreur HTTP '+response.status);
|
|
return data;
|
|
};
|
|
const setMessage = (message, bad=false) => {
|
|
const el = $('message'); if (!el) return;
|
|
el.textContent = message; el.style.color = bad ? '#f1aaaa' : '#a1dcbe';
|
|
};
|
|
const showLogin = () => {
|
|
state.logged = false; $('mainArea').hidden = true; $('authScreen').hidden = false;
|
|
if (polling) clearInterval(polling); polling = null;
|
|
};
|
|
const showPage = () => { state.logged=true; $('mainArea').hidden=false; $('authScreen').hidden=true; };
|
|
const getFieldValue = el => el.type === 'checkbox' ? el.checked : el.value;
|
|
function configDirty() {
|
|
const changed = fields.some(el => String(getFieldValue(el)) !== String(state.baseline[el.id] ?? ''));
|
|
if ($('saveState')) $('saveState').textContent = changed ? 'Modifications non enregistrées' : 'Aucun changement';
|
|
}
|
|
function markInput() {
|
|
if ($('scoreOut')) $('scoreOut').textContent = Number($('ALFRED_WEATHER_ILLUSTRATION_TARGET_SCORE').value || 8.6).toFixed(1) + ' / 10';
|
|
if ($('denoiseOut')) $('denoiseOut').textContent = Number($('ALFRED_WEATHER_REFERENCE_DENOISE').value || .56).toFixed(2);
|
|
configDirty();
|
|
}
|
|
async function loadConfig() {
|
|
const values = (await fetchJSON('/api/control/config')).values || {};
|
|
fields.forEach(el => {
|
|
if (el.type==='checkbox') el.checked = String(values[el.id]).toLowerCase() === 'true';
|
|
else el.value = values[el.id] ?? '';
|
|
state.baseline[el.id] = getFieldValue(el);
|
|
});
|
|
markInput();
|
|
}
|
|
async function saveConfig() {
|
|
const values = {};
|
|
fields.forEach(el => { values[el.id] = getFieldValue(el); });
|
|
try {
|
|
const result = await fetchJSON('/api/control/config', {method:'POST', body:JSON.stringify(values)});
|
|
state.baseline = {};
|
|
fields.forEach(el => state.baseline[el.id] = getFieldValue(el));
|
|
configDirty();
|
|
$('pendingNotice').hidden = false;
|
|
setMessage(result.ok ? 'Configuration sauvegardée.' : 'Enregistrement inconnu.');
|
|
} catch (error) { setMessage(error.message, true); }
|
|
}
|
|
function serviceStatus(el, data) {
|
|
if (!el) return;
|
|
const labels = {ok:'Disponible',down:'Indisponible',warning:'Attention',muted:'Coupé',unknown:'Non vérifié'};
|
|
el.dataset.state = data.state || 'unknown';
|
|
el.textContent = labels[data.state] || 'Inconnu';
|
|
}
|
|
const safeText = (id, value) => { if ($(id)) $(id).textContent = String(value ?? '—'); };
|
|
const readable = value => {
|
|
const x = Number(value) || 0;
|
|
if (x >= 1073741824) return (x / 1073741824).toFixed(1) + ' Go';
|
|
return (x / 1048576).toFixed(1) + ' Mo';
|
|
};
|
|
function renderHealth(data) {
|
|
const timestamp = new Date(data.time).toLocaleTimeString('fr-FR', {hour:'2-digit',minute:'2-digit',second:'2-digit'});
|
|
safeText('updatedAt', timestamp);
|
|
serviceStatus($('cameraStatus'), data.camera); safeText('cameraDetail',data.camera?.detail);
|
|
serviceStatus($('microStatus'), data.microphone); safeText('microDetail',data.microphone?.detail);
|
|
serviceStatus($('speakerStatus'), data.speaker); safeText('speakerDetail',data.speaker?.detail);
|
|
const wake = data.wake || {};
|
|
serviceStatus($('wakeStatus'), {state:!wake.running?'down':wake.paused?'warning':'ok'});
|
|
safeText('wakeLabel', `Vosk · ${wake.word || 'alfred'}`);
|
|
safeText('wakeDetail', wake.error || (wake.paused ? 'Détection en pause' : 'Détection active'));
|
|
const labels = {alfred:'Alfred / Flask', hermes_cli:'Hermes Agent (CLI)', hermes_dashboard:'Hermes WebUI', comfyui:'ComfyUI', ollama:'Ollama', home_assistant:'Home Assistant', wyoming_piper:'Wyoming Piper'};
|
|
const list = $('serviceList'); list.replaceChildren();
|
|
for (const [key,service] of Object.entries(data.services || {})) {
|
|
const row = document.createElement('div'); row.className='service-row';
|
|
const desc = document.createElement('div');
|
|
const title=document.createElement('div');title.className='service-label';title.textContent=labels[key] || key;
|
|
const sub=document.createElement('div');sub.className='service-sub';sub.textContent=service.detail || '';
|
|
desc.append(title,sub);
|
|
const aside=document.createElement('div');aside.className='service-tail';
|
|
if (service.latency_ms != null) {const delay=document.createElement('span');delay.className='muted microtext';delay.textContent=service.latency_ms+' ms';aside.append(delay);}
|
|
const status=document.createElement('span');status.className='status-pill';serviceStatus(status,service);aside.append(status);
|
|
row.append(desc,aside);list.append(row);
|
|
}
|
|
const sys = data.system || {};
|
|
const diskPercent = sys.disk_total ? Math.round(100*sys.disk_used/sys.disk_total) : 0;
|
|
safeText('diskPercent', diskPercent+' %');$('diskBar').style.width=diskPercent+'%';
|
|
safeText('diskText', readable(sys.disk_used)+' utilisés sur '+readable(sys.disk_total));
|
|
const mem=sys.memory || {};
|
|
const memPercent=mem.total ? Math.round(100*mem.used/mem.total) : 0;
|
|
safeText('memPercent',mem.total ? memPercent+' %' : 'N/D');$('memBar').style.width=memPercent+'%';
|
|
safeText('memText',mem.total ? readable(mem.used)+' utilisés sur '+readable(mem.total) : 'Non disponible');
|
|
safeText('loadAvg',sys.load_1m);
|
|
const mcps=$('mcpList');mcps.replaceChildren();
|
|
for (const mcp of Object.values(data.mcps || {})) {
|
|
const card=document.createElement('div');card.className='mcp-item';
|
|
const title=document.createElement('strong');title.textContent=mcp.label || 'MCP';
|
|
const status=document.createElement('span');status.className='status-pill';serviceStatus(status,mcp);
|
|
const p=document.createElement('p');p.textContent=mcp.detail;
|
|
card.append(title,status,p);mcps.append(card);
|
|
}
|
|
}
|
|
let inFlight = false;
|
|
async function refreshHealth() {
|
|
if (inFlight || document.hidden) return;
|
|
inFlight = true;
|
|
try { renderHealth(await fetchJSON('/api/control/health'));setMessage(''); }
|
|
catch(err){setMessage('Surveillance : '+err.message,true);}
|
|
finally{inFlight=false;}
|
|
}
|
|
async function loadAudio() {
|
|
try {
|
|
const d=await fetchJSON('/api/audio/settings');
|
|
$('liveVolume').value=d.volume;$('liveVolumeOut').textContent=d.volume+' %';
|
|
$('liveMic').disabled = d.mic_gain == null;
|
|
if(d.mic_gain != null)$('liveMic').value=d.mic_gain;
|
|
$('liveMicOut').textContent=d.mic_gain == null ? 'N/D' : d.mic_gain+' %';
|
|
$('muteMicro').checked=Boolean(d.mic_muted);$('muteSpeaker').checked=Boolean(d.speaker_muted);
|
|
} catch(err){safeText('audioResult',err.message);}
|
|
}
|
|
async function updateAudio(values) {
|
|
try {
|
|
const d=await fetchJSON('/api/audio/settings', {method:'POST',body:JSON.stringify(values)});
|
|
safeText('audioResult','Réglage appliqué.');
|
|
$('liveVolume').value=d.volume;$('liveVolumeOut').textContent=d.volume+' %';
|
|
$('muteMicro').checked=Boolean(d.mic_muted);$('muteSpeaker').checked=Boolean(d.speaker_muted);
|
|
} catch(err){safeText('audioResult','Erreur : '+err.message);await loadAudio();}
|
|
}
|
|
async function onLogin(event) {
|
|
event.preventDefault();
|
|
const password=$('adminPassword').value;
|
|
try {
|
|
const response=await fetchJSON('/api/control/login',{method:'POST',body:JSON.stringify({password})});
|
|
state.csrf=response.csrf;$('adminPassword').value='';await enterApp();
|
|
} catch(err){safeText('loginMessage',err.message);}
|
|
}
|
|
async function enterApp() {
|
|
showPage();
|
|
if(page==='config'){
|
|
await Promise.all([loadConfig(),loadAudio()]);
|
|
}else{
|
|
await refreshHealth();
|
|
polling=setInterval(refreshHealth,15000);
|
|
}
|
|
}
|
|
async function init() {
|
|
$('loginForm')?.addEventListener('submit', onLogin);
|
|
$('logoutButton')?.addEventListener('click', async ()=>{
|
|
try{await fetchJSON('/api/control/logout',{method:'POST'});}catch(e){}
|
|
state.csrf=null;showLogin();
|
|
});
|
|
fields.forEach(el=>{el.addEventListener('change',markInput);el.addEventListener('input',markInput);});
|
|
$('saveConfig')?.addEventListener('click',saveConfig);
|
|
$('restartButton')?.addEventListener('click',async ()=>{
|
|
if(!confirm('Redémarrer Alfred maintenant ? Les opérations en cours seront interrompues.'))return;
|
|
try{const data=await fetchJSON('/api/control/restart',{method:'POST'});setMessage(data.message||'Demande transmise.');$('restartButton').disabled=true;setTimeout(()=>$('restartButton').disabled=false,5000);}
|
|
catch(err){setMessage(err.message,true);}
|
|
});
|
|
$('testVoice')?.addEventListener('click',async()=>{
|
|
try{await fetchJSON('/api/tts/say',{method:'POST',body:JSON.stringify({text:'Bonjour, je suis Alfred. Voici un test de ma voix actuelle.'})});setMessage('Test vocal demandé.');}
|
|
catch(err){setMessage(err.message,true);}
|
|
});
|
|
let volDelay, gainDelay;
|
|
$('liveVolume')?.addEventListener('input',e=>{safeText('liveVolumeOut',e.target.value+' %');clearTimeout(volDelay);volDelay=setTimeout(()=>updateAudio({volume:Number(e.target.value)}),250);});
|
|
$('liveMic')?.addEventListener('input',e=>{safeText('liveMicOut',e.target.value+' %');clearTimeout(gainDelay);gainDelay=setTimeout(()=>updateAudio({mic_gain:Number(e.target.value)}),250);});
|
|
$('muteMicro')?.addEventListener('change',e=>updateAudio({mic_muted:e.target.checked}));
|
|
$('muteSpeaker')?.addEventListener('change',e=>updateAudio({speaker_muted:e.target.checked}));
|
|
$('refreshHealth')?.addEventListener('click',refreshHealth);
|
|
const login=await fetchJSON('/api/control/auth').catch(()=>({authenticated:false}));
|
|
if (login.authenticated){state.csrf=login.csrf;try{await enterApp();}catch(err){setMessage(err.message,true);}}
|
|
else{showLogin();if(!login.configured)safeText('loginMessage','Mot de passe non défini : exécute set_admin_password.py sur Alfred.');}
|
|
}
|
|
document.addEventListener('DOMContentLoaded',init);
|
|
})();
|