Files
Alfredv2/alfred/app/static/js/alfred-admin-glass.js
T

138 lines
7.9 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.
/* 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();
})();