summaryrefslogtreecommitdiff
path: root/internal/handlers
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-02-07 15:42:07 -1000
committerPeter Stone <thepeterstone@gmail.com>2026-02-07 15:42:07 -1000
commitd793e16f336189d38a9d43310713dd13e3b7c438 (patch)
treeb1e53d42069619912e683ddd1a58ed67b1873190 /internal/handlers
parent0620afc98fdc0f764e82807bb0090b78618ddb1d (diff)
Add build version footer, deploy ldflags, template test helper, and logs script
- Display build commit hash in unobtrusive footer overlay - Inject buildCommit/buildTime via ldflags in deploy.sh - Add assertTemplateContains test helper, refactor existing template tests - Add scripts/logs for fetching production journalctl via SSH Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'internal/handlers')
-rw-r--r--internal/handlers/handlers.go6
-rw-r--r--internal/handlers/handlers_test.go87
2 files changed, 70 insertions, 23 deletions
diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go
index 650eeaa..226f117 100644
--- a/internal/handlers/handlers.go
+++ b/internal/handlers/handlers.go
@@ -29,10 +29,11 @@ type Handler struct {
googleTasksClient api.GoogleTasksAPI
config *config.Config
renderer Renderer
+ BuildVersion string
}
// New creates a new Handler instance
-func New(s *store.Store, todoist api.TodoistAPI, trello api.TrelloAPI, planToEat api.PlanToEatAPI, googleCalendar api.GoogleCalendarAPI, googleTasks api.GoogleTasksAPI, cfg *config.Config) *Handler {
+func New(s *store.Store, todoist api.TodoistAPI, trello api.TrelloAPI, planToEat api.PlanToEatAPI, googleCalendar api.GoogleCalendarAPI, googleTasks api.GoogleTasksAPI, cfg *config.Config, buildVersion string) *Handler {
// Template functions
funcMap := template.FuncMap{
"subtract": func(a, b int) int { return a - b },
@@ -59,6 +60,7 @@ func New(s *store.Store, todoist api.TodoistAPI, trello api.TrelloAPI, planToEat
googleTasksClient: googleTasks,
config: cfg,
renderer: NewTemplateRenderer(tmpl),
+ BuildVersion: buildVersion,
}
}
@@ -96,11 +98,13 @@ func (h *Handler) HandleDashboard(w http.ResponseWriter, r *http.Request) {
ActiveTab string
CSRFToken string
BackgroundURL string
+ BuildVersion string
}{
DashboardData: dashboardData,
ActiveTab: tab,
CSRFToken: auth.GetCSRFTokenFromContext(ctx),
BackgroundURL: backgroundURL,
+ BuildVersion: h.BuildVersion,
}
if err := h.renderer.Render(w, "index.html", data); err != nil {
diff --git a/internal/handlers/handlers_test.go b/internal/handlers/handlers_test.go
index 1842c38..0ad36b2 100644
--- a/internal/handlers/handlers_test.go
+++ b/internal/handlers/handlers_test.go
@@ -1901,6 +1901,18 @@ func TestHandleTimeline_RendersDataToTemplate(t *testing.T) {
}
}
+// assertTemplateContains reads a template file and asserts it contains the expected string.
+func assertTemplateContains(t *testing.T, templatePath, expected, errMsg string) {
+ t.Helper()
+ content, err := os.ReadFile(templatePath)
+ if err != nil {
+ t.Skipf("Cannot read template file: %v", err)
+ }
+ if !strings.Contains(string(content), expected) {
+ t.Error(errMsg)
+ }
+}
+
// =============================================================================
// Dashboard Content Verification Tests
// =============================================================================
@@ -1945,34 +1957,18 @@ func TestHandleDashboard_ContainsSettingsLink(t *testing.T) {
// contains a link to /settings. This catches regressions where the settings
// button is accidentally removed.
func TestDashboardTemplate_HasSettingsLink(t *testing.T) {
- // Read the actual template file and verify it contains a settings link
- content, err := os.ReadFile("../../web/templates/index.html")
- if err != nil {
- t.Skipf("Cannot read template file (running from unexpected directory): %v", err)
- }
-
- templateStr := string(content)
-
- if !strings.Contains(templateStr, `href="/settings"`) {
- t.Error("index.html must contain a link to /settings (settings button missing from UI)")
- }
+ assertTemplateContains(t, "../../web/templates/index.html",
+ `href="/settings"`,
+ "index.html must contain a link to /settings (settings button missing from UI)")
}
// TestSettingsTemplate_HasCSRFToken verifies the settings page exposes a CSRF
// token in a meta tag so that JavaScript (e.g. passkey registration) can include
// it in POST requests. Without this, passkey registration fails with 403.
func TestSettingsTemplate_HasCSRFToken(t *testing.T) {
- content, err := os.ReadFile("../../web/templates/settings.html")
- if err != nil {
- t.Skipf("Cannot read template file: %v", err)
- }
- tmpl := string(content)
-
- // The template must include a meta tag or hidden input with the Go template
- // variable .CSRFToken so JS can retrieve it
- if !strings.Contains(tmpl, ".CSRFToken") {
- t.Error("settings.html must expose .CSRFToken (e.g. via meta tag) for JavaScript passkey registration")
- }
+ assertTemplateContains(t, "../../web/templates/settings.html",
+ ".CSRFToken",
+ "settings.html must expose .CSRFToken (e.g. via meta tag) for JavaScript passkey registration")
}
// TestHandleSettingsPage_PassesCSRFToken verifies the settings handler includes
@@ -2007,6 +2003,53 @@ func TestHandleSettingsPage_PassesCSRFToken(t *testing.T) {
}
}
+// TestHandleDashboard_PassesBuildVersion verifies the dashboard handler includes
+// BuildVersion in the template data.
+func TestHandleDashboard_PassesBuildVersion(t *testing.T) {
+ db, cleanup := setupTestDB(t)
+ defer cleanup()
+
+ mockTodoist := &mockTodoistClient{}
+ mockTrello := &mockTrelloClient{}
+ renderer := NewMockRenderer()
+
+ h := &Handler{
+ store: db,
+ todoistClient: mockTodoist,
+ trelloClient: mockTrello,
+ config: &config.Config{CacheTTLMinutes: 5},
+ renderer: renderer,
+ BuildVersion: "abc123",
+ }
+
+ req := httptest.NewRequest("GET", "/", nil)
+ w := httptest.NewRecorder()
+
+ h.HandleDashboard(w, req)
+
+ if w.Code != http.StatusOK {
+ t.Fatalf("Expected status 200, got %d", w.Code)
+ }
+
+ if len(renderer.Calls) == 0 {
+ t.Fatal("Expected renderer to be called")
+ }
+
+ lastCall := renderer.Calls[len(renderer.Calls)-1]
+ dataStr := fmt.Sprintf("%+v", lastCall.Data)
+ if !strings.Contains(dataStr, "abc123") {
+ t.Error("Dashboard template data must include BuildVersion value")
+ }
+}
+
+// TestDashboardTemplate_HasBuildVersion verifies that index.html contains
+// a build version placeholder so users can see the deployed version.
+func TestDashboardTemplate_HasBuildVersion(t *testing.T) {
+ assertTemplateContains(t, "../../web/templates/index.html",
+ ".BuildVersion",
+ "index.html must contain .BuildVersion to display the build version in the footer")
+}
+
// TestTimelineTemplate_CheckboxesTargetSelf verifies that completion checkboxes
// in the timeline calendar grid target their parent item, not #tab-content.
func TestTimelineTemplate_CheckboxesTargetSelf(t *testing.T) {