Fix dashboard hydration and archive theming

This commit is contained in:
2026-03-23 23:08:18 -04:00
parent c852ad0d80
commit feb2f2c37e
25 changed files with 290 additions and 13 deletions

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-03-23

View File

@@ -0,0 +1,73 @@
## Context
The repository starts with a product plan and OpenSpec configuration but no application code. The first version needs a complete local-first implementation using `Next.js`, `Prisma`, `SQLite`, and a fully offline local LLM runtime, while keeping scope intentionally narrow: one user, manual data entry, fixed categories, merchant-assisted categorization, and dashboard-only insights. Month boundaries are based on the local machine timezone, which affects date parsing, monthly aggregation, and paycheck coverage calculations.
## Goals / Non-Goals
**Goals:**
- Build a single deployable `Next.js` app with UI views and server routes in one codebase.
- Persist expenses, paychecks, and generated monthly insights in a local SQLite database managed by Prisma.
- Centralize monthly aggregation logic so dashboard reads and AI generation use the same numbers.
- Keep AI integration isolated behind a small service layer that prepares structured monthly context and calls a fully offline local inference runtime.
- Make v1 testable with deterministic validation, aggregation, and safe fallback behavior for sparse data.
- Add privacy-preserving merchant category suggestion with deterministic merchant mappings before model inference.
**Non-Goals:**
- Authentication, multi-user support, bank sync, receipt scanning, background jobs, or email delivery.
- Fully automatic uncapped categorization without user review for ambiguous merchants, editing data through AI, or free-form custom categories in v1.
- Complex financial forecasting beyond simple next-month guidance derived from recent activity.
## Decisions
### Use a single `Next.js` app for UI and APIs
- Rationale: the project is small, local-first, and benefits from one codebase for pages, route handlers, and shared utilities.
- Alternative considered: separate frontend and API service. Rejected because it adds deployment and data-sharing complexity without helping the v1 scope.
### Use Prisma with SQLite for persistence
- Rationale: Prisma provides schema management, typed queries, and straightforward migrations while keeping SQLite as a simple embedded database.
- Alternative considered: raw SQLite queries. Rejected because it slows down schema evolution and validation during initial development.
### Store money as integer cents and dates as local calendar strings
- Rationale: integer cents avoid floating-point issues, and local-date strings such as `YYYY-MM-DD` align with the local machine timezone requirement for monthly boundaries.
- Alternative considered: floating-point amounts or UTC timestamps only. Rejected because both introduce avoidable ambiguity for monthly reporting.
### Put aggregation logic in shared server-side services
- Rationale: dashboard totals, paycheck coverage, category breakdowns, and AI snapshots must stay consistent across endpoints.
- Alternative considered: separate logic per route. Rejected because it risks drift between dashboard and insight generation.
### Use `Ollama` with a local Qwen-class instruct model
- Rationale: privacy is a primary product requirement, and the target machine can comfortably run a recent local model for lightweight categorization and summary generation.
- Alternative considered: hosted `OpenAI`. Rejected because it violates the privacy-first goal for personal financial data.
### Add an AI service boundary with structured prompt input and fallback responses
- Rationale: the app needs runtime isolation, predictable prompt shape, and safe messaging when local inference is unavailable or data is too sparse for useful advice.
- Alternative considered: calling the local model directly from a route handler with raw records. Rejected because it couples prompting, aggregation, and transport too tightly.
### Use merchant rules first and local-model fallback second for category suggestion
- Rationale: most repeated merchants can be categorized deterministically and faster than model inference, while unknown merchants still benefit from local AI assistance.
- Alternative considered: model-only categorization. Rejected because it is slower, less predictable, and unnecessary for common merchants.
## Risks / Trade-offs
- [Local timezone handling differs by machine] -> Normalize month calculations around stored local-date strings and test month edges explicitly.
- [SQLite limits concurrency] -> Acceptable for single-user local-first v1; no mitigation beyond keeping writes simple.
- [AI output quality varies with sparse or noisy data] -> Add minimum-data fallback logic and keep prompts grounded in structured aggregates.
- [Local model may be unavailable or not yet pulled] -> Detect runtime/model readiness and return explicit offline setup guidance in the UI/API.
- [Merchant names can be ambiguous] -> Use auto-fill only for known deterministic mappings and require user confirmation for fallback suggestions.
## Migration Plan
1. Scaffold the `Next.js` app and install core dependencies.
2. Add the Prisma schema, create the initial SQLite migration, and generate the client.
3. Implement CRUD routes and UI forms for expenses and paychecks.
4. Implement dashboard aggregation and month filtering.
5. Add the offline AI service, merchant-category suggestion flow, and persistence for generated monthly insights.
6. Run automated tests, then exercise the main flows in the browser.
Rollback is straightforward in early development: revert the code change and reset the local SQLite database if schema changes become invalid.
## Open Questions
- Which exact local Qwen model tag should be the initial default in `Ollama`?
- Should generated monthly insights overwrite prior insights for the same month or create a historical trail of regenerated summaries?
- Do we want soft confirmation in the UI before deleting expenses or paychecks, or is immediate deletion acceptable for v1?

