summaryrefslogtreecommitdiff
path: root/internal/handlers/widget.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/handlers/widget.go')
-rw-r--r--internal/handlers/widget.go117
1 files changed, 117 insertions, 0 deletions
diff --git a/internal/handlers/widget.go b/internal/handlers/widget.go
new file mode 100644
index 0000000..cee2ec8
--- /dev/null
+++ b/internal/handlers/widget.go
@@ -0,0 +1,117 @@
+package handlers
+
+import (
+ "encoding/json"
+ "net/http"
+ "strings"
+ "time"
+
+ "task-dashboard/internal/api"
+ "task-dashboard/internal/models"
+)
+
+// WidgetAuthMiddleware validates the static bearer token for widget API endpoints.
+func WidgetAuthMiddleware(token string, next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ auth := r.Header.Get("Authorization")
+ if !strings.HasPrefix(auth, "Bearer ") || strings.TrimPrefix(auth, "Bearer ") != token {
+ http.Error(w, "unauthorized", http.StatusUnauthorized)
+ return
+ }
+ next.ServeHTTP(w, r)
+ })
+}
+
+// TimelineItemToWidgetItem converts a TimelineItem to a WidgetItem for the widget API.
+// Exported for testability.
+func TimelineItemToWidgetItem(item models.TimelineItem) models.WidgetItem {
+ wi := models.WidgetItem{
+ ID: item.ID,
+ Title: item.Title,
+ Source: item.Source,
+ IsAllDay: item.IsAllDay,
+ URL: item.URL,
+ }
+
+ switch item.Type {
+ case models.TimelineItemTypeEvent:
+ wi.Type = "event"
+ case models.TimelineItemTypeMeal:
+ wi.Type = "event"
+ default:
+ wi.Type = "task"
+ wi.Completable = item.Source == "todoist"
+ }
+
+ // Only populate Start/End for items with a real time (non-all-day, non-zero)
+ if !item.IsAllDay && !item.Time.IsZero() {
+ t := item.Time
+ wi.Start = &t
+ if item.EndTime != nil {
+ wi.End = item.EndTime
+ } else {
+ end := item.Time.Add(time.Hour)
+ wi.End = &end
+ }
+ }
+
+ return wi
+}
+
+// HandleWidgetGet returns today's timeline items as JSON for the Android widget.
+func (h *Handler) HandleWidgetGet(w http.ResponseWriter, r *http.Request) {
+ now := time.Now()
+ start := now.Truncate(24 * time.Hour)
+ end := start.Add(24 * time.Hour)
+
+ items, err := BuildTimeline(r.Context(), h.store, start, end)
+ if err != nil {
+ http.Error(w, "internal error", http.StatusInternalServerError)
+ return
+ }
+
+ widgetItems := make([]models.WidgetItem, 0, len(items))
+ for _, item := range items {
+ // Only include today's items
+ if item.DaySection == models.DaySectionToday || item.IsOverdue {
+ widgetItems = append(widgetItems, TimelineItemToWidgetItem(item))
+ }
+ }
+
+ resp := models.WidgetResponse{
+ Now: now,
+ Items: widgetItems,
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ _ = json.NewEncoder(w).Encode(resp)
+}
+
+type widgetCompleteRequest struct {
+ ID string `json:"id"`
+ Source string `json:"source"`
+}
+
+// HandleWidgetComplete proxies a task completion to the source API.
+func HandleWidgetComplete(todoistClient api.TodoistAPI) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ var req widgetCompleteRequest
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ http.Error(w, "bad request", http.StatusBadRequest)
+ return
+ }
+ if req.Source != "todoist" {
+ http.Error(w, "source not completable", http.StatusBadRequest)
+ return
+ }
+ if todoistClient == nil {
+ http.Error(w, "todoist not configured", http.StatusServiceUnavailable)
+ return
+ }
+ if err := todoistClient.CompleteTask(r.Context(), req.ID); err != nil {
+ http.Error(w, "failed to complete task", http.StatusInternalServerError)
+ return
+ }
+ w.WriteHeader(http.StatusOK)
+ }
+}