/* DONA_V8_DYNAMIC — data-only dashboards. All user/model text is textContent, never HTML. */ (() => { 'use strict'; const ID='donaDynamicV8'; if(document.getElementById(ID))return; const $=id=>document.getElementById(id); const el=(tag,cls,text)=>{ const item=document.createElement(tag);if(cls)item.className=cls; if(text!==undefined)item.textContent=String(text);return item; }; const btn=(text,fn,cls='d8-btn')=>{const b=el('button',cls,text);b.type='button';b.addEventListener('click',fn);return b;}; let active=null,opened=false,busy=false,ticker=null; // DONA_DYNAMIC_PERF_V2 — HTTP 202 is queue acceptance, not generation completion. let creating=false, createTimer=null; const CREATE_MAX_WAIT_MS=240000; function updateCreationControls(){ const root=document.getElementById(ID);if(!root)return; root.querySelectorAll('.d8-submit, .d8-examples button').forEach(button=>{ button.disabled=creating;button.setAttribute('aria-disabled',String(creating)); }); } function finishCreating(){ if(createTimer!==null){clearTimeout(createTimer);createTimer=null;} creating=false;updateCreationControls(); } function startCreating(){ creating=true;updateCreationControls(); if(createTimer!==null)clearTimeout(createTimer); createTimer=setTimeout(()=>{ finishCreating(); if(opened)status('Confirmation non reçue. Tu peux réessayer ; une génération peut encore être en cours côté serveur.'); },CREATE_MAX_WAIT_MS); } const KEY='dona.dynamic.pins.v8'; const safeGetPins=()=>{ try{const pins=JSON.parse(localStorage.getItem(KEY)||'[]');return Array.isArray(pins)?pins.slice(0,8):[];} catch(_){return []} }; const safeSavePins=list=>{try{localStorage.setItem(KEY,JSON.stringify(list.slice(0,8)));}catch(_){status('Stockage local indisponible.');}}; function status(text){$('d8Status').textContent=text||'';} function makeModal(){ const root=el('div','d8-overlay');root.id=ID;root.hidden=true; root.setAttribute('role','dialog');root.setAttribute('aria-modal','true');root.setAttribute('aria-label','Tableaux dynamiques Dona'); const panel=el('section','d8-panel'); const head=el('header','d8-head'); const ident=el('div','d8-brand');ident.append(el('span','d8-eyebrow','DONA · INTERFACE DYNAMIQUE'),el('h1','d8-title','Tableaux de bord')); head.append(ident,btn('×',close,'d8-close'));panel.append(head); const bar=el('form','d8-compose');bar.id='d8Compose'; const input=el('input','d8-input');input.id='d8Input';input.required=true;input.maxLength=500; input.placeholder='Ex. Tableau de bord Proxmox, Wazuh, lumières…';input.setAttribute('aria-label','Demande de tableau dynamique'); bar.append(input,btn('Créer',()=>{},'d8-submit'));bar.lastChild.type='submit'; bar.addEventListener('submit',e=>{e.preventDefault();create(input.value);});panel.append(bar); const examples=el('div','d8-examples'); [['Spotify','Tableau de bord Spotify'],['Lumières','Tableau de bord des lumières'], ['Températures','Tableau de bord des températures'],['Proxmox','Tableau de bord Proxmox'], ['Wazuh','Tableau de bord Wazuh']].forEach(([text,prompt])=>examples.append(btn(text,()=>{input.value=prompt;create(prompt)},'d8-chip'))); panel.append(examples); const actions=el('div','d8-toolbar'); actions.append(btn('↻ Actualiser',refresh,'d8-btn'),btn('☆ Épingler',pin,'d8-btn')); actions.children[0].id='d8Refresh'; actions.children[1].id='d8Pin';panel.append(actions); const caption=el('div','d8-note');caption.id='d8Status';caption.setAttribute('role','status');caption.setAttribute('aria-live','polite');panel.append(caption); const pinned=el('div','d8-pinned');pinned.id='d8Pinned';panel.append(pinned); const grid=el('main','d8-grid');grid.id='d8Grid';panel.append(grid); const foot=el('p','d8-footer','Données provenant de Spotify, Home Assistant ou des outils Hermes. Aucun code produit par l’IA n’est exécuté.');panel.append(foot); root.append(panel);document.body.append(root); root.addEventListener('click',e=>{if(e.target===root)close()}); document.addEventListener('keydown',e=>{if(e.key==='Escape'&&opened)close();}); return root; } makeModal(); function open(){opened=true;$(ID).hidden=false;document.body.classList.add('d8-open');$('d8Input').focus();renderPins();schedule();} function close(){opened=false;$(ID).hidden=true;document.body.classList.remove('d8-open');stopTimer();} function stopTimer(){if(ticker){clearInterval(ticker);ticker=null;}} function schedule(){stopTimer();if(!opened||!active||!['spotify','homeassistant','lights','proxmox'].includes(active.origin))return; ticker=setInterval(()=>{if(!document.hidden&&!busy)reloadLive(false);},active.origin==='spotify'?15000:30000); } function originLabel(origin){return ({spotify:'Spotify · direct',homeassistant:'Home Assistant · direct',lights:'Home Assistant · direct',hermes:'Hermes · MCP',fallback:'Hermes · réponse texte',proxmox:'Proxmox · API directe' /* DONA_DYNAMIC_PROXMOX_V3 */})[origin]||'Données contextuelles';} const tagText=(parent,tag,cls,text)=>{parent.append(el(tag,cls,text));}; function drawCard(c){ const section=el('article','d8-card d8-'+c.type); if(c.title)tagText(section,'h2','d8-card-title',c.title); if(c.type==='metric'){ const value=el('div','d8-metric-value',c.value??'—'); if(c.unit)value.append(el('small','d8-unit',c.unit));section.append(value); if(c.detail)tagText(section,'p','d8-detail',c.detail); } else if(c.type==='text'){ tagText(section,'p','d8-text',c.text||'—'); } else if(c.type==='list'){ const list=el('ul','d8-list');(c.items||[]).forEach(x=>list.append(el('li','',x))); if(!list.childNodes.length)list.append(el('li','','Aucune donnée.'));section.append(list); } else if(c.type==='table'){ const scroll=el('div','d8-table-scroll'),table=el('table','d8-table'),thead=el('thead'),headrow=el('tr'); (c.columns||[]).forEach(x=>headrow.append(el('th','',x)));thead.append(headrow);table.append(thead); const tbody=el('tbody');(c.rows||[]).forEach(r=>{const tr=el('tr');(r||[]).forEach(v=>tr.append(el('td','',v)));tbody.append(tr);}); table.append(tbody);scroll.append(table);section.append(scroll); } else if(c.type==='bars'){ const values=(c.items||[]).map(x=>Number(x.value)).filter(x=>Number.isFinite(x)); const max=Math.max(1,...values.map(x=>Math.abs(x))); (c.items||[]).forEach(item=>{ const row=el('div','d8-bar-row'),line=el('div','d8-bar-line'); line.append(el('span','',item.label),el('strong','',String(item.value)+(item.unit?' '+item.unit:''))); const track=el('div','d8-track'),fill=el('div','d8-fill'); fill.style.width=Math.max(0,Math.min(100,Math.abs(Number(item.value))/max*100))+'%'; track.append(fill);row.append(line,track);section.append(row); }); } else if(c.type==='light' && active?.origin==='lights'){ const state=String(c.state||'unknown');tagText(section,'p','d8-light-state',state==='on'?'Allumée':state==='off'?'Éteinte':'Indisponible'); if(state==='on'||state==='off'){ const b=btn(state==='on'?'Éteindre':'Allumer',()=>toggleLight(c.entity_id,c.title),'d8-btn d8-action');section.append(b); } } return section; } function render(board){ if(!board||board.schema!=='dona.dashboard.v1'||!Array.isArray(board.cards))return; active=board;open(); $('d8Input').value=board.prompt||$('d8Input').value; $('d8Grid').replaceChildren(); const sub=el('div','d8-heading');sub.append(el('h2','',board.title||'Tableau de bord')); sub.append(el('p','',board.subtitle||originLabel(board.origin))); if(board.generated_at){const d=new Date(board.generated_at);sub.append(el('small','',`Mis à jour : ${Number.isNaN(d.getTime())?'—':d.toLocaleTimeString('fr-FR')}`));} $('d8Grid').append(sub); if(board.origin==='spotify'){ const controls=el('div','d8-player-controls'); [['⏮ Précédent','previous'],['▶ Lecture','play'],['Ⅱ Pause','pause'],['⏭ Suivant','next']].forEach(([label,action])=>{ controls.append(btn(label,async()=>{ try{await post('/api/dona/spotify/control',{action});await reloadLive(false);} catch(e){status('Spotify : '+e.message)} },'d8-btn')); }); $('d8Grid').append(controls); } board.cards.slice(0,16).forEach(c=>{if(c&&typeof c==='object')$('d8Grid').append(drawCard(c));}); status(originLabel(board.origin)+(board.origin==='hermes'?' · contenu communiqué par l’agent':'')); renderPins();schedule(); } function renderPins(){ const host=$('d8Pinned');host.replaceChildren(); const pins=safeGetPins();if(!pins.length)return; host.append(el('span','d8-pins-label','Épinglés :')); pins.forEach((p,i)=>{ const item=el('span','d8-pin-item'); item.append(btn(p.title||'Tableau '+(i+1),()=>render(p),'d8-chip')); item.append(btn('×',()=>{const rest=safeGetPins().filter((_,j)=>j!==i);safeSavePins(rest);renderPins();},'d8-pin-remove')); host.append(item); }); } function pin(){if(!active){status('Crée d’abord un tableau.');return;} const pins=safeGetPins();pins.unshift(active);safeSavePins(pins);renderPins();status('Tableau épinglé dans ce navigateur.');} async function post(url,body){ const res=await fetch(url,{method:'POST',credentials:'same-origin',headers:{'Content-Type':'application/json','X-Alfred-Request':'1'},body:JSON.stringify(body)}); const obj=await res.json().catch(()=>({}));if(!res.ok||obj.ok===false){const error=Error(obj.error||'HTTP '+res.status);error.httpStatus=res.status;throw error;}return obj; } async function create(prompt){ const query=String(prompt||'').trim();if(query.length<2||query.length>500){status('Décris le tableau en 2 à 500 caractères.');return;} if(creating){status('Une génération est déjà en cours.');return;} if(busy){status('Une actualisation est en cours.');return;} startCreating();status('Dona prépare le tableau…'); try{ await post('/api/dona/dynamic/create',{prompt:query}); // A fast Socket.IO completion may have already cleared creating. if(creating)status('Dona consulte les données ; la vue apparaîtra ici.'); }catch(e){ if(e.httpStatus===409){ // Another tab or another entry point may have submitted the job. // Wait for its completion event, or release after the safety timeout. status('Une génération est déjà en cours côté serveur. Attente du résultat…'); }else{ finishCreating();status(e.message||'Demande impossible.'); } } } async function reloadLive(notify=true){ if(!active)return; if(['spotify','homeassistant','lights','proxmox'].includes(active.origin)){ if(busy)return;const current=active;busy=true; try{const r=await fetch('/api/dona/dynamic/live?source='+encodeURIComponent(active.origin),{cache:'no-store',credentials:'same-origin'}); const j=await r.json();if(!r.ok||!j.ok)throw Error(j.error||'Source indisponible'); if(current===active)render(j.dashboard); }catch(e){status('Actualisation impossible : '+e.message);}finally{busy=false;} } else if(notify && active.prompt){create(active.prompt);} } function refresh(){if(!active){status('Aucun tableau à actualiser.');return;}reloadLive(true);} async function toggleLight(entity,name){ if(!active||active.origin!=='lights')return; if(!/^light\.[a-z0-9_]{1,100}$/.test(String(entity)))return; if(!confirm('Changer l’état de « '+String(name).slice(0,100)+' » ?'))return; try{await post('/api/dona/dynamic/lights/toggle',{entity_id:entity});await reloadLive();} catch(e){status('Commande refusée : '+e.message);} } function initButton(){ const primary=document.querySelector('#alfredMainV1 .av-head-actions')||document.querySelector('.header-actions'); if(!primary)return; if($('d8Open'))return; const b=btn('▦',open,'d8-open-button');b.id='d8Open';b.title='Tableaux dynamiques';b.setAttribute('aria-label','Ouvrir les tableaux de bord dynamiques'); primary.prepend(b); } let tries=0;const timer=setInterval(()=>{initButton();if($('d8Open')||++tries>70)clearInterval(timer);},150); // DONA_UNIFIED_ROUTING_V4 — conserver le tableau quand Dona confirme à voix haute. let v4IgnoreNextTextUntil=0; const socket=window.alfredSocket; socket?.on('alfred_render',data=>{ if(data?.type==='dona_dynamic' && data.dona_fast_v4===true) v4IgnoreNextTextUntil=Date.now()+15000; if(data?.type==='dona_dynamic'){ // Only a server result (not a pinned board or HTTP 202) ends the wait. if(data.dashboard?.schema==='dona.dashboard.v1'&&Array.isArray(data.dashboard.cards))finishCreating(); render(data.dashboard); } else if(opened && data?.type!=='dona_dynamic'){ if(v4IgnoreNextTextUntil && Date.now(){ if(opened && data?.mode==='error'){ if(creating)finishCreating(); status('Erreur lors de la préparation du tableau.'); } }); document.addEventListener('visibilitychange',()=>{if(!document.hidden&&opened&&active&&['spotify','homeassistant','lights','proxmox'].includes(active.origin))reloadLive(false);}); window.DonaDynamicV8={open,render,close}; })();