Files
HeatGuard/internal/store/warning.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

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()
}