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

109 lines
2.7 KiB
Go

package store
import (
"database/sql"
"fmt"
)
type Occupant struct {
ID int64
RoomID int64
Count int
ActivityLevel string
Vulnerable bool
}
func (s *Store) CreateOccupant(roomID int64, count int, activityLevel string, vulnerable bool) (*Occupant, error) {
if activityLevel == "" {
activityLevel = "sedentary"
}
vuln := 0
if vulnerable {
vuln = 1
}
res, err := s.db.Exec(
`INSERT INTO occupants (room_id, count, activity_level, vulnerable) VALUES (?, ?, ?, ?)`,
roomID, count, activityLevel, vuln,
)
if err != nil {
return nil, fmt.Errorf("create occupant: %w", err)
}
id, _ := res.LastInsertId()
return s.GetOccupant(id)
}
func (s *Store) GetOccupant(id int64) (*Occupant, error) {
o := &Occupant{}
var vuln int
err := s.db.QueryRow(
`SELECT id, room_id, count, activity_level, vulnerable FROM occupants WHERE id = ?`, id,
).Scan(&o.ID, &o.RoomID, &o.Count, &o.ActivityLevel, &vuln)
if err == sql.ErrNoRows {
return nil, fmt.Errorf("occupant not found: %d", id)
}
if err != nil {
return nil, fmt.Errorf("get occupant: %w", err)
}
o.Vulnerable = vuln != 0
return o, nil
}
func (s *Store) ListOccupants(roomID int64) ([]Occupant, error) {
rows, err := s.db.Query(
`SELECT id, room_id, count, activity_level, vulnerable FROM occupants WHERE room_id = ?`, roomID,
)
if err != nil {
return nil, err
}
defer rows.Close()
var occupants []Occupant
for rows.Next() {
var o Occupant
var vuln int
if err := rows.Scan(&o.ID, &o.RoomID, &o.Count, &o.ActivityLevel, &vuln); err != nil {
return nil, err
}
o.Vulnerable = vuln != 0
occupants = append(occupants, o)
}
return occupants, rows.Err()
}
func (s *Store) ListAllOccupants(profileID int64) ([]Occupant, error) {
rows, err := s.db.Query(
`SELECT o.id, o.room_id, o.count, o.activity_level, o.vulnerable
FROM occupants o JOIN rooms r ON o.room_id = r.id WHERE r.profile_id = ?`, profileID,
)
if err != nil {
return nil, err
}
defer rows.Close()
var occupants []Occupant
for rows.Next() {
var o Occupant
var vuln int
if err := rows.Scan(&o.ID, &o.RoomID, &o.Count, &o.ActivityLevel, &vuln); err != nil {
return nil, err
}
o.Vulnerable = vuln != 0
occupants = append(occupants, o)
}
return occupants, rows.Err()
}
func (s *Store) UpdateOccupant(id int64, field, value string) error {
allowed := map[string]bool{"count": true, "activity_level": true, "vulnerable": true}
if !allowed[field] {
return fmt.Errorf("invalid field: %s", field)
}
_, err := s.db.Exec(
fmt.Sprintf(`UPDATE occupants SET %s = ? WHERE id = ?`, field), value, id,
)
return err
}
func (s *Store) DeleteOccupant(id int64) error {
_, err := s.db.Exec(`DELETE FROM occupants WHERE id = ?`, id)
return err
}