256 lines
11 KiB
Markdown
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>
|