add ui, new config opts

This commit is contained in:
2026-07-16 16:32:44 -04:00
parent 8effff311b
commit df01e060e4
33 changed files with 1104 additions and 37 deletions
+57
View File
@@ -0,0 +1,57 @@
{{define "card"}}
<article id="card-{{.Device.SerialNumber}}" class="device-card">
<div class="card-head">
<img class="device-icon" src="{{.Icon}}" alt="" width="56" height="56">
<div class="card-title">
<strong>{{.Device.FriendlyName}}</strong>
<small>{{.Device.SerialNumber}}</small>
</div>
<button class="bug-btn" title="Show raw device JSON" aria-label="Show raw device JSON"
hx-get="/ui/devices/{{.Device.SerialNumber}}/debug"
hx-target="#debug-modal-body"
hx-swap="innerHTML"
onclick="document.getElementById('debug-modal').showModal()">🐛</button>
<span class="dot {{if .Device.Connected}}dot-ok{{else}}dot-off{{end}}"
title="{{if .Device.Connected}}Connected{{else}}Disconnected{{end}}"></span>
</div>
<div class="stats">
<div><span>Status</span>
<b>{{if .Device.Running}}🔥 {{prettyMode .Device.RunningState}}{{else}}Idle{{end}}</b></div>
<div><span>Setpoint</span><b>{{.Device.Setpoint}}&deg;F</b></div>
<div><span>Alerts</span>
<b>{{if gt .Device.AlertCount 0}}⚠ {{.Device.AlertCount}}{{else}}0{{end}}</b></div>
</div>
{{if ge .Device.HotWaterAvailability 0}}
<div class="fill">
<div class="fill-head"><span>Hot water</span><b>{{.Device.HotWaterAvailability}}%</b></div>
<div class="fill-meter">
<div class="fill-bar {{fillClass .Device.HotWaterAvailability}}" style="{{fillWidth .Device.HotWaterAvailability}}"></div>
</div>
</div>
{{end}}
<div class="card-foot">
{{if .Modes}}
<label class="mode-label">
Mode
<select name="mode"
hx-post="/ui/devices/{{.Device.SerialNumber}}/mode"
hx-target="#card-{{.Device.SerialNumber}}"
hx-swap="outerHTML"
hx-trigger="change"
{{if .Pending}}disabled aria-busy="true"{{end}}>
{{range .Modes}}
<option value="{{.Value}}" {{if .Selected}}selected{{end}}>{{.Label}}</option>
{{end}}
</select>
</label>
{{if .Pending}}<span class="badge badge-pending" aria-busy="true">Updating → {{.Pending}}…</span>{{end}}
{{else}}
<small class="mode-fixed">Mode: {{prettyMode .Device.Mode}} (not settable)</small>
{{end}}
{{if .Error}}<span class="badge badge-error" role="alert">{{.Error}}</span>{{end}}
</div>
</article>
{{end}}
+1
View File
@@ -0,0 +1 @@
{{define "debug"}}{{.}}{{end}}
@@ -0,0 +1,10 @@
{{define "devices"}}
{{if .Devices}}
{{range .Devices}}{{template "card" .}}{{end}}
{{else}}
<article class="empty-state">
<p>No EcoNet devices loaded yet.</p>
<small>Device state populates after the first successful cloud poll.</small>
</article>
{{end}}
{{end}}
+57
View File
@@ -0,0 +1,57 @@
{{define "layout"}}<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>EcoNet Admin</title>
<link rel="stylesheet" href="/static/pico.classless.min.css">
<link rel="stylesheet" href="/static/app.css">
<script src="/static/htmx.min.js" defer></script>
<script>
// Set the theme before first paint to avoid a flash.
(function () {
var saved = localStorage.getItem("theme");
var theme = saved || (window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light");
document.documentElement.setAttribute("data-theme", theme);
})();
</script>
</head>
<body>
<header class="app-header">
<hgroup>
<h1>EcoNet Admin</h1>
<p>Rheem water heater status &amp; control</p>
</hgroup>
<button id="theme-toggle" class="secondary theme-toggle" aria-label="Toggle dark mode" onclick="toggleTheme()"></button>
</header>
<main>
<div id="devices" class="device-grid" hx-get="/ui/devices" hx-trigger="every 30s" hx-swap="innerHTML">
{{template "devices" .}}
</div>
</main>
<dialog id="debug-modal" class="debug-modal">
<article>
<header>
<button aria-label="Close" rel="prev" onclick="document.getElementById('debug-modal').close()"></button>
<strong>Raw device JSON</strong>
</header>
<pre><code id="debug-modal-body">Loading…</code></pre>
</article>
</dialog>
<script>
function themeIcon(t) { return t === "dark" ? "☀️" : "🌙"; } // sun / moon
function toggleTheme() {
var cur = document.documentElement.getAttribute("data-theme");
var next = cur === "dark" ? "light" : "dark";
document.documentElement.setAttribute("data-theme", next);
localStorage.setItem("theme", next);
document.getElementById("theme-toggle").textContent = themeIcon(next);
}
document.getElementById("theme-toggle").textContent =
themeIcon(document.documentElement.getAttribute("data-theme"));
</script>
</body>
</html>{{end}}