View File

@@ -0,0 +1,30 @@
## Why
The project currently has a product plan but no runnable application, spec artifacts, or implementation scaffold. Formalizing the first version now creates a clear contract for building a local-first expense tracker with reliable monthly summaries, private offline AI assistance, and no dependency on hosted model providers.
## What Changes
- Add a local-first web app for tracking expenses and biweekly paychecks without authentication.
- Add dashboard capabilities for month-to-date totals, category breakdowns, cash flow, and spending comparisons.
- Add fully offline AI insight generation for a selected month using structured aggregates and transaction samples.
- Add merchant-name-based category suggestion using deterministic rules plus local-model fallback.
- Add local persistence, validation, and API routes for expenses, paychecks, dashboard data, and insight generation.
## Capabilities
### New Capabilities
- `expense-tracking`: Record, list, and delete categorized expenses for a given date.
- `paycheck-tracking`: Record, list, and delete paycheck entries based on actual pay dates.
- `monthly-dashboard`: View month-specific spending, income, and derived financial summaries.
- `monthly-insights`: Generate private offline AI insights from monthly financial activity.
- `category-suggestion`: Suggest expense categories from merchant/shop names without cloud calls.
### Modified Capabilities
- None.
## Impact
- Affected code: new `Next.js` application, server routes, UI views, Prisma schema, and AI integration service.
- APIs: `POST/GET/DELETE` routes for expenses and paychecks, `GET /dashboard`, and `POST /insights/generate`.
- Dependencies: `Next.js`, `Prisma`, `SQLite`, `Ollama`, and a local Qwen-class instruct model.
- Systems: local machine timezone handling for month boundaries and persisted local database storage.

View File

@@ -0,0 +1,23 @@
## ADDED Requirements
### Requirement: System suggests categories from merchant names
The system SHALL support merchant-name-based category suggestion for expense entry while keeping all suggestion logic fully offline.
#### Scenario: Known merchant resolves from deterministic rules
- **WHEN** the user enters a merchant or shop name that matches a known merchant rule
- **THEN** the system assigns the mapped category without needing model inference
#### Scenario: Unknown merchant falls back to local model
- **WHEN** the user enters a merchant or shop name that does not match a known merchant rule
- **THEN** the system asks the local AI service for a category suggestion and returns the suggested category
### Requirement: Ambiguous suggestions remain user-controlled
The system SHALL keep the final saved category under user control for ambiguous or model-generated suggestions.
#### Scenario: User confirms model suggestion before save
- **WHEN** the category suggestion comes from model inference instead of a deterministic rule
- **THEN** the user can review and confirm or change the category before the expense is saved
#### Scenario: No cloud fallback is used
- **WHEN** the local suggestion service is unavailable
- **THEN** the system continues to allow manual category selection and does not send merchant data to a hosted provider

View File

