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
104 lines
2.9 KiB
Go
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()
|
|
}
|