# 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