generated from rmcguire/go-server-with-otel
add ui, new config opts
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
Package econetui serves a small admin UI at / for viewing EcoNet devices and
|
||||
changing their operating mode. It renders server-side HTML (html/template with
|
||||
embedded assets) and uses HTMX for interactivity; device reads and mode writes
|
||||
go through the in-process gRPC server, exactly like the MCP packages. Auth is
|
||||
left to the edge (oauth2-proxy) — this package serves the human surface at /
|
||||
and the mode-write at /ui/, leaving /health, /metrics, and /api untouched.
|
||||
*/
|
||||
package econetui
|
||||
|
||||
import (
|
||||
"context"
|
||||
"embed"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitea.libretechconsulting.com/rmcguire/go-app/pkg/srv/http/opts"
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"gitea.libretechconsulting.com/rmcguire/econet-exporter/pkg/config"
|
||||
"gitea.libretechconsulting.com/rmcguire/econet-exporter/pkg/econet/econetgrpc"
|
||||
)
|
||||
|
||||
//go:embed templates/*.html
|
||||
var templatesFS embed.FS
|
||||
|
||||
//go:embed static
|
||||
var staticFS embed.FS
|
||||
|
||||
// pendingTTL bounds how long an optimistic mode change is shown before the UI
|
||||
// falls back to the reported state.
|
||||
const pendingTTL = 90 * time.Second
|
||||
|
||||
type EconetUIServer struct {
|
||||
ctx context.Context
|
||||
cfg *config.ServiceConfig
|
||||
log *zerolog.Logger
|
||||
econetGRPC *econetgrpc.EconetGRPCServer
|
||||
tmpl *template.Template
|
||||
pending *pendingStore
|
||||
}
|
||||
|
||||
func NewEconetUIServer(ctx context.Context, cfg *config.ServiceConfig,
|
||||
grpc *econetgrpc.EconetGRPCServer,
|
||||
) *EconetUIServer {
|
||||
tmpl := template.Must(template.New("").Funcs(template.FuncMap{
|
||||
"prettyMode": prettyMode,
|
||||
"fillClass": fillClass,
|
||||
"fillWidth": fillWidth,
|
||||
}).ParseFS(templatesFS, "templates/*.html"))
|
||||
|
||||
return &EconetUIServer{
|
||||
ctx: ctx,
|
||||
cfg: cfg,
|
||||
log: zerolog.Ctx(ctx),
|
||||
econetGRPC: grpc,
|
||||
tmpl: tmpl,
|
||||
pending: newPendingStore(pendingTTL),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *EconetUIServer) GetHandlers() []opts.HTTPHandler {
|
||||
staticSub, _ := fs.Sub(staticFS, "static")
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("GET /{$}", s.handleIndex)
|
||||
mux.HandleFunc("GET /ui/devices", s.handleDevices)
|
||||
mux.HandleFunc("GET /ui/devices/{serial}/debug", s.handleDebug)
|
||||
mux.HandleFunc("POST /ui/devices/{serial}/mode", s.handleSetMode)
|
||||
mux.Handle("GET /static/", http.StripPrefix("/static/", http.FileServer(http.FS(staticSub))))
|
||||
|
||||
s.log.Debug().Msg("Econet admin UI ready at /")
|
||||
|
||||
return []opts.HTTPHandler{{Prefix: "/", Handler: mux}}
|
||||
}
|
||||
|
||||
// prettyMode turns a mode slug ("heat-pump") into a display label ("Heat Pump").
|
||||
func prettyMode(slug string) string {
|
||||
switch slug {
|
||||
case "":
|
||||
return "Unknown"
|
||||
case "electric-gas":
|
||||
return "Electric/Gas"
|
||||
}
|
||||
words := strings.Split(slug, "-")
|
||||
for i, w := range words {
|
||||
if w != "" {
|
||||
words[i] = strings.ToUpper(w[:1]) + w[1:]
|
||||
}
|
||||
}
|
||||
return strings.Join(words, " ")
|
||||
}
|
||||
|
||||
// fillClass buckets a hot-water fill percentage into a color class. It takes
|
||||
// int32 to match the proto Device field (html/template is strict about types).
|
||||
func fillClass(pct int32) string {
|
||||
switch {
|
||||
case pct >= 66:
|
||||
return "fill-high"
|
||||
case pct >= 33:
|
||||
return "fill-mid"
|
||||
default:
|
||||
return "fill-low"
|
||||
}
|
||||
}
|
||||
|
||||
// fillWidth returns a safe CSS width for the fill bar.
|
||||
func fillWidth(pct int32) template.CSS {
|
||||
if pct < 0 {
|
||||
pct = 0
|
||||
}
|
||||
return template.CSS(fmt.Sprintf("width:%d%%", pct))
|
||||
}
|
||||
Reference in New Issue
Block a user