This repository has no description
Gleam 82%
Python 6%
Erlang 4%
TypeScript 3%
CSS 3%
JavaScript 2%
Dockerfile <1%
Shell <1%

README.md

Cacheboard #

Cacheboard is a finance dashboard for one household that you run on your own machine. It brings accounts, monthly budgets, cash flow, investments, and retirement planning into one browser app.

Features #

  • Accounts and net worth: combined balances, assets, debts, balance history, and account dates. Disabled accounts are excluded from totals.
  • Monthly budget and cash flow: a main tab with a month picker, income, spending, net cash flow, category targets, and 12 months of history. Month links support reload and browser Back. The current month is marked as month to date.
  • Budget editing: add or remove categories, enter dollar targets, use recent spending as a starting point, save, or discard. Targets repeat each month; edits stay in place across tab changes and background refreshes.
  • Transactions: search, filter by account, category, date, or money in/out; sort, change categories, and export CSV. Transfers are marked and excluded from spending and income totals.
  • Bank data: SimpleFIN imports balances and transactions, avoids duplicates, matches pending transactions, and applies category rules. Deployment mode checks for stale data when the app opens. Connection setup uses terminal commands.
  • CSV imports: preview and import transactions or investment holdings. The browser expects named CSV columns; custom column mappings use the API.
  • Investments: holding values, portfolio shares, and gains or losses against purchase cost, with dates. Holdings and prices come from imported data.
  • Retirement planning: multiple income streams, employer matches, spending phases, and tax, inflation, and return assumptions. The Agent can run repeated market simulations and compare contribution changes. There is no separate retirement-planning tab.
  • Home estimate: a manual value, date, source, and link included in net worth.
  • Agent workspace: saved conversations with charts, tables, spending comparisons, and retirement results. Finance tools calculate the numbers; the model explains them. A model server is required for chat.
  • Local operation: Docker or a source checkout, a local SQLite database, and a browser UI with small-screen layouts and dark mode.

Rough comparison: Mint and Personal Capital #

Mint is the reference here for everyday spending and budgets, using Intuit's published Mint feature list. Personal Capital is the reference for wealth tracking and planning; its tools are now part of Empower Personal Dashboard. This is a rough comparison of the tools available in each app.

  • Daily spending (Mint) — built: transaction review, category budgets, monthly spending, income, and net worth. Cacheboard's budget tab puts these monthly checks together. Subscription tracking and automatic spending alerts are TODO gaps.
  • Wealth tracking (Personal Capital) — partial: account totals, net worth, holdings, allocation, and retirement calculations are built. Dedicated planning screens and deeper portfolio analysis are TODO gaps compared with Empower's tools.
  • Local ownership — on purpose: you run the app and hold its database. There is no hosted Cacheboard account, product-offer feed, or adviser sales flow. Bank sync still uses SimpleFIN; chat uses the model endpoint you choose.
  • Read-only financial assistant — on purpose: the Agent can inspect and explain data and run scenarios. It cannot change your records, move money, trade, or choose for the household. The app's own forms allow data edits.
  • Manual home value — on purpose: the owner supplies the estimate and its source. Cacheboard does not scrape property sites.
  • Budget depth — TODO gap: targets repeat across all months. There are no month-specific target versions, rollovers, savings goals, or upcoming-bill forecasts. These limits are not a finished budgeting philosophy.
  • Editing tools — TODO gap: individual category edits are in the UI; rule management and bulk transaction edits are API-only. Custom category management and transaction splitting have no UI.
  • Access and setup — TODO gap: one household dataset, no sign-in, no separate member permissions, and no native mobile app. Bank linking and account setup are less complete than a hosted consumer app. USD is the only supported currency for bank sync.

Missing features and unresolved scope are listed in the feature TODO list. A TODO is not a delivery promise.

Code layout #

Cacheboard is built with Gleam. The API runs on Erlang/OTP with Mist, Wisp, and SQLite. The browser app uses Lustre and compiles to JavaScript. Money is stored and sent as integer cents; the server listens locally by default.

The repository contains three Gleam projects:

  • shared/ holds wire records, JSON encoders, finance semantics, and retirement projection math. It compiles to Erlang and JavaScript.
  • server/ owns SQLite through one serializing actor, runs migrations, exposes the HTTP API, serves the production browser bundle, and provides CLI commands.
  • web/ is the Lustre application. Chart.js is pinned and bundled locally.

Product direction and the evidence behind it are documented in docs/product-research.md. Architecture and local model research are documented in docs/research-context.md.

Run with Docker #

Docker is the portable deployment path. It builds a fixed browser bundle and Erlang shipment, runs as a non-root user, keeps all mutable data in one volume, and exposes a container health check.

