diff --git a/firmware/esp32/overland-controller/overland-controller.ino b/firmware/esp32/overland-controller/overland-controller.ino index a2e2b50..a2c5a80 100644 --- a/firmware/esp32/overland-controller/overland-controller.ino +++ b/firmware/esp32/overland-controller/overland-controller.ino @@ -48,6 +48,35 @@ h1{font-size:21px;margin:0}.sub{color:var(--muted);font-size:13px;margin-top:4px .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} + +.relayBadge{ + padding:4px 8px; + border-radius:999px; + font-size:11px; + font-weight:700; + border:1px solid var(--line); +} + +.relayBadge.on{ + background:#173c22; + color:#dfffea; +} + +.relayBadge.off{ + background:#3f1820; + color:#ffe2e8; +} + +.relayBadge.pending{ + background:#4d3f12; + color:#fff0b3; +} + +.relayFeedback{ + font-size:11px; + color:var(--muted); + margin-top:4px; +} input{width:100%;border:1px solid var(--line);border-radius:12px;padding:12px;background:#0f151d;color:var(--text);font-size:15px} .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} @@ -241,6 +270,19 @@ input{width:100%;border:1px solid var(--line);border-radius:12px;padding:12px;ba const API_BASE="/api/v1"; function api(path){return API_BASE+path;} +const relayPending={}; +const relayFeedback={}; + +function relayStateLabel(r){ + if(relayPending[r.id]!==undefined) return "SENDING"; + return r.state ? "ON" : "OFF"; +} + +function relayStateClass(r){ + if(relayPending[r.id]!==undefined) return "pending"; + return r.state ? "on" : "off"; +} + const STATUS_FIELDS_OVERVIEW="battery,temps,relays,vehicle,network,alarms,system"; function isConfigTabActive(){ @@ -344,8 +386,42 @@ async function setBmsEnabled(enabled){ await load(); } async function relay(id,on){ - await fetch(api("/relay/set"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id,state:on})}); - await load(); + relayPending[id]=on; + relayFeedback[id]=on ? "Turning on..." : "Turning off..."; + + if(window.lastStatus) render(window.lastStatus); + + try{ + const r=await fetch(api("/relay/set"),{ + method:"POST", + headers:{"Content-Type":"application/json"}, + body:JSON.stringify({id,state:on}) + }); + + if(!r.ok){ + throw new Error("HTTP "+r.status); + } + + relayFeedback[id]=on ? "Relay ON" : "Relay OFF"; + + await load(); + + }catch(err){ + + console.error(err); + relayFeedback[id]="Command failed"; + + if(window.lastStatus) render(window.lastStatus); + + }finally{ + + delete relayPending[id]; + + setTimeout(()=>{ + delete relayFeedback[id]; + if(window.lastStatus) render(window.lastStatus); + },1500); + } } function render(data){ data=mergeStatus(window.lastStatus,data); @@ -389,10 +465,26 @@ function render(data){ $("relays").innerHTML=(data.relays||[]).map(r=>`
- ${r.name||r.id} +
+
+ ${r.name||r.id} + ${relayStateLabel(r)} +
+
${relayFeedback[r.id]||""}
+
+ - - + + +
`).join("");