diff --git a/docs/superpowers/plans/2026-03-26-reorder-validation.md b/docs/superpowers/plans/2026-03-26-reorder-validation.md new file mode 100644 index 0000000..369593e --- /dev/null +++ b/docs/superpowers/plans/2026-03-26-reorder-validation.md @@ -0,0 +1,252 @@ +# Reorder-Validierung Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** `PlaylistStore.Reorder` soll mit `ErrReorderMismatch` abbrechen, wenn die übergebene ID-Liste nicht vollständig oder nicht korrekt ist; beide Handler geben dann 400 zurück. + +**Architecture:** Sentinel-Fehler im store-Package; Vollständigkeitsprüfung per COUNT + RowsAffected-Check im Store; beide HTTP-Handler unterscheiden Validierungsfehler von DB-Fehlern via `errors.Is`. + +**Tech Stack:** Go, pgx/v5, net/http + +--- + +### Task 1: Sentinel-Fehler + Validierung in `store.go` + +**Files:** +- Modify: `server/backend/internal/store/store.go:4-10` (imports) +- Modify: `server/backend/internal/store/store.go:533-550` (Reorder) + +- [ ] **Schritt 1: `errors` zu den Imports hinzufügen** + + Datei: `server/backend/internal/store/store.go`, Zeilen 4–10. + + Vorher: + ```go + import ( + "context" + "fmt" + "time" + + "github.com/jackc/pgx/v5/pgxpool" + ) + ``` + + Nachher: + ```go + import ( + "context" + "errors" + "fmt" + "time" + + "github.com/jackc/pgx/v5/pgxpool" + ) + ``` + +- [ ] **Schritt 2: Sentinel-Variable nach den Imports einfügen** + + Direkt nach dem Import-Block (vor der ersten Typdefinition) einfügen: + + ```go + // ErrReorderMismatch wird von Reorder zurückgegeben, wenn die übergebene + // ID-Liste nicht mit den tatsächlichen Items der Playlist übereinstimmt. + var ErrReorderMismatch = errors.New("reorder: item list does not match playlist") + ``` + +- [ ] **Schritt 3: `Reorder`-Funktion ersetzen** + + Datei: `server/backend/internal/store/store.go`, die gesamte `Reorder`-Funktion (aktuell Zeilen 533–550) ersetzen durch: + + ```go + // Reorder sets order_index for each item ID in the given slice order. + // Returns ErrReorderMismatch if the number of provided IDs does not match + // the number of items in the playlist, or if any ID does not belong to it. + func (s *PlaylistStore) Reorder(ctx context.Context, playlistID string, itemIDs []string) error { + tx, err := s.pool.Begin(ctx) + if err != nil { + return err + } + defer tx.Rollback(ctx) //nolint:errcheck + + var count int + if err := tx.QueryRow(ctx, + `select count(*) from playlist_items where playlist_id=$1`, playlistID, + ).Scan(&count); err != nil { + return err + } + if count != len(itemIDs) { + return fmt.Errorf("%w: got %d ids, playlist has %d items", + ErrReorderMismatch, len(itemIDs), count) + } + + for i, id := range itemIDs { + tag, err := tx.Exec(ctx, + `update playlist_items set order_index=$1 where id=$2 and playlist_id=$3`, + i, id, playlistID, + ) + if err != nil { + return err + } + if tag.RowsAffected() != 1 { + return fmt.Errorf("%w: id %s not found in playlist %s", + ErrReorderMismatch, id, playlistID) + } + } + return tx.Commit(ctx) + } + ``` + +- [ ] **Schritt 4: Kompilieren** + + ```bash + cd server/backend && go build ./... + ``` + + Erwartet: keine Ausgabe, Exit 0. + +- [ ] **Schritt 5: Committen** + + ```bash + git add server/backend/internal/store/store.go + git commit -m "fix(store): Reorder validiert Vollständigkeit und RowsAffected" + ``` + +--- + +### Task 2: `HandleReorderUI` gibt 400 bei Mismatch zurück + +**Files:** +- Modify: `server/backend/internal/httpapi/manage/ui.go:1-20` (imports) +- Modify: `server/backend/internal/httpapi/manage/ui.go:743-746` (Fehlerbehandlung) + +- [ ] **Schritt 1: `errors` zu den Imports hinzufügen** + + Datei: `server/backend/internal/httpapi/manage/ui.go`. Im import-Block `"errors"` ergänzen (alphabetisch vor `"encoding/json"`): + + ```go + import ( + "bytes" + "encoding/json" + "errors" + "html/template" + "log/slog" + "net/http" + "os" + "path/filepath" + "strconv" + "strings" + "time" + + "git.az-it.net/az/morz-infoboard/server/backend/internal/config" + "git.az-it.net/az/morz-infoboard/server/backend/internal/fileutil" + "git.az-it.net/az/morz-infoboard/server/backend/internal/mqttnotifier" + "git.az-it.net/az/morz-infoboard/server/backend/internal/reqcontext" + "git.az-it.net/az/morz-infoboard/server/backend/internal/store" + ) + ``` + +- [ ] **Schritt 2: Fehlerbehandlung in `HandleReorderUI` ersetzen** + + Datei: `server/backend/internal/httpapi/manage/ui.go`, Zeilen 743–746. Den Block: + + ```go + if err := playlists.Reorder(r.Context(), playlist.ID, ids); err != nil { + http.Error(w, "db error", http.StatusInternalServerError) + return + } + ``` + + ersetzen durch: + + ```go + if err := playlists.Reorder(r.Context(), playlist.ID, ids); err != nil { + if errors.Is(err, store.ErrReorderMismatch) { + http.Error(w, "item list mismatch", http.StatusBadRequest) + } else { + http.Error(w, "db error", http.StatusInternalServerError) + } + return + } + ``` + +- [ ] **Schritt 3: Kompilieren** + + ```bash + cd server/backend && go build ./... + ``` + + Erwartet: keine Ausgabe, Exit 0. + +- [ ] **Schritt 4: Committen** + + ```bash + git add server/backend/internal/httpapi/manage/ui.go + git commit -m "fix(manage): HandleReorderUI gibt 400 bei Mismatch zurück" + ``` + +--- + +### Task 3: `HandleReorder` gibt 400 bei Mismatch zurück + +**Files:** +- Modify: `server/backend/internal/httpapi/manage/playlist.go:1-14` (imports) +- Modify: `server/backend/internal/httpapi/manage/playlist.go:244-247` (Fehlerbehandlung) + +- [ ] **Schritt 1: `errors` zu den Imports hinzufügen** + + Datei: `server/backend/internal/httpapi/manage/playlist.go`. Im import-Block `"errors"` ergänzen: + + ```go + import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "strconv" + "strings" + "time" + + "git.az-it.net/az/morz-infoboard/server/backend/internal/mqttnotifier" + "git.az-it.net/az/morz-infoboard/server/backend/internal/reqcontext" + "git.az-it.net/az/morz-infoboard/server/backend/internal/store" + ) + ``` + +- [ ] **Schritt 2: Fehlerbehandlung in `HandleReorder` ersetzen** + + Datei: `server/backend/internal/httpapi/manage/playlist.go`, Zeilen 244–247. Den Block: + + ```go + if err := playlists.Reorder(r.Context(), playlistID, ids); err != nil { + http.Error(w, "db error", http.StatusInternalServerError) + return + } + ``` + + ersetzen durch: + + ```go + if err := playlists.Reorder(r.Context(), playlistID, ids); err != nil { + if errors.Is(err, store.ErrReorderMismatch) { + http.Error(w, "item list mismatch", http.StatusBadRequest) + } else { + http.Error(w, "db error", http.StatusInternalServerError) + } + return + } + ``` + +- [ ] **Schritt 3: Kompilieren und Tests ausführen** + + ```bash + cd server/backend && go build ./... && go test ./... + ``` + + Erwartet: keine Ausgabe bei build, alle Tests grün. + +- [ ] **Schritt 4: Committen** + + ```bash + git add server/backend/internal/httpapi/manage/playlist.go + git commit -m "fix(manage): HandleReorder gibt 400 bei Mismatch zurück" + ```