# 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
``` **Tab Button:** ```html ``` **Badge:** ```html trello ``` **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 {{define "new-feature"}}
{{range .Items}}

{{.Title}}

{{end}}
{{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
``` **Form submission with swap:** ```html
``` **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 # 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 ` 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, "...") // Raw HTML ```