Files
Daniel Krähenbühl 1a7ef09805 feat: Armarium v1.1.0 — dashboard, auth, 2FA, SMTP, settings, deploy
Dashboard:
- ApexCharts bar chart (income vs fixed costs vs expenses) and donut chart
- KPI cards: income, fixed costs, savings rate with configurable goal
- Greeting with time-of-day and locale-aware date/time display

Authentication & security:
- Email-based login (no username), case-insensitive lookup
- JWT access/refresh tokens with rotation and blacklist
- TOTP 2FA with QR code, backup codes (copy + PDF export)
- 2FA recovery via email code
- Cloudflare Turnstile CAPTCHA on login and register

Email flows:
- Email verification on registration (24h token)
- Password reset flow (15min token, anti-enumeration)
- Brevo SMTP integration with HTML + plaintext email templates
- Notification emails: 2FA recovery, password changed, email changed

Settings page:
- 2FA management (enable/disable, QR, backup codes)
- Active sessions list with per-device revoke
- Data export: ZIP with 6 PDFs via fpdf2
- Notification preferences (3 toggles)
- Danger zone: account deletion with mandatory export + confirmation phrase

UI & layout:
- Sidebar with collapsible/flyout mode, Angular signal-based dropdowns
- Dark mode (class-based), language switcher (DE/FR/IT/EN)
- Mobile-responsive layout with touch-friendly targets
- Roboto font via @fontsource (GDPR-compliant, no Google CDN)
- Pure Tailwind CSS v3

Infrastructure:
- Forgejo Actions CI/CD pipeline (auto-deploy on push to main)
- Gunicorn + Nginx + PostgreSQL production setup
- Rate limiting, HSTS, secure cookies, CSRF protection
2026-05-25 22:46:30 +02:00

