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

114 lines
3.4 KiB
Go

package store
import (
"database/sql"
"fmt"
"time"
)
type Device struct {
ID int64
RoomID int64
Name string
DeviceType string
WattsIdle float64
WattsTypical float64
WattsPeak float64
DutyCycle float64
Schedule string
CreatedAt time.Time
}
func (s *Store) CreateDevice(roomID int64, name, deviceType string, wattsIdle, wattsTypical, wattsPeak, dutyCycle float64) (*Device, error) {
if dutyCycle == 0 {
dutyCycle = 1.0
}
res, err := s.db.Exec(
`INSERT INTO devices (room_id, name, device_type, watts_idle, watts_typical, watts_peak, duty_cycle) VALUES (?, ?, ?, ?, ?, ?, ?)`,
roomID, name, deviceType, wattsIdle, wattsTypical, wattsPeak, dutyCycle,
)
if err != nil {
return nil, fmt.Errorf("create device: %w", err)
}
id, _ := res.LastInsertId()
return s.GetDevice(id)
}
func (s *Store) GetDevice(id int64) (*Device, error) {
d := &Device{}
var created string
err := s.db.QueryRow(
`SELECT id, room_id, name, device_type, watts_idle, watts_typical, watts_peak, duty_cycle, schedule, created_at FROM devices WHERE id = ?`, id,
).Scan(&d.ID, &d.RoomID, &d.Name, &d.DeviceType, &d.WattsIdle, &d.WattsTypical, &d.WattsPeak, &d.DutyCycle, &d.Schedule, &created)
if err == sql.ErrNoRows {
return nil, fmt.Errorf("device not found: %d", id)
}
if err != nil {
return nil, fmt.Errorf("get device: %w", err)
}
d.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", created)
return d, nil
}
func (s *Store) ListDevices(roomID int64) ([]Device, error) {
rows, err := s.db.Query(
`SELECT id, room_id, name, device_type, watts_idle, watts_typical, watts_peak, duty_cycle, schedule, created_at FROM devices WHERE room_id = ? ORDER BY name`, roomID,
)
if err != nil {
return nil, err
}
defer rows.Close()
var devices []Device
for rows.Next() {
var d Device
var created string
if err := rows.Scan(&d.ID, &d.RoomID, &d.Name, &d.DeviceType, &d.WattsIdle, &d.WattsTypical, &d.WattsPeak, &d.DutyCycle, &d.Schedule, &created); err != nil {
return nil, err
}
d.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", created)
devices = append(devices, d)
}
return devices, rows.Err()
}
func (s *Store) ListAllDevices(profileID int64) ([]Device, error) {
rows, err := s.db.Query(
`SELECT d.id, d.room_id, d.name, d.device_type, d.watts_idle, d.watts_typical, d.watts_peak, d.duty_cycle, d.schedule, d.created_at
FROM devices d JOIN rooms r ON d.room_id = r.id WHERE r.profile_id = ? ORDER BY d.name`, profileID,
)
if err != nil {
return nil, err
}
defer rows.Close()
var devices []Device
for rows.Next() {
var d Device
var created string
if err := rows.Scan(&d.ID, &d.RoomID, &d.Name, &d.DeviceType, &d.WattsIdle, &d.WattsTypical, &d.WattsPeak, &d.DutyCycle, &d.Schedule, &created); err != nil {
return nil, err
}
d.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", created)
devices = append(devices, d)
}
return devices, rows.Err()
}
func (s *Store) UpdateDevice(id int64, field, value string) error {
allowed := map[string]bool{
"name": true, "device_type": true, "watts_idle": true, "watts_typical": true,
"watts_peak": true, "duty_cycle": true, "schedule": true,
}
if !allowed[field] {
return fmt.Errorf("invalid field: %s", field)
}
_, err := s.db.Exec(
fmt.Sprintf(`UPDATE devices SET %s = ? WHERE id = ?`, field), value, id,
)
return err
}
func (s *Store) DeleteDevice(id int64) error {
_, err := s.db.Exec(`DELETE FROM devices WHERE id = ?`, id)
return err
}