summaryrefslogtreecommitdiff
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
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>
-rw-r--r--cmd/dashboard/main.go9
-rwxr-xr-xdeploy.sh5
-rw-r--r--internal/handlers/handlers.go6
-rw-r--r--internal/handlers/handlers_test.go87
-rwxr-xr-xscripts/logs11
-rw-r--r--test/acceptance_test.go2
-rw-r--r--web/templates/index.html4
7 files changed, 98 insertions, 26 deletions
diff --git a/cmd/dashboard/main.go b/cmd/dashboard/main.go
index 0553041..073364f 100644
--- a/cmd/dashboard/main.go
+++ b/cmd/dashboard/main.go
@@ -27,7 +27,14 @@ import (
"github.com/go-webauthn/webauthn/webauthn"
)
+// Set via -ldflags at build time
+var (
+ buildCommit = "dev"
+ buildTime = "unknown"
+)
+
func main() {
+ log.Printf("task-dashboard build=%s time=%s", buildCommit, buildTime)
// Load .env file (ignore error if file doesn't exist - env vars might be set directly)
_ = godotenv.Load()
@@ -139,7 +146,7 @@ func main() {
}
// Initialize handlers
- h := handlers.New(db, todoistClient, trelloClient, planToEatClient, googleCalendarClient, googleTasksClient, cfg)
+ h := handlers.New(db, todoistClient, trelloClient, planToEatClient, googleCalendarClient, googleTasksClient, cfg, buildCommit)
// Set up router
r := chi.NewRouter()
diff --git a/deploy.sh b/deploy.sh
index abd9cfc..a201d82 100755
--- a/deploy.sh
+++ b/deploy.sh
@@ -11,7 +11,10 @@ echo "==> Building CSS..."
npm run css:build
echo "==> Building binary (linux/amd64)..."
-GOOS=linux GOARCH=amd64 go build -o app cmd/dashboard/main.go
+BUILD_COMMIT=$(git rev-parse --short HEAD)
+BUILD_TIME=$(date -u '+%Y-%m-%dT%H:%M:%SZ')
+LDFLAGS="-X main.buildCommit=${BUILD_COMMIT} -X main.buildTime=${BUILD_TIME}"
+GOOS=linux GOARCH=amd64 go build -ldflags "$LDFLAGS" -o app cmd/dashboard/main.go
echo "==> Pushing code to ${REMOTE}..."
git push ${REMOTE} master
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) {
diff --git a/scripts/logs b/scripts/logs
new file mode 100755
index 0000000..7cec0c6
--- /dev/null
+++ b/scripts/logs
@@ -0,0 +1,11 @@
+#!/bin/bash
+# Fetch logs from the production task-dashboard service
+#
+# Usage:
+# scripts/logs # last 100 lines
+# scripts/logs -f # follow (tail -f)
+# scripts/logs -n 500 # last 500 lines
+# scripts/logs --since "1 hour ago"
+# scripts/logs --grep "error"
+
+ssh titanium journalctl -u task-dashboard@doot.terst.org --no-pager "$@"
diff --git a/test/acceptance_test.go b/test/acceptance_test.go
index fb55be4..0bcf974 100644
--- a/test/acceptance_test.go
+++ b/test/acceptance_test.go
@@ -82,7 +82,7 @@ func setupTestServer(t *testing.T) (*httptest.Server, *store.Store, *http.Client
}
// Initialize handlers
- h := handlers.New(db, todoistClient, trelloClient, nil, nil, nil, cfg)
+ h := handlers.New(db, todoistClient, trelloClient, nil, nil, nil, cfg, "test")
// Set up router (same as main.go)
r := chi.NewRouter()
diff --git a/web/templates/index.html b/web/templates/index.html
index 9c90570..2a07a32 100644
--- a/web/templates/index.html
+++ b/web/templates/index.html
@@ -308,6 +308,10 @@
</div>
</div>
+ <div class="fixed bottom-0 right-0 p-2 text-[10px] text-white/20 pointer-events-none">
+ {{.BuildVersion}}
+ </div>
+
<script src="/static/js/htmx.min.js"></script>
<script src="/static/js/app.js"></script>
</body>