--- phase: 04-ux-improvements plan: 01 type: execute wave: 1 depends_on: [] files_modified: - pkg/diunwebhook/store.go - pkg/diunwebhook/sqlite_store.go - pkg/diunwebhook/postgres_store.go - pkg/diunwebhook/diunwebhook.go - pkg/diunwebhook/diunwebhook_test.go - pkg/diunwebhook/export_test.go - cmd/diunwebhook/main.go autonomous: true requirements: - BULK-01 - BULK-02 must_haves: truths: - "POST /api/updates/acknowledge-all marks all unacknowledged updates and returns the count" - "POST /api/updates/acknowledge-by-tag marks only unacknowledged updates in the given tag and returns the count" - "Both endpoints return 200 with {count: 0} when nothing matches (not 404)" artifacts: - path: "pkg/diunwebhook/store.go" provides: "Extended Store interface with AcknowledgeAll and AcknowledgeByTag" contains: "AcknowledgeAll" - path: "pkg/diunwebhook/sqlite_store.go" provides: "SQLiteStore bulk acknowledge implementations" contains: "func (s *SQLiteStore) AcknowledgeAll" - path: "pkg/diunwebhook/postgres_store.go" provides: "PostgresStore bulk acknowledge implementations" contains: "func (s *PostgresStore) AcknowledgeAll" - path: "pkg/diunwebhook/diunwebhook.go" provides: "HTTP handlers for bulk acknowledge endpoints" contains: "AcknowledgeAllHandler" - path: "cmd/diunwebhook/main.go" provides: "Route registration for new endpoints" contains: "/api/updates/acknowledge-all" key_links: - from: "cmd/diunwebhook/main.go" to: "pkg/diunwebhook/diunwebhook.go" via: "mux.HandleFunc registration" pattern: "HandleFunc.*acknowledge" - from: "pkg/diunwebhook/diunwebhook.go" to: "pkg/diunwebhook/store.go" via: "s.store.AcknowledgeAll() and s.store.AcknowledgeByTag()" pattern: "s\\.store\\.Acknowledge(All|ByTag)" --- Add backend support for bulk acknowledge operations: acknowledge all pending updates at once, and acknowledge all pending updates within a specific tag group. Purpose: Enables the frontend (Plan 03) to offer "Dismiss All" and "Dismiss Group" buttons. Output: Two new Store interface methods, implementations for both SQLite and PostgreSQL, two new HTTP handlers, route registrations, and tests. @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/04-ux-improvements/04-CONTEXT.md @.planning/phases/04-ux-improvements/04-RESEARCH.md From pkg/diunwebhook/store.go: ```go type Store interface { UpsertEvent(event DiunEvent) error GetUpdates() (map[string]UpdateEntry, error) AcknowledgeUpdate(image string) (found bool, err error) ListTags() ([]Tag, error) CreateTag(name string) (Tag, error) DeleteTag(id int) (found bool, err error) AssignTag(image string, tagID int) error UnassignTag(image string) error TagExists(id int) (bool, error) } ``` From pkg/diunwebhook/sqlite_store.go (AcknowledgeUpdate pattern to follow): ```go func (s *SQLiteStore) AcknowledgeUpdate(image string) (found bool, err error) { s.mu.Lock() defer s.mu.Unlock() res, err := s.db.Exec(`UPDATE updates SET acknowledged_at = datetime('now') WHERE image = ?`, image) if err != nil { return false, err } n, _ := res.RowsAffected() return n > 0, nil } ``` From pkg/diunwebhook/postgres_store.go (same method, PostgreSQL dialect): ```go func (s *PostgresStore) AcknowledgeUpdate(image string) (found bool, err error) { res, err := s.db.Exec(`UPDATE updates SET acknowledged_at = NOW() WHERE image = $1`, image) if err != nil { return false, err } n, _ := res.RowsAffected() return n > 0, nil } ``` From pkg/diunwebhook/diunwebhook.go (DismissHandler pattern to follow): ```go func (s *Server) DismissHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPatch { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } // ... } ``` Current route registration order in cmd/diunwebhook/main.go: ```go mux.HandleFunc("/api/updates/", srv.DismissHandler) mux.HandleFunc("/api/updates", srv.UpdatesHandler) ``` Task 1: Extend Store interface and implement AcknowledgeAll + AcknowledgeByTag in both stores pkg/diunwebhook/store.go, pkg/diunwebhook/sqlite_store.go, pkg/diunwebhook/postgres_store.go - pkg/diunwebhook/store.go - pkg/diunwebhook/sqlite_store.go - pkg/diunwebhook/postgres_store.go - Test 1: AcknowledgeAll on empty DB returns count=0, no error - Test 2: AcknowledgeAll with 3 unacknowledged updates returns count=3; subsequent GetUpdates shows all acknowledged - Test 3: AcknowledgeAll with 2 unacknowledged + 1 already acknowledged returns count=2 - Test 4: AcknowledgeByTag with valid tag_id returns count of matching unacknowledged updates in that tag - Test 5: AcknowledgeByTag with non-existent tag_id returns count=0, no error - Test 6: AcknowledgeByTag does not affect updates in other tags or untagged updates 1. Add two methods to the Store interface in `store.go` (per D-01): ```go AcknowledgeAll() (count int, err error) AcknowledgeByTag(tagID int) (count int, err error) ``` 2. Implement in `sqlite_store.go` (following AcknowledgeUpdate pattern with mutex): - `AcknowledgeAll`: `s.mu.Lock()`, `s.db.Exec("UPDATE updates SET acknowledged_at = datetime('now') WHERE acknowledged_at IS NULL")`, return `int(RowsAffected())` - `AcknowledgeByTag`: `s.mu.Lock()`, `s.db.Exec("UPDATE updates SET acknowledged_at = datetime('now') WHERE acknowledged_at IS NULL AND image IN (SELECT image FROM tag_assignments WHERE tag_id = ?)", tagID)`, return `int(RowsAffected())` 3. Implement in `postgres_store.go` (no mutex, use NOW() and $1 positional param): - `AcknowledgeAll`: `s.db.Exec("UPDATE updates SET acknowledged_at = NOW() WHERE acknowledged_at IS NULL")`, return `int(RowsAffected())` - `AcknowledgeByTag`: `s.db.Exec("UPDATE updates SET acknowledged_at = NOW() WHERE acknowledged_at IS NULL AND image IN (SELECT image FROM tag_assignments WHERE tag_id = $1)", tagID)`, return `int(RowsAffected())` cd /home/jean-luc-makiola/Development/projects/DiunDashboard && go build ./... - store.go contains `AcknowledgeAll() (count int, err error)` in the Store interface - store.go contains `AcknowledgeByTag(tagID int) (count int, err error)` in the Store interface - sqlite_store.go contains `func (s *SQLiteStore) AcknowledgeAll() (int, error)` - sqlite_store.go contains `func (s *SQLiteStore) AcknowledgeByTag(tagID int) (int, error)` - sqlite_store.go AcknowledgeAll contains `s.mu.Lock()` - sqlite_store.go AcknowledgeAll contains `WHERE acknowledged_at IS NULL` - sqlite_store.go AcknowledgeByTag contains `SELECT image FROM tag_assignments WHERE tag_id = ?` - postgres_store.go contains `func (s *PostgresStore) AcknowledgeAll() (int, error)` - postgres_store.go contains `func (s *PostgresStore) AcknowledgeByTag(tagID int) (int, error)` - postgres_store.go AcknowledgeByTag contains `$1` (positional param) - `go build ./...` exits 0 Store interface extended with 2 new methods; both SQLiteStore and PostgresStore compile and implement the interface Task 2: Add HTTP handlers, route registration, and tests for bulk acknowledge endpoints pkg/diunwebhook/diunwebhook.go, pkg/diunwebhook/diunwebhook_test.go, pkg/diunwebhook/export_test.go, cmd/diunwebhook/main.go - pkg/diunwebhook/diunwebhook.go - pkg/diunwebhook/diunwebhook_test.go - pkg/diunwebhook/export_test.go - cmd/diunwebhook/main.go - Test: POST /api/updates/acknowledge-all with no updates returns 200 + {"count":0} - Test: POST /api/updates/acknowledge-all with 2 pending updates returns 200 + {"count":2} - Test: GET /api/updates/acknowledge-all returns 405 - Test: POST /api/updates/acknowledge-by-tag with valid tag_id returns 200 + {"count":N} - Test: POST /api/updates/acknowledge-by-tag with tag_id=0 returns 400 - Test: POST /api/updates/acknowledge-by-tag with missing body returns 400 - Test: POST /api/updates/acknowledge-by-tag with non-existent tag returns 200 + {"count":0} - Test: GET /api/updates/acknowledge-by-tag returns 405 1. Add `AcknowledgeAllHandler` to `diunwebhook.go` (per D-02): ```go func (s *Server) AcknowledgeAllHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } count, err := s.store.AcknowledgeAll() if err != nil { log.Printf("AcknowledgeAllHandler: %v", err) http.Error(w, "internal error", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]int{"count": count}) } ``` 2. Add `AcknowledgeByTagHandler` to `diunwebhook.go` (per D-02): ```go func (s *Server) AcknowledgeByTagHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } r.Body = http.MaxBytesReader(w, r.Body, maxBodyBytes) var req struct { TagID int `json:"tag_id"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "bad request", http.StatusBadRequest) return } if req.TagID <= 0 { http.Error(w, "bad request: tag_id required", http.StatusBadRequest) return } count, err := s.store.AcknowledgeByTag(req.TagID) if err != nil { log.Printf("AcknowledgeByTagHandler: %v", err) http.Error(w, "internal error", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]int{"count": count}) } ``` 3. Register routes in `main.go` -- CRITICAL: new specific paths BEFORE the existing `/api/updates/` subtree pattern: ```go mux.HandleFunc("/api/updates/acknowledge-all", srv.AcknowledgeAllHandler) mux.HandleFunc("/api/updates/acknowledge-by-tag", srv.AcknowledgeByTagHandler) mux.HandleFunc("/api/updates/", srv.DismissHandler) // existing -- must remain after mux.HandleFunc("/api/updates", srv.UpdatesHandler) // existing ``` 4. Add test helper to `export_test.go`: ```go func (s *Server) TestCreateTag(name string) (Tag, error) { return s.store.CreateTag(name) } func (s *Server) TestAssignTag(image string, tagID int) error { return s.store.AssignTag(image, tagID) } ``` 5. Write tests in `diunwebhook_test.go` following the existing `Test_` naming convention. Use `NewTestServer()` for each test. Setup: use `TestUpsertEvent` to create events, `TestCreateTag` + `TestAssignTag` to setup tag assignments. 6. Also add the Vite dev proxy for the two new endpoints in `frontend/vite.config.ts` -- NOT needed, the existing proxy config already proxies all `/api` requests to `:8080`. cd /home/jean-luc-makiola/Development/projects/DiunDashboard && go test -v -run "TestAcknowledge(All|ByTag)Handler" ./pkg/diunwebhook/ - diunwebhook.go contains `func (s *Server) AcknowledgeAllHandler(` - diunwebhook.go contains `func (s *Server) AcknowledgeByTagHandler(` - diunwebhook.go AcknowledgeAllHandler contains `r.Method != http.MethodPost` - diunwebhook.go AcknowledgeByTagHandler contains `http.MaxBytesReader` - diunwebhook.go AcknowledgeByTagHandler contains `req.TagID <= 0` - main.go contains `"/api/updates/acknowledge-all"` BEFORE `"/api/updates/"` - main.go contains `"/api/updates/acknowledge-by-tag"` BEFORE `"/api/updates/"` - diunwebhook_test.go contains `TestAcknowledgeAllHandler_Empty` - diunwebhook_test.go contains `TestAcknowledgeByTagHandler` - `go test -run "TestAcknowledge" ./pkg/diunwebhook/` exits 0 - `go vet ./...` exits 0 Both bulk acknowledge endpoints respond correctly; all new tests pass; route order verified ```bash cd /home/jean-luc-makiola/Development/projects/DiunDashboard go build ./... go vet ./... go test -v -run "TestAcknowledge" ./pkg/diunwebhook/ go test -v ./pkg/diunwebhook/ # all existing tests still pass ``` - Store interface has 11 methods (9 existing + 2 new) - Both SQLiteStore and PostgresStore implement all 11 methods - POST /api/updates/acknowledge-all returns 200 + {"count": N} - POST /api/updates/acknowledge-by-tag returns 200 + {"count": N} - All existing tests continue to pass - Route registration order prevents DismissHandler from shadowing new endpoints After completion, create `.planning/phases/04-ux-improvements/04-01-SUMMARY.md`