Files
Alfredv2/alfred/app/static/js/dona-dynamic-v8.js
T
2026-10-30 22:10:03 +01:00

177 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.
/* 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;
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'].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'})[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)throw Error(obj.error||'HTTP '+res.status);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(busy)return;busy=true;status('Dona prépare le tableau…');
try{await post('/api/dona/dynamic/create',{prompt:query});status('Dona consulte les données ; la vue apparaîtra ici.');}
catch(e){status(e.message||'Demande impossible.');}
finally{busy=false;}
}
async function reloadLive(notify=true){
if(!active)return;
if(['spotify','homeassistant','lights'].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);
const socket=window.alfredSocket;
socket?.on('alfred_render',data=>{
if(data?.type==='dona_dynamic')render(data.dashboard);
else if(opened && data?.type!=='dona_dynamic')close();
});
socket?.on('alfred_state',data=>{
if(opened && data?.mode==='error')status('Erreur lors de la préparation du tableau.');
});
document.addEventListener('visibilitychange',()=>{if(!document.hidden&&opened&&active&&['spotify','homeassistant','lights'].includes(active.origin))reloadLive(false);});
window.DonaDynamicV8={open,render,close};
})();