v2 - new interface, many new page for fine tuning
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
/* Admin Alfred Glass — style harmonisé et configuration du cadre Maison. */
|
||||
(() => {
|
||||
'use strict';
|
||||
const page = document.body.dataset.page;
|
||||
if (!['config','surveillance'].includes(page)) return;
|
||||
document.body.classList.add('alfred-admin-glass');
|
||||
const rail = document.createElement('aside');
|
||||
rail.className = 'ag-rail';
|
||||
rail.setAttribute('aria-label','Navigation Alfred');
|
||||
rail.innerHTML = `<a href="/" title="Accueil"><span>⌂</span>Accueil</a>
|
||||
<a href="/config" title="Configuration" class="${page==='config'?'is-active':''}"><span>⚙</span>Réglages</a>
|
||||
<a href="/surveillance" title="Surveillance" class="${page==='surveillance'?'is-active':''}"><span>▤</span>Suivi</a>`;
|
||||
document.querySelector('.admin-layout')?.prepend(rail);
|
||||
const brand = document.querySelector('.admin-header .brand small');
|
||||
if (brand) brand.textContent = 'Assistant vocal · Centre de contrôle';
|
||||
const main = document.getElementById('mainArea');
|
||||
if (!main || page !== 'config') return;
|
||||
|
||||
const editor = document.createElement('section');
|
||||
editor.id='agMaisonEditor';
|
||||
editor.className='panel ag-maison-editor';
|
||||
editor.innerHTML = `
|
||||
<div class="panel-title"><span class="panel-symbol">⌂</span>
|
||||
<div><h2>Cadre « Maison » de l’accueil</h2><p>Personnalise jusqu’à trois informations affichées en haut de l’écran principal</p></div></div>
|
||||
<p class="ag-help">Choisis un compteur global ou une entité précise de Home Assistant, et personnalise le libellé. Les valeurs sont actualisées automatiquement sans redémarrer Alfred.</p>
|
||||
<div id="agMaisonRows" class="ag-maison-rows"></div>
|
||||
<div class="ag-maison-actions">
|
||||
<button class="button ghost" type="button" id="agAddMaison">+ Ajouter une ligne</button>
|
||||
<button class="button ghost" type="button" id="agDefaultMaison">Valeurs par défaut</button>
|
||||
<button class="button primary" type="button" id="agSaveMaison">Enregistrer le cadre Maison</button>
|
||||
</div><p id="agMaisonMessage" role="status" aria-live="polite" class="message"></p>`;
|
||||
main.querySelector('.restart-panel')?.before(editor);
|
||||
|
||||
const types = [
|
||||
['lights_on','Nombre de lumières allumées'],
|
||||
['covers_closed','Nombre de volets fermés'],
|
||||
['indoor_temperature','Température intérieure'],
|
||||
['entity','Entité Home Assistant précise']
|
||||
];
|
||||
const defaults = [
|
||||
{kind:'lights_on',label:'Lumières allumées'},
|
||||
{kind:'covers_closed',label:'Volets fermés'},
|
||||
{kind:'indoor_temperature',label:'Température intérieure'}
|
||||
];
|
||||
const list = editor.querySelector('#agMaisonRows');
|
||||
const message = editor.querySelector('#agMaisonMessage');
|
||||
const add = editor.querySelector('#agAddMaison');
|
||||
let entities = [];
|
||||
let rows = defaults.map(x=>({...x}));
|
||||
let busy=false;
|
||||
const msg=(text,error=false)=>{message.textContent=text;message.classList.toggle('ag-error',error);};
|
||||
const el=(name,cls,text)=>{const node=document.createElement(name);if(cls)node.className=cls;if(text!==undefined)node.textContent=text;return node;};
|
||||
|
||||
function render() {
|
||||
list.replaceChildren();
|
||||
rows.forEach((row,i)=>{
|
||||
const box=el('div','ag-row');
|
||||
const header=el('div','ag-row-header');
|
||||
header.append(el('strong','',`Ligne ${i+1}`));
|
||||
const nav=el('div','ag-row-buttons');
|
||||
const action=(name,symbol,title,fn)=>{
|
||||
const b=el('button','ag-small-button',symbol);b.type='button';b.title=title;b.setAttribute('aria-label',title);
|
||||
b.disabled=busy;b.addEventListener('click',fn);nav.append(b);
|
||||
};
|
||||
action('up','↑','Monter',()=>{if(i){[rows[i-1],rows[i]]=[rows[i],rows[i-1]];render();}});
|
||||
action('down','↓','Descendre',()=>{if(i<rows.length-1){[rows[i],rows[i+1]]=[rows[i+1],rows[i]];render();}});
|
||||
action('remove','×','Supprimer',()=>{if(rows.length>1){rows.splice(i,1);render();}});
|
||||
header.append(nav);box.append(header);
|
||||
const settings=el('div','ag-row-fields');
|
||||
const kindWrap=el('label','ag-field');kindWrap.append(el('span','', 'Information affichée'));
|
||||
const kind=el('select');
|
||||
types.forEach(([value,txt])=>{const o=el('option','',txt);o.value=value;o.selected=row.kind===value;kind.append(o);});
|
||||
kind.addEventListener('change',()=>{row.kind=kind.value;if(row.kind==='entity'&&!row.entity_id)row.entity_id=entities[0]?.id||'';render();});
|
||||
kindWrap.append(kind);settings.append(kindWrap);
|
||||
const labelWrap=el('label','ag-field');labelWrap.append(el('span','', 'Intitulé personnalisé'));
|
||||
const text=el('input');text.type='text';text.maxLength=38;text.required=true;text.value=row.label||'';
|
||||
text.addEventListener('input',()=>{row.label=text.value;});labelWrap.append(text);settings.append(labelWrap);
|
||||
if(row.kind==='entity'){
|
||||
const entityWrap=el('label','ag-field ag-wide');entityWrap.append(el('span','', 'Entité Home Assistant'));
|
||||
const field=el('input');field.type='text';field.maxLength=120;
|
||||
field.value=row.entity_id||'';field.placeholder='sensor.salon_temperature';
|
||||
field.setAttribute('list',`agEntityList${i}`);
|
||||
field.addEventListener('input',()=>{row.entity_id=field.value.trim();});
|
||||
const choices=el('datalist');choices.id=`agEntityList${i}`;
|
||||
entities.forEach(entry=>{const o=el('option');o.value=entry.id;o.label=entry.name;choices.append(o);});
|
||||
entityWrap.append(field,choices);
|
||||
const helper=el('small','',entities.length
|
||||
? 'Choisis une entité proposée ou saisis son identifiant.'
|
||||
: 'Saisis un identifiant comme sensor.salon_temperature si Home Assistant est inaccessible.');
|
||||
entityWrap.append(helper);settings.append(entityWrap);
|
||||
}
|
||||
box.append(settings);list.append(box);
|
||||
});
|
||||
add.disabled=rows.length>=3 || busy;
|
||||
}
|
||||
function tryAuthFetch(url,options={}){
|
||||
return fetch('/api/control/auth',{cache:'no-store',credentials:'same-origin'}).then(async r=>{
|
||||
const a=await r.json();if(!a.authenticated)throw new Error('Connexion administrateur nécessaire.');
|
||||
const headers=options.method==='POST'?{'Content-Type':'application/json','X-Alfred-CSRF':a.csrf}:{};
|
||||
const response=await fetch(url,{...options,headers,credentials:'same-origin',cache:'no-store'});
|
||||
const body=await response.json().catch(()=>({}));
|
||||
if(!response.ok)throw new Error(body.error||`Erreur HTTP ${response.status}`);
|
||||
return body;
|
||||
});
|
||||
}
|
||||
async function refresh(){
|
||||
if(busy)return;
|
||||
busy=true;
|
||||
try{
|
||||
const config=await tryAuthFetch('/api/control/home-card');
|
||||
rows=Array.isArray(config.rows)?config.rows:defaults.map(x=>({...x}));
|
||||
const response=await tryAuthFetch('/api/control/home-entities').catch(err=>{msg('Liste des entités indisponible : '+err.message,true);return null;});
|
||||
entities=response?.entities||[];
|
||||
if(response)msg(`${entities.length} entités Home Assistant disponibles.`);
|
||||
}catch(e){msg(e.message,true);}
|
||||
finally{busy=false;render();}
|
||||
}
|
||||
editor.querySelector('#agDefaultMaison').addEventListener('click',()=>{rows=defaults.map(x=>({...x}));render();msg('Valeurs par défaut prêtes à enregistrer.');});
|
||||
add.addEventListener('click',()=>{if(rows.length>=3)return;rows.push({kind:'entity',label:'Information',entity_id:entities[0]?.id||''});render();});
|
||||
editor.querySelector('#agSaveMaison').addEventListener('click',async()=>{
|
||||
if(busy)return;
|
||||
busy=true;
|
||||
try{
|
||||
const result=await tryAuthFetch('/api/control/home-card',{method:'POST',body:JSON.stringify({rows})});
|
||||
rows=result.rows;
|
||||
msg('Cadre Maison enregistré. Les changements apparaîtront sur l’accueil en quelques secondes.');
|
||||
}catch(e){msg('Impossible d’enregistrer : '+e.message,true);}
|
||||
finally{busy=false;render();}
|
||||
});
|
||||
let loaded=false;
|
||||
async function checkLogin(){
|
||||
if(!main.hidden&&!loaded){loaded=true;await refresh();}
|
||||
if(main.hidden)loaded=false;
|
||||
}
|
||||
new MutationObserver(checkLogin).observe(main,{attributes:true,attributeFilter:['hidden']});
|
||||
checkLogin();
|
||||
})();
|
||||
@@ -54,6 +54,7 @@
|
||||
configDirty();
|
||||
$('pendingNotice').hidden = false;
|
||||
setMessage(result.ok ? 'Configuration sauvegardée.' : 'Enregistrement inconnu.');
|
||||
if (result.identity_sync_warning) setMessage('Nom enregistré, mais identity.md non synchronisé : '+result.identity_sync_warning, true);
|
||||
} catch (error) { setMessage(error.message, true); }
|
||||
}
|
||||
function serviceStatus(el, data) {
|
||||
@@ -165,12 +166,12 @@
|
||||
fields.forEach(el=>{el.addEventListener('change',markInput);el.addEventListener('input',markInput);});
|
||||
$('saveConfig')?.addEventListener('click',saveConfig);
|
||||
$('restartButton')?.addEventListener('click',async ()=>{
|
||||
if(!confirm('Redémarrer Alfred maintenant ? Les opérations en cours seront interrompues.'))return;
|
||||
if(!confirm('Redémarrer '+(document.querySelector('.brand strong')?.textContent || 'l’assistant')+' maintenant ? Les opérations en cours seront interrompues.'))return; // ALFRED_IDENTITY_GLASS_V1
|
||||
try{const data=await fetchJSON('/api/control/restart',{method:'POST'});setMessage(data.message||'Demande transmise.');$('restartButton').disabled=true;setTimeout(()=>$('restartButton').disabled=false,5000);}
|
||||
catch(err){setMessage(err.message,true);}
|
||||
});
|
||||
$('testVoice')?.addEventListener('click',async()=>{
|
||||
try{await fetchJSON('/api/tts/say',{method:'POST',body:JSON.stringify({text:'Bonjour, je suis Alfred. Voici un test de ma voix actuelle.'})});setMessage('Test vocal demandé.');}
|
||||
try{await fetchJSON('/api/tts/say',{method:'POST',body:JSON.stringify({text:'Bonjour, je suis '+(document.querySelector('.brand strong')?.textContent || 'votre assistant')+'. Voici un test de ma voix actuelle.'})});setMessage('Test vocal demandé.');}
|
||||
catch(err){setMessage(err.message,true);}
|
||||
});
|
||||
let volDelay, gainDelay;
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/* Identity editing: admin-protected API, optimistic revision checks, no HTML injection. */
|
||||
(() => {
|
||||
'use strict';
|
||||
const $=id=>document.getElementById(id);
|
||||
let csrf='',revision='',dirty=false,busy=false;
|
||||
const msg=(s,error=false,id='idMessage')=>{const e=$(id);e.textContent=s;e.classList.toggle('error',error);};
|
||||
const setName=name=>{
|
||||
const n=String(name||'Assistant').trim();
|
||||
const initial=Array.from(n)[0]?.toLocaleUpperCase('fr-FR')||'A';
|
||||
$('idName').textContent=n;$('idInitial').textContent=initial;
|
||||
$('idActiveName').textContent=n;$('idAvatar').textContent=initial;
|
||||
document.title='Identité · '+n;
|
||||
};
|
||||
function updateCount(){$('idCount').textContent=$('idText').value.length.toLocaleString('fr-FR')+' caractères';}
|
||||
async function jsonFetch(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 a=await jsonFetch('/api/control/auth');
|
||||
csrf=a.csrf||'';
|
||||
$('idLoginPanel').hidden=!!a.authenticated;
|
||||
$('idEditor').hidden=!a.authenticated;
|
||||
$('idBadge').textContent=a.authenticated?'Administrateur connecté':'Connexion requise';
|
||||
$('idBadge').classList.toggle('ok',!!a.authenticated);
|
||||
if(a.authenticated)await load();
|
||||
}
|
||||
async function load(){
|
||||
try{
|
||||
const d=await jsonFetch('/api/control/identity');
|
||||
$('idText').value=d.text;revision=d.revision;dirty=false;
|
||||
setName(d.name);updateCount();msg('Document chargé.');
|
||||
}catch(e){msg(e.message,true);}
|
||||
}
|
||||
$('idLoginForm').addEventListener('submit',async e=>{
|
||||
e.preventDefault();
|
||||
try{
|
||||
await jsonFetch('/api/control/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({password:$('idPassword').value})});
|
||||
$('idPassword').value='';await auth();
|
||||
}catch(err){msg(err.message,true,'idLoginMessage');}
|
||||
});
|
||||
$('idText').addEventListener('input',()=>{dirty=true;updateCount();msg('Modifications non enregistrées.');});
|
||||
$('idSave').addEventListener('click',async()=>{
|
||||
if(busy||!revision)return;
|
||||
busy=true;$('idSave').disabled=true;
|
||||
try{
|
||||
const d=await jsonFetch('/api/control/identity',{
|
||||
method:'POST',headers:{'Content-Type':'application/json','X-Alfred-CSRF':csrf},
|
||||
body:JSON.stringify({text:$('idText').value,revision})
|
||||
});
|
||||
revision=d.revision;dirty=false;msg(d.updated?'Identité enregistrée. Nouvelle version sauvegardée.':'Aucun changement à enregistrer.');
|
||||
}catch(e){msg(e.message,true);}
|
||||
finally{busy=false;$('idSave').disabled=false;}
|
||||
});
|
||||
$('idReload').addEventListener('click',()=>{if(dirty&&!confirm('Abandonner les modifications non enregistrées ?'))return;load();});
|
||||
window.addEventListener('beforeunload',event=>{if(dirty){event.preventDefault();event.returnValue='';}});
|
||||
auth().catch(e=>{msg(e.message,true,'idLoginMessage');$('idLoginPanel').hidden=false;});
|
||||
})();
|
||||
@@ -0,0 +1,125 @@
|
||||
/* ALFRED_LISTES_GLASS_V1 — course/tâches, fenêtre Glass, pas de dépendance JS. */
|
||||
(() => {
|
||||
'use strict';
|
||||
if (document.getElementById('alfredListsGlassDialog')) return;
|
||||
const $ = id => document.getElementById(id);
|
||||
const icon = (d) => `<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${d}</svg>`;
|
||||
const shopping = icon('<path d="M3 7h18l-2 12H5L3 7ZM8 8V6a4 4 0 0 1 8 0v2"/>');
|
||||
const tasks = icon('<rect x="3" y="3" width="18" height="18" rx="3"/><path d="m7 12 3 3 6-6"/>');
|
||||
const shell = document.getElementById('alfredMainV1');
|
||||
if (!shell) return; // Le tableau de bord doit être installé avant ce module.
|
||||
const modal = document.createElement('dialog');
|
||||
modal.id = 'alfredListsGlassDialog';
|
||||
modal.className = 'al-list-dialog';
|
||||
modal.innerHTML = `<div class="al-list-head">
|
||||
<div><p class="al-eyebrow">ALFRED · ORGANISATION</p><h2>Courses & tâches</h2><p class="al-subtitle">Tes listes sont conservées sur Alfred et mises à jour en temps réel.</p></div>
|
||||
<button type="button" class="al-close" data-al-close title="Fermer" aria-label="Fermer">×</button>
|
||||
</div><div class="al-list-grid">
|
||||
<section class="al-list-section" data-kind="courses"><header class="al-list-sec-head"><div class="al-list-sec-icon">${shopping}</div><div><h3>Liste de courses</h3><p><span id="alCountCourses">—</span> article(s) à acheter</p></div></header>
|
||||
<form class="al-add-form" data-kind="courses"><input name="label" maxlength="160" placeholder="Pain, lait, tomates…" aria-label="Nouvelle course" required /><button type="submit">+ Ajouter</button></form>
|
||||
<ul class="al-items" id="alItemsCourses" aria-label="Courses"> </ul></section>
|
||||
<section class="al-list-section" data-kind="taches"><header class="al-list-sec-head"><div class="al-list-sec-icon al-task-icon">${tasks}</div><div><h3>Liste de tâches</h3><p><span id="alCountTaches">—</span> tâche(s) à faire</p></div></header>
|
||||
<form class="al-add-form" data-kind="taches"><input name="label" maxlength="160" placeholder="Peindre le mur…" aria-label="Nouvelle tâche" required /><button type="submit">+ Ajouter</button></form>
|
||||
<ul class="al-items" id="alItemsTaches" aria-label="Tâches"> </ul></section>
|
||||
</div><div class="al-footer"><span id="alListStatus" role="status" aria-live="polite">Chargement…</span><button type="button" data-al-refresh>Actualiser</button></div>`;
|
||||
document.body.append(modal);
|
||||
|
||||
// Navigation: garder toutes les entrées existantes, y compris « Plus ».
|
||||
const rail = shell.querySelector('.av-rail');
|
||||
if (rail && !rail.querySelector('[data-al-open]')) {
|
||||
const btn = document.createElement('button');
|
||||
btn.type = 'button'; btn.className = 'av-rail-link al-rail-link';
|
||||
btn.setAttribute('data-al-open', '');
|
||||
btn.innerHTML = `${tasks}<span>Listes</span>`;
|
||||
rail.querySelector('[data-av-action="more"]')?.before(btn);
|
||||
if (!btn.isConnected) rail.append(btn);
|
||||
}
|
||||
const shortcuts = shell.querySelector('.av-shortcuts');
|
||||
if (shortcuts && !shortcuts.querySelector('[data-al-open]')) {
|
||||
const btn = document.createElement('button'); btn.type = 'button';
|
||||
btn.setAttribute('data-al-open', '');
|
||||
btn.innerHTML = `${shopping}<span id="alShortcutText">Courses & tâches</span>`;
|
||||
shortcuts.querySelector('[data-av-action="more"]')?.before(btn);
|
||||
if (!btn.isConnected) shortcuts.append(btn);
|
||||
}
|
||||
const more = $('avMoreDialog')?.querySelector('.av-more-grid');
|
||||
if (more && !more.querySelector('[data-al-open]')) {
|
||||
const btn = document.createElement('button');btn.type='button';btn.setAttribute('data-al-open','');
|
||||
btn.innerHTML = `${shopping} Courses & tâches`; more.append(btn);
|
||||
}
|
||||
|
||||
function status(msg, error = false) {
|
||||
const el = $('alListStatus'); if (!el) return;
|
||||
el.textContent = msg;el.classList.toggle('al-error',error);
|
||||
}
|
||||
async function fetchLists() {
|
||||
const res = await fetch('/api/alfred/lists', {cache:'no-store'});
|
||||
const data = await res.json(); if (!res.ok || !data.ok) throw Error(data.error || `HTTP ${res.status}`);
|
||||
return data;
|
||||
}
|
||||
async function send(action, payload) {
|
||||
const res = await fetch('/api/alfred/lists/action', {
|
||||
method: 'POST', headers: {'Content-Type':'application/json','X-Alfred-Request':'1'},
|
||||
body: JSON.stringify({action,...payload})
|
||||
});
|
||||
const data = await res.json();if (!res.ok || !data.ok)throw Error(data.error || `HTTP ${res.status}`);
|
||||
return data;
|
||||
}
|
||||
function element(tag, text, cls) {
|
||||
const e=document.createElement(tag); if (cls)e.className=cls; if (text!==undefined)e.textContent=text;return e;
|
||||
}
|
||||
function render(items, kind) {
|
||||
const target=$(kind==='courses'?'alItemsCourses':'alItemsTaches');if(!target)return;
|
||||
target.replaceChildren();
|
||||
if(!items.length) {
|
||||
const li=element('li', 'Ta liste est vide. Ajoute un premier élément !', 'al-empty');target.append(li);return;
|
||||
}
|
||||
for(const item of items) {
|
||||
const li=element('li',undefined,'al-item'+(item.done?' is-done':''));
|
||||
const check=element('button',item.done?'✓':'','al-check');check.type='button';
|
||||
check.setAttribute('aria-label',item.done?'Réouvrir '+item.label:'Marquer comme terminé : '+item.label);
|
||||
check.title=item.done?'Réouvrir':'Cocher';
|
||||
check.addEventListener('click',()=>modify(item.done?'reopen_item':'complete_item',kind,item.id));
|
||||
const label=element('span',item.label,'al-item-label');
|
||||
const remove=element('button','×','al-remove');remove.type='button';remove.title='Supprimer';
|
||||
remove.setAttribute('aria-label','Supprimer '+item.label);
|
||||
remove.addEventListener('click',()=>modify('delete_item',kind,item.id));
|
||||
li.append(check,label,remove);target.append(li);
|
||||
}
|
||||
}
|
||||
async function refresh() {
|
||||
try {
|
||||
const data = await fetchLists();
|
||||
for(const kind of ['courses','taches']) {
|
||||
render(data.lists[kind] || [],kind);
|
||||
const dest=$(kind==='courses'?'alCountCourses':'alCountTaches');
|
||||
if(dest)dest.textContent=data.counts[kind]?.active ?? 0;
|
||||
}
|
||||
const shortcut=$('alShortcutText');
|
||||
if(shortcut)shortcut.textContent=`Courses ${data.counts.courses.active} · Tâches ${data.counts.taches.active}`;
|
||||
status('Listes à jour');
|
||||
} catch(e) {status(`Erreur : ${e.message}`,true);}
|
||||
}
|
||||
async function modify(action,kind,id) {
|
||||
try {await send(action,{kind,id});await refresh();}catch(e){status(e.message,true);}
|
||||
}
|
||||
document.addEventListener('click',(ev)=>{
|
||||
if(!ev.target.closest('[data-al-open]'))return;
|
||||
if($('avMoreDialog')?.open)$('avMoreDialog').close();
|
||||
modal.showModal();refresh();
|
||||
});
|
||||
modal.querySelector('[data-al-close]').addEventListener('click',()=>modal.close());
|
||||
modal.querySelector('[data-al-refresh]').addEventListener('click',refresh);
|
||||
modal.querySelectorAll('.al-add-form').forEach(form=>form.addEventListener('submit',async ev=>{
|
||||
ev.preventDefault();const input=form.querySelector('input');const label=input.value.trim();if(!label)return;
|
||||
try {
|
||||
const result=await send('add_item',{kind:form.dataset.kind,label});
|
||||
input.value='';await refresh();status(result.already_present?'Cet élément est déjà présent.':'Élément ajouté.');input.focus();
|
||||
}catch(e){status(e.message,true);}
|
||||
}));
|
||||
window.alfredSocket?.on('alfred_lists_changed',()=>refresh());
|
||||
window.addEventListener('focus',refresh);
|
||||
setInterval(refresh,30000);
|
||||
refresh();
|
||||
window.AlfredLists={open:()=>{modal.showModal();refresh();},refresh};
|
||||
})();
|
||||
@@ -0,0 +1,45 @@
|
||||
/* ALFRED_GLASS_LOGS_V2 — aucun HTML de journal injecte, seulement textContent. */
|
||||
(() => {
|
||||
'use strict';
|
||||
const $=id=>document.getElementById(id);
|
||||
let all=[];let authenticated=false;let pending=false;
|
||||
const when=new Intl.DateTimeFormat('fr-FR',{dateStyle:'short',timeStyle:'medium',timeZone:'Europe/Paris'});
|
||||
const el=(tag,cls,text)=>{const e=document.createElement(tag);if(cls)e.className=cls;if(text!=null)e.textContent=String(text);return e;};
|
||||
async function identity(){try{const r=await fetch('/api/state',{cache:'no-store'});if(!r.ok)return;const d=await r.json();const n=String(d.assistant_name||d.title||'').trim();if(!n)return;$('lgName').textContent=n;$('lgLetter').textContent=Array.from(n)[0].toLocaleUpperCase('fr-FR');document.title='Journaux · '+n;}catch(_){}}
|
||||
identity();
|
||||
function render(){
|
||||
const needle=$('lgSearch').value.trim().toLocaleLowerCase('fr-FR');
|
||||
const component=$('lgComponent').value,level=$('lgLevel').value;
|
||||
const filtered=all.filter(x=>(!component||x.component===component)&&(!level||x.level===level)&&(!needle||(x.message||'').toLocaleLowerCase('fr-FR').includes(needle)));
|
||||
const root=$('lgEvents');root.replaceChildren();
|
||||
$('lgCount').textContent=`${filtered.length} / ${all.length} événement(s)`;
|
||||
if(!filtered.length){root.append(el('div','lg-empty',authenticated?'Aucun événement correspondant aux filtres.':'Connecte-toi via la page Configuration pour consulter les journaux.'));return;}
|
||||
for(const x of filtered.slice().reverse()){
|
||||
const line=el('div','lg-event');line.dataset.level=x.level;
|
||||
let date='—';try{date=when.format(new Date(x.at));}catch(_){}
|
||||
const time=el('time','',date);time.dateTime=x.at;
|
||||
line.append(time,el('span','lg-component',x.component),el('span','lg-level',x.level),el('span','lg-message',x.message));
|
||||
root.append(line);
|
||||
}
|
||||
}
|
||||
async function refresh(){
|
||||
if(pending)return;pending=true;$('lgStatus').textContent='Actualisation…';
|
||||
try{const r=await fetch('/api/alfred/logs?limit='+encodeURIComponent($('lgLimit').value),{cache:'no-store'});const d=await r.json();
|
||||
if(r.status===401){authenticated=false;all=[];$('lgStatus').textContent='Accès administrateur requis · Connexion depuis /config';render();return;}
|
||||
if(!r.ok||!d.ok)throw Error(d.error||`HTTP ${r.status}`);
|
||||
authenticated=true;all=Array.isArray(d.items)?d.items:[];
|
||||
$('lgStatus').textContent='Dernière lecture : '+new Date().toLocaleTimeString('fr-FR',{timeZone:'Europe/Paris'});
|
||||
render();
|
||||
}catch(e){$('lgStatus').textContent='Journal indisponible : '+e.message;}finally{pending=false;}
|
||||
}
|
||||
for(const id of ['lgSearch','lgComponent','lgLevel'])$(id).addEventListener('input',render);
|
||||
$('lgLimit').addEventListener('change',refresh);
|
||||
$('lgRefresh').addEventListener('click',refresh);
|
||||
$('lgExport').addEventListener('click',()=>{
|
||||
if(!authenticated||!all.length)return;
|
||||
const file=new Blob([JSON.stringify({source:'alfred.service',exported_at:new Date().toISOString(),events:all},null,2)],{type:'application/json'});
|
||||
const url=URL.createObjectURL(file);const a=document.createElement('a');a.href=url;a.download='alfred-journaux-'+new Date().toISOString().slice(0,10)+'.json';a.click();URL.revokeObjectURL(url);
|
||||
});
|
||||
setInterval(()=>{if($('lgAuto').checked&&!document.hidden)refresh();},10000);
|
||||
window.addEventListener('focus',refresh);refresh();
|
||||
})();
|
||||
@@ -0,0 +1,319 @@
|
||||
/* 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:'<path d="m3 10 9-7 9 7v10a1 1 0 0 1-1 1h-5v-7H9v7H4a1 1 0 0 1-1-1z"/>',
|
||||
bulb:'<path d="M9 18h6m-5 3h4m-6-7c-1-1.2-2-2.5-2-5a6 6 0 0 1 12 0c0 2.5-1 3.8-2 5-.8 1-1 1.9-1 2H9c0-.1-.2-1-1-2z"/>',
|
||||
server:'<rect x="3" y="3" width="18" height="8" rx="2"/><rect x="3" y="13" width="18" height="8" rx="2"/><path d="M7 7h.01M7 17h.01M12 7h6M12 17h6"/>',
|
||||
brain:'<path d="M12 18V5a3 3 0 0 0-5-2 3 3 0 0 0-3 4 4 4 0 0 0 0 7 3.5 3.5 0 0 0 3.3 5A3.5 3.5 0 0 0 12 18m0-13a3 3 0 0 1 5-2 3 3 0 0 1 3 4 4 4 0 0 1 0 7 3.5 3.5 0 0 1-3.3 5A3.5 3.5 0 0 1 12 18"/><path d="m8 10 4 2 4-2"/>',
|
||||
media:'<rect x="3" y="4" width="18" height="16" rx="3"/><circle cx="8.5" cy="9" r="1.3"/><path d="m4 17 5-5 4 4 3-3 4 4"/>',
|
||||
more:'<circle cx="5" cy="12" r="1"/><circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/>',
|
||||
mic:'<rect x="9" y="2.5" width="6" height="13" rx="3"/><path d="M5 11a7 7 0 0 0 14 0M12 18v4m-4 0h8"/>',
|
||||
micOff:'<path d="M9 9v3a3 3 0 0 0 5.5 1.7M15 9V5.5a3 3 0 0 0-5.5-1.7M5 11a7 7 0 0 0 11.5 5.4M19 11a7 7 0 0 1-.5 2.6M12 18v4m-4 0h8M3 3l18 18"/>',
|
||||
speaker:'<path d="M11 5 6 9H3v6h3l5 4V5ZM15 9a4 4 0 0 1 0 6M18 6a8 8 0 0 1 0 12"/>',
|
||||
speakerOff:'<path d="M11 5 6 9H3v6h3l5 4V5ZM16 9l5 6m0-6-5 6"/>',
|
||||
camera:'<rect x="2" y="5" width="14" height="14" rx="3"/><path d="m16 10 6-3v10l-6-3"/>',
|
||||
settings:'<path d="M12 3v2m0 14v2M3 12h2m14 0h2M5.6 5.6 7 7m10 10 1.4 1.4M18.4 5.6 17 7M7 17l-1.4 1.4"/><circle cx="12" cy="12" r="4"/>',
|
||||
calendar:'<rect x="3" y="5" width="18" height="16" rx="2"/><path d="M7 3v4m10-4v4M3 10h18"/>',
|
||||
clock:'<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/>',
|
||||
cloud:'<path d="M7 18h11a4 4 0 0 0 0-8h-1a6 6 0 0 0-11-1 4.5 4.5 0 0 0 1 9Z"/>',
|
||||
shield:'<path d="m12 2 8 3v6c0 6-3.7 9-8 11-4.3-2-8-5-8-11V5z"/><path d="m9 12 2 2 4-4"/>',
|
||||
arrow:'<path d="m9 5 7 7-7 7"/>',
|
||||
chat:'<path d="M5 5h14a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H9l-5 3v-3a2 2 0 0 1-1-2V7a2 2 0 0 1 2-2z"/>',
|
||||
close:'<path d="M5 5l14 14M19 5 5 19"/>',
|
||||
sun:'<circle cx="12" cy="12" r="4"/><path d="M12 1v3m0 16v3M1 12h3m16 0h3M4 4l2 2m12 12 2 2m0-16-2 2M6 18l-2 2"/>',
|
||||
spark:'<path d="m12 2 2.5 7.5L22 12l-7.5 2.5L12 22l-2.5-7.5L2 12l7.5-2.5z"/>',
|
||||
};
|
||||
const icon = (name, extra = '') => `<svg class="a-icon ${extra}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${I[name] || I.more}</svg>`;
|
||||
const initialName = (document.title || 'Alfred').trim();
|
||||
const dashboard = document.createElement('div');
|
||||
dashboard.id = 'alfredMainV1';
|
||||
dashboard.className = 'alfred-main-v1';
|
||||
dashboard.innerHTML = `
|
||||
<header class="av-head">
|
||||
<div class="av-brand"><div class="av-logo">A</div><div><strong id="avName"></strong><small>Assistant vocal</small></div></div>
|
||||
<div class="av-header-right"><div class="av-time"><strong id="avTime">--:--</strong><span id="avDate">—</span></div>
|
||||
<div class="av-head-actions">
|
||||
<button type="button" class="av-round" id="avTopCamera" aria-label="Ouvrir la caméra" title="Caméra">${icon('camera')}</button>
|
||||
<button type="button" class="av-round" id="avTopMic" aria-label="Couper le microphone" aria-pressed="false" title="Couper le micro">${icon('mic')}</button>
|
||||
<button type="button" class="av-round" id="avTopSpeaker" aria-label="Couper la sortie audio" aria-pressed="false" title="Couper le son">${icon('speaker')}</button>
|
||||
<button type="button" class="av-round" id="avTopSettings" aria-label="Réglages audio" title="Réglages audio">${icon('settings')}</button>
|
||||
<a class="av-round" href="/config" aria-label="Configuration d'Alfred" title="Configuration">${icon('more')}</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<nav class="av-rail" aria-label="Navigation principale">
|
||||
<button type="button" class="av-rail-link selected" data-av-action="home">${icon('home')}<span>Accueil</span></button>
|
||||
<button type="button" class="av-rail-link" data-av-action="house">${icon('bulb')}<span>Maison</span></button>
|
||||
<a class="av-rail-link" href="/surveillance">${icon('server')}<span>Système</span></a>
|
||||
<a class="av-rail-link" href="/surveillance">${icon('brain')}<span>IA</span></a>
|
||||
<button type="button" class="av-rail-link" data-av-action="media">${icon('media')}<span>Médias</span></button>
|
||||
<button type="button" class="av-rail-link" data-av-action="more">${icon('more')}<span>Plus</span></button>
|
||||
</nav>
|
||||
<section class="av-summary" aria-label="État de la maison et des services">
|
||||
<button class="av-stat-card" type="button" data-av-action="house"><div class="av-card-top"><span class="av-circle home">${icon('home')}</span><span><strong>Maison</strong><small id="avHomeSummary">Chargement…</small></span>${icon('arrow','av-chevron')}</div><div class="av-lines"><p>${icon('bulb')}<span id="avLights">Lumières : —</span></p><p>${icon('home')}<span id="avCovers">Volets : —</span></p><p>${icon('sun')}<span id="avIndoor">Température : —</span></p></div></button>
|
||||
<a class="av-stat-card" href="/surveillance"><div class="av-card-top"><span class="av-circle system">${icon('server')}</span><span><strong>Système</strong><small>Mini-PC Alfred</small></span>${icon('arrow','av-chevron')}</div><div class="av-lines"><p><span class="av-mini-dot"></span><span id="avLoad">Charge : —</span></p><p><span class="av-mini-dot"></span><span id="avMemory">Mémoire : —</span></p><p><span class="av-mini-dot"></span><span id="avDisk">Stockage : —</span></p></div></a>
|
||||
<a class="av-stat-card" href="/surveillance"><div class="av-card-top"><span class="av-circle ai">${icon('brain')}</span><span><strong>Services IA</strong><small id="avAiSummary">Statuts en cours…</small></span>${icon('arrow','av-chevron')}</div><div class="av-lines"><p><span class="av-mini-dot" id="avOllamaDot"></span><span id="avOllama">Ollama : —</span></p><p><span class="av-mini-dot" id="avHermesDot"></span><span id="avHermes">Hermes : —</span></p><p><span class="av-mini-dot" id="avComfyDot"></span><span id="avComfy">ComfyUI : —</span></p></div></a>
|
||||
<button class="av-stat-card av-weather" type="button" data-av-action="weather"><div class="av-card-top"><span class="av-circle weather">${icon('cloud')}</span><span><strong>Météo</strong><small>Home Assistant</small></span>${icon('arrow','av-chevron')}</div><div class="av-weather-body"><span class="av-weather-icon" id="avWeatherIcon">☁️</span><div><strong id="avWeatherTemp">—</strong><span id="avWeatherCondition">Chargement…</span><small id="avWeatherRange"></small></div></div></button>
|
||||
</section>
|
||||
<section class="av-voice-hero" aria-label="Commande vocale Alfred">
|
||||
<div class="av-voice-controls">
|
||||
<button type="button" id="avMicButton" class="av-sound-control" aria-pressed="false">${icon('mic')}<strong>Micro</strong><small id="avMicLabel">Actif</small></button>
|
||||
<div class="av-voice-center"><div class="av-mic-orb" id="avOrb">${icon('mic')}</div><strong id="avVoiceTitle">En veille</strong><p id="avVoiceSubtitle">Dites « Alfred » puis votre demande</p><span class="av-status-line"><span class="av-status-dot" id="avStatusDot"></span><span id="avStatusText">Prêt à vous écouter</span></span></div>
|
||||
<button type="button" id="avSpeakerButton" class="av-sound-control" aria-pressed="false">${icon('speaker')}<strong>Sortie audio</strong><small id="avSpeakerLabel">Activée</small></button>
|
||||
</div>
|
||||
<div class="av-wave-host" id="avWaveHost"></div>
|
||||
</section>
|
||||
<div class="av-shortcuts" role="group" aria-label="Raccourcis d'Alfred">
|
||||
<button type="button" data-av-action="time">${icon('clock')}<span>Quelle heure est-il ?</span></button>
|
||||
<button type="button" data-av-action="weather">${icon('cloud')}<span>Météo</span></button>
|
||||
<button type="button" data-av-action="house">${icon('home')}<span>État de la maison</span></button>
|
||||
<button type="button" data-av-action="proxmox">${icon('server')}<span>Vérifier Proxmox</span></button>
|
||||
<button type="button" data-av-action="wazuh">${icon('shield')}<span>Alertes Wazuh</span></button>
|
||||
<button type="button" data-av-action="comfyui">${icon('media')}<span>Créer une image</span></button>
|
||||
<button type="button" data-av-action="more">${icon('more')}<span>Plus d'outils</span></button>
|
||||
</div>
|
||||
<footer class="av-history"><span class="av-history-heading">${icon('chat')} <strong>Interactions récentes</strong></span><div id="avHistoryList" aria-live="polite"><span class="av-history-empty">Aucune interaction depuis l’ouverture de cette page</span></div></footer>
|
||||
<button type="button" class="av-back" id="avBack" hidden>${icon('close')} Retour à l’accueil</button>
|
||||
<div class="av-toast" id="avToast" role="status" aria-live="polite" hidden></div>
|
||||
<dialog class="av-image-dialog" id="avComfyDialog"><form method="dialog"><button type="submit" class="av-dialog-close" aria-label="Fermer">${icon('close')}</button></form><h2>Créer une image avec ComfyUI</h2><p>Décris l’image à générer. Alfred transmettra ta demande à Hermes.</p><label for="avComfyPrompt">Description</label><textarea id="avComfyPrompt" rows="4" maxlength="450" placeholder="Une maison futuriste au coucher du soleil…"></textarea><div class="av-dialog-actions"><button id="avComfySubmit" type="button">Envoyer à Alfred</button></div></dialog>
|
||||
<dialog class="av-image-dialog" id="avMoreDialog"><form method="dialog"><button type="submit" class="av-dialog-close" aria-label="Fermer">${icon('close')}</button></form><h2>Outils Alfred</h2><div class="av-more-grid"><a href="/config">${icon('settings')} Configuration</a><a href="/surveillance">${icon('server')} Surveillance</a><button type="button" data-av-action="calendar">${icon('calendar')} Agenda</button><button type="button" data-av-action="camera">${icon('camera')} Caméra</button><button type="button" data-av-action="audio">${icon('speaker')} Réglages audio</button></div></dialog>
|
||||
`;
|
||||
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 */ });
|
||||
})();
|
||||
@@ -0,0 +1,71 @@
|
||||
/* ALFRED_GLASS_V2 — mise en page uniquement ; logique V1 et extensions conservees. */
|
||||
(() => {
|
||||
'use strict';
|
||||
const main = document.getElementById('alfredMainV1');
|
||||
if (!main || main.dataset.glassV2 === 'ready') return;
|
||||
main.dataset.glassV2 = 'ready';
|
||||
const logo = main.querySelector('.av-logo');
|
||||
const name = document.getElementById('avName');
|
||||
const subtitle = document.getElementById('avVoiceSubtitle');
|
||||
if (!logo || !name) return;
|
||||
let assistantName = String(name.textContent || '').trim() || 'Alfred';
|
||||
const cleanName = value => {
|
||||
if(typeof value !== 'string')return '';
|
||||
const s = value.trim();
|
||||
return s.length >= 2 && s.length <= 30 ? s : '';
|
||||
};
|
||||
function setName(value) {
|
||||
const s = cleanName(value);
|
||||
if(!s) return;
|
||||
assistantName=s;
|
||||
name.textContent=s;
|
||||
const first=Array.from(s)[0] || 'A';
|
||||
logo.textContent=first.toLocaleUpperCase('fr-FR');
|
||||
logo.setAttribute('aria-label',`Initiale de ${s}`);
|
||||
// La V1 conserve son nom initial dans la phrase de veille : mise a jour apres chaque evenement.
|
||||
const title=document.getElementById('avVoiceTitle');
|
||||
if(title && subtitle && (title.textContent === 'En veille' || title.textContent === 'En attente')) {
|
||||
subtitle.textContent=`Dites « ${s} » puis votre demande`;
|
||||
}
|
||||
}
|
||||
setName(assistantName);
|
||||
async function fetchName(){
|
||||
try{
|
||||
const r=await fetch('/api/state',{cache:'no-store'});
|
||||
if(!r.ok)return;
|
||||
const d=await r.json();
|
||||
setName(d.assistant_name || d.title || '');
|
||||
} catch(_){ /* La page reste utilisable si /api/state est indisponible. */ }
|
||||
}
|
||||
fetchName();
|
||||
setInterval(()=>{if(!document.hidden)fetchName();},20000);
|
||||
window.addEventListener('focus',fetchName);
|
||||
window.alfredSocket?.on('alfred_state',state=>{
|
||||
setName(state?.assistant_name || state?.title || assistantName);
|
||||
});
|
||||
// Le menu ··· ouvre les options de la V1 au lieu de naviguer directement vers /config.
|
||||
const oldMenu=main.querySelector('.av-head-actions > a[href="/config"]');
|
||||
if(oldMenu){
|
||||
const btn=document.createElement('button');
|
||||
btn.type='button';btn.id='avTopMenu';btn.className=oldMenu.className;
|
||||
btn.setAttribute('data-av-action','more');
|
||||
btn.setAttribute('aria-label','Ouvrir le menu principal');
|
||||
btn.title='Menu : Configuration, Surveillance, Journaux';
|
||||
btn.innerHTML=oldMenu.innerHTML;
|
||||
oldMenu.replaceWith(btn);
|
||||
}
|
||||
const grid=document.querySelector('#avMoreDialog .av-more-grid');
|
||||
if(grid&&!grid.querySelector('a[href="/logs"]')){
|
||||
const link=document.createElement('a');
|
||||
link.href='/logs';link.textContent='Journaux et historiques';
|
||||
link.setAttribute('aria-label','Ouvrir les journaux');
|
||||
const svg=document.createElementNS('http://www.w3.org/2000/svg','svg');
|
||||
svg.setAttribute('viewBox','0 0 24 24');svg.setAttribute('class','a-icon');svg.setAttribute('fill','none');
|
||||
svg.setAttribute('stroke','currentColor');svg.setAttribute('stroke-width','1.8');
|
||||
const d=document.createElementNS('http://www.w3.org/2000/svg','path');
|
||||
d.setAttribute('d','M6 3h9l4 4v14H6a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2zM14 3v5h5M8 12h8M8 16h8');
|
||||
svg.appendChild(d);link.prepend(svg);grid.append(link);
|
||||
}
|
||||
// Rail et historique restes en DOM : les scripts des Listes et de la V1 les utilisent.
|
||||
// Ne pas les supprimer au risque de casser les evenements ou le rafraichissement.
|
||||
})();
|
||||
@@ -0,0 +1,31 @@
|
||||
/* Alfred Glass — aperçu Maison alimenté par les préférences de /config. */
|
||||
(() => {
|
||||
'use strict';
|
||||
let rows=null;
|
||||
function render(){
|
||||
if(!rows)return;
|
||||
const card=document.getElementById('avHomeSummary');
|
||||
if(!card)return;
|
||||
const ids=['avLights','avCovers','avIndoor'];
|
||||
rows.slice(0,3).forEach((r,i)=>{
|
||||
const node=document.getElementById(ids[i]);
|
||||
if(!node)return;
|
||||
node.textContent=`${r.label} : ${r.value}`;
|
||||
node.closest('p').hidden=false;
|
||||
});
|
||||
for(let i=rows.length;i<ids.length;i++){
|
||||
const node=document.getElementById(ids[i]);
|
||||
if(node)node.closest('p').hidden=true;
|
||||
}
|
||||
}
|
||||
window.alfredHomeCardCustomUpdate=render;
|
||||
async function update(){
|
||||
try {
|
||||
const r=await fetch('/api/alfred/home-card',{cache:'no-store'});
|
||||
if(!r.ok)throw new Error('HTTP '+r.status);
|
||||
const data=await r.json();rows=Array.isArray(data.rows)?data.rows:null;
|
||||
render();
|
||||
}catch(e){console.warn('[ALFRED MAISON] Informations personnalisées indisponibles',e);}
|
||||
}
|
||||
update();setInterval(()=>{if(!document.hidden)update();},20000);
|
||||
})();
|
||||
@@ -0,0 +1,93 @@
|
||||
/* ALFRED_IDENTITY_GLASS_V1 — dynamic user-facing assistant branding.
|
||||
Technical keys, service names, tool IDs and logs remain unchanged. */
|
||||
(() => {
|
||||
'use strict';
|
||||
let active='',activeWake='';
|
||||
const replace=(el,oldName,newName)=>{
|
||||
if(!el || !el.firstChild || el.firstChild.nodeType!==3)return;
|
||||
const value=el.firstChild.nodeValue||'';
|
||||
const next=value.replace(/\bAlfred\b/g,newName);
|
||||
if(next!==value)el.firstChild.nodeValue=next;
|
||||
};
|
||||
function apply(n,wake){
|
||||
if(typeof n!=='string' || n.trim().length<2 || n.length>30)return;
|
||||
active=n.trim();
|
||||
if(typeof wake==='string'&&wake.trim())activeWake=wake.trim();
|
||||
const letter=Array.from(active)[0]?.toLocaleUpperCase('fr-FR')||'A';
|
||||
const main=document.getElementById('alfredMainV1');
|
||||
if(main){
|
||||
const logo=main.querySelector('.av-logo');
|
||||
if(logo)logo.textContent=letter;
|
||||
const title=document.getElementById('avName');
|
||||
if(title)title.textContent=active;
|
||||
const info=document.getElementById('avVoiceSubtitle');
|
||||
if(info&&/Dites\s+[«"]/.test(info.textContent||''))info.textContent=`Dites « ${activeWake||active} » puis votre demande`;
|
||||
const system=main.querySelector('.av-circle.system')?.parentElement?.querySelector('small');
|
||||
if(system)system.textContent='Mini-PC '+active;
|
||||
replace(document.querySelector('#avMoreDialog h2'),'',active);
|
||||
const moreTitle=document.querySelector('#avMoreDialog h2');if(moreTitle)moreTitle.textContent='Outils '+active;
|
||||
const comfyDesc=document.querySelector('#avComfyDialog p');if(comfyDesc)comfyDesc.textContent='Décris l’image à générer. '+active+' transmettra ta demande à Hermes.';
|
||||
const submit=document.getElementById('avComfySubmit');if(submit)submit.textContent='Envoyer à '+active;
|
||||
const hero=main.querySelector('.av-voice-hero');if(hero)hero.setAttribute('aria-label','Commande vocale '+active);
|
||||
const oldConfig=main.querySelector('a[href="/config"][aria-label^="Configuration d"]');if(oldConfig)oldConfig.setAttribute('aria-label','Configuration de '+active);
|
||||
const nav=document.querySelector('#avMoreDialog .av-more-grid');
|
||||
if(nav && !nav.querySelector('a[href="/identity"]')){
|
||||
const link=document.createElement('a');link.href='/identity';link.textContent='Identité de '+active;nav.append(link);
|
||||
}else if(nav){const link=nav.querySelector('a[href="/identity"]');if(link)link.textContent='Identité de '+active;}
|
||||
const legacyStage=document.getElementById('stage');if(legacyStage)legacyStage.setAttribute('aria-label','Réponse de '+active);
|
||||
const legacyCamera=document.getElementById('cameraFeed');if(legacyCamera)legacyCamera.alt='Vidéo en direct de la caméra de '+active;
|
||||
document.title=active+' · Accueil';
|
||||
}
|
||||
const brand=document.querySelector('.admin-header .brand');
|
||||
if(brand){
|
||||
const b=brand.querySelector('strong'),o=brand.querySelector('.brand-orb');
|
||||
if(b)b.textContent=active;if(o)o.textContent=letter;
|
||||
const loginOrb=document.querySelector('.auth-box .brand-orb');if(loginOrb)loginOrb.textContent=letter;
|
||||
const authIntro=document.querySelector('.auth-box p');if(authIntro)authIntro.textContent=document.body.dataset.page==='surveillance'?('Connecte-toi pour consulter l’état de '+active+'.'):('Connecte-toi pour modifier la configuration de '+active+'.');
|
||||
const help=document.querySelector('.ag-help');if(help)help.textContent=help.textContent.replace(/redémarrer Alfred/g,'redémarrer '+active);
|
||||
const restartInfo=document.querySelector('.restart-panel p');if(restartInfo)restartInfo.textContent='Redémarrer '+active+' pour appliquer les paramètres enregistrés. Action protégée par mot de passe administrateur.';
|
||||
const configLabel=document.getElementById('ALFRED_NAME');if(configLabel)configLabel.placeholder=active;
|
||||
const hint=document.getElementById('pendingNotice');
|
||||
if(hint){const strong=hint.querySelector('strong');if(strong)strong.textContent='Redémarre '+active;}
|
||||
const restartButton=document.getElementById('restartButton');if(restartButton)restartButton.textContent='Redémarrer '+active;
|
||||
document.title=active+' · '+(document.body.dataset.page==='surveillance'?'Surveillance':'Configuration');
|
||||
const nav=brand.closest('.admin-header')?.querySelector('nav');
|
||||
if(nav && !nav.querySelector('a[href="/identity"]')){
|
||||
const a=document.createElement('a');a.href='/identity';a.textContent='Identité';
|
||||
const logout=nav.querySelector('button');if(logout)nav.insertBefore(a,logout);else nav.append(a);
|
||||
}
|
||||
const rail=document.querySelector('.ag-rail');
|
||||
if(rail && !rail.querySelector('a[href="/identity"]')){
|
||||
const a=document.createElement('a');a.href='/identity';a.innerHTML='<span>✎</span>Identité';rail.append(a);
|
||||
}
|
||||
}
|
||||
const legacyAudio=document.querySelector('body > main .subtitle');
|
||||
if(legacyAudio){legacyAudio.textContent=active+' · Paramètres audio';document.title=active+' — Configuration audio';
|
||||
const hints=document.querySelectorAll('body > main p.hint');if(hints[0])hints[0].textContent='Désactive les réponses vocales de '+active+'.';if(hints[1])hints[1].textContent='Suspend l’écoute et la détection du mot « '+(activeWake||active)+' ».';}
|
||||
const logsTitle=document.getElementById('lgName');
|
||||
if(logsTitle){
|
||||
logsTitle.textContent=active;
|
||||
const letterEl=document.getElementById('lgLetter');if(letterEl)letterEl.textContent=letter;
|
||||
const eyebrow=document.querySelector('.lg-eyebrow');if(eyebrow)eyebrow.textContent='OBSERVABILITÉ · '+active.toLocaleUpperCase('fr-FR');
|
||||
const nav=document.querySelector('.lg-nav');
|
||||
if(nav&&!nav.querySelector('a[href="/identity"]')){const a=document.createElement('a');a.href='/identity';a.textContent='Identité';nav.append(a);}
|
||||
document.title='Journaux · '+active;
|
||||
}
|
||||
}
|
||||
async function refresh(){
|
||||
try{const r=await fetch('/api/alfred/identity/name',{cache:'no-store'});if(!r.ok)return;const d=await r.json();apply(d.name,d.wake_word);}catch(_){}
|
||||
}
|
||||
const subtitle=document.getElementById('avVoiceSubtitle');
|
||||
if(subtitle){
|
||||
const observer=new MutationObserver(()=>{
|
||||
if(active&&/Dites\s+[«"]/.test(subtitle.textContent||'')){
|
||||
const desired=`Dites « ${activeWake||active} » puis votre demande`;
|
||||
if(subtitle.textContent!==desired)subtitle.textContent=desired;
|
||||
}
|
||||
});
|
||||
observer.observe(subtitle,{childList:true,characterData:true,subtree:true});
|
||||
}
|
||||
refresh();window.addEventListener('focus',refresh);
|
||||
document.addEventListener('visibilitychange',()=>{if(!document.hidden)refresh();});
|
||||
setInterval(()=>{if(!document.hidden)refresh();},10000);
|
||||
})();
|
||||
Reference in New Issue
Block a user