docs(06): create phase 6 plan — preset data, first-run detection, and DB safety
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -45,7 +45,11 @@ Plans:
|
||||
3. All existing v1.0 users have `profiles.setup_completed = true` after the backfill migration runs — they will not be shown the wizard on their next login
|
||||
4. `useFirstRunState` hook returns `true` only for users with zero categories or zero template items — returning `false` for any user with existing data
|
||||
5. `src/data/presets.ts` contains ~15-20 curated budget items with sensible default amounts, grouped by category type, with both English and German i18n translation keys defined
|
||||
**Plans**: TBD
|
||||
**Plans:** 3 plans
|
||||
Plans:
|
||||
- [ ] 06-01-PLAN.md — DB migrations (uniqueness constraints + setup_completed column + Profile type update)
|
||||
- [ ] 06-02-PLAN.md — Preset library (src/data/presets.ts) and i18n translations (en.json + de.json)
|
||||
- [ ] 06-03-PLAN.md — useFirstRunState hook, DB schema push, and verification checkpoint
|
||||
|
||||
### Phase 7: Setup Wizard
|
||||
**Goal**: A new user can set up their budget template in under 3 minutes by following a guided 3-step wizard with pre-filled common items and a live running balance
|
||||
@@ -57,7 +61,11 @@ Plans:
|
||||
3. A "remaining to allocate" balance updates live as items are checked or unchecked in step 2, showing income minus the sum of selected item amounts
|
||||
4. User can click "Skip" on any individual step or "Skip setup" to exit the wizard entirely without creating any data — and lands on the dashboard
|
||||
5. Completing the wizard creates a populated template from selected items — user lands on the dashboard with their template in place, and refreshing the browser mid-wizard restores the wizard at the correct step
|
||||
**Plans**: TBD
|
||||
**Plans:** 3 plans
|
||||
Plans:
|
||||
- [ ] 06-01-PLAN.md — DB migrations (uniqueness constraints + setup_completed column + Profile type update)
|
||||
- [ ] 06-02-PLAN.md — Preset library (src/data/presets.ts) and i18n translations (en.json + de.json)
|
||||
- [ ] 06-03-PLAN.md — useFirstRunState hook, DB schema push, and verification checkpoint
|
||||
|
||||
### Phase 8: Auto-Budget Creation
|
||||
**Goal**: Users never manually trigger budget creation — visiting a month for the first time automatically creates their budget from the template, silently and correctly
|
||||
@@ -68,7 +76,11 @@ Plans:
|
||||
2. The first time a budget is auto-created in a session, a toast notification appears naming the month and indicating it was created from the template — subsequent months are created silently
|
||||
3. The auto-created budget uses the user's configured currency from their profile settings, not a hardcoded default
|
||||
4. Rapidly navigating between months or opening the dashboard in two tabs does not produce duplicate budget rows
|
||||
**Plans**: TBD
|
||||
**Plans:** 3 plans
|
||||
Plans:
|
||||
- [ ] 06-01-PLAN.md — DB migrations (uniqueness constraints + setup_completed column + Profile type update)
|
||||
- [ ] 06-02-PLAN.md — Preset library (src/data/presets.ts) and i18n translations (en.json + de.json)
|
||||
- [ ] 06-03-PLAN.md — useFirstRunState hook, DB schema push, and verification checkpoint
|
||||
|
||||
### Phase 9: Inline Add and Dashboard Simplification
|
||||
**Goal**: Users add one-off items to their budget directly from the budget view, the Quick Add page is gone, and the dashboard shows this month's data at a glance without chart overload
|
||||
@@ -80,7 +92,11 @@ Plans:
|
||||
3. The dashboard displays this month's summary cards (income, expenses, balance) with correct totals that match what the user entered in the budget detail page
|
||||
4. The dashboard does not show the 3-column chart grid by default — the primary view is the at-a-glance summary and collapsible category sections
|
||||
5. Empty template, empty dashboard, and empty budget view each show a clear empty state with a visible call-to-action guiding the user to the next step
|
||||
**Plans**: TBD
|
||||
**Plans:** 3 plans
|
||||
Plans:
|
||||
- [ ] 06-01-PLAN.md — DB migrations (uniqueness constraints + setup_completed column + Profile type update)
|
||||
- [ ] 06-02-PLAN.md — Preset library (src/data/presets.ts) and i18n translations (en.json + de.json)
|
||||
- [ ] 06-03-PLAN.md — useFirstRunState hook, DB schema push, and verification checkpoint
|
||||
|
||||
## Requirements Traceability
|
||||
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
---
|
||||
phase: 06-preset-data-first-run-detection-and-db-safety
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- supabase/migrations/006_uniqueness_constraints.sql
|
||||
- supabase/migrations/007_setup_completed.sql
|
||||
- src/lib/types.ts
|
||||
autonomous: true
|
||||
requirements:
|
||||
- AUTO-01
|
||||
- AUTO-03
|
||||
- SETUP-01
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "Migration 006 adds a unique constraint on budgets(user_id, start_date) with safe deduplication"
|
||||
- "Migration 006 adds a unique constraint on categories(user_id, name) with safe deduplication"
|
||||
- "Migration 007 adds setup_completed boolean NOT NULL DEFAULT false to profiles"
|
||||
- "Migration 007 backfills setup_completed = true for all users with existing categories"
|
||||
- "Profile TypeScript interface includes setup_completed: boolean"
|
||||
artifacts:
|
||||
- path: "supabase/migrations/006_uniqueness_constraints.sql"
|
||||
provides: "Atomic deduplication + unique constraint DDL for budgets and categories"
|
||||
- path: "supabase/migrations/007_setup_completed.sql"
|
||||
provides: "ALTER TABLE profiles ADD COLUMN setup_completed + backfill UPDATE"
|
||||
- path: "src/lib/types.ts"
|
||||
provides: "Updated Profile interface with setup_completed field"
|
||||
contains: "setup_completed: boolean"
|
||||
key_links:
|
||||
- from: "supabase/migrations/007_setup_completed.sql"
|
||||
to: "profiles table"
|
||||
via: "ALTER TABLE profiles ADD COLUMN"
|
||||
pattern: "setup_completed boolean NOT NULL DEFAULT false"
|
||||
- from: "src/lib/types.ts"
|
||||
to: "Profile interface"
|
||||
via: "TypeScript field"
|
||||
pattern: "setup_completed: boolean"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Write two Supabase migration files and update the Profile TypeScript type.
|
||||
|
||||
Migration 006 makes duplicate budget/category creation impossible at the DB level by first deduplicating any existing rows then adding UNIQUE constraints. Migration 007 adds the `setup_completed` boolean column to `profiles` and backfills existing users who have categories to `true`. The TypeScript `Profile` interface in `src/lib/types.ts` is updated to include `setup_completed: boolean`.
|
||||
|
||||
Purpose: The DB layer must enforce uniqueness atomically (prevents race conditions) and must know which users have already completed setup (prevents existing v1.0 users from seeing the wizard on first v2.0 login).
|
||||
Output: 2 `.sql` migration files + 1 updated TypeScript type file.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.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
|
||||
|
||||
<interfaces>
|
||||
<!-- Existing migration chain: 001-005. New files must be named 006_... and 007_... for correct lexicographic ordering. -->
|
||||
|
||||
From supabase/migrations/001_profiles.sql — profiles table columns:
|
||||
id, display_name, locale, currency, created_at, updated_at
|
||||
(NO setup_completed yet)
|
||||
|
||||
From supabase/migrations/002_categories.sql — categories table:
|
||||
id, user_id, name, type, icon, sort_order, created_at, updated_at
|
||||
Existing index: categories_user_id_idx
|
||||
(NO unique constraint on (user_id, name) yet)
|
||||
|
||||
From supabase/migrations/004_budgets.sql — budgets table:
|
||||
id, user_id, start_date, end_date, currency, carryover_amount, created_at, updated_at
|
||||
Existing index: budgets_user_id_idx
|
||||
(NO unique constraint on (user_id, start_date) yet)
|
||||
|
||||
From src/lib/types.ts — Profile interface (before this plan):
|
||||
export interface Profile {
|
||||
id: string
|
||||
display_name: string | null
|
||||
locale: string
|
||||
currency: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Write migration 006 — uniqueness constraints with safe deduplication</name>
|
||||
<files>supabase/migrations/006_uniqueness_constraints.sql</files>
|
||||
<read_first>
|
||||
- supabase/migrations/002_categories.sql
|
||||
- supabase/migrations/004_budgets.sql
|
||||
- .planning/phases/06-preset-data-first-run-detection-and-db-safety/06-RESEARCH.md (Pattern 1: Safe Deduplication)
|
||||
</read_first>
|
||||
<action>
|
||||
Create `supabase/migrations/006_uniqueness_constraints.sql` with this exact content — a single transaction that deduplicates then constrains:
|
||||
|
||||
```sql
|
||||
-- Migration 006: Add uniqueness constraints to budgets and categories
|
||||
-- Safe deduplication runs first inside the transaction before each constraint.
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- Deduplicate budgets: keep the oldest row per (user_id, start_date)
|
||||
DELETE FROM budgets
|
||||
WHERE id NOT IN (
|
||||
SELECT DISTINCT ON (user_id, start_date) id
|
||||
FROM budgets
|
||||
ORDER BY user_id, start_date, created_at ASC
|
||||
);
|
||||
|
||||
ALTER TABLE budgets
|
||||
ADD CONSTRAINT budgets_user_month_unique UNIQUE (user_id, start_date);
|
||||
|
||||
-- Deduplicate categories: keep the oldest row per (user_id, name)
|
||||
DELETE FROM categories
|
||||
WHERE id NOT IN (
|
||||
SELECT DISTINCT ON (user_id, name) id
|
||||
FROM categories
|
||||
ORDER BY user_id, name, created_at ASC
|
||||
);
|
||||
|
||||
ALTER TABLE categories
|
||||
ADD CONSTRAINT categories_user_name_unique UNIQUE (user_id, name);
|
||||
|
||||
COMMIT;
|
||||
```
|
||||
|
||||
Key: DISTINCT ON requires ORDER BY on the same leading columns — already satisfied above. Wrapping both operations in a single transaction means if constraint ADD fails, cleanup rolls back too.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>grep -c "ADD CONSTRAINT" supabase/migrations/006_uniqueness_constraints.sql && grep -c "BEGIN" supabase/migrations/006_uniqueness_constraints.sql && grep -c "COMMIT" supabase/migrations/006_uniqueness_constraints.sql</automated>
|
||||
</verify>
|
||||
<done>File exists. Contains exactly 2 ADD CONSTRAINT statements, 1 BEGIN, 1 COMMIT. Both constraints named: budgets_user_month_unique and categories_user_name_unique.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Write migration 007 — setup_completed column + backfill</name>
|
||||
<files>supabase/migrations/007_setup_completed.sql</files>
|
||||
<read_first>
|
||||
- supabase/migrations/001_profiles.sql
|
||||
- .planning/phases/06-preset-data-first-run-detection-and-db-safety/06-RESEARCH.md (Pattern 2: ADD COLUMN with Default + Backfill, Pitfall 3)
|
||||
</read_first>
|
||||
<action>
|
||||
Create `supabase/migrations/007_setup_completed.sql` with this exact content:
|
||||
|
||||
```sql
|
||||
-- Migration 007: Add setup_completed to profiles
|
||||
-- New signups default to false (not set up).
|
||||
-- Existing users who have any categories are backfilled to true (already set up).
|
||||
-- Wider backfill also includes users with template items to protect against
|
||||
-- edge case where user created template items but skipped category creation.
|
||||
|
||||
ALTER TABLE profiles
|
||||
ADD COLUMN setup_completed boolean NOT NULL DEFAULT false;
|
||||
|
||||
-- Backfill: users with categories OR template items are considered set up
|
||||
UPDATE profiles
|
||||
SET setup_completed = true
|
||||
WHERE id IN (
|
||||
SELECT DISTINCT user_id FROM categories
|
||||
UNION
|
||||
SELECT t.user_id FROM templates t
|
||||
INNER JOIN template_items ti ON ti.template_id = t.id
|
||||
);
|
||||
```
|
||||
|
||||
Note: The wider backfill UNION (categories OR template items) matches Pitfall 3 guidance from RESEARCH.md — protects v1.0 users who may have template items but no categories.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>grep -c "ADD COLUMN setup_completed" supabase/migrations/007_setup_completed.sql && grep -c "UPDATE profiles" supabase/migrations/007_setup_completed.sql</automated>
|
||||
</verify>
|
||||
<done>File exists. Contains ADD COLUMN statement with `boolean NOT NULL DEFAULT false` and UPDATE statement with UNION backfill covering both categories and template_items.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Update Profile TypeScript interface to include setup_completed</name>
|
||||
<files>src/lib/types.ts</files>
|
||||
<read_first>
|
||||
- src/lib/types.ts
|
||||
</read_first>
|
||||
<action>
|
||||
In `src/lib/types.ts`, add `setup_completed: boolean` to the `Profile` interface. The field goes after `currency` and before `created_at`:
|
||||
|
||||
```typescript
|
||||
export interface Profile {
|
||||
id: string
|
||||
display_name: string | null
|
||||
locale: string
|
||||
currency: string
|
||||
setup_completed: boolean // <-- ADD THIS LINE
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
```
|
||||
|
||||
No other changes to types.ts.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>grep -n "setup_completed: boolean" src/lib/types.ts</automated>
|
||||
</verify>
|
||||
<done>`src/lib/types.ts` contains `setup_completed: boolean` inside the Profile interface. `tsc --noEmit` passes with no new errors.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| Migration SQL → Supabase DB | DDL runs with superuser privileges via Supabase CLI — only run locally or in controlled CI |
|
||||
| Client → profiles RLS | Client can UPDATE own profile row including setup_completed — acceptable since it's a UX flag, not a security gate |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-06-01 | Tampering | budgets table — duplicate row creation via concurrent requests | mitigate | UNIQUE constraint on (user_id, start_date) enforced at DB level; second INSERT returns PostgREST error 23505 |
|
||||
| T-06-02 | Tampering | categories table — duplicate name creation | mitigate | UNIQUE constraint on (user_id, name) enforced at DB level; second INSERT returns PostgREST error 23505 |
|
||||
| T-06-03 | Elevation of privilege | profiles.setup_completed — client sets own flag to false to force wizard re-display | accept | setup_completed is a UX routing flag only; no data is gated behind it; RLS allows own-row update |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
After all 3 tasks complete:
|
||||
|
||||
```bash
|
||||
# Confirm both migration files exist
|
||||
ls supabase/migrations/006_uniqueness_constraints.sql supabase/migrations/007_setup_completed.sql
|
||||
|
||||
# Confirm TypeScript type updated
|
||||
grep "setup_completed: boolean" src/lib/types.ts
|
||||
|
||||
# Confirm TypeScript compiles cleanly
|
||||
npx tsc --noEmit
|
||||
```
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- `supabase/migrations/006_uniqueness_constraints.sql` exists, contains BEGIN/COMMIT, two DISTINCT ON dedup DELETEs, two ADD CONSTRAINT statements
|
||||
- `supabase/migrations/007_setup_completed.sql` exists, adds column with `boolean NOT NULL DEFAULT false`, backfills with UNION covering categories and template_items
|
||||
- `src/lib/types.ts` Profile interface has `setup_completed: boolean`
|
||||
- `tsc --noEmit` passes with no errors
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/06-preset-data-first-run-detection-and-db-safety/06-01-SUMMARY.md`
|
||||
</output>
|
||||
@@ -0,0 +1,283 @@
|
||||
---
|
||||
phase: 06-preset-data-first-run-detection-and-db-safety
|
||||
plan: 02
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- src/data/presets.ts
|
||||
- src/i18n/en.json
|
||||
- src/i18n/de.json
|
||||
autonomous: true
|
||||
requirements:
|
||||
- SETUP-02
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "src/data/presets.ts exports a PRESETS array of exactly 19 typed items"
|
||||
- "PRESETS covers all 6 category types with the agreed distribution (4+4+5+2+2+2)"
|
||||
- "Every preset item has a slug, type (valid CategoryType), defaultAmount (round number EUR), and item_tier (fixed or variable)"
|
||||
- "en.json has a top-level presets key with all 19 slugs translated to English"
|
||||
- "de.json has a top-level presets key with all 19 slugs translated to German"
|
||||
artifacts:
|
||||
- path: "src/data/presets.ts"
|
||||
provides: "PresetItem interface and PRESETS array"
|
||||
exports: ["PresetItem", "PRESETS"]
|
||||
- path: "src/i18n/en.json"
|
||||
provides: "English preset translations under presets.* key"
|
||||
contains: "presets"
|
||||
- path: "src/i18n/de.json"
|
||||
provides: "German preset translations under presets.* key"
|
||||
contains: "presets"
|
||||
key_links:
|
||||
- from: "src/data/presets.ts"
|
||||
to: "src/lib/types.ts"
|
||||
via: "import CategoryType"
|
||||
pattern: "import.*CategoryType.*from.*types"
|
||||
- from: "src/i18n/en.json"
|
||||
to: "presets.{type}.{slug}"
|
||||
via: "react-i18next t() dot-path"
|
||||
pattern: "\"presets\":"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Create the static preset budget item library and its i18n translations.
|
||||
|
||||
`src/data/presets.ts` exports `PresetItem` interface and `PRESETS` array with 19 items across 6 category types (4 income, 4 bill, 5 variable_expense, 2 debt, 2 saving, 2 investment). Both `src/i18n/en.json` and `src/i18n/de.json` get a new top-level `"presets"` key containing all 19 English/German display names.
|
||||
|
||||
Purpose: This is the curated item library the Phase 7 wizard shows to new users for one-click budget template setup. All amounts are plain EUR numbers — the wizard reads currency from `profiles.currency`, not from this file.
|
||||
Output: `src/data/presets.ts`, updated `en.json`, updated `de.json`.
|
||||
</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
|
||||
|
||||
<interfaces>
|
||||
From src/lib/types.ts:
|
||||
```typescript
|
||||
export type CategoryType =
|
||||
| "income"
|
||||
| "bill"
|
||||
| "variable_expense"
|
||||
| "debt"
|
||||
| "saving"
|
||||
| "investment"
|
||||
|
||||
export type ItemTier = "fixed" | "variable" | "one_off"
|
||||
```
|
||||
|
||||
From src/i18n/en.json — existing top-level structure (add "presets" alongside these):
|
||||
"app", "nav", "auth", "categories", "template", "budget", "settings", "common"
|
||||
i18n library: react-i18next — uses t('dot.path.key') syntax confirmed.
|
||||
|
||||
NOTE: Do NOT hardcode currency symbols in preset display names or amounts.
|
||||
Amounts are plain numbers (EUR value). The wizard (Phase 7) will format them.
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Create src/data/presets.ts — 19-item preset library</name>
|
||||
<files>src/data/presets.ts</files>
|
||||
<read_first>
|
||||
- src/lib/types.ts
|
||||
- .planning/phases/06-preset-data-first-run-detection-and-db-safety/06-RESEARCH.md (Pattern 4: Preset Data File Shape)
|
||||
</read_first>
|
||||
<action>
|
||||
Create `src/data/presets.ts`. The file must NOT import from Supabase or React — it is a pure static data module.
|
||||
|
||||
```typescript
|
||||
import type { CategoryType } from "@/lib/types"
|
||||
|
||||
export interface PresetItem {
|
||||
slug: string
|
||||
type: CategoryType
|
||||
defaultAmount: number // EUR, round number — do NOT suffix with currency symbol
|
||||
item_tier: "fixed" | "variable"
|
||||
}
|
||||
|
||||
export const PRESETS: PresetItem[] = [
|
||||
// income (4)
|
||||
{ slug: "salary", type: "income", defaultAmount: 3000, item_tier: "fixed" },
|
||||
{ slug: "freelance", type: "income", defaultAmount: 500, item_tier: "variable" },
|
||||
{ slug: "rental_income", type: "income", defaultAmount: 800, item_tier: "fixed" },
|
||||
{ slug: "other_income", type: "income", defaultAmount: 200, item_tier: "variable" },
|
||||
// bill (4)
|
||||
{ slug: "rent", type: "bill", defaultAmount: 1000, item_tier: "fixed" },
|
||||
{ slug: "electricity", type: "bill", defaultAmount: 80, item_tier: "fixed" },
|
||||
{ slug: "internet", type: "bill", defaultAmount: 40, item_tier: "fixed" },
|
||||
{ slug: "phone", type: "bill", defaultAmount: 30, item_tier: "fixed" },
|
||||
// variable_expense (5)
|
||||
{ slug: "groceries", type: "variable_expense", defaultAmount: 400, item_tier: "variable" },
|
||||
{ slug: "transport", type: "variable_expense", defaultAmount: 100, item_tier: "variable" },
|
||||
{ slug: "dining_out", type: "variable_expense", defaultAmount: 150, item_tier: "variable" },
|
||||
{ slug: "health", type: "variable_expense", defaultAmount: 50, item_tier: "variable" },
|
||||
{ slug: "clothing", type: "variable_expense", defaultAmount: 100, item_tier: "variable" },
|
||||
// debt (2)
|
||||
{ slug: "loan_repayment", type: "debt", defaultAmount: 200, item_tier: "fixed" },
|
||||
{ slug: "credit_card", type: "debt", defaultAmount: 100, item_tier: "fixed" },
|
||||
// saving (2)
|
||||
{ slug: "emergency_fund", type: "saving", defaultAmount: 200, item_tier: "fixed" },
|
||||
{ slug: "vacation", type: "saving", defaultAmount: 100, item_tier: "fixed" },
|
||||
// investment (2)
|
||||
{ slug: "etf", type: "investment", defaultAmount: 200, item_tier: "fixed" },
|
||||
{ slug: "pension", type: "investment", defaultAmount: 100, item_tier: "fixed" },
|
||||
]
|
||||
```
|
||||
|
||||
The `item_tier` for all items must be either `"fixed"` or `"variable"` — never `"one_off"` (which is excluded from template_items by the DB check constraint).
|
||||
</action>
|
||||
<verify>
|
||||
<automated>grep -c "slug:" src/data/presets.ts</automated>
|
||||
</verify>
|
||||
<done>File exists. Contains exactly 19 objects (grep -c "slug:" returns 19). All `type` values are valid CategoryType strings. No `item_tier: "one_off"` present. `tsc --noEmit` passes.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Add preset translations to en.json and de.json</name>
|
||||
<files>src/i18n/en.json, src/i18n/de.json</files>
|
||||
<read_first>
|
||||
- src/i18n/en.json
|
||||
- src/i18n/de.json
|
||||
</read_first>
|
||||
<action>
|
||||
Add a top-level `"presets"` key to both i18n files. The key structure is `presets.{category_type}.{slug}` — matching the `type` and `slug` fields in PRESETS.
|
||||
|
||||
For `src/i18n/en.json`, add after the last existing top-level key:
|
||||
```json
|
||||
"presets": {
|
||||
"income": {
|
||||
"salary": "Salary",
|
||||
"freelance": "Freelance Income",
|
||||
"rental_income": "Rental Income",
|
||||
"other_income": "Other Income"
|
||||
},
|
||||
"bill": {
|
||||
"rent": "Rent",
|
||||
"electricity": "Electricity",
|
||||
"internet": "Internet",
|
||||
"phone": "Phone"
|
||||
},
|
||||
"variable_expense": {
|
||||
"groceries": "Groceries",
|
||||
"transport": "Transport",
|
||||
"dining_out": "Dining Out",
|
||||
"health": "Health & Pharmacy",
|
||||
"clothing": "Clothing"
|
||||
},
|
||||
"debt": {
|
||||
"loan_repayment": "Loan Repayment",
|
||||
"credit_card": "Credit Card"
|
||||
},
|
||||
"saving": {
|
||||
"emergency_fund": "Emergency Fund",
|
||||
"vacation": "Vacation Fund"
|
||||
},
|
||||
"investment": {
|
||||
"etf": "ETF / Index Fund",
|
||||
"pension": "Pension"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
For `src/i18n/de.json`, add the same structure with German translations:
|
||||
```json
|
||||
"presets": {
|
||||
"income": {
|
||||
"salary": "Gehalt",
|
||||
"freelance": "Freelance-Einkommen",
|
||||
"rental_income": "Mieteinnahmen",
|
||||
"other_income": "Sonstiges Einkommen"
|
||||
},
|
||||
"bill": {
|
||||
"rent": "Miete",
|
||||
"electricity": "Strom",
|
||||
"internet": "Internet",
|
||||
"phone": "Telefon"
|
||||
},
|
||||
"variable_expense": {
|
||||
"groceries": "Lebensmittel",
|
||||
"transport": "Transport",
|
||||
"dining_out": "Auswärts essen",
|
||||
"health": "Gesundheit & Apotheke",
|
||||
"clothing": "Kleidung"
|
||||
},
|
||||
"debt": {
|
||||
"loan_repayment": "Kreditrückzahlung",
|
||||
"credit_card": "Kreditkarte"
|
||||
},
|
||||
"saving": {
|
||||
"emergency_fund": "Notfallfonds",
|
||||
"vacation": "Urlaubskasse"
|
||||
},
|
||||
"investment": {
|
||||
"etf": "ETF / Indexfonds",
|
||||
"pension": "Altersvorsorge"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Both files must remain valid JSON after the edit. Add the `"presets"` key as the last entry in each JSON object (before the closing `}`), preceded by a comma on the previous last key.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>node -e "JSON.parse(require('fs').readFileSync('src/i18n/en.json','utf8')); JSON.parse(require('fs').readFileSync('src/i18n/de.json','utf8')); console.log('JSON valid')" && grep -c '"slug_key"' src/i18n/en.json || grep -c '"salary"' src/i18n/en.json</automated>
|
||||
</verify>
|
||||
<done>Both JSON files are valid (node JSON.parse succeeds). `en.json` and `de.json` each contain a top-level `"presets"` key. `grep '"salary"' src/i18n/en.json` returns 1 match. `grep '"Gehalt"' src/i18n/de.json` returns 1 match.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| presets.ts → wizard UI | Static read-only data — no user input, no network calls |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-06-04 | Information Disclosure | presets.ts amounts | accept | Amounts are generic EUR defaults, not user-specific data; public knowledge |
|
||||
| T-06-05 | Tampering | i18n JSON malformed after edit | mitigate | Verify both JSON files parse cleanly after edit (`node -e "JSON.parse(...)"`) before committing |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
```bash
|
||||
# Confirm presets file exists with 19 items
|
||||
grep -c "slug:" src/data/presets.ts
|
||||
|
||||
# Confirm no one_off tier
|
||||
grep "one_off" src/data/presets.ts || echo "no one_off found (correct)"
|
||||
|
||||
# Confirm i18n JSON files are valid
|
||||
node -e "JSON.parse(require('fs').readFileSync('src/i18n/en.json','utf8')); console.log('en.json valid')"
|
||||
node -e "JSON.parse(require('fs').readFileSync('src/i18n/de.json','utf8')); console.log('de.json valid')"
|
||||
|
||||
# Confirm presets key present in both
|
||||
grep '"presets"' src/i18n/en.json src/i18n/de.json
|
||||
|
||||
# TypeScript clean
|
||||
npx tsc --noEmit
|
||||
```
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- `src/data/presets.ts` exports `PresetItem` interface and `PRESETS` array with exactly 19 items
|
||||
- Distribution: 4 income, 4 bill, 5 variable_expense, 2 debt, 2 saving, 2 investment
|
||||
- All `item_tier` values are `"fixed"` or `"variable"` only
|
||||
- `en.json` and `de.json` both contain a valid `"presets"` nested object with all 19 slugs translated
|
||||
- Both JSON files remain valid (parseable) after edits
|
||||
- `tsc --noEmit` passes
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/06-preset-data-first-run-detection-and-db-safety/06-02-SUMMARY.md`
|
||||
</output>
|
||||
@@ -0,0 +1,255 @@
|
||||
---
|
||||
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>
|
||||
Reference in New Issue
Block a user