Docker is an alternative installation, not this Mac's production service. Its commands are not part of the native Mac verification checks. On this Mac, set CACHEBOARD_PORT=4302 in .env before starting Docker so it cannot compete with production on port 3000. Use port 4302 in the browser for that setup.

cp .env.example .env
docker compose build
# For an existing configured volume:
docker compose up -d

Open http://127.0.0.1:3000. Deployment mode requires an existing database at /data/cacheboard.sqlite with saved household settings. A missing file or an empty household stops startup; it cannot open onboarding or create a replacement.

For a new, empty installation only, stop the service and run setup on loopback in development mode, finish the household form, then stop it and start deployment mode:

docker compose stop cacheboard
docker compose run --rm --service-ports -e CACHEBOARD_ENV=development cacheboard
# After setup, stop the foreground process with Ctrl-C.
docker compose up -d

Do not use this setup command to repair an existing production installation. A missing household needs a local check of the configured volume and database. Never run docker compose down -v against household data; -v deletes the volume.

Settings also accepts an optional home estimate with its as-of date, source, and link. The value is stored locally and included in net worth; Cacheboard does not scrape Zillow.

The Compose port binds to loopback by default. On a trusted local network, set CACHEBOARD_BIND=0.0.0.0 in .env. The named cacheboard-data volume survives container replacement. SQLite requires one running Cacheboard instance and a local persistent volume; do not place its WAL-mode database on NFS or another network filesystem.

The container reaches a model server on the host through host.docker.internal. The model server must listen on an interface reachable from Docker, or LM_BASE_URL must name another reachable machine.

Source requirements #

The verified toolchain on this Mac is managed by mise:

  • Gleam 1.18.1
  • Erlang/OTP 28.4
  • Bun 1.4.0, used by Lustre's production bundler

Run all commands below from the repository root unless a block says otherwise. Install mise, Python 3.11 or newer, Git, and Apple's command-line developer tools (xcode-select --install on a new Mac). Playwright also needs Node.js on PATH. The app tool versions are pinned in .mise.toml; Python and Node are host tools.

Install and check the selected tools:

mise trust .mise.toml
mise install
mise current
mise exec -- gleam --version
mise tasks ls

Seed and run #

Start the live-reloading browser app and a separate sample API:

mise run dev

Open http://127.0.0.1:5173. Its API proxy targets 127.0.0.1:4300. Both listeners bind only to loopback. The command builds current code, creates a temporary sample database, and starts both processes. Ctrl-C stops them and removes that sample database. Restart this command after server-code changes; browser-code changes reload through Lustre.

The sample contains six accounts, 468 transactions, 216 monthly balance snapshots, budgets, holdings, rules, and settings, dated 2026-07-30. The command clears inherited app, bank, and model environment variables before setting its own sample paths. It does not copy bank credentials. Chat needs a model server at the default local endpoint.

To check startup and the development proxy, then exit automatically:

mise run dev --check

Development refuses occupied ports. Build commands also refuse to overwrite web/dist if the installed production service still points there.

Production on this Mac #

Production is the macOS LaunchAgent com.cacheboard, listening on 127.0.0.1:3000. Its two addresses are:

ops/macos/production.json is the source of truth for the service file, URLs, port, database, data directory, release directory, logs, mise, and Tailscale CLI. It contains paths only, with no finance data or credentials. Do not search for other databases or pick one by size or date.

This Mac: deploy and verify #

Show the saved production identity and the installed release without reading household records:

mise run prod:show

The installed service uses the checkout's server/build/erlang-shipment and web/dist when show reports checkout build (no release ID). Builds in those locations can affect the live app. Use the isolated release build below. A release installed through this command has a fixed commit ID and its own server and browser files under the release directory; editing the checkout cannot change those files.

Build a reviewed, committed checkout into a new absolute directory:

mise trust .mise.toml
mise run prod:build /tmp/cacheboard-candidate
python3 ops/macos/production.py render /tmp/cacheboard-candidate > /tmp/cacheboard-candidate.plist
plutil -lint /tmp/cacheboard-candidate.plist

mise run prod:build (also ./deploy.sh) builds committed application source in a temporary directory using mise, then writes a release with file checksums and its Git commit ID. It never starts a service, creates a household database, or writes the checkout's live build folders. It refuses uncommitted changes or an existing destination. The release has server code, migrations, public assets, and tool versions only. Python 3 is required for the release tools.

Local operator command: after checking the household in your own browser, install the reviewed release from a local terminal:

mise run prod:install /tmp/cacheboard-candidate

Installation checks that the installed service still names the recorded data paths, checks for saved household settings, and makes a private SQLite backup in the data directory's backups/ folder. SQLite's backup API includes committed writes from its WAL file (the file holding recent writes). The installer copies the release into its fixed directory, preserves extra service environment settings, and restarts the existing LaunchAgent through mise. It does not seed, move, replace, or restore the household database. Server startup runs schema migrations only after the existing-household check passes.

