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 }