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
97 lines
3.3 KiB
Go
97 lines
3.3 KiB
Go
package store
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
type Forecast struct {
|
|
ID int64
|
|
ProfileID int64
|
|
Timestamp time.Time
|
|
TemperatureC *float64
|
|
HumidityPct *float64
|
|
WindSpeedMs *float64
|
|
CloudCoverPct *float64
|
|
PrecipitationMm *float64
|
|
SunshineMin *float64
|
|
PressureHpa *float64
|
|
DewPointC *float64
|
|
ApparentTempC *float64
|
|
Condition string
|
|
Source string
|
|
FetchedAt time.Time
|
|
}
|
|
|
|
func (s *Store) UpsertForecast(f *Forecast) error {
|
|
_, err := s.db.Exec(
|
|
`INSERT INTO forecasts (profile_id, timestamp, temperature_c, humidity_pct, wind_speed_ms, cloud_cover_pct, precipitation_mm, sunshine_min, pressure_hpa, dew_point_c, apparent_temp_c, condition, source)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
ON CONFLICT(profile_id, timestamp, source) DO UPDATE SET
|
|
temperature_c=excluded.temperature_c, humidity_pct=excluded.humidity_pct,
|
|
wind_speed_ms=excluded.wind_speed_ms, cloud_cover_pct=excluded.cloud_cover_pct,
|
|
precipitation_mm=excluded.precipitation_mm, sunshine_min=excluded.sunshine_min,
|
|
pressure_hpa=excluded.pressure_hpa, dew_point_c=excluded.dew_point_c,
|
|
apparent_temp_c=excluded.apparent_temp_c, condition=excluded.condition,
|
|
fetched_at=datetime('now')`,
|
|
f.ProfileID, f.Timestamp.Format(time.RFC3339),
|
|
f.TemperatureC, f.HumidityPct, f.WindSpeedMs, f.CloudCoverPct,
|
|
f.PrecipitationMm, f.SunshineMin, f.PressureHpa, f.DewPointC,
|
|
f.ApparentTempC, f.Condition, f.Source,
|
|
)
|
|
return err
|
|
}
|
|
|
|
func (s *Store) GetForecasts(profileID int64, from, to time.Time, source string) ([]Forecast, error) {
|
|
query := `SELECT id, profile_id, timestamp, temperature_c, humidity_pct, wind_speed_ms, cloud_cover_pct, precipitation_mm, sunshine_min, pressure_hpa, dew_point_c, apparent_temp_c, condition, source, fetched_at
|
|
FROM forecasts WHERE profile_id = ? AND timestamp >= ? AND timestamp <= ?`
|
|
args := []any{profileID, from.Format(time.RFC3339), to.Format(time.RFC3339)}
|
|
if source != "" {
|
|
query += ` AND source = ?`
|
|
args = append(args, source)
|
|
}
|
|
query += ` ORDER BY timestamp`
|
|
|
|
rows, err := s.db.Query(query, args...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var forecasts []Forecast
|
|
for rows.Next() {
|
|
var f Forecast
|
|
var ts, fetched string
|
|
if err := rows.Scan(&f.ID, &f.ProfileID, &ts, &f.TemperatureC, &f.HumidityPct, &f.WindSpeedMs, &f.CloudCoverPct, &f.PrecipitationMm, &f.SunshineMin, &f.PressureHpa, &f.DewPointC, &f.ApparentTempC, &f.Condition, &f.Source, &fetched); err != nil {
|
|
return nil, err
|
|
}
|
|
f.Timestamp, _ = time.Parse(time.RFC3339, ts)
|
|
f.FetchedAt, _ = time.Parse("2006-01-02 15:04:05", fetched)
|
|
forecasts = append(forecasts, f)
|
|
}
|
|
return forecasts, rows.Err()
|
|
}
|
|
|
|
func (s *Store) GetLastFetchTime(profileID int64, source string) (time.Time, error) {
|
|
var fetched string
|
|
err := s.db.QueryRow(
|
|
`SELECT MAX(fetched_at) FROM forecasts WHERE profile_id = ? AND source = ?`,
|
|
profileID, source,
|
|
).Scan(&fetched)
|
|
if err != nil || fetched == "" {
|
|
return time.Time{}, fmt.Errorf("no forecasts found")
|
|
}
|
|
t, _ := time.Parse("2006-01-02 15:04:05", fetched)
|
|
return t, nil
|
|
}
|
|
|
|
func (s *Store) CleanupOldForecasts(olderThan time.Time) (int64, error) {
|
|
res, err := s.db.Exec(
|
|
`DELETE FROM forecasts WHERE timestamp < ?`,
|
|
olderThan.Format(time.RFC3339),
|
|
)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return res.RowsAffected()
|
|
}
|