Redesign embedded dashboard with tabs
This commit is contained in:
@@ -24,20 +24,36 @@ const char INDEX_HTML[] PROGMEM = R"rawliteral(
|
||||
<title>Overland Controller</title>
|
||||
<style>
|
||||
:root{--bg:#0b0f14;--card:#141b24;--muted:#7f8da3;--text:#eef4ff;--good:#48d17a;--warn:#ffd166;--bad:#ff5c7a;--line:#223044;--btn:#233249}
|
||||
*{box-sizing:border-box} body{margin:0;background:var(--bg);color:var(--text);font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Arial,sans-serif}
|
||||
.wrap{max-width:980px;margin:0 auto;padding:18px}
|
||||
.top{display:flex;justify-content:space-between;gap:12px;align-items:center;margin-bottom:14px}
|
||||
h1{font-size:22px;margin:0}.sub{color:var(--muted);font-size:13px;margin-top:4px}.pill{border:1px solid var(--line);border-radius:999px;padding:7px 10px;color:var(--muted);font-size:12px}
|
||||
.grid{display:grid;grid-template-columns:repeat(12,1fr);gap:12px}.card{background:var(--card);border:1px solid var(--line);border-radius:18px;padding:16px;box-shadow:0 10px 28px rgba(0,0,0,.22)}
|
||||
.span6{grid-column:span 6}.span4{grid-column:span 4}.span12{grid-column:span 12}
|
||||
.label{color:var(--muted);font-size:12px;text-transform:uppercase;letter-spacing:.08em}.big{font-size:42px;font-weight:750;margin:8px 0}.row{display:flex;justify-content:space-between;gap:10px;border-top:1px solid var(--line);padding-top:10px;margin-top:10px}
|
||||
.v{font-weight:650}.good{color:var(--good)}.warn{color:var(--warn)}.bad{color:var(--bad)}.muted{color:var(--muted)}
|
||||
.bar{height:10px;background:#0e141c;border-radius:999px;overflow:hidden;border:1px solid var(--line)}.fill{height:100%;background:linear-gradient(90deg,var(--good),#9be15d);width:0%}
|
||||
button{width:100%;border:0;border-radius:12px;padding:12px;background:var(--btn);color:var(--text);font-weight:700;font-size:15px}
|
||||
*{box-sizing:border-box}
|
||||
body{margin:0;background:var(--bg);color:var(--text);font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Arial,sans-serif}
|
||||
.wrap{max-width:980px;margin:0 auto;padding:14px 14px 90px}
|
||||
.top{display:flex;justify-content:space-between;gap:12px;align-items:center;margin-bottom:12px}
|
||||
h1{font-size:21px;margin:0}.sub{color:var(--muted);font-size:13px;margin-top:4px}
|
||||
.pill{border:1px solid var(--line);border-radius:999px;padding:7px 10px;color:var(--muted);font-size:12px;white-space:nowrap}
|
||||
.tabs{position:fixed;left:0;right:0;bottom:0;background:#080c11;border-top:1px solid var(--line);display:grid;grid-template-columns:repeat(3,1fr);z-index:10}
|
||||
.tabbtn{border:0;border-radius:0;background:#080c11;color:var(--muted);padding:13px 8px;font-weight:800}
|
||||
.tabbtn.active{color:var(--text);background:#111923}
|
||||
.page{display:none}.page.active{display:block}
|
||||
.grid{display:grid;grid-template-columns:repeat(12,1fr);gap:12px}
|
||||
.card{background:var(--card);border:1px solid var(--line);border-radius:18px;padding:16px;box-shadow:0 10px 28px rgba(0,0,0,.22)}
|
||||
.span4{grid-column:span 4}.span6{grid-column:span 6}.span12{grid-column:span 12}
|
||||
.label{color:var(--muted);font-size:12px;text-transform:uppercase;letter-spacing:.08em}
|
||||
.big{font-size:42px;font-weight:800;margin:8px 0}
|
||||
.medium{font-size:25px;font-weight:800;margin:6px 0}
|
||||
.row{display:flex;justify-content:space-between;gap:10px;border-top:1px solid var(--line);padding-top:10px;margin-top:10px}
|
||||
.v{font-weight:700}.good{color:var(--good)}.warn{color:var(--warn)}.bad{color:var(--bad)}.muted{color:var(--muted)}
|
||||
.bar{height:10px;background:#0e141c;border-radius:999px;overflow:hidden;border:1px solid var(--line)}
|
||||
.fill{height:100%;background:linear-gradient(90deg,var(--good),#9be15d);width:0%}
|
||||
button{width:100%;border:0;border-radius:12px;padding:12px;background:var(--btn);color:var(--text);font-weight:800;font-size:15px}
|
||||
button.on{background:#17462a;color:#dfffea}button.off{background:#46202a;color:#ffe2e8}
|
||||
input{width:100%;border:1px solid var(--line);border-radius:12px;padding:12px;background:#0f151d;color:var(--text);font-size:15px}
|
||||
button.on{background:#17462a;color:#dfffea}button.off{background:#46202a;color:#ffe2e8}.relayBtns{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:12px}
|
||||
.tempList,.alarmList{display:grid;gap:8px;margin-top:12px}.item{display:flex;justify-content:space-between;gap:8px;background:#0f151d;border:1px solid var(--line);border-radius:12px;padding:10px}
|
||||
@media(max-width:760px){.span6,.span4{grid-column:span 12}.big{font-size:36px}.wrap{padding:12px}}
|
||||
.list{display:grid;gap:8px;margin-top:12px}
|
||||
.item{display:flex;justify-content:space-between;gap:8px;background:#0f151d;border:1px solid var(--line);border-radius:12px;padding:10px}
|
||||
.btnrow{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:12px}
|
||||
.kpi{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-top:12px}
|
||||
.kpi div{background:#0f151d;border:1px solid var(--line);border-radius:12px;padding:10px}
|
||||
.kpi .num{font-size:20px;font-weight:800;margin-top:4px}
|
||||
@media(max-width:760px){.span4,.span6{grid-column:span 12}.big{font-size:36px}.wrap{padding:12px 12px 82px}.kpi{grid-template-columns:1fr}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -45,118 +61,166 @@ const char INDEX_HTML[] PROGMEM = R"rawliteral(
|
||||
<div class="top">
|
||||
<div>
|
||||
<h1 id="device">Overland Controller</h1>
|
||||
<div class="sub">Local camp dashboard</div>
|
||||
<div class="sub" id="subtitle">Local camp dashboard</div>
|
||||
</div>
|
||||
<div class="pill" id="conn">Connecting...</div>
|
||||
</div>
|
||||
|
||||
<div id="overviewPage" class="page active">
|
||||
<div class="grid">
|
||||
<section class="card span6">
|
||||
<div class="label">Battery</div>
|
||||
<div class="big"><span id="soc">--</span>%</div>
|
||||
<div class="bar"><div class="fill" id="socbar"></div></div>
|
||||
<div class="row"><span class="muted">Voltage</span><span class="v" id="voltage">--</span></div>
|
||||
<div class="row"><span class="muted">Current</span><span class="v" id="current">--</span></div>
|
||||
<div class="row"><span class="muted">Remaining</span><span class="v" id="ah">--</span></div>
|
||||
</section>
|
||||
|
||||
<section class="card span6">
|
||||
<div class="label">BMS</div>
|
||||
<div class="big" id="bmsState">--</div>
|
||||
<div class="row"><span class="muted">Battery temp</span><span class="v" id="battTemp">--</span></div>
|
||||
<div class="row"><span class="muted">Cell delta</span><span class="v" id="delta">--</span></div>
|
||||
<div class="row"><span class="muted">Cycles</span><span class="v" id="cycles">--</span></div>
|
||||
<div class="row"><span class="muted">BMS Polling</span><span class="v" id="bmsEnabledText">--</span></div>
|
||||
<div class="relayBtns">
|
||||
<button onclick="setBmsEnabled(true)">Enable BMS</button>
|
||||
<button class="off" onclick="setBmsEnabled(false)">Disable BMS</button>
|
||||
<div class="kpi">
|
||||
<div><div class="muted">Voltage</div><div class="num" id="voltage">--</div></div>
|
||||
<div><div class="muted">Current</div><div class="num" id="current">--</div></div>
|
||||
<div><div class="muted">Runtime</div><div class="num" id="runtime">--</div></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card span6">
|
||||
<div class="label">Temperatures</div>
|
||||
<div class="tempList" id="temps"></div>
|
||||
</section>
|
||||
|
||||
<section class="card span6">
|
||||
<div class="label">Relays</div>
|
||||
<div class="tempList" id="relays"></div>
|
||||
<div class="list" id="relays"></div>
|
||||
</section>
|
||||
|
||||
<section class="card span6">
|
||||
<div class="label">Temperatures</div>
|
||||
<div class="list" id="temps"></div>
|
||||
</section>
|
||||
|
||||
<section class="card span6">
|
||||
<div class="label">Network / Alarms</div>
|
||||
<div class="row"><span class="muted">STA</span><span class="v" id="wifiSta">--</span></div>
|
||||
<div class="row"><span class="muted">STA IP</span><span class="v" id="wifiIp">--</span></div>
|
||||
<div class="list" id="alarmSummary"></div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="batteryPage" class="page">
|
||||
<div class="grid">
|
||||
<section class="card span6">
|
||||
<div class="label">Battery Detail</div>
|
||||
<div class="medium" id="bmsState">--</div>
|
||||
<div class="row"><span class="muted">Source</span><span class="v" id="battSource">--</span></div>
|
||||
<div class="row"><span class="muted">Remaining</span><span class="v" id="ah">--</span></div>
|
||||
<div class="row"><span class="muted">Capacity</span><span class="v" id="capacity">--</span></div>
|
||||
<div class="row"><span class="muted">Battery temp</span><span class="v" id="battTemp">--</span></div>
|
||||
<div class="row"><span class="muted">Cycles</span><span class="v" id="cycles">--</span></div>
|
||||
</section>
|
||||
|
||||
<section class="card span6">
|
||||
<div class="label">Cells</div>
|
||||
<div class="row"><span class="muted">Cell count</span><span class="v" id="cellCount">--</span></div>
|
||||
<div class="row"><span class="muted">Cell delta</span><span class="v" id="delta">--</span></div>
|
||||
<div class="list" id="cells"></div>
|
||||
</section>
|
||||
|
||||
<section class="card span12">
|
||||
<div class="label">WiFi</div>
|
||||
<div class="sub">AP stays available at 192.168.4.1. STA networks are tried in order.</div>
|
||||
<div class="tempList" style="margin-top:12px">
|
||||
<div class="label">BMS Control</div>
|
||||
<div class="row"><span class="muted">BMS Polling</span><span class="v" id="bmsEnabledText">--</span></div>
|
||||
<div class="btnrow">
|
||||
<button onclick="setBmsEnabled(true)">Enable BMS</button>
|
||||
<button class="off" onclick="setBmsEnabled(false)">Disable BMS</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="configPage" class="page">
|
||||
<div class="grid">
|
||||
<section class="card span12">
|
||||
<div class="label">WiFi Config</div>
|
||||
<div class="sub">AP remains available at 192.168.4.1. Lower priority number is tried first.</div>
|
||||
<div class="list">
|
||||
<input id="w1s" placeholder="Network 1 SSID">
|
||||
<input id="w1p" placeholder="Network 1 password" type="password">
|
||||
<input id="w1r" placeholder="Network 1 priority, lower = first" inputmode="numeric">
|
||||
<input id="w1r" placeholder="Network 1 priority" inputmode="numeric">
|
||||
<input id="w2s" placeholder="Network 2 SSID">
|
||||
<input id="w2p" placeholder="Network 2 password" type="password">
|
||||
<input id="w2r" placeholder="Network 2 priority, lower = first" inputmode="numeric">
|
||||
<input id="w2r" placeholder="Network 2 priority" inputmode="numeric">
|
||||
<input id="w3s" placeholder="Network 3 SSID">
|
||||
<input id="w3p" placeholder="Network 3 password" type="password">
|
||||
<input id="w3r" placeholder="Network 3 priority, lower = first" inputmode="numeric">
|
||||
<input id="w3r" placeholder="Network 3 priority" inputmode="numeric">
|
||||
<button onclick="saveWifi()">Save WiFi</button>
|
||||
<button onclick="connectWifi()">Connect Now</button>
|
||||
</div>
|
||||
<div class="row"><span class="muted">STA</span><span class="v" id="wifiSta">--</span></div>
|
||||
<div class="row"><span class="muted">STA IP</span><span class="v" id="wifiIp">--</span></div>
|
||||
</section>
|
||||
|
||||
<section class="card span12">
|
||||
<div class="label">Alarms / System</div>
|
||||
<div class="alarmList" id="alarms"></div>
|
||||
<div class="label">System</div>
|
||||
<div class="row"><span class="muted">Firmware</span><span class="v" id="fw">--</span></div>
|
||||
<div class="row"><span class="muted">Uptime</span><span class="v" id="uptime">--</span></div>
|
||||
<div class="row"><span class="muted">AP IP</span><span class="v" id="apIp">--</span></div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tabs">
|
||||
<button class="tabbtn active" onclick="showTab('overview')">Overview</button>
|
||||
<button class="tabbtn" onclick="showTab('battery')">Battery</button>
|
||||
<button class="tabbtn" onclick="showTab('config')">Config</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const $=id=>document.getElementById(id);
|
||||
function fmt(n,d=1){return typeof n==="number"?n.toFixed(d):"--"}
|
||||
function temp(v){return typeof v==="number"?fmt(v,1)+"°F":"Offline"}
|
||||
function clsAlarm(v){return v?"bad":"good"}
|
||||
async function setBmsEnabled(enabled){
|
||||
try{
|
||||
await fetch("/config/bms",{
|
||||
method:"POST",
|
||||
headers:{"Content-Type":"application/json"},
|
||||
body:JSON.stringify({enabled:enabled})
|
||||
function runtimeText(b){
|
||||
if(!b.connected) return "--";
|
||||
if(typeof b.runtime_hours==="number" && b.runtime_hours>0) return fmt(b.runtime_hours,1)+"h";
|
||||
if(typeof b.current==="number" && b.current<-.1 && typeof b.remaining_ah==="number"){
|
||||
return fmt(b.remaining_ah/Math.abs(b.current),1)+"h";
|
||||
}
|
||||
return "Idle";
|
||||
}
|
||||
function showTab(name){
|
||||
["overview","battery","config"].forEach(t=>{
|
||||
$(t+"Page").classList.toggle("active",t===name);
|
||||
});
|
||||
document.querySelectorAll(".tabbtn").forEach((b,i)=>{
|
||||
b.classList.toggle("active",["overview","battery","config"][i]===name);
|
||||
});
|
||||
}
|
||||
async function setBmsEnabled(enabled){
|
||||
await fetch("/config/bms",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({enabled})});
|
||||
await fetch("/config/save",{method:"POST"});
|
||||
await load();
|
||||
}catch(e){}
|
||||
}
|
||||
|
||||
async function relay(id,on){
|
||||
try{
|
||||
await fetch("/relay/set",{
|
||||
method:"POST",
|
||||
headers:{"Content-Type":"application/json"},
|
||||
body:JSON.stringify({id:id,state:on})
|
||||
});
|
||||
await fetch("/relay/set",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id,state:on})});
|
||||
await load();
|
||||
}catch(e){}
|
||||
}
|
||||
function render(data){
|
||||
$("conn").textContent="Live";
|
||||
$("conn").className="pill good";
|
||||
$("device").textContent=data.config?.device_name||"Overland Controller";
|
||||
|
||||
const b=data.battery||{};
|
||||
$("soc").textContent=fmt(b.soc,0);
|
||||
$("socbar").style.width=Math.max(0,Math.min(100,b.soc||0))+"%";
|
||||
$("voltage").textContent=fmt(b.voltage,2)+" V";
|
||||
$("current").textContent=fmt(b.current,2)+" A";
|
||||
$("runtime").textContent=runtimeText(b);
|
||||
$("bmsState").textContent=b.connected?"BMS Online":"BMS Offline";
|
||||
$("bmsState").className=b.connected?"medium good":"medium bad";
|
||||
$("battSource").textContent=b.source||"--";
|
||||
$("ah").textContent=fmt(b.remaining_ah,1)+" Ah";
|
||||
$("bmsState").textContent=b.connected?"Online":"Offline";
|
||||
$("bmsState").className=b.connected?"big good":"big bad";
|
||||
$("capacity").textContent=fmt(b.capacity_ah,1)+" Ah";
|
||||
$("battTemp").textContent=temp(b.temperature_f);
|
||||
$("delta").textContent=(typeof b.cell_delta_mv==="number"?b.cell_delta_mv:"--")+" mV";
|
||||
$("cycles").textContent=b.cycle_count??"--";
|
||||
$("cellCount").textContent=b.cell_count??"--";
|
||||
$("delta").textContent=(typeof b.cell_delta_mv==="number"?b.cell_delta_mv:"--")+" mV";
|
||||
|
||||
const bmsCfg=data.config?.bms||{};
|
||||
$("bmsEnabledText").textContent=bmsCfg.enabled ? "Enabled" : "Disabled";
|
||||
|
||||
$("cells").innerHTML=(b.cell_voltages||[]).map((v,i)=>`
|
||||
<div class="item"><span>Cell ${i+1}</span><strong>${fmt(v,3)} V</strong></div>`).join("") ||
|
||||
`<div class="item"><span>No cell data</span><strong class="muted">--</strong></div>`;
|
||||
|
||||
$("temps").innerHTML=(data.temps||[]).map(t=>`
|
||||
<div class="item">
|
||||
<span>${t.name||t.id}</span>
|
||||
@@ -165,21 +229,27 @@ function render(data){
|
||||
|
||||
$("relays").innerHTML=(data.relays||[]).map(r=>`
|
||||
<div class="item" style="display:block">
|
||||
<div style="display:flex;justify-content:space-between"><span>${r.name||r.id}</span><strong class="${r.state?"good":"muted"}">${r.state?"ON":"OFF"}</strong></div>
|
||||
<div class="relayBtns">
|
||||
<div style="display:flex;justify-content:space-between">
|
||||
<span>${r.name||r.id}</span>
|
||||
<strong class="${r.state?"good":"muted"}">${r.state?"ON":"OFF"}</strong>
|
||||
</div>
|
||||
<div class="btnrow">
|
||||
<button class="on" onclick="relay('${r.id}',true)">ON</button>
|
||||
<button class="off" onclick="relay('${r.id}',false)">OFF</button>
|
||||
</div>
|
||||
</div>`).join("");
|
||||
|
||||
const alarms=data.alarms||{};
|
||||
$("alarms").innerHTML=Object.keys(alarms).map(k=>`
|
||||
<div class="item"><span>${k.replaceAll("_"," ")}</span><strong class="${clsAlarm(alarms[k])}">${alarms[k]?"ALARM":"OK"}</strong></div>`).join("");
|
||||
const active=Object.keys(alarms).filter(k=>alarms[k]);
|
||||
$("alarmSummary").innerHTML=active.length
|
||||
? active.map(k=>`<div class="item"><span>${k.replaceAll("_"," ")}</span><strong class="bad">ALARM</strong></div>`).join("")
|
||||
: `<div class="item"><span>Alarms</span><strong class="good">OK</strong></div>`;
|
||||
|
||||
const s=data.system||{};
|
||||
const n=data.network||{};
|
||||
$("wifiSta").textContent=n.sta_connected ? (n.sta_ssid||"connected") : "Not connected";
|
||||
$("wifiIp").textContent=n.sta_ip||"--";
|
||||
$("apIp").textContent=n.ap_ip||"--";
|
||||
$("fw").textContent=(s.firmware_name||"--")+" "+(s.firmware_version||"");
|
||||
$("uptime").textContent=(s.uptime_seconds||0)+" sec";
|
||||
}
|
||||
@@ -190,10 +260,8 @@ async function loadWifiConfig(){
|
||||
const nets=d.wifi?.networks||[];
|
||||
for(let i=0;i<3;i++){
|
||||
const n=nets[i]||{};
|
||||
const s=$("w"+(i+1)+"s");
|
||||
if(s) s.value=n.ssid||"";
|
||||
const pr=$("w"+(i+1)+"r");
|
||||
if(pr) pr.value=n.priority||i+1;
|
||||
$("w"+(i+1)+"s").value=n.ssid||"";
|
||||
$("w"+(i+1)+"r").value=n.priority||i+1;
|
||||
}
|
||||
}catch(e){}
|
||||
}
|
||||
@@ -205,11 +273,7 @@ async function saveWifi(){
|
||||
const priority=parseInt($("w"+i+"r").value||i,10);
|
||||
if(ssid) networks.push({ssid,password,priority});
|
||||
}
|
||||
await fetch("/config/wifi",{
|
||||
method:"POST",
|
||||
headers:{"Content-Type":"application/json"},
|
||||
body:JSON.stringify({networks})
|
||||
});
|
||||
await fetch("/config/wifi",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({networks})});
|
||||
await loadWifiConfig();
|
||||
await load();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user