A service reload retries launchd error 5 for up to 15 seconds because unloading can return before the old process exits. The same retry applies to recovery. A local startup or asset-check failure restores the prior service file and checks its health after restarting it. If recovery fails, the command reports both failures; it does not claim success. The exact prior file is saved as .plist.previous beside the service file. Database rollback is never automatic: schema changes can require a local operator to review the backup before restoring it. A Tailscale-only failure is reported while the healthy local release stays running.

Check both URLs after installation:

mise run prod:verify /tmp/cacheboard-candidate

This checks /healthz for the exact commit ID and checks every public asset against the release checksums. It never calls finance APIs. A health check alone does not establish that the expected household is present; that check stays in the local operator's browser.

Onboarding and data boundaries #

  • Setup opens only for the explicit SETTINGS_NOT_CONFIGURED API response in a new development or test household. Generic 404s, server errors, and network failures show a settings error and cannot load setup defaults.
  • A household that has loaded or saved settings stays out of setup for that page session. Old setup links return to Overview. Reload reads the saved settings.
  • Deployment mode requires an explicit absolute CACHEBOARD_DB path. SQLite opens it with mode=rw, which cannot create a file. Missing files, invalid databases, and missing household settings stop startup before migrations. Missing settings during a running deployment return HTTP 503, not onboarding.
  • Coding agents use source, service identity, release metadata, public assets, and synthetic fixtures. They must not open real databases, backups, bank credentials, finance APIs, or the production app in a browser. They may build, render, and verify releases; the local operator runs install.
  • These agent rules are workflow limits. The app has no sign-in; filesystem and network access must be restricted outside the app to enforce data isolation.

Service and Tailscale recovery #

Use python3 ops/macos/production.py show for the exact saved paths. The service runs in gui/$(id -u), starts at login, and is kept alive by macOS. Service commands use /opt/homebrew/bin/mise exec -- because launchd does not load an interactive shell or add Erlang to PATH.

Use the main Tailscale app's CLI:

/Applications/Tailscale.app/Contents/MacOS/Tailscale serve status

Its saved HTTPS route forwards to http://127.0.0.1:3000. If that route is absent, the local operator can restore it with:

/Applications/Tailscale.app/Contents/MacOS/Tailscale serve --bg http://127.0.0.1:3000

Preserve unrelated routes. The Homebrew Tailscale daemon and ~/.config/tailscale-kimi/tailscaled.sock belong to Kimi at kimi.tailcb772b.ts.net. The bare tailscale command selects that CLI, so use the full app path above. Cacheboard uses tailnet-only Serve. Do not enable Funnel.

Separate development and browser tests #

Use mise run dev for current code and temporary sample data. Its frontend proxy targets sample port 4300. Production owns port 3000.

The browser fixtures create sample databases and choose free ports. To test a release without rebuilding the live checkout's assets:

mise run test:browser --assets /tmp/cacheboard-candidate/public

Each fixture explicitly sets its temporary database path, overriding inherited configuration. Production-mode browser tests use synthetic households only.

Production-like sample app #

Run the packaged server and browser files in deployment mode with a temporary sample household:

mise run prod:like /tmp/cacheboard-candidate

Open http://127.0.0.1:4301. Ctrl-C stops the server and removes its sample data. There is no live reload or Tailscale route. The command validates the release checksums and serves the packaged files. Deployment mode requires the seeded household and refuses a missing database, just as production does.

mise run prod:like /tmp/cacheboard-candidate --check
mise run test:release /tmp/cacheboard-candidate

The first command checks startup and assets, then exits. The second checks restart preservation and missing-data errors. Browser tests with --assets use the current checkout's server and the supplied browser bundle; the release test and production-like app use the packaged server.

Frontend structure #

The browser app keeps its root model-view-update wiring in web/src/cacheboard/app/. Each route has a module under web/src/cacheboard/pages/, with larger views split into feature-local submodules. Shared visual elements live in web/src/cacheboard/components/ and web/src/cacheboard/ui/; API decoders and encoders live in web/src/cacheboard/api/.

The existing design system is in web/assets/styles.css. Add small, composable layout or spacing adjustments with Tailwind utilities in Gleam class lists. web/src/cacheboard_web.css is the Tailwind entry point, and Lustre builds it alongside the JavaScript bundle.

Build and test #

These root commands are defined in .mise.toml and ops/workflow.py:

mise run build
mise run test
(cd web && mise exec -- bunx playwright install chromium)
mise run test:browser

