Files
SimpleFinanceDash/.planning/phases/06-preset-data-first-run-detection-and-db-safety/06-03-PLAN.md

256 lines
11 KiB
Markdown

---
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"
---
<objective>
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.
</objective>
<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.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
<interfaces>
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
}
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Create src/hooks/useFirstRunState.ts</name>
<files>src/hooks/useFirstRunState.ts</files>
<read_first>
- 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)
</read_first>
<action>
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.
</action>
<verify>
<automated>grep -n "isFirstRun" src/hooks/useFirstRunState.ts && grep -n "loading" src/hooks/useFirstRunState.ts && npx tsc --noEmit</automated>
</verify>
<done>File exists. Exports `useFirstRunState` function. Returns `{ isFirstRun: boolean, loading: boolean }`. `tsc --noEmit` passes with no errors.</done>
</task>
<task type="auto" id="db-push-blocking">
<name>Task 2: [BLOCKING] Push database schema (migrations 006 + 007)</name>
<files>supabase/migrations/006_uniqueness_constraints.sql, supabase/migrations/007_setup_completed.sql</files>
<read_first>
- supabase/migrations/006_uniqueness_constraints.sql
- supabase/migrations/007_setup_completed.sql
</read_first>
<action>
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.
</action>
<verify>
<automated>supabase db push --dry-run 2>&1 | grep -E "No migrations|already applied|006|007" || echo "Check supabase CLI output above"</automated>
</verify>
<done>Both migrations applied. `supabase db push` exits 0 (or reports migrations already applied). No error output containing "ERROR" or "FATAL".</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<name>Task 3: Verify DB constraints and setup_completed column are live</name>
<what-built>
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.
</what-built>
<how-to-verify>
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.
</how-to-verify>
<resume-signal>Type "approved" if all constraints are live and tsc passes, or describe any issues found.</resume-signal>
</task>
</tasks>
<threat_model>
## 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) |
</threat_model>
<verification>
```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)
```
</verification>
<success_criteria>
- `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
</success_criteria>
<output>
After completion, create `.planning/phases/06-preset-data-first-run-detection-and-db-safety/06-03-SUMMARY.md`
</output>