---
phase: 15-ux-improvement
plan: 01
type: implement
wave: 1
depends_on: []
files_modified: [internal/tagsweep/store.go, internal/server/handlers_tagsweep.go, web/src/routes/TagReviewView.tsx, web/src/api/client.ts]
autonomous: true
requirements: [UX-01]

must_haves:
  truths:
    - "Bulk-approve shows non-blocking progress indicator after submission"
    - "User sees 'Applying... N/M' state while commit is in-flight"
    - "Success state shown only after commit lands"
    - "Stale revision (409) per-page failures surfaced individually"
    - "User can navigate away and return to see progress"
  artifacts:
    - path: "internal/tagsweep/store.go"
      provides: "SweepProgress struct + Store.SweepStatus() method returning job queue progress"
    - path: "internal/server/handlers_tagsweep.go"
      provides: "GET /api/v1/admin/tags/sweep/status endpoint"
---

<objective>
Improve bulk-approve UX: replace the fire-and-forget response with a non-blocking progress indicator. Backend exposes sweep job progress (queued/done/failed counts). Admin UI polls progress live. User can navigate away and return — progress persists as store state.
</objective>

<execution_context>
@$HOME/.claude/anvil/workflows/execute-plan.md
</execution_context>

<context>
@.planning/ROADMAP.md
@.planning/REQUIREMENTS.md §UX-01
@.planning/phases/15-ux-improvement/15-CONTEXT.md
</context>

<tasks>

<task type="implement" tdd="true">
  <name>Task 1: Add sweep progress tracking to tagsweep store</name>
  <files>internal/tagsweep/store.go</files>
  <read_first>
    - internal/tagsweep/store.go — current Store struct, methods, SQL schema
    - internal/tagsweep/job.go — SuggestHandler, how jobs flow through the store
  </read_first>
  <behavior>
    Add a `SweepProgress` struct and `SweepStatus()` method to the Store:
    - `SweepProgress` has fields: `Queued int` (pending jobs not yet started), `Processing int` (currently running, always 1 with single worker), `Done int` (completed successfully), `Failed int` (completed with error), `Total int` (sum of all)
    - `SweepStatus(ctx)` queries the job queue state — counts jobs by status in the job store
    - Also add a `PendingApprovalCount(ctx) int` method returning count of rows in tag_suggestions awaiting approval (for the review queue badge)
    Integration with the existing jobs.Worker — use the worker's stats or maintain a lightweight counter in the store.
  </behavior>
  <action>
    1. Read `internal/tagsweep/store.go` to understand current schema
    2. Read `internal/jobs/` to understand Worker stats API
    3. Add `SweepProgress` struct:
    ```go
    type SweepProgress struct {
        Queued     int `json:"queued"`
        Processing int `json:"processing"`
        Done       int `json:"done"`
        Failed     int `json:"failed"`
        Total      int `json:"total"`
    }
    ```
    4. Add `SweepStatus(ctx context.Context) (SweepProgress, error)` method that queries the job queue
    5. Add `PendingApprovalCount(ctx context.Context) (int, error)` if not already present
    6. Write unit test in `store_test.go`
  </action>
  <acceptance_criteria>
    - grep 'SweepProgress' internal/tagsweep/store.go returns match
    - grep 'SweepStatus' internal/tagsweep/store.go returns match
    - grep 'PendingApprovalCount' internal/tagsweep/store.go returns match
    - go test ./internal/tagsweep/... exits 0
    - go build ./... exits 0
  </acceptance_criteria>
  <done>SweepProgress tracking added to store with unit tests.</done>
</task>

