Files
HeatGuard/internal/cli/summary.go
vikingowl 1c9db02334 feat: add web UI with full CRUD setup page
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
2026-02-09 10:39:00 +01:00

118 lines
3.2 KiB
Go

package cli
import (
"context"
"fmt"
"time"
"github.com/cnachtigall/heatwave-autopilot/internal/heat"
"github.com/cnachtigall/heatwave-autopilot/internal/llm"
"github.com/cnachtigall/heatwave-autopilot/internal/risk"
"github.com/spf13/cobra"
)
var summaryDate string
func init() {
summaryCmd := &cobra.Command{
Use: "summary",
Short: "Generate a 3-bullet AI summary of the heat model",
RunE: func(cmd *cobra.Command, args []string) error {
p, err := getActiveProfile()
if err != nil {
return err
}
provider := getLLMProvider()
if provider.Name() == "none" {
return fmt.Errorf("LLM not configured. Set llm.provider in config or use --llm flag")
}
dateStr := summaryDate
if dateStr == "" {
dateStr = time.Now().Format("2006-01-02")
}
date, err := time.Parse("2006-01-02", dateStr)
if err != nil {
return fmt.Errorf("invalid date: %s", dateStr)
}
loc, _ := time.LoadLocation(p.Timezone)
from := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, loc)
to := from.Add(24 * time.Hour)
forecasts, err := db.GetForecasts(p.ID, from, to, "")
if err != nil || len(forecasts) == 0 {
return fmt.Errorf("no forecast data for %s", dateStr)
}
hourlyData := buildHourlyData(forecasts, loc)
dayRisk := risk.AnalyzeDay(hourlyData, risk.DefaultThresholds())
// Build heat sources from devices
devices, _ := db.ListAllDevices(p.ID)
var sources []llm.HeatSource
for _, d := range devices {
sources = append(sources, llm.HeatSource{
Name: d.Name,
Watts: d.WattsTypical * d.DutyCycle,
})
}
// AC headroom (simplified — sum all AC units)
acUnits, _ := db.ListACUnits(p.ID)
var totalACBTU float64
for _, ac := range acUnits {
totalACBTU += ac.CapacityBTU
}
var totalGainW float64
for _, s := range sources {
totalGainW += s.Watts
}
headroom := totalACBTU - heat.WattsToBTUH(totalGainW)
// Warnings
warnings, _ := db.GetActiveWarnings(p.ID, time.Now())
var warningStrs []string
for _, w := range warnings {
warningStrs = append(warningStrs, w.Headline)
}
// Risk windows
var riskWindows []llm.RiskWindowSummary
for _, w := range dayRisk.Windows {
riskWindows = append(riskWindows, llm.RiskWindowSummary{
StartHour: w.StartHour,
EndHour: w.EndHour,
PeakTempC: w.PeakTempC,
Level: w.Level.String(),
})
}
input := llm.SummaryInput{
Date: dateStr,
PeakTempC: dayRisk.PeakTempC,
MinNightTempC: dayRisk.MinNightTempC,
RiskLevel: dayRisk.Level.String(),
TopHeatSources: sources,
ACHeadroomBTUH: headroom,
BudgetStatus: heat.Comfortable.String(),
ActiveWarnings: warningStrs,
RiskWindows: riskWindows,
}
result, err := provider.Summarize(context.Background(), input)
if err != nil {
fmt.Fprintf(cmd.ErrOrStderr(), "LLM call failed: %v\nFalling back to raw data:\n", err)
fmt.Printf("Peak: %.1f°C | Night min: %.1f°C | Risk: %s\n", dayRisk.PeakTempC, dayRisk.MinNightTempC, dayRisk.Level)
return nil
}
fmt.Println(result)
return nil
},
}
summaryCmd.Flags().StringVar(&summaryDate, "date", "", "date (YYYY-MM-DD)")
rootCmd.AddCommand(summaryCmd)
}