/* ALFRED_MAIN_VISUAL_V1 — accueil vivant, conservation des contrôleurs existants. */
(() => {
'use strict';
if (document.getElementById('alfredMainV1')) return;
const shell = document.querySelector('.alfred-shell');
if (!shell || !document.getElementById('stage') || !document.getElementById('content')) return;
const I = {
home:'',
bulb:'',
server:'',
brain:'',
media:'',
more:'',
mic:'',
micOff:'',
speaker:'',
speakerOff:'',
camera:'',
settings:'',
calendar:'',
clock:'',
cloud:'',
shield:'',
arrow:'',
chat:'',
close:'',
sun:'',
spark:'',
};
const icon = (name, extra = '') => ``;
const initialName = (document.title || 'Alfred').trim();
const dashboard = document.createElement('div');
dashboard.id = 'alfredMainV1';
dashboard.className = 'alfred-main-v1';
dashboard.innerHTML = `
${icon('mic')}
En veilleDites « Alfred » puis votre demande
Prêt à vous écouter
`;
shell.insertBefore(dashboard, shell.firstChild);
document.body.classList.add('alfred-glass-theme');
document.getElementById('avName').textContent = initialName;
const wave = document.getElementById('wave');
document.getElementById('avWaveHost').appendChild(wave);
window.dispatchEvent(new Event('resize'));
const $ = id => document.getElementById(id);
const timeZone = 'Europe/Paris';
let lastStatus = 'idle';
let mute = {mic_muted:false,speaker_muted:false};
let refreshingAudio = false;
let toastTimer;
function toast(message) {
const el = $('avToast'); el.textContent = String(message); el.hidden = false;
clearTimeout(toastTimer); toastTimer = setTimeout(() => { el.hidden = true; }, 5500);
}
function clock() {
const now = new Date();
$('avTime').textContent = new Intl.DateTimeFormat('fr-FR',{timeZone,hour:'2-digit',minute:'2-digit'}).format(now);
const date = new Intl.DateTimeFormat('fr-FR',{timeZone,weekday:'long',day:'numeric',month:'long',year:'numeric'}).format(now);
$('avDate').textContent = date.charAt(0).toUpperCase() + date.slice(1);
}
clock(); setInterval(clock, 15000);
function updateAudio(a) {
mute = {...mute,...a};
const mic = Boolean(mute.mic_muted), speaker = Boolean(mute.speaker_muted);
for (const id of ['avTopMic','avMicButton']) {
const button = $(id); button.setAttribute('aria-pressed',String(mic));
button.setAttribute('aria-label', mic ? 'Réactiver le microphone' : 'Couper le microphone');
button.title = button.getAttribute('aria-label');
button.querySelector('svg').outerHTML = icon(mic?'micOff':'mic');
}
for (const id of ['avTopSpeaker','avSpeakerButton']) {
const button = $(id); button.setAttribute('aria-pressed',String(speaker));
button.setAttribute('aria-label', speaker?'Réactiver la sortie audio':'Couper la sortie audio');
button.title = button.getAttribute('aria-label');
button.querySelector('svg').outerHTML = icon(speaker?'speakerOff':'speaker');
}
$('avMicLabel').textContent = mic?'Coupé':'Actif';
$('avSpeakerLabel').textContent = speaker?'Coupée':'Activée';
dashboard.classList.toggle('av-mic-is-muted',mic);
dashboard.classList.toggle('av-speaker-is-muted',speaker);
renderMode(lastStatus);
}
async function refreshAudio() {
if (refreshingAudio) return;
refreshingAudio = true;
try { const r=await fetch('/api/audio/settings',{cache:'no-store'});
if (!r.ok) throw Error('HTTP '+r.status);
updateAudio(await r.json());
} catch (e) { console.warn('[ALFRED UI] Réglages audio :',e); }
finally {refreshingAudio=false;}
}
async function toggleAudio(kind) {
const prop = kind==='mic'?'mic_muted':'speaker_muted';
try {
const r=await fetch('/api/audio/settings',{method:'POST',headers:{'Content-Type':'application/json','X-Alfred-Request':'1'},body:JSON.stringify({[prop]:!mute[prop]})});
const data=await r.json();
if (!r.ok) throw Error(data.error || 'HTTP '+r.status);
updateAudio(data);
toast(kind==='mic' ? (data.mic_muted?'Microphone coupé':'Microphone réactivé') : (data.speaker_muted?'Sortie audio coupée':'Sortie audio réactivée'));
} catch (e) { toast('Impossible de modifier le son : '+e.message); }
}
for (const id of ['avTopMic','avMicButton']) $(id).addEventListener('click',()=>toggleAudio('mic'));
for (const id of ['avTopSpeaker','avSpeakerButton']) $(id).addEventListener('click',()=>toggleAudio('speaker'));
$('avTopSettings').addEventListener('click',()=>{const btn=$('audioSettingsButton'); if(btn)btn.click();else location.assign('/config');});
refreshAudio(); setInterval(()=>{if(!document.hidden)refreshAudio();},30000);
function serviceText(state) {return state==='ok'?'Connecté':state==='offline'?'Hors ligne':state==='installed'?'Installé (non testé)':state==='unconfigured'?'Non configuré':'Non vérifié';}
function setService(id,dotid,label,state) {
$(id).textContent = `${label} : ${serviceText(state)}`;
$(dotid).className = `av-mini-dot ${state==='ok'?'is-ok':state==='offline'?'is-error':''}`;
}
async function refreshDashboard() {
try {
const r=await fetch('/api/alfred/dashboard',{cache:'no-store'});
if(!r.ok) throw Error('HTTP '+r.status);
const d=await r.json();
const h=d.home||{}, s=d.system||{}, ai=d.services||{}, w=d.weather||{};
$('avHomeSummary').textContent=h.available?'Home Assistant connecté':(h.detail||'Données non disponibles');
$('avLights').textContent = h.available?`${h.lights_on} lumière(s) allumée(s)`:'Lumières : indisponible';
$('avCovers').textContent = h.available?`${h.covers_closed} volet(s) fermé(s)`:'Volets : indisponible';
$('avIndoor').textContent = h.indoor_temperature!=null?`${h.indoor_temperature} °C · intérieur`:'Température : non disponible';
if (window.alfredHomeCardCustomUpdate) window.alfredHomeCardCustomUpdate();
$('avLoad').textContent = s.load_1m!=null?`Charge 1 min : ${s.load_1m}`:'Charge : non disponible';
$('avMemory').textContent = s.memory_percent!=null?`Mémoire : ${s.memory_percent} %`:'Mémoire : non disponible';
$('avDisk').textContent = s.disk_percent!=null?`Stockage : ${s.disk_percent} %`:'Stockage : non disponible';
setService('avOllama','avOllamaDot','Ollama',ai.ollama);
setService('avHermes','avHermesDot','Hermes',ai.hermes);
setService('avComfy','avComfyDot','ComfyUI',ai.comfyui);
$('avAiSummary').textContent='États vérifiés / installation locale';
$('avWeatherTemp').textContent=w.available&&w.temperature!=null?`${w.temperature} ${w.unit||'°C'}`:'—';
$('avWeatherCondition').textContent=w.available?w.condition:'Non disponible';
const sunny=['sunny','clear-night','partlycloudy'].includes(w.condition_id);
$('avWeatherIcon').textContent=sunny?'🌤️':w.condition_id==='rainy'?'🌧️':w.condition_id==='snowy'?'❄️':'☁️';
} catch (e) {
$('avHomeSummary').textContent='Données indisponibles';
$('avAiSummary').textContent='Surveillance indisponible';
$('avWeatherCondition').textContent='Indisponible';
console.warn('[ALFRED UI] Tableau de bord :',e);
}
}
refreshDashboard(); setInterval(()=>{if(!document.hidden)refreshDashboard();},30000);
async function refreshForecast() {
try {
const r=await fetch('/api/ha/weather',{cache:'no-store'});
if(!r.ok)return;
const d=await r.json();
const day=(d.daily||[])[0];
const vals=[];
if(day?.temperature!=null) vals.push('↑ '+day.temperature+'°');
if(day?.templow!=null) vals.push('↓ '+day.templow+'°');
$('avWeatherRange').textContent=vals.join(' ');
} catch (_) { /* Not all installations expose the weather forecast route. */ }
}
refreshForecast();setInterval(()=>{if(!document.hidden)refreshForecast();},180000);
function renderMode(mode) {
lastStatus = mode||'idle';
const states = {idle:['En veille','Dites « '+initialName+' » puis votre demande','Prêt'],
waiting:['En veille','Dites « '+initialName+' » puis votre demande','Prêt'],
listening:['Je vous écoute…','Quelle est votre demande ?','Écoute en cours'],
thinking:['Je réfléchis…','Traitement de la demande','Hermes travaille'],
speaking:['Je vous réponds…','Réponse vocale en cours','Piper parle'],
error:['Un problème est survenu','Consultez la transcription ou la surveillance','Erreur']};
const item=states[mode]||states.idle;
$('avVoiceTitle').textContent=mute.mic_muted?'Microphone coupé':item[0];
$('avVoiceSubtitle').textContent=mute.mic_muted?'Réactivez le micro pour utiliser le mot d’activation':item[1];
$('avStatusText').textContent=mute.mic_muted?'Écoute désactivée':item[2];
dashboard.dataset.mode=mode;
$('avOrb').setAttribute('aria-label',item[0]);
}
renderMode('idle');
const recent=[];
let lastTranscript='';
function pushRecent(text) {
const value=String(text||'').trim();
if(!value || value===lastTranscript)return;
lastTranscript=value;
recent.unshift({text:value,at:new Date()});
if(recent.length>5)recent.pop();
const root=$('avHistoryList');root.replaceChildren();
for(const e of recent) {
const node=document.createElement('span');node.className='av-history-item';
const t=document.createElement('time');t.textContent=new Intl.DateTimeFormat('fr-FR',{timeZone,hour:'2-digit',minute:'2-digit'}).format(e.at);
const content=document.createElement('span');content.textContent=e.text;
node.append(t,content);root.append(node);
}
}
function focusScene(show) {
dashboard.classList.toggle('is-focused', Boolean(show));
$('avBack').hidden = !show;
}
function onRender(d) { if(!d || typeof d!=='object')return;
if(d.type==='home')focusScene(false);
else if(d.type && d.type!=='text' || (d.text||d.title||d.url))focusScene(true);
}
const socket=window.alfredSocket;
socket?.on('alfred_state',state=>{
renderMode(String(state.mode||'idle').toLowerCase());
if(state.mode==='thinking' && state.transcript)pushRecent(state.transcript);
});
socket?.on('alfred_render',onRender);
socket?.on('audio_settings',updateAudio);
fetch('/api/state',{cache:'no-store'}).then(r=>r.ok?r.json():null).then(s=>{if(s)renderMode(String(s.mode||'idle').toLowerCase());}).catch(()=>{});
async function goHome() {
focusScene(false);
if(window.AlfredViews?.goHome){try{await window.AlfredViews.goHome();}catch(_){}}
else if($('viewHome'))$('viewHome').click();
}
$('avBack').addEventListener('click',goHome);
function openCamera() {
if($('cameraPopupButton')){ $('cameraPopupButton').click();return; }
const camera=document.querySelector('.camera-panel');
if(!camera){ toast('Caméra non installée');return; }
const isOpen=document.body.classList.toggle('av-legacy-camera-open');
const toggle=$('cameraToggle');
if(toggle && (toggle.getAttribute('aria-pressed')==='true')!==isOpen)toggle.click();
if(isOpen && !$('avLegacyCameraClose')){
const close=document.createElement('button');close.id='avLegacyCameraClose';close.type='button';close.textContent='Fermer la caméra';
close.addEventListener('click',openCamera);camera.prepend(close);
}
}
$('avTopCamera').addEventListener('click',openCamera);
function openView(name) {
const f=window.AlfredViews?.[name];
if(typeof f==='function'){ focusScene(true);f();return; }
const fallback={openWeather:'viewWeather',openTemperatures:'viewTemperatures',openCalendar:'viewCalendar'}[name];
if(fallback && $(fallback)){focusScene(true);$(fallback).click();return;}
toast('Cette fonction n’est pas disponible sur cette installation.');
}
async function quickCommand(action,description) {
try {
const r=await fetch('/api/alfred/dashboard/command',{method:'POST',headers:{'Content-Type':'application/json','X-Alfred-Request':'1'},body:JSON.stringify({action,description})});
const data=await r.json(); if(!r.ok)throw Error(data.error||'HTTP '+r.status);
pushRecent(({proxmox:'Vérifier Proxmox',wazuh:'Consulter les alertes Wazuh',comfyui:'Générer une image'})[action]||action);
toast('Demande transmise à Hermes');
} catch(e) {toast('Commande indisponible : '+e.message);}
}
function showTime() {
const current=new Intl.DateTimeFormat('fr-FR',{timeZone,hour:'2-digit',minute:'2-digit',second:'2-digit'}).format(new Date());
const content=$('content');content.replaceChildren();
const title=document.createElement('h1');title.textContent='Il est '+current;
const subtitle=document.createElement('p');subtitle.textContent='Heure de Paris · horloge locale du navigateur';
content.append(title,subtitle); focusScene(true);pushRecent('Quelle heure est-il ?');
}
async function act(name) {
if(name==='home'){await goHome();return;}
if(name==='house'){openView('openTemperatures');return;}
if(name==='weather'){openView('openWeather');return;}
if(name==='calendar'){ $('avMoreDialog').close();openView('openCalendar');return; }
if(name==='camera'){ $('avMoreDialog').close();openCamera();return; }
if(name==='media'){ $('avComfyDialog').showModal();$('avComfyPrompt').focus();return;}
if(name==='time'){showTime();return;}
if(name==='proxmox'||name==='wazuh'){await quickCommand(name);return;}
if(name==='comfyui'){ $('avComfyDialog').showModal();$('avComfyPrompt').focus();return;}
if(name==='more'){ $('avMoreDialog').showModal();return;}
if(name==='audio'){ $('avMoreDialog').close();$('avTopSettings').click();return; }
}
dashboard.addEventListener('click',event=>{
const button=event.target.closest('[data-av-action]');
if(button)act(button.dataset.avAction);
});
$('avComfySubmit').addEventListener('click',()=>{
const text=$('avComfyPrompt').value.trim();
if(text.length<4){toast('Décris l’image à générer.');return;}
$('avComfyDialog').close();quickCommand('comfyui',text);
});
window.addEventListener('resize',()=>{ /* legacy alfred.js recalculates the waveform canvas */ });
})();