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

104 lines
2.9 KiB
Go

package store
import (
"database/sql"
"fmt"
"time"
)
type Profile struct {
ID int64
Name string
Latitude float64
Longitude float64
Timezone string
CreatedAt time.Time
UpdatedAt time.Time
}
func (s *Store) CreateProfile(name string, lat, lon float64, tz string) (*Profile, error) {
if tz == "" {
tz = "Europe/Berlin"
}
res, err := s.db.Exec(
`INSERT INTO profiles (name, latitude, longitude, timezone) VALUES (?, ?, ?, ?)`,
name, lat, lon, tz,
)
if err != nil {
return nil, fmt.Errorf("create profile: %w", err)
}
id, _ := res.LastInsertId()
return s.GetProfile(id)
}
func (s *Store) GetProfile(id int64) (*Profile, error) {
p := &Profile{}
var created, updated string
err := s.db.QueryRow(
`SELECT id, name, latitude, longitude, timezone, created_at, updated_at FROM profiles WHERE id = ?`, id,
).Scan(&p.ID, &p.Name, &p.Latitude, &p.Longitude, &p.Timezone, &created, &updated)
if err == sql.ErrNoRows {
return nil, fmt.Errorf("profile not found: %d", id)
}
if err != nil {
return nil, fmt.Errorf("get profile: %w", err)
}
p.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", created)
p.UpdatedAt, _ = time.Parse("2006-01-02 15:04:05", updated)
return p, nil
}
func (s *Store) GetProfileByName(name string) (*Profile, error) {
p := &Profile{}
var created, updated string
err := s.db.QueryRow(
`SELECT id, name, latitude, longitude, timezone, created_at, updated_at FROM profiles WHERE name = ?`, name,
).Scan(&p.ID, &p.Name, &p.Latitude, &p.Longitude, &p.Timezone, &created, &updated)
if err == sql.ErrNoRows {
return nil, fmt.Errorf("profile not found: %s", name)
}
if err != nil {
return nil, fmt.Errorf("get profile by name: %w", err)
}
p.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", created)
p.UpdatedAt, _ = time.Parse("2006-01-02 15:04:05", updated)
return p, nil
}
func (s *Store) UpdateProfile(id int64, field, value string) error {
allowed := map[string]bool{"name": true, "latitude": true, "longitude": true, "timezone": true}
if !allowed[field] {
return fmt.Errorf("invalid field: %s", field)
}
_, err := s.db.Exec(
fmt.Sprintf(`UPDATE profiles SET %s = ?, updated_at = datetime('now') WHERE id = ?`, field),
value, id,
)
return err
}
func (s *Store) DeleteProfile(id int64) error {
_, err := s.db.Exec(`DELETE FROM profiles WHERE id = ?`, id)
return err
}
func (s *Store) ListProfiles() ([]Profile, error) {
rows, err := s.db.Query(`SELECT id, name, latitude, longitude, timezone, created_at, updated_at FROM profiles ORDER BY name`)
if err != nil {
return nil, err
}
defer rows.Close()
var profiles []Profile
for rows.Next() {
var p Profile
var created, updated string
if err := rows.Scan(&p.ID, &p.Name, &p.Latitude, &p.Longitude, &p.Timezone, &created, &updated); err != nil {
return nil, err
}
p.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", created)
p.UpdatedAt, _ = time.Parse("2006-01-02 15:04:05", updated)
profiles = append(profiles, p)
}
return profiles, rows.Err()
}