28 KiB
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:
useCategories().create× N (create one category row per selected preset)useTemplate().createItem× N (create template items, each linked to the new categories)navigate("/")— DashboardPage loads,useFirstRunStatereturnsneedsSetup=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.tssrc/pages/SetupWizardPage.tsxsrc/components/wizard/WizardStep.tsxsrc/components/wizard/CategoryDefaults.tsxsrc/components/wizard/TemplateDefaults.tsxsrc/data/presets.ts
Modified files:
src/App.tsx— add/setuproute + 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 →
attemptedref 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 fromnavItemsarraysrc/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 fromDashboardContent
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.tsreferences 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 inlinestructure
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 neededsrc/hooks/useFirstRunState.ts— depends on existinguseCategories+useTemplate; no UI yet
Phase 3: Setup Wizard (depends on Phase 2)
src/components/wizard/WizardStep.tsx— pure presentationalsrc/components/wizard/CategoryDefaults.tsx— depends on presets.tssrc/components/wizard/TemplateDefaults.tsx— depends on presets.tssrc/pages/SetupWizardPage.tsx— assembles wizard components + useFirstRunState + mutation hookssrc/App.tsx— wire/setuproute + WizardRoute guard
Phase 4: Auto-Budget Creation (depends on Phase 3 — template must be populatable)
src/pages/DashboardPage.tsx— adduseTemplate()import + auto-createuseEffect- 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 onuseCategories+useBudgets(both existing)src/pages/DashboardPage.tsx— remove chart grid; wire AddOneOffSheetsrc/pages/BudgetDetailPage.tsx— wire AddOneOffSheetsrc/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.tsxsrc/pages/DashboardPage.tsx,src/pages/TemplatePage.tsx,src/pages/BudgetDetailPage.tsxsrc/hooks/useTemplate.ts,src/hooks/useBudgets.ts,src/hooks/useQuickAdd.ts,src/hooks/useMonthParam.tssrc/components/QuickAddPicker.tsx,src/components/shared/PageShell.tsxsrc/lib/types.ts,src/lib/palette.tssrc/index.css,src/i18n/en.jsonsupabase/migrations/002_categories.sqlthrough005_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