docs(04-ux-improvements): create phase plan
This commit is contained in:
317
.planning/phases/04-ux-improvements/04-01-PLAN.md
Normal file
317
.planning/phases/04-ux-improvements/04-01-PLAN.md
Normal file
@@ -0,0 +1,317 @@
|
||||
---
|
||||
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)"
|
||||
---
|
||||
|
||||
<objective>
|
||||
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.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<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
|
||||
|
||||
<interfaces>
|
||||
<!-- Store interface the executor must extend -->
|
||||
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)
|
||||
```
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Extend Store interface and implement AcknowledgeAll + AcknowledgeByTag in both stores</name>
|
||||
<files>pkg/diunwebhook/store.go, pkg/diunwebhook/sqlite_store.go, pkg/diunwebhook/postgres_store.go</files>
|
||||
<read_first>
|
||||
- pkg/diunwebhook/store.go
|
||||
- pkg/diunwebhook/sqlite_store.go
|
||||
- pkg/diunwebhook/postgres_store.go
|
||||
</read_first>
|
||||
<behavior>
|
||||
- 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
|
||||
</behavior>
|
||||
<action>
|
||||
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())`
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/jean-luc-makiola/Development/projects/DiunDashboard && go build ./...</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- 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
|
||||
</acceptance_criteria>
|
||||
<done>Store interface extended with 2 new methods; both SQLiteStore and PostgresStore compile and implement the interface</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 2: Add HTTP handlers, route registration, and tests for bulk acknowledge endpoints</name>
|
||||
<files>pkg/diunwebhook/diunwebhook.go, pkg/diunwebhook/diunwebhook_test.go, pkg/diunwebhook/export_test.go, cmd/diunwebhook/main.go</files>
|
||||
<read_first>
|
||||
- pkg/diunwebhook/diunwebhook.go
|
||||
- pkg/diunwebhook/diunwebhook_test.go
|
||||
- pkg/diunwebhook/export_test.go
|
||||
- cmd/diunwebhook/main.go
|
||||
</read_first>
|
||||
<behavior>
|
||||
- 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
|
||||
</behavior>
|
||||
<action>
|
||||
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<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`.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/jean-luc-makiola/Development/projects/DiunDashboard && go test -v -run "TestAcknowledge(All|ByTag)Handler" ./pkg/diunwebhook/</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- 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
|
||||
</acceptance_criteria>
|
||||
<done>Both bulk acknowledge endpoints respond correctly; all new tests pass; route order verified</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
```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
|
||||
```
|
||||
</verification>
|
||||
|
||||
<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>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/04-ux-improvements/04-01-SUMMARY.md`
|
||||
</output>
|
||||
Reference in New Issue
Block a user