Expensive bee

This commit is contained in:
2026-02-03 16:49:05 +02:00
parent d3f29e3927
commit cdcdf2c644
10 changed files with 376 additions and 50 deletions

View File

@@ -4,6 +4,7 @@ import (
"encoding/json"
"net/http"
"strconv"
"strings"
"woke/db"
)
@@ -12,13 +13,14 @@ type Notifier func()
// Server provides a REST API for alarm CRUD.
type Server struct {
store *db.Store
mux *http.ServeMux
notify Notifier
store *db.Store
mux *http.ServeMux
notify Notifier
password string
}
func New(store *db.Store, notify Notifier) *Server {
s := &Server{store: store, notify: notify}
func New(store *db.Store, password string, notify Notifier) *Server {
s := &Server{store: store, notify: notify, password: password}
s.mux = http.NewServeMux()
s.mux.HandleFunc("GET /api/alarms", s.listAlarms)
s.mux.HandleFunc("GET /api/alarms/{id}", s.getAlarm)
@@ -30,7 +32,18 @@ func New(store *db.Store, notify Notifier) *Server {
}
func (s *Server) Handler() http.Handler {
return s.mux
if s.password == "" {
return s.mux
}
// Wrap with auth middleware
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
auth := r.Header.Get("Authorization")
if !strings.HasPrefix(auth, "Bearer ") || auth[7:] != s.password {
writeError(w, http.StatusUnauthorized, "invalid or missing authorization")
return
}
s.mux.ServeHTTP(w, r)
})
}
type alarmRequest struct {

192
api/client.go Normal file
View File

@@ -0,0 +1,192 @@
package api
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"time"
"woke/db"
)
// Client implements db.AlarmStore via HTTP calls to a remote woke server.
type Client struct {
baseURL string
password string
http *http.Client
}
func NewClient(baseURL, password string) *Client {
return &Client{
baseURL: baseURL,
password: password,
http: &http.Client{Timeout: 10 * time.Second},
}
}
func (c *Client) do(method, path string, body any) (*http.Response, error) {
var reqBody *bytes.Buffer
if body != nil {
data, err := json.Marshal(body)
if err != nil {
return nil, err
}
reqBody = bytes.NewBuffer(data)
} else {
reqBody = &bytes.Buffer{}
}
req, err := http.NewRequest(method, c.baseURL+path, reqBody)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
if c.password != "" {
req.Header.Set("Authorization", "Bearer "+c.password)
}
return c.http.Do(req)
}
func (c *Client) ListAlarms() ([]db.Alarm, error) {
resp, err := c.do("GET", "/api/alarms", nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("server returned %d", resp.StatusCode)
}
var alarms []alarmResponse
if err := json.NewDecoder(resp.Body).Decode(&alarms); err != nil {
return nil, err
}
result := make([]db.Alarm, len(alarms))
for i, a := range alarms {
result[i] = a.toAlarm()
}
return result, nil
}
func (c *Client) GetAlarm(id int) (db.Alarm, error) {
resp, err := c.do("GET", fmt.Sprintf("/api/alarms/%d", id), nil)
if err != nil {
return db.Alarm{}, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return db.Alarm{}, fmt.Errorf("server returned %d", resp.StatusCode)
}
var a alarmResponse
if err := json.NewDecoder(resp.Body).Decode(&a); err != nil {
return db.Alarm{}, err
}
return a.toAlarm(), nil
}
func (c *Client) CreateAlarm(a db.Alarm) (int, error) {
req := alarmRequest{
Name: a.Name,
Description: a.Description,
Time: a.Time,
Trigger: a.Trigger,
SoundPath: a.SoundPath,
Enabled: &a.Enabled,
}
resp, err := c.do("POST", "/api/alarms", req)
if err != nil {
return 0, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusCreated {
return 0, fmt.Errorf("server returned %d", resp.StatusCode)
}
var created alarmResponse
if err := json.NewDecoder(resp.Body).Decode(&created); err != nil {
return 0, err
}
return created.ID, nil
}
func (c *Client) UpdateAlarm(a db.Alarm) error {
req := alarmRequest{
Name: a.Name,
Description: a.Description,
Time: a.Time,
Trigger: a.Trigger,
SoundPath: a.SoundPath,
Enabled: &a.Enabled,
}
resp, err := c.do("PUT", fmt.Sprintf("/api/alarms/%d", a.ID), req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("server returned %d", resp.StatusCode)
}
return nil
}
func (c *Client) DeleteAlarm(id int) error {
resp, err := c.do("DELETE", fmt.Sprintf("/api/alarms/%d", id), nil)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusNoContent {
return fmt.Errorf("server returned %d", resp.StatusCode)
}
return nil
}
func (c *Client) ToggleAlarm(id int) error {
resp, err := c.do("PATCH", fmt.Sprintf("/api/alarms/%d/toggle", id), nil)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("server returned %d", resp.StatusCode)
}
return nil
}
func (c *Client) MarkTriggered(id int) error {
// Client mode doesn't mark triggered — server handles this
return nil
}
func (a alarmResponse) toAlarm() db.Alarm {
alarm := db.Alarm{
ID: a.ID,
Name: a.Name,
Description: a.Description,
Time: a.Time,
Trigger: a.Trigger,
SoundPath: a.SoundPath,
Enabled: a.Enabled,
SnoozeCount: a.SnoozeCount,
}
if t, err := time.Parse("2006-01-02T15:04:05Z", a.CreatedAt); err == nil {
alarm.CreatedAt = t
}
if t, err := time.Parse("2006-01-02T15:04:05Z", a.UpdatedAt); err == nil {
alarm.UpdatedAt = t
}
if a.LastTriggered != nil {
if t, err := time.Parse("2006-01-02T15:04:05Z", *a.LastTriggered); err == nil {
alarm.LastTriggered = &t
}
}
return alarm
}