v2.2 - Small improvement Interface change

This commit is contained in:
pporcheret
2026-10-29 11:13:02 +01:00
parent a17c3cb614
commit 7f26f9b015
271 changed files with 6207 additions and 16044 deletions
@@ -0,0 +1,45 @@
(() => {
'use strict';
const $=id=>document.getElementById(id);
let csrf='',selected=null,latest=null, busy=false;
const message=(txt,ok=false,id='dvMessage')=>{let e=$(id);e.textContent=txt;e.classList.toggle('ok',ok);};
async function fetchJson(url,options={}){
const r=await fetch(url,{credentials:'same-origin',cache:'no-store',...options});
const d=await r.json().catch(()=>({}));
if(!r.ok)throw Error(d.error || 'Erreur HTTP '+r.status);
return d;
}
async function auth(){
const d=await fetchJson('/api/control/auth');csrf=d.csrf||'';
$('dvLoginBox').hidden=!!d.authenticated;$('dvApp').hidden=!d.authenticated;
if(d.authenticated)await refresh();
}
async function getName(){try{const r=await fetchJson('/api/alfred/identity/name');let n=r.name||'Assistant';$('dvName').textContent=n;$('dvInitial').textContent=Array.from(n)[0]?.toLocaleUpperCase('fr-FR')||'A';document.title='Maintenance · '+n;}catch(_){}}
function renderTasks(tasks){$('dvCount').textContent=tasks.length+' propositions';const host=$('dvTasks');host.replaceChildren();
if(!tasks.length){let p=document.createElement('p');p.className='dv-muted';p.textContent='Aucune proposition pour le moment.';host.append(p);return;}
for(const t of tasks){let btn=document.createElement('button');btn.type='button';btn.className='dv-task'+(selected===t.id?' selected':'');
let strong=document.createElement('strong');strong.textContent=t.title;let small=document.createElement('small');small.textContent=t.id+' · '+t.status+' · '+t.changed.length+' fichier(s)';btn.append(strong,small);btn.addEventListener('click',()=>select(t.id));host.append(btn);}
}
async function refresh(){const r=await fetchJson('/api/control/developer/tasks');renderTasks(r.tasks);if(selected)await select(selected,false);}
async function select(id,again=true){selected=id;const d=await fetchJson('/api/control/developer/tasks/'+id);latest=d.task;
$('dvEmpty').hidden=true;$('dvSelected').hidden=false;$('dvTitle').textContent=latest.title;
$('dvState').textContent=latest.status;$('dvMeta').textContent='Proposition '+id+' · '+latest.changed.length+' fichier(s) · '+latest.created_at;
$('dvDiff').textContent=latest.diff||'Aucun changement détecté.';
const c=$('dvChecks');c.replaceChildren();for(const t of latest.checks||[]){let div=document.createElement('div');div.className='dv-check'+(t.ok?'':' fail');div.textContent=(t.ok?'✓ ':'✗ ')+t.file+' — '+t.detail;c.append(div);}
if(!c.childNodes.length)c.textContent='Contrôles pas encore réalisés.';
$('dvActions').hidden=latest.status!=='pending';$('dvNext').hidden=latest.status!=='approved';
$('dvCommand').textContent='sudo python3 /opt/alfred/app/tools/alfred_deploy.py deploy '+id+' --apply';
if(again)await refresh();
}
async function decide(decision){if(!latest||busy)return;const desc=decision==='approve'?'approuver':'refuser';
if(!confirm('Confirmer : '+desc+' la proposition '+latest.id+' ?'))return;
busy=true;try{const id=latest.id;const d=await fetchJson('/api/control/developer/tasks/'+id+'/decision',{
method:'POST',headers:{'Content-Type':'application/json','X-Alfred-CSRF':csrf},body:JSON.stringify({decision,diff_sha256:latest.diff_sha256})
});message('Décision enregistrée : '+d.result.status,true);await select(id);}catch(e){message(e.message);}finally{busy=false;}
}
$('dvLogin').addEventListener('submit',async e=>{e.preventDefault();try{await fetchJson('/api/control/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({password:$('dvPassword').value})});$('dvPassword').value='';await auth();}catch(err){message(err.message,false,'dvLoginInfo');}});
$('dvRefresh').addEventListener('click',()=>refresh().catch(err=>message(err.message)));
$('dvApprove').addEventListener('click',()=>decide('approve'));
$('dvReject').addEventListener('click',()=>decide('reject'));
getName();auth().catch(e=>{$('dvLoginBox').hidden=false;message(e.message,false,'dvLoginInfo');});
})();
@@ -0,0 +1,111 @@
/* ALFRED_HERMES_NEW_SESSION_V1 — reset sûr de la seule session conversationnelle Hermes. */
(() => {
'use strict';
function setup() {
const grid = document.querySelector('#avMoreDialog .av-more-grid');
if (!grid || document.getElementById('avHermesNewSession')) return false;
const button = document.createElement('button');
button.type = 'button';
button.id = 'avHermesNewSession';
button.className = 'av-hermes-new-session';
button.innerHTML = '<svg viewBox="0 0 24 24" class="a-icon" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 12a9 9 0 1 0 3-6.7"/><path d="M3 3v6h6"/><path d="M12 7v5l4 2"/></svg><span>Nouvelle session Hermes</span>';
grid.appendChild(button);
const dialog = document.createElement('dialog');
dialog.id = 'avHermesSessionDialog';
dialog.className = 'av-hermes-session-dialog';
dialog.setAttribute('aria-labelledby', 'avHermesSessionTitle');
dialog.innerHTML = `
<form method="dialog" class="av-hermes-session-content">
<h2 id="avHermesSessionTitle">Nouvelle session Hermes</h2>
<p>Repartir sans le contexte de l’ancienne conversation. La prochaine demande utilisera une nouvelle session avec les outils Hermes disponibles. La session actuelle sera sauvegardée.</p>
<p class="av-hermes-session-note">Les outils, les Skills et les réglages de Dona restent inchangés. Aucune tâche Hermes n'est annulée.</p>
<p id="avHermesSessionMessage" class="av-hermes-session-message" role="status" aria-live="polite"></p>
<div class="av-hermes-session-actions">
<button type="button" id="avHermesSessionCancel" class="av-hermes-session-secondary">Annuler</button>
<button type="button" id="avHermesSessionConfirm" class="av-hermes-session-primary">Créer une nouvelle session</button>
</div>
</form>`;
document.body.appendChild(dialog);
const msg = dialog.querySelector('#avHermesSessionMessage');
const confirm = dialog.querySelector('#avHermesSessionConfirm');
const cancel = dialog.querySelector('#avHermesSessionCancel');
let csrf = '';
let busy = false;
function setMessage(text, error = false) {
msg.textContent = text;
msg.classList.toggle('is-error', error);
}
button.addEventListener('click', async () => {
document.getElementById('avMoreDialog')?.close();
setMessage('');
confirm.hidden = false;
cancel.textContent = 'Annuler';
dialog.showModal();
confirm.disabled = true;
try {
const response = await fetch('/api/control/auth', {credentials:'same-origin',cache:'no-store'});
if (!response.ok) throw new Error('Vérification administrateur impossible.');
const auth = await response.json();
if (!auth.authenticated || !auth.csrf) {
setMessage('Connecte-toi d’abord en administrateur dans Configuration (/config), puis reviens ici.', true);
confirm.hidden = true;
cancel.textContent = 'Fermer';
const link = document.createElement('a');
link.href = '/config';
link.textContent = 'Ouvrir Configuration';
link.className = 'av-hermes-session-login';
const old = dialog.querySelector('.av-hermes-session-login');
old?.remove();
msg.after(link);
} else {
dialog.querySelector('.av-hermes-session-login')?.remove();
csrf = auth.csrf;
confirm.disabled = false;
}
} catch (e) {
setMessage(e.message, true);
}
});
cancel.addEventListener('click', () => { if (!busy) dialog.close(); });
confirm.addEventListener('click', async () => {
if (busy || !csrf) return;
busy = true;
confirm.disabled = true;
cancel.disabled = true;
setMessage('Création de la nouvelle session…');
try {
const response = await fetch('/api/control/hermes/new-session', {
method:'POST', credentials:'same-origin', cache:'no-store',
headers: {'X-Alfred-CSRF':csrf,'Content-Type':'application/json'},
body:'{}'
});
const data = await response.json();
if (!response.ok || !data.ok) throw new Error(data.error || 'Erreur HTTP '+response.status);
setMessage(data.message || 'Nouvelle session prête.');
confirm.hidden = true;
cancel.textContent = 'Fermer';
} catch (e) {
setMessage(e.message, true);
} finally {
busy = false;
cancel.disabled = false;
confirm.disabled = false;
}
});
return true;
}
function init() {
if (setup()) return;
const observer = new MutationObserver(() => { if (setup()) observer.disconnect(); });
observer.observe(document.body,{childList:true,subtree:true});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init, {once:true});
else init();
})();
@@ -0,0 +1,15 @@
/* Add a maintenance link without changing any existing dashboard handler. */
(() => {
'use strict';
function run(){
const grid=document.querySelector('#avMoreDialog .av-more-grid');
if(grid&&!grid.querySelector('a[href="/maintenance"]')){
const link=document.createElement('a');link.href='/maintenance';link.textContent='Maintenance & développement';grid.append(link);
}
for(const nav of document.querySelectorAll('header nav')){
if(nav.querySelector('a[href="/maintenance"]'))continue;
const a=document.createElement('a');a.href='/maintenance';a.textContent='Maintenance';nav.append(a);
}
}
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',run,{once:true});else run();
})();
@@ -0,0 +1,51 @@
/* ALFRED_REPONSE_CLOSE_V1 — Boutons visibles dans la scène, sans modifier Hermes. */
(() => {
'use strict';
const stage = document.getElementById('stage');
const main = document.getElementById('alfredMainV1');
const returnButton = document.getElementById('avBack');
if (!stage || !main || !returnButton) {
console.warn('[Dona] Fermeture réponse : éléments Glass V2 manquants.');
return;
}
if (stage.dataset.alfredReplyCloseV1 === 'ready') return;
stage.dataset.alfredReplyCloseV1 = 'ready';
// La V1 crée le bouton « Retour à l’accueil » DANS #alfredMainV1 (z-index 1),
// alors que la scène #stage est un frère au-dessus (z-index 8).
// On déplace le bouton D'ORIGINE : son écouteur de clic et son id restent intacts.
const bar = document.createElement('div');
bar.className = 'av-reponse-actions';
bar.setAttribute('role', 'group');
bar.setAttribute('aria-label', 'Navigation dans la réponse');
const closeButton = document.createElement('button');
closeButton.type = 'button';
closeButton.className = 'av-reponse-close';
closeButton.setAttribute('aria-label', 'Fermer la réponse et revenir à l’accueil');
closeButton.title = 'Fermer la réponse';
closeButton.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M5 5l14 14M19 5 5 19"/></svg>';
returnButton.classList.add('av-reponse-home');
returnButton.removeAttribute('style');
returnButton.title = 'Fermer cette réponse et revenir à l’accueil';
bar.append(returnButton, closeButton);
stage.insertBefore(bar, stage.firstChild);
const goHome = () => {
// Utilise le gestionnaire déjà attaché à #avBack par Glass V1 :
// focusScene(false) + éventuel AlfredViews.goHome().
returnButton.click();
};
closeButton.addEventListener('click', goHome);
document.addEventListener('keydown', event => {
if (event.key !== 'Escape' || event.defaultPrevented || event.altKey || event.ctrlKey || event.metaKey) return;
// Les boîtes de dialogue (caméra, ComfyUI, etc.) gardent leur propre Escape.
if (document.querySelector('dialog[open], [role="dialog"][aria-modal="true"]')) return;
if (main.classList.contains('is-focused')) {
event.preventDefault();
goHome();
}
});
})();
@@ -0,0 +1,28 @@
/* ALFRED_STT_INLINE_GLASS_V1 — deplacement de la transcription existante, sans dupliquer les elements. */
(() => {
'use strict';
function mount() {
const hero = document.querySelector('#alfredMainV1 .av-voice-hero');
const panel = document.querySelector('.transcript-panel');
if (!hero || !panel) return false;
if (panel.classList.contains('av-stt-inline')) return true;
// alfred.js et les autres extensions gardent les memes noeuds et les memes IDs.
// Les mises a jour Vosk/Whisper restent donc fonctionnelles sans nouveau listener.
hero.insertBefore(panel, hero.firstChild);
panel.classList.add('av-stt-inline');
hero.classList.add('av-stt-inline-on');
panel.setAttribute('aria-label', 'Transcription vocale en direct');
// Taille du canvas recalculee par le gestionnaire actuel en cas de resize.
window.requestAnimationFrame(() => window.dispatchEvent(new Event('resize')));
return true;
}
if (!mount()) {
// Les scripts Glass sont generalement synchrones, mais on gere un chargement differe.
let count = 0;
const interval = window.setInterval(() => {
if (mount() || ++count >= 16) window.clearInterval(interval);
}, 300);
}
})();
+140
View File
@@ -0,0 +1,140 @@
/* DONA_HOLOGRAPHIC_V1
* Cartes contextuelles issues du TEXTE réellement reçu d'Hermes.
* Aucune donnée, état de serveur, distance ou durée n'est calculé/inventé.
* Les autres vues (images, calendriers, cartes de renderer) restent inchangées.
*/
(() => {
'use strict';
const words = Object.freeze({zero:0,un:1,une:1,deux:2,trois:3,quatre:4,cinq:5,six:6,sept:7,huit:8,neuf:9,dix:10,onze:11,douze:12,treize:13,quatorze:14,quinze:15,seize:16,dixsept:17,dixhuit:18,dixneuf:19,vingt:20});
function numberValue(s){
if(!s)return null;
const normalized=String(s).toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g,'').replace(/[\s-]/g,'');
if(Object.prototype.hasOwnProperty.call(words,normalized))return String(words[normalized]);
const str=String(s).trim();return /^\d{1,5}$/.test(str)?str:null;
}
const number='(\\d{1,5}|vingt|dix[- ]?neuf|dix[- ]?huit|dix[- ]?sept|seize|quinze|quatorze|treize|douze|onze|dix|neuf|huit|sept|six|cinq|quatre|trois|deux|une|un)';
function getFacts(input) {
const text=String(input||'').replace(/\u00a0/g,' ').slice(0,16000);
const facts=[];const seen=new Set();
function add(key,icon,label,value,note='Mentionné dans la réponse'){
if(!value||seen.has(key)||facts.length>=4)return;
facts.push({key,icon,label,value:String(value),note});seen.add(key);
}
let m;
m=text.match(new RegExp('\\b'+number+'\\s+(?:machines?\\s+virtuelles?|VMs?\\b)','i'));
if(m)add('vm','⬡','Machines virtuelles',numberValue(m[1]),'Chiffre annoncé par Hermes');
m=text.match(new RegExp('\\b'+number+'\\s+(?:n[œo]uds?|nodes?)(?:\\s|\\.|,|$)','i'));
if(m)add('nodes','▦','Nœuds mentionnés',numberValue(m[1]),'Chiffre annoncé par Hermes');
m=text.match(/\b(\d{1,3})\s*\/\s*(\d{1,3})\s*(?:n[œo]uds|serveurs|nodes?)\b/i);
if(m)add('online','●','Nœuds disponibles',m[1]+' / '+m[2],'Valeur citée par Hermes');
const route=/\b(trajet|distance|route|entre\s+\S+\s+et\s+\S+|autoroute|en voiture)\b/i.test(text);
const building=/\b(hauteur|haut(?:e|s)?|b[aâ]timent|immeuble|tour|monument|construction)\b/i.test(text);
if(route){
m=text.match(/\b(\d{1,5}(?:[,.]\d+)?)\s*(kilom[eè]tres?|km)\b/i);
if(m)add('distance','↗','Distance indiquée',m[1].replace(',', '.')+' km','Distance issue de la réponse');
m=text.match(/\b(\d{1,3})\s*(?:à|a|–|-)\s*(\d{1,3})\s*(minutes?|min|heures?|h)\b/i);
if(m)add('duration','◷','Durée annoncée',m[1]+' à '+m[2]+' '+m[3],'Estimation communiquée par Hermes');
else {
m=text.match(/\b(\d{1,3})\s*(minutes?|min|heures?|h)\b/i);
if(m)add('duration','◷','Durée annoncée',m[1]+' '+m[2],'Estimation communiquée par Hermes');
}
}
if(building&&!route){
m=text.match(/\b(\d{1,5}(?:[,.]\d+)?)\s*(?:m[eè]tres?|m)\b/i);
if(m)add('height','♜','Hauteur indiquée',m[1]+' m','Valeur issue de la réponse');
}
if(/\b(temp[eé]rature|m[eé]t[eé]o|degr[eé]s)\b/i.test(text)){
m=text.match(/(-?\d{1,3}(?:[,.]\d+)?)\s*°\s*C\b/i);
if(m)add('temp','☀','Température citée',m[1]+' °C','Valeur issue de la réponse');
}
// Ne jamais induire qu'un nœud est en ligne à partir du seul nom.
if(facts.length<4){
m=text.match(/[«"']\s*([\wÀ-ÿ][\wÀ-ÿ_.-]{1,39})\s*[»"'][^.!?\n]{0,90}(arr[eê]t[eé]e?|[eé]teint(?:e)?)(?=[\s,.!?]|$)/i);
if(m)add('stopped','⏻','Équipement signalé arrêté',m[1],'État décrit dans la réponse');
}
if(facts.length===0){
m=text.match(/\b(?:il est(?: actuellement)?|heure(?: locale)?\s*[::])\s*(\d{1,2}[:h]\d{2})\b/i);
if(m)add('time','◷','Heure indiquée',m[1].replace('h',':'),'Issue de la réponse, non vérifiée ici');
}
const names=[];
const re=/(?:n[œo]ud|node)\s*['"«]?([\wÀ-ÿ_.-]{2,35})['"»]?\s*:/gi;
while((m=re.exec(text))&&names.length<10){if(!names.includes(m[1])) names.push(m[1]);}
return {facts,names};
}
function summaryOf(text,title){
const clean=String(text||'').trim();const t=String(title||'').trim();
const first=clean.split(/(?<=[.!?])\s+|\n{2,}/)[0]?.trim()||clean;
const headline=t && !/^(r[eé]ponse|hermes|texte)$/i.test(t)?t:first;
const truncated=headline.length>170?headline.slice(0,167).replace(/\s+\S*$/,'')+'…':headline;
let excerpt=clean;
if(clean.startsWith(first)&&first.length>35)excerpt=clean.slice(first.length).replace(/^\s+/,'');
if(!excerpt)excerpt=clean;
if(excerpt.length>290)excerpt=excerpt.slice(0,287).replace(/\s+\S*$/,'')+'…';
return {headline:truncated,excerpt};
}
if(typeof module==='object'&&module.exports){module.exports={getFacts,summaryOf,numberValue};}
if(typeof document==='undefined')return;
const body=document.body;
if(!body||body.dataset.donaHoloV1==='active')return;
body.dataset.donaHoloV1='active';body.classList.add('dona-holo-v1');
const main=document.getElementById('alfredMainV1');
const content=document.getElementById('content');
if(!main||!content)return; // Pages secondaires : seulement le thème transversal.
function el(tag,className,txt){const node=document.createElement(tag);if(className)node.className=className;if(txt!==undefined)node.textContent=String(txt);return node;}
let busy=false;let original='';
function renderCards(){
if(busy || content.firstElementChild?.classList.contains('dh-response'))return;
if(content.querySelector('.cards, img.hero, video, canvas, iframe'))return;
// N'afficher les cartes que pour une réponse réellement ouverte par Alfred.
if(!main.classList.contains('is-focused'))return;
const children=[...content.children];
if(!children.length)return;
const text=children.filter(n=>n.matches('h1,h2,p,pre')).map(n=>n.textContent.trim()).filter(Boolean).join('\n\n').trim();
if(!text || text.length<13)return;
if(/^(bonjour\.|à votre écoute\.|en attente|chargement)/i.test(text))return;
if(text===original && content.dataset.holoTransformed==='done')return;
const title=content.querySelector('h1,h2')?.textContent||'';
const raw=(title && text.startsWith(title+'\n\n'))?text.slice(title.length+2):text;
const {facts,names}=getFacts(text);
const summary=summaryOf(raw,title);
const wrap=el('section','dh-response');
wrap.setAttribute('aria-label','Synthèse contextuelle de la réponse Hermes');
const primary=el('article','dh-primary');
primary.append(el('div','dh-kicker','RÉPONSE · DONA'),el('div','dh-headline',summary.headline));
if(summary.excerpt && summary.excerpt!==summary.headline)primary.append(el('div','dh-excerpt',summary.excerpt));
else primary.append(el('div','dh-excerpt','Réponse disponible dans son intégralité.'));
const more=el('button','dh-more');more.type='button';more.setAttribute('aria-expanded','false');more.append(el('span','', '▤ Afficher la réponse complète'),el('span','','›'));
primary.append(more);wrap.append(primary);
if(facts.length||names.length){
const grid=el('div','dh-facts');
facts.forEach(f=>{
const c=el('article','dh-fact');
c.append(el('span','dh-fact-icon',f.icon),el('div','dh-fact-label',f.label),el('strong','dh-fact-value',f.value),el('div','dh-fact-note',f.note));
grid.append(c);
});
if(names.length && grid.children.length<4){
const c=el('article','dh-fact');c.append(el('span','dh-fact-icon','⬡'),el('div','dh-fact-label','Nœuds cités'));
const tags=el('div','dh-tags');names.forEach(n=>tags.append(el('span','dh-tag',n)));c.append(tags,el('div','dh-fact-note','Noms présents dans la réponse'));grid.append(c);
}
wrap.append(grid);
} else {
wrap.classList.add('dh-no-facts');
}
const full=el('section','dh-full');full.hidden=true;full.id='dhFullReply';
full.append(el('h3','', 'Réponse complète d’Hermes'),el('div','dh-full-text',text),el('p','dh-source','Affichage fidèle au texte reçu. Les faits n’ont pas été vérifiés séparément.'));
more.setAttribute('aria-controls','dhFullReply');
more.addEventListener('click',()=>{
const opened=full.hidden;full.hidden=!opened;more.setAttribute('aria-expanded',String(opened));
more.firstElementChild.textContent=opened?'▤ Masquer la réponse complète':'▤ Afficher la réponse complète';
more.lastElementChild.textContent=opened?'⌃':'›';
});
wrap.append(full);
busy=true;content.replaceChildren(wrap);content.dataset.holoTransformed='done';original=text;busy=false;
}
const obs=new MutationObserver(()=>{if(!busy)renderCards();});
obs.observe(content,{childList:true});
// Accès à l'écran de réponse après mise à jour de la classe focus, sans boucle.
main.addEventListener('click',()=>setTimeout(renderCards,0));
// Le renderer/socket réactualise #content : la mutation constitue l'événement canonique.
setTimeout(renderCards,100);
})();