@@ -0,0 +1,30 @@
## ADDED Requirements
### Requirement: User can record categorized expenses
The system SHALL allow the user to create an expense with a title, amount, category, and local calendar date using fixed starter categories.
#### Scenario: Valid expense is created
- **WHEN** the user submits a title, positive amount, valid category, and valid local date
- **THEN** the system stores the expense and returns the created record
#### Scenario: Invalid expense is rejected
- **WHEN** the user submits a missing title, invalid amount, invalid category, or invalid date
- **THEN** the system rejects the request with a validation error and does not store the expense
### Requirement: User can review and delete expenses
The system SHALL allow the user to list recorded expenses and delete a specific expense by identifier.
#### Scenario: Expenses are listed
- **WHEN** the user requests expenses for the app
- **THEN** the system returns stored expenses in a stable order with their recorded fields
### Requirement: User can browse expense history by month
The system SHALL allow the user to select a `YYYY-MM` month when reviewing expense history and SHALL return the expenses recorded for that month.
#### Scenario: Prior month entries are visible
- **WHEN** the user selects February 2026 in the add-expense history view
- **THEN** the system shows the expenses recorded in February 2026 and exposes delete actions for deletable entries in that month
#### Scenario: Expense is deleted
- **WHEN** the user deletes an existing expense
- **THEN** the system removes that expense and it no longer appears in future listings or aggregates

View File

@@ -0,0 +1,23 @@
## ADDED Requirements
### Requirement: Dashboard shows month-specific financial totals
The system SHALL return month-specific dashboard data for a requested `YYYY-MM` month using the local machine timezone for month boundaries.
#### Scenario: Dashboard totals are calculated for a populated month
- **WHEN** the user requests the dashboard for a month with expenses and paychecks
- **THEN** the system returns total expenses, total paychecks, net cash flow, and a category breakdown for that month
#### Scenario: Dashboard supports partial current-month data
- **WHEN** the user requests the dashboard for the current month before the month is complete
- **THEN** the system returns meaningful month-to-date totals and comparisons using the transactions recorded so far
### Requirement: Dashboard includes derived spending comparisons
The system SHALL provide derived comparisons for the selected month, including highest category, largest expense, average daily spend, and paycheck coverage information.
#### Scenario: Derived comparisons are available
- **WHEN** the selected month contains enough data for comparisons
- **THEN** the system returns the highest category, largest single expense, average daily spend, and spend-versus-paycheck coverage values
#### Scenario: Derived comparisons degrade safely for sparse data
- **WHEN** the selected month has no expenses or otherwise insufficient data for a comparison
- **THEN** the system returns null or empty-safe comparison fields instead of failing

View File

@@ -0,0 +1,30 @@
## ADDED Requirements
### Requirement: User can generate monthly AI insights on demand
The system SHALL allow the user to manually generate monthly AI insights for any month with existing or sparse data by sending structured monthly context to a fully offline local inference runtime.
#### Scenario: Insights are generated for a month with data
- **WHEN** the user requests insight generation for a month with recorded activity
- **THEN** the system sends monthly aggregates plus transaction samples to the local AI service and returns a rendered narrative summary with structured supporting totals
#### Scenario: Prior month insights can be generated
- **WHEN** the user requests insight generation for a previous month that has recorded data
- **THEN** the system generates and stores insight output for that requested month
### Requirement: Insight generation is read-only and safe for sparse months
The system SHALL keep AI insight generation read-only and return a safe fallback summary when a month does not have enough data for meaningful guidance.
#### Scenario: Sparse month returns fallback insight
- **WHEN** the user requests insight generation for a month with empty or near-empty data
- **THEN** the system returns a fallback message instead of low-confidence advice
#### Scenario: AI does not mutate financial records
- **WHEN** the system generates or stores monthly insights
- **THEN** no expense or paycheck records are created, updated, or deleted as part of that request
### Requirement: Insight generation remains private and resilient offline
The system SHALL keep monthly insight generation fully offline and provide a clear fallback response when the local model runtime or selected model is unavailable.
#### Scenario: Local runtime is unavailable
- **WHEN** the user requests monthly insights while the local AI runtime is not running or the configured model is unavailable
- **THEN** the system returns a clear setup or availability message instead of attempting a cloud fallback

View File

@@ -0,0 +1,23 @@
## ADDED Requirements
### Requirement: User can record paychecks by pay date
The system SHALL allow the user to create a paycheck with a positive amount and a local pay date.
#### Scenario: Valid paycheck is created
- **WHEN** the user submits a positive amount and valid local pay date
- **THEN** the system stores the paycheck and returns the created record
#### Scenario: Invalid paycheck is rejected
- **WHEN** the user submits a missing or invalid amount or date
- **THEN** the system rejects the request with a validation error and does not store the paycheck
### Requirement: User can review and delete paychecks
The system SHALL allow the user to list recorded paychecks and delete a specific paycheck by identifier.
#### Scenario: Paychecks are listed
- **WHEN** the user requests paychecks for the app
- **THEN** the system returns stored paychecks in a stable order with their recorded fields
#### Scenario: Paycheck is deleted
- **WHEN** the user deletes an existing paycheck
- **THEN** the system removes that paycheck and it no longer appears in future dashboard totals or insight inputs

