summaryrefslogtreecommitdiff
path: root/DESIGN.md
diff options
context:
space:
mode:
Diffstat (limited to 'DESIGN.md')
-rw-r--r--DESIGN.md706
1 files changed, 706 insertions, 0 deletions
diff --git a/DESIGN.md b/DESIGN.md
new file mode 100644
index 0000000..59f3cb8
--- /dev/null
+++ b/DESIGN.md
@@ -0,0 +1,706 @@
+# Task Dashboard - Design Document
+
+## Overview
+
+Task Dashboard is a personal productivity web application that aggregates tasks, meals, calendar events, and shopping lists from multiple external services into a unified, mobile-friendly interface. Built with Go backend, HTMX frontend, Tailwind CSS styling, and SQLite storage.
+
+**Stack:** Go 1.21+ | chi router | HTMX 1.x | Tailwind CSS | SQLite (WAL mode)
+
+---
+
+## Project Intent
+
+The dashboard serves as a **personal consolidation hub** designed to:
+
+1. **Aggregate multiple data sources** into one view (Trello, Todoist, PlanToEat, Google Calendar, Google Tasks)
+2. **Prioritize reliability** over speed - graceful degradation when APIs fail
+3. **Support mobile-first usage** - optimized for phone/tablet in-store or on-the-go
+4. **Minimize context switching** - complete tasks from any source without opening multiple apps
+5. **Provide live monitoring** - volcano webcams and weather for Hawaii location
+
+---
+
+## Architecture
+
+### Directory Structure
+
+```
+task-dashboard/
+├── cmd/dashboard/
+│ └── main.go # Application entry point, route registration
+├── internal/
+│ ├── api/ # External API clients
+│ │ ├── interfaces.go # API client contracts (interfaces)
+│ │ ├── http.go # BaseClient HTTP utilities
+│ │ ├── trello.go # Trello API client
+│ │ ├── todoist.go # Todoist API (REST v2 + Sync v9)
+│ │ ├── plantoeat.go # PlanToEat web scraper
+│ │ ├── google_calendar.go # Google Calendar integration
+│ │ └── google_tasks.go # Google Tasks integration
+│ ├── auth/
+│ │ ├── auth.go # Authentication service (bcrypt)
+│ │ ├── handlers.go # Login/logout handlers
+│ │ └── middleware.go # Session + CSRF protection
+│ ├── config/
+│ │ ├── config.go # Environment configuration
+│ │ ├── constants.go # App constants (timeouts, limits)
+│ │ └── timezone.go # Display timezone helpers
+│ ├── handlers/
+│ │ ├── handlers.go # Main HTTP handlers (~2000 LOC)
+│ │ ├── response.go # JSON/HTML response utilities
+│ │ ├── cache.go # Generic cache fetcher pattern
+│ │ ├── atoms.go # Unified task aggregation
+│ │ ├── timeline.go # Timeline view handler
+│ │ └── timeline_logic.go # Timeline data processing
+│ ├── middleware/
+│ │ └── security.go # Security headers + rate limiting
+│ ├── models/
+│ │ ├── types.go # Data models (Task, Card, Meal, etc.)
+│ │ ├── atom.go # Unified Atom model + converters
+│ │ └── timeline.go # TimelineItem + DaySection models
+│ └── store/
+│ └── sqlite.go # SQLite database layer (~700 LOC)
+├── migrations/ # SQL migration files (001-009)
+├── web/
+│ ├── templates/
+│ │ ├── index.html # Main dashboard shell
+│ │ ├── login.html # Authentication page
+│ │ ├── conditions.html # Standalone live feeds page
+│ │ ├── shopping-mode.html # Full-screen shopping mode
+│ │ └── partials/ # HTMX partial templates
+│ └── static/
+│ ├── css/input.css # Tailwind source
+│ ├── css/output.css # Compiled CSS
+│ └── js/app.js # Client-side logic
+└── tailwind.config.js
+```
+
+### Request Flow
+
+```
+Browser (HTMX request)
+ ↓
+chi Router (middleware stack)
+ ├── Logger, Recoverer, Timeout
+ ├── SecurityHeaders
+ ├── Session (LoadAndSave)
+ └── CSRFProtect
+ ↓
+Handler Function
+ ├── API Client (fetch from external service)
+ │ └── BaseClient.Get/Post (HTTP with context)
+ └── Store (SQLite cache)
+ └── Get*/Save* methods
+ ↓
+Template Rendering
+ ├── Full page: ExecuteTemplate(w, "filename.html", data)
+ └── Partial: ExecuteTemplate(w, "partial-name", data)
+ ↓
+HTML Response → HTMX swap
+```
+
+### Data Flow Patterns
+
+**Cache-First with Fallback:**
+```go
+// internal/handlers/cache.go pattern
+1. Check IsCacheValid(key) - TTL-based
+2. If valid, return cached data from SQLite
+3. If expired, fetch from API
+4. If API fails, return stale cache (graceful degradation)
+5. On success, save to cache + update metadata
+```
+
+**Unified Atom Model:**
+```go
+// internal/models/atom.go
+// Normalizes Todoist tasks, Trello cards, bugs → single Atom struct
+TaskToAtom(Task) → Atom{Source: "todoist", SourceIcon: "🔴", ...}
+CardToAtom(Card) → Atom{Source: "trello", SourceIcon: "📋", ...}
+BugToAtom(Bug) → Atom{Source: "bug", SourceIcon: "🐛", ...}
+```
+
+**Timeline Aggregation:**
+```go
+// internal/handlers/timeline_logic.go
+BuildTimeline(ctx, store, calendarClient, tasksClient, start, end)
+├── Fetch tasks with due dates (Todoist)
+├── Fetch cards with due dates (Trello)
+├── Fetch meals (PlanToEat) → apply default times
+├── Fetch events (Google Calendar)
+├── Fetch tasks (Google Tasks)
+├── Convert all to TimelineItem
+├── Compute DaySection (today/tomorrow/later)
+└── Sort by Time
+```
+
+---
+
+## Visual Design
+
+### Color Palette
+
+| Usage | Value | Tailwind |
+|-------|-------|----------|
+| Panel background | `rgba(0,0,0,0.4)` | `bg-black/40` |
+| Card background | `rgba(0,0,0,0.5)` | `bg-black/50` |
+| Card hover | `rgba(0,0,0,0.6)` | `bg-black/60` |
+| Primary text | `#ffffff` | `text-white` |
+| Secondary text | `rgba(255,255,255,0.7)` | `text-white/70` |
+| Muted text | `rgba(255,255,255,0.5)` | `text-white/50` |
+| Border subtle | `rgba(255,255,255,0.1)` | `border-white/10` |
+| Border accent | `rgba(255,255,255,0.2)` | `border-white/20` |
+| Todoist accent | `#e44332` | `border-red-500` |
+| Trello accent | `#0079bf` | `border-blue-500` |
+| PlanToEat accent | `#10b981` | `border-green-500` |
+
+### Component Patterns
+
+**Card Container:**
+```html
+<div class="bg-card bg-card-hover transition-colors rounded-lg border border-white/5">
+ <div class="flex items-start gap-3 p-3">
+ <!-- checkbox → icon → content → link -->
+ </div>
+</div>
+```
+
+**Tab Button:**
+```html
+<button class="tab-button {{if .IsActive}}tab-button-active{{end}}">
+ <span>🗓️</span> Timeline
+</button>
+```
+
+**Badge:**
+```html
+<span class="text-xs px-2 py-0.5 rounded bg-blue-900/50 text-blue-300">
+ trello
+</span>
+```
+
+**Glass Morphism:**
+- `bg-black/40 backdrop-blur-sm` for panels
+- `bg-black/60 backdrop-blur-lg` for modals
+- `box-shadow: 0 0 12px black` for depth
+
+### Typography
+
+- **Font:** Inter (300, 400, 500, 600 weights)
+- **Headings:** `font-light tracking-wide text-white`
+- **Labels:** `text-xs font-medium tracking-wider uppercase`
+- **Body:** `text-sm text-white/70`
+
+---
+
+## API Integrations
+
+### Summary Table
+
+| Service | Auth Method | Config Var | Read | Write | Notes |
+|---------|-------------|------------|------|-------|-------|
+| Todoist | Bearer token | `TODOIST_API_KEY` | ✓ Tasks, Projects | ✓ Create, Complete | Incremental sync available |
+| Trello | Key + Token | `TRELLO_API_KEY`, `TRELLO_TOKEN` | ✓ Boards, Cards, Lists | ✓ Create, Archive | Concurrent board fetching |
+| PlanToEat | Session cookie | `PLANTOEAT_SESSION` | ✓ Meals, Shopping | ✗ | Web scraping (brittle) |
+| Google Calendar | OAuth file | `GOOGLE_CREDENTIALS_FILE` | ✓ Events | ✗ | Multi-calendar support |
+| Google Tasks | OAuth file | `GOOGLE_CREDENTIALS_FILE` | ✓ Tasks | ✓ Complete | Shared credentials with Calendar |
+
+### Interface Pattern
+
+All API clients implement interfaces (in `internal/api/interfaces.go`):
+
+```go
+type TodoistAPI interface {
+ GetTasks(ctx context.Context) ([]models.Task, error)
+ GetProjects(ctx context.Context) ([]models.Project, error)
+ CreateTask(ctx context.Context, content, projectID string, dueDate *time.Time, priority int) (*models.Task, error)
+ CompleteTask(ctx context.Context, taskID string) error
+ ReopenTask(ctx context.Context, taskID string) error
+ // ...
+}
+```
+
+Compile-time enforcement:
+```go
+var _ TodoistAPI = (*TodoistClient)(nil)
+```
+
+### Timezone Handling
+
+All times normalized to display timezone:
+```go
+config.SetDisplayTimezone("Pacific/Honolulu") // Set at startup
+config.Now() // Current time in display TZ
+config.Today() // Current date in display TZ
+config.ToDisplayTZ(t) // Convert any time
+config.ParseDateInDisplayTZ("2026-01-27") // Parse in display TZ
+```
+
+---
+
+## Features
+
+### Timeline View (`/tabs/timeline`)
+
+Chronological view of all upcoming items grouped by day section.
+
+**Data Sources:** Todoist, Trello, PlanToEat, Google Calendar, Google Tasks
+
+**Organization:**
+- **Today** (expanded) - Items due today
+- **Tomorrow** (collapsed) - Items due tomorrow
+- **Later** (collapsed) - Items 2+ days out
+
+**Item Display:**
+- Color-coded dot (blue=event, orange=meal, green=task, yellow=gtask)
+- Time display (hidden for all-day items at midnight)
+- Checkbox for completable items
+- Title + description (2-line clamp)
+- External link icon
+
+### Tasks View (`/tabs/tasks`)
+
+Unified task grid showing Atoms (normalized tasks from all sources).
+
+**Layout:** 3-column responsive grid
+
+**Features:**
+- Priority sorting (overdue → today → future → no date)
+- Source icons (🔴 Todoist, 📋 Trello, 🐛 Bug)
+- Expandable descriptions
+- Collapsible "Future" section
+
+### Shopping View (`/tabs/shopping`)
+
+Aggregated shopping lists from Trello + PlanToEat + user items.
+
+**Tab Mode:** Store sections with items, quick-add form
+
+**Shopping Mode (`/shopping/mode/{store}`):** Full-screen mobile-optimized view
+- Store switcher (horizontal tabs)
+- Large touch targets
+- Fixed bottom quick-add
+
+### Conditions Page (`/conditions`)
+
+Standalone live feeds page with:
+- 3 Kilauea volcano webcams (USGS)
+- 2 Hawaii weather maps (Windy.com)
+- 1 National weather map
+
+Auto-refresh webcam images every 60 seconds.
+
+---
+
+## Database Schema
+
+**Engine:** SQLite with WAL mode (concurrent reads)
+
+**Key Tables:**
+
+| Table | Purpose | Key Columns |
+|-------|---------|-------------|
+| `tasks` | Todoist cache | id, content, due_date, priority, completed |
+| `boards` | Trello boards | id, name |
+| `cards` | Trello cards | id, name, board_id, list_id, due_date |
+| `meals` | PlanToEat meals | id, recipe_name, date, meal_type |
+| `users` | Authentication | id, username, password_hash |
+| `sessions` | SCS sessions | token, data, expiry |
+| `bugs` | Bug reports | id, description, resolved_at |
+| `user_shopping_items` | User-added items | id, name, store, checked |
+| `shopping_item_checks` | External item state | source, item_id, checked |
+| `cache_metadata` | Cache timestamps | key, last_fetch, ttl_minutes |
+| `sync_tokens` | Incremental sync | service, token |
+
+---
+
+## Development Guide for Claude Code
+
+### Getting Started
+
+1. **Read this file first** - Understand architecture and patterns
+2. **Check `CLAUDE.md`** - Project-specific instructions
+3. **Run tests before changes:** `go test ./...`
+4. **Run after changes:** `go test ./...` then `go build`
+
+### Key Principles
+
+1. **Cache-First, Fail-Soft:** Never hard-fail if an API is down. Return cached data.
+2. **Minimal Changes:** Surgical edits only. Don't refactor unrelated code.
+3. **Interface-Driven:** API clients implement interfaces for testability.
+4. **HTMX for Updates:** Use partials and `hx-swap` for reactive UI.
+
+### Common Patterns
+
+**Adding a new handler:**
+```go
+// internal/handlers/handlers.go
+func (h *Handler) HandleNewFeature(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ // 1. Parse input
+ if err := r.ParseForm(); err != nil {
+ JSONError(w, http.StatusBadRequest, "Parse failed", err)
+ return
+ }
+
+ // 2. Validate
+ value := r.FormValue("key")
+ if value == "" {
+ JSONError(w, http.StatusBadRequest, "Missing key", nil)
+ return
+ }
+
+ // 3. Business logic
+ result, err := h.apiClient.DoSomething(ctx, value)
+ if err != nil {
+ JSONError(w, http.StatusInternalServerError, "Failed", err)
+ return
+ }
+
+ // 4. Save to cache (best-effort)
+ _ = h.store.SaveResult(result)
+
+ // 5. Respond
+ HTMLResponse(w, h.templates, "partial-name", result)
+}
+```
+
+**Adding a new API method:**
+```go
+// 1. Add to interface (internal/api/interfaces.go)
+type MyAPI interface {
+ NewMethod(ctx context.Context, param string) (Result, error)
+}
+
+// 2. Implement in client (internal/api/myclient.go)
+func (c *MyClient) NewMethod(ctx context.Context, param string) (Result, error) {
+ var result Result
+ if err := c.Get(ctx, "/endpoint/"+param, c.authHeaders(), &result); err != nil {
+ return Result{}, fmt.Errorf("failed to fetch: %w", err)
+ }
+ return result, nil
+}
+```
+
+**Adding a new template:**
+```html
+<!-- web/templates/partials/new-feature.html -->
+{{define "new-feature"}}
+<div class="space-y-4">
+ {{range .Items}}
+ <div class="bg-card rounded-lg p-4">
+ <h3 class="text-white font-medium">{{.Title}}</h3>
+ </div>
+ {{end}}
+</div>
+{{end}}
+```
+
+**Adding a new route:**
+```go
+// cmd/dashboard/main.go inside protected routes group
+r.Get("/tabs/newfeature", h.HandleNewFeature)
+r.Post("/newfeature/action", h.HandleNewAction)
+```
+
+### Template Naming Convention
+
+| Type | Location | Reference |
+|------|----------|-----------|
+| Standalone page | `web/templates/name.html` | `"name.html"` |
+| Partial | `web/templates/partials/name.html` | `"name"` (define name) |
+
+**Example:**
+```go
+// Standalone page - use filename
+h.templates.ExecuteTemplate(w, "shopping-mode.html", data)
+
+// Partial - use define name
+HTMLResponse(w, h.templates, "shopping-tab", data)
+```
+
+### HTMX Patterns
+
+**Tab content loading:**
+```html
+<div id="tab-content"
+ hx-get="/tabs/{{.ActiveTab}}"
+ hx-trigger="load"
+ hx-swap="innerHTML">
+</div>
+```
+
+**Form submission with swap:**
+```html
+<form hx-post="/complete-atom"
+ hx-vals='{"id": "{{.ID}}", "source": "{{.Source}}"}'
+ hx-target="closest div.rounded-lg"
+ hx-swap="outerHTML">
+```
+
+**Refresh trigger:**
+```javascript
+// After action completes
+htmx.trigger(document.body, 'refresh-tasks')
+```
+
+### Test Patterns
+
+**Unit test for store:**
+```go
+func TestStore_SaveTasks(t *testing.T) {
+ db := setupTestStore(t)
+ defer db.Close()
+
+ tasks := []models.Task{{ID: "1", Content: "Test"}}
+ err := db.SaveTasks(tasks)
+ require.NoError(t, err)
+
+ result, err := db.GetTasks()
+ require.NoError(t, err)
+ assert.Len(t, result, 1)
+}
+```
+
+**Handler test:**
+```go
+func TestHandler_HandleDashboard(t *testing.T) {
+ h := setupTestHandler(t)
+
+ req := httptest.NewRequest(http.MethodGet, "/", nil)
+ w := httptest.NewRecorder()
+
+ h.HandleDashboard(w, req)
+
+ assert.Equal(t, http.StatusOK, w.Code)
+}
+```
+
+### Debugging Tips
+
+1. **Check logs:** All errors logged with `log.Printf`
+2. **Cache issues:** Check `cache_metadata` table for stale timestamps
+3. **API failures:** Look for `"Warning:"` or `"ERROR:"` prefixes
+4. **Template errors:** Check for `html/template: "name" is undefined`
+5. **HTMX issues:** Browser DevTools Network tab shows all requests
+
+### Files You'll Modify Most
+
+| Task | Primary Files |
+|------|--------------|
+| New feature | `handlers.go`, `main.go`, new template |
+| Bug fix | Usually `handlers.go` or specific API client |
+| UI change | Template in `partials/`, maybe `input.css` |
+| API integration | New file in `internal/api/`, update `interfaces.go` |
+| Database change | New migration, update `sqlite.go` |
+
+### Configuration Reference
+
+**Required:**
+- `TODOIST_API_KEY` - Todoist API key
+- `TRELLO_API_KEY` - Trello API key
+- `TRELLO_TOKEN` - Trello token
+- `DEFAULT_PASS` - Admin password (must be set)
+
+**Optional:**
+- `DEFAULT_USER` (default: "admin")
+- `PLANTOEAT_SESSION` - PlanToEat session cookie
+- `GOOGLE_CREDENTIALS_FILE` - OAuth credentials JSON path
+- `GOOGLE_CALENDAR_ID` (default: "primary")
+- `GOOGLE_TASKS_LIST_ID` (default: "@default")
+- `DATABASE_PATH` (default: "./dashboard.db")
+- `PORT` (default: "8080")
+- `CACHE_TTL_MINUTES` (default: 5)
+- `TIMEZONE` (default: "Pacific/Honolulu")
+- `DEBUG` (default: false)
+
+### Common Mistakes to Avoid
+
+1. **Don't use `"name"` for standalone pages** - Use `"name.html"`
+2. **Don't forget context propagation** - All handlers should use `r.Context()`
+3. **Don't panic on API errors** - Return cached data or empty slice
+4. **Don't modify tests without running them** - `go test ./...`
+5. **Don't add unnecessary comments** - Code should be self-explanatory
+6. **Don't create new files unless necessary** - Prefer editing existing
+7. **Don't refactor working code** - Only touch what's needed
+
+### Bug Management Workflow
+
+Use the bug tracking system to report and resolve issues:
+
+**Viewing bugs (production):**
+```bash
+./scripts/bugs
+# Lists all bugs from production database with ID, description, and created_at
+```
+
+**Resolving a bug:**
+```bash
+./scripts/resolve-bug <bug_id>
+# Shows bug description, then deletes it from the database
+```
+
+**Workflow:**
+1. Bugs are reported via the dashboard UI (🐛 tab in quick-add modal)
+2. Run `./scripts/bugs` to see current issues
+3. Fix the bug with tests
+4. Deploy the fix
+5. Run `./scripts/resolve-bug <id>` to close it
+
+Bugs appear as atoms in the Tasks view (🐛 icon) until resolved.
+
+### Test-Driven Development (TDD)
+
+**Required approach for all changes:**
+
+1. **Write failing test first** - Define expected behavior before implementation
+2. **Make it pass** - Write minimal code to pass the test
+3. **Refactor** - Clean up while tests stay green
+
+**Test locations:**
+| Component | Test File |
+|-----------|-----------|
+| Store methods | `internal/store/sqlite_test.go` |
+| Handlers | `internal/handlers/*_test.go` |
+| API clients | `internal/api/*_test.go` |
+| Integration | `test/acceptance_test.go` |
+
+**Running tests:**
+```bash
+# All tests
+go test ./...
+
+# Specific package
+go test ./internal/handlers/...
+
+# Verbose with coverage
+go test -v -cover ./...
+
+# Single test
+go test -run TestBuildTimeline ./internal/handlers/...
+```
+
+**Test patterns:**
+```go
+func TestFeature_Behavior(t *testing.T) {
+ // Arrange
+ db := setupTestStore(t)
+ defer db.Close()
+
+ // Act
+ result, err := db.SomeMethod(input)
+
+ // Assert
+ require.NoError(t, err)
+ assert.Equal(t, expected, result)
+}
+```
+
+**Before submitting any change:**
+- `go test ./...` must pass
+- Add tests for new functionality
+- Update tests for changed behavior
+
+### Design Documents and ADRs
+
+**When to update DESIGN.md:**
+- Adding new features or views
+- Changing data flow patterns
+- Modifying API integrations
+- Updating database schema
+
+**When to create an ADR (Architecture Decision Record):**
+- Choosing between multiple implementation approaches
+- Adding new external dependencies
+- Changing authentication/security model
+- Significant refactoring decisions
+
+**ADR location:** `docs/adr/NNN-title.md`
+
+**ADR template:**
+```markdown
+# ADR NNN: Title
+
+## Status
+Proposed | Accepted | Deprecated | Superseded
+
+## Context
+What is the issue or decision we need to make?
+
+## Decision
+What did we decide and why?
+
+### Technical Details:
+- Implementation specifics
+- Key code locations
+
+## Consequences
+- **Pros:** Benefits of this approach
+- **Cons:** Tradeoffs and limitations
+
+## Alternatives Considered
+- Option A: Why rejected
+- Option B: Why rejected
+```
+
+**Numbering:** Use sequential 3-digit numbers (001, 002, etc.)
+
+**Example ADRs to consider:**
+- API client retry strategy
+- Caching invalidation approach
+- New data source integration
+- UI framework changes
+
+---
+
+## Quick Reference
+
+### Commands
+
+```bash
+# Run application
+go run cmd/dashboard/main.go
+
+# Run tests
+go test ./...
+
+# Build binary
+go build -o dashboard cmd/dashboard/main.go
+
+# Rebuild Tailwind CSS
+npx tailwindcss -i web/static/css/input.css -o web/static/css/output.css --watch
+```
+
+### Key Endpoints
+
+| Method | Path | Purpose |
+|--------|------|---------|
+| GET | `/` | Main dashboard |
+| GET | `/tabs/timeline` | Timeline partial |
+| GET | `/tabs/tasks` | Tasks partial |
+| GET | `/tabs/shopping` | Shopping partial |
+| GET | `/conditions` | Live feeds page |
+| POST | `/complete-atom` | Complete task/card |
+| POST | `/uncomplete-atom` | Reopen task/card |
+| POST | `/unified-add` | Quick add task |
+| POST | `/shopping/add` | Add shopping item |
+| GET | `/shopping/mode/{store}` | Shopping mode |
+
+### Handler Signature
+
+```go
+func (h *Handler) HandleName(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+ // ... implementation
+}
+```
+
+### Response Helpers
+
+```go
+JSONResponse(w, data) // 200 + JSON
+JSONError(w, status, "message", err) // Error + log
+HTMLResponse(w, h.templates, "name", data) // Partial render
+HTMLString(w, "<html>...</html>") // Raw HTML
+```