v1.5.0 - admin: global settings panel, per-client control/sleep override, settings API ref
This commit is contained in:
+113
-7
@@ -25,6 +25,7 @@
|
||||
.badge{font-size:.65rem;padding:2px 8px;border-radius:10px;text-transform:uppercase;font-weight:600;letter-spacing:.3px}
|
||||
.badge.online{background:rgba(74,222,128,.15);color:#86efac} .badge.offline{background:rgba(255,255,255,.06);color:#777}
|
||||
.badge.playing{background:rgba(96,165,250,.15);color:#93c5fd} .badge.sleeping{background:rgba(251,191,36,.15);color:#fcd34d}
|
||||
.badge.auto{background:rgba(168,85,247,.15);color:#d8b4fe} .badge.manual{background:rgba(148,163,184,.15);color:#cbd5e1}
|
||||
.name-input{background:transparent;border:1px solid rgba(255,255,255,.12);border-radius:6px;color:#fff;font-size:.85rem;padding:3px 7px;width:130px}
|
||||
.name-input:focus{outline:none;border-color:#6366f1}
|
||||
.info-row{font-size:.72rem;color:#666;margin-bottom:.75rem;display:flex;gap:1rem;flex-wrap:wrap}
|
||||
@@ -43,6 +44,8 @@
|
||||
select:focus{outline:none;border-color:#6366f1}
|
||||
option{background:#1a1a2e;color:#e0e0e0}
|
||||
input[type=range]{width:110px;accent-color:#6366f1}
|
||||
input[type=time]{padding:4px 8px;border:1px solid rgba(255,255,255,.12);border-radius:7px;background:rgba(255,255,255,.05);color:#ddd;font-size:.78rem}
|
||||
input[type=time]:focus{outline:none;border-color:#6366f1}
|
||||
.rval{font-size:.78rem;color:#999;min-width:28px}
|
||||
.tgl{position:relative;width:36px;height:20px;cursor:pointer}.tgl input{display:none}
|
||||
.tgl-s{position:absolute;inset:0;background:rgba(255,255,255,.08);border-radius:10px;transition:.2s}
|
||||
@@ -60,7 +63,7 @@
|
||||
.api-card{background:rgba(255,255,255,.02);border:1px solid rgba(255,255,255,.05);border-radius:10px;padding:1rem;margin-bottom:.6rem}
|
||||
.api-card h3{font-size:.85rem;font-weight:500;color:#a5b4fc;margin-bottom:.4rem}
|
||||
.mtd{display:inline-block;font-size:.65rem;font-weight:700;padding:1px 5px;border-radius:3px;margin-right:5px}
|
||||
.mtd.get{background:rgba(34,197,94,.18);color:#86efac}.mtd.post{background:rgba(59,130,246,.18);color:#93c5fd}.mtd.del{background:rgba(239,68,68,.18);color:#fca5a5}
|
||||
.mtd.get{background:rgba(34,197,94,.18);color:#86efac}.mtd.post{background:rgba(59,130,246,.18);color:#93c5fd}.mtd.del{background:rgba(239,68,68,.18);color:#fca5a5}.mtd.put{background:rgba(234,179,8,.18);color:#fde68a}
|
||||
.ep{font-family:monospace;font-size:.82rem;color:#bbb}
|
||||
.api-card p{font-size:.75rem;color:#777;margin:.3rem 0}
|
||||
pre{background:rgba(0,0,0,.25);border:1px solid rgba(255,255,255,.06);border-radius:7px;padding:8px 10px;font-size:.72rem;color:#bbb;overflow-x:auto;margin-top:.4rem;white-space:pre-wrap;word-break:break-all;position:relative}
|
||||
@@ -78,6 +81,18 @@
|
||||
.input-row input{padding:6px 10px;background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.12);border-radius:6px;color:#ddd;font-size:.82rem;width:100%}
|
||||
.input-row input:focus{outline:none;border-color:#6366f1}
|
||||
.input-row .field{flex:1;min-width:140px}
|
||||
/* Global settings panel */
|
||||
.settings-panel{background:rgba(99,102,241,.05);border:1px solid rgba(99,102,241,.2);border-radius:12px;padding:1.25rem;margin-bottom:1rem}
|
||||
.settings-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:1.25rem}
|
||||
.settings-block h3{font-size:.82rem;font-weight:600;color:#a5b4fc;text-transform:uppercase;letter-spacing:.4px;margin-bottom:.6rem}
|
||||
.srow{display:flex;align-items:center;gap:.6rem;margin-bottom:.55rem;flex-wrap:wrap}
|
||||
.srow .clbl{min-width:80px}
|
||||
.save-bar{display:flex;align-items:center;gap:1rem;margin-top:1rem;padding-top:1rem;border-top:1px solid rgba(255,255,255,.06)}
|
||||
.save-msg{font-size:.78rem;color:#86efac;opacity:0;transition:opacity .3s}.save-msg.show{opacity:1}
|
||||
.btn.save{background:rgba(99,102,241,.25);border-color:rgba(99,102,241,.5);color:#c7d2fe;font-weight:500;padding:7px 18px}
|
||||
.btn.save:hover{background:rgba(99,102,241,.4)}
|
||||
.sleep-fields{display:flex;align-items:center;gap:.5rem;flex-wrap:wrap}
|
||||
.hint{font-size:.68rem;color:#666;margin-top:.3rem;line-height:1.4}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -89,13 +104,54 @@
|
||||
<button class="btn logout" id="logout-btn" style="display:none" onclick="doLogout()">Logout</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid" id="grid"><div class="empty"><h2>No frames seen yet</h2><p>Open Frambe on a tablet or screen to see it here</p></div></div>
|
||||
|
||||
<!-- Global Settings -->
|
||||
<div class="sec-head" onclick="toggle('gs-sec','gs-arr')" style="margin-top:0;padding-top:0;border-top:none">Global Settings <span class="arr open" id="gs-arr">▶</span></div>
|
||||
<div id="gs-sec">
|
||||
<div class="settings-panel">
|
||||
<div class="settings-grid">
|
||||
<div class="settings-block">
|
||||
<h3>Default Photo Source</h3>
|
||||
<div class="srow"><span class="clbl">Source</span>
|
||||
<select id="gs-source"><option value="random">Random Photos</option><option value="favorites">Favorites</option></select>
|
||||
</div>
|
||||
<div class="hint">New frames (and any frame set to follow the server) start with this source automatically.</div>
|
||||
</div>
|
||||
<div class="settings-block">
|
||||
<h3>Display Defaults</h3>
|
||||
<div class="srow"><span class="clbl">Interval</span><input type="range" id="gs-interval" min="5" max="600" value="30" oninput="document.getElementById('gs-interval-v').textContent=this.value+'s'"><span class="rval" id="gs-interval-v">30s</span></div>
|
||||
<div class="srow"><span class="clbl">Clock</span><label class="tgl"><input type="checkbox" id="gs-clock" checked><span class="tgl-s"></span></label>
|
||||
<span class="clbl">Date</span><label class="tgl"><input type="checkbox" id="gs-date" checked><span class="tgl-s"></span></label>
|
||||
<span class="clbl">EXIF</span><label class="tgl"><input type="checkbox" id="gs-exif"><span class="tgl-s"></span></label>
|
||||
<span class="clbl">Bar</span><label class="tgl"><input type="checkbox" id="gs-progress" checked><span class="tgl-s"></span></label></div>
|
||||
</div>
|
||||
<div class="settings-block">
|
||||
<h3>Sleep Schedule</h3>
|
||||
<div class="srow"><span class="clbl">Enabled</span><label class="tgl"><input type="checkbox" id="gs-sleep-on"><span class="tgl-s"></span></label></div>
|
||||
<div class="srow sleep-fields"><span class="clbl">Sleep at</span><input type="time" id="gs-sleep-at" value="23:00"><span class="clbl">Wake at</span><input type="time" id="gs-wake-at" value="06:00"></div>
|
||||
<div class="hint">Frames sleep (black screen) during this window. Crosses midnight automatically. Uses the server's local time.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="save-bar">
|
||||
<button class="btn save" onclick="saveSettings()">Save Global Settings</button>
|
||||
<span class="save-msg" id="gs-saved">Saved & applied to all server-controlled frames</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sec-head" onclick="toggle('frames-sec','frames-arr')">Frames <span class="arr open" id="frames-arr">▶</span></div>
|
||||
<div id="frames-sec">
|
||||
<div class="grid" id="grid"><div class="empty"><h2>No frames seen yet</h2><p>Open Frambe on a tablet or screen to see it here</p></div></div>
|
||||
</div>
|
||||
|
||||
<div class="sec-head" onclick="toggle('api-sec','api-arr')">REST API Reference <span class="arr" id="api-arr">▶</span></div>
|
||||
<div id="api-sec" style="display:none">
|
||||
<div class="api-card"><h3><span class="mtd get">GET</span><span class="ep">/api/clients</span></h3><p>List all known frames with status, IP, name, and config.</p><pre id="c-list">curl -s -H "Authorization: Bearer YOUR_TOKEN" http://YOUR_HOST:3030/api/clients<span class="cpb" onclick="cc('c-list')">Copy</span></pre></div>
|
||||
<div class="api-card"><h3><span class="mtd post">POST</span><span class="ep">/api/clients/:id/command</span></h3><p>Send a command to a frame. Actions: <code>start</code> <code>stop</code> <code>next</code> <code>prev</code> <code>sleep</code> <code>wake</code> <code>refresh</code> <code>setSource</code> <code>setConfig</code></p><pre id="c-cmd">curl -s -X POST -H "Authorization: Bearer YOUR_TOKEN" -H "Content-Type: application/json" -d '{"action":"next"}' http://YOUR_HOST:3030/api/clients/CLIENT_ID/command<span class="cpb" onclick="cc('c-cmd')">Copy</span></pre></div>
|
||||
<div class="api-card"><h3><span class="mtd del">DELETE</span><span class="ep">/api/clients/:id</span></h3><p>Remove a frame from the registry.</p><pre id="c-del">curl -s -X DELETE -H "Authorization: Bearer YOUR_TOKEN" http://YOUR_HOST:3030/api/clients/CLIENT_ID<span class="cpb" onclick="cc('c-del')">Copy</span></pre></div>
|
||||
<div class="api-card"><h3><span class="mtd get">GET</span><span class="ep">/api/settings</span> · <span class="mtd put">PUT</span><span class="ep">/api/settings</span></h3><p>Read or update global settings (default source, interval, display toggles, sleep schedule). PUT requires the API token.</p><pre id="c-set">curl -s -X PUT -H "Authorization: Bearer YOUR_TOKEN" -H "Content-Type: application/json" -d '{"sleep":{"enabled":true,"sleepAt":"23:00","wakeAt":"06:00"}}' http://YOUR_HOST:3030/api/settings<span class="cpb" onclick="cc('c-set')">Copy</span></pre></div>
|
||||
</div>
|
||||
|
||||
<div class="modal-overlay" id="ha-modal">
|
||||
<div class="modal">
|
||||
<button class="modal-close" onclick="closeModal()">×</button>
|
||||
@@ -112,7 +168,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
var ws=null,D={},albums=[],people=[],auth=false;
|
||||
var ws=null,D={},albums=[],people=[],auth=false,settings=null,settingsDirty=false;
|
||||
(async function(){try{var r=await(await fetch('/api/auth/status')).json();auth=r.authEnabled;if(auth)document.getElementById('logout-btn').style.display='';}catch(e){}})();
|
||||
async function doLogout(){try{await fetch('/api/auth/logout',{method:'POST'});}catch(e){}location.href='/admin/login';}
|
||||
function toggle(sid,aid){var s=document.getElementById(sid),a=document.getElementById(aid);if(s.style.display==='none'){s.style.display='block';a.classList.add('open');}else{s.style.display='none';a.classList.remove('open');}}
|
||||
@@ -123,13 +179,54 @@ function connect(){
|
||||
var p=location.protocol==='https:'?'wss:':'ws:';
|
||||
ws=new WebSocket(p+'//'+location.host+'/ws');
|
||||
ws.onopen=function(){document.getElementById('ws-pill').textContent='Connected';document.getElementById('ws-pill').className='ws-pill on';ws.send(JSON.stringify({type:'register',role:'admin'}));loadMeta();};
|
||||
ws.onmessage=function(e){var m=JSON.parse(e.data);if(m.type==='clientList'){D={};m.clients.forEach(function(c){D[c.id]=c;});render();}else if(m.type==='clientUpdate'){D[m.clientId]=m.client;render();}};
|
||||
ws.onmessage=function(e){var m=JSON.parse(e.data);if(m.type==='clientList'){D={};m.clients.forEach(function(c){D[c.id]=c;});if(m.settings)applySettingsToForm(m.settings);render();}else if(m.type==='settings'||m.type==='settingsSaved'){applySettingsToForm(m.settings);if(m.type==='settingsSaved')flashSaved();}else if(m.type==='clientUpdate'){D[m.clientId]=m.client;render();}};
|
||||
ws.onclose=function(){document.getElementById('ws-pill').textContent='Disconnected';document.getElementById('ws-pill').className='ws-pill off';setTimeout(connect,3000);};
|
||||
}
|
||||
async function loadMeta(){try{var c=await(await fetch('/api/config')).json();document.getElementById('ver').textContent='v'+(c.version||'?');albums=await(await fetch('/api/albums')).json();people=await(await fetch('/api/people')).json();}catch(e){}}
|
||||
async function loadMeta(){try{var c=await(await fetch('/api/config')).json();document.getElementById('ver').textContent='v'+(c.version||'?');albums=await(await fetch('/api/albums')).json();people=await(await fetch('/api/people')).json();populateGsSource();if(settings)applySettingsToForm(settings);}catch(e){}}
|
||||
|
||||
// === GLOBAL SETTINGS FORM ===
|
||||
function populateGsSource(){
|
||||
var sel=document.getElementById('gs-source'),cur=sel.value;
|
||||
var h='<option value="random">Random Photos</option><option value="favorites">Favorites</option>';
|
||||
albums.forEach(function(a){h+='<option value="album:'+a.id+'">'+esc(a.albumName)+' ('+a.assetCount+')</option>';});
|
||||
people.filter(function(p){return p.name;}).forEach(function(p){h+='<option value="person:'+p.id+'">'+esc(p.name)+'</option>';});
|
||||
sel.innerHTML=h;sel.value=cur;
|
||||
}
|
||||
function sourceToValue(s){if(!s)return'random';if(s.source==='album'&&s.albumId)return'album:'+s.albumId;if(s.source==='person'&&s.personId)return'person:'+s.personId;return s.source||'random';}
|
||||
function valueToSource(v){if(v==='random')return{source:'random',albumId:null,personId:null};if(v==='favorites')return{source:'favorites',albumId:null,personId:null};if(v.indexOf('album:')===0)return{source:'album',albumId:v.substring(6),personId:null};if(v.indexOf('person:')===0)return{source:'person',personId:v.substring(7),albumId:null};return{source:'random',albumId:null,personId:null};}
|
||||
function applySettingsToForm(s){settings=s;if(!s)return;
|
||||
var srcVal=sourceToValue(s.source);var sel=document.getElementById('gs-source');
|
||||
if(!Array.prototype.some.call(sel.options,function(o){return o.value===srcVal;}))populateGsSource();
|
||||
sel.value=srcVal;
|
||||
document.getElementById('gs-interval').value=s.slideshowInterval||30;document.getElementById('gs-interval-v').textContent=(s.slideshowInterval||30)+'s';
|
||||
document.getElementById('gs-clock').checked=s.showClock!==false;
|
||||
document.getElementById('gs-date').checked=s.showDate!==false;
|
||||
document.getElementById('gs-exif').checked=s.showExif!==false;
|
||||
document.getElementById('gs-progress').checked=s.showProgress!==false;
|
||||
document.getElementById('gs-sleep-on').checked=!!(s.sleep&&s.sleep.enabled);
|
||||
document.getElementById('gs-sleep-at').value=(s.sleep&&s.sleep.sleepAt)||'23:00';
|
||||
document.getElementById('gs-wake-at').value=(s.sleep&&s.sleep.wakeAt)||'06:00';
|
||||
}
|
||||
function saveSettings(){
|
||||
var patch={source:valueToSource(document.getElementById('gs-source').value),
|
||||
slideshowInterval:parseInt(document.getElementById('gs-interval').value,10),
|
||||
showClock:document.getElementById('gs-clock').checked,
|
||||
showDate:document.getElementById('gs-date').checked,
|
||||
showExif:document.getElementById('gs-exif').checked,
|
||||
showProgress:document.getElementById('gs-progress').checked,
|
||||
sleep:{enabled:document.getElementById('gs-sleep-on').checked,
|
||||
sleepAt:document.getElementById('gs-sleep-at').value,
|
||||
wakeAt:document.getElementById('gs-wake-at').value}};
|
||||
if(ws&&ws.readyState===1)ws.send(JSON.stringify({type:'updateSettings',settings:patch}));
|
||||
}
|
||||
function flashSaved(){var m=document.getElementById('gs-saved');m.classList.add('show');setTimeout(function(){m.classList.remove('show');},2500);}
|
||||
|
||||
function cmd(id,a,pl){if(ws&&ws.readyState===1)ws.send(JSON.stringify({type:'adminCommand',targetId:id,action:a,payload:pl||{}}));}
|
||||
function ren(id,n){if(ws&&ws.readyState===1)ws.send(JSON.stringify({type:'renameClient',targetId:id,name:n}));}
|
||||
function rem(id,n){if(confirm('Remove "'+(n||id)+'" from client list?'))if(ws&&ws.readyState===1)ws.send(JSON.stringify({type:'removeClient',targetId:id}));}
|
||||
function setControl(id,v){if(ws&&ws.readyState===1)ws.send(JSON.stringify({type:'setClientControl',targetId:id,serverControlled:v}));}
|
||||
function setSleep(id){var on=document.getElementById('cs-on-'+id).checked,sa=document.getElementById('cs-at-'+id).value,wa=document.getElementById('cs-wake-'+id).value;if(ws&&ws.readyState===1)ws.send(JSON.stringify({type:'setClientSleep',targetId:id,sleep:{override:true,enabled:on,sleepAt:sa,wakeAt:wa}}));}
|
||||
function clearSleepOverride(id){if(ws&&ws.readyState===1)ws.send(JSON.stringify({type:'setClientSleep',targetId:id,sleep:{override:false}}));}
|
||||
function src(id,v){if(!v)return;if(v==='random')cmd(id,'setSource',{source:'random'});else if(v==='favorites')cmd(id,'setSource',{source:'favorites'});else if(v.indexOf('album:')===0)cmd(id,'setSource',{source:'album',albumId:v.substring(6)});else if(v.indexOf('person:')===0)cmd(id,'setSource',{source:'person',personId:v.substring(7)});}
|
||||
var haClientId='';
|
||||
function openHaModal(id,name){haClientId=id;document.getElementById('ha-client-name').textContent='Generating for: '+(name||id);document.getElementById('ha-cid').value=id;document.getElementById('ha-host').value=location.host;document.getElementById('ha-modal').classList.add('open');genYaml();}
|
||||
@@ -153,9 +250,10 @@ function render(){
|
||||
var h='';
|
||||
ids.forEach(function(id){var c=D[id],off=c.status==='offline';
|
||||
var sc=off?'offline':c.status==='playing'?'playing':c.status==='sleeping'?'sleeping':'online';var cfg=c.config||{};
|
||||
var auto=c.serverControlled!==false;var csleep=cfg.sleep||{};
|
||||
h+='<div class="card'+(off?' offline':'')+'">';
|
||||
h+='<div class="card-head"><div><div class="card-name"><span class="dot '+sc+'"></span><input class="name-input" value="'+esc(c.name||'')+'" placeholder="'+esc(c.ip)+'" onchange="ren(\''+id+'\',this.value)"/></div><div class="card-ip">'+esc(c.ip)+' · ID: <strong>'+esc(id)+'</strong></div></div>';
|
||||
h+='<div class="card-meta"><span class="badge '+sc+'">'+esc(c.status||'unknown')+'</span>';
|
||||
h+='<div class="card-meta"><span class="badge '+sc+'">'+esc(c.status||'unknown')+'</span><span class="badge '+(auto?'auto':'manual')+'">'+(auto?'Server':'Manual')+'</span>';
|
||||
if(off)h+='<button class="btn sm red" onclick="rem(\''+id+'\',\''+esc(c.name||c.ip)+'\')">Remove</button>';
|
||||
h+='</div></div>';
|
||||
h+='<div class="info-row">';
|
||||
@@ -167,6 +265,8 @@ function render(){
|
||||
h+='<div class="crow" style="justify-content:center"><button class="btn blue sm" onclick="openHaModal(\''+id+'\',\''+esc(c.name||c.ip)+'\')">Generate HA YAML</button></div>';
|
||||
} else {
|
||||
h+='<div class="controls">';
|
||||
h+='<div class="crow"><span class="clbl">Control</span><label class="tgl"><input type="checkbox" '+(auto?'checked':'')+' onchange="setControl(\''+id+'\',this.checked)"><span class="tgl-s"></span></label><span class="clbl" style="min-width:auto">'+(auto?'Following server defaults':'Manual override')+'</span></div>';
|
||||
h+='<hr class="divider">';
|
||||
h+='<div class="crow"><span class="clbl">Source</span><select onchange="src(\''+id+'\',this.value)"><option value="">-- Select --</option><option value="random">Random Photos</option><option value="favorites">Favorites</option>';
|
||||
albums.forEach(function(a){h+='<option value="album:'+a.id+'">'+esc(a.albumName)+' ('+a.assetCount+')</option>';});
|
||||
people.filter(function(p){return p.name;}).forEach(function(p){h+='<option value="person:'+p.id+'">'+esc(p.name)+'</option>';});
|
||||
@@ -174,11 +274,17 @@ function render(){
|
||||
h+='<div class="crow"><span class="clbl">Playback</span><button class="btn grn" onclick="cmd(\''+id+'\',\'start\')">Start</button><button class="btn" onclick="cmd(\''+id+'\',\'stop\')">Stop</button><button class="btn" onclick="cmd(\''+id+'\',\'next\')">Next</button><button class="btn" onclick="cmd(\''+id+'\',\'prev\')">Prev</button></div>';
|
||||
h+='<div class="crow"><span class="clbl">Power</span><button class="btn red" onclick="cmd(\''+id+'\',\'sleep\')">Sleep</button><button class="btn grn" onclick="cmd(\''+id+'\',\'wake\')">Wake</button><button class="btn" onclick="cmd(\''+id+'\',\'refresh\')">Refresh</button></div>';
|
||||
h+='<hr class="divider">';
|
||||
h+='<div class="crow"><span class="clbl">Interval</span><input type="range" min="5" max="120" value="'+(cfg.slideshowInterval||30)+'" oninput="this.nextElementSibling.textContent=this.value+\'s\'" onchange="cmd(\''+id+'\',\'setConfig\',{slideshowInterval:parseInt(this.value)})"><span class="rval">'+(cfg.slideshowInterval||30)+'s</span></div>';
|
||||
h+='<div class="crow"><span class="clbl">Interval</span><input type="range" min="5" max="600" value="'+(cfg.slideshowInterval||30)+'" oninput="this.nextElementSibling.textContent=this.value+\'s\'" onchange="cmd(\''+id+'\',\'setConfig\',{slideshowInterval:parseInt(this.value)})"><span class="rval">'+(cfg.slideshowInterval||30)+'s</span></div>';
|
||||
h+='<div class="crow"><span class="clbl">Clock</span><label class="tgl"><input type="checkbox" '+(cfg.showClock!==false?'checked':'')+' onchange="cmd(\''+id+'\',\'setConfig\',{showClock:this.checked})"><span class="tgl-s"></span></label>';
|
||||
h+='<span class="clbl">Date</span><label class="tgl"><input type="checkbox" '+(cfg.showDate!==false?'checked':'')+' onchange="cmd(\''+id+'\',\'setConfig\',{showDate:this.checked})"><span class="tgl-s"></span></label>';
|
||||
h+='<span class="clbl">EXIF</span><label class="tgl"><input type="checkbox" '+(cfg.showExif!==false?'checked':'')+' onchange="cmd(\''+id+'\',\'setConfig\',{showExif:this.checked})"><span class="tgl-s"></span></label></div>';
|
||||
h+='<hr class="divider">';
|
||||
h+='<div class="crow"><span class="clbl">Sleep</span><label class="tgl"><input type="checkbox" id="cs-on-'+id+'" '+(csleep.override&&csleep.enabled?'checked':'')+' onchange="setSleep(\''+id+'\')"><span class="tgl-s"></span></label>';
|
||||
h+='<input type="time" id="cs-at-'+id+'" value="'+(csleep.sleepAt||'23:00')+'" onchange="setSleep(\''+id+'\')"><span class="clbl" style="min-width:auto">to</span><input type="time" id="cs-wake-'+id+'" value="'+(csleep.wakeAt||'06:00')+'" onchange="setSleep(\''+id+'\')"></div>';
|
||||
h+='<div class="crow"><span class="clbl"></span><span style="font-size:.68rem;color:#666">'+(csleep.override?'Per-frame override active.':'Following global schedule.')+'</span>';
|
||||
if(csleep.override)h+='<button class="btn sm" onclick="clearSleepOverride(\''+id+'\')">Use global</button>';
|
||||
h+='</div>';
|
||||
h+='<hr class="divider">';
|
||||
h+='<div class="crow"><button class="btn blue" onclick="openHaModal(\''+id+'\',\''+esc(c.name||c.ip)+'\')">Generate HA YAML</button></div>';
|
||||
h+='</div>';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user