335 lines
31 KiB
PHP
335 lines
31 KiB
PHP
<?php include 'header.php'; ?>
|
|
<div class="main-panel"><div class="content-wrapper">
|
|
<style>
|
|
|
|
/* Header */
|
|
.mhdr{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:12px;margin-bottom:20px}
|
|
.mhdr h2{font-size:1.2rem;font-weight:800;color:#1e293b;margin:0;display:flex;align-items:center;gap:8px}
|
|
.mhdr h2 i{color:#3b82f6}
|
|
.mhdr-r{display:flex;align-items:center;gap:8px}
|
|
.mclk{font-family:monospace;font-size:.9rem;font-weight:700;color:#3b82f6;background:#eff6ff;padding:5px 12px;border-radius:6px;border:1px solid #dbeafe}
|
|
.mcon{padding:4px 12px;border-radius:16px;font-size:.7rem;font-weight:600;display:inline-flex;align-items:center;gap:5px}
|
|
.mcon.on{background:#ecfdf5;color:#059669;border:1px solid #d1fae5}
|
|
.mcon.off{background:#f1f5f9;color:#64748b;border:1px solid #e2e8f0}
|
|
.mcon-dot{width:7px;height:7px;border-radius:50%}
|
|
.mcon.on .mcon-dot{background:#10b981}.mcon.off .mcon-dot{background:#cbd5e1}
|
|
.mbtn{width:32px;height:32px;border-radius:6px;border:1px solid #e2e8f0;background:#fff;color:#475569;cursor:pointer;display:flex;align-items:center;justify-content:center}
|
|
.mbtn:hover{border-color:#3b82f6;color:#3b82f6}
|
|
.mbtn.spin i{animation:sp .6s linear infinite}
|
|
@keyframes sp{to{transform:rotate(360deg)}}
|
|
/* Summary */
|
|
.sbar{display:flex;gap:8px;margin-bottom:20px;flex-wrap:wrap}
|
|
.sbar-i{flex:1;min-width:100px;background:#fff;border:1px solid #e5e9f0;border-radius:10px;padding:12px 14px;display:flex;align-items:center;gap:8px}
|
|
.sbar-d{width:8px;height:8px;border-radius:50%;flex-shrink:0}
|
|
.sbar-d.on{background:#10b981}.sbar-d.wr{background:#f59e0b}.sbar-d.er{background:#ef4444}.sbar-d.of{background:#cbd5e1}
|
|
.sbar-n{font-size:1.3rem;font-weight:800;color:#1e293b;line-height:1}
|
|
.sbar-l{font-size:.6rem;color:#475569;text-transform:uppercase;letter-spacing:.5px}
|
|
/* Section */
|
|
.sec{font-weight:700;font-size:.72rem;letter-spacing:1px;text-transform:uppercase;margin:0 0 10px 2px;display:flex;align-items:center;gap:5px;color:#1e293b}
|
|
.sec i{font-size:.9rem}
|
|
/* Grid */
|
|
.g{display:grid;grid-template-columns:repeat(12,1fr);gap:12px;margin-bottom:20px}
|
|
.g12{grid-column:span 12}.g6{grid-column:span 6}.g4{grid-column:span 4}.g3{grid-column:span 3}
|
|
@media(max-width:1200px){.g4,.g6{grid-column:span 6}.g3{grid-column:span 6}}
|
|
@media(max-width:768px){.g4,.g6{grid-column:span 12}.g3{grid-column:span 6}}
|
|
@media(max-width:480px){.g3{grid-column:span 12}}
|
|
/* Card */
|
|
.cd{background:#fff;border:1px solid #e5e9f0;border-radius:10px;overflow:hidden;display:flex;flex-direction:column;height:100%}
|
|
.cd:hover{box-shadow:0 2px 12px rgba(0,0,0,.04)}
|
|
.cd-top{height:3px;width:100%}
|
|
.cd-top.on{background:#10b981}.cd-top.off{background:#cbd5e1}.cd-top.wr{background:#f59e0b}.cd-top.er{background:#ef4444}
|
|
.cd-b{padding:14px 16px;flex:1}
|
|
.cd-f{padding:5px 16px;border-top:1px solid #f1f5f9;font-size:.65rem;color:#475569;display:flex;align-items:center;gap:4px;background:#fafbfc}
|
|
/* Card header */
|
|
.cd-h{display:flex;align-items:center;gap:8px;margin-bottom:10px}
|
|
.cd-ic{width:32px;height:32px;border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:1rem;flex-shrink:0}
|
|
.cd-nm{font-weight:700;font-size:.82rem;color:#1e293b}
|
|
.cd-tp{font-size:.58rem;color:#475569;text-transform:uppercase;letter-spacing:.5px}
|
|
.bd{font-size:.58rem;font-weight:700;padding:2px 8px;border-radius:10px;text-transform:uppercase;margin-left:auto;white-space:nowrap}
|
|
.bd.on{background:#ecfdf5;color:#059669}.bd.off{background:#f1f5f9;color:#64748b}
|
|
.bd.wr{background:#fffbeb;color:#d97706}.bd.er{background:#fef2f2;color:#dc2626}
|
|
/* Metric row */
|
|
.m{display:flex;align-items:center;justify-content:space-between;padding:5px 10px;border-radius:6px;background:#f8fafc;margin-bottom:3px;font-size:.78rem;gap:6px}
|
|
.m-l{color:#1e293b;display:flex;align-items:center;gap:4px;white-space:nowrap;flex-shrink:0;font-weight:500}
|
|
.m-l i{font-size:.82rem;color:#64748b}
|
|
.m-v{font-weight:700;font-family:monospace;font-size:.78rem;color:#1e293b;text-align:right;white-space:nowrap}
|
|
.m-v.g{color:#059669}.m-v.r{color:#dc2626}.m-v.b{color:#2563eb}.m-v.o{color:#d97706}.m-v.c{color:#0891b2}.m-v.p{color:#7c3aed}
|
|
/* Big metrics */
|
|
.bm{display:flex;gap:0;margin:8px 0;border-radius:8px;overflow:hidden;border:1px solid #e5e9f0}
|
|
.bm-i{flex:1;text-align:center;padding:10px 4px;background:#fafbfc;border-right:1px solid #e5e9f0}
|
|
.bm-i:last-child{border-right:none}
|
|
.bm-v{font-size:1rem;font-weight:800;font-family:monospace;line-height:1;margin-bottom:2px;color:#1e293b}
|
|
.bm-l{font-size:.5rem;color:#475569;text-transform:uppercase;letter-spacing:.5px}
|
|
/* Chart */
|
|
.ch{background:#fafbfc;border:1px solid #e5e9f0;border-radius:8px;padding:8px 10px;margin-top:8px}
|
|
.ch-t{font-size:.6rem;color:#475569;margin-bottom:4px;display:flex;align-items:center;gap:4px;text-transform:uppercase;letter-spacing:.5px;font-weight:600}
|
|
.ch-w{position:relative;height:160px;width:100%}
|
|
.ch-w canvas{position:absolute;top:0;left:0;width:100%!important;height:100%!important}
|
|
/* Battery */
|
|
.bat-w{background:#f1f5f9;border-radius:5px;height:10px;overflow:hidden;margin:4px 0;border:1px solid #e5e9f0}
|
|
.bat-b{height:100%;border-radius:5px;transition:width .4s}
|
|
.bat-b.hi{background:#10b981}.bat-b.mid{background:#f59e0b}.bat-b.low{background:#ef4444}
|
|
/* Lock */
|
|
.lk{display:flex;align-items:center;justify-content:center;gap:8px;padding:8px;border-radius:8px;margin:4px 0;font-weight:700;font-size:.82rem}
|
|
.lk.locked{background:#fef2f2;border:1px solid #fecaca;color:#dc2626}
|
|
.lk.unlocked{background:#ecfdf5;border:1px solid #d1fae5;color:#059669}
|
|
/* LED */
|
|
.led{width:24px;height:24px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center}
|
|
.led.r-on{background:#ef4444;box-shadow:0 0 14px #ef4444;animation:gl 1.2s ease-in-out infinite}
|
|
.led.g-on{background:#10b981;box-shadow:0 0 14px #10b981;animation:gl 1.2s ease-in-out infinite}
|
|
.led.blink{animation:bl .5s ease-in-out infinite!important}
|
|
.led.none{background:#e2e8f0}
|
|
@keyframes gl{0%,100%{opacity:1}50%{opacity:.4}}
|
|
@keyframes bl{0%,100%{opacity:1}50%{opacity:0}}
|
|
/* Relay */
|
|
.rl{display:flex;align-items:center;gap:5px;padding:5px 8px;background:#f8fafc;border-radius:6px;border:1px solid #e5e9f0;margin:4px 0;font-size:.6rem;color:#475569}
|
|
.rl-n{width:7px;height:7px;border-radius:50%}
|
|
.rl-n.a{background:#10b981;box-shadow:0 0 4px #10b981}.rl-n.i{background:#cbd5e1}
|
|
.rl-w{flex:1;height:2px;border-radius:1px}
|
|
.rl-w.a{background:#10b981}.rl-w.i{background:#cbd5e1}
|
|
/* WiFi */
|
|
.wf{display:inline-flex;align-items:flex-end;gap:1.5px;height:12px}
|
|
.wf span{width:3px;border-radius:1px}
|
|
</style>
|
|
|
|
<div class="row">
|
|
<div class="col-md-12 grid-margin">
|
|
<div class="row">
|
|
<div class="col-12 col-xl-8 mb-4 mb-xl-0">
|
|
<h3 class="font-weight-bold">Monitoring Board</h3>
|
|
<h6 class="font-weight-normal mb-0">Pantau status perangkat secara real-time, <span class="text-primary">Semua sistem terpantau disini !</span></h6>
|
|
</div>
|
|
<div class="col-12 col-xl-4">
|
|
<div class="justify-content-end d-flex" style="gap:8px;align-items:center;flex-wrap:wrap;">
|
|
<div class="mclk" id="clk">--:--:--</div>
|
|
<span class="mcon off" id="cB"><span class="mcon-dot" id="cD"></span><span id="cT">Offline</span></span>
|
|
<button class="mbtn" id="bR"><i class="mdi mdi-refresh"></i></button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="sbar">
|
|
<div class="sbar-i"><div class="sbar-d on"></div><div><div class="sbar-n" id="sOn">0</div><div class="sbar-l">Online</div></div></div>
|
|
<div class="sbar-i"><div class="sbar-d wr"></div><div><div class="sbar-n" id="sWr">0</div><div class="sbar-l">Warning</div></div></div>
|
|
<div class="sbar-i"><div class="sbar-d er"></div><div><div class="sbar-n" id="sEr">0</div><div class="sbar-l">Error</div></div></div>
|
|
<div class="sbar-i"><div class="sbar-d of"></div><div><div class="sbar-n" id="sOf">0</div><div class="sbar-l">Offline</div></div></div>
|
|
</div>
|
|
|
|
<div class="sec" style="color:#2563eb"><i class="mdi mdi-chip"></i> Controller</div>
|
|
<div class="g" id="gCtrl"></div>
|
|
<div class="sec" style="color:#7c3aed"><i class="mdi mdi-lock-smart"></i> Access & Actuators</div>
|
|
<div class="g" id="gAcc"></div>
|
|
<div class="sec" style="color:#0891b2"><i class="mdi mdi-led-on"></i> Indicators</div>
|
|
<div class="g" id="gInd"></div>
|
|
<div class="sec" style="color:#d97706"><i class="mdi mdi-flash"></i> Power Management</div>
|
|
<div class="g" id="gPow"></div>
|
|
|
|
</div><?php include 'footer.php'; ?>
|
|
<script>
|
|
(function(){
|
|
'use strict';
|
|
const API='api_monitoring.php',POLL=3000,MP=30;
|
|
let comps=[],busy=false,built=false;
|
|
const CH={},HD={};
|
|
|
|
function tick(){const n=new Date();document.getElementById('clk').textContent=[n.getHours(),n.getMinutes(),n.getSeconds()].map(v=>String(v).padStart(2,'0')).join(':')}
|
|
setInterval(tick,1000);tick();
|
|
|
|
async function pull(){
|
|
if(busy)return;busy=true;document.getElementById('bR').classList.add('spin');
|
|
try{const r=await fetch(API+'?action=status'),j=await r.json();
|
|
if(j.success){comps=j.data;uSum(j.summary);if(!built){buildAll();built=true;}else{updateAll();}cn(true);}
|
|
}catch(e){cn(false);}
|
|
busy=false;document.getElementById('bR').classList.remove('spin');
|
|
}
|
|
function uSum(s){$('sOn',s.online);$('sWr',s.warning);$('sEr',s.error);$('sOf',s.offline);}
|
|
function $(id,v){document.getElementById(id).textContent=v;}
|
|
function cn(ok){document.getElementById('cB').className='mcon '+(ok?'on':'off');document.getElementById('cT').textContent=ok?'Connected':'Offline';}
|
|
function el(id){return document.getElementById(id);}
|
|
function F(k){return comps.find(c=>c.component_key===k);}
|
|
function E(c,k){return(c&&c.extra_data&&c.extra_data[k])||'0';}
|
|
function tA(d){if(!d)return'Never';const s=Math.floor((Date.now()-new Date(d).getTime())/1000);if(s<5)return'Just now';if(s<60)return s+'s ago';if(s<3600)return Math.floor(s/60)+'m ago';return Math.floor(s/3600)+'h ago';}
|
|
function fm(v,d){return parseFloat(v||0).toFixed(d||1);}
|
|
function fUp(s){s=parseInt(s)||0;const h=Math.floor(s/3600),m=Math.floor((s%3600)/60),sc=s%60;return String(h).padStart(2,'0')+':'+String(m).padStart(2,'0')+':'+String(sc).padStart(2,'0');}
|
|
function fH(b){b=parseInt(b)||0;if(b>1048576)return(b/1048576).toFixed(1)+' MB';if(b>1024)return(b/1024).toFixed(1)+' KB';return b+' B';}
|
|
function sc(s){return s==='online'?'on':s==='warning'?'wr':s==='error'?'er':'off';}
|
|
function sl(s){return{online:'ONLINE',offline:'OFFLINE',warning:'WARNING',error:'ERROR'}[s]||'OFFLINE';}
|
|
function ft(c,id){return`<div class="cd-f" id="ft-${id}"><i class="mdi mdi-clock-outline"></i> ${tA(c.last_seen)}</div>`;}
|
|
function mr(icon,lbl,val,cls){return`<div class="m"><span class="m-l"><i class="mdi ${icon}"></i> ${lbl}</span><span class="m-v ${cls||''}">${val}</span></div>`;}
|
|
function wfb(rssi){rssi=parseInt(rssi)||0;let bars=rssi<=-90?0:rssi<=-75?1:rssi<=-60?2:rssi<=-45?3:4;const h=[3,6,9,12];let s='<span class="wf">';for(let i=0;i<4;i++)s+=`<span style="height:${h[i]}px;background:${i<bars?'#10b981':'#cbd5e1'}"></span>`;return s+'</span>';}
|
|
|
|
function buildAll(){try{bCtrl();}catch(e){console.warn(e);}try{bAcc();}catch(e){console.warn(e);}try{bInd();}catch(e){console.warn(e);}try{bPow();}catch(e){console.warn(e);}}
|
|
function updateAll(){try{uCtrl();}catch(e){}try{uAcc();}catch(e){}try{uInd();}catch(e){}try{uPow();}catch(e){}}
|
|
|
|
// CONTROLLER
|
|
function bCtrl(){
|
|
const c=F('esp32');if(!c)return;const e=c.extra_data||{};
|
|
el('gCtrl').innerHTML=`<div class="g12"><div class="cd"><div class="cd-top ${sc(c.status)}" id="bar-esp"></div><div class="cd-b">
|
|
<div class="cd-h"><div class="cd-ic" style="background:#f5f3ff;color:#7c3aed"><i class="mdi mdi-developer-board"></i></div>
|
|
<div><div class="cd-nm">${c.component_name}</div><div class="cd-tp">Main Controller</div></div>
|
|
<span class="bd ${sc(c.status)}" id="bdg-esp">${sl(c.status)}</span></div>
|
|
<div style="display:flex;gap:14px;flex-wrap:wrap">
|
|
<div style="flex:1;min-width:200px">
|
|
<div style="font-size:.62rem;color:#2563eb;font-weight:700;text-transform:uppercase;letter-spacing:1px;margin-bottom:4px"><i class="mdi mdi-wifi"></i> Connectivity</div>
|
|
<div id="esp-conn">${mr('mdi-wifi','WiFi',e.wifi_status==='on'?'CONNECTED':'DISCONNECTED',e.wifi_status==='on'?'g':'r')}${mr('mdi-signal','Signal',wfb(e.wifi_rssi)+' '+E(c,'wifi_rssi')+' dBm','')}${mr('mdi-access-point','SSID',e.wifi_ssid||'—','b')}${mr('mdi-ip-network','IP',e.wifi_ip||'—','c')}</div>
|
|
</div>
|
|
<div style="flex:1;min-width:200px">
|
|
<div style="font-size:.62rem;color:#7c3aed;font-weight:700;text-transform:uppercase;letter-spacing:1px;margin-bottom:4px"><i class="mdi mdi-cog"></i> System</div>
|
|
<div id="esp-sys">${mr('mdi-speedometer','CPU',E(c,'cpu_freq')+' MHz','b')}${mr('mdi-memory','Heap',fH(e.free_heap),'c')}${mr('mdi-clock-fast','Uptime',fUp(e.uptime),'g')}${mr('mdi-thermometer','Temp',fm(e.temperature)+' °C','o')}${mr('mdi-flash','V Input',fm(e.voltage_in)+' V','o')}</div>
|
|
</div>
|
|
</div>
|
|
</div>${ft(c,'esp')}</div></div>`;
|
|
}
|
|
function uCtrl(){
|
|
const c=F('esp32');if(!c)return;const e=c.extra_data||{};
|
|
const b=el('bar-esp');if(b)b.className='cd-top '+sc(c.status);
|
|
const bd=el('bdg-esp');if(bd){bd.className='bd '+sc(c.status);bd.textContent=sl(c.status);}
|
|
const cn=el('esp-conn');if(cn)cn.innerHTML=mr('mdi-wifi','WiFi',e.wifi_status==='on'?'CONNECTED':'DISCONNECTED',e.wifi_status==='on'?'g':'r')+mr('mdi-signal','Signal',wfb(e.wifi_rssi)+' '+E(c,'wifi_rssi')+' dBm','')+mr('mdi-access-point','SSID',e.wifi_ssid||'—','b')+mr('mdi-ip-network','IP',e.wifi_ip||'—','c');
|
|
const sy=el('esp-sys');if(sy)sy.innerHTML=mr('mdi-speedometer','CPU',E(c,'cpu_freq')+' MHz','b')+mr('mdi-memory','Heap',fH(e.free_heap),'c')+mr('mdi-clock-fast','Uptime',fUp(e.uptime),'g')+mr('mdi-thermometer','Temp',fm(e.temperature)+' °C','o')+mr('mdi-flash','V Input',fm(e.voltage_in)+' V','o');
|
|
const f=el('ft-esp');if(f)f.innerHTML='<i class="mdi mdi-clock-outline"></i> '+tA(c.last_seen);
|
|
}
|
|
|
|
// ACCESS
|
|
function bAcc(){
|
|
const fp=F('fingerprint'),so=F('solenoid_lock'),rl=F('relay');let h='';
|
|
if(fp){const e=fp.extra_data||{};
|
|
h+=`<div class="g4"><div class="cd"><div class="cd-top ${sc(fp.status)}" id="bar-fp"></div><div class="cd-b">
|
|
<div class="cd-h"><div class="cd-ic" style="background:#fff1f2;color:#e11d48"><i class="mdi mdi-fingerprint"></i></div>
|
|
<div><div class="cd-nm">${fp.component_name}</div><div class="cd-tp">Biometric</div></div>
|
|
<span class="bd ${sc(fp.status)}" id="bdg-fp">${(fp.extra_data?.sensor_state||'ready').toUpperCase()}</span></div>
|
|
<div id="info-fp">${mr('mdi-account-group','Enrolled',e.enrolled_count||'0','b')}${mr('mdi-check-decagram','Result',e.last_scan_result||'—',e.last_scan_result==='matched'?'g':'r')}${mr('mdi-percent','Confidence',fm(e.confidence)+' %','c')}</div>
|
|
</div>${ft(fp,'fp')}</div></div>`;}
|
|
if(so){const e=so.extra_data||{};const lk=e.lock_state==='locked';
|
|
h+=`<div class="g4"><div class="cd"><div class="cd-top ${lk?'er':'on'}" id="bar-so"></div><div class="cd-b">
|
|
<div class="cd-h"><div class="cd-ic" style="background:${lk?'#fef2f2':'#ecfdf5'};color:${lk?'#dc2626':'#059669'}"><i class="mdi ${lk?'mdi-lock':'mdi-lock-open-variant'}"></i></div>
|
|
<div><div class="cd-nm">${so.component_name}</div><div class="cd-tp">Door Lock</div></div>
|
|
<span class="bd ${lk?'er':'on'}" id="bdg-so">${lk?'LOCKED':'UNLOCKED'}</span></div>
|
|
<div id="info-so">
|
|
<div class="lk ${lk?'locked':'unlocked'}">${lk?'🔒 SECURED':'🔓 OPEN'}</div>
|
|
${mr('mdi-flash','Voltage',fm(e.voltage)+' V','o')}${mr('mdi-lightning-bolt','Power',fm(e.power_watts)+' W','c')}${mr('mdi-counter','Opens',e.open_count||'0','b')}
|
|
</div>
|
|
</div>${ft(so,'so')}</div></div>`;}
|
|
if(rl){const e=rl.extra_data||{};const isOn=e.relay_state==='on';const cir=e.circuit_connected==='yes';
|
|
h+=`<div class="g4"><div class="cd"><div class="cd-top ${isOn?'on':'off'}" id="bar-rl"></div><div class="cd-b">
|
|
<div class="cd-h"><div class="cd-ic" style="background:#f5f3ff;color:#7c3aed"><i class="mdi mdi-electric-switch"></i></div>
|
|
<div><div class="cd-nm">${rl.component_name}</div><div class="cd-tp">Switch</div></div>
|
|
<span class="bd ${isOn?'on':'off'}" id="bdg-rl">${isOn?'ENERGIZED':'OFF'}</span></div>
|
|
<div id="info-rl">
|
|
<div class="rl">Relay <div class="rl-n ${cir?'a':'i'}"></div><div class="rl-w ${cir?'a':'i'}"></div><div class="rl-n ${cir?'a':'i'}"></div> Solenoid</div>
|
|
${mr('mdi-electric-switch','State',isOn?'ON':'OFF',isOn?'g':'')}${mr('mdi-link','Circuit',cir?'CONNECTED':'DISCONNECTED',cir?'g':'r')}${mr('mdi-counter','Triggers',e.trigger_count||'0','b')}
|
|
</div>
|
|
</div>${ft(rl,'rl')}</div></div>`;}
|
|
el('gAcc').innerHTML=h;
|
|
}
|
|
function uAcc(){
|
|
const fp=F('fingerprint'),so=F('solenoid_lock'),rl=F('relay');
|
|
if(fp){const e=fp.extra_data||{};el('info-fp').innerHTML=mr('mdi-account-group','Enrolled',e.enrolled_count||'0','b')+mr('mdi-check-decagram','Result',e.last_scan_result||'—',e.last_scan_result==='matched'?'g':'r')+mr('mdi-percent','Confidence',fm(e.confidence)+' %','c');el('bar-fp').className='cd-top '+sc(fp.status);el('ft-fp').innerHTML='<i class="mdi mdi-clock-outline"></i> '+tA(fp.last_seen);}
|
|
if(so){const e=so.extra_data||{};const lk=e.lock_state==='locked';el('info-so').innerHTML=`<div class="lk ${lk?'locked':'unlocked'}">${lk?'🔒 SECURED':'🔓 OPEN'}</div>`+mr('mdi-flash','Voltage',fm(e.voltage)+' V','o')+mr('mdi-lightning-bolt','Power',fm(e.power_watts)+' W','c')+mr('mdi-counter','Opens',e.open_count||'0','b');el('bar-so').className='cd-top '+(lk?'er':'on');el('ft-so').innerHTML='<i class="mdi mdi-clock-outline"></i> '+tA(so.last_seen);}
|
|
if(rl){const e=rl.extra_data||{};const isOn=e.relay_state==='on';const cir=e.circuit_connected==='yes';el('info-rl').innerHTML=`<div class="rl">Relay <div class="rl-n ${cir?'a':'i'}"></div><div class="rl-w ${cir?'a':'i'}"></div><div class="rl-n ${cir?'a':'i'}"></div> Solenoid</div>`+mr('mdi-electric-switch','State',isOn?'ON':'OFF',isOn?'g':'')+mr('mdi-link','Circuit',cir?'CONNECTED':'DISCONNECTED',cir?'g':'r')+mr('mdi-counter','Triggers',e.trigger_count||'0','b');el('bar-rl').className='cd-top '+(isOn?'on':'off');el('ft-rl').innerHTML='<i class="mdi mdi-clock-outline"></i> '+tA(rl.last_seen);}
|
|
}
|
|
|
|
// INDICATORS
|
|
function bInd(){
|
|
const tf=F('tft_screen'),bz=F('buzzer'),lr=F('led_red'),lg=F('led_green');let h='';
|
|
if(tf){const e=tf.extra_data||{};const act=(e.display_state||'active')==='active';
|
|
h+=`<div class="g3"><div class="cd"><div class="cd-top ${act?'on':'off'}" id="bar-tf"></div><div class="cd-b">
|
|
<div class="cd-h"><div class="cd-ic" style="background:#eff6ff;color:#2563eb"><i class="mdi mdi-monitor"></i></div>
|
|
<div><div class="cd-nm">TFT ILI9488</div><div class="cd-tp">Display</div></div>
|
|
<span class="bd ${act?'on':'off'}" id="bdg-tf">${act?'ACTIVE':'SLEEP'}</span></div>
|
|
<div id="info-tf">${mr('mdi-page-layout-body','Page',e.current_page||'Standby','b')}${mr('mdi-brightness-6','Brightness',(e.brightness||100)+'%','c')}</div>
|
|
</div>${ft(tf,'tf')}</div></div>`;}
|
|
if(bz){const e=bz.extra_data||{};const bp=e.buzzer_state==='beeping';
|
|
h+=`<div class="g3"><div class="cd"><div class="cd-top ${bp?'wr':'off'}" id="bar-bz"></div><div class="cd-b">
|
|
<div class="cd-h"><div class="cd-ic" style="background:#ecfeff;color:#0891b2"><i class="mdi mdi-volume-high"></i></div>
|
|
<div><div class="cd-nm">Buzzer</div><div class="cd-tp">Audio</div></div>
|
|
<span class="bd ${bp?'wr':'off'}" id="bdg-bz">${bp?'BEEPING':'SILENT'}</span></div>
|
|
<div id="info-bz">${mr('mdi-music-note','Tone',e.last_tone||'None','c')}${mr('mdi-counter','Beeps',e.beep_count||'0','b')}</div>
|
|
</div>${ft(bz,'bz')}</div></div>`;}
|
|
if(lg){const e=lg.extra_data||{};const on=e.led_state==='on';const bl=e.blink_mode==='blink';
|
|
h+=`<div class="g3"><div class="cd"><div class="cd-top ${on?'on':'off'}" id="bar-lg"></div><div class="cd-b">
|
|
<div class="cd-h"><div class="cd-ic" style="background:#ecfdf5;color:#059669"><i class="mdi mdi-led-on"></i></div>
|
|
<div><div class="cd-nm">LED Hijau</div><div class="cd-tp">Indicator</div></div></div>
|
|
<div id="info-lg" style="text-align:center;padding:6px 0"><div class="led ${on?'g-on':'none'} ${bl?'blink':''}"></div>
|
|
<div style="font-size:.72rem;color:${on?'#059669':'#475569'};margin-top:4px;font-weight:700">${on?'ON':'OFF'}${bl?' · BLINK':''}</div></div>
|
|
</div>${ft(lg,'lg')}</div></div>`;}
|
|
if(lr){const e=lr.extra_data||{};const on=e.led_state==='on';const bl=e.blink_mode==='blink';
|
|
h+=`<div class="g3"><div class="cd"><div class="cd-top ${on?'er':'off'}" id="bar-lr"></div><div class="cd-b">
|
|
<div class="cd-h"><div class="cd-ic" style="background:#fef2f2;color:#dc2626"><i class="mdi mdi-led-on"></i></div>
|
|
<div><div class="cd-nm">LED Merah</div><div class="cd-tp">Indicator</div></div></div>
|
|
<div id="info-lr" style="text-align:center;padding:6px 0"><div class="led ${on?'r-on':'none'} ${bl?'blink':''}"></div>
|
|
<div style="font-size:.72rem;color:${on?'#dc2626':'#475569'};margin-top:4px;font-weight:700">${on?'ON':'OFF'}${bl?' · BLINK':''}</div></div>
|
|
</div>${ft(lr,'lr')}</div></div>`;}
|
|
el('gInd').innerHTML=h;
|
|
}
|
|
function uInd(){
|
|
const tf=F('tft_screen'),bz=F('buzzer'),lr=F('led_red'),lg=F('led_green');
|
|
if(tf){const e=tf.extra_data||{};el('info-tf').innerHTML=mr('mdi-page-layout-body','Page',e.current_page||'Standby','b')+mr('mdi-brightness-6','Brightness',(e.brightness||100)+'%','c');el('ft-tf').innerHTML='<i class="mdi mdi-clock-outline"></i> '+tA(tf.last_seen);}
|
|
if(bz){const e=bz.extra_data||{};el('info-bz').innerHTML=mr('mdi-music-note','Tone',e.last_tone||'None','c')+mr('mdi-counter','Beeps',e.beep_count||'0','b');el('ft-bz').innerHTML='<i class="mdi mdi-clock-outline"></i> '+tA(bz.last_seen);}
|
|
if(lg){const e=lg.extra_data||{};const on=e.led_state==='on';const bl=e.blink_mode==='blink';el('info-lg').innerHTML=`<div class="led ${on?'g-on':'none'} ${bl?'blink':''}"></div><div style="font-size:.72rem;color:${on?'#059669':'#475569'};margin-top:4px;font-weight:700">${on?'ON':'OFF'}${bl?' · BLINK':''}</div>`;}
|
|
if(lr){const e=lr.extra_data||{};const on=e.led_state==='on';const bl=e.blink_mode==='blink';el('info-lr').innerHTML=`<div class="led ${on?'r-on':'none'} ${bl?'blink':''}"></div><div style="font-size:.72rem;color:${on?'#dc2626':'#475569'};margin-top:4px;font-weight:700">${on?'ON':'OFF'}${bl?' · BLINK':''}</div>`;}
|
|
}
|
|
|
|
// POWER
|
|
function bPow(){
|
|
const ps=F('power_supply'),sd=F('step_down'),bt=F('battery');let h='';
|
|
if(ps){const e=ps.extra_data||{};
|
|
h+=`<div class="g4"><div class="cd"><div class="cd-top ${sc(ps.status)}" id="bar-ps"></div><div class="cd-b">
|
|
<div class="cd-h"><div class="cd-ic" style="background:#fff7ed;color:#d97706"><i class="mdi mdi-power-plug"></i></div>
|
|
<div><div class="cd-nm">${ps.component_name}</div><div class="cd-tp">Power Supply</div></div>
|
|
<span class="bd ${sc(ps.status)}" id="bdg-ps">${sl(ps.status)}</span></div>
|
|
<div class="bm"><div class="bm-i"><div class="bm-v" style="color:#d97706" id="pV">${fm(e.voltage_out)} V</div><div class="bm-l">Volt</div></div>
|
|
<div class="bm-i"><div class="bm-v" style="color:#0891b2" id="pA">${fm(e.current_out)} A</div><div class="bm-l">Amp</div></div>
|
|
<div class="bm-i"><div class="bm-v" style="color:#059669" id="pW">${fm(e.power_watts)} W</div><div class="bm-l">Watt</div></div></div>
|
|
<div id="info-ps">${mr('mdi-flash','V In',fm(e.voltage_in)+' V','o')}${mr('mdi-flash','V Out',fm(e.voltage_out)+' V','o')}</div>
|
|
<div class="ch"><div class="ch-t"><i class="mdi mdi-chart-line"></i> Voltage & Current</div><div class="ch-w"><canvas id="cPS"></canvas></div></div>
|
|
</div>${ft(ps,'ps')}</div></div>`;}
|
|
if(sd){const e=sd.extra_data||{};const tmp=parseFloat(e.temperature)||0;
|
|
h+=`<div class="g4"><div class="cd"><div class="cd-top ${sc(sd.status)}" id="bar-sd"></div><div class="cd-b">
|
|
<div class="cd-h"><div class="cd-ic" style="background:#eff6ff;color:#2563eb"><i class="mdi mdi-arrow-down-bold"></i></div>
|
|
<div><div class="cd-nm">${sd.component_name}</div><div class="cd-tp">Regulator</div></div>
|
|
<span class="bd ${tmp>70?'er':sc(sd.status)}" id="bdg-sd">${tmp>70?'OVERHEAT':(e.state==='active'?'ACTIVE':'INACTIVE')}</span></div>
|
|
<div class="bm"><div class="bm-i"><div class="bm-v" style="color:#2563eb" id="sV">${fm(e.voltage_out)} V</div><div class="bm-l">Volt</div></div>
|
|
<div class="bm-i"><div class="bm-v" style="color:#0891b2" id="sA">${fm(e.current_out)} A</div><div class="bm-l">Amp</div></div>
|
|
<div class="bm-i"><div class="bm-v" style="color:#059669" id="sW">${fm(e.power_watts)} W</div><div class="bm-l">Watt</div></div></div>
|
|
<div id="info-sd">${mr('mdi-arrow-right','V In',fm(e.voltage_in)+' V','o')}${mr('mdi-arrow-left','V Out (5V)',fm(e.voltage_out)+' V','b')}${mr('mdi-thermometer','Temp',fm(e.temperature)+' °C',tmp>60?'r':'c')}${mr('mdi-percent','Efficiency',fm(e.efficiency)+' %','g')}</div>
|
|
<div class="ch"><div class="ch-t"><i class="mdi mdi-chart-line"></i> Output Stability</div><div class="ch-w"><canvas id="cSD"></canvas></div></div>
|
|
</div>${ft(sd,'sd')}</div></div>`;}
|
|
if(bt){const e=bt.extra_data||{};const pct=parseInt(e.charge_percent)||0;const bst=(e.bat_state||'standby').toUpperCase();const bcl=pct>50?'hi':pct>20?'mid':'low';const pcol=pct>50?'#059669':pct>20?'#d97706':'#dc2626';
|
|
h+=`<div class="g4"><div class="cd"><div class="cd-top ${sc(bt.status)}" id="bar-bt"></div><div class="cd-b">
|
|
<div class="cd-h"><div class="cd-ic" style="background:#ecfdf5;color:#059669"><i class="mdi mdi-battery"></i></div>
|
|
<div><div class="cd-nm">${bt.component_name}</div><div class="cd-tp">Backup</div></div>
|
|
<span class="bd ${sc(bt.status)}" id="bdg-bt">${bst}</span></div>
|
|
<div class="bm"><div class="bm-i"><div class="bm-v" style="color:#059669" id="bV">${fm(e.voltage,2)} V</div><div class="bm-l">Volt</div></div>
|
|
<div class="bm-i"><div class="bm-v" style="color:#0891b2" id="bA">${fm(e.current)} A</div><div class="bm-l">Amp</div></div>
|
|
<div class="bm-i"><div class="bm-v" style="color:#d97706" id="bW">${fm(e.power_watts)} W</div><div class="bm-l">Watt</div></div></div>
|
|
<div style="display:flex;align-items:center;gap:6px;margin:4px 0"><span style="font-size:.65rem;color:#1e293b;font-weight:600">Cap</span><div class="bat-w" style="flex:1"><div class="bat-b ${bcl}" id="batBar" style="width:${pct}%"></div></div><span style="font-size:.78rem;font-weight:800;color:${pcol};font-family:monospace" id="batPct">${pct}%</span></div>
|
|
<div id="info-bt">${mr('mdi-thermometer','Temp',fm(e.temperature)+' °C','c')}</div>
|
|
<div class="ch"><div class="ch-t"><i class="mdi mdi-chart-line"></i> Discharge / Charge</div><div class="ch-w"><canvas id="cBT"></canvas></div></div>
|
|
</div>${ft(bt,'bt')}</div></div>`;}
|
|
el('gPow').innerHTML=h;
|
|
try{if(ps)initCh('cPS',ps);}catch(e){console.warn(e);}
|
|
try{if(sd)initCh('cSD',sd);}catch(e){console.warn(e);}
|
|
try{if(bt)initCh('cBT',bt);}catch(e){console.warn(e);}
|
|
}
|
|
function uPow(){
|
|
const ps=F('power_supply'),sd=F('step_down'),bt=F('battery');
|
|
if(ps){const e=ps.extra_data||{};el('pV').textContent=fm(e.voltage_out)+' V';el('pA').textContent=fm(e.current_out)+' A';el('pW').textContent=fm(e.power_watts)+' W';el('info-ps').innerHTML=mr('mdi-flash','V In',fm(e.voltage_in)+' V','o')+mr('mdi-flash','V Out',fm(e.voltage_out)+' V','o');el('bar-ps').className='cd-top '+sc(ps.status);el('ft-ps').innerHTML='<i class="mdi mdi-clock-outline"></i> '+tA(ps.last_seen);try{updCh('cPS',ps);}catch(e){}}
|
|
if(sd){const e=sd.extra_data||{};const tmp=parseFloat(e.temperature)||0;el('sV').textContent=fm(e.voltage_out)+' V';el('sA').textContent=fm(e.current_out)+' A';el('sW').textContent=fm(e.power_watts)+' W';el('info-sd').innerHTML=mr('mdi-arrow-right','V In',fm(e.voltage_in)+' V','o')+mr('mdi-arrow-left','V Out (5V)',fm(e.voltage_out)+' V','b')+mr('mdi-thermometer','Temp',fm(e.temperature)+' °C',tmp>60?'r':'c')+mr('mdi-percent','Efficiency',fm(e.efficiency)+' %','g');el('bar-sd').className='cd-top '+sc(sd.status);el('ft-sd').innerHTML='<i class="mdi mdi-clock-outline"></i> '+tA(sd.last_seen);try{updCh('cSD',sd);}catch(e){}}
|
|
if(bt){const e=bt.extra_data||{};const pct=parseInt(e.charge_percent)||0;const pcol=pct>50?'#059669':pct>20?'#d97706':'#dc2626';const bcl=pct>50?'hi':pct>20?'mid':'low';el('bV').textContent=fm(e.voltage,2)+' V';el('bA').textContent=fm(e.current)+' A';el('bW').textContent=fm(e.power_watts)+' W';el('batBar').className='bat-b '+bcl;el('batBar').style.width=pct+'%';el('batPct').textContent=pct+'%';el('batPct').style.color=pcol;el('info-bt').innerHTML=mr('mdi-thermometer','Temp',fm(e.temperature)+' °C','c');el('bar-bt').className='cd-top '+sc(bt.status);el('ft-bt').innerHTML='<i class="mdi mdi-clock-outline"></i> '+tA(bt.last_seen);try{updCh('cBT',bt);}catch(e){}}
|
|
}
|
|
|
|
// CHARTS
|
|
function initCh(id,comp){const cv=document.getElementById(id);if(!cv)return;HD[id]={labels:[],ds:{}};pushD(id,comp);CH[id]=new Chart(cv.getContext('2d'),{type:'line',data:{labels:[...HD[id].labels],datasets:getDS(id,comp)},options:chO()});}
|
|
function updCh(id,comp){if(!CH[id])return;pushD(id,comp);const h=HD[id];CH[id].data.labels=[...h.labels];const ds=getDS(id,comp);ds.forEach((d,i)=>{if(CH[id].data.datasets[i])CH[id].data.datasets[i].data=d.data;});CH[id].update('none');}
|
|
function pushD(id,comp){const h=HD[id],e=comp.extra_data||{};const now=new Date().toLocaleTimeString('id-ID',{hour:'2-digit',minute:'2-digit',second:'2-digit'});h.labels.push(now);if(h.labels.length>MP)h.labels.shift();if(comp.component_key==='power_supply'){pu(h,'v',parseFloat(e.voltage_out||0));pu(h,'a',parseFloat(e.current_out||0));}else if(comp.component_key==='step_down'){pu(h,'vi',parseFloat(e.voltage_in||0));pu(h,'vo',parseFloat(e.voltage_out||0));pu(h,'a',parseFloat(e.current_out||0));}else if(comp.component_key==='battery'){pu(h,'v',parseFloat(e.voltage||0));pu(h,'p',parseFloat(e.charge_percent||0));}}
|
|
function getDS(id,comp){const h=HD[id];if(comp.component_key==='power_supply')return[ld('Voltage',h.ds.v,'#d97706','rgba(217,119,6,.06)',true),ld('Current',h.ds.a,'#0891b2','rgba(8,145,178,.06)',true)];if(comp.component_key==='step_down')return[ld('V In',h.ds.vi,'#dc2626',null,false),ld('V Out',h.ds.vo,'#059669','rgba(5,150,105,.04)',true),ld('Current',h.ds.a,'#2563eb',null,false)];if(comp.component_key==='battery')return[ld('Voltage',h.ds.v,'#059669','rgba(5,150,105,.04)',true),ld('Capacity %',h.ds.p,'#d97706','rgba(217,119,6,.06)',true)];return[];}
|
|
function ld(l,d,c,bg,f){return{label:l,data:[...d],borderColor:c,backgroundColor:bg||'transparent',borderWidth:1.5,fill:!!f,tension:.4,pointRadius:0,pointBackgroundColor:c};}
|
|
function pu(h,k,v){if(!h.ds[k])h.ds[k]=[];h.ds[k].push(v);if(h.ds[k].length>MP)h.ds[k].shift();}
|
|
function chO(){return{responsive:true,maintainAspectRatio:false,animation:false,plugins:{legend:{display:true,position:'bottom',labels:{color:'#475569',boxWidth:6,font:{size:7},padding:4}},tooltip:{mode:'index',intersect:false,backgroundColor:'#fff',titleColor:'#1e293b',bodyColor:'#475569',borderColor:'#e5e9f0',borderWidth:1,titleFont:{size:9},bodyFont:{size:8}}},scales:{x:{display:true,grid:{color:'rgba(0,0,0,.03)',drawBorder:false},ticks:{color:'#475569',font:{size:6},maxRotation:45,maxTicksLimit:6}},y:{display:true,grid:{color:'rgba(0,0,0,.04)',drawBorder:false},ticks:{color:'#475569',font:{size:7,family:'monospace'},padding:3,maxTicksLimit:5}}},interaction:{mode:'nearest',axis:'x',intersect:false}};}
|
|
|
|
document.getElementById('bR').addEventListener('click',pull);
|
|
pull();setInterval(pull,POLL);
|
|
})();
|
|
</script>
|