From 9a87f9db5af943ea253b814d4196020341e3c2ba Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Sun, 12 Jul 2026 05:36:32 +0000 Subject: fix(widget): surface unmatched task IDs instead of silently no-opping CompleteNativeTask/UncompleteNativeTask/RescheduleNativeTask ran a plain UPDATE ... WHERE id = ? and returned whatever error Exec gave back -- which is nil even when 0 rows match, since that's not a SQL error. A stale or wrong id from the widget looked identical to a real completion: HTTP 200, nothing changed in the database. Real incident: 1 of 3 widget completeTask taps silently no-opped this way. Now checks RowsAffected() and returns ErrNativeTaskNotFound (mirrors the existing pattern in sqlite.go's ApproveAgentSession/DenyAgentSession). HandleWidgetComplete and HandleWidgetReschedule surface this as 404 instead of a fake 200, so the widget can tell 'nothing changed' apart from 'it worked.' --- internal/handlers/widget.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) (limited to 'internal/handlers/widget.go') diff --git a/internal/handlers/widget.go b/internal/handlers/widget.go index 876aaad..6c3b455 100644 --- a/internal/handlers/widget.go +++ b/internal/handlers/widget.go @@ -2,12 +2,14 @@ package handlers import ( "encoding/json" + "errors" "net/http" "strings" "time" "task-dashboard/internal/config" "task-dashboard/internal/models" + "task-dashboard/internal/store" ) // WidgetAuthMiddleware validates the static bearer token for widget API endpoints. @@ -126,6 +128,10 @@ func (h *Handler) HandleWidgetReschedule(w http.ResponseWriter, r *http.Request) tz := config.GetDisplayTimezone() dueDate := time.Date(parsed.Year(), parsed.Month(), parsed.Day(), 0, 0, 0, 0, tz) if err := h.store.RescheduleNativeTask(req.ID, dueDate); err != nil { + if errors.Is(err, store.ErrNativeTaskNotFound) { + http.Error(w, "task not found", http.StatusNotFound) + return + } http.Error(w, "failed to reschedule", http.StatusInternalServerError) return } @@ -143,6 +149,14 @@ func (h *Handler) HandleWidgetComplete(w http.ResponseWriter, r *http.Request) { switch req.Source { case "doot": if err := h.store.CompleteNativeTask(req.ID); err != nil { + if errors.Is(err, store.ErrNativeTaskNotFound) { + // Surfaced as 404 (not a silent 200) so the caller -- the + // Android widget -- can tell "nothing changed" apart from + // "it worked", instead of the previous behavior where a + // stale/wrong id looked identical to a real completion. + http.Error(w, "task not found", http.StatusNotFound) + return + } http.Error(w, "failed to complete task", http.StatusInternalServerError) return } -- cgit v1.2.3