# Architecture Research **Domain:** Personal budget dashboard — v2.0 UX simplification with wizard setup, auto-budget creation, inline add-from-library, design system rework **Researched:** 2026-04-02 **Confidence:** HIGH (based on full codebase inspection; all claims verified against source files) --- ## Existing Architecture Baseline This is a subsequent milestone. The section below describes what EXISTS today, then each sub-section documents exactly what changes and what stays the same. ### Current System Layout ``` ┌─────────────────────────────────────────────────────────────────────┐ │ React 19 SPA (Vite + TypeScript) │ │ │ │ Auth Layer App Layer (ProtectedRoute + AppLayout) │ │ ┌───────────────┐ ┌─────────────────────────────────────────┐ │ │ │ LoginPage │ │ SidebarProvider > Sidebar > SidebarInset│ │ │ │ RegisterPage │ │ 6 nav items → 6 protected page routes │ │ │ └───────────────┘ └─────────────────────────────────────────┘ │ │ │ │ Pages (each uses PageShell + direct hook calls): │ │ DashboardPage CategoriesPage TemplatePage BudgetListPage │ │ BudgetDetailPage QuickAddPage SettingsPage │ │ │ │ Hooks (TanStack Query v5, direct supabase-js client): │ │ useAuth useCategories useTemplate useBudgets useQuickAdd │ │ useMonthParam useBudgetDetail │ │ │ │ Design layer: │ │ index.css @theme inline → OKLCH tokens → Tailwind 4 utility classes│ │ lib/palette.ts → categoryColors (CSS var references only) │ │ shadcn/ui (radix-ui primitives + generated component files) │ └─────────────────────────┬───────────────────────────────────────────┘ │ @supabase/supabase-js v2 (REST + RLS) ┌─────────────────────────▼───────────────────────────────────────────┐ │ Supabase (PostgreSQL 16 + Auth + Row Level Security) │ │ Tables: profiles, categories, templates, template_items, │ │ budgets, budget_items, quick_add_items │ └─────────────────────────────────────────────────────────────────────┘ ``` ### Key Existing Patterns (Carry Forward Unchanged) | Pattern | Location | Keep? | |---------|----------|-------| | PageShell header wrapper | `components/shared/PageShell.tsx` | YES | | DashboardContent inner component keyed by budgetId | `DashboardPage.tsx` | YES — key prop pattern prevents stale state on month change | | useMonthParam URL search param | `hooks/useMonthParam.ts` | YES | | getOrCreateTemplate (auto-creates template row) | `hooks/useTemplate.ts` | YES | | generateFromTemplate mutation | `hooks/useBudgets.ts` | YES — v2.0 triggers it automatically | | Two-tier OKLCH color tokens (text ~0.55L, fill ~0.68L) | `index.css` | YES — values tuned, names unchanged | | categoryColors CSS var references | `lib/palette.ts` | YES — names unchanged | | Direction-aware diff (income under = bad, spending over = bad) | `BudgetDetailPage.tsx`, `CategorySection.tsx` | YES | | TanStack Query cache invalidation per resource key | All mutation hooks | YES | --- ## v2.0 Architecture Changes ### 1. First-Run Detection and Wizard Routing **Problem:** New users land on Dashboard with empty categories, no template, no budget. The blank state offers no guidance. **Solution:** A `useFirstRunState` hook detects first-run status. A `WizardRoute` guard in `App.tsx` redirects first-run users to `/setup` (a dedicated wizard page) before they can reach any other protected route. **New hook — `src/hooks/useFirstRunState.ts`:** ```typescript // Returns { needsSetup: boolean, loading: boolean } // needsSetup = true when: categories.length === 0 OR template_items.length === 0 // Uses existing useCategories() and useTemplate() — no new DB queries export function useFirstRunState(): { needsSetup: boolean; loading: boolean } ``` **Route change — `src/App.tsx`:** ``` // Add /setup route // Add WizardRoute guard that wraps the existing ProtectedRoute subtree: // if needsSetup && path !== "/setup" → redirect to /setup // if !needsSetup && path === "/setup" → redirect to / } /> ``` **Wizard state — local `useState` only in `SetupWizardPage`:** ```typescript type WizardState = { step: 1 | 2 | 3 selectedPresets: string[] // preset IDs chosen in step 1 amounts: Record // presetId → budgeted amount (step 2) } ``` Wizard state is ephemeral. It lives in the page component and is discarded on unmount. No context, no global store. **Completion flow:** `handleFinish()` in `SetupWizardPage` runs: 1. `useCategories().create` × N (create one category row per selected preset) 2. `useTemplate().createItem` × N (create template items, each linked to the new categories) 3. `navigate("/")` — DashboardPage loads, `useFirstRunState` returns `needsSetup=false` **Preset data — `src/data/presets.ts`:** Static array of common budget items (rent, salary, groceries, car insurance, etc.) with suggested amounts and category types. No DB table needed — this is compile-time data. **No new DB migration.** Wizard writes to existing `categories` and `template_items` tables. **New files:** - `src/hooks/useFirstRunState.ts` - `src/pages/SetupWizardPage.tsx` - `src/components/wizard/WizardStep.tsx` - `src/components/wizard/CategoryDefaults.tsx` - `src/components/wizard/TemplateDefaults.tsx` - `src/data/presets.ts` **Modified files:** - `src/App.tsx` — add `/setup` route + WizardRoute guard --- ### 2. Auto-Budget Creation on Month Visit **Problem:** Visiting the dashboard for a new month shows an empty state with two manual buttons ("Create Budget", "Generate from Template"). This friction contradicts the "just works" goal. **Solution:** Auto-trigger `generateFromTemplate` inside a `useEffect` in `DashboardPage` when no budget exists for the current month and the user's template has items. **Integration point — `src/pages/DashboardPage.tsx`:** Add `useTemplate()` call alongside the existing `useBudgets()` call. Add a `useEffect` with these guards: ```typescript const { items: templateItems } = useTemplate() const hasTemplateItems = templateItems.length > 0 const isCurrentMonth = month === currentMonth // currentMonth from useMonthParam baseline const attempted = useRef(false) useEffect(() => { if ( !loading && !currentBudget && hasTemplateItems && isCurrentMonth && !attempted.current && !generateFromTemplate.isPending ) { attempted.current = true generateFromTemplate.mutate({ month: parsedMonth, year: parsedYear, currency }) } }, [loading, currentBudget, hasTemplateItems, isCurrentMonth]) ``` **Currency source:** The `generateFromTemplate` mutation currently accepts `currency` as a param. Pull the user's preferred currency from the profile via a `useProfile` hook or extend `useAuth` to expose it. If `useProfile` is not yet implemented, fall back to reading from the most recent budget's `currency` field, or default to the settings page value. **No new API endpoint.** The `generateFromTemplate` mutation in `useBudgets.ts` already handles the full create + seed flow server-side. This is a pure frontend trigger change. **Edge cases handled by the guards:** - Template has 0 items → `hasTemplateItems=false` → skip auto-create, show prompt to configure template - Budget already exists → `!currentBudget=false` → skip - Past/future month navigation → `isCurrentMonth=false` → skip (only auto-create for today's month) - Multiple renders before mutation completes → `attempted` ref prevents double-fire **Modified files:** - `src/pages/DashboardPage.tsx` — add useTemplate(), useEffect trigger, currency source --- ### 3. Inline Add-from-Category-Library (Replaces QuickAdd Page) **Problem:** QuickAdd is a separate nav page (`/quick-add`), a separate table (`quick_add_items`), and a Popover on the Dashboard. Three disconnected surfaces for one concept. The mental model is unclear. **Solution:** Replace the QuickAdd Popover with an `AddOneOffSheet` component (a shadcn Sheet/side panel) available directly on BudgetDetailPage and Dashboard. It shows the user's category list grouped by type. The user picks a category, enters an amount, and a `one_off` budget item is created. The `quick_add_items` table remains intact but is removed from the primary workflow. **New component — `src/components/budget/AddOneOffSheet.tsx`:** ```typescript interface AddOneOffSheetProps { budgetId: string open: boolean onOpenChange: (open: boolean) => void } // Uses: useCategories() + useBudgets().createItem // On save: createItem.mutate({ budgetId, category_id, budgeted_amount, actual_amount, notes, item_tier: "one_off" }) ``` This replaces `QuickAddPicker.tsx` at both call sites. The existing `useBudgets().createItem` mutation is unchanged — it already inserts `item_tier: "one_off"` budget items. **QuickAdd page fate:** Remove from `AppLayout.tsx` nav items array. Keep the route and page file in place (backwards compat, existing data). Do NOT drop the `quick_add_items` table or `useQuickAdd` hook. **Modified files:** - `src/components/AppLayout.tsx` — remove QuickAdd from `navItems` array - `src/pages/DashboardPage.tsx` — replace `` with `` - `src/pages/BudgetDetailPage.tsx` — replace `` with `` **New files:** - `src/components/budget/AddOneOffSheet.tsx` **No DB migration needed.** `one_off` budget items already exist in `budget_items` with `item_tier = 'one_off'`. --- ### 4. Dashboard Simplification **Problem:** The current dashboard renders three chart types (donut + two bar charts) in a 3-column grid plus collapsible sections. This is dense and the charts don't clearly reflect user-entered data. v2.0 goal: "this month's budget at a glance." **Solution:** Remove the 3-column chart grid from `DashboardContent`. Keep `SummaryStrip` (3 KPI cards) and `CollapsibleSections`. The chart components stay in the codebase — they are correct — but are removed from the Dashboard layout. **Modified files:** - `src/pages/DashboardPage.tsx` — remove chart grid `
` and the three chart imports from `DashboardContent` **Dashboard data correctness:** The aggregation logic (`totalIncome`, `totalExpenses`, `budgetedIncome`, `budgetedExpenses`) is already correct in `DashboardContent`. The perceived "data not reflecting input" issue is caused by the auto-create flow not existing yet — once a budget exists and items are populated, the existing calculations are accurate. No aggregation logic changes needed. **Empty state after removing charts:** The removed chart area frees vertical space. Collapsible sections become the primary view. The `SummaryStrip` at the top provides the quick-glance summary. Consider adding a single "at a glance" progress indicator to `SummaryStrip` to compensate for the removed chart density. --- ### 5. Design System Token Rework **Problem:** `--radius: 0.625rem` produces rounded corners everywhere. The palette has high chroma values that read as saturated rather than pastel. **Solution:** Lower the radius token and tune OKLCH lightness/chroma in `index.css`. All shadcn/ui components reference `--radius` via `rounded-[--radius]` — a single value change propagates everywhere. **The only changed file: `src/index.css`** Key changes: ```css /* Sharp edges */ --radius: 0.125rem; /* was 0.625rem */ /* Background: warmer, closer to pure white */ --color-background: oklch(0.99 0.003 260); /* was 0.98 0.005 260 */ /* Softer border */ --color-border: oklch(0.91 0.008 260); /* was 0.88 0.01 260 */ /* Category pastels: increase lightness slightly, keep chroma */ /* e.g. --color-income-fill: oklch(0.72 0.14 155) → 0.78 0.12 155 */ ``` **What does NOT change:** - Token names (e.g., `--color-income`, `--color-bill`) — `palette.ts` references these by name - Two-tier pattern (text tokens at ~0.55L, fill tokens at ~0.68–0.78L) - Category color identities (income=green, bill=orange-red, etc.) - The `@theme inline` structure **shadcn component customization:** Zero component-file changes required. Radius change propagates automatically. Color changes propagate via CSS var resolution. --- ## Updated System Layout (v2.0) ``` ┌─────────────────────────────────────────────────────────────────────┐ │ React 19 SPA │ │ │ │ Auth Layer App Layer │ │ ┌───────────────┐ ┌─────────────────────────────────────────┐ │ │ │ LoginPage │ │ WizardRoute (NEW) │ │ │ │ RegisterPage │ │ └─ ProtectedRoute + AppLayout (MOD) │ │ │ └───────────────┘ │ Sidebar (5 nav items, -QuickAdd) │ │ │ └─────────────────────────────────────────┘ │ │ │ │ New route /setup: │ │ SetupWizardPage (NEW) → 3 steps → writes categories + template │ │ │ │ Modified pages: │ │ DashboardPage (MOD: auto-create, -charts, AddOneOffSheet) │ │ BudgetDetailPage (MOD: AddOneOffSheet replaces QuickAddPicker) │ │ │ │ Unchanged pages: │ │ CategoriesPage TemplatePage BudgetListPage SettingsPage │ │ QuickAddPage (hidden from nav, route kept) │ │ │ │ New components: │ │ wizard/WizardStep wizard/CategoryDefaults wizard/TemplateDefaults│ │ budget/AddOneOffSheet │ │ │ │ New hooks: │ │ useFirstRunState │ │ │ │ New data: │ │ src/data/presets.ts (static, no DB) │ └─────────────────────────┬───────────────────────────────────────────┘ │ @supabase/supabase-js v2 (unchanged) ┌─────────────────────────▼───────────────────────────────────────────┐ │ Supabase (unchanged schema, no new migrations) │ │ All new features write to existing tables │ └─────────────────────────────────────────────────────────────────────┘ ``` --- ## Component Boundary Map (v2.0) ``` App.tsx ├── /login → LoginPage ├── /register → RegisterPage ├── /setup → ProtectedRoute > SetupWizardPage (NEW) │ ├── WizardStep (NEW) │ ├── step 1: CategoryDefaults (NEW) — checkboxes from presets.ts │ └── step 2: TemplateDefaults (NEW) — amount inputs per selected preset └── /* → WizardRoute > ProtectedRoute > AppLayout (MODIFIED: -QuickAdd nav) ├── / → DashboardPage (MODIFIED) │ ├── MonthNavigator │ ├── DashboardContent (MODIFIED: no chart grid) │ │ ├── SummaryStrip (unchanged) │ │ ├── CollapsibleSections (unchanged) │ │ └── AddOneOffSheet (NEW, replaces QuickAddPicker) ├── /categories → CategoriesPage (unchanged) ├── /template → TemplatePage (unchanged) ├── /budgets → BudgetListPage (unchanged) ├── /budgets/:id → BudgetDetailPage (MODIFIED) │ └── AddOneOffSheet (NEW, replaces QuickAddPicker) ├── /quick-add → QuickAddPage (hidden from nav, route kept) └── /settings → SettingsPage (unchanged) ``` --- ## Data Flow Changes ### First-Run + Wizard Completion Flow ``` New user lands on any protected route ↓ WizardRoute: useFirstRunState() → needsSetup=true ↓ redirect /setup ↓ SetupWizardPage renders (step 1) User checks preset categories (Rent, Salary, Groceries, ...) ↓ [Next] step 2: User adjusts amounts per selected category ↓ [Finish] → handleFinish() ↓ useCategories().create × N → INSERT categories (N rows) ↓ await useTemplate().createItem × N → INSERT template_items (N rows) ↓ await navigate("/") ↓ DashboardPage: useFirstRunState() → needsSetup=false Auto-create effect fires (see below) ``` ### Auto-Budget Creation Flow ``` DashboardPage mounts (month = "2026-04", current month) ↓ useBudgets() → budgets list → currentBudget = undefined useTemplate() → items → hasTemplateItems = true ↓ useEffect fires: !loading && !currentBudget && hasTemplateItems && isCurrentMonth && !attempted ↓ generateFromTemplate.mutate({ month: 4, year: 2026, currency: "EUR" }) ↓ Supabase: 1. INSERT budgets (start_date=2026-04-01, end_date=2026-04-30) 2. SELECT template_items WHERE template_id = user's template 3. INSERT budget_items × N (actual_amount=0 per item) ↓ onSuccess: invalidate ["budgets"], ["budgets", id], ["budgets", id, "items"] ↓ DashboardPage rerenders: currentBudget = new budget DashboardContent mounts (keyed by budgetId) SummaryStrip + CollapsibleSections render with template-populated data ``` ### Inline Add-One-Off Flow ``` DashboardPage or BudgetDetailPage "Add one-off" button → AddOneOffSheet opens (Sheet side panel) ↓ useCategories() provides category list (grouped by type) User picks category, enters amount, optional notes ↓ [Add] → useBudgets().createItem.mutate({ budgetId, category_id, budgeted_amount: amount, actual_amount: amount, // one-off: budget = actual at time of entry notes, item_tier: "one_off" }) ↓ onSuccess: invalidate ["budgets", budgetId, "items"] Sheet closes → item appears in appropriate CollapsibleSection ``` --- ## New vs Modified File Summary ### New Files | File | Purpose | |------|---------| | `src/hooks/useFirstRunState.ts` | Detect first-run: categories=0 OR template_items=0 | | `src/data/presets.ts` | Static preset category list with suggested amounts | | `src/pages/SetupWizardPage.tsx` | Multi-step first-run wizard (local state, 3 steps) | | `src/components/wizard/WizardStep.tsx` | Step wrapper with step indicator / progress | | `src/components/wizard/CategoryDefaults.tsx` | Preset category checkbox picker (step 1) | | `src/components/wizard/TemplateDefaults.tsx` | Amount inputs per selected preset (step 2) | | `src/components/budget/AddOneOffSheet.tsx` | Sheet-based one-off item adder (replaces QuickAddPicker) | ### Modified Files | File | What Changes | |------|-------------| | `src/App.tsx` | Add `/setup` route; add WizardRoute guard | | `src/components/AppLayout.tsx` | Remove QuickAdd from `navItems` array | | `src/pages/DashboardPage.tsx` | Add useTemplate(); add auto-create useEffect; remove chart grid; swap AddOneOffSheet for QuickAddPicker | | `src/pages/BudgetDetailPage.tsx` | Swap AddOneOffSheet for QuickAddPicker | | `src/index.css` | Lower `--radius`; tune OKLCH pastel token values | | `src/i18n/en.json` | Add wizard i18n keys; remove/rename quick-add nav key | | `src/i18n/de.json` | Same additions as en.json (bilingual requirement) | ### Unchanged Files (confirmed) `useBudgets.ts`, `useTemplate.ts`, `useCategories.ts`, `useQuickAdd.ts`, `useAuth.ts`, `useMonthParam.ts`, `lib/types.ts`, `lib/palette.ts`, `lib/format.ts`, `lib/supabase.ts` — all hooks and library files are read-only for this milestone. --- ## Build Order (Dependency-Aware) Phase dependencies must be respected. Build bottom-up. **Phase 1: Design Tokens (no deps)** - `src/index.css` — lower `--radius`, refine OKLCH pastel values - Verify all 9 existing pages render correctly; no logic changes - Fast and reversible — do this first to establish visual baseline **Phase 2: Preset Data + First-Run Hook (no deps)** - `src/data/presets.ts` — static data, no imports needed - `src/hooks/useFirstRunState.ts` — depends on existing `useCategories` + `useTemplate`; no UI yet **Phase 3: Setup Wizard (depends on Phase 2)** - `src/components/wizard/WizardStep.tsx` — pure presentational - `src/components/wizard/CategoryDefaults.tsx` — depends on presets.ts - `src/components/wizard/TemplateDefaults.tsx` — depends on presets.ts - `src/pages/SetupWizardPage.tsx` — assembles wizard components + useFirstRunState + mutation hooks - `src/App.tsx` — wire `/setup` route + WizardRoute guard **Phase 4: Auto-Budget Creation (depends on Phase 3 — template must be populatable)** - `src/pages/DashboardPage.tsx` — add `useTemplate()` import + auto-create `useEffect` - Test: wizard → dashboard → budget auto-creates → SummaryStrip shows template data **Phase 5: Inline Add-One-Off + Dashboard Simplification (depends on Phase 4)** - `src/components/budget/AddOneOffSheet.tsx` — depends on `useCategories` + `useBudgets` (both existing) - `src/pages/DashboardPage.tsx` — remove chart grid; wire AddOneOffSheet - `src/pages/BudgetDetailPage.tsx` — wire AddOneOffSheet - `src/components/AppLayout.tsx` — remove QuickAdd nav item --- ## Anti-Patterns to Avoid ### Anti-Pattern 1: Global State for Wizard **What people do:** Store wizard state in a React context, Zustand store, or TanStack Query mutation cache. **Why it's wrong:** Wizard runs once, on first login. Global state persists unnecessarily and creates cleanup complexity. **Do this instead:** Local `useState` in `SetupWizardPage`. When the page unmounts (after `navigate("/")`), state is automatically cleaned up. ### Anti-Pattern 2: Auto-Create Without Template Guard **What people do:** Trigger `generateFromTemplate` whenever `!currentBudget`, regardless of template state. **Why it's wrong:** If a user skips the wizard or has an empty template, an empty budget gets silently created. This is worse than the empty state — it looks like a bug, not a fresh start. **Do this instead:** Guard with `hasTemplateItems && isCurrentMonth`. Show a helpful CTA to configure the template if template is empty. ### Anti-Pattern 3: Dropping the quick_add_items Table **What people do:** Add a migration to drop `quick_add_items` when the QuickAdd nav item is removed. **Why it's wrong:** Existing users have data there. The DB migration is irreversible. The table costs nothing to keep. **Do this instead:** Remove the nav item and surface only. Leave the table, `useQuickAdd` hook, and `/quick-add` route in place. Schedule a cleanup migration in a future milestone after confirming no users rely on it. ### Anti-Pattern 4: Renaming Design Token CSS Variables **What people do:** Rename tokens during the design rework (e.g., `--color-income` → `--category-color-income`) to make the naming more semantic. **Why it's wrong:** `palette.ts` references token names as strings: `var(--color-income)`. All chart components use `var(--color-income-fill)`. Renaming causes silent CSS failures — no TypeScript error, no build error, just missing colors. **Do this instead:** Change only the VALUES of existing tokens. Add new token names only for net-new concepts. ### Anti-Pattern 5: Wizard as Modal Overlay **What people do:** Show the wizard as a Dialog or Sheet over the main app layout on first load. **Why it's wrong:** No URL, so refresh drops the user back to the start or the main app with incomplete setup. Focus management in a full-screen modal is complex. Back button behavior is broken. **Do this instead:** Dedicated `/setup` route. `WizardRoute` handles redirect logic. The browser URL reflects wizard state, refresh works, and no special cleanup is needed. ### Anti-Pattern 6: Adding useEffect Dependencies by Feel **What people do:** Add `generateFromTemplate` itself to the `useEffect` deps array. **Why it's wrong:** Mutation objects from TanStack Query are re-created on every render, causing the effect to re-run in a loop. **Do this instead:** Use a `useRef` guard (`attempted.current`) and gate on stable primitive values (`!loading`, `!currentBudget`, `hasTemplateItems`). Do not include the mutation object in the dependency array. --- ## Supabase / DB Notes (v2.0) **No new migrations required.** All v2.0 features write to existing tables: | Feature | Writes To | Via | |---------|-----------|-----| | Wizard | `categories`, `template_items` | existing `useCategories().create`, `useTemplate().createItem` | | Auto-budget | `budgets`, `budget_items` | existing `useBudgets().generateFromTemplate` | | Inline one-off | `budget_items` | existing `useBudgets().createItem` | | Design tokens | — | CSS only | The `quick_add_items` table becomes read-only from the primary UI flow. No rows deleted, no schema change. --- ## Integration Points: New vs Existing | New Feature | Existing Hooks/Mutations Used | New Additions | |-------------|------------------------------|---------------| | First-run wizard | `useCategories().create`, `useTemplate().createItem` | `useFirstRunState`, `presets.ts`, wizard components | | Auto-budget creation | `useBudgets().generateFromTemplate` (already exists) | `useEffect` trigger in DashboardPage, `hasTemplateItems` guard | | Inline one-off add | `useBudgets().createItem` (already exists), `useCategories()` | `AddOneOffSheet` component | | Dashboard simplification | `useBudgetDetail`, `useMonthParam`, `SummaryStrip`, `CollapsibleSections` | Remove 3-column chart grid from DashboardContent | | Design tokens | All existing components consume tokens automatically | Token value adjustments in `index.css` only | --- ## Sources - Full codebase inspection (April 2026): - `src/App.tsx`, `src/components/AppLayout.tsx` - `src/pages/DashboardPage.tsx`, `src/pages/TemplatePage.tsx`, `src/pages/BudgetDetailPage.tsx` - `src/hooks/useTemplate.ts`, `src/hooks/useBudgets.ts`, `src/hooks/useQuickAdd.ts`, `src/hooks/useMonthParam.ts` - `src/components/QuickAddPicker.tsx`, `src/components/shared/PageShell.tsx` - `src/lib/types.ts`, `src/lib/palette.ts` - `src/index.css`, `src/i18n/en.json` - `supabase/migrations/002_categories.sql` through `005_quick_add.sql` - Confidence: HIGH — all claims are grounded in verified source code, not assumptions --- *Architecture research for: SimpleFinanceDash v2.0 — wizard setup, auto-budget, inline add, design rework* *Researched: 2026-04-02*