diff options
Diffstat (limited to 'DESIGN.md')
| -rw-r--r-- | DESIGN.md | 706 |
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 +``` |
