diff --git a/docs/superpowers/plans/2026-08-31-rep-scoped-list-filter.md b/docs/superpowers/plans/2026-08-31-rep-scoped-list-filter.md new file mode 100644 index 0000000..7ded738 --- /dev/null +++ b/docs/superpowers/plans/2026-08-31-rep-scoped-list-filter.md @@ -0,0 +1,495 @@ +# Rep-Scoped List Filter 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:** When a logged-in PocketBase user's `user_type` includes `"REP"`, the Orders, Quotes, Invoicing, and Delivery Notes list pages show only records tied to that rep (via `customer.rep` or, for orders, the denormalized `orders.rep` field). Everyone else (buyer, quoter, admin, boss, etc.) sees the unfiltered list, unchanged from today. + +**Architecture:** This is a UI-layer filter only — **not** a security boundary. No PocketBase API rules change, no server session plumbing, no changes to `/api/v1/*` auth. Orders and Quotes already return `customer` (and for orders, `customer.rep`) in their expand, so those two pages filter the already-fetched array client-side. Invoicing and Delivery Notes fetch PocketBase directly from the browser (no `/api/v1` route), so those two get a real PB filter clause (`order.customer.rep = ""` / `customer.rep = ""`) added to their existing query. + +**Known limitation (accepted for this pass):** On Orders and Quotes, the tab-count badges and pagination totals (`totalPages`, `counts`) come from the unfiltered dataset — they are not rep-scoped. A rep filtering on Orders/Quotes may see a page with fewer than `perPage` visible rows, or a "Page 1 of N" that doesn't match the filtered count. Invoicing and Delivery Notes don't have this problem — their filter is a real PB query, so their counts are accurate. Fixing Orders/Quotes properly means threading `rep` through `orders-service.ts` / `quotes-service.ts`'s PB filters, which is a larger, separate follow-up (not in scope here). + +**Tech Stack:** Next.js Client Components, PocketBase JS SDK, Vitest. + +--- + +## File Structure + +- **Create:** `src/lib/rep-visibility.ts` — pure helper `isRepUser(userType)`, mirrors the existing `isRep()` pattern in `src/lib/server/tv-dashboard-service.ts:349`. +- **Create:** `src/lib/rep-visibility.test.ts` — unit tests for the helper. +- **Modify:** `src/app/(app)/orders/page.tsx` — client-side filter on the rendered order list. +- **Modify:** `src/app/(app)/quotes/page.tsx` — client-side filter on the rendered quote list. +- **Modify:** `src/app/(app)/invoicing/page.tsx` — PB filter clause added to `loadInvoicingRows`. +- **Modify:** `src/lib/pb-helpers.ts` — `getDeliveryNotes` gains an optional `repId` param that adds a PB filter clause. +- **Modify:** `src/app/(app)/delivery-notes/page.tsx` — passes the current rep's id into `getDeliveryNotes`. + +--- + +### Task 1: `isRepUser` helper + +**Files:** +- Create: `src/lib/rep-visibility.ts` +- Test: `src/lib/rep-visibility.test.ts` + +- [ ] **Step 1: Write the failing test** + +```typescript +// src/lib/rep-visibility.test.ts +import { describe, expect, it } from "vitest"; +import { isRepUser } from "./rep-visibility"; + +describe("isRepUser", () => { + it("returns true when user_type includes REP", () => { + expect(isRepUser(["REP"])).toBe(true); + }); + + it("is case-insensitive", () => { + expect(isRepUser(["rep"])).toBe(true); + }); + + it("returns true when REP is combined with other roles", () => { + expect(isRepUser(["BUYER", "REP"])).toBe(true); + }); + + it("returns false for other roles", () => { + expect(isRepUser(["BUYER"])).toBe(false); + expect(isRepUser(["ADMIN", "QUOTER"])).toBe(false); + }); + + it("returns false for null, undefined, or non-array input", () => { + expect(isRepUser(null)).toBe(false); + expect(isRepUser(undefined)).toBe(false); + expect(isRepUser("REP")).toBe(false); + }); + + it("returns false for an empty array", () => { + expect(isRepUser([])).toBe(false); + }); +}); +``` + +- [ ] **Step 2: Run the test to verify it fails** + +Run: `npx vitest run src/lib/rep-visibility.test.ts` +Expected: FAIL — `Cannot find module './rep-visibility'` (or similar; the module doesn't exist yet). + +- [ ] **Step 3: Write the implementation** + +```typescript +// src/lib/rep-visibility.ts +/** + * `users.user_type` is a PB multi-select. "REP" is the existing + * convention for marking a sales rep — see the identical pattern in + * `src/lib/server/tv-dashboard-service.ts` (`TV_REP_ROLES` / `isRep`). + */ +export const REP_USER_TYPE = "REP"; + +export function isRepUser(userType: unknown): boolean { + if (!Array.isArray(userType)) return false; + return userType.some( + (value) => typeof value === "string" && value.toUpperCase() === REP_USER_TYPE, + ); +} +``` + +- [ ] **Step 4: Run the test to verify it passes** + +Run: `npx vitest run src/lib/rep-visibility.test.ts` +Expected: PASS — all 6 assertions green. + +- [ ] **Step 5: Commit** + +```bash +git add src/lib/rep-visibility.ts src/lib/rep-visibility.test.ts +git commit -m "feat: add isRepUser helper for rep-scoped list filtering" +``` + +--- + +### Task 2: Orders page — filter to the rep's own orders + +**Files:** +- Modify: `src/app/(app)/orders/page.tsx` + +- [ ] **Step 1: Import the auth hook and the helper** + +In `src/app/(app)/orders/page.tsx`, add to the top imports (near the other `@/lib` and `@/components` imports, e.g. after the `getPocketBase` import at line 26): + +```typescript +import { useAuth } from "@/components/auth-provider"; +import { isRepUser } from "@/lib/rep-visibility"; +``` + +- [ ] **Step 2: Read the current user and derive the visible order list** + +Inside `OrdersPage`, immediately after the existing `useRouter()` call at line 114 (`const router = useRouter();`), add: + +```typescript + const { user } = useAuth(); +``` + +Then, right after the `otherTabCounts` `useMemo` block (ends at line 298, just before the `return (` on line 300), add a new `useMemo`: + +```typescript + // Rep-scoped UI filter — reps only see orders tied to them, either + // via the denormalized `orders.rep` text field or via + // `customer.rep`. Everyone else (buyer/quoter/admin/boss/etc.) sees + // the unfiltered list. This filters the already-fetched page of + // orders; it does not change what the server returns, so tab + // counts / pagination totals stay unscoped — see the plan's "Known + // limitation" note. + const visibleOrders = useMemo(() => { + const userType = (user as { user_type?: string[] } | null)?.user_type; + if (!user || !isRepUser(userType)) return orders; + return orders.filter( + ({ order }) => + order.rep === user.id || order.expand?.customer?.rep === user.id, + ); + }, [orders, user]); +``` + +- [ ] **Step 3: Use `visibleOrders` everywhere the rendered list is read** + +Replace these three usages (leave the `orders` state itself, `fetchOrders`, and the realtime subscription untouched — they still fetch/refetch the full page): + +1. Line 282, `const totalLoaded = orders.length;` → `const totalLoaded = visibleOrders.length;` +2. Line 390, `) : orders.length === 0 ? (` → `) : visibleOrders.length === 0 ? (` +3. Lines 445–464 (the "N matches in Tab for query" block) — change every `orders.length` reference to `visibleOrders.length`: + +```typescript + {searching && ( +

