summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-01-28 23:32:26 -1000
committerPeter Stone <thepeterstone@gmail.com>2026-01-28 23:32:26 -1000
commitd39220eac03fbc5b714bde989665ed1c92dd24a5 (patch)
tree03e1985745043b9af6e7442ac21fdcd5d843146f
parent05b1930e04ac222d73ffb2f45c1b1febb69f893d (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>
-rw-r--r--SESSION_STATE.md23
-rw-r--r--internal/handlers/agent.go101
-rw-r--r--internal/handlers/handlers.go28
-rw-r--r--internal/models/types.go10
-rw-r--r--internal/store/sqlite.go52
-rw-r--r--migrations/011_completed_tasks.sql12
-rw-r--r--web/templates/agent-context.html258
7 files changed, 402 insertions, 82 deletions
diff --git a/SESSION_STATE.md b/SESSION_STATE.md
index b4dd64d..cc637a7 100644
--- a/SESSION_STATE.md
+++ b/SESSION_STATE.md
@@ -8,14 +8,23 @@ Agent Context API - Refactored for simplicity and clarity
- Status: [REVIEW_READY] Phase 1 Complete + Browser-Only Agent Endpoints + Refactored
- Phase 2 (Write Operations) and Phase 3 (Create + Management) pending
-### Recent Refactoring (agent.go)
+### Recent Updates
+**Refactoring (agent.go):**
- Reused `BuildTimeline()` from timeline_logic.go instead of duplicating fetch logic
-- Removed ~60 lines of duplicate timeline building code
-- Added section headers for code organization: Constants, Types, Helpers, Auth Handlers, Context Handlers, Middleware, Web Handlers
-- Extracted `isSessionExpired()` helper to reduce duplication
-- Extracted `renderAgentTemplate()` helper to simplify template rendering
-- Moved `AgentRequestPayload` from websocket.go to agent.go (agent-specific type)
-- Used `config.Now()` and `config.Today()` for consistent timezone handling
+- Added section headers for code organization
+- Extracted helpers: `isSessionExpired()`, `renderAgentTemplate()`
+
+**Enhanced Context API:**
+- Extended date range: 7 days back (overdue) to 14 days forward
+- Added `completed_log` - tracks completed tasks with title, due date, completion date
+- Added `day_section` field to timeline items (overdue/today/tomorrow/later)
+- Calendar-style view for today (6am-10pm) in HTML template
+- Tabs for Timeline vs Completed Log in web view
+
+**New Database:**
+- `completed_tasks` table (migration 011) - logs completed task history
+- `SaveCompletedTask()` / `GetCompletedTasks()` store methods
+- `CompletedTask` model type
### Phase 1 Completed Items
- [x] Migration `migrations/010_agent_tables.sql` — agents and agent_sessions tables
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()
+}
diff --git a/migrations/011_completed_tasks.sql b/migrations/011_completed_tasks.sql
new file mode 100644
index 0000000..0b782e9
--- /dev/null
+++ b/migrations/011_completed_tasks.sql
@@ -0,0 +1,12 @@
+-- Completed tasks log
+CREATE TABLE IF NOT EXISTS completed_tasks (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ source TEXT NOT NULL, -- 'todoist', 'trello', 'gtasks', 'bug'
+ source_id TEXT NOT NULL, -- original ID from source
+ title TEXT NOT NULL,
+ due_date TEXT, -- original due date
+ completed_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')),
+ UNIQUE(source, source_id)
+);
+
+CREATE INDEX IF NOT EXISTS idx_completed_tasks_completed_at ON completed_tasks(completed_at);
diff --git a/web/templates/agent-context.html b/web/templates/agent-context.html
index 3a4778a..db618ba 100644
--- a/web/templates/agent-context.html
+++ b/web/templates/agent-context.html
@@ -5,16 +5,34 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Agent Context - {{.AgentName}}</title>
<style>
- body { font-family: system-ui, sans-serif; max-width: 800px; margin: 40px auto; padding: 20px; background: #1a1a2e; color: #eee; }
+ body { font-family: system-ui, sans-serif; max-width: 1000px; margin: 40px auto; padding: 20px; background: #1a1a2e; color: #eee; }
.card { background: #16213e; border-radius: 8px; padding: 24px; margin-bottom: 20px; }
h1 { color: #e94560; margin-top: 0; }
- h2 { color: #0f3460; font-size: 1.2em; margin-top: 24px; }
+ h2 { color: #4da6ff; font-size: 1.2em; margin-top: 24px; margin-bottom: 16px; }
.label { color: #888; font-size: 0.9em; margin-bottom: 4px; }
.value { font-family: monospace; background: #0f0f23; padding: 8px 12px; border-radius: 4px; word-break: break-all; margin-bottom: 16px; }
- .summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 12px; margin-bottom: 20px; }
+ .summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); gap: 12px; margin-bottom: 20px; }
.summary-item { background: #0f3460; padding: 12px; border-radius: 6px; text-align: center; }
.summary-value { font-size: 1.5em; font-weight: bold; color: #e94560; }
.summary-label { font-size: 0.8em; color: #888; }
+
+ /* Calendar styles */
+ .calendar { position: relative; border-left: 2px solid #333; margin-left: 60px; min-height: 640px; }
+ .calendar-hour { position: relative; height: 40px; border-bottom: 1px solid #222; }
+ .calendar-hour-label { position: absolute; left: -60px; top: -8px; font-size: 0.75em; color: #666; width: 50px; text-align: right; }
+ .calendar-item { position: absolute; left: 10px; right: 10px; background: #0f3460; border-radius: 4px; padding: 6px 10px; font-size: 0.85em; border-left: 3px solid; overflow: hidden; z-index: 1; }
+ .calendar-item:hover { z-index: 10; background: #1a4a80; }
+ .calendar-item.source-todoist { border-color: #e44332; }
+ .calendar-item.source-trello { border-color: #0079bf; }
+ .calendar-item.source-plantoeat { border-color: #5cb85c; }
+ .calendar-item.source-calendar { border-color: #9b59b6; }
+ .calendar-item.source-gtasks { border-color: #f39c12; }
+ .calendar-item-time { font-size: 0.75em; color: #888; }
+ .calendar-item-title { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+ .all-day { background: #1a1a2e; padding: 8px; margin-bottom: 10px; border-radius: 4px; }
+ .all-day-item { display: inline-block; background: #0f3460; padding: 4px 8px; border-radius: 4px; margin: 2px; font-size: 0.85em; border-left: 3px solid #5cb85c; }
+
+ /* Table styles */
table { width: 100%; border-collapse: collapse; margin-top: 16px; }
th, td { padding: 10px; text-align: left; border-bottom: 1px solid #333; }
th { background: #0f3460; color: #fff; }
@@ -23,36 +41,61 @@
.source-todoist { background: #e44332; color: #fff; }
.source-trello { background: #0079bf; color: #fff; }
.source-plantoeat { background: #5cb85c; color: #fff; }
- .due-overdue { color: #dc3545; }
- .due-today { color: #ffc107; }
- .due-future { color: #28a745; }
+ .source-calendar { background: #9b59b6; color: #fff; }
+ .source-gtasks { background: #f39c12; color: #fff; }
+ .section-overdue { color: #dc3545; }
+ .section-today { color: #ffc107; }
+ .section-tomorrow { color: #28a745; }
+ .section-later { color: #6c757d; }
a { color: #4da6ff; }
+ .completed-item { opacity: 0.7; }
+ .tabs { display: flex; gap: 4px; margin-bottom: 16px; }
+ .tab { padding: 8px 16px; background: #0f3460; border: none; color: #fff; cursor: pointer; border-radius: 4px 4px 0 0; }
+ .tab.active { background: #16213e; }
+ .tab-content { display: none; }
+ .tab-content.active { display: block; }
</style>
</head>
<body>
<script type="application/json" id="agent-data">
{
"generated_at": "{{.GeneratedAt}}",
+ "today": "{{.Today}}",
"range": {
"start": "{{.RangeStart}}",
"end": "{{.RangeEnd}}"
},
"summary": {
"total_items": {{with .Summary}}{{.total_items}}{{else}}0{{end}},
- "overdue": {{with .Summary}}{{.overdue}}{{else}}0{{end}},
- "today": {{with .Summary}}{{.today}}{{else}}0{{end}}
+ "by_section": {{with .Summary}}{{if .by_section}}{"overdue": {{if index .by_section "overdue"}}{{index .by_section "overdue"}}{{else}}0{{end}}, "today": {{if index .by_section "today"}}{{index .by_section "today"}}{{else}}0{{end}}, "tomorrow": {{if index .by_section "tomorrow"}}{{index .by_section "tomorrow"}}{{else}}0{{end}}}{{else}}{}{{end}}{{else}}{}{{end}}
},
+ "today_items": [{{range $i, $item := .TodayItems}}{{if $i}},{{end}}
+ {
+ "id": "{{$item.ID}}",
+ "source": "{{$item.Source}}",
+ "type": "{{$item.Type}}",
+ "title": "{{$item.Title}}",
+ "due": {{if $item.Due}}"{{$item.Due.Format "2006-01-02T15:04:05Z07:00"}}"{{else}}null{{end}},
+ "completable": {{$item.Completable}}
+ }{{end}}
+ ],
"timeline": [{{range $i, $item := .Timeline}}{{if $i}},{{end}}
{
"id": "{{$item.ID}}",
"source": "{{$item.Source}}",
"type": "{{$item.Type}}",
"title": "{{$item.Title}}",
- "description": "{{$item.Description}}",
"due": {{if $item.Due}}"{{$item.Due.Format "2006-01-02T15:04:05Z07:00"}}"{{else}}null{{end}},
- "priority": {{$item.Priority}},
- "completable": {{$item.Completable}},
- "url": "{{$item.URL}}"
+ "day_section": "{{$item.DaySection}}",
+ "completable": {{$item.Completable}}
+ }{{end}}
+ ],
+ "completed_log": [{{range $i, $item := .CompletedLog}}{{if $i}},{{end}}
+ {
+ "source": "{{$item.Source}}",
+ "title": "{{$item.Title}}",
+ "due_date": {{if $item.DueDate}}"{{$item.DueDate.Format "2006-01-02T15:04:05Z07:00"}}"{{else}}null{{end}},
+ "completed_at": "{{$item.CompletedAt.Format "2006-01-02T15:04:05Z07:00"}}"
}{{end}}
]
}
@@ -60,7 +103,7 @@
<div class="card">
<h1>Agent Context</h1>
- <p>Timeline data for <strong>{{.AgentName}}</strong></p>
+ <p>Full timeline data for <strong>{{.AgentName}}</strong></p>
<div class="label">Generated At</div>
<div class="value">{{.GeneratedAt}}</div>
@@ -76,46 +119,173 @@
<div class="summary-value">{{with .Summary}}{{.total_items}}{{else}}0{{end}}</div>
<div class="summary-label">Total Items</div>
</div>
+ {{with .Summary}}{{with .by_section}}
<div class="summary-item">
- <div class="summary-value">{{with .Summary}}{{.overdue}}{{else}}0{{end}}</div>
+ <div class="summary-value section-overdue">{{if index . "overdue"}}{{index . "overdue"}}{{else}}0{{end}}</div>
<div class="summary-label">Overdue</div>
</div>
<div class="summary-item">
- <div class="summary-value">{{with .Summary}}{{.today}}{{else}}0{{end}}</div>
- <div class="summary-label">Due Today</div>
+ <div class="summary-value section-today">{{if index . "today"}}{{index . "today"}}{{else}}0{{end}}</div>
+ <div class="summary-label">Today</div>
+ </div>
+ <div class="summary-item">
+ <div class="summary-value section-tomorrow">{{if index . "tomorrow"}}{{index . "tomorrow"}}{{else}}0{{end}}</div>
+ <div class="summary-label">Tomorrow</div>
+ </div>
+ {{end}}{{end}}
+ <div class="summary-item">
+ <div class="summary-value">{{len .CompletedLog}}</div>
+ <div class="summary-label">Recently Done</div>
</div>
</div>
</div>
<div class="card">
- <h2>Timeline</h2>
- {{if .Timeline}}
- <table>
- <thead>
- <tr>
- <th>Source</th>
- <th>Title</th>
- <th>Due</th>
- <th>Type</th>
- </tr>
- </thead>
- <tbody>
- {{range .Timeline}}
- <tr>
- <td><span class="source source-{{.Source}}">{{.Source}}</span></td>
- <td>
- {{if .URL}}<a href="{{.URL}}" target="_blank">{{.Title}}</a>{{else}}{{.Title}}{{end}}
- {{if .Description}}<br><small style="color: #888;">{{.Description}}</small>{{end}}
- </td>
- <td>{{if .Due}}{{.Due.Format "Jan 2, 3:04 PM"}}{{else}}-{{end}}</td>
- <td>{{.Type}}</td>
- </tr>
- {{end}}
- </tbody>
- </table>
- {{else}}
- <p>No items in the timeline for this date range.</p>
- {{end}}
+ <h2>Today's Schedule ({{.Today}})</h2>
+ <div class="all-day" id="all-day-items"></div>
+ <div class="calendar" id="today-calendar"></div>
+ <script>
+ (function() {
+ const data = document.getElementById('agent-data');
+ const items = data ? JSON.parse(data.textContent).today_items || [] : [];
+ const calendar = document.getElementById('today-calendar');
+ const allDay = document.getElementById('all-day-items');
+ const startHour = 6;
+ const endHour = 22;
+ const hourHeight = 40;
+
+ // Generate hour rows
+ for (let hour = startHour; hour <= endHour; hour++) {
+ const row = document.createElement('div');
+ row.className = 'calendar-hour';
+ row.dataset.hour = hour;
+ const label = document.createElement('span');
+ label.className = 'calendar-hour-label';
+ if (hour < 12) label.textContent = hour + 'am';
+ else if (hour === 12) label.textContent = '12pm';
+ else label.textContent = (hour - 12) + 'pm';
+ row.appendChild(label);
+ calendar.appendChild(row);
+ }
+
+ if (items.length === 0) {
+ calendar.innerHTML = '<p style="color: #888; padding: 20px;">No items scheduled for today.</p>';
+ allDay.style.display = 'none';
+ return;
+ }
+
+ items.forEach(item => {
+ if (!item.due) return;
+ const due = new Date(item.due);
+ const hour = due.getHours();
+ const minutes = due.getMinutes();
+
+ // All-day items (midnight) go in separate section
+ if (hour === 0 && minutes === 0) {
+ const el = document.createElement('span');
+ el.className = 'all-day-item source-' + item.source;
+ el.textContent = item.title;
+ allDay.appendChild(el);
+ return;
+ }
+
+ // Skip items outside 6am-10pm range
+ if (hour < startHour || hour > endHour) return;
+
+ const el = document.createElement('div');
+ el.className = 'calendar-item source-' + item.source;
+ const top = (hour - startHour) * hourHeight + (minutes / 60) * hourHeight;
+ el.style.top = top + 'px';
+ el.style.height = '34px';
+ el.innerHTML = '<div class="calendar-item-time">' + due.toLocaleTimeString('en-US', {hour: 'numeric', minute: '2-digit'}) + '</div><div class="calendar-item-title">' + item.title + '</div>';
+ if (item.url) {
+ el.style.cursor = 'pointer';
+ el.onclick = () => window.open(item.url, '_blank');
+ }
+ calendar.appendChild(el);
+ });
+
+ // Hide all-day section if empty
+ if (allDay.children.length === 0) {
+ allDay.style.display = 'none';
+ }
+ })();
+ </script>
</div>
+
+ <div class="card">
+ <div class="tabs">
+ <button class="tab active" onclick="showTab('timeline')">Timeline</button>
+ <button class="tab" onclick="showTab('completed')">Completed Log</button>
+ </div>
+
+ <div id="timeline" class="tab-content active">
+ <h2>Upcoming &amp; Overdue</h2>
+ {{if .Timeline}}
+ <table>
+ <thead>
+ <tr>
+ <th>Source</th>
+ <th>Title</th>
+ <th>Due</th>
+ <th>Section</th>
+ </tr>
+ </thead>
+ <tbody>
+ {{range .Timeline}}
+ <tr>
+ <td><span class="source source-{{.Source}}">{{.Source}}</span></td>
+ <td>
+ {{if .URL}}<a href="{{.URL}}" target="_blank">{{.Title}}</a>{{else}}{{.Title}}{{end}}
+ {{if .Description}}<br><small style="color: #888;">{{.Description}}</small>{{end}}
+ </td>
+ <td>{{if .Due}}{{.Due.Format "Jan 2, 3:04 PM"}}{{else}}-{{end}}</td>
+ <td><span class="section-{{.DaySection}}">{{.DaySection}}</span></td>
+ </tr>
+ {{end}}
+ </tbody>
+ </table>
+ {{else}}
+ <p style="color: #888;">No upcoming or overdue items.</p>
+ {{end}}
+ </div>
+
+ <div id="completed" class="tab-content">
+ <h2>Recently Completed</h2>
+ {{if .CompletedLog}}
+ <table>
+ <thead>
+ <tr>
+ <th>Source</th>
+ <th>Title</th>
+ <th>Due Date</th>
+ <th>Completed</th>
+ </tr>
+ </thead>
+ <tbody>
+ {{range .CompletedLog}}
+ <tr class="completed-item">
+ <td><span class="source source-{{.Source}}">{{.Source}}</span></td>
+ <td>{{.Title}}</td>
+ <td>{{if .DueDate}}{{.DueDate.Format "Jan 2"}}{{else}}-{{end}}</td>
+ <td>{{.CompletedAt.Format "Jan 2, 3:04 PM"}}</td>
+ </tr>
+ {{end}}
+ </tbody>
+ </table>
+ {{else}}
+ <p style="color: #888;">No completed tasks logged yet.</p>
+ {{end}}
+ </div>
+ </div>
+
+ <script>
+ function showTab(tabId) {
+ document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
+ document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
+ document.querySelector('.tab[onclick*="' + tabId + '"]').classList.add('active');
+ document.getElementById(tabId).classList.add('active');
+ }
+ </script>
</body>
</html>