1
0
Fork
You've already forked finance-dashboard
0
A personal finance dashboard I built for myself. Uses SimpleFIN for account connectivity. Includes a LTI module for tracking Long-Term Incentive program grant/vesting and AI-supported transaction categorization.
  • Python 76.7%
  • TypeScript 21.1%
  • CSS 1.2%
  • JavaScript 0.6%
  • Rust 0.2%
  • Other 0.2%
David Turley ff5c340d13
Some checks failed
Tests / test (push) Has been cancelled
Desktop: negotiate the backend port so a busy 8765 can't wedge the app
The bundled app hard-bound the FastAPI sidecar to 127.0.0.1:8765 and the
webview hard-coded that same URL. If any unrelated process was already
holding 8765, the sidecar died on "address already in use" (silently —
only an eprintln), the webview's /health poll hit the stranger instead,
and the "Starting Finance..." splash spun forever. That's the startup hang.
Fix the whole path so a port collision degrades gracefully instead of
hanging:
- Tauri shell (lib.rs): negotiate a free port at launch (prefer 8765,
 scan upward, OS-assign as last resort), hand it to the sidecar via
 FINANCE_API_PORT, and expose the resulting base URL to the webview via
 a new `api_base` command. Add a `backend_alive` command (try_wait on
 the child) so the UI can tell "still booting" from "died".
- Frontend (http.ts): resolve the API base at runtime (VITE_API_BASE >
 Tauri `api_base` > 127.0.0.1:8765 default) instead of a build-time
 constant; all requests await it.
- Health check now verifies a distinctive `service` marker in
 GET /health (added in api/main.py) so a different server answering 200
 on the same port is no longer mistaken for our backend.
- App gate fails loud: poll health, but give up (error screen with a
 Retry) once the shell reports the sidecar exited or a deadline passes,
 instead of an eternal spinner.
