package handlers import ( "context" "crypto/rand" "encoding/base64" "encoding/json" "log" "net/http" "time" "github.com/go-chi/chi/v5" "task-dashboard/internal/config" "task-dashboard/internal/models" "task-dashboard/internal/store" ) // ----------------------------------------------------------------------------- // Constants // ----------------------------------------------------------------------------- const ( AgentRequestExpiry = 5 * time.Minute AgentSessionTTL = 1 * time.Hour TokenBytes = 32 ) // Context key for agent session in request context type contextKey string const agentSessionContextKey contextKey = "agent_session" // ----------------------------------------------------------------------------- // Types // ----------------------------------------------------------------------------- // AgentRequestPayload is sent via WebSocket when an agent requests access type AgentRequestPayload struct { RequestToken string `json:"request_token"` AgentName string `json:"agent_name"` AgentID string `json:"agent_id"` TrustLevel models.AgentTrustLevel `json:"trust_level"` ExpiresAt time.Time `json:"expires_at"` } // agentContextItem is the JSON-serializable timeline item for agent context API type agentContextItem struct { ID string `json:"id"` Source string `json:"source"` Type string `json:"type"` Title string `json:"title"` Description string `json:"description,omitempty"` Due *time.Time `json:"due,omitempty"` Priority int `json:"priority,omitempty"` Completable bool `json:"completable"` URL string `json:"url,omitempty"` DaySection string `json:"day_section,omitempty"` // "overdue", "today", "tomorrow", "later" } // agentCompletedItem represents a completed task in the log type agentCompletedItem struct { Source string `json:"source"` Title string `json:"title"` DueDate *time.Time `json:"due_date,omitempty"` CompletedAt time.Time `json:"completed_at"` } // ----------------------------------------------------------------------------- // Helpers // ----------------------------------------------------------------------------- // generateToken creates a cryptographically random token func generateToken() (string, error) { b := make([]byte, TokenBytes) if _, err := rand.Read(b); err != nil { return "", err } return base64.URLEncoding.EncodeToString(b), nil } // isSessionExpired checks if a pending session has expired func isSessionExpired(session *models.AgentSession) bool { return time.Now().After(session.ExpiresAt) && session.Status == "pending" } // timelineItemToAgentItem converts a TimelineItem to the agent API format func timelineItemToAgentItem(item models.TimelineItem) agentContextItem { t := item.Time return agentContextItem{ ID: item.ID, Source: item.Source, Type: string(item.Type), Title: item.Title, Description: item.Description, Due: &t, Completable: item.Type == models.TimelineItemTypeTask || item.Type == models.TimelineItemTypeCard || item.Type == models.TimelineItemTypeGTask, URL: item.URL, DaySection: string(item.DaySection), } } // renderAgentTemplate renders an agent template with common error handling func (h *Handler) renderAgentTemplate(w http.ResponseWriter, templateName string, data interface{}) { w.Header().Set("Content-Type", "text/html; charset=utf-8") if h.renderer == nil { h.renderAgentError(w, "Renderer not configured", http.StatusInternalServerError) return } if err := h.renderer.Render(w, templateName, data); err != nil { h.renderAgentError(w, "Template error", http.StatusInternalServerError) } } // ----------------------------------------------------------------------------- // Auth Handlers // ----------------------------------------------------------------------------- // HandleAgentAuthRequest handles POST /agent/auth/request func (h *Handler) HandleAgentAuthRequest(w http.ResponseWriter, r *http.Request) { var req models.AgentAuthRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) return } if req.Name == "" || req.AgentID == "" { http.Error(w, "name and agent_id are required", http.StatusBadRequest) return } // Invalidate any previous sessions for this agent if err := h.store.InvalidatePreviousAgentSessions(req.AgentID); err != nil { http.Error(w, "Internal server error", http.StatusInternalServerError) return } // Generate request token requestToken, err := generateToken() if err != nil { http.Error(w, "Failed to generate token", http.StatusInternalServerError) return } // Create pending session session := &models.AgentSession{ RequestToken: requestToken, AgentName: req.Name, AgentID: req.AgentID, ExpiresAt: time.Now().Add(AgentRequestExpiry), } if err := h.store.CreateAgentSession(session); err != nil { http.Error(w, "Failed to create session", http.StatusInternalServerError) return } // Check trust level for WebSocket notification trustLevel, err := h.store.CheckAgentTrust(req.Name, req.AgentID) if err != nil { trustLevel = models.AgentTrustNew } // Broadcast to connected browsers via WebSocket h.BroadcastAgentRequest(session, trustLevel) // Return response w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(models.AgentAuthResponse{ RequestToken: requestToken, Status: "pending", }) } // HandleAgentAuthPoll handles GET /agent/auth/poll func (h *Handler) HandleAgentAuthPoll(w http.ResponseWriter, r *http.Request) { token := r.URL.Query().Get("token") if token == "" { http.Error(w, "token parameter required", http.StatusBadRequest) return } session, err := h.store.GetAgentSessionByRequestToken(token) if err != nil { http.Error(w, "Internal server error", http.StatusInternalServerError) return } if session == nil { http.Error(w, "Session not found", http.StatusNotFound) return } if isSessionExpired(session) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(models.AgentPollResponse{Status: "expired"}) return } resp := models.AgentPollResponse{Status: session.Status} if session.Status == "approved" && session.SessionToken != "" { resp.SessionToken = session.SessionToken resp.ExpiresAt = session.SessionExpiresAt } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(resp) } // HandleAgentAuthApprove handles POST /agent/auth/approve (browser auth required) func (h *Handler) HandleAgentAuthApprove(w http.ResponseWriter, r *http.Request) { var req struct { RequestToken string `json:"request_token"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) return } if req.RequestToken == "" { http.Error(w, "request_token required", http.StatusBadRequest) return } // Verify session exists and is pending session, err := h.store.GetAgentSessionByRequestToken(req.RequestToken) if err != nil { http.Error(w, "Internal server error", http.StatusInternalServerError) return } if session == nil { http.Error(w, "Session not found", http.StatusNotFound) return } if session.Status != "pending" { http.Error(w, "Session already processed", http.StatusConflict) return } if time.Now().After(session.ExpiresAt) { http.Error(w, "Session expired", http.StatusGone) return } // Generate session token sessionToken, err := generateToken() if err != nil { http.Error(w, "Failed to generate session token", http.StatusInternalServerError) return } sessionExpiresAt := time.Now().Add(AgentSessionTTL) // Approve the session if err := h.store.ApproveAgentSession(req.RequestToken, sessionToken, sessionExpiresAt); err != nil { http.Error(w, "Failed to approve session", http.StatusInternalServerError) return } // Register/update agent in the trusted agents table if err := h.store.CreateOrUpdateAgent(session.AgentName, session.AgentID); err != nil { // Don't fail - the session was approved; this just affects future trust level checks log.Printf("warning: failed to register agent %q: %v", session.AgentName, err) } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{"status": "approved"}) } // HandleAgentAuthDeny handles POST /agent/auth/deny (browser auth required) func (h *Handler) HandleAgentAuthDeny(w http.ResponseWriter, r *http.Request) { var req struct { RequestToken string `json:"request_token"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) return } if req.RequestToken == "" { http.Error(w, "request_token required", http.StatusBadRequest) return } if err := h.store.DenyAgentSession(req.RequestToken); err != nil { http.Error(w, "Failed to deny session", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{"status": "denied"}) } // ----------------------------------------------------------------------------- // Context Handlers // ----------------------------------------------------------------------------- // HandleAgentContext handles GET /agent/context (agent auth required) func (h *Handler) HandleAgentContext(w http.ResponseWriter, r *http.Request) { session := r.Context().Value(agentSessionContextKey).(*models.AgentSession) _ = h.store.UpdateAgentLastSeen(session.AgentID) now := config.Now() today := config.Today() // Extend range: 7 days back (overdue) to 14 days forward startDate := today.Add(-7 * 24 * time.Hour) endDate := today.Add(14 * 24 * time.Hour) ctx := r.Context() timeline := h.buildAgentContext(ctx, startDate, endDate) completedLog := h.buildCompletedLog(50) // Last 50 completed tasks resp := map[string]interface{}{ "generated_at": now.Format(time.RFC3339), "range": map[string]string{ "start": startDate.Format("2006-01-02"), "end": endDate.Format("2006-01-02"), }, "timeline": timeline, "completed_log": completedLog, "summary": h.buildContextSummary(timeline, today), } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(resp) } // buildAgentContext builds the context timeline by reusing BuildTimeline func (h *Handler) buildAgentContext(ctx context.Context, start, end time.Time) []agentContextItem { // Reuse the main BuildTimeline function (excludes live API calls for Google services) timelineItems, err := BuildTimeline(ctx, h.store, start, end) if err != nil { return nil } // Convert to agent API format, filtering completed items var items []agentContextItem for _, item := range timelineItems { if item.IsCompleted { continue } items = append(items, timelineItemToAgentItem(item)) } return items } // buildContextSummary builds summary statistics for the agent context func (h *Handler) buildContextSummary(items []agentContextItem, today time.Time) map[string]interface{} { bySource := make(map[string]int) bySection := make(map[string]int) endOfToday := today.Add(24 * time.Hour) for _, item := range items { bySource[item.Source]++ if item.DaySection != "" { bySection[item.DaySection]++ } else if item.Due != nil { if item.Due.Before(today) { bySection["overdue"]++ } else if item.Due.Before(endOfToday) { bySection["today"]++ } } } return map[string]interface{}{ "total_items": len(items), "by_source": bySource, "by_section": bySection, } } // buildCompletedLog retrieves recently completed tasks func (h *Handler) buildCompletedLog(limit int) []agentCompletedItem { completed, err := h.store.GetCompletedTasks(limit) if err != nil { return nil } items := make([]agentCompletedItem, len(completed)) for i, c := range completed { items[i] = agentCompletedItem{ Source: c.Source, Title: c.Title, DueDate: c.DueDate, CompletedAt: c.CompletedAt, } } return items } // ----------------------------------------------------------------------------- // Write Handlers // ----------------------------------------------------------------------------- // HandleAgentTaskComplete handles POST /agent/tasks/{id}/complete func (h *Handler) HandleAgentTaskComplete(w http.ResponseWriter, r *http.Request) { h.handleAgentTaskToggle(w, r, true) } // HandleAgentTaskUncomplete handles POST /agent/tasks/{id}/uncomplete func (h *Handler) HandleAgentTaskUncomplete(w http.ResponseWriter, r *http.Request) { h.handleAgentTaskToggle(w, r, false) } // handleAgentTaskToggle handles both complete and uncomplete operations for agents func (h *Handler) handleAgentTaskToggle(w http.ResponseWriter, r *http.Request, complete bool) { id := chi.URLParam(r, "id") source := r.URL.Query().Get("source") if id == "" || source == "" { http.Error(w, "id and source are required", http.StatusBadRequest) return } var err error ctx := r.Context() switch source { case "todoist": if complete { err = h.todoistClient.CompleteTask(ctx, id) } else { err = h.todoistClient.ReopenTask(ctx, id) } case "trello": err = h.trelloClient.UpdateCard(ctx, id, map[string]interface{}{"closed": complete}) case "gtasks": listID := r.URL.Query().Get("listId") if listID == "" { listID = "@default" } if h.googleTasksClient != nil { if complete { err = h.googleTasksClient.CompleteTask(ctx, listID, id) } else { err = h.googleTasksClient.UncompleteTask(ctx, listID, id) } } else { http.Error(w, "Google Tasks not configured", http.StatusServiceUnavailable) return } default: http.Error(w, "Unknown source: "+source, http.StatusBadRequest) return } if err != nil { http.Error(w, "Failed to toggle task: "+err.Error(), http.StatusInternalServerError) return } if complete { title, dueDate := h.getAtomDetails(id, source) _ = h.store.SaveCompletedTask(source, id, title, dueDate) switch source { case "todoist": _ = h.store.DeleteTask(id) case "trello": _ = h.store.DeleteCard(id) } } else { switch source { case "todoist": _ = h.store.InvalidateCache(store.CacheKeyTodoistTasks) case "trello": _ = h.store.InvalidateCache(store.CacheKeyTrelloBoards) } } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{"status": "success"}) } // HandleAgentTaskUpdateDue handles PATCH /agent/tasks/{id}/due func (h *Handler) HandleAgentTaskUpdateDue(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") source := r.URL.Query().Get("source") var req struct { Due *time.Time `json:"due"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) return } var err error ctx := r.Context() switch source { case "todoist": err = h.todoistClient.UpdateTask(ctx, id, map[string]interface{}{"due_datetime": req.Due}) case "trello": err = h.trelloClient.UpdateCard(ctx, id, map[string]interface{}{"due": req.Due}) default: http.Error(w, "Source does not support due date updates via this endpoint", http.StatusBadRequest) return } if err != nil { http.Error(w, "Failed to update due date: "+err.Error(), http.StatusInternalServerError) return } // Invalidate cache switch source { case "todoist": _ = h.store.InvalidateCache(store.CacheKeyTodoistTasks) case "trello": _ = h.store.InvalidateCache(store.CacheKeyTrelloBoards) } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{"status": "success"}) } // HandleAgentTaskUpdate handles PATCH /agent/tasks/{id} func (h *Handler) HandleAgentTaskUpdate(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") source := r.URL.Query().Get("source") var updates map[string]interface{} if err := json.NewDecoder(r.Body).Decode(&updates); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) return } var err error ctx := r.Context() switch source { case "todoist": // Map generic updates to Todoist specific ones if needed if title, ok := updates["title"].(string); ok { updates["content"] = title delete(updates, "title") } err = h.todoistClient.UpdateTask(ctx, id, updates) case "trello": if title, ok := updates["title"].(string); ok { updates["name"] = title delete(updates, "title") } if desc, ok := updates["description"].(string); ok { updates["desc"] = desc delete(updates, "description") } err = h.trelloClient.UpdateCard(ctx, id, updates) default: http.Error(w, "Source does not support updates via this endpoint", http.StatusBadRequest) return } if err != nil { http.Error(w, "Failed to update task: "+err.Error(), http.StatusInternalServerError) return } // Invalidate cache switch source { case "todoist": _ = h.store.InvalidateCache(store.CacheKeyTodoistTasks) case "trello": _ = h.store.InvalidateCache(store.CacheKeyTrelloBoards) } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{"status": "success"}) } // HandleAgentTaskCreate handles POST /agent/tasks func (h *Handler) HandleAgentTaskCreate(w http.ResponseWriter, r *http.Request) { var req struct { Title string `json:"title"` Source string `json:"source"` DueDate *time.Time `json:"due_date"` ProjectID string `json:"project_id"` ListID string `json:"list_id"` Priority int `json:"priority"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) return } if req.Title == "" || req.Source == "" { http.Error(w, "title and source are required", http.StatusBadRequest) return } var err error ctx := r.Context() switch req.Source { case "todoist": _, err = h.todoistClient.CreateTask(ctx, req.Title, req.ProjectID, req.DueDate, req.Priority) _ = h.store.InvalidateCache(store.CacheKeyTodoistTasks) case "trello": if req.ListID == "" { http.Error(w, "list_id is required for Trello", http.StatusBadRequest) return } _, err = h.trelloClient.CreateCard(ctx, req.ListID, req.Title, "", req.DueDate) _ = h.store.InvalidateCache(store.CacheKeyTrelloBoards) default: http.Error(w, "Unsupported source for task creation", http.StatusBadRequest) return } if err != nil { http.Error(w, "Failed to create task: "+err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{"status": "success"}) } // HandleAgentShoppingAdd handles POST /agent/shopping func (h *Handler) HandleAgentShoppingAdd(w http.ResponseWriter, r *http.Request) { var req struct { Name string `json:"name"` Store string `json:"store"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) return } if req.Name == "" || req.Store == "" { http.Error(w, "name and store are required", http.StatusBadRequest) return } if err := h.store.SaveUserShoppingItem(req.Name, req.Store); err != nil { http.Error(w, "Failed to save shopping item: "+err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{"status": "success"}) } // HandleDeleteAgent handles DELETE /settings/agents/{id} func (h *Handler) HandleDeleteAgent(w http.ResponseWriter, r *http.Request) { agentID := chi.URLParam(r, "id") if agentID == "" { http.Error(w, "agent ID is required", http.StatusBadRequest) return } if err := h.store.RevokeAgent(agentID); err != nil { http.Error(w, "Failed to revoke agent: "+err.Error(), http.StatusInternalServerError) return } // Also invalidate their sessions _ = h.store.InvalidatePreviousAgentSessions(agentID) w.WriteHeader(http.StatusOK) } // ----------------------------------------------------------------------------- // Middleware // ----------------------------------------------------------------------------- // AgentAuthMiddleware verifies agent session token func (h *Handler) AgentAuthMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { authHeader := r.Header.Get("Authorization") if authHeader == "" || len(authHeader) < 8 || authHeader[:7] != "Bearer " { http.Error(w, "Authorization header required", http.StatusUnauthorized) return } token := authHeader[7:] session, err := h.store.GetAgentSessionBySessionToken(token) if err != nil || session == nil { http.Error(w, "Invalid session token", http.StatusUnauthorized) return } // Check session expiry if session.SessionExpiresAt != nil && time.Now().After(*session.SessionExpiresAt) { http.Error(w, "Session expired", http.StatusUnauthorized) return } // Add session to context ctx := context.WithValue(r.Context(), agentSessionContextKey, session) next.ServeHTTP(w, r.WithContext(ctx)) }) } // ----------------------------------------------------------------------------- // Web Handlers (HTML pages for browser-only agents) // ----------------------------------------------------------------------------- // HandleAgentWebRequest handles GET /agent/web/request for browser-only agents func (h *Handler) HandleAgentWebRequest(w http.ResponseWriter, r *http.Request) { name := r.URL.Query().Get("name") agentID := r.URL.Query().Get("agent_id") if name == "" || agentID == "" { h.renderAgentError(w, "name and agent_id query parameters are required", http.StatusBadRequest) return } // Check for existing pending session existingSession, err := h.store.GetPendingAgentSessionByAgentID(agentID) if err != nil { h.renderAgentError(w, "Internal server error", http.StatusInternalServerError) return } if existingSession != nil { // Return existing pending session h.renderAgentRequest(w, existingSession) return } // Invalidate any previous sessions for this agent if err := h.store.InvalidatePreviousAgentSessions(agentID); err != nil { h.renderAgentError(w, "Internal server error", http.StatusInternalServerError) return } // Generate request token requestToken, err := generateToken() if err != nil { h.renderAgentError(w, "Failed to generate token", http.StatusInternalServerError) return } // Create pending session session := &models.AgentSession{ RequestToken: requestToken, AgentName: name, AgentID: agentID, ExpiresAt: time.Now().Add(AgentRequestExpiry), } if err := h.store.CreateAgentSession(session); err != nil { h.renderAgentError(w, "Failed to create session", http.StatusInternalServerError) return } // Check trust level for WebSocket notification trustLevel, err := h.store.CheckAgentTrust(name, agentID) if err != nil { trustLevel = models.AgentTrustNew } // Broadcast to connected browsers via WebSocket h.BroadcastAgentRequest(session, trustLevel) h.renderAgentRequest(w, session) } // HandleAgentWebStatus handles GET /agent/web/status for browser-only agents // Returns HTML page with approval status and session token if approved func (h *Handler) HandleAgentWebStatus(w http.ResponseWriter, r *http.Request) { token := r.URL.Query().Get("token") if token == "" { h.renderAgentError(w, "token query parameter required", http.StatusBadRequest) return } session, err := h.store.GetAgentSessionByRequestToken(token) if err != nil { h.renderAgentError(w, "Internal server error", http.StatusInternalServerError) return } if session == nil { h.renderAgentError(w, "Session not found", http.StatusNotFound) return } h.renderAgentStatus(w, session) } // HandleAgentWebContext handles GET /agent/web/context for browser-only agents func (h *Handler) HandleAgentWebContext(w http.ResponseWriter, r *http.Request) { sessionToken := r.URL.Query().Get("session") if sessionToken == "" { h.renderAgentError(w, "session query parameter required", http.StatusBadRequest) return } session, err := h.store.GetAgentSessionBySessionToken(sessionToken) if err != nil || session == nil { h.renderAgentError(w, "Invalid session token", http.StatusUnauthorized) return } if session.SessionExpiresAt != nil && time.Now().After(*session.SessionExpiresAt) { h.renderAgentError(w, "Session expired", http.StatusUnauthorized) return } _ = h.store.UpdateAgentLastSeen(session.AgentID) now := config.Now() today := config.Today() startDate := today.Add(-7 * 24 * time.Hour) endDate := today.Add(14 * 24 * time.Hour) ctx := r.Context() timeline := h.buildAgentContext(ctx, startDate, endDate) completedLog := h.buildCompletedLog(50) h.renderAgentContextFull(w, session, timeline, completedLog, today, startDate, endDate, now) } // renderAgentError renders an error page for agent web endpoints func (h *Handler) renderAgentError(w http.ResponseWriter, message string, status int) { w.Header().Set("Content-Type", "text/html; charset=utf-8") w.WriteHeader(status) if h.renderer != nil { _ = h.renderer.Render(w, "agent-error.html", map[string]interface{}{ "Error": message, "Status": status, }) } else { // Fallback if renderer not configured w.Write([]byte(`Error

