diff --git a/internal/web/web.go b/internal/web/web.go index 891d079..70af19f 100644 --- a/internal/web/web.go +++ b/internal/web/web.go @@ -50,6 +50,8 @@ func (s *Server) Router() *gin.Engine { r.POST("/commitment", s.handleCommitment) r.POST("/complete", s.handleComplete) r.POST("/end", s.handleEnd) + r.POST("/refocus", s.handleRefocus) + r.POST("/ontask", s.handleOnTask) return r } @@ -97,9 +99,10 @@ func (s *Server) handleCoach(c *gin.Context) { } type commitmentRequest struct { - NextAction string `json:"next_action"` - SuccessCondition string `json:"success_condition"` - TimeboxSecs int64 `json:"timebox_secs"` + NextAction string `json:"next_action"` + SuccessCondition string `json:"success_condition"` + TimeboxSecs int64 `json:"timebox_secs"` + AllowedWindowClasses []string `json:"allowed_window_classes"` } func (s *Server) handleCommitment(c *gin.Context) { @@ -108,7 +111,7 @@ func (s *Server) handleCommitment(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid json"}) return } - err := s.ctrl.StartManualCommitment(req.NextAction, req.SuccessCondition, time.Duration(req.TimeboxSecs)*time.Second, nil) + err := s.ctrl.StartManualCommitment(req.NextAction, req.SuccessCondition, time.Duration(req.TimeboxSecs)*time.Second, req.AllowedWindowClasses) if err == nil { s.armExpiry() } @@ -124,6 +127,14 @@ func (s *Server) handleEnd(c *gin.Context) { s.respond(c, s.ctrl.End()) } +func (s *Server) handleRefocus(c *gin.Context) { + s.respond(c, s.ctrl.Refocus()) +} + +func (s *Server) handleOnTask(c *gin.Context) { + s.respond(c, s.ctrl.OnTask()) +} + func (s *Server) handleEvents(c *gin.Context) { c.Header("Content-Type", "text/event-stream") c.Header("Cache-Control", "no-cache") diff --git a/internal/web/web_test.go b/internal/web/web_test.go index b504b5a..af31c5c 100644 --- a/internal/web/web_test.go +++ b/internal/web/web_test.go @@ -158,3 +158,34 @@ func TestCoachRouteUnavailableDegrades(t *testing.T) { t.Fatalf("want error status, got %+v", cv) } } + +func TestRefocusAndOnTaskOutsideActiveIs400(t *testing.T) { + s := newTestServer(t) + r := s.Router() + if w := post(t, r, "/refocus", ""); w.Code != http.StatusBadRequest { + t.Fatalf("refocus outside Active: want 400, got %d", w.Code) + } + if w := post(t, r, "/ontask", ""); w.Code != http.StatusBadRequest { + t.Fatalf("ontask outside Active: want 400, got %d", w.Code) + } +} + +func TestCommitmentCarriesAllowedClassesAndRoutesWork(t *testing.T) { + s := newTestServer(t) + r := s.Router() + _ = post(t, r, "/planning", "") + body := `{"next_action":"a","success_condition":"b","timebox_secs":1500,"allowed_window_classes":["code"]}` + if w := post(t, r, "/commitment", body); w.Code != http.StatusOK { + t.Fatalf("commitment: want 200, got %d (%s)", w.Code, w.Body.String()) + } + if got := s.ctrl.AllowedClassesForTest(); len(got) != 1 || got[0] != "code" { + t.Fatalf("commitment did not carry allowed classes: %#v", got) + } + // Now Active: overrides succeed. + if w := post(t, r, "/ontask", ""); w.Code != http.StatusOK { + t.Fatalf("ontask while Active: want 200, got %d", w.Code) + } + if w := post(t, r, "/refocus", ""); w.Code != http.StatusOK { + t.Fatalf("refocus while Active: want 200, got %d", w.Code) + } +}