summaryrefslogtreecommitdiff
path: root/docs
diff options
context:
space:
mode:
Diffstat (limited to 'docs')
-rw-r--r--docs/adr/002-timeline-aggregation.md76
-rw-r--r--docs/adr/003-htmx-write-operations.md85
-rw-r--r--docs/adr/004-concurrent-data-fetching.md104
-rw-r--r--docs/adr/005-agent-api-authentication.md95
4 files changed, 360 insertions, 0 deletions
diff --git a/docs/adr/002-timeline-aggregation.md b/docs/adr/002-timeline-aggregation.md
new file mode 100644
index 0000000..2700776
--- /dev/null
+++ b/docs/adr/002-timeline-aggregation.md
@@ -0,0 +1,76 @@
+# ADR 002: Timeline Aggregation Architecture
+
+## Status
+Accepted
+
+## Context
+The dashboard aggregates data from multiple sources (Todoist, Trello, PlanToEat, Google Calendar, Google Tasks). Users need a unified chronological view of upcoming items across all sources without opening multiple apps.
+
+Key challenges:
+- Different data sources have different models (Task, Card, Meal, Event)
+- Some sources have time-of-day, others only have dates
+- Meals have no explicit time but should appear at logical meal times
+- Need to support grouping by day section (Today, Tomorrow, Later)
+
+## Decision
+Implement a **polymorphic TimelineItem model** that normalizes all data sources into a single sortable structure.
+
+### Technical Details:
+
+**Unified Model (`internal/models/timeline.go`):**
+```go
+type TimelineItem struct {
+ ID string
+ Type TimelineItemType // task, meal, card, event, gtask
+ Title string
+ Description string
+ Time time.Time
+ AllDay bool
+ URL string
+ DaySection string // overdue, today, tomorrow, later
+ // ... additional fields
+}
+```
+
+**Meal Time Defaults:**
+- Breakfast → 08:00
+- Lunch → 12:00
+- Dinner → 19:00
+- Other → 12:00
+
+**Implementation:**
+- `internal/handlers/timeline_logic.go` - `BuildTimeline()` aggregation function
+- `internal/handlers/timeline.go` - HTTP handler
+- `internal/models/timeline.go` - Data models
+- Store methods: `GetTasksByDateRange()`, `GetMealsByDateRange()`, `GetCardsByDateRange()`
+
+**Grouping Strategy:**
+Items are grouped into collapsible day sections:
+- **Overdue** - Past due items (expanded)
+- **Today** - Due today (expanded)
+- **Tomorrow** - Due tomorrow (expanded)
+- **Later** - 2+ days out (collapsed by default)
+
+## Consequences
+
+**Pros:**
+- Single unified view reduces context switching
+- Consistent sorting regardless of data source
+- Extensible - new sources just need a converter to TimelineItem
+- Collapsible sections reduce cognitive load
+
+**Cons:**
+- Meal times are assumed, not actual (user might eat breakfast at 10am)
+- All-day items appear at midnight, requiring special handling
+- Multiple API calls per request (mitigated by caching)
+
+## Alternatives Considered
+
+**Option A: Source-specific views only**
+- Rejected: Defeats the purpose of a unified dashboard
+
+**Option B: Store normalized items in database**
+- Rejected: Adds complexity, staleness issues, harder to keep in sync
+
+**Option C: Client-side aggregation (JavaScript)**
+- Rejected: More complex, slower on mobile, harder to test
diff --git a/docs/adr/003-htmx-write-operations.md b/docs/adr/003-htmx-write-operations.md
new file mode 100644
index 0000000..2471a90
--- /dev/null
+++ b/docs/adr/003-htmx-write-operations.md
@@ -0,0 +1,85 @@
+# ADR 003: HTMX-Based Write Operations
+
+## Status
+Accepted
+
+## Context
+Phase 1-2 of the dashboard were read-only views. Phase 3 required transforming it into an active command center where users can:
+- Complete tasks/cards
+- Create new tasks
+- Add shopping items
+- Toggle item states
+
+Traditional approaches would require either:
+- Full page reloads (poor UX)
+- Complex JavaScript SPA framework (React, Vue)
+- Custom AJAX handlers
+
+## Decision
+Use **HTMX attributes** for all write operations, returning HTML partials that swap into the DOM.
+
+### Technical Details:
+
+**Pattern: Form submission with partial swap**
+```html
+<form hx-post="/complete-atom"
+ hx-vals='{"id": "{{.ID}}", "source": "{{.Source}}"}'
+ hx-target="closest .task-card"
+ hx-swap="outerHTML">
+ <button type="submit">✓</button>
+</form>
+```
+
+**Handler returns HTML partial:**
+```go
+func (h *Handler) HandleCompleteAtom(w http.ResponseWriter, r *http.Request) {
+ // ... complete the task via API
+ // Return completed state HTML or empty response
+ HTMLResponse(w, h.renderer, "completed-atom", data)
+}
+```
+
+**Quick Add Pattern:**
+```html
+<input hx-post="/unified-add"
+ hx-trigger="keyup[key=='Enter']"
+ hx-target="#task-list"
+ hx-swap="afterbegin"
+ name="content">
+```
+
+**Optimistic UI:**
+- Use CSS transitions for immediate visual feedback
+- On error, swap in error banner partial
+- HTMX `hx-on` events for state management
+
+**Unified Quick Add:**
+- Single input parses text for routing hints
+- `#groceries` tag → routes to shopping
+- `#work` tag → routes to Trello
+- Default → routes to Todoist
+
+## Consequences
+
+**Pros:**
+- No JavaScript framework needed
+- Server renders all HTML (single source of truth)
+- Progressive enhancement (works without JS, enhanced with)
+- Trivial to test (just HTTP handlers)
+- Fast development cycle
+
+**Cons:**
+- More HTTP requests than SPA approach
+- Partial HTML responses require careful template organization
+- Limited offline capability
+
+## Alternatives Considered
+
+**Option A: React/Vue SPA**
+- Rejected: Overkill for this use case, adds build complexity, harder to maintain
+
+**Option B: Full page reloads**
+- Rejected: Poor UX, especially on mobile
+
+**Option C: Custom JavaScript + JSON APIs**
+- Rejected: Reinventing what HTMX provides, more code to maintain
diff --git a/docs/adr/004-concurrent-data-fetching.md b/docs/adr/004-concurrent-data-fetching.md
new file mode 100644
index 0000000..c7238f8
--- /dev/null
+++ b/docs/adr/004-concurrent-data-fetching.md
@@ -0,0 +1,104 @@
+# ADR 004: Concurrent Data Fetching with Graceful Degradation
+
+## Status
+Accepted
+
+## Context
+The dashboard fetches data from 5+ external APIs on each page load:
+- Todoist (tasks)
+- Trello (boards, cards)
+- PlanToEat (meals, shopping)
+- Google Calendar (events)
+- Google Tasks (tasks)
+
+Sequential fetching would be unacceptably slow (5+ seconds). However, concurrent fetching introduces:
+- Rate limiting concerns (especially Trello)
+- Partial failure scenarios
+- Data race risks
+
+## Decision
+Implement **parallel fetching with semaphore-limited concurrency** and **cache-first fallback** for graceful degradation.
+
+### Technical Details:
+
+**Parallel Aggregation (`internal/handlers/handlers.go`):**
+```go
+func (h *Handler) aggregateData(ctx context.Context) (*DashboardData, error) {
+ var wg sync.WaitGroup
+ var mu sync.Mutex
+ data := &DashboardData{}
+
+ // Launch goroutines for each data source
+ wg.Add(4)
+ go func() { defer wg.Done(); fetchTasks(ctx, &mu, data) }()
+ go func() { defer wg.Done(); fetchBoards(ctx, &mu, data) }()
+ go func() { defer wg.Done(); fetchMeals(ctx, &mu, data) }()
+ go func() { defer wg.Done(); fetchCalendar(ctx, &mu, data) }()
+ wg.Wait()
+
+ return data, nil
+}
+```
+
+**Semaphore for Trello (`internal/api/trello.go`):**
+```go
+const maxConcurrentRequests = 5
+
+func (c *TrelloClient) GetBoardsWithCards(ctx context.Context) ([]Board, error) {
+ sem := make(chan struct{}, maxConcurrentRequests)
+ // ... limit concurrent board fetches
+}
+```
+
+**Cache-First Fallback:**
+```go
+func (h *Handler) fetchWithFallback(ctx context.Context, fetch func() error, getCache func() (T, error)) T {
+ if err := fetch(); err != nil {
+ log.Printf("Warning: API fetch failed, using cache: %v", err)
+ cached, _ := getCache()
+ return cached
+ }
+ return freshData
+}
+```
+
+**Error Collection:**
+- Errors are collected in `data.Errors` slice
+- UI displays warning banner if any source failed
+- Page still renders with available data
+
+## Consequences
+
+**Pros:**
+- Fast page loads (parallel fetching)
+- Resilient to individual API failures
+- Respects rate limits (semaphore pattern)
+- Users see cached data rather than error page
+
+**Cons:**
+- Complexity in error handling
+- Cache fallback may show stale data without clear indication
+- Debugging requires checking multiple error sources
+- Mutex needed for shared data structure
+
+## Alternatives Considered
+
+**Option A: Sequential fetching**
+- Rejected: 5+ second page loads unacceptable
+
+**Option B: Fail-fast on any error**
+- Rejected: One flaky API shouldn't break entire dashboard
+
+**Option C: Background sync with eventual consistency**
+- Rejected: Adds complexity, users expect fresh data on load
+
+## Implementation Notes
+
+**Race Condition Prevention:**
+- All goroutines write to different fields OR use mutex
+- Trello client uses mutex for `boards[i]` slice element writes
+- Run `go test -race ./...` to verify
+
+**Monitoring:**
+- Log all API failures with `Warning:` prefix
+- Consider adding metrics for cache hit/miss rates
diff --git a/docs/adr/005-agent-api-authentication.md b/docs/adr/005-agent-api-authentication.md
new file mode 100644
index 0000000..e64a5a8
--- /dev/null
+++ b/docs/adr/005-agent-api-authentication.md
@@ -0,0 +1,95 @@
+# ADR 005: Agent API Authentication Architecture
+
+## Status
+Accepted
+
+## Context
+External agents (CLI tools, automation scripts, AI assistants) need secure access to dashboard context without exposing user credentials. Traditional approaches have weaknesses:
+
+- **API Keys**: Long-lived, often stored insecurely, difficult to revoke
+- **OAuth2**: Complex setup, overkill for personal dashboard
+- **SSH Keys**: Key management burden on user
+
+Requirements:
+- User awareness of agent access (no hidden API keys)
+- Easy revocation (deny re-auth requests)
+- Time-limited sessions to reduce exposure
+- Support for both CLI and browser-based agents
+
+## Decision
+Implement **notification-based approval flow** with ephemeral sessions.
+
+### Technical Details:
+
+**Identity Model:**
+- Agents identified by `name` + `agent_id` (UUID) pair
+- Binding persisted on first approval
+- Impersonation detected if name/agent_id mismatch
+
+**Trust Levels:**
+- `TRUSTED`: Previously approved, auto-approve on re-auth
+- `UNKNOWN`: New agent, requires user approval
+- `BLOCKED`: Denied agent, reject immediately
+
+**Session Model:**
+- 1-hour TTL tokens
+- One active session per agent_id
+- Session token in Authorization header
+
+**Approval Flow:**
+```
+Agent Dashboard Browser
+ | | |
+ |-- POST /auth/request --> | |
+ | |-- WebSocket push ----> |
+ | | | (user sees modal)
+ | | |
+ |<-- request_token --------| |
+ | | |
+ |-- GET /auth/poll ------> | |
+ | (polling) | |
+ | | <-- POST /approve ---- |
+ | | |
+ |<-- session_token --------| |
+ | | |
+ |-- GET /context --------> | |
+ | (with session_token) | |
+```
+
+**Implementation:**
+- `internal/handlers/agent.go` - Auth flow handlers (615 LOC)
+- `internal/handlers/websocket.go` - Real-time notifications (216 LOC)
+- `migrations/010_agent_tables.sql` - Storage schema
+- `web/static/js/app.js` - Browser UI for approval modal
+
+**Rate Limiting:**
+- 10 requests/minute per IP on auth endpoints
+- Prevents brute-force token guessing
+
+## Consequences
+
+**Pros:**
+- User-visible agent access (approval modal shows agent name)
+- Easy revocation by denying re-auth or blocking agent
+- Trust level caching reduces approval fatigue for known agents
+- Time-limited sessions reduce credential exposure
+- Works for both CLI agents (polling) and browser agents (web endpoints)
+
+**Cons:**
+- Requires browser window open for first-time approval
+- Poll loop adds latency to auth flow (~1-5 seconds)
+- Session expiry interrupts long-running agent tasks (must re-auth)
+
+## Alternatives Considered
+
+**Option A: Traditional API Keys**
+- Rejected: Long-lived credentials, no user awareness of access
+
+**Option B: OAuth2 Device Flow**
+- Rejected: Complex implementation, requires external auth server setup
+
+**Option C: SSH-style Key Pairs**
+- Rejected: Key management burden, harder to revoke
+
+**Option D: Magic Links (email-based)**
+- Rejected: Adds email dependency, slower approval flow