Add server-side rendered setup UI accessible via `heatwave web`. The dashboard is now re-rendered per request and includes a nav bar linking to the new /setup page. Setup provides full CRUD for profiles, rooms, devices, occupants, AC units (with room assignment), scenario toggles, and forecast fetching — all via POST/redirect/GET forms. - Add ShowNav field to DashboardData for conditional nav bar - Extract fetchForecastForProfile() for reuse by web handler - Create setup.html.tmpl with Tailwind-styled entity sections - Create web_handlers.go with 15 route handlers and flash cookies - Switch web.go from pre-rendered to per-request dashboard rendering - Graceful dashboard fallback when no forecast data exists
156 lines
3.1 KiB
Go
156 lines
3.1 KiB
Go
package report
|
|
|
|
import (
|
|
"bytes"
|
|
_ "embed"
|
|
"fmt"
|
|
"html/template"
|
|
"io"
|
|
|
|
"github.com/cnachtigall/heatwave-autopilot/internal/static"
|
|
)
|
|
|
|
//go:embed templates/dashboard.html.tmpl
|
|
var dashboardTmpl string
|
|
|
|
// templateData extends DashboardData with CSS for the template.
|
|
type templateData struct {
|
|
DashboardData
|
|
CSS template.CSS
|
|
}
|
|
|
|
var funcMap = template.FuncMap{
|
|
"formatTemp": formatTemp,
|
|
"formatWatts": formatWatts,
|
|
"formatBTU": formatBTU,
|
|
"tempColor": tempColor,
|
|
"riskBadge": riskBadge,
|
|
"riskBg": riskBg,
|
|
"riskBadgeBg": riskBadgeBg,
|
|
"statusBadge": statusBadge,
|
|
"statusColor": statusColor,
|
|
"statusBadgeBg": statusBadgeBg,
|
|
}
|
|
|
|
// Generate renders the dashboard HTML to the given writer.
|
|
func Generate(w io.Writer, data DashboardData) error {
|
|
tmpl, err := template.New("dashboard").Funcs(funcMap).Parse(dashboardTmpl)
|
|
if err != nil {
|
|
return fmt.Errorf("parse template: %w", err)
|
|
}
|
|
|
|
td := templateData{
|
|
DashboardData: data,
|
|
CSS: template.CSS(static.TailwindCSS),
|
|
}
|
|
return tmpl.Execute(w, td)
|
|
}
|
|
|
|
// GenerateString renders the dashboard HTML to a string.
|
|
func GenerateString(data DashboardData) (string, error) {
|
|
var buf bytes.Buffer
|
|
if err := Generate(&buf, data); err != nil {
|
|
return "", err
|
|
}
|
|
return buf.String(), nil
|
|
}
|
|
|
|
func formatTemp(c float64) string {
|
|
return fmt.Sprintf("%.1f°C", c)
|
|
}
|
|
|
|
func formatWatts(w float64) string {
|
|
return fmt.Sprintf("%.0f W", w)
|
|
}
|
|
|
|
func formatBTU(b float64) string {
|
|
return fmt.Sprintf("%.0f BTU/h", b)
|
|
}
|
|
|
|
func tempColor(c float64) string {
|
|
switch {
|
|
case c >= 40:
|
|
return "text-red-700 dark:text-red-400"
|
|
case c >= 35:
|
|
return "text-red-600 dark:text-red-400"
|
|
case c >= 30:
|
|
return "text-orange-600 dark:text-orange-400"
|
|
case c >= 25:
|
|
return "text-yellow-600 dark:text-yellow-400"
|
|
default:
|
|
return "text-green-600 dark:text-green-400"
|
|
}
|
|
}
|
|
|
|
func riskBadge(level string) string {
|
|
switch level {
|
|
case "extreme":
|
|
return "EXTREME"
|
|
case "high":
|
|
return "HIGH"
|
|
case "moderate":
|
|
return "MODERATE"
|
|
default:
|
|
return "LOW"
|
|
}
|
|
}
|
|
|
|
func riskBg(level string) string {
|
|
switch level {
|
|
case "extreme":
|
|
return "bg-red-50 dark:bg-red-950"
|
|
case "high":
|
|
return "bg-orange-50 dark:bg-orange-950"
|
|
case "moderate":
|
|
return "bg-yellow-50 dark:bg-yellow-950"
|
|
default:
|
|
return "bg-white dark:bg-gray-800"
|
|
}
|
|
}
|
|
|
|
func riskBadgeBg(level string) string {
|
|
switch level {
|
|
case "extreme":
|
|
return "bg-red-600"
|
|
case "high":
|
|
return "bg-orange-600"
|
|
case "moderate":
|
|
return "bg-yellow-600"
|
|
default:
|
|
return "bg-green-600"
|
|
}
|
|
}
|
|
|
|
func statusBadge(status string) string {
|
|
switch status {
|
|
case "overloaded":
|
|
return "OVERLOADED"
|
|
case "marginal":
|
|
return "MARGINAL"
|
|
default:
|
|
return "OK"
|
|
}
|
|
}
|
|
|
|
func statusColor(status string) string {
|
|
switch status {
|
|
case "overloaded":
|
|
return "text-red-600 dark:text-red-400"
|
|
case "marginal":
|
|
return "text-orange-600 dark:text-orange-400"
|
|
default:
|
|
return "text-green-600 dark:text-green-400"
|
|
}
|
|
}
|
|
|
|
func statusBadgeBg(status string) string {
|
|
switch status {
|
|
case "overloaded":
|
|
return "bg-red-600"
|
|
case "marginal":
|
|
return "bg-orange-600"
|
|
default:
|
|
return "bg-green-600"
|
|
}
|
|
}
|