252 lines
23 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.1.0] - 2026-05-19
### Added
- Auth: E-Mail-Verifikation bei Registrierung — Token (SHA-256-Hash in DB, 24h gültig) wird per Mail versendet; `/verify-email?token=` Frontend-Route löst automatisch `POST /api/auth/verify-email/` aus
- Auth: Passwort vergessen / Reset — `POST /api/auth/password-reset/` (anti-enumeration); `POST /api/auth/password-reset/confirm/` setzt Passwort und invalidiert alle aktiven Sessions; Token (SHA-256-Hash, 15min TTL) via Brevo-Mail mit Link
- Auth: `ForgotPassword`-Komponente (`/forgot-password`), `ResetPassword`-Komponente (`/reset-password`), `VerifyEmail`-Komponente (`/verify-email`)
- Auth: "Passwort vergessen?"-Link auf Login-Seite
- E-Mail-Templates: `registration_confirm`, `password_reset`, `password_changed`, `email_changed` (je HTML + Plaintext)
- Backend: `finance/email.py` — generischer `send_email()` Helper mit `EmailMultiAlternatives`
- Backend: `FRONTEND_URL` Env-Var für absolute Links in Mails; `EMAIL_BACKEND` via Env-Var überschreibbar
- i18n: `auth.forgot_password`, `auth.reset_password`, `auth.new_password`, `auth.email_verified` + Error-Keys (DE/EN/FR/IT)
- Feature: Jahresplanung (`/financial-year`) — Jahres-Dropdown, 3 Summary-Cards, Tabs Einnahmen/Fixkosten mit Inline-CRUD; "Neues Jahr starten" (max. 1 Jahr im Voraus)
- Backend: `FinancialYear`, `YearlyIncome`, `YearlyBudgetItem`, `Household`, `HouseholdMembership` Modelle (Migration 0019)
- Backend: vollständige REST-API für Jahresplanung und Haushalte inkl. Invite/Accept/Leave/SetRole
- Backend: Django Management Command `migrate_to_financial_year` (idempotent, `--dry-run`)
- Frontend: `FinancialYearService` mit allen Typen und API-Methoden
- Frontend: Household-Sektion auf `/financial-year` (Gründen, Einladen, Rollen, Annehmen, Verlassen)
- Sidebar: "Jahresplanung" Nav-Item
- Dashboard: Einnahmen/Fixkosten aus `FinancialYearService` statt Account/Budget-Daten
- Dashboard: Einnahmen vs. Ausgaben — Flowbite-Redesign, 3 Serien, Jahres-Dropdown
- Dashboard: Fixkostenaufschlüsselung — Pie Chart mit %-Labels, Toggle zur Listenansicht
- Dashboard: Sparquote — personalisierbarer Ziel-Marker, Settings-Toggle zum Anpassen
- Security: Cloudflare Turnstile CAPTCHA auf Login + Register
- Infrastructure: Brevo SMTP (`smtp-relay.brevo.com:587`), Domain `armarium.ch` verifiziert (SPF/DKIM)
- i18n: `sidebar.financial_year`, `financial_year.*`, `dashboard.*`, `auth.errors.captcha_failed` (DE/EN/FR/IT)
- Settings: Active Sessions card — lists all logged-in devices (device name, IP, last active); individual revoke and "sign out all others" buttons; current session marked with badge; `UserSession` model with `session_key`, `refresh_jti`, `device_name`, `ip_address`; `_create_session()` called on every successful login (including 2FA and recovery flows)
- Settings: Data Export — downloads a ZIP containing 6 structured PDFs (Profil, Konten, Budgets, Ausgaben, Transaktionen, Termine) generated server-side with fpdf2; violet header bar, alternating row fill, gray footer with page numbers
- Settings: Notification Preferences — toggles for "Anstehende Termine", "Budget-Warnungen", "Monatliche Zusammenfassung"; saved via `PATCH /api/notifications/prefs/`; loaded from profile on page open
- Settings: Account deletion now requires two steps — (1) mandatory data export, (2) confirmation form with password field (show/hide eye icon) and translated confirmation phrase (`profile.delete_account` in current language); delete button disabled until both conditions met
- Settings: After account deletion, user is redirected to `https://www.armarium.ch` and both storages are cleared
- Auth: Language switcher (`LangSwitcher` component) inside the login and register cards (top-left); uses `@HostListener` for outside-click close and `[class]` binding to avoid Tailwind dark-mode colon conflicts
- Auth: "Angemeldet bleiben" checkbox on login with two-line label; toggles between `localStorage` (persistent) and `sessionStorage` (session-only); persisted through full 2FA and recovery flows via `keepSignedIn` parameter
- Auth: `session_key` stored alongside JWT tokens in same storage; sent as `X-Session-Key` header by interceptor so backend can identify current session
- Backend: `PATCH /api/notifications/prefs/` endpoint (`NotificationPrefsView`)
- Backend: `GET /api/auth/sessions/`, `DELETE /api/auth/sessions/<key>/`, `DELETE /api/auth/sessions/revoke-all/` endpoints
- Backend: `GET /api/export/` returns ZIP with 6 PDFs via fpdf2
- Backend: Admin URL configurable via `ADMIN_URL` env var (default `manage/`); obscures the standard `/admin/` path from scanners
- Backend: SMTP email config from env vars (`EMAIL_HOST`, `EMAIL_PORT`, `EMAIL_HOST_USER`, `EMAIL_HOST_PASSWORD`, `EMAIL_USE_TLS`, `DEFAULT_FROM_EMAIL`); console backend in DEBUG mode
- Backend: production security block (`if not DEBUG`) — `SECURE_SSL_REDIRECT`, HSTS (1 year, preload, subdomains), secure cookies, `SECURE_CONTENT_TYPE_NOSNIFF`, proxy SSL header
- Backend: `DATA_UPLOAD_MAX_MEMORY_SIZE` and `FILE_UPLOAD_MAX_MEMORY_SIZE` capped at 5 MB
- Backend: `CSRF_TRUSTED_ORIGINS` from env var
- i18n: `settings.sessions_*`, `settings.export_*`, `settings.notif_*`, `settings.delete_*` keys (DE/FR/IT/EN)
- i18n: `auth.keep_signed_in`, `auth.keep_signed_in_hint`, `auth.back_to_login` keys (DE/FR/IT/EN)
- Security: TOTP-based two-factor authentication (2FA) — setup via QR code, verified on login; production-ready with HMAC-signed temp token (5 min expiry), replay protection via `totp_last_used_code`, and 8 backup codes in `XXXXXXXX-XXXXXXXX` format (SHA-256 hashed in DB)
- Security: `TwoFactorSetupView`, `TwoFactorEnableView`, `TwoFactorDisableView`, `TwoFactorLoginView` backend endpoints; `BackupCode` model with index on `(user, used)`
- Settings page (`/settings`): 2FA card (enable/disable, QR scan, backup code copy + PDF download) and Danger Zone (account deletion); accessible from navbar avatar menu
- Login: two-step flow — step 1 credentials, step 2 TOTP/backup code entry when 2FA is active
- Backup codes: copy to clipboard and PDF download via jsPDF (client-side, no server round-trip)
- Dashboard: donut chart "Fixed Costs Breakdown" now shows individual budget entry names and amounts (was: grouped by category)
- Dashboard: toggle button (top-right of donut card) switches between chart view and a scrollable breakdown list with color dot, name, CHF amount and percentage per entry
- i18n: `auth.totp_title`, `auth.totp_hint`, `auth.totp_or_backup`, `auth.back_to_login`, `auth.errors.invalid_totp` keys (DE/FR/IT/EN)
- i18n: `profile.totp_*`, `profile.backup_*` keys for all 2FA labels, backup codes and messages (DE/FR/IT/EN)
- i18n: `settings.subtitle` key (DE/FR/IT/EN)
### Changed
- `.env.example`: `FRONTEND_URL` und `EMAIL_BACKEND` Variablen ergänzt
- Profile-Model: `email_verify_token` neu als SHA-256-Hash gespeichert; `email_verify_token_expires` (24h TTL) hinzugefügt (Migration 0020)
- **UI:** migrated frontend to Flowbite design system — custom Tailwind v4 theme (`budget-app-theme.css`) with violet primary color, consistent rounded-lg cards, Flowbite outline SVG icons throughout
- **Navbar:** avatar dropdown and notification panel converted from Flowbite JS (`data-dropdown-toggle`) to Angular state management (`avatarDropdownOpen` signal + backdrop `<div>` for outside-click close); eliminates Flowbite JS runtime dependency
- **Navbar:** sun/moon/bell icons replaced with Flowbite outline variants; logout entry in avatar dropdown now shows `arrow-right-to-bracket` icon
- **Sidebar:** navigation icons restored to fill style (`fill="currentColor" viewBox="0 0 20 20"`) for consistency with pre-migration appearance; Settings link added to mobile drawer after Profile link
- **Dashboard:** greeting H1 changed to `font-light` (weight 300)
- **Mobile — tables:** non-critical columns hidden on small screens (`hidden sm:table-cell` for category, `hidden md:table-cell` for account) in expense and transaction lists
- **Mobile — touch targets:** all icon-only buttons (edit, delete, modal close, calendar navigation arrows) updated to `p-2` minimum (was `p-1.5`)
- **Mobile — form grids:** `grid-cols-2` changed to `grid-cols-1 sm:grid-cols-2` in expense modals and profile form
- **Mobile — budget entries:** `min-w-0 flex-1` and `truncate` on name/account spans prevent overflow on narrow screens
- **Mobile — calendar:** day cells `min-h-[48px] sm:min-h-[64px]`, day detail drawer `w-full sm:w-80`
- **Mobile — dashboard:** KPI card padding `p-3 sm:p-5`, KPI values `text-xl sm:text-2xl`
- **Mobile — login:** OTP digit inputs `h-10 w-10 sm:h-12 sm:w-12`; backup code field placeholder removed (hint text above suffices)
- **Mobile — settings:** recovery email row `flex-col sm:flex-row` so button stacks below input on narrow screens
- **Search:** placeholder text styled `placeholder-gray-400` (was unstyled)
- Typography: Roboto font self-hosted via `@fontsource/roboto` (300/400/500/700 weights) — no Google Fonts CDN, DSGVO/nDSG compliant; replaces Inter which was defined in the theme but never actually loaded
- Typography: unified font-size system — page title H1 `text-2xl` (24px), card/section H2 headers `text-base` (16px), sidebar navigation `text-sm` (14px), savings rate display `text-3xl` (30px); applied across Dashboard, Budgets, Expenses, Calendar, Profile, Settings and Sidebar
- Profile: password change fields no longer show browser autofill suggestions (`autocomplete="new-password"`); show/hide eye icon added to both password fields
- Sidebar: version number updated to 1.1.0
- Login: registration is email-only — username field removed; backend auto-sets `username=email`; `RegisterSerializer` updated accordingly
- Login: font sizes increased throughout login/register flow (`text-xs``text-sm`, `text-sm``text-base`, card `max-w-sm``max-w-md`)
- Settings: 2FA card and Danger Zone remain; Recovery Email, Active Sessions, Data Export, Notification Preferences added above Danger Zone
- Settings: Danger Zone delete flow is now three-step (export → credentials + phrase → redirect)
- `LogoutView`: added `permission_classes = [AllowAny]` so logout works even when access token is expired; now also deletes `UserSession` by JTI and `X-Session-Key`
- `ChangePasswordView`: invalidates and blacklists all other active sessions on password change
- `authInterceptor`: sends `X-Session-Key` header on all internal API requests
- Auth: `completeLogin()` and `storeTokens()` accept optional `session_key`; `logout()` clears session key from both storages
- Auth: `refreshToken()` bug fixed — was storing `tokens.access` twice instead of `tokens.access` and `tokens.refresh`
- Login endpoint `POST /api/auth/token/` replaced with custom `LoginView` supporting 2FA challenge response
- Profile page: 2FA and Danger Zone sections removed and moved to new Settings page
- Authenticator app recommendation updated to: Proton Pass, Aegis (Android), Raivo OTP (iOS)
- Calendar: holidays and school holidays now shown in the current app language; reloaded automatically on language change via `translate.onLangChange` subscription
- Calendar: OpenHolidays API requests include `languageIsoCode` parameter; cache key includes language
- Calendar: today's date shown as violet ring/outline only (not filled) to prevent white-on-white hover text
- Notifications: "Mark as read" checkmark button per notification (replaces X icon)
- Notifications: "Mark all as read" button in panel header
- i18n: `nav.mark_read`, `nav.mark_all_read` keys (DE/FR/IT/EN)
- Calendar: live holiday and school holiday data via OpenHolidays API (openholidaysapi.org, AGPL-3.0) with automatic fallback to static data on API failure
- Calendar: in-memory cache per year/canton to avoid redundant API requests
- Budgets: show info modal when no accounts exist, with link button to accounts page
- Expenses: show info modal when no accounts exist, with link button to accounts page
- i18n: `common.no_accounts_title`, `common.no_accounts_text`, `common.go_to_accounts` keys (DE/FR/IT/EN)
- Login: authentication changed from username-based to email-based; password manager only needs email + password
- Mobile sidebar drawer: Notifications, Dark/Light toggle, Profile link and Logout moved from navbar into the sidebar so all user actions are accessible via the hamburger menu
### Security
- Password Reset invalidiert alle aktiven Sessions des Nutzers (analog zu `ChangePasswordView`)
- Email-Verify-Token: SHA-256-Hash statt Klartext in DB; Ablaufzeit 24h
- `VerifyEmailView` + `PasswordResetConfirmView` mit `AuthThrottle` (5/min) gesichert
### Fixed
- Security: `TwoFactorRecoverConfirmView` now verifies `temp_token` (password proof) before accepting a recovery code — previously anyone with a valid recovery code could bypass authentication entirely
- Security: `ProfileView.delete` now requires password verification (`check_password`) before deletion; returns 403 on failure
- Backend: URL conflict — `api/notifications/` was mapped to both `NotificationsView` and `NotificationPrefsView`; prefs endpoint moved to `api/notifications/prefs/`
- Backend: fpdf2 export — em/en dash characters (`—`, ``) caused `FPDFUnicodeEncodingException` with Helvetica (Latin-1 only); fixed with `safe()` helper using `encode('latin-1', errors='replace')` and replaced placeholder dashes with ASCII `-`
- Backend: migration 0017 (`finance_profile.notif_deadlines` etc.) was not applied on server restart, causing 500 on login; now documented that `python manage.py migrate` must be run after deployment
- Auth: `LangSwitcher``[class.dark:text-violet-400]` Angular binding error caused by Tailwind dark-mode colon in class name; fixed by using `itemClass()` method with `[class]` binding
- Auth: `LangSwitcher``current` signal initialized before `langService` was available; fixed by setting default `'de'` and calling `this.current.set(langService.current)` in constructor body
- Dashboard: bar chart right padding increased so December bars are no longer clipped at the container edge
- Calendar: legend block removed from footer
- Calendar: ZH Spring Holidays 2026 corrected to 20.04.02.05. in static fallback data (source: OpenHolidays API)
- Calendar: date input now renders in the app language format (lang attribute bound to current language)
- Calendar: deadline type dropdown now shows placeholder on open; defaults to "other" if none selected
- Calendar: title input placeholder text styled correctly in gray
- Login: show/hide password toggle with eye icon
- Register: show/hide password toggle on both password fields, independently toggleable
- Register: hint text below username field explaining it is used as in-app display name (DE/FR/IT/EN)
- i18n: `nav.dark_mode`, `nav.light_mode` keys for sidebar mobile theme toggle (DE/FR/IT/EN)
- i18n: `auth.username_hint` key for register page (DE/FR/IT/EN)
- Mobile navbar: right-side icons (notifications, theme toggle, avatar) caused the navbar to wrap to a second line on narrow screens, pushing page content below the fixed offset; icons are now hidden on mobile and integrated into the sidebar drawer
- Mobile notification panel: was full-width and flush against the top of the screen; now has `left-4`/`right-4` margins, `top-20` offset and `rounded-xl` corners
- Auth: new `EmailAuthBackend` performs case-insensitive email lookup so login works regardless of capitalisation
- Login/Register: placeholder texts removed from all username and password fields
---
## [1.0.1] - 2026-04-14
### Fixed
- Production deployment: replaced hardcoded `http://127.0.0.1:8000` with relative paths (`/api`, `/api/auth`) in `ApiService` and `AuthService` so the frontend works on any server
- Production deployment: avatar image URLs in `profile.ts` and `navbar.ts` now use the relative path returned by the backend instead of prepending `http://127.0.0.1:8000`
- Sidebar: Budgets and Accounts submenus not expanding on mobile — replaced Flowbite `data-collapse-toggle` with Angular signal-based state (`budgetsOpen`, `accountsOpen` in `SidebarService`)
### Added
- `nginx.conf`: reverse proxy config — serves Angular static build, proxies `/api/` to Gunicorn on port 8000, serves `/media/` as static files
---
## [1.0.0] - 2026-04-13
### Added
- Branding: app renamed to "Armarium"; browser tab title updated
- Branding: `Logo_horizontal.svg` in navbar, `Logo_vertikal.svg` on login/register, `Icon.svg` as favicon
- Branding: navbar logo inverts colors in dark mode via `dark:invert`
- Responsive layout: mobile-first redesign across all pages
- Responsive layout: sidebar becomes a slide-in overlay drawer on mobile (hamburger in navbar)
- Responsive layout: mobile backdrop closes sidebar on outside click or navigation
- Responsive layout: main content padding reduced on mobile (`p-4 lg:p-8`), sidebar margin shifted to `lg:` breakpoint
- Responsive layout: tables (accounts, expenses, transactions) get `min-w` and `overflow-x-auto` for horizontal scroll on mobile
- Responsive layout: notifications panel becomes a full-width top drawer on mobile (`fixed top-[57px] left-0 right-0`), dropdown on desktop
- Responsive layout: desktop sidebar toggle button hidden on mobile (`hidden lg:flex`)
- Database: migrated from SQLite to PostgreSQL; connection configured via `.env` variables (`DB_NAME`, `DB_USER`, `DB_PASSWORD`, `DB_HOST`, `DB_PORT`)
- Database: added `psycopg2-binary` as PostgreSQL adapter; added `.env.example` for onboarding
- i18n: full 4-language support (DE/FR/IT/EN) via ngx-translate; translation files in `assets/i18n/`
- i18n: language selector in Profile settings, persisted to backend `Profile.language` field and `localStorage`
- i18n: browser language auto-detection on Login and Register pages
- i18n: `LanguageService` for centralised language init, detection and switching
- i18n: canton names translated per UI language via `canton_names` keys; ZH = "Zürich" (DE), "Zuerich" (EN), "Zurigo" (IT), "Zurich" (FR)
- Calendar year view: clicking a month card opens a modal with an enlarged month view
- Sidebar collapse toggle in navbar: sidebar can be collapsed to icon-only view and expanded again
- Collapsed sidebar: flyout submenus for Budgets and Accounts with backdrop to close on outside click
- Collapsed sidebar: tooltips on hover for all navigation icons
- Dark/Light mode toggle in navbar with sun/moon icons; preference persisted in localStorage (dark mode default)
- iCal feed: `GET /api/calendar/ical-url/` returns personal iCal feed URL per user
- iCal feed: `GET /api/calendar/ical/<user_id>/<token>/` serves `.ics` file with deadlines and expense due dates (HMAC-SHA256 token auth)
- Calendar: "Subscribe" button in header opens popup with iCal feed URL and copy button
### Changed
- Calendar day detail: replaced bottom panel with a slide-in drawer from the right side
- All UI strings replaced with i18n translation keys (DE/FR/IT/EN) across all pages and components
### Fixed
- Profile: canton and language selects not saving — replaced `[value]`/`(change)` with `[ngModel]`/`(ngModelChange)` for reliable Angular select binding
- Calendar: canton selector not reflecting the profile canton on load
- Sidebar collapse button was not wired to any action
- Sidebar flyout submenus were clipped inside collapsed sidebar due to `overflow-y-auto`
- iCal feed: removed unused `pytz` import that caused `ModuleNotFoundError`
- i18n: translations not loading due to `src/assets` missing from `angular.json` asset sources
---
## [0.1.0] - 2026-03-08
### Added
#### Backend (Django)
- Initialized Django project with `core` configuration app and `finance` app
- Configured SQLite3 as development database
- Integrated Django REST Framework with JWT authentication via `djangorestframework-simplejwt`
- Enabled JWT token blacklist for secure logout and token rotation
- Configured `django-cors-headers` to allow requests from Angular frontend (`localhost:4200`)
- Implemented rate limiting: 5/min (auth), 200/min (authenticated users), 20/min (anonymous)
- Configured environment variables via `python-dotenv` (SECRET_KEY, DEBUG, ALLOWED_HOSTS, CORS)
- Added media file handling for avatar image uploads
#### Data Models
- `Account` asset, expense, and revenue account types with balance tracking
- `Transaction` double-entry bookkeeping with source and destination accounts
- `Budget` 7 categories (fixed expenses, mobile/internet, subscriptions, leisure, tax reserves, insurance, loans)
- `Expense` 10 categories with optional due date and notes
- `Profile` user profile with avatar (image + color), name, email, and canton (all 26 Swiss cantons)
- `Deadline` 5 types (tax, insurance, invoice, personal, other)
#### REST API
- `POST /api/auth/register/` user registration
- `POST /api/auth/token/` JWT login
- `POST /api/auth/token/refresh/` token refresh
- `POST /api/auth/logout/` logout with token blacklist
- `POST /api/auth/password/` password change
- `GET/POST/PUT/DELETE /api/accounts/` account management
- `GET/POST/PUT/DELETE /api/transactions/` transaction management
- `GET/POST/PUT/DELETE /api/budgets/` budget management
- `GET/POST/PUT/DELETE /api/expenses/` expense management
- `GET/POST/PUT/DELETE /api/deadlines/` deadline management
- `GET/PUT/DELETE /api/profile/` user profile with avatar upload
#### Frontend (Angular)
- Initialized Angular 21.2 project using standalone component architecture
- Integrated Tailwind CSS v3.4 for utility-first styling
- Integrated Flowbite 4.0.1 as UI component library (initialized via `initFlowbite()` in shell)
- Integrated ApexCharts 3.46.0 for data visualization
- Added `typings.d.ts` for Flowbite TypeScript module declaration
- Implemented `ApiService` for all HTTP communication with the Django REST API
- Implemented `AuthService` for JWT token storage and management
- Implemented `authGuard` to protect routes requiring authentication
- Implemented `authInterceptor` to attach Bearer token to all outgoing requests and handle 401 errors
- Configured `SidebarService` for sidebar open/close state management
#### Pages & Components
- Login and Register pages with JWT-based authentication
- Shell layout with responsive sidebar and navbar (user avatar, canton display)
- Dashboard with KPIs (total income, fixed costs, expenses, available balance, savings rate) and ApexCharts bar and donut charts
- Accounts page with full CRUD for asset, expense, and revenue accounts
- Budgets page with category grouping and budget suggestions per category
- Expenses page with category filtering and due date tracking
- Transactions page with double-entry transaction management
- Calendar page with year and month view, Swiss public holidays and school holidays by canton (20252026), expense due dates and personal deadlines
- Profile page with avatar upload, color selection, name, email, and canton selection