diff options
| author | Peter Stone <thepeterstone@gmail.com> | 2026-01-28 23:32:26 -1000 |
|---|---|---|
| committer | Peter Stone <thepeterstone@gmail.com> | 2026-01-28 23:32:26 -1000 |
| commit | d39220eac03fbc5b714bde989665ed1c92dd24a5 (patch) | |
| tree | 03e1985745043b9af6e7442ac21fdcd5d843146f /internal | |
| parent | 05b1930e04ac222d73ffb2f45c1b1febb69f893d (diff) | |
Expand agent context API with completed log and calendar view
- Add completed_tasks table to log task completions with title, due date,
and completion timestamp
- Extend agent context date range: 7 days back to 14 days forward
- Add completed_log to API response (last 50 completed tasks)
- Add day_section field to timeline items (overdue/today/tomorrow/later)
- Add calendar-style view for today's schedule (6am-10pm hourly grid)
- Add tabbed interface for Timeline vs Completed Log in HTML view
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/handlers/agent.go | 101 | ||||
| -rw-r--r-- | internal/handlers/handlers.go | 28 | ||||
| -rw-r--r-- | internal/models/types.go | 10 | ||||
| -rw-r--r-- | internal/store/sqlite.go | 52 |
4 files changed, 160 insertions, 31 deletions
diff --git a/internal/handlers/agent.go b/internal/handlers/agent.go index 6f47524..92f4ce8 100644 --- a/internal/handlers/agent.go +++ b/internal/handlers/agent.go @@ -51,6 +51,15 @@ type agentContextItem struct { 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"` } // ----------------------------------------------------------------------------- @@ -83,6 +92,7 @@ func timelineItemToAgentItem(item models.TimelineItem) agentContextItem { Due: &t, Completable: item.Type == models.TimelineItemTypeTask || item.Type == models.TimelineItemTypeCard || item.Type == models.TimelineItemTypeGTask, URL: item.URL, + DaySection: string(item.DaySection), } } @@ -285,10 +295,14 @@ func (h *Handler) HandleAgentContext(w http.ResponseWriter, r *http.Request) { _ = h.store.UpdateAgentLastSeen(session.AgentID) now := config.Now() - startDate := config.Today() - endDate := startDate.Add(7 * 24 * time.Hour) + 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) - timeline := h.buildAgentContext(r.Context(), startDate, endDate) + 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), @@ -296,8 +310,9 @@ func (h *Handler) HandleAgentContext(w http.ResponseWriter, r *http.Request) { "start": startDate.Format("2006-01-02"), "end": endDate.Format("2006-01-02"), }, - "timeline": timeline, - "summary": h.buildContextSummary(timeline, startDate), + "timeline": timeline, + "completed_log": completedLog, + "summary": h.buildContextSummary(timeline, today), } w.Header().Set("Content-Type", "application/json") @@ -326,16 +341,18 @@ func (h *Handler) buildAgentContext(ctx context.Context, start, end time.Time) [ // 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) - var overdue, todayCount int + bySection := make(map[string]int) endOfToday := today.Add(24 * time.Hour) for _, item := range items { bySource[item.Source]++ - if item.Due != nil { + if item.DaySection != "" { + bySection[item.DaySection]++ + } else if item.Due != nil { if item.Due.Before(today) { - overdue++ + bySection["overdue"]++ } else if item.Due.Before(endOfToday) { - todayCount++ + bySection["today"]++ } } } @@ -343,9 +360,27 @@ func (h *Handler) buildContextSummary(items []agentContextItem, today time.Time) return map[string]interface{}{ "total_items": len(items), "by_source": bySource, - "overdue": overdue, - "today": todayCount, + "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 } // ----------------------------------------------------------------------------- @@ -489,11 +524,15 @@ func (h *Handler) HandleAgentWebContext(w http.ResponseWriter, r *http.Request) _ = h.store.UpdateAgentLastSeen(session.AgentID) now := config.Now() - startDate := config.Today() - endDate := startDate.Add(7 * 24 * time.Hour) + today := config.Today() + startDate := today.Add(-7 * 24 * time.Hour) + endDate := today.Add(14 * 24 * time.Hour) - timeline := h.buildAgentContext(r.Context(), startDate, endDate) - h.renderAgentContext(w, session, timeline, startDate, endDate, now) + 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 @@ -547,14 +586,30 @@ func (h *Handler) renderAgentStatus(w http.ResponseWriter, session *models.Agent h.renderAgentTemplate(w, "agent-status.html", data) } -// renderAgentContext renders the context page with timeline data -func (h *Handler) renderAgentContext(w http.ResponseWriter, session *models.AgentSession, timeline []agentContextItem, startDate, endDate, now time.Time) { +// 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), - "RangeStart": startDate.Format("2006-01-02"), - "RangeEnd": endDate.Format("2006-01-02"), - "Timeline": timeline, - "Summary": h.buildContextSummary(timeline, startDate), + "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), }) } diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index 9fe1b2c..0e5edcc 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -673,8 +673,11 @@ func (h *Handler) handleAtomToggle(w http.ResponseWriter, r *http.Request, compl } if complete { - // Get task title before removing from cache - title := h.getAtomTitle(id, source) + // Get task details before removing from cache + title, dueDate := h.getAtomDetails(id, source) + + // Log to completed tasks + _ = h.store.SaveCompletedTask(source, id, title, dueDate) // Remove from local cache switch source { @@ -706,14 +709,14 @@ func (h *Handler) handleAtomToggle(w http.ResponseWriter, r *http.Request, compl } } -// getAtomTitle retrieves the title for a task/card/bug from the store -func (h *Handler) getAtomTitle(id, source string) string { +// getAtomDetails retrieves title and due date for a task/card/bug from the store +func (h *Handler) getAtomDetails(id, source string) (string, *time.Time) { switch source { case "todoist": if tasks, err := h.store.GetTasks(); err == nil { for _, t := range tasks { if t.ID == id { - return t.Content + return t.Content, t.DueDate } } } @@ -722,7 +725,7 @@ func (h *Handler) getAtomTitle(id, source string) string { for _, b := range boards { for _, c := range b.Cards { if c.ID == id { - return c.Name + return c.Name, c.DueDate } } } @@ -733,13 +736,22 @@ func (h *Handler) getAtomTitle(id, source string) string { if _, err := fmt.Sscanf(id, "bug-%d", &bugID); err == nil { for _, b := range bugs { if b.ID == bugID { - return b.Description + return b.Description, nil } } } } + case "gtasks": + // Google Tasks don't have local cache, return generic title + return "Google Task", nil } - return "Task" + return "Task", nil +} + +// getAtomTitle retrieves the title for a task/card/bug from the store (legacy) +func (h *Handler) getAtomTitle(id, source string) string { + title, _ := h.getAtomDetails(id, source) + return title } // HandleUnifiedAdd creates a task in Todoist or a card in Trello from the Quick Add form diff --git a/internal/models/types.go b/internal/models/types.go index 5214bf8..e28d985 100644 --- a/internal/models/types.go +++ b/internal/models/types.go @@ -197,3 +197,13 @@ const ( AgentTrustRecognized AgentTrustLevel = "recognized" AgentTrustSuspicious AgentTrustLevel = "suspicious" ) + +// CompletedTask represents a logged completed task +type CompletedTask struct { + ID int64 `json:"id"` + Source string `json:"source"` + SourceID string `json:"source_id"` + Title string `json:"title"` + DueDate *time.Time `json:"due_date,omitempty"` + CompletedAt time.Time `json:"completed_at"` +} diff --git a/internal/store/sqlite.go b/internal/store/sqlite.go index b324e9f..48bcae5 100644 --- a/internal/store/sqlite.go +++ b/internal/store/sqlite.go @@ -1141,3 +1141,55 @@ func (s *Store) CheckAgentTrust(name, agentID string) (models.AgentTrustLevel, e return models.AgentTrustNew, nil } + +// Completed tasks log + +// SaveCompletedTask logs a completed task +func (s *Store) SaveCompletedTask(source, sourceID, title string, dueDate *time.Time) error { + var dueDateStr sql.NullString + if dueDate != nil { + dueDateStr = sql.NullString{String: dueDate.Format(time.RFC3339), Valid: true} + } + _, err := s.db.Exec(` + INSERT OR REPLACE INTO completed_tasks (source, source_id, title, due_date, completed_at) + VALUES (?, ?, ?, ?, datetime('now', 'localtime')) + `, source, sourceID, title, dueDateStr) + return err +} + +// GetCompletedTasks retrieves recently completed tasks +func (s *Store) GetCompletedTasks(limit int) ([]models.CompletedTask, error) { + rows, err := s.db.Query(` + SELECT id, source, source_id, title, due_date, completed_at + FROM completed_tasks + ORDER BY completed_at DESC + LIMIT ? + `, limit) + if err != nil { + return nil, err + } + defer func() { _ = rows.Close() }() + + var tasks []models.CompletedTask + for rows.Next() { + var task models.CompletedTask + var dueDate sql.NullString + var completedAt string + + if err := rows.Scan(&task.ID, &task.Source, &task.SourceID, &task.Title, &dueDate, &completedAt); err != nil { + return nil, err + } + + if dueDate.Valid { + if t, err := time.Parse(time.RFC3339, dueDate.String); err == nil { + task.DueDate = &t + } + } + if t, err := time.Parse("2006-01-02 15:04:05", completedAt); err == nil { + task.CompletedAt = t + } + + tasks = append(tasks, task) + } + return tasks, rows.Err() +} |