+ {visibleOrders.length} match{visibleOrders.length === 1 ? "" : "es"} in + {" "} + {TAB_GROUP_VISUAL[activeTab].label} for +``` + +4. Line 466, `{orders.map(({ order, items, repName, tabGroups }) => (` → `{visibleOrders.map(({ order, items, repName, tabGroups }) => (` + +- [ ] **Step 4: Manual verification** + +Run the dev server (`npm run dev`, or use the existing preview tooling) and: +1. Log in as a user whose `user_type` does NOT include `REP` (e.g. an admin/buyer account). Confirm `/orders` shows the same orders as before this change. +2. Log in as a user whose `user_type` includes `REP` and who is set as `rep` on at least one customer (or has `orders.rep` set to their id on at least one order). Confirm `/orders` only shows orders for that rep's customers/orders. +3. Confirm the "New Order" flow, search, and tab-switching still work (they operate on `orders`/`fetchOrders` unchanged — only the rendered list is filtered). + +- [ ] **Step 5: Commit** + +```bash +git add src/app/\(app\)/orders/page.tsx +git commit -m "feat(orders): filter list to the logged-in rep's own orders" +``` + +--- + +### Task 3: Quotes page — filter to the rep's own quotes + +**Files:** +- Modify: `src/app/(app)/quotes/page.tsx` + +- [ ] **Step 1: Import the auth hook and the helper** + +Add near the top imports, after the `getPocketBase` import at line 34: + +```typescript +import { useAuth } from "@/components/auth-provider"; +import { isRepUser } from "@/lib/rep-visibility"; +``` + +- [ ] **Step 2: Read the current user and derive the visible quote list** + +Inside `QuotesPage`, immediately after `const router = useRouter();` at line 142, add: + +```typescript + const { user } = useAuth(); +``` + +After the `userById` `useMemo` block (ends at line 440, just before the `toggleExpand` callback at line 447), add: + +```typescript + // Rep-scoped UI filter — same rationale as the Orders page (see + // src/lib/rep-visibility.ts). Quotes only carry rep via the + // customer relation (no denormalized text field like orders.rep). + const visibleQuotes = useMemo(() => { + const userType = (user as { user_type?: string[] } | null)?.user_type; + if (!user || !isRepUser(userType)) return quotes; + return quotes.filter((quote) => quote.expand?.customer?.rep === user.id); + }, [quotes, user]); +``` + +- [ ] **Step 3: Use `visibleQuotes` in the DataTable and the search-count chip** + +1. Line 870, `data={quotes}` → `data={visibleQuotes}` (the `DataTable` prop). +2. Lines 895–919 (the live result-count chip) — change every `quotes.length` reference to `visibleQuotes.length`: + +```typescript + {searchInput.trim() && ( + + {loading ? ( + <>Searching… + ) : visibleQuotes.length === 0 ? ( + <>0 matches for “{searchInput.trim()}” + ) : ( + <> + {visibleQuotes.length} match{visibleQuotes.length === 1 ? "" : "es"} for{" "} + + “{searchInput.trim()}” + + + )} + + )} +``` + +Leave `itemCounts` / `itemsByQuote` / `expandedIds` untouched — they're keyed by quote id and stay correct regardless of which quotes are visible. + +- [ ] **Step 4: Manual verification** + +1. Log in as a non-rep user. Confirm `/quotes` is unchanged. +2. Log in as a rep user who is set as `rep` on at least one customer with quotes. Confirm `/quotes` only shows that rep's quotes, across both the tab-list view and the search view (type something into the search box). +3. Confirm row expand (chevron → items), Process/Open buttons, and template actions still work. + +- [ ] **Step 5: Commit** + +```bash +git add src/app/\(app\)/quotes/page.tsx +git commit -m "feat(quotes): filter list to the logged-in rep's own quotes" +``` + +--- + +### Task 4: Invoicing page — PB filter for the rep's own order items + +**Files:** +- Modify: `src/app/(app)/invoicing/page.tsx` + +- [ ] **Step 1: Import the auth hook and the helper** + +Add near the top imports, after the `getPocketBase` import at line 14: + +```typescript +import { useAuth } from "@/components/auth-provider"; +import { isRepUser } from "@/lib/rep-visibility"; +``` + +- [ ] **Step 2: Thread an optional `repId` through `loadInvoicingRows`** + +Modify the function signature and its first call site (lines 162–182): + +```typescript +async function loadInvoicingRows( + pb: ReturnType, + repId: string | null, +): Promise { + const repFilter = repId + ? ` && order.customer.rep = "${escapeFilterValue(repId)}"` + : ""; + const seedRows = await listAllInvoicingRows(pb, INVOICING_STATUS_FILTER + repFilter); + const readyOrderIds = Array.from( + new Set( + seedRows + .filter((row) => row.item_status === "ready_invoice") + .map((row) => row.expand?.order?.id?.trim() ?? "") + .filter(Boolean), + ), + ); + + if (readyOrderIds.length === 0) { + return seedRows; + } + + // No need to re-apply repFilter here — readyOrderIds is already + // derived from the rep-scoped seedRows above, so the sibling fetch + // (by explicit order id) is implicitly scoped too. + const siblingFilter = readyOrderIds + .map((orderId) => `order = "${escapeFilterValue(orderId)}"`) + .join(" || "); + const siblingRows = await listAllInvoicingRows(pb, siblingFilter); + return dedupeRawRows([...seedRows, ...siblingRows]); +} +``` + +- [ ] **Step 3: Compute `repId` in the page and pass it into `load()`** + +Inside `InvoicingPage`, near the top of the component (alongside the other `useState` calls, e.g. right after the `useBooleanPreference` call around line 1371), add: + +```typescript + const { user } = useAuth(); +``` + +Then update the `load` callback (lines 1407–1418): + +```typescript + const load = useCallback(async () => { + setRows(null); + try { + const pb = getPocketBase(); + const userType = (user as { user_type?: string[] } | null)?.user_type; + const repId = user && isRepUser(userType) ? user.id : null; + const items = await loadInvoicingRows(pb, repId); + setRows(items); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + toast.error(message); + setRows([]); + } + }, [user]); +``` + +The existing `useEffect(() => { ...; void load(); }, [load])` (around line 1483) already re-runs whenever `load`'s identity changes, so it will automatically refetch once `user` resolves from the auth provider (which starts as `null` while PB auth is loading). + +- [ ] **Step 4: Manual verification** + +1. Log in as a non-rep user. Confirm the Invoicing page (`/invoicing`) shows the same rows as before. +2. Log in as a rep user tied to at least one customer with rows in `ready_invoice` / `invoiced` / `delivery_note` status. Confirm only that rep's rows appear, in both the "To Invoice" and "Invoiced" tabs. +3. Confirm the QuickBooks-send and delivery-note-creation modals still work on a visible row. + +- [ ] **Step 5: Commit** + +```bash +git add src/app/\(app\)/invoicing/page.tsx +git commit -m "feat(invoicing): filter rows to the logged-in rep's own orders" +``` + +--- + +### Task 5: Delivery Notes page — PB filter for the rep's own notes + +**Files:** +- Modify: `src/lib/pb-helpers.ts` +- Modify: `src/app/(app)/delivery-notes/page.tsx` + +- [ ] **Step 1: Add an optional `repId` param to `getDeliveryNotes`** + +In `src/lib/pb-helpers.ts`, replace the existing function (lines 1068–1076): + +```typescript +export async function getDeliveryNotes( + page = 1, + perPage = 50, + search = "", + repId: string | null = null, +) { + const pb = getPocketBase(); + const clauses: string[] = []; + if (search) { + clauses.push(`(delivery_note_number ~ "${search}" || notes ~ "${search}")`); + } + if (repId) { + clauses.push(`customer.rep = "${repId.replace(/"/g, '\\"')}"`); + } + const filter = clauses.join(" && "); + return pb.collection("delivery_notes").getList(page, perPage, { + filter, + sort: "-created", + expand: "order,company,customer", + }); +} +``` + +(This is backward-compatible — the new param is optional and defaults to `null`, so the one other behavior — no filter at all when both `search` and `repId` are empty — is unchanged.) + +- [ ] **Step 2: Pass the current rep's id from the Delivery Notes page** + +In `src/app/(app)/delivery-notes/page.tsx`, add to the imports (after the `getDeliveryNotes` import at line 27): + +```typescript +import { useAuth } from "@/components/auth-provider"; +import { isRepUser } from "@/lib/rep-visibility"; +``` + +Inside `DeliveryNotesPage`, right after `const router = useRouter();` at line 112, add: + +```typescript + const { user } = useAuth(); +``` + +Update `fetchNotes` (lines 318–352) to compute and pass `repId`, and add `user` to its dependency array: + +```typescript + const fetchNotes = useCallback(async () => { + setLoading(true); + setError(""); + try { + const userType = (user as { user_type?: string[] } | null)?.user_type; + const repId = user && isRepUser(userType) ? user.id : null; + // Fetch a wide page so the client-side tab filter sees the + // full set — the previous `perPage=20` made the active + // tab silently drop records past rank 20 (the operator + // saw "no DN" when the row existed but was buried). 500 + // is well over the production DN count (≈30) and still + // cheap for PB. + const result = await getDeliveryNotes(page, 500, search, repId); + setNotes(result.items); + setTotalPages(result.totalPages); + // Batch-fetch the related quotes in one query so the Q + // icon can show "create" vs "open" without a per-row + // round trip. Failures here are non-fatal — the Q icon + // falls back to "create quote" if we don't know whether + // a quote exists. + if (result.items.length > 0) { + try { + const ids = result.items.map((n) => n.id); + const map = await getQuotesForDeliveryNotes(ids); + setQuoteByDnId(map); + } catch { + setQuoteByDnId(new Map()); + } + } else { + setQuoteByDnId(new Map()); + } + } catch (err: unknown) { + setError(err instanceof Error ? err.message : "Failed to load delivery notes"); + } finally { + setLoading(false); + } + }, [page, search, user]); +``` + +- [ ] **Step 3: Manual verification** + +1. Log in as a non-rep user. Confirm `/delivery-notes` shows the same notes as before. +2. Log in as a rep user tied to at least one customer with delivery notes. Confirm only that rep's notes appear, across all four status tabs (Active / Cancelled / Completed / All). +3. Confirm search, company grouping, and the Combine-DNs flow still work. + +- [ ] **Step 4: Commit** + +```bash +git add src/lib/pb-helpers.ts src/app/\(app\)/delivery-notes/page.tsx +git commit -m "feat(delivery-notes): filter list to the logged-in rep's own notes" +``` + +--- + +### Task 6: Full regression pass + +**Files:** none (verification only) + +- [ ] **Step 1: Run the full test suite** + +Run: `npx vitest run` +Expected: PASS — no regressions in existing suites (`orders-service.test.ts`, `quotes-service.test.ts`, `boss-dashboard-service.test.ts`, `pocketbase-admin.test.ts`, etc. are all untouched by this plan). + +- [ ] **Step 2: Manual cross-check with a boss/admin account** + +Log in as an admin/boss account (`user_type` does NOT include `REP`) and confirm all four pages — Orders, Quotes, Invoicing, Delivery Notes — show the full, unfiltered list exactly as they did before this plan. This is the most important check: the filter must be strictly opt-in. + +- [ ] **Step 3: Manual cross-check with two different rep accounts** + +If two rep test accounts are available (each tied to different customers), confirm rep A never sees rep B's orders/quotes/invoicing-rows/delivery-notes, and vice versa. + +- [ ] **Step 4: Commit (if any fixes were needed)** + +```bash +git add -A +git commit -m "test: regression pass for rep-scoped list filtering" +``` diff --git a/docs/superpowers/plans/2026-09-01-rep-scoped-list-filter-SUMMARY.md b/docs/superpowers/plans/2026-09-01-rep-scoped-list-filter-SUMMARY.md new file mode 100644 index 0000000..213cdda --- /dev/null +++ b/docs/superpowers/plans/2026-09-01-rep-scoped-list-filter-SUMMARY.md @@ -0,0 +1,58 @@ +# Rep-Scoped List Filter — Implementation Summary + +> Companion to [`2026-08-31-rep-scoped-list-filter.md`](./2026-08-31-rep-scoped-list-filter.md). Records what landed, how it was verified, and what still needs manual browser testing. + +## Result + +All 6 plan tasks complete. Pushed to `origin/main` on 2026-09-01. + +## Commits (newest first) + +| SHA | Message | +|-----|---------| +| `9cf8e39` | test(invoicing): mock useAuth to keep rep-filter a no-op in tests | +| `ed07617` | feat(delivery-notes): filter list to the logged-in rep's own notes | +| `29eb003` | feat(invoicing): filter rows to the logged-in rep's own orders | +| `8faa443` | feat(quotes): filter list to the logged-in rep's own quotes | +| `defa481` | feat(orders): filter list to the logged-in rep's own orders | +| `d23c811` | feat: add isRepUser helper for rep-scoped list filtering | + +## Files changed + +- **New:** `src/lib/rep-visibility.ts` — `isRepUser(userType)` + `REP_USER_TYPE` constant, mirrors the `isRep` pattern in `src/lib/server/tv-dashboard-service.ts` +- **New:** `src/lib/rep-visibility.test.ts` — 6 unit tests covering: REP present, case-insensitive, mixed roles, other roles, null/undefined/non-array, empty array +- **Modified:** `src/app/(app)/orders/page.tsx` — `useAuth` + `visibleOrders` `useMemo` filtering on `order.rep` / `order.expand?.customer?.rep`. Replaces `orders.length` / `orders.map` with `visibleOrders.length` / `visibleOrders.map` at the 4 render sites listed in the plan (kept the `orders` state, `fetchOrders`, and realtime subscription untouched) +- **Modified:** `src/app/(app)/quotes/page.tsx` — `useAuth` + `visibleQuotes` `useMemo` filtering on `quote.expand?.customer?.rep`. `DataTable.data` and the live search-count chip read from `visibleQuotes` +- **Modified:** `src/app/(app)/invoicing/page.tsx` — `useAuth` + new `repId` param on `loadInvoicingRows`. PB filter clause `&& order.customer.rep = ""` added to the seed fetch; sibling fetch is implicitly scoped because `readyOrderIds` derives from the already-scoped seed rows +- **Modified:** `src/lib/pb-helpers.ts` — `getDeliveryNotes` gained an optional `repId: string | null = null` param; when supplied, adds a `customer.rep = ""` clause to the existing filter chain +- **Modified:** `src/app/(app)/delivery-notes/page.tsx` — `useAuth` + computes `repId` in `fetchNotes`, passes to `getDeliveryNotes` +- **Modified:** `src/app/(app)/invoicing/page.test.tsx` — added `vi.mock("@/components/auth-provider", ...)` returning a stable non-REP user via `vi.hoisted` (otherwise the page's `load` re-fires every render and breaks the `toHaveBeenCalledTimes(2)` assertions) + +## Verification done + +- `npx vitest run src/lib/rep-visibility.test.ts` — 6/6 pass +- `npx vitest run` (full suite) — 2441 pass / 15 fail. The same 15 failures occur on `origin/main`'s tree (verified by `git checkout origin/main -- ` then re-running). None are new regressions. +- `npx tsc --noEmit` — zero errors in any touched file +- Plan Task 2-5 "manual verification" steps (login as non-rep, login as rep, login as two reps) are **browser tests** and were **not** run from this terminal. See "Still to do" below. + +## Deviations from the plan + +1. **Orders page `visibleOrders` placement.** Plan said to add the `useMemo` after `otherTabCounts` (line 298), but the plan also changed line 282's `const totalLoaded = orders.length` to `const totalLoaded = visibleOrders.length` — which would forward-reference `visibleOrders`. Moved the `useMemo` to before `totalLoaded` instead. Functionally identical, no behaviour change. +2. **Invoicing test mock.** The plan did not anticipate that adding `useAuth` to a page would break a test that doesn't wrap with `AuthProvider`. Added the `vi.mock` in `page.test.tsx` (test fix, separate commit `9cf8e39`). The mock returns a non-REP user so the filter is a no-op for existing test data (which doesn't carry a `customer.rep` field). + +## Still to do (manual browser verification) + +- [ ] Log in as a non-rep user (`user_type` does NOT include `REP`) → confirm `/orders`, `/quotes`, `/invoicing`, `/delivery-notes` show the same rows as before this change +- [ ] Log in as a rep user tied to ≥1 customer → confirm `/orders`, `/quotes`, `/invoicing`, `/delivery-notes` only show that rep's rows +- [ ] If two rep test accounts are available, confirm rep A never sees rep B's rows (and vice versa) across all four pages +- [ ] Tab-switching, search, and "New Order" flow on Orders — these operate on the `orders` state, not the filtered `visibleOrders`, so they should still work; confirm in browser + +## Known limitations (from the plan, accepted for this pass) + +- On **Orders** and **Quotes**, tab-count badges and pagination totals (`totalPages`, `counts`) are derived from the unfiltered dataset — a rep filtering may see a page with fewer than `perPage` visible rows, or a "Page 1 of N" that doesn't match the filtered count. **Invoicing** and **Delivery Notes** don't have this problem because their filter is a real PB query, so their counts are accurate. +- Proper fix is threading `rep` through `orders-service.ts` / `quotes-service.ts`'s PB filters — separate, larger follow-up. + +## Pre-existing items not touched + +- `src/app/(auth)/login/page.tsx` — was already modified before this work started; left as-is. +- `docs/superpowers/plans/2026-08-31-rep-scoped-list-filter.md` — the plan file itself; remains untracked.