The focused tests cover the exact mulberry32 sequence on Erlang and JavaScript, frozen retirement projection output, the bounded agent loop, the frozen tool catalog and model-facing tool-result format, transaction filters and mutations, CSV deduplication and atomic imports, recorded SimpleFIN payloads, GFM configuration, and markdown sanitization.

The browser tests start separate local servers with temporary empty or seeded sample databases. They cover setup, saved-household route guards, server errors, and production mode. They find controls by visible roles, names, and labels.

Deployment tests cover release integrity, fixed service/data paths, private backups, and recovery after a simulated service failure:

python3 -m unittest discover -s ops -p 'test_*.py'
mise run test:release /tmp/cacheboard-candidate
mise run test:launchd

The release smoke test starts the packaged server twice on a free loopback port, checks its commit and public assets, compares sample settings and transactions across the restart, checks HTTP 503 when sample settings disappear, and confirms that a missing database stops startup. It uses a temporary synthetic household and removes it when done.

test:launchd requires macOS and a logged-in GUI session. It creates a disposable service that takes three seconds to stop and checks that reload succeeds. It never unloads production. test:browser builds first unless --assets is supplied. The lower-level web script bun run test:e2e only runs Playwright; it does not build assets.

Keeping these instructions accurate #

Change the command implementation, its tests, and this README in the same commit. Run the affected command with sample data before calling it verified. Keep production paths in ops/macos/production.json and tool versions in .mise.toml. Use mise tasks ls to list commands; use prod:show to identify the installed release. Documentation cannot guarantee future tool behavior; the checked-in commands and repeatable checks are the maintenance contract.

SimpleFIN #

These are local-operator commands; they access bank data. Use the exact data directory and database listed by mise run prod:show. Do not run them in a sample environment or leave their paths implicit. For this Mac:

cd server
export CACHEBOARD_DATA_DIR='/Users/avery/Library/Application Support/Cacheboard'
export CACHEBOARD_DB='/Users/avery/Library/Application Support/Cacheboard/cacheboard.sqlite'
SIMPLEFIN_SETUP_TOKEN='…' mise exec -- gleam run -m cacheboard/cli/simplefin_claim
mise exec -- gleam run -m cacheboard/cli/simplefin_verify

The access URL is stored as simplefin-access.json in the data directory with mode 0600. In deployment mode, each page load checks the last successful SimpleFIN update. Saved dashboard data appears at once, and a new update runs in the background when the last one is at least 12 hours old. A completed update reloads the dashboard data.

Synchronization is deployment-only. It can also be run by hand:

CACHEBOARD_ENV=deployment mise exec -- gleam run -m cacheboard/cli/simplefin_sync

Run that command in the same local terminal and server/ directory as the explicit exports above. Close that terminal afterward so other commands do not inherit production paths. Manual sync uses its own database connection; do not run it alongside an import or another sync.

The manual command asks how to classify new remote accounts. Automatic updates classify clear account names, including spending, checking, savings, credit, investment, and loan accounts. An unclear new account stops the automatic update with an error so it can be classified with the manual command first.

Synchronization accepts USD accounts, updates balance snapshots, deduplicates remote transactions, replaces matching pending transactions, applies categorization rules, and detects cross-account transfers.

Configuration #

Variable Default Purpose
CACHEBOARD_DATA_DIR data/ relative to the working directory Directory holding the database and SimpleFIN credentials
CACHEBOARD_DB Development: $CACHEBOARD_DATA_DIR/cacheboard.sqlite; deployment: required Explicit absolute existing database path in deployment
CACHEBOARD_STATIC_DIR ../web/dist when run from server/ Built browser bundle directory
CACHEBOARD_HOST 127.0.0.1 Listener address; loopback always listens too (tailscale serve)
PORT 3000 HTTP port
CACHEBOARD_TODAY 2026-07-30 Synthetic seed date
CACHEBOARD_ENV unset deployment requires an existing household and enables SimpleFIN sync; development allows initial setup and seeding
CACHEBOARD_RELEASE development Commit ID returned by /healthz; set by the release installer
SIMPLEFIN_SETUP_TOKEN unset One-time SimpleFIN claim token
LM_BASE_URL http://localhost:1234/v1 OpenAI-compatible local model endpoint
LM_MODEL ternary-bonsai-27b-mlx Local model
LM_TEMPERATURE unset (provider default) Sampling temperature for the agent loop; do not set to 0
LM_TURN_TIMEOUT_MS 300000 Maximum total assistant-turn time

There is no authentication. Keep the default loopback listener unless access is restricted by the deployment environment.

Repository hygiene #

Real financial data and credentials live outside the repository. .gitignore and .dockerignore exclude environment files, databases, SQLite WAL files, SimpleFIN credentials, dependency caches, and build output. .env.example contains names and safe defaults only. The synthetic seed is development-only and exits unless CACHEBOARD_ENV=development is set explicitly.