View File

@@ -0,0 +1,38 @@
## 1. Project setup
- [x] 1.1 Scaffold the `Next.js` app with TypeScript, linting, and baseline project configuration.
- [x] 1.2 Add runtime dependencies for Prisma, SQLite, validation, charts, and offline AI integration.
- [x] 1.3 Add development dependencies and scripts for testing, Prisma generation, and local development.
- [x] 1.4 Add base environment and ignore-file setup for local database and API key configuration.
## 2. Persistence and shared services
- [x] 2.1 Define Prisma models for `Expense`, `Paycheck`, and `MonthlyInsight` and create the initial SQLite migration.
- [x] 2.2 Implement shared validation schemas for expenses, paychecks, and month query parameters.
- [x] 2.3 Implement shared money and local-date utilities for month boundary calculations.
## 3. Expense and paycheck workflows
- [x] 3.1 Implement expense API routes for create, list, and delete operations.
- [x] 3.2 Implement paycheck API routes for create, list, and delete operations.
- [x] 3.3 Build the `Add Expense` view with form submission, validation feedback, and expense listing.
- [x] 3.4 Build the `Income/Paychecks` view with form submission, validation feedback, and paycheck listing.
## 4. Dashboard and insights
- [x] 4.1 Implement monthly dashboard aggregation services for totals, category breakdowns, and derived comparisons.
- [x] 4.2 Implement the dashboard API route and render dashboard sections for month-to-date metrics and comparisons.
- [x] 4.3 Implement the offline `Ollama` insight service with structured monthly snapshot input and sparse-month fallback logic.
- [x] 4.4 Implement insight generation and display in the dashboard, including persisted monthly insight records and offline-runtime fallback messaging.
## 5. Offline categorization
- [x] 5.1 Implement deterministic merchant-to-category mapping for known merchants.
- [x] 5.2 Implement a local-model category suggestion endpoint for unknown merchants.
- [x] 5.3 Update the expense entry flow to auto-fill known merchants and require confirmation for model-generated suggestions.
- [x] 5.4 Add local runtime availability handling so category suggestion falls back to manual selection without cloud calls.
## 6. Verification
- [x] 6.1 Add automated tests for validation, persistence, dashboard aggregates, offline insight fallback behavior, and category suggestion rules.
- [x] 6.2 Verify the primary user flows in the browser, including expense entry, paycheck entry, dashboard updates, category suggestion, and insight generation.

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-03-23

View File

