- Python 76.7%
- TypeScript 21.1%
- CSS 1.2%
- JavaScript 0.6%
- Rust 0.2%
- Other 0.2%
Finance Dashboard
A personal finance dashboard with two front ends over one shared Python
backend: a native desktop app (React + TypeScript in a Tauri shell,
talking to a FastAPI seam) and the original Streamlit dashboard (app.py).
Both read the same local SQLite database and reuse the same financial-logic
modules — neither front end owns the logic.
It connects to your financial institutions through SimpleFIN, syncs balances, transactions, and investment holdings into a local SQLite database, and presents the picture across a dozen screens: Net Worth, Findings, Accounts, Transactions, Holdings, Equity/LTI (long-term incentive / equity comp), Spending, Cash Flow, Taxes, Plan, and Giving.
Built primarily for one user, shared as a starting point. See "Adapting it for yourself" below if you'd like to use it.
Guiding principle
This dashboard is built to help its user understand their financial situation and make their own decisions. It explains, models, and surfaces information — it does not give advice. Where features could be framed as recommendations, prefer framing them as comparisons, scenarios, or visualizations the user can interpret.
Screenshots
All screenshots use synthetic sample data — a fictional household, not anyone's real accounts. They show the layout and features, not real figures. The data comes from
scripts/seed_demo_db.py, which builds a throwaway demo database and never touches your realdata/finance.db.
The desktop app (React + Tauri) is the primary front end. It ships light, dark, and system themes:
Net Worth (dark theme)
Net Worth — desktop, dark theme
Holdings — fee audit, allocation drift, asset location, tax-loss harvesting
Taxes — full-year multi-jurisdiction projection
Cash Flow — forecast, recurring charges, savings rate, cash vehicles
Findings — impact-ranked observations across every module
Plan — emergency fund, buckets, and FI trajectory
More tabs — Net Worth (light), Accounts, Transactions, Equity/LTI, Spending, Giving
Net Worth (light theme)
Accounts
Transactions
Equity / LTI
Spending
Giving
The original Streamlit UI — same data, same logic, browser-based
The Streamlit dashboard (app.py) runs side-by-side against the same database.
These shots are regenerated by
scripts/regenerate_screenshots.py.
Streamlit — Net Worth Streamlit — Holdings Streamlit — Taxes
The two front ends
Both front ends are additive and run against the same data/finance.db. Pick
whichever you prefer; changes made in one (category edits, overrides, manual
assets, etc.) show up in the other after a refresh.
- Desktop app (
api/+desktop/) — a native app: a React/TypeScript UI in a Tauri shell talking to a thin FastAPI seam that wraps the existing Python logic. Shared design system (light/dark/system themes), a ⌘K command palette, global search, native notifications, and a Settings screen for pointing the app at a data directory. The bundled.app/.dmgis self-contained — it auto-starts the frozen Python backend as a sidecar, so there's no separate Python process to run. - Streamlit dashboard (
app.py) — the original browser-based UI. Nothing to build; run it and it opens in your browser. Good for quick local use and for developing/validating the shared logic.
The FastAPI seam adds no financial logic: each router in
api/routers/<screen>.py calls a service in api/services/<screen>.py that
wraps the same modules Streamlit uses (db.py, lti.py, tax_projection.py,
plan.py, giving.py, concentration.py, investment_efficiency.py,
findings.py, forecast.py, recurring.py, ...) and serializes their output
to JSON via Pydantic models that drive the typed front end.
Quick start (Streamlit)
Requires Python 3.14+ and a SimpleFIN setup token (see "Connecting SimpleFIN" below).
git clone https://codeberg.org/synath/finance-dashboard
cd finance-dashboard
python3.14 -m venv venv
venv/bin/pip install -r requirements.txt
# Set up your .env (see next section), then:
venv/bin/python main.py # one-time: claims SimpleFIN access URL, syncs initial 90 days
venv/bin/streamlit run app.py # opens the dashboard at http://localhost:8501
Running the desktop app
The desktop app needs Node (front end) and, for a native build only, the Rust
toolchain (Tauri). The backend runs from the same venv with fastapi +
uvicorn installed.
Development (two processes)
# 1. Backend API — serves the existing modules over HTTP on 127.0.0.1:8765
venv/bin/uvicorn api.main:app --reload --port 8765
# 2. Front end — Vite dev server (talks to the API above)
npm --prefix desktop install # first time only
npm --prefix desktop run dev
Point the app at data by setting FINANCE_DATA_DIR (and/or FINANCE_DB_PATH)
before launching the backend. With neither set, it uses data/finance.db like
Streamlit does.
To develop the UI against synthetic data (no real figures), seed the demo database first:
venv/bin/python scripts/seed_demo_db.py --force # builds a synthetic data/finance.db
Front-end housekeeping:
npm --prefix desktop run build # typecheck (tsc) + production build
npm --prefix desktop run lint # eslint
Native build (self-contained .app + .dmg)
# Freeze the Python backend as a PyInstaller sidecar, then bundle it with Tauri:
scripts/build_sidecar.sh && npm --prefix desktop run tauri:build
The bundled app resolves its data directory in priority order:
FINANCE_DATA_DIR env → a persisted pointer in the platform app-support dir
(editable from the app's Settings screen) → an app-support default (which
gets a synthetic demo seed on first run). It binds loopback only (127.0.0.1).
Environment variables (.env)
Create .env in the repo root with at minimum your SimpleFIN setup token:
SIMPLEFIN_SETUP_TOKEN=... # only needed once, to claim the access URL
SIMPLEFIN_ACCESS_URL=... # auto-written on first run of main.py — leave blank initially
ANTHROPIC_API_KEY=sk-ant-... # optional: LLM-assisted categorization (Anthropic)
OPENAI_API_KEY=sk-... # optional: LLM-assisted categorization (OpenAI)
LTI_TICKER=... # optional: ticker symbol for the LTI module (else read from welcome_grant.json)
The Streamlit app halts with a clear error at startup if neither
SIMPLEFIN_SETUP_TOKEN nor SIMPLEFIN_ACCESS_URL is set. AI categorization
keys are only needed if you use the "AI categorization suggestions" feature —
without them you'll see a warning instead of an error.
Connecting SimpleFIN
SimpleFIN is a third-party service that brokers a unified read-only API to your banks, brokerages, and credit cards. It's a one-time setup.
- Sign up at https://beta-bridge.simplefin.org/.
- Connect your financial institutions through SimpleFIN's own UI. Each connection takes a few minutes and may require MFA. SimpleFIN handles the institution-side authentication; credentials go directly to them, not to this dashboard.
- Generate a Setup Token. It's a base64-encoded URL.
- Paste it into
.envasSIMPLEFIN_SETUP_TOKEN. - Run
venv/bin/python main.py. On first run it claims the URL, saves the long-livedSIMPLEFIN_ACCESS_URLback to.env, and syncs 90 days of data intodata/finance.db.
After that, you don't need the setup token anymore. Future runs use the
access URL. Both front ends can re-run the sync in place: Streamlit has a
sidebar Sync Now button, and the desktop app has a Sync now action
(both mirror main.py's fetch-and-db.sync).
If your institutions predate your SimpleFIN connection, several backfill importers can pull older history from files you download yourself (OFX/QFX, CSV, Betterment year-end PDFs, cost-basis CSVs). See specs/data_import.md.
What's in each screen
- Net Worth — top-line totals (Net Worth, Total Assets, Total Liabilities), Assets/Liabilities breakdown by category, net-worth milestone tracker, per-account list, manual assets editor (property, vehicles, anything not in a connected account), and a net-worth trend over time.
- Findings — an impact-ranked feed of observations the dashboard has surfaced across modules (tax projection, plan, investment efficiency, concentration, giving). Each finding is framed as an observation, not a directive, with a status workflow (new / reviewed / snoozed / addressed / dismissed) and a one-click recompute. See The Findings feed.
- Accounts — every connected account grouped by institution, plus per-account detail (balance history, holdings, transactions, spending breakdown). An override editor fixes misclassified accounts and toggles the "Exclude from NW" flag for accounts you don't want counted in Net Worth.
- Transactions — full transaction list with category filter, spending/income direction filter, inline editable category, save-as-rule option, and an LLM-assisted categorization feature (Anthropic or OpenAI).
- Holdings — current investment holdings with cost basis and gain/loss, plus five analytical sections: Fee Audit, Allocation Drift, Asset Location, Tax Loss Harvesting, and Concentration. See Holdings analytics.
- Equity / LTI — cash-settled equity-comp tracking. See The LTI module.
- Spending — spending breakdown over a configurable date range, by category and by account.
- Cash Flow — forward monthly cash-flow forecast, detected recurring charges, savings-rate trend, and a cash-vehicle (where-to-hold-cash) comparison. See The Cash Flow tab.
- Taxes — full-year tax projection across federal / Oregon / Multnomah PFA / Metro SHS / FICA / NIIT, with safe-harbor flags, suggested estimated payments, scenario sliders, a charitable-giving comparison, an in-app tax- config editor, and the paystub ingestion + review pipeline. See The Taxes tab.
- Plan — emergency fund, named buckets, FI trajectory with a permission panel, and asset-allocation profile modeling. See The Plan tab.
- Giving — track giving as a practice: annual goal, donation log, year-over-year history, neutral strategy framings, plus DAF-account and multi-year-pledge tracking. See The Giving tab.
The desktop app adds a few cross-cutting conveniences on top of these screens: a ⌘K command palette, global search across transactions/holdings/ accounts/findings, per-category spending budgets with current-month actuals, and named scenarios (persisted alternative configurations you can switch between — "what if I configured this differently?").
Manual assets
For things SimpleFIN can't reach: your house, your cars, anything else with a value but no connected account. Add rows (Name, Type: property/vehicle/other, Value, Last Updated, optional Notes). They contribute to Net Worth, Total Assets, and the Assets-by-Category breakdown.
There's no auto-valuation: Zillow killed their public Zestimate API in 2021, KBB doesn't expose one for individuals, and the paid AVM providers are enterprise-priced. The realistic workflow is: bump the value when you have a fresh number, update Last Updated, save.
The Streamlit editor wipes-and-replaces the manual-asset table on save; the
desktop app uses per-asset add/update/delete with a soft-delete archived_at
so a removal is reversible (undo). The Net Worth Over Time chart shows your
synced-account trend with the current manual assets total added as a flat
offset (history for a manual value isn't tracked — bumping it overwrites the
prior number).
Excluding accounts from Net Worth
Some accounts you connect via SimpleFIN may report values you don't want counted as net worth. The motivating example: a brokerage account that shows the notional value of unvested equity awards — useful to see, misleading to sum into your "real" net worth.
Toggle Exclude from NW on the relevant account (via the Accounts override editor). The account stays visible (balance history, transactions, etc.) but stops contributing to Net Worth, the by-category breakdowns, and the trend chart. A caption under the Net Worth tile lists everything currently excluded so it never silently disappears.
The Findings feed
findings.py is a centralized, impact-ranked feed of what the dashboard has
noticed — safe-harbor gaps, threshold crossings, asset-location inefficiency,
allocation drift, an over-funded emergency fund, and so on. A finding is an
observation, not a directive.
Findings are produced by per-module "producers" (tax projection, plan,
investment efficiency, concentration, giving), deduplicated and ranked by
estimated impact, and stored in the findings table with a status you drive
(new → reviewed → snoozed → addressed → dismissed). Recompute rescans your
data on demand. See specs/findings.md.
Holdings analytics
In addition to the main holdings table, the Holdings screen has five analytical sections — all framed as comparisons / surfaces, not advice:
- Fee Audit (
investment_efficiency.py): AUM-weighted expense ratio across the portfolio, per-provider breakdown including advisory fees (Betterment 0.25%, Fidelity/Schwab/Vanguard 0%), top-cost holdings, and a per-symbol ER override editor. Seed data for common ETF tickers ships indata/holding_classifications_seed.json(committed). - Allocation Drift: current vs. target asset allocation, drift in percentage points and dollars, a configurable rebalance threshold, and a target-allocation editor with sum-to-1.0 validation.
- Asset Location: per-holding analysis of whether the asset class sits in a tax-efficient shelter (e.g., bonds in Roth, munis in taxable), an estimated annual tax drag, and comparison-style relocation scenarios. Auto-pulls the marginal rate from the Taxes projection when available.
- Tax Loss Harvesting: lot-level opportunities for taxable accounts.
Requires lot-level cost-basis data imported via
import_lots.py(seedata/lots_import_format.md). Shows unrealized loss, estimated tax savings, wash-sale risk flags, and replacement-fund suggestions from a curated family list. Includes a harvest log + wash-sale-alert table. - Concentration (
concentration.py): single-underlying exposure across direct holdings, unvested LTI (when the peg ticker matches), and salary present value (when enabled). Stress-test panel with adjustable price-decline % and salary-correlation slider. Config:data/concentration_config.json(gitignored; skeleton committed).
See specs/investment_efficiency.md and specs/concentration_risk.md.
Importing lots for the TLH section
# Reshape your broker CSV into the format documented at
# data/lots_import_format.md, then:
venv/bin/python import_lots.py path/to/lots.csv --account-id ACT-xxx
venv/bin/python import_lots.py --list # show import batches
venv/bin/python import_lots.py --delete-batch <id> # undo an import
The LTI module
The Equity / LTI screen tracks cash-settled equity-style awards — units that pay out at a configured ticker's share price at vesting, typically taxed as ordinary income. Originally built for a phantom-equity / SAR-style plan that pegs unit value to a public ticker; generalizable to similar structures.
If you don't have anything like this, leave the operator data files absent — the tab will just show empty sections.
Operator-specific data files (gitignored)
The LTI module reads two JSON files from data/. Both are gitignored; you
create them for yourself. Skeleton examples are committed as
data/welcome_grant.json.example and data/grants.json.example:
cp data/welcome_grant.json.example data/welcome_grant.json
cp data/grants.json.example data/grants.json
The module supports two grant shapes:
- Explicit-schedule grant (
welcome_grant.json) — a single grant whose per-installment unit counts and vest dates are listed directly. Optionalnamefield controls the section heading; defaults to"Welcome Grant". - Computed-schedule grants (
grants.json) — a list of grants whose vest schedules are generated from parameters: a grant value, a pricing window, a cliff, a number of vests, and per-vest percentages.
How pricing works
- Grant price (computed-schedule grants only): the average closing price
over a configurable window of trading days, ending on
price_window_end. While the window is open the dashboard shows a running estimate; oncetoday >= price_window_end, the price is finalized and frozen.total_units = grant_value / grant_price. - Vest price (all grants): each vest pays out at an n-day average closing
price ending on the vest date —
n=20by default, configurable viavest_price()inlti.py. For past vests this is the actual payout price (from history); for future vests it's the rolling n-day average as of today, used as the best-guess estimate. - Vest payment detection:
db.sync()scans a configured deposit account for credits within a window after each vest date and matches them to the expected net, so vested awards flip from "awaiting deposit" to "paid" automatically (reversible).
The Cash Flow tab
Forward-looking money-movement view, built from three modules:
- Forecast (
forecast.py): projects the next N calendar months by combining recurring income/outflows, detected recurring charges, and known scheduled items (e.g., upcoming LTI vests). - Recurring charges (
recurring.py): detects subscriptions and other repeating charges from the transaction ledger (cadence + amount clustering), so you can see what's on autopilot. - Cash vehicles (
cash_vehicles.py): a catalog + after-tax-yield comparison for where liquid cash lives (Cash Reserve, HYSA, checking), because tax treatment and rate differences on idle cash add up.
The desktop app also surfaces a savings-rate trend (monthly + trailing-12- month) alongside these.
The Taxes tab
Full-year tax projection across every jurisdiction the tool models: federal, Oregon, Multnomah Preschool for All (PFA), Metro Supportive Housing Services (SHS), FICA, and NIIT. Includes a paystub ingestion pipeline so the projection reads authoritative income figures rather than approximating from bank deposits.
See specs/tax_planning.md for the full design.
Paystub ingestion
ADP-format PDF paystubs are parsed into a local SQLite table.
Sensitive data — paystubs contain gross/net pay, full withholding
breakdowns, employer details, and depending on your employer may have an SSN
embedded in the source PDF. They stay local: data/pay_statements/ and the
paystubs / paystub_line_items tables are gitignored.
# Drop ADP-format paystub PDFs into data/pay_statements/.
venv/bin/python import_paystubs.py --report # parse + sanity-check, no DB writes
venv/bin/python import_paystubs.py # parse + stage for review
venv/bin/python import_paystubs.py --auto-verify # parse + auto-mark verified
venv/bin/python import_paystubs.py --reparse-all # rerun parser on stored paystubs
The CLI is idempotent (dedups by ADP voucher number). Each imported paystub
gets an internal-consistency check (gross_pay - sum(deductions) == net_pay).
Currently supports ADP paystubs; other payroll vendors would need a sibling
parser registered in paystub_parser.PARSERS. Once paystubs are in, the Taxes
tab's review section lets you verify each one (PDF preview + editable form +
sanity-check badges).
Tax tables + projection
2026 tax tables are hard-coded in tax_tables.py (sources cited in the module
docstring). The projection (tax_projection.py) reads verified paystubs +
remaining LTI vests + transaction-derived investment income to produce a
per-jurisdiction liability-vs-withholding gap, safe-harbor flags (110% of
prior-year tax), marginal rates, and quarterly estimated-payment suggestions.
Operator-specific config lives in data/tax_config.json (gitignored, skeleton
committed): spouse income, prior-year total tax, estimated payments paid YTD
per jurisdiction, itemize/standard toggle, payroll source. The tab also has a
charitable-giving comparison panel: model the after-tax cost of a hypothetical
donation via cash, appreciated securities, or DAF bunching.
The Plan tab
Forward-looking planning surface: emergency fund, named buckets, FI trajectory, permission panel, asset-allocation modeling. See specs/plan.md.
- Emergency Fund: target = essential monthly avg ×ばつ target months
(configurable). Essential categories are user-designated; emergency-fund
accounts are flagged via
is_emergency_fund. Includes a history chart with the target line overlaid. - Named Buckets: concrete near-term claims (roof, tax bill, car) with a target amount + optional target date. Allocate funds across real accounts; over-allocations are flagged. An unallocated-cash callout lists checking/savings cash not yet claimed by any bucket.
- FI Trajectory: FI Number from essential + discretionary spending, withdrawal rate, and tax drag. Years-to-FI via numeric root-finding on the compounding-with-contribution curve. Coast-FI status included.
- Permission Panel: how much later does FI hit if you spend $X more per month? Models both the higher target and the lower contribution — framing is explicitly "tradeoff visible, not values prescribed."
- Asset Allocation: five built-in profiles (cash_only / conservative / balanced / aggressive / all_equity) plus custom, with per-bucket projections.
Config lives in data/plan_config.json (gitignored; skeleton committed).
The Giving tab
Tracks giving as a practice. Annual goal as a fixed dollar amount or a percent of income (you pick the framing). Donation log with recipient, category, amount, method, tax-deductibility flag, and notes. History view: annual totals chart, year-over-year breakdowns by category and recipient. A neutral Strategy section introduces common giving frameworks (tithing, effective altruism, local/community, DAF bunching) as options to explore, not recommendations.
A second phase tracks donor-advised-fund accounts + movements and multi-year pledges with pace status. Complements the charitable scenario panel in the Taxes tab. See specs/charitable_giving.md.
Transaction categorization
Each transaction can have a category. Categories get assigned three ways:
- Manual edit — inline editable category, committed via Save, optionally
creating a
transaction_rulesentry for the payee. In the desktop app, manual edits are stored as a reversible overlay (transaction_overrides) that survives re-syncs and can be reverted to the rule-derived value. - Pattern rules (
transaction_rules) — applied automatically on every sync to any uncategorized transaction. - LLM-assisted — sends uncategorized transactions to Anthropic or OpenAI, shows suggestions for bulk accept/reject, optionally saving accepted ones as new rules.
The categorization seed file
data/seed_rules.json (gitignored) holds your starter rules; a skeleton lives
at data/seed_rules.json.example. Format is a list of
{pattern, field, category, priority} objects:
[
{"pattern": "uber eats", "field": "payee", "category": "Food: Delivery", "priority": 100},
{"pattern": "uber", "field": "payee", "category": "Transit", "priority": 50}
]
Higher priority wins; with equal priority, lower id (earlier insert) wins,
so order more-specific patterns before broader ones. Run
venv/bin/python categorize.py to seed the rules table and apply them.
Transfer detection
A subset of transactions are auto-labelled as transfers (and excluded from
spending breakdowns). The detector groups uncategorized transactions by
absolute amount, pairs opposite-sign rows across different accounts within 3
days, and labels the pair as Internal Transfer, Credit Card Payment, or
Investment Transfer based on the account subtypes. See detect_transfers()
in db.py.
Useful commands
# Streamlit front end
venv/bin/streamlit run app.py # run the dashboard
# Desktop front end (dev)
venv/bin/uvicorn api.main:app --reload --port 8765 # backend API
npm --prefix desktop run dev # Vite dev server
npm --prefix desktop run build # typecheck + build
scripts/build_sidecar.sh && npm --prefix desktop run tauri:build # native app
# Data
venv/bin/python main.py # sync latest data from SimpleFIN
venv/bin/python scripts/seed_demo_db.py --force # synthetic demo DB (no real data)
venv/bin/python categorize.py # seed rules + apply to existing transactions
venv/bin/python categorize.py --report # categorization coverage report (no changes)
venv/bin/python import_paystubs.py # ingest ADP paystubs (Taxes tab)
venv/bin/python import_lots.py [...] # ingest lot-level cost basis CSVs (TLH section)
venv/bin/python explore.py # ad-hoc DB exploration
# Quality
venv/bin/ruff check . # lint (Python)
venv/bin/pytest # run tests (Python)
Tests
tests/ currently holds 810 Python tests across 26 files, covering the
shared logic modules (db, lti, tax_tables, tax_projection, plan,
concentration, giving, investment_efficiency, paystub_parser,
paystub_validators, findings, forecast, ...), the paystub parser against
synthetic ADP-format fixtures (no PII, no real PDFs), and API-service +
Streamlit AppTest smoke coverage of the screens. All tests are isolated: fresh
temp SQLite per DB test, temp data/ per config test, no network calls, no
.env bleed-through. Add a tests/test_<module>.py and pytest discovery picks
it up.
The React/TypeScript front end is typechecked via npm --prefix desktop run build (tsc) and linted via npm --prefix desktop run lint (eslint).
llm_categorize.py is still untested (would need provider-API mocking).
Database
SQLite, at data/finance.db (override with FINANCE_DB_PATH, or point the
whole data/config directory with FINANCE_DATA_DIR). Migrations run
automatically on startup (initialize_db()), so adding a column or table is
just an edit to db.py plus a restart. Selected tables (see db.py for full
DDL):
accounts/account_overrides— accounts plusexcluded_from_net_worthandis_emergency_fundflags, and type/subtype overrides that survive syncbalances/historical_networth— daily balance per account; net-worth historytransactions/transaction_rules/transaction_overrides— the ledger, categorization rules, and the reversible manual-edit overlaytxn_link_groups/txn_link_members— linked/related transaction groupingsholdings/holding_classifications/lots/harvested_losses— positions, asset-class classifications, lot-level cost basis, and the TLH logmanual_assets— property, vehicles, other manually-tracked assetspaystubs/paystub_line_items— parsed ADP paystubs (sensitive)buckets/bucket_allocations/bucket_snapshots— Plan-tab bucketscategory_budgets— per-category monthly spending targetsdonations/daf_accounts/daf_movements/pledges— Giving-tab datafindings— the impact-ranked observations feedscenarios/scenario_configs— named alternative configurationsvest_payments— three-state LTI vest tracking (upcoming / vested / paid)
data/* is gitignored (with !data/*.example), so real balances, the DB, and
operator config JSON never enter git history.
Adapting it for yourself
- Fork or clone, set up
.envand SimpleFIN as above. - Run
venv/bin/python main.pyfor the initial sync. Your accounts appear with whatever names your institutions report. db.py:classify_account()makes a best guess at asset/liability and subtype from account-name keywords. Override anything it gets wrong.- The LTI module supports cash-settled equity-style plans. If you don't have
one, leave the
data/*.jsongrant files absent. If you do, copy the.examplefiles and edit — the cadence, percentages, and pricing windows in the examples are placeholders; check your actual plan documents. - The Taxes tab assumes a Portland-area resident (federal + Oregon +
Multnomah PFA + Metro SHS). Elsewhere, the federal + FICA + NIIT
computation still applies; state/local jurisdictions would need editing in
tax_tables.py. Copydata/tax_config.json.exampleand edit before using. - The Plan, Giving, and tax configs share
plan_config.json/tax_config.json; ship with reasonable defaults and edit via the in-UI editors or directly. - Concentration tracking is optional — leave
data/concentration_config.jsonabsent if you don't have concentrated employer-stock risk. - The investment-efficiency seed covers common ETF tickers; the classifier falls back to keyword matching, and you can override any classification.
- Categorization is personal. Build rules organically in the Transactions
screen, or copy
data/seed_rules.json.exampleand edit before runningcategorize.pyonce.
Known limitations / friction
- LLM integration is untested. The Streamlit AppTest smoke tests cover
app.py, butllm_categorize.py(Anthropic / OpenAI calls) is not yet mocked into the suite. Manual smoke tests are the current bar. - venv portability. Renaming the project directory breaks
venv/bin/*shebangs (they hard-code absolute paths). Fix by sed-rewriting them or rebuilding withpython3.14 -m venv venv && venv/bin/pip install -r requirements.txt. - Manual asset history isn't kept. Updating a value overwrites the prior one; the over-time chart adds the current total as a flat offset.
- Two front ends, one DB. A change to a shared logic module affects both Streamlit and the desktop app. Edits made in one front end show up in the other only after a refresh/re-fetch.
See BACKLOG.md for things on the radar but not yet built, and ORCHESTRATION.md for how the desktop port was built.
Project conventions
- Python 3.14+, Streamlit 1.57; React + TypeScript + Vite + Tauri for the desktop app.
- All database operations flow through
db.py. Sync pipeline order: accounts → apply overrides → balances → transactions → apply rules → detect transfers → apply transaction overrides → holdings. - The FastAPI seam (
api/) adds no financial logic — one service + router per screen, wrapping the shared modules and serializing via Pydantic. - Streamlit specifics:
width="stretch"on dataframes (use_container_width=Trueis deprecated); currency columns viast.column_config.NumberColumn(format="dollar")(don't pre-format to strings — it breaks sorting). CLAUDE.mdmirrors this README for AI agents working on the codebase, with a more architectural lean.