- Settings shows the resolved endpoint rather than a hard-coded URL.
Frontend typechecks; cargo check is clean; /health marker verified via
the API test client.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026年07月15日 22:06:58 -07:00
.forgejo/workflows test: CI coverage flags + 6 new smoke tests for Holdings + Giving sections 2026年05月19日 00:07:04 -07:00
api Desktop: negotiate the backend port so a busy 8765 can't wedge the app 2026年07月15日 22:06:58 -07:00
data feat(plan): cash vehicle comparison + strategy menu 2026年05月19日 15:01:00 -07:00
desktop Desktop: negotiate the backend port so a busy 8765 can't wedge the app 2026年07月15日 22:06:58 -07:00
docs/screenshots docs: refresh README for the desktop app + current feature set 2026年07月05日 19:40:23 -07:00
scripts Desktop: PyInstaller one-dir backend + explicit sidecar teardown 2026年07月02日 16:17:19 -07:00
specs feat(import): CreditKarma backfill — historical net worth + transaction history 2026年05月22日 12:40:44 -07:00
tests Salvage transaction-importer dedup/id fixes (prevent silent row loss) 2026年07月02日 15:59:28 -07:00
.gitignore Bundle the backend as a Tauri sidecar for a standalone desktop app 2026年06月03日 22:10:33 -07:00
app.py fix(transactions): guard date filter against empty transaction set 2026年05月31日 10:55:34 -07:00
BACKLOG.md docs: import desktop UX-feedback backlog notes 2026年07月02日 15:59:29 -07:00
balance_synthesis.py feat(net-worth): synthesize cash + debt balance history from txn ledger 2026年05月20日 17:51:57 -07:00
cash_vehicles.py Bundle the backend as a Tauri sidecar for a standalone desktop app 2026年06月03日 22:10:33 -07:00
categorize.py Initial release 2026年05月06日 22:21:40 -07:00
CLAUDE.md Removes nonsense about "advice" 2026年06月05日 09:30:47 -07:00
concentration.py Bundle the backend as a Tauri sidecar for a standalone desktop app 2026年06月03日 22:10:33 -07:00
db.py Salvage shared-logic correctness & data-integrity fixes 2026年07月02日 15:59:13 -07:00
DESKTOP_OVERHAUL.md Add in-app paystub import (Taxes tab) 2026年06月15日 15:09:53 -07:00
explore.py Initial release 2026年05月06日 22:21:40 -07:00
findings.py feat(findings): PR 7 — Phase 2 polish (conversation export + history view + timestamps) 2026年05月19日 16:36:47 -07:00
forecast.py Desktop app overhaul: shell infra, per-screen fixes, new features 2026年06月15日 14:32:44 -07:00
giving.py Salvage shared-logic correctness & data-integrity fixes 2026年07月02日 15:59:13 -07:00
import_betterment.py Salvage transaction-importer dedup/id fixes (prevent silent row loss) 2026年07月02日 15:59:28 -07:00
import_costbasis.py chore: scrub PII from source/tests/specs 2026年05月20日 15:04:22 -07:00
import_creditkarma.py feat(creditkarma): enrichment loop wrapper + dedup-window for CK/OFX date drift 2026年05月25日 10:08:22 -07:00
import_csv.py Salvage transaction-importer dedup/id fixes (prevent silent row loss) 2026年07月02日 15:59:28 -07:00
import_lots.py feat(investment-efficiency): Phase 3 — lots + TLH + wash sale 2026年05月19日 00:24:24 -07:00
import_ofx.py Salvage transaction-importer dedup/id fixes (prevent silent row loss) 2026年07月02日 15:59:28 -07:00
import_paystubs.py fix(paystubs): capture Medicare Surtax + reconcile net from granular line items 2026年05月29日 09:04:49 -07:00
investment_efficiency.py Salvage shared-logic correctness & data-integrity fixes 2026年07月02日 15:59:13 -07:00
LICENSE Initial release 2026年05月06日 22:21:40 -07:00
llm_categorize.py Initial release 2026年05月06日 22:21:40 -07:00
loan_backfill.py feat(loan-backfill): amortization module for credit-report-extracted mortgage history 2026年05月22日 15:59:11 -07:00
lti.py Salvage shared-logic correctness & data-integrity fixes 2026年07月02日 15:59:13 -07:00
main.py Initial release 2026年05月06日 22:21:40 -07:00
manual_asset_backfill.py feat(net-worth): S&P-scaled investment history + per-asset interpolation + CK overshoot smoothing 2026年05月22日 14:26:54 -07:00
market_backfill.py Salvage shared-logic correctness & data-integrity fixes 2026年07月02日 15:59:13 -07:00
ORCHESTRATION.md Add native desktop app: FastAPI seam + React/Tauri UI over existing modules 2026年06月03日 21:55:01 -07:00
paystub_parser.py Salvage shared-logic correctness & data-integrity fixes 2026年07月02日 15:59:13 -07:00
paystub_preview.py feat(taxes): Phase-1 paystub review UI — PR 2 of specs/tax_planning.md 2026年05月18日 23:06:39 -07:00
paystub_validators.py fix(paystubs): capture Medicare Surtax + reconcile net from granular line items 2026年05月29日 09:04:49 -07:00
plan.py Salvage shared-logic correctness & data-integrity fixes 2026年07月02日 15:59:13 -07:00
pyproject.toml test: add pytest suite for db.py and lti.py (94% coverage) 2026年05月06日 22:35:21 -07:00
README.md docs: refresh README for the desktop app + current feature set 2026年07月05日 19:40:23 -07:00
recurring.py Desktop app overhaul: shell infra, per-screen fixes, new features 2026年06月15日 14:32:44 -07:00
requirements.txt feat(import): OFX/QFX backfill importer for pre-tracking transaction history 2026年05月20日 11:08:03 -07:00
scenarios.py feat(scenarios): PR 6.2 — config-resolution refactor; switching scenarios now changes tab rendering 2026年05月19日 16:28:41 -07:00
tax_projection.py Salvage shared-logic correctness & data-integrity fixes 2026年07月02日 15:59:13 -07:00
tax_tables.py feat(taxes): tax tables + projection module — PR 3 of specs/tax_planning.md 2026年05月18日 23:16:30 -07:00
welcome_grant.json.example Desktop app overhaul: shell infra, per-screen fixes, new features 2026年06月15日 14:32:44 -07:00

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

