Files
DiunDashboard/.planning/phases/04-ux-improvements/04-01-PLAN.md
Jean-Luc Makiola 010daa227d
All checks were successful
CI / build-test (push) Successful in 1m31s
fix(04): revise plans based on checker feedback
2026-03-24 09:50:11 +01:00

16 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
04-ux-improvements 01 execute 1
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
true
BULK-01
BULK-02
truths artifacts key_links
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)
path provides contains
pkg/diunwebhook/store.go Extended Store interface with AcknowledgeAll and AcknowledgeByTag AcknowledgeAll
path provides contains
pkg/diunwebhook/sqlite_store.go SQLiteStore bulk acknowledge implementations func (s *SQLiteStore) AcknowledgeAll
path provides contains
pkg/diunwebhook/postgres_store.go PostgresStore bulk acknowledge implementations func (s *PostgresStore) AcknowledgeAll
path provides contains
pkg/diunwebhook/diunwebhook.go HTTP handlers for bulk acknowledge endpoints AcknowledgeAllHandler
path provides contains
cmd/diunwebhook/main.go Route registration for new endpoints /api/updates/acknowledge-all
from to via pattern
cmd/diunwebhook/main.go pkg/diunwebhook/diunwebhook.go mux.HandleFunc registration HandleFunc.*acknowledge
from to via pattern
pkg/diunwebhook/diunwebhook.go pkg/diunwebhook/store.go s.store.AcknowledgeAll() and s.store.AcknowledgeByTag() 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.

<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>

@.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):

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):

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):

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:

mux.HandleFunc("/api/updates/", srv.DismissHandler)
mux.HandleFunc("/api/updates", srv.UpdatesHandler)
Task 1: Extend Store interface and implement AcknowledgeAll + AcknowledgeByTag with store-level tests pkg/diunwebhook/store.go, pkg/diunwebhook/sqlite_store.go, pkg/diunwebhook/postgres_store.go, pkg/diunwebhook/diunwebhook_test.go, pkg/diunwebhook/export_test.go - pkg/diunwebhook/store.go - pkg/diunwebhook/sqlite_store.go - pkg/diunwebhook/postgres_store.go - pkg/diunwebhook/diunwebhook_test.go - pkg/diunwebhook/export_test.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 TDD approach -- write tests first, then implement:
1. Add test helper exports to `export_test.go`:
   ```go
   func (s *Server) TestAcknowledgeAll() (int, error) {
       return s.Store().AcknowledgeAll()
   }
   func (s *Server) TestAcknowledgeByTag(tagID int) (int, error) {
       return s.Store().AcknowledgeByTag(tagID)
   }
   ```
   (Add a `Store() Store` accessor method on Server if not already present, or access the store field directly via an existing test export pattern.)

2. Write store-level tests in `diunwebhook_test.go` following existing `Test<Function>_<Scenario>` convention:
   - `TestAcknowledgeAll_Empty`: create server, call TestAcknowledgeAll, assert count=0, no error
   - `TestAcknowledgeAll_AllUnacknowledged`: upsert 3 events via TestUpsertEvent, call TestAcknowledgeAll, assert count=3, then call GetUpdates and verify all have acknowledged=true
   - `TestAcknowledgeAll_MixedState`: upsert 3 events, acknowledge 1 via existing dismiss, call TestAcknowledgeAll, assert count=2
   - `TestAcknowledgeByTag_MatchingTag`: upsert 2 events, create tag, assign both to tag, call TestAcknowledgeByTag(tagID), assert count=2
   - `TestAcknowledgeByTag_NonExistentTag`: call TestAcknowledgeByTag(9999), assert count=0, no error
   - `TestAcknowledgeByTag_OnlyAffectsTargetTag`: upsert 3 events, create 2 tags, assign 2 events to tag1 and 1 to tag2, call TestAcknowledgeByTag(tag1.ID), assert count=2, verify tag2's event is still unacknowledged via GetUpdates

   Run tests -- they must FAIL (RED) since methods don't exist yet.

3. 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)
   ```

4. 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())`

5. 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())`

6. Run tests again -- they must PASS (GREEN).
cd /home/jean-luc-makiola/Development/projects/DiunDashboard && go test -v -run "TestAcknowledge(All|ByTag)_" ./pkg/diunwebhook/ - 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) - diunwebhook_test.go contains `TestAcknowledgeAll_Empty` - diunwebhook_test.go contains `TestAcknowledgeByTag_OnlyAffectsTargetTag` - `go test -v -run "TestAcknowledge(All|ByTag)_" ./pkg/diunwebhook/` exits 0 Store interface extended with 2 new methods; both SQLiteStore and PostgresStore compile and implement the interface; 6 store-level tests pass Task 2: Add HTTP handlers, route registration, and handler 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 handler tests in `diunwebhook_test.go` following the existing `Test<Handler>_<Scenario>` 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 ```

<success_criteria>

  • 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 </success_criteria>
After completion, create `.planning/phases/04-ux-improvements/04-01-SUMMARY.md`