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
92 lines
2.9 KiB
Go
92 lines
2.9 KiB
Go
package store
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
type Warning struct {
|
|
ID int64
|
|
ProfileID int64
|
|
WarningID string
|
|
EventType string
|
|
Severity string
|
|
Headline string
|
|
Description string
|
|
Instruction string
|
|
Onset time.Time
|
|
Expires time.Time
|
|
FetchedAt time.Time
|
|
}
|
|
|
|
func (s *Store) UpsertWarning(w *Warning) error {
|
|
_, err := s.db.Exec(
|
|
`INSERT INTO warnings (profile_id, warning_id, event_type, severity, headline, description, instruction, onset, expires)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
ON CONFLICT(warning_id) DO UPDATE SET
|
|
severity=excluded.severity, headline=excluded.headline,
|
|
description=excluded.description, instruction=excluded.instruction,
|
|
onset=excluded.onset, expires=excluded.expires, fetched_at=datetime('now')`,
|
|
w.ProfileID, w.WarningID, w.EventType, w.Severity, w.Headline,
|
|
w.Description, w.Instruction,
|
|
w.Onset.Format(time.RFC3339), w.Expires.Format(time.RFC3339),
|
|
)
|
|
return err
|
|
}
|
|
|
|
func (s *Store) GetActiveWarnings(profileID int64, now time.Time) ([]Warning, error) {
|
|
rows, err := s.db.Query(
|
|
`SELECT id, profile_id, warning_id, event_type, severity, headline, description, instruction, onset, expires, fetched_at
|
|
FROM warnings WHERE profile_id = ? AND expires > ? ORDER BY onset`,
|
|
profileID, now.Format(time.RFC3339),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var warnings []Warning
|
|
for rows.Next() {
|
|
var w Warning
|
|
var onset, expires, fetched string
|
|
if err := rows.Scan(&w.ID, &w.ProfileID, &w.WarningID, &w.EventType, &w.Severity, &w.Headline, &w.Description, &w.Instruction, &onset, &expires, &fetched); err != nil {
|
|
return nil, err
|
|
}
|
|
w.Onset, _ = time.Parse(time.RFC3339, onset)
|
|
w.Expires, _ = time.Parse(time.RFC3339, expires)
|
|
w.FetchedAt, _ = time.Parse("2006-01-02 15:04:05", fetched)
|
|
warnings = append(warnings, w)
|
|
}
|
|
return warnings, rows.Err()
|
|
}
|
|
|
|
func (s *Store) GetWarning(warningID string) (*Warning, error) {
|
|
w := &Warning{}
|
|
var onset, expires, fetched string
|
|
err := s.db.QueryRow(
|
|
`SELECT id, profile_id, warning_id, event_type, severity, headline, description, instruction, onset, expires, fetched_at
|
|
FROM warnings WHERE warning_id = ?`, warningID,
|
|
).Scan(&w.ID, &w.ProfileID, &w.WarningID, &w.EventType, &w.Severity, &w.Headline, &w.Description, &w.Instruction, &onset, &expires, &fetched)
|
|
if err == sql.ErrNoRows {
|
|
return nil, fmt.Errorf("warning not found: %s", warningID)
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get warning: %w", err)
|
|
}
|
|
w.Onset, _ = time.Parse(time.RFC3339, onset)
|
|
w.Expires, _ = time.Parse(time.RFC3339, expires)
|
|
w.FetchedAt, _ = time.Parse("2006-01-02 15:04:05", fetched)
|
|
return w, nil
|
|
}
|
|
|
|
func (s *Store) CleanupExpiredWarnings(now time.Time) (int64, error) {
|
|
res, err := s.db.Exec(
|
|
`DELETE FROM warnings WHERE expires < ?`,
|
|
now.Format(time.RFC3339),
|
|
)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return res.RowsAffected()
|
|
}
|