summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.agent/coding_standards.md70
-rw-r--r--.agent/config.md62
-rw-r--r--.agent/design.md (renamed from DESIGN.md)0
-rw-r--r--.agent/mission.md16
-rw-r--r--.agent/narrative.md32
-rw-r--r--.agent/preferences.md15
-rw-r--r--.agent/timeline_design.md (renamed from TIMELINE_DESIGN.md)0
-rw-r--r--.agent/ux_philosophy.md82
-rw-r--r--.agent/worklog.md (renamed from SESSION_STATE.md)0
-rw-r--r--.gemini/GEMINI.md13
-rw-r--r--.gemini/skills/bug-manager/SKILL.md34
-rw-r--r--.gemini/skills/bug-manager/references/bug-triage.md36
-rw-r--r--CLAUDE.md76
-rw-r--r--IMPLEMENTOR.md47
-rw-r--r--QUICKSTART.md48
-rw-r--r--bug-manager/SKILL.md34
-rw-r--r--bug-manager/references/bug-triage.md36
-rw-r--r--instructions.md77
-rw-r--r--internal/api/interfaces.go1
-rw-r--r--internal/api/todoist.go155
-rw-r--r--internal/api/todoist_test.go166
-rw-r--r--internal/handlers/handlers.go62
-rw-r--r--internal/handlers/handlers_test.go216
-rw-r--r--internal/store/sqlite.go6
-rw-r--r--package.py18
-rwxr-xr-xscripts/clear-cache17
-rw-r--r--task-sync-manager/SKILL.md29
-rw-r--r--task-sync-manager/references/session-state-standard.md41
-rw-r--r--web/templates/partials/timeline-tab.html9
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.
diff --git a/CLAUDE.md b/CLAUDE.md
index 237fdaa..4140859 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -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>