Files
Alfredv2/alfred/app/static/js/alfred-admin.js
T

189 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
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.
(() => {
'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.');
if (result.identity_sync_warning) setMessage('Nom enregistré, mais identity.md non synchronisé : '+result.identity_sync_warning, true);
} 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', wyoming_whisper:'Whisper (STT)', alfred_timers:'Minuteurs Alfred'};
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);
}
if (typeof window.alfredAdminV2Health === 'function') {
window.alfredAdminV2Health(data);
}
}
// ALFRED_ADMIN_SURVEILLANCE_V2: monitoring hook
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 '+(document.querySelector('.brand strong')?.textContent || 'l’assistant')+' maintenant ? Les opérations en cours seront interrompues.'))return; // ALFRED_IDENTITY_GLASS_V1
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 '+(document.querySelector('.brand strong')?.textContent || 'votre assistant')+'. 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);
})();