--- phase: 06-preset-data-first-run-detection-and-db-safety plan: 03 type: execute wave: 2 depends_on: - "06-01" - "06-02" files_modified: - src/hooks/useFirstRunState.ts autonomous: false requirements: - AUTO-01 - AUTO-03 - SETUP-01 - SETUP-02 must_haves: truths: - "useFirstRunState hook returns isFirstRun=true when categories array is empty" - "useFirstRunState hook returns isFirstRun=true when template items array is empty" - "useFirstRunState hook returns isFirstRun=false when user has both categories and template items" - "useFirstRunState hook returns loading=true while either underlying query is in flight" - "DB schema push completes without errors — uniqueness constraints and setup_completed column live in Supabase" artifacts: - path: "src/hooks/useFirstRunState.ts" provides: "Derived first-run state from cached useCategories + useTemplate queries" exports: ["useFirstRunState"] key_links: - from: "src/hooks/useFirstRunState.ts" to: "src/hooks/useCategories.ts" via: "useCategories() call — reads from cache key ['categories']" pattern: "useCategories" - from: "src/hooks/useFirstRunState.ts" to: "src/hooks/useTemplate.ts" via: "useTemplate() call — reads from cache keys ['template', 'template-items']" pattern: "useTemplate" --- Write the `useFirstRunState` hook and push all migrations to the database. `useFirstRunState` derives first-run state from existing React Query caches — no extra network call. It returns `{ isFirstRun: boolean, loading: boolean }` where `isFirstRun` is `true` when either categories or template items count is zero, and `loading` is `true` while either underlying query is still fetching. The DB schema push applies migrations 006 and 007, making uniqueness constraints and the `setup_completed` column live. This is a `[BLOCKING]` step — all verification depends on the schema being applied. Purpose: Phase 7 wizard reads `useFirstRunState().isFirstRun` to decide whether to redirect new users to setup. The DB constraints prevent the data corruption that would require rollback work in Phase 8. Output: `src/hooks/useFirstRunState.ts` + DB schema pushed + human verification checkpoint. @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @.planning/phases/06-preset-data-first-run-detection-and-db-safety/06-CONTEXT.md @.planning/phases/06-preset-data-first-run-detection-and-db-safety/06-RESEARCH.md @.planning/phases/06-preset-data-first-run-detection-and-db-safety/06-01-SUMMARY.md @.planning/phases/06-preset-data-first-run-detection-and-db-safety/06-02-SUMMARY.md From src/hooks/useCategories.ts: ```typescript export function useCategories() { // queryKey: ["categories"] return { categories: query.data ?? [], // Category[] loading: query.isLoading, create, update, remove, } } ``` From src/hooks/useTemplate.ts: ```typescript export function useTemplate() { // queryKeys: ["template"], ["template-items"] return { template: templateQuery.data ?? null, items: itemsQuery.data ?? [], // TemplateItem[] loading: templateQuery.isLoading || itemsQuery.isLoading, updateName, createItem, updateItem, deleteItem, reorderItems, } } ``` From src/lib/types.ts (after Plan 01): ```typescript export interface Profile { id: string display_name: string | null locale: string currency: string setup_completed: boolean // added by Plan 01 created_at: string updated_at: string } ``` Task 1: Create src/hooks/useFirstRunState.ts src/hooks/useFirstRunState.ts - src/hooks/useCategories.ts - src/hooks/useTemplate.ts - .planning/phases/06-preset-data-first-run-detection-and-db-safety/06-RESEARCH.md (Pattern 3: Derived State Hook, Pitfall 2) Create `src/hooks/useFirstRunState.ts` with the following exact implementation. Do not add mutations, do not import supabase directly — this hook is read-only and derives state from already-cached queries. ```typescript import { useCategories } from "@/hooks/useCategories" import { useTemplate } from "@/hooks/useTemplate" /** * Derives first-run state from cached category and template queries. * No additional network calls are made — data is read from React Query cache. * * isFirstRun is true when: * - categories.length === 0 (user has not created any categories), OR * - items.length === 0 (user has no template items) * * IMPORTANT: Always check `loading` before acting on `isFirstRun`. * While queries are in flight, both arrays default to [] which would * cause isFirstRun to be true spuriously. * * Usage: * const { isFirstRun, loading } = useFirstRunState() * if (!loading && isFirstRun) { redirect to /setup } */ export function useFirstRunState() { const { categories, loading: catLoading } = useCategories() const { items, loading: tmplLoading } = useTemplate() return { isFirstRun: categories.length === 0 || items.length === 0, loading: catLoading || tmplLoading, } } ``` The `loading` guard is critical — see Pitfall 2 in RESEARCH.md. Callers in Phase 7 MUST check `!loading && isFirstRun` before redirecting. grep -n "isFirstRun" src/hooks/useFirstRunState.ts && grep -n "loading" src/hooks/useFirstRunState.ts && npx tsc --noEmit File exists. Exports `useFirstRunState` function. Returns `{ isFirstRun: boolean, loading: boolean }`. `tsc --noEmit` passes with no errors. Task 2: [BLOCKING] Push database schema (migrations 006 + 007) supabase/migrations/006_uniqueness_constraints.sql, supabase/migrations/007_setup_completed.sql - supabase/migrations/006_uniqueness_constraints.sql - supabase/migrations/007_setup_completed.sql Run the Supabase schema push to apply migrations 006 and 007 to the database. This command requires the Supabase CLI and an active project link. ```bash supabase db push ``` If the push prompts interactively and cannot be suppressed, set the access token first: ```bash SUPABASE_ACCESS_TOKEN=$(cat ~/.supabase/access-token 2>/dev/null || echo "SET_TOKEN_HERE") supabase db push ``` Expected outcome: Both migrations apply in order (006 then 007). The command exits 0. If the command fails with a duplicate constraint error (constraint already exists), the migrations may have been partially applied. In that case, check `supabase db diff` to see what's pending and resolve manually. If `supabase db push` requires interactive confirmation that cannot be bypassed, flag this task with a `checkpoint:human-action` and have the user run it manually from their terminal. supabase db push --dry-run 2>&1 | grep -E "No migrations|already applied|006|007" || echo "Check supabase CLI output above" Both migrations applied. `supabase db push` exits 0 (or reports migrations already applied). No error output containing "ERROR" or "FATAL". Task 3: Verify DB constraints and setup_completed column are live Migration 006 added UNIQUE constraints on budgets(user_id, start_date) and categories(user_id, name). Migration 007 added setup_completed boolean column to profiles and backfilled existing users. useFirstRunState hook created at src/hooks/useFirstRunState.ts. 1. Open the Supabase dashboard for this project. 2. Go to Table Editor > profiles — confirm the `setup_completed` column exists with boolean type. 3. Go to Database > Tables > budgets — confirm constraint `budgets_user_month_unique` exists on (user_id, start_date). 4. Go to Database > Tables > categories — confirm constraint `categories_user_name_unique` exists on (user_id, name). 5. If you have an existing v1.0 user row in profiles, confirm setup_completed = true for that row. 6. Run `npx tsc --noEmit` in the terminal — confirm zero TypeScript errors. Type "approved" if all constraints are live and tsc passes, or describe any issues found. ## Trust Boundaries | Boundary | Description | |----------|-------------| | useFirstRunState → React Query cache | Reads cached data from existing hooks — no new trust boundary introduced | | supabase db push → Supabase project | CLI command with project-level credentials — run only in trusted environment | ## STRIDE Threat Register | Threat ID | Category | Component | Disposition | Mitigation Plan | |-----------|----------|-----------|-------------|-----------------| | T-06-06 | Spoofing | useFirstRunState — isFirstRun=true while loading | mitigate | loading flag exported; callers must check `!loading && isFirstRun` before acting (documented in hook JSDoc) | | T-06-07 | Denial of Service | supabase db push failing mid-migration | accept | Transaction wrapping in 006 ensures atomicity; 007 is idempotent (ADD COLUMN IF NOT EXISTS can be added if needed) | ```bash # Hook exports correctly grep "export function useFirstRunState" src/hooks/useFirstRunState.ts # Hook uses both existing hooks grep "useCategories\|useTemplate" src/hooks/useFirstRunState.ts # TypeScript clean across all new files npx tsc --noEmit # Migration files in place ls supabase/migrations/006_uniqueness_constraints.sql supabase/migrations/007_setup_completed.sql # All 4 requirement IDs traceable across plans: # AUTO-01: useFirstRunState (plan 03) + budgets constraint (plan 01) # AUTO-03: setup_completed not hardcoding currency (plan 01, 02, 03 — amounts plain numbers) # SETUP-01: setup_completed column + backfill (plan 01) + useFirstRunState (plan 03) # SETUP-02: PRESETS array 19 items (plan 02) ``` - `src/hooks/useFirstRunState.ts` exists and exports `useFirstRunState()` returning `{ isFirstRun: boolean, loading: boolean }` - `isFirstRun` is derived from `categories.length === 0 || items.length === 0` — no direct Supabase calls - DB push completes: `budgets_user_month_unique` and `categories_user_name_unique` constraints live in Supabase - `profiles` table has `setup_completed boolean NOT NULL DEFAULT false` column - All existing users with categories (or template items) have `setup_completed = true` - `tsc --noEmit` passes with zero errors across all modified files After completion, create `.planning/phases/06-preset-data-first-run-detection-and-db-safety/06-03-SUMMARY.md`