<task type="implement" tdd="true">
  <name>Task 2: Add GET /api/v1/admin/tags/sweep/status endpoint</name>
  <files>internal/server/handlers_tagsweep.go</files>
  <read_first>
    - internal/server/handlers_tagsweep.go — existing sweep handlers
    - internal/tagsweep/store.go — SweepStatus method from Task 1
  </read_first>
  <behavior>
    Add `GET /api/v1/admin/tags/sweep/status` endpoint (admin auth, behind RequireRole(admin)):
    - Returns `SweepProgress` JSON with current job queue state
    - Also returns `pending_approval` count (pages in review queue)
    - 200 with progress; 500 if store unavailable
    Register route in the router setup.
  </behavior>
  <action>
    1. Add handler function `handleTagSweepStatus`:
    ```go
    func (h *authHandlers) handleTagSweepStatus(w http.ResponseWriter, r *http.Request) {
        if h.tagSuggestions == nil {
            writeError(w, http.StatusInternalServerError, tagSweepUnavailable)
            return
        }
        progress, err := h.tagSuggestions.SweepStatus(r.Context())
        if err != nil {
            slog.Error("tag sweep status failed", slog.String("error", err.Error()))
            writeError(w, http.StatusInternalServerError, tagSweepUnavailable)
            return
        }
        pending, _ := h.tagSuggestions.PendingApprovalCount(r.Context())
        writeJSON(w, http.StatusOK, map[string]any{
            "progress":         progress,
            "pending_approval": pending,
        })
    }
    ```
    2. Register route: `mux.Get("/api/v1/admin/tags/sweep/status", ...)` in the admin subgroup
    3. Add route registration in the appropriate setup function
  </action>
  <acceptance_criteria>
    - grep 'handleTagSweepStatus' internal/server/handlers_tagsweep.go returns match
    - grep '/api/v1/admin/tags/sweep/status' internal/server/ returns match
    - go build ./... exits 0
  </acceptance_criteria>
  <done>Sweep status endpoint added, returning progress + pending counts.</done>
</task>

<task type="implement" tdd="true">
  <name>Task 3: Add progress polling to admin UI</name>
  <files>web/src/routes/TagReviewView.tsx, web/src/api/client.ts</files>
  <read_first>
    - web/src/routes/TagReviewView.tsx — current review queue UI
    - web/src/api/client.ts — API client types and functions
    - web/src/hooks/useGraph.ts — react-query pattern to follow
  </read_first>
  <behavior>
    Update the Tag Review admin UI to:
    - After bulk-approve submission, poll `GET /api/v1/admin/tags/sweep/status` every 2s
    - Display progress bar + "Applying... N/M pages" text while jobs are in-flight
    - Switch to success state (checkmark + "Applied N pages") when done count reaches total
    - Show per-page failure details for stale/notfound pages
    - Non-blocking: user can navigate away and the progress persists on return (re-poll on mount)
    - Add `getSweepStatus()` to the API client
  </behavior>
  <action>
    1. Add to `web/src/api/client.ts`:
    - `SweepProgress` type matching the Go struct
    - `getSweepStatus(): Promise<{progress: SweepProgress; pending_approval: number}>` function
    2. Update `web/src/routes/TagReviewView.tsx`:
    - Add `useQuery` with refetchInterval for sweep status polling
    - Add progress bar component (simple `<progress>` or div-based bar)
    - Show "Applying... N/M" state after approve click
    - Show success when done; show failures inline
    - Use react-query `refetchInterval: 2000` for polling, `enabled: hasActiveSweep`
  </action>
  <acceptance_criteria>
    - grep 'SweepProgress' web/src/api/client.ts returns match
    - grep 'getSweepStatus' web/src/api/client.ts returns match
    - grep 'sweep/status' web/src/api/client.ts returns match
    - grep 'progress' web/src/routes/TagReviewView.tsx returns match (case-insensitive)
    - npx tsc --noEmit -p web/tsconfig.json exits 0
  </acceptance_criteria>
  <done>Progress polling added to admin UI with progress bar and success/failure states.</done>
</task>

</tasks>

<success_criteria>
- Bulk-approve shows non-blocking progress after submission
- User sees live "Applying... N/M" count
- Success shown only after all jobs complete
- Per-page failures surfaced
- go build + tsc both clean
</success_criteria>
