summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-01-13 08:56:26 -1000
committerPeter Stone <thepeterstone@gmail.com>2026-01-13 08:56:26 -1000
commit2292dff2d8d6f4b43dad8dffd3d559f7c1e5bb35 (patch)
tree3d4749226897b410292ea1858327d76464fecc1a
parent043f48c12eb4dfc410e8724b430166000d7cb905 (diff)
Implement 4-Tab Architecture with unified Atom model
-rw-r--r--PHASE_2_SURGICAL_PLAN.md2
-rw-r--r--SESSION_STATE.md19
-rw-r--r--internal/handlers/tabs.go178
-rw-r--r--web/templates/index.html18
-rw-r--r--web/templates/partials/meals-tab.html6
-rw-r--r--web/templates/partials/planning-tab.html6
-rw-r--r--web/templates/partials/tasks-tab.html79
7 files changed, 287 insertions, 21 deletions
diff --git a/PHASE_2_SURGICAL_PLAN.md b/PHASE_2_SURGICAL_PLAN.md
index dbac3a6..e300639 100644
--- a/PHASE_2_SURGICAL_PLAN.md
+++ b/PHASE_2_SURGICAL_PLAN.md
@@ -25,7 +25,7 @@ Implement mapper functions.
```
## 2. Information Architecture: The 4-Tab Split
-**Status:** [ ] Pending
+**Status:** [x] Complete
```text
Refactor the frontend and router to support 4 distinct tabs.
diff --git a/SESSION_STATE.md b/SESSION_STATE.md
index e3a498e..dd8e30f 100644
--- a/SESSION_STATE.md
+++ b/SESSION_STATE.md
@@ -39,6 +39,18 @@ Frontend modernization with tabs, HTMX, and Tailwind build pipeline complete.
- Priority normalization (1-4 scale), brand color mapping (Trello=Blue, Todoist=Red, Obsidian=Purple, PlanToEat=Green)
- Preserves raw data for future write operations
- All tests passing after implementation
+- **4-Tab Architecture:** Implemented unified information architecture using Atom model
+ - internal/handlers/tabs.go: New TabsHandler with 4 specialized methods
+ - HandleTasks: Unified view of Todoist + Trello cards with due dates, converted to Atoms, sorted by due date and priority
+ - HandlePlanning: Trello boards view for project planning
+ - HandleNotes: Obsidian notes view
+ - HandleMeals: PlanToEat meals view
+ - cmd/dashboard/main.go: Registered 4 tab routes (/tabs/tasks, /tabs/planning, /tabs/notes, /tabs/meals)
+ - web/templates/index.html: Updated navigation with 4 tabs
+ - web/templates/partials/tasks-tab.html: Rewritten to render unified Atom list with source icons, priorities, and brand colors
+ - web/templates/partials/planning-tab.html: New tab for Trello boards
+ - web/templates/partials/meals-tab.html: New tab for PlanToEat meals
+ - Clean separation of concerns: Tasks (due items), Planning (all boards), Notes (knowledge), Meals (calendar)
- **Build Pipeline:** npm + PostCSS + Tailwind configuration (replaced CDN)
- package.json, tailwind.config.js, postcss.config.js, Makefile
- Custom design system with brand colors (Trello, Todoist, Obsidian, PlanToEat)
@@ -82,10 +94,9 @@ Frontend modernization with tabs, HTMX, and Tailwind build pipeline complete.
- **Decision:** Unified Atom Model - Abstract all data sources (Trello, Todoist, Obsidian, PlanToEat) into a single `models.Atom` type for consistent handling, sorting, and rendering across the UI.
## 📋 Next Steps
-1. **Phase 2 Step 2:** Implement 4-Tab Split (Tasks, Planning, Notes, Meals) using the Atom model.
-2. **Phase 2 Step 3:** Trello smart sorting (activity-based, modification date).
-3. **Phase 2 Step 4:** Todoist "due first" sorting.
-4. **Phase 2 Remaining:** Search, visual overhaul, write operations, PWA.
+1. **Phase 2 Step 3:** Trello smart sorting (activity-based, modification date).
+2. **Phase 2 Step 4:** Todoist "due first" sorting.
+3. **Phase 2 Remaining:** Obsidian search & categorization, visual overhaul (glassmorphism), write operations, PWA.
## ⚠️ Known Blockers / Debt
- None currently.
diff --git a/internal/handlers/tabs.go b/internal/handlers/tabs.go
new file mode 100644
index 0000000..fa60a10
--- /dev/null
+++ b/internal/handlers/tabs.go
@@ -0,0 +1,178 @@
+package handlers
+
+import (
+ "html/template"
+ "log"
+ "net/http"
+ "sort"
+ "time"
+
+ "task-dashboard/internal/models"
+ "task-dashboard/internal/store"
+)
+
+// TabsHandler handles tab-specific rendering with Atom model
+type TabsHandler struct {
+ store *store.Store
+ templates *template.Template
+}
+
+// NewTabsHandler creates a new TabsHandler instance
+func NewTabsHandler(store *store.Store) *TabsHandler {
+ // Parse templates including partials
+ tmpl, err := template.ParseGlob("web/templates/*.html")
+ if err != nil {
+ log.Printf("Warning: failed to parse templates: %v", err)
+ }
+
+ // Also parse partials
+ tmpl, err = tmpl.ParseGlob("web/templates/partials/*.html")
+ if err != nil {
+ log.Printf("Warning: failed to parse partial templates: %v", err)
+ }
+
+ return &TabsHandler{
+ store: store,
+ templates: tmpl,
+ }
+}
+
+// HandleTasks renders the unified Tasks tab (Todoist + Trello cards with due dates)
+func (h *TabsHandler) HandleTasks(w http.ResponseWriter, r *http.Request) {
+ // Fetch Todoist tasks
+ tasks, err := h.store.GetTasks()
+ if err != nil {
+ http.Error(w, "Failed to fetch tasks", http.StatusInternalServerError)
+ log.Printf("Error fetching tasks: %v", err)
+ return
+ }
+
+ // Fetch Trello boards
+ boards, err := h.store.GetBoards()
+ if err != nil {
+ http.Error(w, "Failed to fetch boards", http.StatusInternalServerError)
+ log.Printf("Error fetching boards: %v", err)
+ return
+ }
+
+ // Convert to Atoms
+ atoms := make([]models.Atom, 0)
+
+ // Convert Todoist tasks
+ for _, task := range tasks {
+ if !task.Completed {
+ atoms = append(atoms, models.TaskToAtom(task))
+ }
+ }
+
+ // Convert Trello cards with due dates
+ for _, board := range boards {
+ for _, card := range board.Cards {
+ if card.DueDate != nil {
+ atoms = append(atoms, models.CardToAtom(card))
+ }
+ }
+ }
+
+ // Sort atoms: by DueDate (earliest first), then by Priority (descending)
+ sort.SliceStable(atoms, func(i, j int) bool {
+ // Handle nil due dates (push to end)
+ if atoms[i].DueDate == nil && atoms[j].DueDate != nil {
+ return false
+ }
+ if atoms[i].DueDate != nil && atoms[j].DueDate == nil {
+ return true
+ }
+
+ // Both have due dates, sort by date
+ if atoms[i].DueDate != nil && atoms[j].DueDate != nil {
+ if !atoms[i].DueDate.Equal(*atoms[j].DueDate) {
+ return atoms[i].DueDate.Before(*atoms[j].DueDate)
+ }
+ }
+
+ // Same due date (or both nil), sort by priority (descending)
+ return atoms[i].Priority > atoms[j].Priority
+ })
+
+ // Render template
+ data := struct {
+ Atoms []models.Atom
+ }{
+ Atoms: atoms,
+ }
+
+ if err := h.templates.ExecuteTemplate(w, "tasks-tab", data); err != nil {
+ http.Error(w, "Failed to render template", http.StatusInternalServerError)
+ log.Printf("Error rendering tasks tab: %v", err)
+ }
+}
+
+// HandlePlanning renders the Planning tab (Trello boards)
+func (h *TabsHandler) HandlePlanning(w http.ResponseWriter, r *http.Request) {
+ // Fetch Trello boards
+ boards, err := h.store.GetBoards()
+ if err != nil {
+ http.Error(w, "Failed to fetch boards", http.StatusInternalServerError)
+ log.Printf("Error fetching boards: %v", err)
+ return
+ }
+
+ data := struct {
+ Boards []models.Board
+ }{
+ Boards: boards,
+ }
+
+ if err := h.templates.ExecuteTemplate(w, "planning-tab", data); err != nil {
+ http.Error(w, "Failed to render template", http.StatusInternalServerError)
+ log.Printf("Error rendering planning tab: %v", err)
+ }
+}
+
+// HandleNotes renders the Notes tab (Obsidian notes)
+func (h *TabsHandler) HandleNotes(w http.ResponseWriter, r *http.Request) {
+ // Fetch Obsidian notes
+ notes, err := h.store.GetNotes(20)
+ if err != nil {
+ http.Error(w, "Failed to fetch notes", http.StatusInternalServerError)
+ log.Printf("Error fetching notes: %v", err)
+ return
+ }
+
+ data := struct {
+ Notes []models.Note
+ }{
+ Notes: notes,
+ }
+
+ if err := h.templates.ExecuteTemplate(w, "notes-tab", data); err != nil {
+ http.Error(w, "Failed to render template", http.StatusInternalServerError)
+ log.Printf("Error rendering notes tab: %v", err)
+ }
+}
+
+// HandleMeals renders the Meals tab (PlanToEat)
+func (h *TabsHandler) HandleMeals(w http.ResponseWriter, r *http.Request) {
+ // Fetch meals for next 7 days
+ startDate := time.Now()
+ endDate := startDate.AddDate(0, 0, 7)
+
+ meals, err := h.store.GetMeals(startDate, endDate)
+ if err != nil {
+ http.Error(w, "Failed to fetch meals", http.StatusInternalServerError)
+ log.Printf("Error fetching meals: %v", err)
+ return
+ }
+
+ data := struct {
+ Meals []models.Meal
+ }{
+ Meals: meals,
+ }
+
+ if err := h.templates.ExecuteTemplate(w, "meals-tab", data); err != nil {
+ http.Error(w, "Failed to render template", http.StatusInternalServerError)
+ log.Printf("Error rendering meals tab: %v", err)
+ }
+}
diff --git a/web/templates/index.html b/web/templates/index.html
index 2d35b37..b544ec3 100644
--- a/web/templates/index.html
+++ b/web/templates/index.html
@@ -31,7 +31,15 @@
hx-target="#tab-content"
hx-push-url="false"
onclick="setActiveTab(this)">
- 📋 Tasks & Planning
+ ✓ Tasks
+ </button>
+ <button
+ class="tab-button"
+ hx-get="/tabs/planning"
+ hx-target="#tab-content"
+ hx-push-url="false"
+ onclick="setActiveTab(this)">
+ 📋 Planning
</button>
<button
class="tab-button"
@@ -41,6 +49,14 @@
onclick="setActiveTab(this)">
📝 Notes
</button>
+ <button
+ class="tab-button"
+ hx-get="/tabs/meals"
+ hx-target="#tab-content"
+ hx-push-url="false"
+ onclick="setActiveTab(this)">
+ 🍽️ Meals
+ </button>
</nav>
</div>
diff --git a/web/templates/partials/meals-tab.html b/web/templates/partials/meals-tab.html
new file mode 100644
index 0000000..5900368
--- /dev/null
+++ b/web/templates/partials/meals-tab.html
@@ -0,0 +1,6 @@
+{{define "meals-tab"}}
+<div class="space-y-6">
+ <!-- PlanToEat Meals Section -->
+ {{template "plantoeat-meals" .}}
+</div>
+{{end}}
diff --git a/web/templates/partials/planning-tab.html b/web/templates/partials/planning-tab.html
new file mode 100644
index 0000000..e538578
--- /dev/null
+++ b/web/templates/partials/planning-tab.html
@@ -0,0 +1,6 @@
+{{define "planning-tab"}}
+<div class="space-y-6">
+ <!-- Trello Boards Section -->
+ {{template "trello-boards" .}}
+</div>
+{{end}}
diff --git a/web/templates/partials/tasks-tab.html b/web/templates/partials/tasks-tab.html
index 5678193..f528553 100644
--- a/web/templates/partials/tasks-tab.html
+++ b/web/templates/partials/tasks-tab.html
@@ -1,22 +1,71 @@
{{define "tasks-tab"}}
-<div class="space-y-10">
- <!-- Error Messages -->
- {{template "error-banner" .}}
+<div class="space-y-6">
+ <!-- Unified Tasks Section -->
+ <section>
+ <h2 class="section-header mb-6">
+ <span class="section-accent bg-gradient-to-r from-red-500 to-blue-500"></span>
+ Upcoming Tasks
+ </h2>
- <!-- Trello Boards Section -->
- {{template "trello-boards" .}}
+ {{if .Atoms}}
+ <div class="space-y-4">
+ {{range .Atoms}}
+ <div class="bg-white rounded-lg p-6 shadow-sm hover:shadow-md transition-shadow border-l-4 {{.ColorClass}}">
+ <div class="flex items-start justify-between">
+ <div class="flex-1">
+ <div class="flex items-center gap-3 mb-2">
+ <span class="text-2xl">{{.SourceIcon}}</span>
+ <h3 class="text-lg font-semibold text-gray-900">{{.Title}}</h3>
+ </div>
- <!-- Todoist + PlanToEat Grid -->
- <div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
- <!-- Todoist (2 cols) -->
- <div class="lg:col-span-2">
- {{template "todoist-tasks" .}}
- </div>
+ {{if .Description}}
+ <p class="text-sm text-gray-600 mb-3">{{.Description}}</p>
+ {{end}}
+
+ <div class="flex items-center gap-4 text-sm text-gray-500">
+ {{if .DueDate}}
+ <div class="flex items-center gap-1">
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
+ </svg>
+ <span>{{.DueDate.Format "Jan 2, 3:04 PM"}}</span>
+ </div>
+ {{end}}
+
+ <div class="flex items-center gap-1">
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"></path>
+ </svg>
+ <span>{{if eq .Source "trello"}}Trello{{else if eq .Source "todoist"}}Todoist{{else}}{{.Source}}{{end}}</span>
+ </div>
- <!-- PlanToEat (1 col) -->
- <div>
- {{template "plantoeat-meals" .}}
+ {{if gt .Priority 2}}
+ <div class="flex items-center gap-1 text-red-600 font-medium">
+ <svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
+ <path d="M10 2a8 8 0 100 16 8 8 0 000-16zm1 11H9v-2h2v2zm0-4H9V5h2v4z"></path>
+ </svg>
+ <span>Priority {{.Priority}}</span>
+ </div>
+ {{end}}
+ </div>
+ </div>
+
+ {{if .URL}}
+ <a href="{{.URL}}" target="_blank" class="ml-4 text-primary-600 hover:text-primary-800 transition-colors">
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"></path>
+ </svg>
+ </a>
+ {{end}}
+ </div>
+ </div>
+ {{end}}
+ </div>
+ {{else}}
+ <div class="bg-gray-50 rounded-lg p-8 text-center">
+ <p class="text-gray-600">No upcoming tasks with due dates.</p>
</div>
- </div>
+ {{end}}
+ </section>
</div>
{{end}}