@@ -0,0 +1,54 @@
## Context
The project uses Next.js with Tailwind CSS v4 and a warm stone/amber palette. All styling is done via Tailwind utility classes directly in JSX. There are no separate CSS modules for components. The root layout owns the `<html>` and `<body>` tags, making it the natural place to manage the theme class and prevent flash-of-unstyled-content.
## Goals / Non-Goals
**Goals:**
- Implement class-based dark mode that works with Tailwind v4's `@custom-variant` API.
- Prevent theme flash on page load with an inline blocking script.
- Persist user preference in `localStorage` under the key `theme`.
- Fall back to system preference (`prefers-color-scheme`) when no preference is saved.
- Keep the dark palette warm and consistent with the existing amber/stone design language.
**Non-Goals:**
- Server-side theme rendering or cookies (local-only app, no SSR theme concerns beyond flash prevention).
- Per-page or per-component theme overrides.
- Animated theme transitions beyond the existing `transition-duration: 180ms` on interactive elements.
## Decisions
### Use Tailwind v4 `@custom-variant` for class-based dark mode
- Rationale: Tailwind v4's default dark mode is media-query-based. Adding `@custom-variant dark (&:where(.dark, .dark *))` in `globals.css` enables the `dark:` prefix to respond to a class on any ancestor, which is the standard pattern for toggle-able dark mode.
- Alternative considered: Continue using media-query dark mode with no toggle. Rejected because users cannot override system preference.
### Store theme preference in `localStorage`
- Rationale: Simple, synchronous, requires no server or cookies. Reads correctly in the blocking inline script.
- Alternative considered: Cookies for SSR. Rejected because this app is local-first and has no meaningful SSR theme benefit.
### Inject an inline blocking script in `<head>` to set the `dark` class before first paint
- Rationale: Prevents the flash where the page briefly renders in light mode before React hydrates and reads `localStorage`. The script is small and runs synchronously.
- Alternative considered: Set the class in a React `useEffect`. Rejected because `useEffect` runs after paint, causing a visible flash.
### Warm deep-stone dark palette
- Rationale: The light theme is built on warm stone and amber tones. The dark theme mirrors this with deep stone backgrounds (`stone-950`, `stone-900`, `stone-800`) and dimmed amber/emerald accents, keeping the visual identity coherent.
- Alternative considered: Neutral dark greys. Rejected because they clash with the warm amber accents and feel disconnected from the brand.
## Risks / Trade-offs
- [Inline script adds a small amount of render-blocking HTML] → Acceptable; the script is under 200 bytes and only runs once per page load.
- [LocalStorage is unavailable in some privacy modes] → The script wraps the read in a try/catch and falls back to system preference.
- [Many components have hardcoded warm background strings like `bg-[#fffaf2]`] → These are replaced with equivalent Tailwind tokens plus `dark:` overrides so the mapping is explicit and maintainable.
## Migration Plan
1. Update `globals.css` with `@custom-variant dark` and dark-mode CSS variable overrides.
2. Create the `ThemeToggle` client component.
3. Update `layout.tsx` to add the blocking script and render `ThemeToggle` in the header.
4. Update `site-nav.tsx` with dark-mode nav styles.
5. Update `home-dashboard.tsx`, `expense-workspace.tsx`, `paycheck-workspace.tsx`, and `recurring-expense-manager.tsx` with `dark:` variants.
6. Update page-level header text that uses hardcoded colour classes.
## Open Questions
- Should the toggle also expose a "System" option (three-way: Light / Dark / System) rather than a binary flip? Deferred to a follow-up; the initialisation script already handles system fallback on first visit.

View File

@@ -0,0 +1,27 @@
## Why
The app currently ships with a warm light theme only. Adding a system-aware dark mode with a manual toggle removes eye strain in low-light conditions and meets baseline accessibility expectations for a personal finance tool used at any hour.
## What Changes
- Add a class-based dark mode variant using Tailwind v4 `@custom-variant` so all `dark:` utilities are controlled by a `dark` class on `<html>`.
- Add a `ThemeToggle` component that persists the user's preference in `localStorage` and initialises the correct class before first paint to prevent flash.
- Update the root layout to inject the inline initialisation script and render the toggle inside the existing header.
- Apply `dark:` variants to all UI components to produce a warm, deep-stone dark palette consistent with the existing amber/stone design language.
## Capabilities
### New Capabilities
- `dark-mode`: Users can switch between light and dark themes with a toggle that persists across sessions. System preference is respected on first visit.
### Modified Capabilities
- `expense-tracking`: Expense workspace and recurring expense manager now render correctly in both themes.
- `paycheck-tracking`: Paycheck workspace now renders correctly in both themes.
- `monthly-dashboard`: Dashboard sections, insight cards, category bars, and stat cards now render correctly in both themes.
## Impact
- Affected code: `globals.css`, `layout.tsx`, all components under `src/components/`, and page header text in `src/app/`.
- APIs: None.
- Dependencies: None — uses Tailwind v4 built-ins and the Web Storage API.
- Systems: No server-side changes required; theme state is fully client-side.

View File

@@ -0,0 +1,26 @@
## ADDED Requirements
### Requirement: User can switch between light and dark themes
The system SHALL allow the user to toggle between light and dark themes with a persistent preference.
#### Scenario: User toggles dark mode on
- **WHEN** the user activates the theme toggle while the app is in light mode
- **THEN** the system applies the dark theme and saves the preference for future visits
#### Scenario: User toggles dark mode off
- **WHEN** the user activates the theme toggle while the app is in dark mode
- **THEN** the system applies the light theme and saves the preference for future visits
### Requirement: Theme preference respects system defaults on first visit
The system SHALL use the user's system color scheme preference when no saved preference exists.
#### Scenario: No stored preference uses system theme
- **WHEN** the user opens the app for the first time without a saved theme preference
- **THEN** the system applies dark mode when the operating system prefers dark color schemes and light mode otherwise
### Requirement: Theme selection loads without a flash of the wrong theme
The system SHALL initialize the theme before the first visible paint so the page does not briefly render in the wrong theme.
#### Scenario: Initial paint matches saved theme
- **WHEN** the app loads and a saved theme preference exists
- **THEN** the document theme is applied before the page content is visibly rendered

