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
177 lines
4.0 KiB
Go
177 lines
4.0 KiB
Go
package report
|
|
|
|
import (
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func testDashboardData() DashboardData {
|
|
return DashboardData{
|
|
GeneratedAt: time.Date(2025, 7, 15, 10, 0, 0, 0, time.UTC),
|
|
ProfileName: "home",
|
|
Date: "2025-07-15",
|
|
RiskLevel: "high",
|
|
PeakTempC: 37.2,
|
|
MinNightTempC: 22.5,
|
|
PoorNightCool: true,
|
|
Warnings: []WarningData{
|
|
{
|
|
Headline: "Heat warning Berlin",
|
|
Severity: "Severe",
|
|
Description: "Temperatures up to 37C",
|
|
Instruction: "Stay hydrated",
|
|
Onset: "2025-07-15 11:00",
|
|
Expires: "2025-07-16 19:00",
|
|
},
|
|
},
|
|
RiskWindows: []RiskWindowData{
|
|
{StartHour: 11, EndHour: 18, PeakTempC: 37.2, Level: "high", Reason: "very hot"},
|
|
},
|
|
Timeline: []TimelineSlotData{
|
|
{
|
|
Hour: 12, HourStr: "12:00", TempC: 35.5, RiskLevel: "high", BudgetStatus: "marginal",
|
|
Actions: []ActionData{
|
|
{Name: "Hydration reminder", Category: "hydration", Impact: "medium"},
|
|
},
|
|
},
|
|
{
|
|
Hour: 0, HourStr: "00:00", TempC: 22, RiskLevel: "low", BudgetStatus: "comfortable",
|
|
},
|
|
},
|
|
RoomBudgets: []RoomBudgetData{
|
|
{
|
|
RoomName: "Office",
|
|
InternalGainsW: 300,
|
|
SolarGainW: 600,
|
|
VentGainW: 150,
|
|
TotalGainW: 1050,
|
|
TotalGainBTUH: 3583,
|
|
ACCapacityBTUH: 8000,
|
|
HeadroomBTUH: 4417,
|
|
Status: "comfortable",
|
|
},
|
|
},
|
|
CareChecklist: []string{"Check elderly at 14:00"},
|
|
}
|
|
}
|
|
|
|
func TestGenerate_ProducesValidHTML(t *testing.T) {
|
|
html, err := GenerateString(testDashboardData())
|
|
if err != nil {
|
|
t.Fatalf("GenerateString: %v", err)
|
|
}
|
|
if !strings.Contains(html, "<!DOCTYPE html>") {
|
|
t.Error("missing DOCTYPE")
|
|
}
|
|
if !strings.Contains(html, "</html>") {
|
|
t.Error("missing closing html tag")
|
|
}
|
|
}
|
|
|
|
func TestGenerate_ContainsProfileAndDate(t *testing.T) {
|
|
html, err := GenerateString(testDashboardData())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(html, "home") {
|
|
t.Error("missing profile name")
|
|
}
|
|
if !strings.Contains(html, "2025-07-15") {
|
|
t.Error("missing date")
|
|
}
|
|
}
|
|
|
|
func TestGenerate_ContainsWarning(t *testing.T) {
|
|
html, err := GenerateString(testDashboardData())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(html, "Heat warning Berlin") {
|
|
t.Error("missing warning headline")
|
|
}
|
|
if !strings.Contains(html, "Stay hydrated") {
|
|
t.Error("missing warning instruction")
|
|
}
|
|
}
|
|
|
|
func TestGenerate_ContainsTimeline(t *testing.T) {
|
|
html, err := GenerateString(testDashboardData())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(html, "12:00") {
|
|
t.Error("missing timeline hour")
|
|
}
|
|
if !strings.Contains(html, "Hydration reminder") {
|
|
t.Error("missing timeline action")
|
|
}
|
|
}
|
|
|
|
func TestGenerate_ContainsRoomBudget(t *testing.T) {
|
|
html, err := GenerateString(testDashboardData())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(html, "Office") {
|
|
t.Error("missing room name")
|
|
}
|
|
if !strings.Contains(html, "8000 BTU/h") {
|
|
t.Error("missing AC capacity")
|
|
}
|
|
}
|
|
|
|
func TestGenerate_ContainsCareChecklist(t *testing.T) {
|
|
html, err := GenerateString(testDashboardData())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(html, "Check elderly at 14:00") {
|
|
t.Error("missing care checklist item")
|
|
}
|
|
}
|
|
|
|
func TestGenerate_ContainsCSS(t *testing.T) {
|
|
html, err := GenerateString(testDashboardData())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(html, "box-sizing") {
|
|
t.Error("missing inlined CSS")
|
|
}
|
|
}
|
|
|
|
func TestGenerate_EmptyData(t *testing.T) {
|
|
_, err := GenerateString(DashboardData{
|
|
GeneratedAt: time.Now(),
|
|
ProfileName: "test",
|
|
Date: "2025-07-15",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("should handle empty data: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestGenerate_WriteFile(t *testing.T) {
|
|
path := t.TempDir() + "/report.html"
|
|
f, err := os.Create(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer f.Close()
|
|
|
|
if err := Generate(f, testDashboardData()); err != nil {
|
|
t.Fatalf("Generate to file: %v", err)
|
|
}
|
|
f.Close()
|
|
|
|
info, err := os.Stat(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if info.Size() < 1000 {
|
|
t.Errorf("report file too small: %d bytes", info.Size())
|
|
}
|
|
}
|