Error

` + message + `

`)) } } // renderAgentRequest renders the request page with token info func (h *Handler) renderAgentRequest(w http.ResponseWriter, session *models.AgentSession) { h.renderAgentTemplate(w, "agent-request.html", map[string]interface{}{ "RequestToken": session.RequestToken, "AgentName": session.AgentName, "AgentID": session.AgentID, "Status": "pending", "PollURL": "/agent/web/status?token=" + session.RequestToken, "ExpiresAt": session.ExpiresAt.Format(time.RFC3339), }) } // renderAgentStatus renders the status page func (h *Handler) renderAgentStatus(w http.ResponseWriter, session *models.AgentSession) { status := session.Status if isSessionExpired(session) { status = "expired" } data := map[string]interface{}{ "RequestToken": session.RequestToken, "AgentName": session.AgentName, "Status": status, } if status == "approved" && session.SessionToken != "" { data["SessionToken"] = session.SessionToken data["ContextURL"] = "/agent/web/context?session=" + session.SessionToken if session.SessionExpiresAt != nil { data["SessionExpiresAt"] = session.SessionExpiresAt.Format(time.RFC3339) } } h.renderAgentTemplate(w, "agent-status.html", data) } // renderAgentContextFull renders the context page with full timeline data and completed log func (h *Handler) renderAgentContextFull(w http.ResponseWriter, session *models.AgentSession, timeline []agentContextItem, completedLog []agentCompletedItem, today, startDate, endDate, now time.Time) { // Separate today's items for calendar view var todayItems []agentContextItem var otherItems []agentContextItem endOfToday := today.Add(24 * time.Hour) for _, item := range timeline { if item.Due != nil && !item.Due.Before(today) && item.Due.Before(endOfToday) { todayItems = append(todayItems, item) } else { otherItems = append(otherItems, item) } } h.renderAgentTemplate(w, "agent-context.html", map[string]interface{}{ "AgentName": session.AgentName, "GeneratedAt": now.Format(time.RFC3339), "Today": today.Format("2006-01-02"), "RangeStart": startDate.Format("2006-01-02"), "RangeEnd": endDate.Format("2006-01-02"), "TodayItems": todayItems, "Timeline": otherItems, "CompletedLog": completedLog, "Summary": h.buildContextSummary(timeline, today), }) }