View File

@@ -0,0 +1,12 @@
## ADDED Requirements
### Requirement: Expense tracking UI renders correctly in both themes
The system SHALL render the expense tracking workspace, history list, forms, and item states with readable contrast in both light and dark themes.
#### Scenario: Expense workspace renders in dark mode
- **WHEN** the user opens the expense tracking view while dark mode is active
- **THEN** the form card, history card, inputs, actions, and expense rows use dark-compatible colors and remain readable
#### Scenario: Expense workspace renders in light mode
- **WHEN** the user opens the expense tracking view while light mode is active
- **THEN** the form card, history card, inputs, actions, and expense rows use light-compatible colors and remain readable

View File

@@ -0,0 +1,12 @@
## ADDED Requirements
### Requirement: Monthly dashboard UI renders correctly in both themes
The system SHALL render dashboard sections, insight cards, category bars, stat tiles, and empty states with readable contrast in both light and dark themes.
#### Scenario: Dashboard renders in dark mode
- **WHEN** the user opens the dashboard while dark mode is active
- **THEN** the summary cards, comparison cards, progress bars, and empty states use dark-compatible colors and remain readable
#### Scenario: Dashboard renders in light mode
- **WHEN** the user opens the dashboard while light mode is active
- **THEN** the summary cards, comparison cards, progress bars, and empty states use light-compatible colors and remain readable

View File

@@ -0,0 +1,12 @@
## ADDED Requirements
### Requirement: Paycheck tracking UI renders correctly in both themes
The system SHALL render the paycheck tracking workspace, schedule panel, form, and list items with readable contrast in both light and dark themes.
#### Scenario: Paycheck workspace renders in dark mode
- **WHEN** the user opens the paycheck tracking view while dark mode is active
- **THEN** the schedule panel, form card, inputs, and paycheck rows use dark-compatible colors and remain readable
#### Scenario: Paycheck workspace renders in light mode
- **WHEN** the user opens the paycheck tracking view while light mode is active
- **THEN** the schedule panel, form card, inputs, and paycheck rows use light-compatible colors and remain readable

View File

@@ -0,0 +1,25 @@
## 1. CSS and variant setup
- [x] 1.1 Add `@custom-variant dark (&:where(.dark, .dark *))` to `globals.css` and update `:root` / `@theme` blocks with dark-mode CSS variable overrides.
## 2. Theme toggle infrastructure
- [x] 2.1 Create `src/components/theme-toggle.tsx` — a client component that reads and writes `localStorage` theme preference, toggles the `dark` class on `<html>`, and renders a sun/moon button.
- [x] 2.2 Update `src/app/layout.tsx` to inject the inline blocking script in `<head>` and render `ThemeToggle` in the header alongside `SiteNav`.
## 3. Component dark-mode styles
- [x] 3.1 Update `src/components/site-nav.tsx` with `dark:` variants for link backgrounds, borders, and text.
- [x] 3.2 Update `src/components/home-dashboard.tsx` with `dark:` variants for all section cards, stat tiles, insight blocks, progress bars, and empty states.
- [x] 3.3 Update `src/components/expense-workspace.tsx` with `dark:` variants for the form card, list card, inputs, and expense articles.
- [x] 3.4 Update `src/components/paycheck-workspace.tsx` with `dark:` variants for the schedule panel, form, and paycheck list.
- [x] 3.5 Update `src/components/recurring-expense-manager.tsx` with `dark:` variants for the panel, inline form, and definition articles.
## 4. Page header text
- [x] 4.1 Update hardcoded text colours in `src/app/add-expense/page.tsx` and `src/app/income/page.tsx` with `dark:` overrides.
## 5. Verification
- [x] 5.1 Visually verify light and dark modes across the dashboard, add-expense, and income pages in the browser.
- [x] 5.2 Verify that theme preference persists across page refreshes and that there is no flash of the wrong theme on load.