summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile2
-rw-r--r--internal/handlers/handlers.go40
-rw-r--r--internal/handlers/handlers_test.go108
-rw-r--r--migrations/017_add_bugs_resolved_at.sql2
-rw-r--r--web/templates/partials/bugs.html12
-rw-r--r--web/templates/partials/lists-options.html4
-rw-r--r--web/templates/partials/task-detail.html12
7 files changed, 147 insertions, 33 deletions
diff --git a/Makefile b/Makefile
index bee9968..50d0f68 100644
--- a/Makefile
+++ b/Makefile
@@ -16,7 +16,7 @@ css: ## Build CSS once
css-watch: ## Watch CSS for changes
npm run dev
-build: css ## Build Go binary with CSS
+build: ## Build Go binary
go build -o dashboard cmd/dashboard/main.go
run: css ## Build CSS and run server
diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go
index 4988038..39e67c9 100644
--- a/internal/handlers/handlers.go
+++ b/internal/handlers/handlers.go
@@ -835,12 +835,7 @@ func (h *Handler) HandleGetListsOptions(w http.ResponseWriter, r *http.Request)
return
}
- w.Header().Set("Content-Type", "text/html")
- for _, list := range lists {
- _, _ = fmt.Fprintf(w, `<option value="%s">%s</option>`,
- template.HTMLEscapeString(list.ID),
- template.HTMLEscapeString(list.Name))
- }
+ HTMLResponse(w, h.renderer, "lists-options", lists)
}
// HandleGetBugs returns the list of reported bugs
@@ -850,19 +845,7 @@ func (h *Handler) HandleGetBugs(w http.ResponseWriter, r *http.Request) {
JSONError(w, http.StatusInternalServerError, "Failed to fetch bugs", err)
return
}
-
- w.Header().Set("Content-Type", "text/html")
- if len(bugs) == 0 {
- _, _ = fmt.Fprint(w, `<p class="text-gray-500 text-sm">No bugs reported yet.</p>`)
- return
- }
-
- for _, bug := range bugs {
- _, _ = fmt.Fprintf(w, `<div class="text-sm border-b border-gray-100 py-2">
- <p class="text-gray-900">%s</p>
- <p class="text-gray-400 text-xs">%s</p>
- </div>`, template.HTMLEscapeString(bug.Description), bug.CreatedAt.Format("Jan 2, 3:04 PM"))
- }
+ HTMLResponse(w, h.renderer, "bugs", bugs)
}
// HandleReportBug saves a new bug report
@@ -919,19 +902,12 @@ func (h *Handler) HandleGetTaskDetail(w http.ResponseWriter, r *http.Request) {
}
}
- html := fmt.Sprintf(`
- <div class="p-4">
- <h3 class="font-semibold text-gray-900 mb-3">%s</h3>
- <form hx-post="/tasks/update" hx-swap="none" hx-on::after-request="if(event.detail.successful) { closeTaskModal(); htmx.trigger(document.body, 'refresh-tasks'); }">
- <input type="hidden" name="id" value="%s">
- <input type="hidden" name="source" value="%s">
- <label class="block text-sm font-medium text-gray-700 mb-1">Description</label>
- <textarea name="description" class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm mb-3 h-32">%s</textarea>
- <button type="submit" class="w-full bg-primary-600 hover:bg-primary-700 text-white px-4 py-2 rounded-lg text-sm font-medium">Save</button>
- </form>
- </div>
- `, template.HTMLEscapeString(title), template.HTMLEscapeString(id), template.HTMLEscapeString(source), template.HTMLEscapeString(description))
- HTMLString(w, html)
+ HTMLResponse(w, h.renderer, "task-detail", struct {
+ Title string
+ ID string
+ Source string
+ Description string
+ }{title, id, source, description})
}
// HandleUpdateTask updates a task description
diff --git a/internal/handlers/handlers_test.go b/internal/handlers/handlers_test.go
index 470ea5c..9a2287f 100644
--- a/internal/handlers/handlers_test.go
+++ b/internal/handlers/handlers_test.go
@@ -2827,6 +2827,114 @@ func TestHandleSettingsPage_IncludesSyncLog(t *testing.T) {
}
}
+// =============================================================================
+// HandleGetBugs template tests
+// =============================================================================
+
+// TestHandleGetBugs_RendersTemplate verifies that HandleGetBugs uses the renderer
+// with the "bugs" template and passes the bug list as data.
+func TestHandleGetBugs_RendersTemplate(t *testing.T) {
+ h, cleanup := setupTestHandler(t)
+ defer cleanup()
+
+ _ = h.store.SaveBug("test bug description")
+
+ req := httptest.NewRequest("GET", "/bugs", nil)
+ w := httptest.NewRecorder()
+ h.HandleGetBugs(w, req)
+
+ mr := h.renderer.(*MockRenderer)
+ var found bool
+ for _, call := range mr.Calls {
+ if call.Name == "bugs" {
+ found = true
+ break
+ }
+ }
+ if !found {
+ t.Error("Expected renderer to be called with 'bugs' template")
+ }
+}
+
+// TestHandleGetBugs_NoBugs_RendersTemplate verifies that HandleGetBugs uses the
+// renderer even when there are no bugs.
+func TestHandleGetBugs_NoBugs_RendersTemplate(t *testing.T) {
+ h, cleanup := setupTestHandler(t)
+ defer cleanup()
+
+ req := httptest.NewRequest("GET", "/bugs", nil)
+ w := httptest.NewRecorder()
+ h.HandleGetBugs(w, req)
+
+ mr := h.renderer.(*MockRenderer)
+ var found bool
+ for _, call := range mr.Calls {
+ if call.Name == "bugs" {
+ found = true
+ break
+ }
+ }
+ if !found {
+ t.Error("Expected renderer to be called with 'bugs' template even when no bugs")
+ }
+}
+
+// =============================================================================
+// HandleGetTaskDetail template tests
+// =============================================================================
+
+// TestHandleGetTaskDetail_RendersTemplate verifies that HandleGetTaskDetail uses
+// the renderer with the "task-detail" template.
+func TestHandleGetTaskDetail_RendersTemplate(t *testing.T) {
+ h, cleanup := setupTestHandler(t)
+ defer cleanup()
+
+ req := httptest.NewRequest("GET", "/tasks/detail?id=abc&source=todoist", nil)
+ w := httptest.NewRecorder()
+ h.HandleGetTaskDetail(w, req)
+
+ mr := h.renderer.(*MockRenderer)
+ var found bool
+ for _, call := range mr.Calls {
+ if call.Name == "task-detail" {
+ found = true
+ break
+ }
+ }
+ if !found {
+ t.Error("Expected renderer to be called with 'task-detail' template")
+ }
+}
+
+// =============================================================================
+// HandleGetListsOptions template tests
+// =============================================================================
+
+// TestHandleGetListsOptions_RendersTemplate verifies that HandleGetListsOptions uses
+// the renderer with the "lists-options" template.
+func TestHandleGetListsOptions_RendersTemplate(t *testing.T) {
+ h, cleanup := setupTestHandler(t)
+ defer cleanup()
+
+ h.trelloClient = &mockTrelloClient{boards: []models.Board{}}
+
+ req := httptest.NewRequest("GET", "/trello/lists?board_id=board1", nil)
+ w := httptest.NewRecorder()
+ h.HandleGetListsOptions(w, req)
+
+ mr := h.renderer.(*MockRenderer)
+ var found bool
+ for _, call := range mr.Calls {
+ if call.Name == "lists-options" {
+ found = true
+ break
+ }
+ }
+ if !found {
+ t.Error("Expected renderer to be called with 'lists-options' template")
+ }
+}
+
// TestHandleSyncSources_AddsLogEntry verifies that syncing sources creates a sync log entry.
func TestHandleSyncSources_AddsLogEntry(t *testing.T) {
h, cleanup := setupTestHandler(t)
diff --git a/migrations/017_add_bugs_resolved_at.sql b/migrations/017_add_bugs_resolved_at.sql
new file mode 100644
index 0000000..836b1d2
--- /dev/null
+++ b/migrations/017_add_bugs_resolved_at.sql
@@ -0,0 +1,2 @@
+-- Add resolved_at column to bugs table for tracking resolution status
+ALTER TABLE bugs ADD COLUMN resolved_at DATETIME;
diff --git a/web/templates/partials/bugs.html b/web/templates/partials/bugs.html
new file mode 100644
index 0000000..cb5ba25
--- /dev/null
+++ b/web/templates/partials/bugs.html
@@ -0,0 +1,12 @@
+{{define "bugs"}}
+{{if .}}
+{{range .}}
+<div class="text-sm border-b border-gray-100 py-2">
+ <p class="text-gray-900">{{.Description}}</p>
+ <p class="text-gray-400 text-xs">{{.CreatedAt.Format "Jan 2, 3:04 PM"}}</p>
+</div>
+{{end}}
+{{else}}
+<p class="text-gray-500 text-sm">No bugs reported yet.</p>
+{{end}}
+{{end}}
diff --git a/web/templates/partials/lists-options.html b/web/templates/partials/lists-options.html
new file mode 100644
index 0000000..f08fdcf
--- /dev/null
+++ b/web/templates/partials/lists-options.html
@@ -0,0 +1,4 @@
+{{define "lists-options"}}
+{{range .}}<option value="{{.ID}}">{{.Name}}</option>
+{{end}}
+{{end}}
diff --git a/web/templates/partials/task-detail.html b/web/templates/partials/task-detail.html
new file mode 100644
index 0000000..ace604f
--- /dev/null
+++ b/web/templates/partials/task-detail.html
@@ -0,0 +1,12 @@
+{{define "task-detail"}}
+<div class="p-4">
+ <h3 class="font-semibold text-gray-900 mb-3">{{.Title}}</h3>
+ <form hx-post="/tasks/update" hx-swap="none" hx-on::after-request="if(event.detail.successful) { closeTaskModal(); htmx.trigger(document.body, 'refresh-tasks'); }">
+ <input type="hidden" name="id" value="{{.ID}}">
+ <input type="hidden" name="source" value="{{.Source}}">
+ <label class="block text-sm font-medium text-gray-700 mb-1">Description</label>
+ <textarea name="description" class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm mb-3 h-32">{{.Description}}</textarea>
+ <button type="submit" class="w-full bg-primary-600 hover:bg-primary-700 text-white px-4 py-2 rounded-lg text-sm font-medium">Save</button>
+ </form>
+</div>
+{{end}}