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
103 lines
3.1 KiB
Go
103 lines
3.1 KiB
Go
package report
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestGenerateSampleReport(t *testing.T) {
|
|
if os.Getenv("WRITE_SAMPLE") == "" {
|
|
t.Skip("set WRITE_SAMPLE=1 to generate sample report")
|
|
}
|
|
|
|
temps := []float64{22, 21, 20.5, 20, 19.5, 19.5, 20, 22, 24, 27, 30, 32, 34, 35.5, 37.2, 36.8, 35, 33, 31, 28, 26, 24.5, 23.5, 22.5}
|
|
|
|
data := DashboardData{
|
|
GeneratedAt: time.Now(),
|
|
ProfileName: "home — Berlin",
|
|
Date: "2025-07-15",
|
|
RiskLevel: "high",
|
|
PeakTempC: 37.2,
|
|
MinNightTempC: 19.5,
|
|
PoorNightCool: true,
|
|
Warnings: []WarningData{
|
|
{
|
|
Headline: "Amtliche WARNUNG vor HITZE",
|
|
Severity: "Severe",
|
|
Description: "Es tritt eine starke Wärmebelastung auf. Temperaturen bis 37°C erwartet.",
|
|
Instruction: "Trinken Sie ausreichend. Vermeiden Sie direkte Sonneneinstrahlung.",
|
|
Onset: "2025-07-15 11:00",
|
|
Expires: "2025-07-16 19:00",
|
|
},
|
|
},
|
|
RiskWindows: []RiskWindowData{
|
|
{StartHour: 10, EndHour: 18, PeakTempC: 37.2, Level: "high", Reason: "very hot daytime temperatures"},
|
|
},
|
|
RoomBudgets: []RoomBudgetData{
|
|
{
|
|
RoomName: "Office", InternalGainsW: 300, SolarGainW: 622, VentGainW: 150,
|
|
TotalGainW: 1072, TotalGainBTUH: 3658, ACCapacityBTUH: 8000, HeadroomBTUH: 4342, Status: "comfortable",
|
|
},
|
|
{
|
|
RoomName: "Bedroom", InternalGainsW: 100, SolarGainW: 200, VentGainW: 80,
|
|
TotalGainW: 380, TotalGainBTUH: 1297, ACCapacityBTUH: 0, HeadroomBTUH: -1297, Status: "overloaded",
|
|
},
|
|
},
|
|
CareChecklist: []string{
|
|
"Check elderly occupant (bedroom) at 10:00, 14:00, 18:00",
|
|
"Ensure water bottles are filled and accessible",
|
|
"Verify medication storage temperature",
|
|
},
|
|
}
|
|
|
|
for i, temp := range temps {
|
|
slot := TimelineSlotData{
|
|
Hour: i,
|
|
HourStr: fmt.Sprintf("%02d:00", i),
|
|
TempC: temp,
|
|
}
|
|
switch {
|
|
case temp >= 35:
|
|
slot.RiskLevel = "high"
|
|
slot.BudgetStatus = "marginal"
|
|
case temp >= 30:
|
|
slot.RiskLevel = "moderate"
|
|
slot.BudgetStatus = "comfortable"
|
|
default:
|
|
slot.RiskLevel = "low"
|
|
slot.BudgetStatus = "comfortable"
|
|
}
|
|
|
|
isDay := i >= 6 && i < 21
|
|
if i >= 11 && i <= 18 && temp >= 30 {
|
|
slot.Actions = append(slot.Actions, ActionData{Name: "Hydration reminder", Category: "hydration", Impact: "medium"})
|
|
}
|
|
if i >= 10 && i <= 18 && temp >= 30 {
|
|
slot.Actions = append(slot.Actions, ActionData{Name: "Check vulnerable occupants", Category: "care", Impact: "high"})
|
|
}
|
|
if i <= 10 && temp >= 25 {
|
|
slot.Actions = append(slot.Actions, ActionData{Name: "Close south-facing shutters", Category: "shading", Impact: "high"})
|
|
}
|
|
if i >= 6 && i <= 9 {
|
|
slot.Actions = append(slot.Actions, ActionData{Name: "Pre-cool rooms with AC", Category: "ac_strategy", Impact: "high"})
|
|
}
|
|
if !isDay && temp < 25 {
|
|
slot.Actions = append(slot.Actions, ActionData{Name: "Night ventilation", Category: "ventilation", Impact: "high"})
|
|
}
|
|
data.Timeline = append(data.Timeline, slot)
|
|
}
|
|
|
|
f, err := os.Create("/tmp/heatwave-sample-report.html")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer f.Close()
|
|
|
|
if err := Generate(f, data); err != nil {
|
|
t.Fatalf("Generate: %v", err)
|
|
}
|
|
t.Log("Sample report written to /tmp/heatwave-sample-report.html")
|
|
}
|