v2 - new interface, many new page for fine tuning

This commit is contained in:
pporcheret
2026-10-28 21:45:40 +01:00
parent a6169bcfdb
commit a17c3cb614
89 changed files with 8691 additions and 13 deletions
+137
View File
@@ -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();
})();