summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-01-25 11:56:29 -1000
committerPeter Stone <thepeterstone@gmail.com>2026-01-25 11:56:29 -1000
commitec8a9c0ea46dec7d26caa763e3adefcaf3fc7552 (patch)
tree1f91bbc7ec87314189a441c53b7c3b25f1817db0
parent83beddfab9584ae4b64a782c978236472b6d5745 (diff)
Fix bugs and add bug management scripts
Bug fixes: - #36: Hide recurring tasks until due day (add IsRecurring to Task/Atom) - Trello cards missing: change filter=visible to filter=open - Build fix: add missing fmt import in atom.go Infrastructure: - Add scripts/bugs and scripts/resolve-bug for DB bug tracking - Remove issues/ directory (bugs now tracked in DB) - Add timeline_logic_test.go Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
-rw-r--r--SESSION_STATE.md50
-rw-r--r--internal/api/todoist.go29
-rw-r--r--internal/api/todoist_test.go12
-rw-r--r--internal/api/trello.go8
-rw-r--r--internal/handlers/handlers.go4
-rw-r--r--internal/handlers/timeline_logic_test.go160
-rw-r--r--internal/models/atom.go40
-rw-r--r--internal/models/types.go8
-rwxr-xr-xscripts/bugs4
-rwxr-xr-xscripts/resolve-bug23
10 files changed, 280 insertions, 58 deletions
diff --git a/SESSION_STATE.md b/SESSION_STATE.md
index 091929d..5f68a3d 100644
--- a/SESSION_STATE.md
+++ b/SESSION_STATE.md
@@ -1,34 +1,28 @@
# Session State
-**Active Task:** None
+## Current Focus
+Bug verification and fixes completed.
-**Completed Tasks:**
-- **Obsidian Removal:** ✅
-- **Authentication:** ✅
-- **VPS Deployment Preparation:** ✅
-- **Issue Batch (001-016):** ✅
- - 001: Hide future tasks behind fold
- - 002: Modal menu for quick add/bug report
- - 003: Fix tap to expand
- - 005: Visual task timing differentiation
- - 006: Reorder tasks by urgency
- - 007: Fix outdated Todoist link
- - 009: Keep completed tasks visible until refresh
- - 010: Fix quick add timestamp (evening date bug)
- - 015: Random landscape background
- - 016: Click task to edit details
+## Status
+* [x] **Build Fix:** Added missing `fmt` import in `internal/models/atom.go`
+* [x] **Bug Fix:** Trello cards missing - changed `filter=visible` to `filter=open`
+* [x] **Bug Verification:** Resolved already-fixed bugs from DB (#24-27, #32, #39)
+* [x] **Bug #36 Fixed:** Recurring tasks now hidden until due day
+ - Added `IsRecurring` field to Task model and Atom
+ - Updated Todoist API to parse `is_recurring` from due object
+ - Filter recurring future tasks from display
-**Current Status:** [REVIEW_READY]
+## Scripts Created
+- `scripts/bugs` - List all bugs from production DB
+- `scripts/resolve-bug <id>` - Resolve a bug by ID
-**Files Modified:**
-- `internal/api/todoist.go` - Updated URL format, added UpdateTask method
-- `internal/api/interfaces.go` - Added UpdateTask to TodoistAPI interface
-- `internal/handlers/handlers.go` - Added task detail/update handlers, completed task HTML response
-- `internal/handlers/tabs.go` - Added urgency sorting, future task partitioning
-- `internal/handlers/handlers_test.go` - Added UpdateTask mock
-- `internal/models/atom.go` - Added IsFuture field
-- `cmd/dashboard/main.go` - Added task detail/update routes
-- `web/templates/index.html` - Added unified modal, task edit modal, random background
-- `web/templates/partials/tasks-tab.html` - Checkbox complete, expand details, urgency styling, future fold
+## Remaining Items (Feature Requests, not bugs)
+- #12: Research task durations
+- #28: Bugs as first-class atoms
+- #30: Consistent background opacity
+- #31: PlanToEat ingredients
+- #33-38: Shopping/timeline features
-**All Issues Complete - Ready for Review**
+## Next Steps
+* Deploy and verify recurring task fix
+* Prioritize feature requests
diff --git a/internal/api/todoist.go b/internal/api/todoist.go
index 6c998cf..2c94e08 100644
--- a/internal/api/todoist.go
+++ b/internal/api/todoist.go
@@ -41,11 +41,8 @@ type todoistTaskResponse struct {
ProjectID string `json:"project_id"`
Priority int `json:"priority"`
Labels []string `json:"labels"`
- Due *struct {
- Date string `json:"date"`
- Datetime string `json:"datetime"`
- } `json:"due"`
- URL string `json:"url"`
+ Due *dueInfo `json:"due"`
+ URL string `json:"url"`
CreatedAt string `json:"created_at"`
}
@@ -73,11 +70,8 @@ type SyncItemResponse struct {
ProjectID string `json:"project_id"`
Priority int `json:"priority"`
Labels []string `json:"labels"`
- Due *struct {
- Date string `json:"date"`
- Datetime string `json:"datetime"`
- } `json:"due"`
- IsCompleted bool `json:"is_completed"`
+ Due *dueInfo `json:"due"`
+ IsCompleted bool `json:"is_completed"`
IsDeleted bool `json:"is_deleted"`
AddedAt string `json:"added_at"`
}
@@ -125,6 +119,9 @@ func (c *TodoistClient) GetTasks(ctx context.Context) ([]models.Task, error) {
}
task.DueDate = parseDueDate(apiTask.Due)
+ if apiTask.Due != nil {
+ task.IsRecurring = apiTask.Due.IsRecurring
+ }
tasks = append(tasks, task)
}
@@ -276,10 +273,14 @@ func (c *TodoistClient) ReopenTask(ctx context.Context, taskID string) error {
}
// parseDueDate parses due date from API response
-func parseDueDate(due *struct {
- Date string `json:"date"`
- Datetime string `json:"datetime"`
-}) *time.Time {
+// dueInfo represents the due date structure from Todoist API
+type dueInfo struct {
+ Date string `json:"date"`
+ Datetime string `json:"datetime"`
+ IsRecurring bool `json:"is_recurring"`
+}
+
+func parseDueDate(due *dueInfo) *time.Time {
if due == nil {
return nil
}
diff --git a/internal/api/todoist_test.go b/internal/api/todoist_test.go
index 88f94f8..2fa6e28 100644
--- a/internal/api/todoist_test.go
+++ b/internal/api/todoist_test.go
@@ -104,20 +104,16 @@ func TestTodoistClient_CreateTask_WithDueDate(t *testing.T) {
}
// Return mock response with due date
- dueStruct := struct {
- Date string `json:"date"`
- Datetime string `json:"datetime"`
- }{
- Date: "2026-01-15",
- Datetime: "",
- }
response := todoistTaskResponse{
ID: "task-789",
Content: "Task with Due Date",
ProjectID: "project-456",
URL: "https://todoist.com/task/789",
CreatedAt: time.Now().Format(time.RFC3339),
- Due: &dueStruct,
+ Due: &dueInfo{
+ Date: "2026-01-15",
+ Datetime: "",
+ },
}
w.Header().Set("Content-Type", "application/json")
diff --git a/internal/api/trello.go b/internal/api/trello.go
index 4cf7e9e..a19bbea 100644
--- a/internal/api/trello.go
+++ b/internal/api/trello.go
@@ -86,7 +86,7 @@ func (c *TrelloClient) GetBoards(ctx context.Context) ([]models.Board, error) {
// GetCards fetches all cards for a specific board
func (c *TrelloClient) GetCards(ctx context.Context, boardID string) ([]models.Card, error) {
params := c.authParams()
- params.Set("filter", "visible")
+ params.Set("filter", "open")
params.Set("fields", "id,name,idList,due,url,idBoard")
var apiCards []trelloCardResponse
@@ -95,7 +95,11 @@ func (c *TrelloClient) GetCards(ctx context.Context, boardID string) ([]models.C
return nil, fmt.Errorf("failed to fetch cards: %w", err)
}
- log.Printf("Trello GetCards: board %s returned %d cards from API", boardID, len(apiCards))
+ if len(apiCards) == 0 {
+ log.Printf("Trello GetCards: board %s returned 0 cards (may have only archived cards)", boardID)
+ } else {
+ log.Printf("Trello GetCards: board %s returned %d cards", boardID, len(apiCards))
+ }
// Fetch lists to get list names
lists, err := c.getLists(ctx, boardID)
diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go
index 5014f39..5c86ce2 100644
--- a/internal/handlers/handlers.go
+++ b/internal/handlers/handlers.go
@@ -996,6 +996,10 @@ func (h *Handler) HandleTabTasks(w http.ResponseWriter, r *http.Request) {
var currentAtoms, futureAtoms []models.Atom
for _, a := range atoms {
+ // Don't show recurring tasks until the day they're due
+ if a.IsRecurring && a.IsFuture {
+ continue
+ }
if a.IsFuture {
futureAtoms = append(futureAtoms, a)
} else {
diff --git a/internal/handlers/timeline_logic_test.go b/internal/handlers/timeline_logic_test.go
new file mode 100644
index 0000000..a0576d6
--- /dev/null
+++ b/internal/handlers/timeline_logic_test.go
@@ -0,0 +1,160 @@
+package handlers
+
+import (
+ "context"
+ "os"
+ "path/filepath"
+ "testing"
+ "time"
+
+ "task-dashboard/internal/models"
+ "task-dashboard/internal/store"
+
+ _ "github.com/mattn/go-sqlite3"
+)
+
+// MockCalendarClient implements GoogleCalendarAPI interface for testing
+type MockCalendarClient struct {
+ Events []models.CalendarEvent
+ Err error
+}
+
+func (m *MockCalendarClient) GetUpcomingEvents(ctx context.Context, maxResults int) ([]models.CalendarEvent, error) {
+ return m.Events, m.Err
+}
+
+func (m *MockCalendarClient) GetEventsByDateRange(ctx context.Context, start, end time.Time) ([]models.CalendarEvent, error) {
+ return m.Events, m.Err
+}
+
+func setupTestStore(t *testing.T) *store.Store {
+ t.Helper()
+ tempDir := t.TempDir()
+ dbPath := filepath.Join(tempDir, "test.db")
+ migrationDir := filepath.Join(tempDir, "migrations")
+
+ if err := os.MkdirAll(migrationDir, 0755); err != nil {
+ t.Fatalf("Failed to create migration dir: %v", err)
+ }
+
+ schema := `
+ CREATE TABLE IF NOT EXISTS tasks (
+ id TEXT PRIMARY KEY,
+ content TEXT NOT NULL,
+ description TEXT,
+ project_id TEXT,
+ project_name TEXT,
+ due_date DATETIME,
+ priority INTEGER DEFAULT 1,
+ completed BOOLEAN DEFAULT FALSE,
+ labels TEXT,
+ url TEXT,
+ created_at DATETIME,
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
+ );
+ CREATE TABLE IF NOT EXISTS meals (
+ id TEXT PRIMARY KEY,
+ recipe_name TEXT NOT NULL,
+ date DATETIME,
+ meal_type TEXT,
+ recipe_url TEXT,
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
+ );
+ CREATE TABLE IF NOT EXISTS boards (
+ id TEXT PRIMARY KEY,
+ name TEXT NOT NULL,
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
+ );
+ CREATE TABLE IF NOT EXISTS cards (
+ id TEXT PRIMARY KEY,
+ name TEXT NOT NULL,
+ board_id TEXT NOT NULL,
+ list_id TEXT,
+ list_name TEXT,
+ due_date DATETIME,
+ url TEXT,
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
+ );
+ `
+ if err := os.WriteFile(filepath.Join(migrationDir, "001_init.sql"), []byte(schema), 0644); err != nil {
+ t.Fatalf("Failed to write migration file: %v", err)
+ }
+
+ // Initialize store (this creates tables)
+ s, err := store.New(dbPath, migrationDir)
+ if err != nil {
+ t.Fatalf("Failed to create store: %v", err)
+ }
+ return s
+}
+
+func TestBuildTimeline(t *testing.T) {
+ s := setupTestStore(t)
+
+ // Fix a base time: 2023-01-01 08:00:00
+ baseTime := time.Date(2023, 1, 1, 8, 0, 0, 0, time.UTC)
+
+ // Task: 10:00
+ taskDate := baseTime.Add(2 * time.Hour)
+ s.SaveTasks([]models.Task{
+ {ID: "t1", Content: "Task 1", DueDate: &taskDate},
+ })
+
+ // Meal: Lunch (defaults to 12:00)
+ mealDate := baseTime // Date part matters
+ s.SaveMeals([]models.Meal{
+ {ID: "m1", RecipeName: "Lunch", Date: mealDate, MealType: "lunch"},
+ })
+
+ // Card: 14:00
+ cardDate := baseTime.Add(6 * time.Hour)
+ s.SaveBoards([]models.Board{
+ {
+ ID: "b1",
+ Name: "Board 1",
+ Cards: []models.Card{
+ {ID: "c1", Name: "Card 1", DueDate: &cardDate, ListID: "l1"},
+ },
+ },
+ })
+
+ // Calendar Event: 09:00
+ eventDate := baseTime.Add(1 * time.Hour)
+ mockCal := &MockCalendarClient{
+ Events: []models.CalendarEvent{
+ {ID: "e1", Summary: "Event 1", Start: eventDate, End: eventDate.Add(1 * time.Hour)},
+ },
+ }
+
+ // Test Range: Full Day
+ start := time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)
+ end := time.Date(2023, 1, 2, 0, 0, 0, 0, time.UTC)
+
+ items, err := BuildTimeline(context.Background(), s, mockCal, start, end)
+ if err != nil {
+ t.Fatalf("BuildTimeline failed: %v", err)
+ }
+
+ if len(items) != 4 {
+ t.Errorf("Expected 4 items, got %d", len(items))
+ }
+
+ // Expected Order:
+ // 1. Event (09:00)
+ // 2. Task (10:00)
+ // 3. Meal (12:00)
+ // 4. Card (14:00)
+
+ if items[0].Type != models.TimelineItemTypeEvent {
+ t.Errorf("Expected item 0 to be Event, got %s", items[0].Type)
+ }
+ if items[1].Type != models.TimelineItemTypeTask {
+ t.Errorf("Expected item 1 to be Task, got %s", items[1].Type)
+ }
+ if items[2].Type != models.TimelineItemTypeMeal {
+ t.Errorf("Expected item 2 to be Meal, got %s", items[2].Type)
+ }
+ if items[3].Type != models.TimelineItemTypeCard {
+ t.Errorf("Expected item 3 to be Card, got %s", items[3].Type)
+ }
+}
diff --git a/internal/models/atom.go b/internal/models/atom.go
index 10d14d1..3e08896 100644
--- a/internal/models/atom.go
+++ b/internal/models/atom.go
@@ -1,6 +1,9 @@
package models
-import "time"
+import (
+ "fmt"
+ "time"
+)
type AtomSource string
@@ -8,6 +11,7 @@ const (
SourceTrello AtomSource = "trello"
SourceTodoist AtomSource = "todoist"
SourceMeal AtomSource = "plantoeat"
+ SourceBug AtomSource = "bug"
)
type AtomType string
@@ -16,6 +20,7 @@ const (
TypeTask AtomType = "task"
TypeNote AtomType = "note"
TypeMeal AtomType = "meal"
+ TypeBug AtomType = "bug"
)
// Atom represents a unified unit of work or information
@@ -34,11 +39,12 @@ type Atom struct {
Priority int // Normalized: 1 (Low) to 4 (Urgent)
// UI Helpers (to be populated by mappers)
- SourceIcon string // e.g., "trello-icon.svg" or emoji
- ColorClass string // e.g., "border-blue-500"
- IsOverdue bool // True if due date is before today
- IsFuture bool // True if due date is after today
- HasSetTime bool // True if due time is not midnight (has specific time)
+ SourceIcon string // e.g., "trello-icon.svg" or emoji
+ ColorClass string // e.g., "border-blue-500"
+ IsOverdue bool // True if due date is before today
+ IsFuture bool // True if due date is after today
+ HasSetTime bool // True if due time is not midnight (has specific time)
+ IsRecurring bool // True if this is a recurring task
// Original Data (for write operations)
Raw interface{}
@@ -89,6 +95,7 @@ func TaskToAtom(t Task) Atom {
Priority: priority,
SourceIcon: "🔴", // Red circle for Todoist
ColorClass: "border-red-500",
+ IsRecurring: t.IsRecurring,
Raw: t,
}
}
@@ -135,3 +142,24 @@ func MealToAtom(m Meal) Atom {
Raw: m,
}
}
+
+// BugToAtom converts a Bug to an Atom
+func BugToAtom(b Bug) Atom {
+ // Bugs get high priority (3) to encourage fixing
+ priority := 3
+
+ return Atom{
+ ID: fmt.Sprintf("bug-%d", b.ID),
+ Title: b.Description,
+ Description: "Bug Report",
+ Source: SourceBug,
+ Type: TypeBug,
+ URL: "",
+ DueDate: nil, // Bugs don't have due dates
+ CreatedAt: b.CreatedAt,
+ Priority: priority,
+ SourceIcon: "🐛",
+ ColorClass: "border-red-700",
+ Raw: b,
+ }
+}
diff --git a/internal/models/types.go b/internal/models/types.go
index f45e346..0284a3a 100644
--- a/internal/models/types.go
+++ b/internal/models/types.go
@@ -15,6 +15,7 @@ type Task struct {
Labels []string `json:"labels"`
URL string `json:"url"`
CreatedAt time.Time `json:"created_at"`
+ IsRecurring bool `json:"is_recurring"`
}
// Meal represents a meal from PlanToEat
@@ -100,6 +101,13 @@ type CalendarEvent struct {
HTMLLink string `json:"html_link"`
}
+// Bug represents a bug report
+type Bug struct {
+ ID int64 `json:"id"`
+ Description string `json:"description"`
+ CreatedAt time.Time `json:"created_at"`
+}
+
// CacheMetadata tracks when data was last fetched
type CacheMetadata struct {
Key string `json:"key"`
diff --git a/scripts/bugs b/scripts/bugs
new file mode 100755
index 0000000..4f3c064
--- /dev/null
+++ b/scripts/bugs
@@ -0,0 +1,4 @@
+#!/bin/bash
+# List all bugs from the production database
+
+ssh titanium "sqlite3 -column -header /site/doot.terst.org/data/dashboard.db 'SELECT id, description, created_at FROM bugs ORDER BY id'"
diff --git a/scripts/resolve-bug b/scripts/resolve-bug
new file mode 100755
index 0000000..a3f0979
--- /dev/null
+++ b/scripts/resolve-bug
@@ -0,0 +1,23 @@
+#!/bin/bash
+# Resolve (delete) a bug by ID
+
+if [ -z "$1" ]; then
+ echo "Usage: resolve-bug <bug_id>"
+ exit 1
+fi
+
+BUG_ID="$1"
+
+# Show the bug being resolved
+echo "Resolving bug #$BUG_ID:"
+ssh titanium "sqlite3 -column /site/doot.terst.org/data/dashboard.db \"SELECT description FROM bugs WHERE id = $BUG_ID\""
+
+# Delete the bug
+ssh titanium "sqlite3 /site/doot.terst.org/data/dashboard.db \"DELETE FROM bugs WHERE id = $BUG_ID\""
+
+if [ $? -eq 0 ]; then
+ echo "Bug #$BUG_ID resolved."
+else
+ echo "Failed to resolve bug #$BUG_ID"
+ exit 1
+fi