diff options
29 files changed, 708 insertions, 690 deletions
diff --git a/.agent/coding_standards.md b/.agent/coding_standards.md new file mode 100644 index 0000000..50281d0 --- /dev/null +++ b/.agent/coding_standards.md @@ -0,0 +1,70 @@ +# Coding Standards + +This document defines the technical standards for the **Doot** project. Adherence to these standards ensures consistency, security, and maintainability. + +## 1. Go (Backend) + +### Idiomatic Go +- Follow standard Go idioms (Effective Go). +- Keep functions small and focused on a single responsibility. +- Use meaningful, descriptive names for variables, functions, and types. + +### Concurrency +- Use `sync.WaitGroup` and `sync.Mutex` for managing concurrent operations. +- For parallel API fetching, use a semaphore pattern (e.g., buffered channel) to limit concurrency and avoid rate limits. +- **Example:** See `internal/api/trello.go` for the 5-request concurrency limit. + +### Error Handling +- Use the "Partial Data / Cache Fallback" pattern: If an API call fails, return cached data (if available) or an empty set with an error logged, rather than failing the entire request. +- Use structured logging or the `data.Errors` slice in handlers to report non-fatal errors to the UI. + +### Database (SQLite) +- **SQL Injection Prevention:** ALWAYS use parameterized queries (`?` placeholders). +- **Concurrency:** Enable WAL mode and set `MaxOpenConns(1)` to avoid "database is locked" errors. +- **Migrations:** All schema changes must be added as a new file in the `migrations/` directory. + +### Project Structure +- `cmd/dashboard/`: Application entry point. +- `internal/api/`: External service clients (Todoist, Trello, etc.). +- `internal/auth/`: Authentication logic and middleware. +- `internal/config/`: Configuration loading and validation. +- `internal/handlers/`: HTTP request handlers and UI logic. +- `internal/models/`: Shared data structures. +- `internal/store/`: Database operations. + +## 2. Frontend (HTMX + Tailwind CSS) + +### HTMX +- Use HTMX for all state-changing operations (Create, Update, Delete) and tab switching. +- **Response Headers:** Use `HX-Trigger` and `HX-Reswap` to coordinate UI updates. +- **Partial Rendering:** Handlers should return HTML partials (from `web/templates/partials/`) for HTMX requests. + +### CSS & Styling +- Use **Tailwind CSS** for all styling. +- Maintain a consistent mobile-first responsive design. +- Follow the Z-index hierarchy defined in `design.md`. + +### User Feedback +- Provide immediate visual feedback for user actions (loading indicators, success/error banners). +- Ensure all forms clear their inputs upon successful submission. + +## 3. Testing Philosophy + +### Reproduction First +- Before fixing a bug, implement a failing test case that reproduces the issue. +- Verify the fix by ensuring the new test (and all existing tests) passes. + +### Coverage Areas +- **Store:** Unit tests for all SQL operations (`sqlite_test.go`). +- **Handlers:** Integration tests for critical paths (`handlers_test.go`, `agent_test.go`). +- **API Clients:** Use mocks or test servers for external API testing (`todoist_test.go`, `trello_test.go`). + +## 4. Documentation & Git + +### Commit Messages +- Propose clear, concise messages that explain the "why" behind the change. +- Match existing style (usually imperative mood). + +### File Header +- Maintain the standardized `SESSION_STATE.md` (now `worklog.md`) format. +- Document significant architectural changes in `docs/adr/`. diff --git a/.agent/config.md b/.agent/config.md new file mode 100644 index 0000000..45ead3b --- /dev/null +++ b/.agent/config.md @@ -0,0 +1,62 @@ +# Agent Configuration & Master Rulebook (.agent/config.md) + +This is the primary source of truth for all AI agents working on **Doot**. These instructions take absolute precedence over general defaults. + +## 1. Project Directory Structure (.agent/) + +| File | Purpose | +|------|---------| +| `config.md` | **Main Entry Point** — Rules, workflows, and core mandates. | +| `worklog.md` | **Session State** — Current focus, recently completed, and next steps. | +| `design.md` | **Architecture** — High-level design, database schema, and components. | +| `coding_standards.md` | **Technical Standards** — Go/HTMX idioms, testing, and security. | +| `ux_philosophy.md` | **UX Principles** — Interaction models, palette, and mobile-first design. | +| `narrative.md` | **Background** — Historical context and evolution of the project. | + +## 2. Core Mandates + +### ULTRA-STRICT ROOT SAFETY PROTOCOL +1. **Inquiry-Only Default:** Treat every message as research/analysis unless it is an explicit, imperative command (Directive). +2. **Zero Unsolicited Implementation:** Never modify files, directories, or processes based on assumptions. +3. **Interactive Strategy Checkpoint:** Research first, present a strategy, and **WAIT** for an explicit "GO" before any system-changing tool call. +4. **No Destructive Assumptions:** Always verify state (`ps`, `ls`, `git status`) before proposing actions. +5. **Root-Awareness:** Prioritize system integrity and user confirmation over proactiveness. + +### Task Management +- **Worklog:** Update `.agent/worklog.md` at the start and end of every session. +- **Claudomator:** Use `claudomator create "Name" --instructions "Goals"` for new tasks. Use `claudomator finish <id>` when done. + +### Living Documentation Mandate +1. **Continuous Capture:** Agents MUST proactively update the files in `.agent/` (Design, UX, Standards, Mission, Preferences) as new decisions, patterns, or user preferences are revealed. +2. **No Stale Instructions:** If a workflow or technical standard evolves, the agent is responsible for reflecting that change in the Master Rulebook immediately. +3. **Worklog Integrity:** The `.agent/worklog.md` must be updated at the start and end of EVERY session to ensure continuity. + +## 3. Workflows + +### Research -> Strategy -> Execution +1. **Research:** Map codebase, validate assumptions, reproduce bugs. +2. **Strategy:** Share a summary. Wait for approval if significant. +3. **Execution (Plan-Act-Validate):** + - **Plan:** Define implementation and testing. + - **Act:** Apply surgical, idiomatic changes. + - **Validate:** Run `go test ./...` and `npm run build`. + +## 4. Agent Context API (Snapshots) + +The project includes an internal API for agents to understand the current user's task and meal context. + +- **Request Access:** `POST /agent/auth/request` (returns `request_token`). +- **Poll Status:** `GET /agent/auth/poll?token=<request_token>` (awaits browser approval). +- **Get Context:** `GET /agent/context` (requires Bearer token). +- **Snapshot Endpoint:** `GET /api/claude/snapshot` (legacy, see `internal/handlers/agent.go` for new unified flow). + +## 5. Essential Commands + +| Command | Action | +|---------|--------| +| `go run cmd/dashboard/main.go` | Start application | +| `go test ./...` | Run all tests | +| `./scripts/deploy` | Deploy to production | +| `./scripts/logs` | View production logs | +| `./scripts/bugs` | List open production bugs | +| `./scripts/resolve-bug <id>` | Resolve a production bug | diff --git a/DESIGN.md b/.agent/design.md index f7b55c8..f7b55c8 100644 --- a/DESIGN.md +++ b/.agent/design.md diff --git a/.agent/mission.md b/.agent/mission.md new file mode 100644 index 0000000..64575e5 --- /dev/null +++ b/.agent/mission.md @@ -0,0 +1,16 @@ +# Project Mission & Strategic Values + +## 1. Core Mission +**Unified Situational Awareness.** Doot is the single "lens" through which the user views their entire digital life. It is not a replacement for the source tools (Trello, Todoist), but a high-speed aggregation layer for clarity. + +## 2. Strategic Values +- **Simplicity over Complexity:** If a feature makes the dashboard harder to read at a glance, it doesn't belong. +- **Glanceable Information:** The UI is designed for "5-second updates," not "5-minute deep dives." +- **Resilience is Mandatory:** Never show an error spinner where stale/cached data could be shown instead. The dashboard must be "always on." +- **Mobile-First Utility:** The primary use case is "checking the plan" while on the move or in the kitchen (meals/shopping). + +## 3. Agent Personality & Role +- **The Proactive Chief of Staff:** Agents should anticipate gaps and propose solutions, but always remain within safety guardrails. +- **Continuous Clarification:** A "GO" (Directive) is a permission to implement, but it is NOT a mandate to stop asking questions. If a sub-task is ambiguous or an unexpected edge case is found during execution, the agent MUST stop and clarify with the user before proceeding. +- **High-Signal, Low-Noise:** Communication should be professional, technical, and concise. +- **Surgical Execution:** Changes should be minimal and idiomatic. diff --git a/.agent/narrative.md b/.agent/narrative.md new file mode 100644 index 0000000..778df85 --- /dev/null +++ b/.agent/narrative.md @@ -0,0 +1,32 @@ +# Engineering Narrative & Project Evolution + +This document tracks the chronological history and major milestones of the **Doot** ecosystem (including its relationship with Claudomator). + +## 1. Ecosystem Origins (Claudomator) +**Claudomator** was developed as the core "task-execution engine." It introduced: +- **Human-in-the-Loop Workflow:** A `READY` state that requires operator sign-off (`Accept/Reject`) before a task is considered `COMPLETED`. +- **Question/Answer Flow:** Agents can transition a task to `BLOCKED` by writing a `question.json` file, allowing users to provide context asynchronously. +- **Subtask Graphing:** The `claudomator` CLI allows agents to break complex goals into manageable subtasks. + +## 2. The Birth of Doot (Consolidation) +**Doot** (this project) was created to solve "Context Blindness." Instead of managing tasks across Todoist, Trello, and Google Calendar separately, Doot aggregates them into a single, high-speed interface. + +### Key Milestones: +- **Phase 1: Read-Only Aggregation:** Initial dashboard with Trello, Todoist, and PlanToEat. +- **Phase 2: Authentication:** Wired up `internal/auth` with SQLite sessions and default admin user. +- **Phase 3: Timeline & Google Integration:** Added the "Timeline" view, aggregating Google Calendar and Google Tasks via a cached store layer. +- **Phase 4: Agent Context API:** Built the `/agent/context` endpoint and WebSocket-based approval flow to let AI agents "see" what the user is working on safely. + +## 3. Current Direction: Strategic Execution +The project is currently evolving into a **Strategic Agent Studio**, where: +- Doot serves as the "Lens" (View/Approve). +- Claudomator serves as the "Muscle" (Execute/Deliver). +- The `.agent/` directory serves as the "Brain" (Config/Context). + +## 4. Historical Activity Log (Raw) + +--- 2026-03-16T01:52:44Z --- +Do a thorough review for refactoring opportunities, logic bugs and code, redundancy and shaky abstractions. Create claudomator tasks for each + +--- 2026-03-17T09:02:26Z --- +Completing Trello tasks doesn't work: failed to load content (Fix: Verified and corrected Trello API param mapping) diff --git a/.agent/preferences.md b/.agent/preferences.md new file mode 100644 index 0000000..4d79ba2 --- /dev/null +++ b/.agent/preferences.md @@ -0,0 +1,15 @@ +# User Preferences & Workflow Quirks + +This file is a "living record" of the user's personal preferences, discovered patterns, and workflow expectations. Agents must proactively update this file as new preferences are revealed during interaction. + +## 1. Interaction & Workflow +- **Safety First:** The user prefers caution and deliberate action. +- **The "Checkpoint" Model:** Research first, strategy second, and wait for a "GO" (Directive). +- **Continuous Clarification:** A "GO" is not a mandate to stop asking questions. If execution hits an ambiguity, stop and ask. +- **Living Docs:** Agents are empowered and expected to update any file in the `.agent/` directory when a new fact, decision, or preference is established. + +## 2. Technical Preferences +- (To be populated as preferences are revealed during the project...) + +## 3. UI/UX Style +- (To be populated as preferences are revealed during the project...) diff --git a/TIMELINE_DESIGN.md b/.agent/timeline_design.md index 61c71b5..61c71b5 100644 --- a/TIMELINE_DESIGN.md +++ b/.agent/timeline_design.md diff --git a/.agent/ux_philosophy.md b/.agent/ux_philosophy.md new file mode 100644 index 0000000..e9b6a87 --- /dev/null +++ b/.agent/ux_philosophy.md @@ -0,0 +1,82 @@ +# UI/UX Review & Redesign Proposal + +## Current State Analysis: "The Unusable Form" + +The current "Quick Add" modal (in `index.html`) and the "Planning" tab form suffer from several friction points: + +1. **Context Blindness:** When adding a Trello card, you must first pick a source, then a board, then a list. This is 3+ clicks before you even type the task name. +2. **Repetitive Data Entry:** Creating multiple related stories (e.g., across 3 repos) requires filling the entire form 3 times. +3. **No "Smart" Defaults:** The form doesn't remember your last used project, repo, or base image. +4. **Mobile Friction:** Select dropdowns are native and small, making them hard to use for long lists of repos or boards. +5. **Information Overload:** The "Task" and "Shopping" tabs in the modal use the same `HandleUnifiedAdd` endpoint but different fields, leading to a "leaky abstraction" in the UI. + +--- + +## 1. Redesign: The "Command Palette" Model + +Instead of a traditional form, we should move towards a **Command Palette** style interaction (Ctrl+K). + +### Proposed Streamlined Flow: +1. **Trigger:** `Ctrl+K` opens a search-first modal. +2. **Type:** User types "doot: fix css in dashboard" or "claudomator: update go version". +3. **Prefix Detection:** + * `d:` or `t:` -> Todoist task. + * `tr:` -> Trello card. + * `c:` or `s:` -> Claudomator Story (New). +4. **Contextual Suggestions:** As the user types, the bottom of the modal shows: + * "Repo: doot" (detected from prefix or frequency) + * "Project: Phase 6" + * "Base Image: Go 1.24" + +--- + +## 2. Resource Management Model (Repos, Images, Projects) + +The user requested a "better model for management." We should move away from flat strings and toward a **Resource Graph**. + +### New SQLite Schema (Internal) +* **`claudomator_repos`:** `id, name, remote_url, default_branch, local_path` +* **`claudomator_images`:** `id, name, tag, description (e.g., "Go 1.24 + Node 22")` +* **`claudomator_projects`:** `id, name, description` +* **`project_repos`:** Junction table linking projects to multiple repositories. + +### Management UI +Instead of hiding these in settings, we should have a **"Library" view** (perhaps a new tab or a sub-section of Planning) where users can: +* Quick-add a new repo. +* Toggle which "Base Images" are active for selection. +* Group repos into "Projects" for batch operations. + +--- + +## 3. UI/UX Recommendations & Plugins + +### Recommended Plugins/Libraries +1. **[Tom Select](https://tom-select.js.org/):** To replace native `<select>`. This allows for: + * Searchable dropdowns (essential for 20+ repos). + * Custom rendering (show repo icons/logos in the list). + * Remote data loading (HTMX-friendly). +2. **[Alpine.js](https://alpinejs.dev/):** To handle "Local UI State" without page refreshes. + * Example: Toggling between "Simple Task" and "Complex Story" modes in the same modal. + * Example: Multi-select for repos. +3. **[Tailwind Forms Plugin](https://github.com/tailwindlabs/tailwindcss-forms):** To make inputs look professional with zero effort. +4. **[Lucide Icons](https://lucide.dev/):** To replace the emoji-heavy UI with cleaner, more professional SVG icons. + +--- + +## 4. Proposed User Flow for Story Creation + +**Goal: From "Idea" to "Running Agent" in < 15 seconds.** + +1. **Open Studio:** User clicks a new "Create Story" button (or `Ctrl+K` then `s`). +2. **Draft:** User types the goal. +3. **Auto-Select:** System auto-selects the "Active Project" and its associated "Primary Repo". +4. **Elaborate (AI):** User hits `Enter`. HTMX swaps the form for a "Thinking..." animation. +5. **Review Tree:** AI returns a task tree. User can click a task to edit its "Validation Spec". +6. **Launch:** User hits "Execute". Story moves to `IN_PROGRESS` and appears on the main dashboard. + +--- + +## Next Steps (Deferred to Phase 7) +* Implement the SQLite migrations for the new Resource Graph. +* Build the "Story Studio" using Alpine.js + Tom Select. +* Create the management UI for Repos and Projects. diff --git a/SESSION_STATE.md b/.agent/worklog.md index 08f6409..08f6409 100644 --- a/SESSION_STATE.md +++ b/.agent/worklog.md diff --git a/.gemini/GEMINI.md b/.gemini/GEMINI.md new file mode 100644 index 0000000..9e0d6ac --- /dev/null +++ b/.gemini/GEMINI.md @@ -0,0 +1,13 @@ +# Doot — Gemini CLI Instructions + +This repository uses a centralized agent configuration. All agents should refer to the following location for rules, standards, and the current worklog: + +**Primary Source of Truth:** `.agent/config.md` + +## Quick Reference +- **Worklog:** `.agent/worklog.md` +- **Design:** `.agent/design.md` +- **Coding Standards:** `.agent/coding_standards.md` +- **UX Philosophy:** `.agent/ux_philosophy.md` + +Refer to `.agent/config.md` before performing any tasks. These project-specific instructions take absolute precedence over general defaults. diff --git a/.gemini/skills/bug-manager/SKILL.md b/.gemini/skills/bug-manager/SKILL.md new file mode 100644 index 0000000..bf48357 --- /dev/null +++ b/.gemini/skills/bug-manager/SKILL.md @@ -0,0 +1,34 @@ +--- +name: bug-manager +description: Manage, list, and resolve production bugs using custom project scripts. Use when investigating reported errors, viewing production logs, or marking bugs as resolved in the dashboard's database. +--- + +# Bug Manager + +This skill formalizes the workflow for handling production bugs in the `doot` project. + +## Workflow + +### 1. Identify Bugs +- Run `./scripts/bugs` to list currently open production bugs. +- Each bug has an `id`, a `description`, and a `created_at` timestamp. + +### 2. Diagnose with Logs +- Use `./scripts/logs` to investigate. +- For recent errors: `./scripts/logs -n 100` +- To filter for errors: `./scripts/logs --grep "error"` +- Refer to [references/bug-triage.md](references/bug-triage.md) for common error patterns. + +### 3. Fix and Verify +- Implement a reproduction test locally. +- Apply the fix. +- Deploy via `./scripts/deploy`. + +### 4. Resolve +- Mark the bug as resolved using `./scripts/resolve-bug <id>`. +- This script also displays the bug description before deletion for confirmation. + +## Quick Start +- "List all production bugs" -> `./scripts/bugs` +- "Show me the last 50 lines of production logs" -> `./scripts/logs -n 50` +- "Mark bug #12 as resolved" -> `./scripts/resolve-bug 12` diff --git a/.gemini/skills/bug-manager/references/bug-triage.md b/.gemini/skills/bug-manager/references/bug-triage.md new file mode 100644 index 0000000..45fe6cf --- /dev/null +++ b/.gemini/skills/bug-manager/references/bug-triage.md @@ -0,0 +1,36 @@ +# Bug Triage & Resolution Guide + +This reference provides a structured approach to managing production bugs using the `doot` project's custom scripts. + +## 1. Listing Bugs +Use `./scripts/bugs` to list all currently open bugs from the production database. +Bugs are stored in the `bugs` table with `id`, `description`, and `created_at`. + +## 2. Investigating Logs +When a bug is reported, always check the production logs first. +Use `./scripts/logs` with various flags: +- `scripts/logs -n 200`: View the last 200 lines. +- `scripts/logs --since "1 hour ago"`: View recent logs. +- `scripts/logs --grep "error"`: Search for specific error patterns. +- `scripts/logs -f`: Follow logs in real-time (useful for reproduction). + +### Common Error Patterns +- **Database Locked:** SQLite `database is locked` errors usually indicate concurrent writes or long-running transactions. +- **Unauthorized:** Check for `401` or `403` status codes in API clients (Todoist, Trello, etc.). +- **Template Errors:** Look for `template: ...: executing ...: nil pointer evaluating ...` which indicates missing data in the renderer. + +## 3. Reproduction +Before applying a fix, attempt to reproduce the bug locally. +1. Use `go test ./...` to ensure the current state is stable. +2. Create a new test case in the relevant `*_test.go` file that specifically triggers the reported bug. +3. Confirm the test fails. + +## 4. Resolution +Once a fix is implemented and verified locally: +1. Deploy the fix using `./scripts/deploy`. +2. Verify the fix in production if possible (e.g., by checking logs). +3. Use `./scripts/resolve-bug <id>` to mark the bug as resolved in the production database. + +## 5. Verification +After resolving, run `./scripts/bugs` again to ensure it no longer appears in the list. +Update `SESSION_STATE.md` to reflect the completed bug fix. @@ -1,71 +1,13 @@ -# Task Dashboard — Project Guidelines +# Doot — Agent Instructions -## Overview -A unified web dashboard aggregating Trello, Todoist, PlanToEat, Google Calendar, and Google Tasks. -**Stack:** Go 1.24 + chi router + HTMX + Tailwind CSS + SQLite. +This repository uses a centralized agent configuration. All agents should refer to the following location for rules, standards, and the current worklog: -## Development Standards -See `~/.claude/CLAUDE.md` for methodology (TDD, workflow, state management, efficiency, git practices). -Agent roles defined in `~/.claude/roles/`. +**Primary Source of Truth:** `.agent/config.md` -## Key Documents +## Quick Reference +- **Worklog:** `.agent/worklog.md` +- **Design:** `.agent/design.md` +- **Coding Standards:** `.agent/coding_standards.md` +- **UX Philosophy:** `.agent/ux_philosophy.md` -| Document | Purpose | When to Read | -|----------|---------|--------------| -| `DESIGN.md` | Architecture, features, visual design, dev patterns | Before any significant work | -| `SESSION_STATE.md` | Current task state, next steps | Start of every session | -| `docs/adr/*.md` | Architecture Decision Records | Before implementing features in that area | - -## Essential Commands -- **Run:** `go run cmd/dashboard/main.go` -- **Test:** `go test ./...` -- **Build:** `go build -o dashboard cmd/dashboard/main.go` -- **Deploy:** `./scripts/deploy` (builds with ldflags, syncs locally, restarts service) - -## Debugging -- **Production logs:** `./scripts/logs` — fetches journalctl from the local service - - `./scripts/logs -n 100` last N lines - - `./scripts/logs -f` follow - - `./scripts/logs --since "1 hour ago"` - - Pipe through `grep` to filter: `./scripts/logs -n 500 2>&1 | grep -i error` -- **View bugs:** `./scripts/bugs` — lists open bugs from local production database -- **Resolve bug:** `./scripts/resolve-bug <id>` — marks a bug as resolved -- **Always check logs first** when debugging reported issues - -## Technical Context -- **Trello is PRIMARY:** Key + Token required in query params. -- **Architecture:** chi router → Handlers (`internal/handlers/`) → Store (`internal/store/sqlite.go`). -- **Errors:** Partial data/cache fallback preferred over hard failure. -- **Full details:** See `DESIGN.md` → Architecture section. - -## Coding Style -- Use concise, idiomatic Go. -- Avoid verbose explanations or comments for self-evident logic. -- Prioritize terminal-based verification over manual code review. -- **Patterns:** See `DESIGN.md` → Development Guide for handler/template patterns. - -## Configuration Reference - -**Required:** -- `TODOIST_API_KEY` — Todoist API key -- `TRELLO_API_KEY` — Trello API key -- `TRELLO_TOKEN` — Trello token -- `DEFAULT_PASS` — Admin password - -**Optional:** -- `DEFAULT_USER` (default: "admin") -- `PLANTOEAT_SESSION` — PlanToEat session cookie -- `PLANTOEAT_API_KEY` — PlanToEat API key -- `GOOGLE_CREDENTIALS_FILE` — OAuth credentials JSON path -- `GOOGLE_CALENDAR_ID` (default: "primary") — comma-separated for multiple -- `GOOGLE_TASKS_LIST_ID` (default: "@default") -- `WEBAUTHN_RP_ID` — Passkey Relying Party ID (e.g., "doot.terst.org") -- `WEBAUTHN_ORIGIN` — Passkey expected origin (e.g., "https://doot.terst.org") -- `DATABASE_PATH` (default: "./dashboard.db") -- `PORT` (default: "8080") -- `CACHE_TTL_MINUTES` (default: 5) -- `TIMEZONE` (default: "Pacific/Honolulu") -- `TEMPLATE_DIR` (default: "web/templates") -- `STATIC_DIR` (default: "web/static") -- `MIGRATION_DIR` (default: "migrations") -- `DEBUG` (default: false) +Refer to `.agent/config.md` before performing any tasks. diff --git a/IMPLEMENTOR.md b/IMPLEMENTOR.md deleted file mode 100644 index ba77c2b..0000000 --- a/IMPLEMENTOR.md +++ /dev/null @@ -1,47 +0,0 @@ -# Implementation Plan: Timeline Feature - -## Phase 1: Database Layer (Store) -**Goal:** Enable fetching data by date range. - -1. **Update Interface:** Modify `internal/store/store.go` to add: - * `GetTasksByDateRange(start, end time.Time) ([]models.Task, error)` - * `GetMealsByDateRange(start, end time.Time) ([]models.Meal, error)` - * `GetCardsByDateRange(start, end time.Time) ([]models.Card, error)` -2. **Update Implementation:** Modify `internal/store/sqlite.go` to implement these methods using SQL queries. -3. **Test:** Create/Update `internal/store/store_test.go` to verify the queries work correctly. - -## Phase 2: Core Logic & Models -**Goal:** Aggregate and normalize data into a timeline structure. - -1. **Create Models:** Create `internal/models/timeline.go` with `TimelineItem` and `TimelineItemType`. -2. **Create Logic:** Create `internal/handlers/timeline_logic.go`. - * Implement `BuildTimeline(store Store, calendarClient *api.GoogleCalendarClient, start, end time.Time)`. - * **Logic:** - * Fetch Tasks, Meals, Cards from Store. - * Fetch Events from Calendar Client. - * Convert all to `TimelineItem`. - * **Apply Meal Defaults:** - * If `MealType` == "breakfast" -> Set time to 08:00 - * If `MealType` == "lunch" -> Set time to 12:00 - * If `MealType` == "dinner" -> Set time to 19:00 - * Else -> Set time to 12:00 - * Sort items by `Time`. -3. **Test:** Create `internal/handlers/timeline_logic_test.go` to test merging and sorting (TDD). - -## Phase 3: HTTP Handler & Routing -**Goal:** Expose the timeline via HTTP. - -1. **Create Handler:** Create `internal/handlers/timeline.go`. - * Inject `Store` and `GoogleCalendarClient`. - * Parse query params (`start`, `days`). - * Call `BuildTimeline`. - * Render template. -2. **Register Route:** Update `cmd/dashboard/main.go` to map `/timeline` to the new handler. - -## Phase 4: UI -**Goal:** Visualize the timeline. - -1. **Create Template:** Create `web/templates/partials/timeline-tab.html`. - * Iterate over items grouped by day. - * Show time, icon, title, and details. -2. **Update Main View:** Update `web/templates/index.html` to add the "Timeline" tab and HTMX trigger. diff --git a/QUICKSTART.md b/QUICKSTART.md new file mode 100644 index 0000000..f933506 --- /dev/null +++ b/QUICKSTART.md @@ -0,0 +1,48 @@ +# Doot Quickstart Guide + +This guide will help you get **Doot** up and running in under 5 minutes. + +## 1. Prerequisites +- Go 1.24+ +- SQLite3 +- Node.js (for Tailwind build) + +## 2. Setup (3 Minutes) + +```bash +# 1. Clone & Install +git clone <repo_url> && cd doot +go mod download +npm install + +# 2. Configure Environment +cp .env.example .env +# Open .env and add your TRELLO_API_KEY, TRELLO_TOKEN, and TODOIST_API_KEY. + +# 3. Build & Run +npm run build # Generates CSS +go run cmd/dashboard/main.go +``` + +## 3. Configuration Reference + +| Variable | Description | +|----------|-------------| +| `TODOIST_API_KEY` | Your Todoist API Token | +| `TRELLO_API_KEY` | Your Trello API Key | +| `TRELLO_TOKEN` | Your Trello Token | +| `DEFAULT_PASS` | Admin password for login | + +## 4. AI Agent Integration + +Doot provides a specialized API for AI agents to access your context safely. + +1. **Enable API:** Set `AI_AGENT_API_KEY` in your `.env`. +2. **Access Context:** Use the internal endpoint `/agent/context` for a JSON snapshot of your day. +3. **Browser Approval:** When an agent requests access, a notification will appear in your Doot dashboard for you to approve or deny. + +## 5. Maintenance Commands + +- **Run Tests:** `go test ./...` +- **View Logs:** `./scripts/logs` +- **Deploy:** `./scripts/deploy` diff --git a/bug-manager/SKILL.md b/bug-manager/SKILL.md new file mode 100644 index 0000000..bf48357 --- /dev/null +++ b/bug-manager/SKILL.md @@ -0,0 +1,34 @@ +--- +name: bug-manager +description: Manage, list, and resolve production bugs using custom project scripts. Use when investigating reported errors, viewing production logs, or marking bugs as resolved in the dashboard's database. +--- + +# Bug Manager + +This skill formalizes the workflow for handling production bugs in the `doot` project. + +## Workflow + +### 1. Identify Bugs +- Run `./scripts/bugs` to list currently open production bugs. +- Each bug has an `id`, a `description`, and a `created_at` timestamp. + +### 2. Diagnose with Logs +- Use `./scripts/logs` to investigate. +- For recent errors: `./scripts/logs -n 100` +- To filter for errors: `./scripts/logs --grep "error"` +- Refer to [references/bug-triage.md](references/bug-triage.md) for common error patterns. + +### 3. Fix and Verify +- Implement a reproduction test locally. +- Apply the fix. +- Deploy via `./scripts/deploy`. + +### 4. Resolve +- Mark the bug as resolved using `./scripts/resolve-bug <id>`. +- This script also displays the bug description before deletion for confirmation. + +## Quick Start +- "List all production bugs" -> `./scripts/bugs` +- "Show me the last 50 lines of production logs" -> `./scripts/logs -n 50` +- "Mark bug #12 as resolved" -> `./scripts/resolve-bug 12` diff --git a/bug-manager/references/bug-triage.md b/bug-manager/references/bug-triage.md new file mode 100644 index 0000000..45fe6cf --- /dev/null +++ b/bug-manager/references/bug-triage.md @@ -0,0 +1,36 @@ +# Bug Triage & Resolution Guide + +This reference provides a structured approach to managing production bugs using the `doot` project's custom scripts. + +## 1. Listing Bugs +Use `./scripts/bugs` to list all currently open bugs from the production database. +Bugs are stored in the `bugs` table with `id`, `description`, and `created_at`. + +## 2. Investigating Logs +When a bug is reported, always check the production logs first. +Use `./scripts/logs` with various flags: +- `scripts/logs -n 200`: View the last 200 lines. +- `scripts/logs --since "1 hour ago"`: View recent logs. +- `scripts/logs --grep "error"`: Search for specific error patterns. +- `scripts/logs -f`: Follow logs in real-time (useful for reproduction). + +### Common Error Patterns +- **Database Locked:** SQLite `database is locked` errors usually indicate concurrent writes or long-running transactions. +- **Unauthorized:** Check for `401` or `403` status codes in API clients (Todoist, Trello, etc.). +- **Template Errors:** Look for `template: ...: executing ...: nil pointer evaluating ...` which indicates missing data in the renderer. + +## 3. Reproduction +Before applying a fix, attempt to reproduce the bug locally. +1. Use `go test ./...` to ensure the current state is stable. +2. Create a new test case in the relevant `*_test.go` file that specifically triggers the reported bug. +3. Confirm the test fails. + +## 4. Resolution +Once a fix is implemented and verified locally: +1. Deploy the fix using `./scripts/deploy`. +2. Verify the fix in production if possible (e.g., by checking logs). +3. Use `./scripts/resolve-bug <id>` to mark the bug as resolved in the production database. + +## 5. Verification +After resolving, run `./scripts/bugs` again to ensure it no longer appears in the list. +Update `SESSION_STATE.md` to reflect the completed bug fix. diff --git a/instructions.md b/instructions.md deleted file mode 100644 index b01168b..0000000 --- a/instructions.md +++ /dev/null @@ -1,77 +0,0 @@ -# Surgical Instructions: Wire Up Authentication - -## Context -The `internal/auth` package is fully implemented, and the database migrations are ready. We need to wire everything up in `cmd/dashboard/main.go` and ensure the application is protected. - -## Plan -1. **Update `cmd/dashboard/main.go`** to initialize sessions, auth service, and protect routes. -2. **Verify** the login flow. - -## Step 1: Update `cmd/dashboard/main.go` - -**Action:** Edit `cmd/dashboard/main.go`. - -**Imports to Add:** -```go -"github.com/alexedwards/scs/v2" -"github.com/alexedwards/scs/sqlite3store" -"task-dashboard/internal/auth" -``` - -**Changes in `main()` function:** - -1. **Initialize Session Manager** (After `store` init, before `router` init): - ```go - // Initialize Session Manager - sessionManager := scs.New() - sessionManager.Store = sqlite3store.New(store.DB()) - sessionManager.Lifetime = 24 * time.Hour - sessionManager.Cookie.Persist = true - sessionManager.Cookie.SameSite = http.SameSiteLaxMode - sessionManager.Cookie.Secure = !cfg.Debug - ``` - -2. **Initialize Auth Service & Handlers** (After `templates` init): - ```go - // Initialize Auth - authService := auth.NewService(store.DB()) - // Ensure default admin user exists (for development/first run) - if err := authService.EnsureDefaultUser("admin", "admin"); err != nil { - log.Printf("WARNING: Failed to ensure default user: %v", err) - } - - authHandlers := auth.NewHandlers(authService, sessionManager, tmpl) - ``` - -3. **Configure Router Middleware & Routes**: - * Add `r.Use(sessionManager.LoadAndSave)` to the global middleware stack. - * **Refactor Routes**: - * Keep `/static/*` public. - * Add Public Auth Routes: - ```go - r.Get("/login", authHandlers.HandleLoginPage) - r.Post("/login", authHandlers.HandleLogin) - r.Post("/logout", authHandlers.HandleLogout) - ``` - * **Protect Application Routes**: Wrap the main application routes in a group using `RequireAuth`. - ```go - r.Group(func(r chi.Router) { - r.Use(authHandlers.Middleware().RequireAuth) - - // Move existing application routes here: - r.Get("/", handlers.HandleHome) - r.Get("/tabs/{type}", handlers.HandleTab) - // ... and any other app routes - }) - ``` - -## Step 2: Verification - -**Action:** -1. **Update Dependencies:** Run `go mod tidy` to ensure new imports are tracked correctly. -2. **Ensure CSS is built:** Run `npm run build` to generate `web/static/css/output.css`. -3. **Run the application:** `go run cmd/dashboard/main.go`. -4. **Verify Flow:** - * Accessing `/` should redirect to `/login`. - * Login with `admin` / `admin` should work and redirect to `/`. - * Logout should work and redirect to `/login`. diff --git a/internal/api/interfaces.go b/internal/api/interfaces.go index c9962c9..cec64be 100644 --- a/internal/api/interfaces.go +++ b/internal/api/interfaces.go @@ -15,7 +15,6 @@ type TodoistAPI interface { UpdateTask(ctx context.Context, taskID string, updates map[string]interface{}) error CompleteTask(ctx context.Context, taskID string) error ReopenTask(ctx context.Context, taskID string) error - Sync(ctx context.Context, syncToken string) (*TodoistSyncResponse, error) } // TrelloAPI defines the interface for Trello operations diff --git a/internal/api/todoist.go b/internal/api/todoist.go index be699ce..d6058d3 100644 --- a/internal/api/todoist.go +++ b/internal/api/todoist.go @@ -10,22 +10,19 @@ import ( ) const ( - todoistBaseURL = "https://api.todoist.com/api/v1" - todoistSyncBaseURL = "https://api.todoist.com/sync/v9" + todoistBaseURL = "https://api.todoist.com/api/v1" ) // TodoistClient handles interactions with the Todoist API type TodoistClient struct { BaseClient - syncClient BaseClient - apiKey string + apiKey string } // NewTodoistClient creates a new Todoist API client func NewTodoistClient(apiKey string) *TodoistClient { return &TodoistClient{ BaseClient: NewBaseClient(todoistBaseURL), - syncClient: NewBaseClient(todoistSyncBaseURL), apiKey: apiKey, } } @@ -53,43 +50,33 @@ type todoistProjectResponse struct { Name string `json:"name"` } -// Sync API v9 response types -// TodoistSyncResponse represents the Sync API response -type TodoistSyncResponse struct { - SyncToken string `json:"sync_token"` - FullSync bool `json:"full_sync"` - Items []SyncItemResponse `json:"items"` - Projects []SyncProjectResponse `json:"projects"` -} - -// SyncItemResponse represents a task item from Sync API -type SyncItemResponse struct { - ID string `json:"id"` - Content string `json:"content"` - Description string `json:"description"` - ProjectID string `json:"project_id"` - Priority int `json:"priority"` - Labels []string `json:"labels"` - Due *dueInfo `json:"due"` - IsCompleted bool `json:"is_completed"` - IsDeleted bool `json:"is_deleted"` - AddedAt string `json:"added_at"` -} - -// SyncProjectResponse represents a project from Sync API -type SyncProjectResponse struct { - ID string `json:"id"` - Name string `json:"name"` - IsDeleted bool `json:"is_deleted"` +// todoistTasksPage represents the paginated response from the Todoist REST API v1 +type todoistTasksPage struct { + Results []todoistTaskResponse `json:"results"` + NextCursor *string `json:"next_cursor"` } // GetTasks fetches all active tasks from Todoist func (c *TodoistClient) GetTasks(ctx context.Context) ([]models.Task, error) { - var apiTasks []todoistTaskResponse - if err := c.Get(ctx, "/tasks", c.authHeaders(), &apiTasks); err != nil { - return nil, fmt.Errorf("failed to fetch tasks: %w", err) + var allTasks []todoistTaskResponse + cursor := "" + for { + path := "/tasks" + if cursor != "" { + path = "/tasks?cursor=" + cursor + } + var page todoistTasksPage + if err := c.Get(ctx, path, c.authHeaders(), &page); err != nil { + return nil, fmt.Errorf("failed to fetch tasks: %w", err) + } + allTasks = append(allTasks, page.Results...) + if page.NextCursor == nil || *page.NextCursor == "" { + break + } + cursor = *page.NextCursor } + apiTasks := allTasks // Fetch projects to get project names projects, err := c.GetProjects(ctx) @@ -129,12 +116,32 @@ func (c *TodoistClient) GetTasks(ctx context.Context) ([]models.Task, error) { return tasks, nil } +// todoistProjectsPage represents the paginated response for projects +type todoistProjectsPage struct { + Results []todoistProjectResponse `json:"results"` + NextCursor *string `json:"next_cursor"` +} + // GetProjects fetches all projects func (c *TodoistClient) GetProjects(ctx context.Context) ([]models.Project, error) { - var apiProjects []todoistProjectResponse - if err := c.Get(ctx, "/projects", c.authHeaders(), &apiProjects); err != nil { - return nil, fmt.Errorf("failed to fetch projects: %w", err) + var allProjects []todoistProjectResponse + cursor := "" + for { + path := "/projects" + if cursor != "" { + path = "/projects?cursor=" + cursor + } + var page todoistProjectsPage + if err := c.Get(ctx, path, c.authHeaders(), &page); err != nil { + return nil, fmt.Errorf("failed to fetch projects: %w", err) + } + allProjects = append(allProjects, page.Results...) + if page.NextCursor == nil || *page.NextCursor == "" { + break + } + cursor = *page.NextCursor } + apiProjects := allProjects projects := make([]models.Project, 0, len(apiProjects)) for _, apiProj := range apiProjects { @@ -147,76 +154,6 @@ func (c *TodoistClient) GetProjects(ctx context.Context) ([]models.Project, erro return projects, nil } -// Sync performs an incremental sync using the Sync API v9 -func (c *TodoistClient) Sync(ctx context.Context, syncToken string) (*TodoistSyncResponse, error) { - if syncToken == "" { - syncToken = "*" // Full sync - } - - payload := map[string]interface{}{ - "sync_token": syncToken, - "resource_types": []string{"items", "projects"}, - } - - var syncResp TodoistSyncResponse - if err := c.syncClient.Post(ctx, "/sync", c.authHeaders(), payload, &syncResp); err != nil { - return nil, fmt.Errorf("failed to perform sync: %w", err) - } - - return &syncResp, nil -} - -// ConvertSyncItemToTask converts a single sync item to a Task model. -// Returns the task and true if the item is active, or a zero Task and false if it should be skipped. -func ConvertSyncItemToTask(item SyncItemResponse, projectMap map[string]string) (models.Task, bool) { - if item.IsCompleted || item.IsDeleted { - return models.Task{}, false - } - - task := models.Task{ - ID: item.ID, - Content: item.Content, - Description: item.Description, - ProjectID: item.ProjectID, - ProjectName: projectMap[item.ProjectID], - Priority: item.Priority, - Completed: false, - Labels: item.Labels, - URL: fmt.Sprintf("https://todoist.com/app/task/%s", item.ID), - } - - if item.AddedAt != "" { - if createdAt, err := time.Parse(time.RFC3339, item.AddedAt); err == nil { - task.CreatedAt = createdAt - } - } - - task.DueDate = parseDueDate(item.Due) - return task, true -} - -// ConvertSyncItemsToTasks converts sync API items to Task models -func ConvertSyncItemsToTasks(items []SyncItemResponse, projectMap map[string]string) []models.Task { - tasks := make([]models.Task, 0, len(items)) - for _, item := range items { - if task, ok := ConvertSyncItemToTask(item, projectMap); ok { - tasks = append(tasks, task) - } - } - return tasks -} - -// BuildProjectMapFromSync builds a project ID to name map from sync response -func BuildProjectMapFromSync(projects []SyncProjectResponse) map[string]string { - projectMap := make(map[string]string) - for _, proj := range projects { - if !proj.IsDeleted { - projectMap[proj.ID] = proj.Name - } - } - return projectMap -} - // CreateTask creates a new task in Todoist func (c *TodoistClient) CreateTask(ctx context.Context, content, projectID string, dueDate *time.Time, priority int) (*models.Task, error) { payload := map[string]interface{}{"content": content} diff --git a/internal/api/todoist_test.go b/internal/api/todoist_test.go index 2204469..99b9e80 100644 --- a/internal/api/todoist_test.go +++ b/internal/api/todoist_test.go @@ -14,7 +14,6 @@ import ( func newTestTodoistClient(baseURL, apiKey string) *TodoistClient { client := NewTodoistClient(apiKey) client.BaseURL = baseURL - client.syncClient.BaseURL = baseURL return client } @@ -220,9 +219,11 @@ func TestTodoistClient_GetProjects(t *testing.T) { } // Return mock response - response := []todoistProjectResponse{ - {ID: "proj-1", Name: "Project 1"}, - {ID: "proj-2", Name: "Project 2"}, + response := todoistProjectsPage{ + Results: []todoistProjectResponse{ + {ID: "proj-1", Name: "Project 1"}, + {ID: "proj-2", Name: "Project 2"}, + }, } w.Header().Set("Content-Type", "application/json") @@ -265,17 +266,21 @@ func TestTodoistClient_GetTasks(t *testing.T) { // GetTasks also calls GetProjects internally if r.URL.Path == "/projects" { - response := []todoistProjectResponse{ - {ID: "proj-1", Name: "Project 1"}, + response := todoistProjectsPage{ + Results: []todoistProjectResponse{ + {ID: "proj-1", Name: "Project 1"}, + }, } json.NewEncoder(w).Encode(response) return } if r.URL.Path == "/tasks" { - response := []todoistTaskResponse{ - {ID: "task-1", Content: "Task 1", ProjectID: "proj-1", CreatedAt: time.Now().Format(time.RFC3339)}, - {ID: "task-2", Content: "Task 2", ProjectID: "proj-1", CreatedAt: time.Now().Format(time.RFC3339)}, + response := todoistTasksPage{ + Results: []todoistTaskResponse{ + {ID: "task-1", Content: "Task 1", ProjectID: "proj-1", CreatedAt: time.Now().Format(time.RFC3339)}, + {ID: "task-2", Content: "Task 2", ProjectID: "proj-1", CreatedAt: time.Now().Format(time.RFC3339)}, + }, } json.NewEncoder(w).Encode(response) return @@ -345,146 +350,3 @@ func TestTodoistClient_UpdateTask(t *testing.T) { } } -func TestTodoistClient_Sync(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Method != "POST" { - t.Errorf("Expected POST, got %s", r.Method) - } - if r.URL.Path != "/sync" { - t.Errorf("Expected path /sync, got %s", r.URL.Path) - } - - response := TodoistSyncResponse{ - SyncToken: "new-sync-token", - FullSync: true, - Items: []SyncItemResponse{ - {ID: "item-1", Content: "Item 1", ProjectID: "proj-1"}, - }, - Projects: []SyncProjectResponse{ - {ID: "proj-1", Name: "Project 1"}, - }, - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(response) - })) - defer server.Close() - - client := newTestTodoistClient(server.URL, "test-key") - resp, err := client.Sync(context.Background(), "*") - if err != nil { - t.Fatalf("Sync failed: %v", err) - } - - if resp.SyncToken != "new-sync-token" { - t.Errorf("Expected sync token 'new-sync-token', got '%s'", resp.SyncToken) - } - if len(resp.Items) != 1 { - t.Errorf("Expected 1 item, got %d", len(resp.Items)) - } -} - -func TestConvertSyncItemsToTasks(t *testing.T) { - projects := map[string]string{ - "proj-1": "Project 1", - } - - items := []SyncItemResponse{ - { - ID: "item-1", - Content: "Task 1", - Description: "Description 1", - ProjectID: "proj-1", - Priority: 3, - Labels: []string{"label1"}, - }, - { - ID: "item-2", - Content: "Completed Task", - ProjectID: "proj-1", - IsCompleted: true, - }, - } - - tasks := ConvertSyncItemsToTasks(items, projects) - - // Should skip completed task - if len(tasks) != 1 { - t.Errorf("Expected 1 task (excluding completed), got %d", len(tasks)) - } - - if tasks[0].ID != "item-1" { - t.Errorf("Expected task ID 'item-1', got '%s'", tasks[0].ID) - } - if tasks[0].ProjectName != "Project 1" { - t.Errorf("Expected project name 'Project 1', got '%s'", tasks[0].ProjectName) - } -} - -func TestConvertSyncItemToTask(t *testing.T) { - projects := map[string]string{"proj-1": "Project 1"} - - t.Run("active item returns task and true", func(t *testing.T) { - item := SyncItemResponse{ - ID: "item-1", - Content: "Active Task", - Description: "desc", - ProjectID: "proj-1", - Priority: 2, - Labels: []string{"work"}, - AddedAt: "2026-01-01T00:00:00Z", - } - task, ok := ConvertSyncItemToTask(item, projects) - if !ok { - t.Fatal("expected ok=true for active item") - } - if task.ID != "item-1" { - t.Errorf("expected ID 'item-1', got '%s'", task.ID) - } - if task.Content != "Active Task" { - t.Errorf("expected Content 'Active Task', got '%s'", task.Content) - } - if task.ProjectName != "Project 1" { - t.Errorf("expected ProjectName 'Project 1', got '%s'", task.ProjectName) - } - if task.Completed { - t.Error("expected Completed=false") - } - if task.URL != "https://todoist.com/app/task/item-1" { - t.Errorf("unexpected URL: %s", task.URL) - } - }) - - t.Run("completed item returns false", func(t *testing.T) { - item := SyncItemResponse{ID: "item-2", Content: "Done", ProjectID: "proj-1", IsCompleted: true} - _, ok := ConvertSyncItemToTask(item, projects) - if ok { - t.Error("expected ok=false for completed item") - } - }) - - t.Run("deleted item returns false", func(t *testing.T) { - item := SyncItemResponse{ID: "item-3", Content: "Gone", ProjectID: "proj-1", IsDeleted: true} - _, ok := ConvertSyncItemToTask(item, projects) - if ok { - t.Error("expected ok=false for deleted item") - } - }) -} - -func TestBuildProjectMapFromSync(t *testing.T) { - projects := []SyncProjectResponse{ - {ID: "proj-1", Name: "Project 1"}, - {ID: "proj-2", Name: "Project 2"}, - } - - projectMap := BuildProjectMapFromSync(projects) - - if len(projectMap) != 2 { - t.Errorf("Expected 2 projects in map, got %d", len(projectMap)) - } - - if projectMap["proj-1"] != "Project 1" { - t.Errorf("Expected 'Project 1', got '%s'", projectMap["proj-1"]) - } -} diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index c2e903f..b0fd952 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -327,10 +327,9 @@ func filterAndSortTrelloTasks(boards []models.Board) []models.Card { return tasks } -// fetchTasks fetches tasks from cache or API using incremental sync +// fetchTasks fetches tasks from cache or API func (h *Handler) fetchTasks(ctx context.Context, forceRefresh bool) ([]models.Task, error) { cacheKey := store.CacheKeyTodoistTasks - syncService := "todoist" // Check cache validity if !forceRefresh { @@ -340,22 +339,9 @@ func (h *Handler) fetchTasks(ctx context.Context, forceRefresh bool) ([]models.T } } - // Get stored sync token (empty string means full sync) - syncToken, err := h.store.GetSyncToken(syncService) + tasks, err := h.todoistClient.GetTasks(ctx) if err != nil { - log.Printf("Failed to get sync token, will do full sync: %v", err) - syncToken = "" - } - - // Force full sync if requested - if forceRefresh { - syncToken = "" - } - - // Fetch using Sync API - syncResp, err := h.todoistClient.Sync(ctx, syncToken) - if err != nil { - // Try to return cached data even if stale + // Fall back to cached data if available cachedTasks, cacheErr := h.store.GetTasks() if cacheErr == nil && len(cachedTasks) > 0 { return cachedTasks, nil @@ -363,49 +349,15 @@ func (h *Handler) fetchTasks(ctx context.Context, forceRefresh bool) ([]models.T return nil, err } - // Build project map from sync response - projectMap := api.BuildProjectMapFromSync(syncResp.Projects) - - // Process sync response - if syncResp.FullSync { - // Full sync: replace all tasks - tasks := api.ConvertSyncItemsToTasks(syncResp.Items, projectMap) - if err := h.store.SaveTasks(tasks); err != nil { - log.Printf("Failed to save tasks to cache: %v", err) - } - } else { - // Incremental sync: merge changes - var deletedIDs []string - for _, item := range syncResp.Items { - if item.IsDeleted || item.IsCompleted { - deletedIDs = append(deletedIDs, item.ID) - } else { - // Upsert active task - task, _ := api.ConvertSyncItemToTask(item, projectMap) - if err := h.store.UpsertTask(task); err != nil { - log.Printf("Failed to upsert task %s: %v", item.ID, err) - } - } - } - // Delete removed tasks - if len(deletedIDs) > 0 { - if err := h.store.DeleteTasksByIDs(deletedIDs); err != nil { - log.Printf("Failed to delete tasks: %v", err) - } - } - } - - // Store the new sync token - if err := h.store.SetSyncToken(syncService, syncResp.SyncToken); err != nil { - log.Printf("Failed to save sync token: %v", err) + if err := h.store.SaveTasks(tasks); err != nil { + log.Printf("Failed to save tasks to cache: %v", err) } - // Update cache metadata if err := h.store.UpdateCacheMetadata(cacheKey, h.config.CacheTTLMinutes); err != nil { log.Printf("Failed to update cache metadata: %v", err) } - return h.store.GetTasks() + return tasks, nil } // fetchMeals fetches meals from cache or API @@ -1022,7 +974,7 @@ type CombinedMeal struct { // HandleTabMeals renders the Meals tab (PlanToEat) func (h *Handler) HandleTabMeals(w http.ResponseWriter, r *http.Request) { - startDate := config.Now() + startDate := config.Today() endDate := startDate.AddDate(0, 0, 7) meals, err := h.store.GetMeals(startDate, endDate) diff --git a/internal/handlers/handlers_test.go b/internal/handlers/handlers_test.go index 793ccdd..0d097c8 100644 --- a/internal/handlers/handlers_test.go +++ b/internal/handlers/handlers_test.go @@ -15,7 +15,6 @@ import ( "github.com/go-chi/chi/v5" - "task-dashboard/internal/api" "task-dashboard/internal/config" "task-dashboard/internal/models" "task-dashboard/internal/store" @@ -118,31 +117,6 @@ func (m *mockTodoistClient) ReopenTask(ctx context.Context, taskID string) error return nil } -func (m *mockTodoistClient) Sync(ctx context.Context, syncToken string) (*api.TodoistSyncResponse, error) { - if m.err != nil { - return nil, m.err - } - // Return a mock sync response with tasks converted to sync items - items := make([]api.SyncItemResponse, 0, len(m.tasks)) - for _, task := range m.tasks { - items = append(items, api.SyncItemResponse{ - ID: task.ID, - Content: task.Content, - Description: task.Description, - ProjectID: task.ProjectID, - Priority: task.Priority, - Labels: task.Labels, - IsCompleted: task.Completed, - IsDeleted: false, - }) - } - return &api.TodoistSyncResponse{ - SyncToken: "test-sync-token", - FullSync: true, - Items: items, - Projects: []api.SyncProjectResponse{}, - }, nil -} // mockTrelloClient creates a mock Trello client for testing type mockTrelloClient struct { @@ -2236,210 +2210,104 @@ func TestHandleTimeline_InvalidParams(t *testing.T) { } } -// syncAwareMockTodoist records the sync token passed to Sync and returns a configurable response. -type syncAwareMockTodoist struct { - mockTodoistClient - syncResponse *api.TodoistSyncResponse - receivedTokens []string // tracks tokens passed to Sync -} - -func (m *syncAwareMockTodoist) Sync(ctx context.Context, syncToken string) (*api.TodoistSyncResponse, error) { - m.receivedTokens = append(m.receivedTokens, syncToken) - if m.err != nil { - return nil, m.err - } - return m.syncResponse, nil -} - -func TestFetchTasks_IncrementalSync_UpsertsActiveTasks(t *testing.T) { +func TestFetchTasks_FetchesAndSavesTasks(t *testing.T) { h, cleanup := setupTestHandler(t) defer cleanup() - // Seed DB with an existing task (simulating previous full sync) - existingTask := models.Task{ID: "existing-1", Content: "Old task", Priority: 1} - if err := h.store.SaveTasks([]models.Task{existingTask}); err != nil { - t.Fatalf("Failed to seed task: %v", err) - } - // Set a sync token so fetchTasks uses incremental sync - if err := h.store.SetSyncToken("todoist", "previous-token"); err != nil { - t.Fatalf("Failed to set sync token: %v", err) - } - - mock := &syncAwareMockTodoist{ - syncResponse: &api.TodoistSyncResponse{ - SyncToken: "new-token-1", - FullSync: false, - Items: []api.SyncItemResponse{ - {ID: "new-1", Content: "New task", Priority: 2}, - {ID: "existing-1", Content: "Updated task", Priority: 3}, - }, - Projects: []api.SyncProjectResponse{}, + h.todoistClient = &mockTodoistClient{ + tasks: []models.Task{ + {ID: "t1", Content: "Task one", Priority: 1}, + {ID: "t2", Content: "Task two", Priority: 2}, }, } - h.todoistClient = mock tasks, err := h.fetchTasks(context.Background(), false) if err != nil { t.Fatalf("fetchTasks returned error: %v", err) } - - // Should have 2 tasks: the upserted existing-1 (updated) and new-1 if len(tasks) != 2 { t.Fatalf("Expected 2 tasks, got %d", len(tasks)) } - // Verify the existing task was updated - taskMap := make(map[string]models.Task) - for _, task := range tasks { - taskMap[task.ID] = task - } - if taskMap["existing-1"].Content != "Updated task" { - t.Errorf("Expected existing-1 content 'Updated task', got %q", taskMap["existing-1"].Content) + // Verify tasks are persisted in the store + stored, err := h.store.GetTasks() + if err != nil { + t.Fatalf("Failed to get tasks from store: %v", err) } - if taskMap["new-1"].Content != "New task" { - t.Errorf("Expected new-1 content 'New task', got %q", taskMap["new-1"].Content) + if len(stored) != 2 { + t.Errorf("Expected 2 stored tasks, got %d", len(stored)) } } -func TestFetchTasks_IncrementalSync_DeletesCompletedAndDeletedTasks(t *testing.T) { +func TestFetchTasks_ReturnsCachedTasksWhenValid(t *testing.T) { h, cleanup := setupTestHandler(t) defer cleanup() - // Seed DB with tasks - seeds := []models.Task{ - {ID: "keep-1", Content: "Keep me"}, - {ID: "complete-1", Content: "Will be completed"}, - {ID: "delete-1", Content: "Will be deleted"}, - } - if err := h.store.SaveTasks(seeds); err != nil { + // Seed cache with tasks and mark it valid + cached := []models.Task{{ID: "cached-1", Content: "Cached task"}} + if err := h.store.SaveTasks(cached); err != nil { t.Fatalf("Failed to seed tasks: %v", err) } - if err := h.store.SetSyncToken("todoist", "prev-token"); err != nil { - t.Fatalf("Failed to set sync token: %v", err) + if err := h.store.UpdateCacheMetadata(store.CacheKeyTodoistTasks, 60); err != nil { + t.Fatalf("Failed to set cache metadata: %v", err) } - mock := &syncAwareMockTodoist{ - syncResponse: &api.TodoistSyncResponse{ - SyncToken: "new-token-2", - FullSync: false, - Items: []api.SyncItemResponse{ - {ID: "complete-1", Content: "Will be completed", IsCompleted: true}, - {ID: "delete-1", Content: "Will be deleted", IsDeleted: true}, - }, - Projects: []api.SyncProjectResponse{}, - }, + // API would return different tasks — should not be called + h.todoistClient = &mockTodoistClient{ + tasks: []models.Task{{ID: "api-1", Content: "API task"}}, } - h.todoistClient = mock tasks, err := h.fetchTasks(context.Background(), false) if err != nil { t.Fatalf("fetchTasks returned error: %v", err) } - - // Only keep-1 should remain - if len(tasks) != 1 { - t.Fatalf("Expected 1 task, got %d: %+v", len(tasks), tasks) - } - if tasks[0].ID != "keep-1" { - t.Errorf("Expected remaining task ID 'keep-1', got %q", tasks[0].ID) + if len(tasks) != 1 || tasks[0].ID != "cached-1" { + t.Errorf("Expected cached task, got %+v", tasks) } } -func TestFetchTasks_IncrementalSync_StoresNewSyncToken(t *testing.T) { +func TestFetchTasks_ForceRefresh_BypassesCache(t *testing.T) { h, cleanup := setupTestHandler(t) defer cleanup() - if err := h.store.SetSyncToken("todoist", "old-token"); err != nil { - t.Fatalf("Failed to set sync token: %v", err) - } - - mock := &syncAwareMockTodoist{ - syncResponse: &api.TodoistSyncResponse{ - SyncToken: "brand-new-token", - FullSync: false, - Items: []api.SyncItemResponse{}, - Projects: []api.SyncProjectResponse{}, - }, - } - h.todoistClient = mock - - _, err := h.fetchTasks(context.Background(), false) - if err != nil { - t.Fatalf("fetchTasks returned error: %v", err) - } - - // Verify the new sync token was stored - token, err := h.store.GetSyncToken("todoist") - if err != nil { - t.Fatalf("Failed to get sync token: %v", err) + // Seed cache and mark it valid + if err := h.store.SaveTasks([]models.Task{{ID: "old-1", Content: "Old"}}); err != nil { + t.Fatalf("Failed to seed tasks: %v", err) } - if token != "brand-new-token" { - t.Errorf("Expected sync token 'brand-new-token', got %q", token) + if err := h.store.UpdateCacheMetadata(store.CacheKeyTodoistTasks, 60); err != nil { + t.Fatalf("Failed to set cache metadata: %v", err) } -} - -func TestFetchTasks_IncrementalSync_UsesSavedSyncToken(t *testing.T) { - h, cleanup := setupTestHandler(t) - defer cleanup() - // Set a known sync token - if err := h.store.SetSyncToken("todoist", "my-saved-token"); err != nil { - t.Fatalf("Failed to set sync token: %v", err) + h.todoistClient = &mockTodoistClient{ + tasks: []models.Task{{ID: "fresh-1", Content: "Fresh"}}, } - mock := &syncAwareMockTodoist{ - syncResponse: &api.TodoistSyncResponse{ - SyncToken: "next-token", - FullSync: false, - Items: []api.SyncItemResponse{}, - Projects: []api.SyncProjectResponse{}, - }, - } - h.todoistClient = mock - - _, err := h.fetchTasks(context.Background(), false) + tasks, err := h.fetchTasks(context.Background(), true) if err != nil { t.Fatalf("fetchTasks returned error: %v", err) } - - // Verify the saved token was passed to Sync - if len(mock.receivedTokens) != 1 { - t.Fatalf("Expected 1 Sync call, got %d", len(mock.receivedTokens)) - } - if mock.receivedTokens[0] != "my-saved-token" { - t.Errorf("Expected Sync to receive token 'my-saved-token', got %q", mock.receivedTokens[0]) + if len(tasks) != 1 || tasks[0].ID != "fresh-1" { + t.Errorf("Expected fresh task from API, got %+v", tasks) } } -func TestFetchTasks_ForceRefresh_ClearsSyncToken(t *testing.T) { +func TestFetchTasks_FallsBackToCacheOnAPIError(t *testing.T) { h, cleanup := setupTestHandler(t) defer cleanup() - if err := h.store.SetSyncToken("todoist", "existing-token"); err != nil { - t.Fatalf("Failed to set sync token: %v", err) + // Seed stale cache + if err := h.store.SaveTasks([]models.Task{{ID: "stale-1", Content: "Stale"}}); err != nil { + t.Fatalf("Failed to seed tasks: %v", err) } - mock := &syncAwareMockTodoist{ - syncResponse: &api.TodoistSyncResponse{ - SyncToken: "fresh-token", - FullSync: true, - Items: []api.SyncItemResponse{}, - Projects: []api.SyncProjectResponse{}, - }, - } - h.todoistClient = mock + h.todoistClient = &mockTodoistClient{err: fmt.Errorf("API error")} - _, err := h.fetchTasks(context.Background(), true) + tasks, err := h.fetchTasks(context.Background(), false) if err != nil { - t.Fatalf("fetchTasks returned error: %v", err) - } - - // forceRefresh should send empty token (full sync) - if len(mock.receivedTokens) != 1 { - t.Fatalf("Expected 1 Sync call, got %d", len(mock.receivedTokens)) + t.Fatalf("Expected fallback to cache, got error: %v", err) } - if mock.receivedTokens[0] != "" { - t.Errorf("Expected empty sync token for forceRefresh, got %q", mock.receivedTokens[0]) + if len(tasks) != 1 || tasks[0].ID != "stale-1" { + t.Errorf("Expected stale cached task as fallback, got %+v", tasks) } } diff --git a/internal/store/sqlite.go b/internal/store/sqlite.go index 33edbf2..166cd63 100644 --- a/internal/store/sqlite.go +++ b/internal/store/sqlite.go @@ -1229,7 +1229,7 @@ func (s *Store) GetCompletedTasks(limit int) ([]models.CompletedTask, error) { for rows.Next() { var task models.CompletedTask var dueDate sql.NullString - var completedAt string + var completedAt time.Time if err := rows.Scan(&task.ID, &task.Source, &task.SourceID, &task.Title, &dueDate, &completedAt); err != nil { return nil, err @@ -1240,9 +1240,7 @@ func (s *Store) GetCompletedTasks(limit int) ([]models.CompletedTask, error) { task.DueDate = &t } } - if t, err := time.Parse("2006-01-02 15:04:05", completedAt); err == nil { - task.CompletedAt = t - } + task.CompletedAt = completedAt tasks = append(tasks, task) } diff --git a/package.py b/package.py new file mode 100644 index 0000000..1e64be2 --- /dev/null +++ b/package.py @@ -0,0 +1,18 @@ +import zipfile +import os +import sys + +def package_skill(skill_path, output_filename): + with zipfile.ZipFile(output_filename, 'w', zipfile.ZIP_DEFLATED) as zipf: + for root, dirs, files in os.walk(skill_path): + for file in files: + file_path = os.path.join(root, file) + arcname = os.path.relpath(file_path, skill_path) + zipf.write(file_path, arcname) + print(f"Successfully packaged {skill_path} to {output_filename}") + +if __name__ == "__main__": + if len(sys.argv) < 3: + print("Usage: python3 package.py <skill_folder> <output_file>") + sys.exit(1) + package_skill(sys.argv[1], sys.argv[2]) diff --git a/scripts/clear-cache b/scripts/clear-cache new file mode 100755 index 0000000..624a068 --- /dev/null +++ b/scripts/clear-cache @@ -0,0 +1,17 @@ +#!/bin/bash +# Clear all cached data from the production database. +# Usage: ./scripts/clear-cache +# Deletes: cards, boards, tasks, calendar_events, cache_metadata, sync_tokens + +DB=/site/doot.terst.org/data/dashboard.db + +sqlite3 "$DB" " +DELETE FROM cards; +DELETE FROM boards; +DELETE FROM tasks; +DELETE FROM calendar_events; +DELETE FROM cache_metadata; +DELETE FROM sync_tokens; +" + +echo "Cache cleared." diff --git a/task-sync-manager/SKILL.md b/task-sync-manager/SKILL.md new file mode 100644 index 0000000..69d9bfb --- /dev/null +++ b/task-sync-manager/SKILL.md @@ -0,0 +1,29 @@ +--- +name: task-sync-manager +description: Manage and synchronize session state with task management tools. Use when updating SESSION_STATE.md, moving tasks to Recently Completed, or creating/completing stories in claudomator. +--- + +# Task Sync Manager + +This skill formalizes the synchronization of task progress between documentation and management tools. + +## Workflow + +### 1. Update Session State +- When a task or sub-task is completed, move it from **Current Focus** to **Recently Completed** in `SESSION_STATE.md`. +- Be specific about what was done (e.g., "Migration 017 added, tests passing"). +- Ensure **Next Steps** are clearly defined. + +### 2. Claudomator Integration +- After completing a task, verify if it corresponds to a `claudomator` story. +- Create new stories as needed: `claudomator create "Task Name" --instructions "Detailed goals"`. +- Use `claudomator list` to view open tasks. + +### 3. Maintain Documentation +- Ensure `SESSION_STATE.md` follows the structure in [references/session-state-standard.md](references/session-state-standard.md). +- Keep **Known Gaps** and **Remaining Items** up-to-date with findings from the current session. + +## Quick Start +- "Update session state to show Task X is done" -> Move Task X to Recently Completed. +- "Create a claudomator story for refactoring meals" -> `claudomator create "Refactor Meal Grouping" --instructions "RF-03 from REFACTOR_PLAN.md"`. +- "Show me the next steps" -> Check `SESSION_STATE.md`. diff --git a/task-sync-manager/references/session-state-standard.md b/task-sync-manager/references/session-state-standard.md new file mode 100644 index 0000000..d5149f2 --- /dev/null +++ b/task-sync-manager/references/session-state-standard.md @@ -0,0 +1,41 @@ +# Session State Standard + +This reference defines the expected structure and format for `SESSION_STATE.md` in the `doot` project. + +## Required Sections + +### 1. Current Focus +- A single sentence or a few bullet points describing the primary task for the current session. +- Only one focus should be active at a time. + +### 2. Recently Completed +- A list of tasks completed in the current or most recent sessions. +- Use a bulleted list with a summary of the work (e.g., "Migration 016, Store methods..."). +- Include tests written and verified. + +### 3. Previously Completed +- A historical list of completed tasks, grouped by milestone or theme. +- This section can be moved to a `HISTORY.md` if it grows too large. + +### 4. Known Gaps +- A list of outstanding bugs, missing tests, or implementation details that need future attention. + +### 5. Remaining Items (Feature Requests) +- A list of features that have been discussed but not yet implemented. +- These should often correspond to `claudomator` stories. + +### 6. Next Steps +- A numbered list of the very next actions to take. +- Use this to maintain momentum across sessions. + +## Claudomator Integration +Whenever a task is completed: +1. Move it from "Current Focus" to "Recently Completed". +2. Run `claudomator finish <story_id>` (if using ID-based stories). +3. Update "Next Steps" with the next `claudomator` story or sub-task. +4. If a new task is identified, run `claudomator create "Name" --instructions "Goals"`. + +## Formatting +- Use standard GitHub Flavored Markdown. +- Keep the file concise; avoid deep nesting. +- Ensure the date and baseline commit are mentioned in related plans (e.g., `REFACTOR_PLAN.md`). diff --git a/web/templates/partials/timeline-tab.html b/web/templates/partials/timeline-tab.html index 4979744..c82513d 100644 --- a/web/templates/partials/timeline-tab.html +++ b/web/templates/partials/timeline-tab.html @@ -113,8 +113,7 @@ hx-target="#tab-content" hx-swap="innerHTML"> - <!-- Today Section (Calendar View) --> - {{if .TodayItems}} + <!-- Today Section (Calendar View) — always shown so the NOW line is visible --> <details class="group" open> <summary class="text-lg font-semibold mb-3 flex items-center gap-2 text-white/90 cursor-pointer hover:text-white sticky top-0 bg-black/20 backdrop-blur-md py-2 z-10 rounded-lg px-2 list-none"> <span>📅</span> {{.TodayLabel}} @@ -125,6 +124,8 @@ </summary> <!-- Overdue + All-Day + Untimed Items Section --> + {{$hasUntimed := false}}{{range .TodayItems}}{{if or .IsOverdue .IsAllDay}}{{$hasUntimed = true}}{{end}}{{end}} + {{if $hasUntimed}} <div class="mb-4 flex flex-wrap gap-1 p-2 bg-black/20 rounded-lg" id="untimed-section"> {{range .TodayItems}} {{if or .IsOverdue .IsAllDay}} @@ -151,6 +152,7 @@ {{end}} {{end}} </div> + {{end}} <!-- Calendar Grid (dynamic hours) --> <div class="calendar-grid" id="today-calendar" data-start-hour="{{.TodayStartHour}}" data-now-hour="{{.NowHour}}" data-now-minute="{{.NowMinute}}"> @@ -200,7 +202,6 @@ </div> </details> - {{end}} <!-- Tomorrow Section (Calendar View) --> {{if .TomorrowItems}} @@ -305,7 +306,7 @@ </details> {{end}} - {{if and (not .TodayItems) (not .TomorrowItems) (not .LaterItems)}} + {{if and (not .TomorrowItems) (not .LaterItems)}} <div class="text-center py-8 text-white/50"> <p>No items found for the selected range.</p> </div> |
