summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-07-12 10:34:34 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-07-12 10:34:34 +0000
commitdfb06e896b92003d219979cbb1476ed16b7734a3 (patch)
treee8ff6a8ad2e73172a6b352dfe9a12094ee2c27ef
parente1e632c1e57c8aa0940e7ff566a1c7ab267625c8 (diff)
feat(widget): add POST /api/widget/add for quick-add
-rw-r--r--cmd/dashboard/main.go1
-rw-r--r--internal/handlers/widget.go31
-rw-r--r--internal/handlers/widget_test.go60
3 files changed, 92 insertions, 0 deletions
diff --git a/cmd/dashboard/main.go b/cmd/dashboard/main.go
index b38b9e1..5414a9d 100644
--- a/cmd/dashboard/main.go
+++ b/cmd/dashboard/main.go
@@ -371,6 +371,7 @@ func main() {
r.With(widgetAuth).Get("/api/widget", h.HandleWidgetGet)
r.With(widgetAuth).Post("/api/widget/complete", h.HandleWidgetComplete)
r.With(widgetAuth).Post("/api/widget/reschedule", h.HandleWidgetReschedule)
+ r.With(widgetAuth).Post("/api/widget/add", h.HandleWidgetAdd)
} else {
log.Println("WIDGET_TOKEN not set — /api/widget disabled")
}
diff --git a/internal/handlers/widget.go b/internal/handlers/widget.go
index 3a30fcf..0c88c16 100644
--- a/internal/handlers/widget.go
+++ b/internal/handlers/widget.go
@@ -124,6 +124,10 @@ type widgetCompleteRequest struct {
Source string `json:"source"`
}
+type widgetAddRequest struct {
+ Title string `json:"title"`
+}
+
type widgetRescheduleRequest struct {
ID string `json:"id"`
Source string `json:"source"`
@@ -189,3 +193,30 @@ func (h *Handler) HandleWidgetComplete(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}
+
+// HandleWidgetAdd creates a new undated native task from the widget's quick-add sheet.
+func (h *Handler) HandleWidgetAdd(w http.ResponseWriter, r *http.Request) {
+ var req widgetAddRequest
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ http.Error(w, "bad request", http.StatusBadRequest)
+ return
+ }
+
+ title := strings.TrimSpace(req.Title)
+ if title == "" {
+ http.Error(w, "title is required", http.StatusBadRequest)
+ return
+ }
+
+ task := models.Task{
+ ID: newID(),
+ Content: title,
+ Priority: 1,
+ }
+ if err := h.store.CreateNativeTask(task); err != nil {
+ http.Error(w, "failed to create task", http.StatusInternalServerError)
+ return
+ }
+
+ w.WriteHeader(http.StatusOK)
+}
diff --git a/internal/handlers/widget_test.go b/internal/handlers/widget_test.go
index 14dd396..e4a1d16 100644
--- a/internal/handlers/widget_test.go
+++ b/internal/handlers/widget_test.go
@@ -313,6 +313,66 @@ func TestHandleWidgetComplete_UnknownID_Returns404(t *testing.T) {
}
}
+// TestHandleWidgetAdd_CreatesTask proves the quick-add feature: POSTing a
+// title to /api/widget/add creates an undated native task the same way the
+// web UI's HandleUnifiedAdd does, but via the widget's bearer-token JSON
+// API instead of a session-authenticated HTML form.
+func TestHandleWidgetAdd_CreatesTask(t *testing.T) {
+ s, cleanup := setupTestDB(t)
+ defer cleanup()
+ h := &Handler{store: s}
+ body := `{"title":"Buy milk"}`
+ req := httptest.NewRequest("POST", "/api/widget/add", strings.NewReader(body))
+ w := httptest.NewRecorder()
+ http.HandlerFunc(h.HandleWidgetAdd).ServeHTTP(w, req)
+
+ if w.Code != http.StatusOK {
+ t.Fatalf("expected 200, got %d", w.Code)
+ }
+
+ tasks, err := s.GetUndatedNativeTasks()
+ if err != nil {
+ t.Fatalf("failed to read back tasks: %v", err)
+ }
+ found := false
+ for _, task := range tasks {
+ if task.Content == "Buy milk" {
+ found = true
+ }
+ }
+ if !found {
+ t.Error("expected a task with content 'Buy milk' to have been created")
+ }
+}
+
+func TestHandleWidgetAdd_EmptyTitle_Returns400(t *testing.T) {
+ s, cleanup := setupTestDB(t)
+ defer cleanup()
+ h := &Handler{store: s}
+ body := `{"title":""}`
+ req := httptest.NewRequest("POST", "/api/widget/add", strings.NewReader(body))
+ w := httptest.NewRecorder()
+ http.HandlerFunc(h.HandleWidgetAdd).ServeHTTP(w, req)
+
+ if w.Code != http.StatusBadRequest {
+ t.Fatalf("expected 400, got %d", w.Code)
+ }
+}
+
+func TestHandleWidgetAdd_WhitespaceOnlyTitle_Returns400(t *testing.T) {
+ s, cleanup := setupTestDB(t)
+ defer cleanup()
+ h := &Handler{store: s}
+ body := `{"title":" "}`
+ req := httptest.NewRequest("POST", "/api/widget/add", strings.NewReader(body))
+ w := httptest.NewRecorder()
+ http.HandlerFunc(h.HandleWidgetAdd).ServeHTTP(w, req)
+
+ if w.Code != http.StatusBadRequest {
+ t.Fatalf("expected 400, got %d", w.Code)
+ }
+}
+
func TestWidgetAuthMiddleware_EmptyToken(t *testing.T) {
inner := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
h := WidgetAuthMiddleware("", inner)