Files
Alfredv2/alfred/app/static/js/dona-dynamic-v8.js
T
2026-11-02 08:50:05 +01:00

225 lines
13 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.
/* 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()<v4IgnoreNextTextUntil){
v4IgnoreNextTextUntil=0; // Ignorer UNE confirmation de la commande directe.
}else{v4IgnoreNextTextUntil=0;close();}
}
});
socket?.on('alfred_state',data=>{
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};
})();