Holdings tab

Taxes — full-year multi-jurisdiction projection

Taxes tab

Cash Flow — forecast, recurring charges, savings rate, cash vehicles

Cash Flow tab

Findings — impact-ranked observations across every module

Findings tab

Plan — emergency fund, buckets, and FI trajectory

Plan tab

More tabs — Net Worth (light), Accounts, Transactions, Equity/LTI, Spending, Giving

Net Worth (light theme)

Net Worth tab

Accounts

Accounts tab

Transactions

Transactions tab

Equity / LTI

LTI tab

Spending

Spending tab

Giving

Giving tab

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/.dmg is 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.

  1. Sign up at https://beta-bridge.simplefin.org/.
  2. 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.
  3. Generate a Setup Token. It's a base64-encoded URL.
  4. Paste it into .env as SIMPLEFIN_SETUP_TOKEN.
  5. Run venv/bin/python main.py. On first run it claims the URL, saves the long-lived SIMPLEFIN_ACCESS_URL back to .env, and syncs 90 days of data into data/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 in data/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 (see data/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:

  1. Explicit-schedule grant (welcome_grant.json) — a single grant whose per-installment unit counts and vest dates are listed directly. Optional name field controls the section heading; defaults to "Welcome Grant".
  2. 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; once today >= 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=20 by default, configurable via vest_price() in lti.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:

  1. Manual edit — inline editable category, committed via Save, optionally creating a transaction_rules entry 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.
  2. Pattern rules (transaction_rules) — applied automatically on every sync to any uncategorized transaction.
  3. 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 plus excluded_from_net_worth and is_emergency_fund flags, and type/subtype overrides that survive sync
  • balances / historical_networth — daily balance per account; net-worth history
  • transactions / transaction_rules / transaction_overrides — the ledger, categorization rules, and the reversible manual-edit overlay
  • txn_link_groups / txn_link_members — linked/related transaction groupings
  • holdings / holding_classifications / lots / harvested_losses — positions, asset-class classifications, lot-level cost basis, and the TLH log
  • manual_assets — property, vehicles, other manually-tracked assets
  • paystubs / paystub_line_items — parsed ADP paystubs (sensitive)
  • buckets / bucket_allocations / bucket_snapshots — Plan-tab buckets
  • category_budgets — per-category monthly spending targets
  • donations / daf_accounts / daf_movements / pledges — Giving-tab data
  • findings — the impact-ranked observations feed
  • scenarios / scenario_configs — named alternative configurations
  • vest_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

  1. Fork or clone, set up .env and SimpleFIN as above.
  2. Run venv/bin/python main.py for the initial sync. Your accounts appear with whatever names your institutions report.
  3. db.py:classify_account() makes a best guess at asset/liability and subtype from account-name keywords. Override anything it gets wrong.
  4. The LTI module supports cash-settled equity-style plans. If you don't have one, leave the data/*.json grant files absent. If you do, copy the .example files and edit — the cadence, percentages, and pricing windows in the examples are placeholders; check your actual plan documents.
  5. 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. Copy data/tax_config.json.example and edit before using.
  6. 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.
  7. Concentration tracking is optional — leave data/concentration_config.json absent if you don't have concentrated employer-stock risk.
  8. The investment-efficiency seed covers common ETF tickers; the classifier falls back to keyword matching, and you can override any classification.
  9. Categorization is personal. Build rules organically in the Transactions screen, or copy data/seed_rules.json.example and edit before running categorize.py once.

Known limitations / friction

  • LLM integration is untested. The Streamlit AppTest smoke tests cover app.py, but llm_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 with python3.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=True is deprecated); currency columns via st.column_config.NumberColumn(format="dollar") (don't pre-format to strings — it breaks sorting).
  • CLAUDE.md mirrors this README for AI agents working on the codebase, with a more architectural lean.