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