Files
SimpleFinanceDash/.planning/research/ARCHITECTURE.md

28 KiB
Raw Blame History

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:

// 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 /

<Route path="/setup" element={<ProtectedRoute><SetupWizardPage /></ProtectedRoute>} />

Wizard state — local useState only in SetupWizardPage:

type WizardState = {
  step: 1 | 2 | 3
  selectedPresets: string[]        // preset IDs chosen in step 1
  amounts: Record<string, number>  // 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:

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:

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 <QuickAddPicker> with <AddOneOffSheet>
  • src/pages/BudgetDetailPage.tsx — replace <QuickAddPicker> with <AddOneOffSheet>

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 <div> 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:

/* 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.680.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