Skip to content

Bulma Banking Stack

  type: reference
  owner: bulma + trunks + jimmy-neutron
  first_touched: 2026-05-24
  last_touched: 2026-06-11

Dual-Provider OAuth Architecture

source: documentation/activities/reference_bulma-banking-stack.md

Rule: Bulma uses BOTH TrueLayer AND Monzo OAuth in parallel, not one or the other.

Why: Monzo's own API gives the cleanest data (categorised transactions, pots, merchant enrichment) but only covers Monzo accounts. TrueLayer covers everything else (Natwest, savings, credit cards). Both needed for net-worth visibility.

How to apply

  • CT109 bulma schema: tables include source ENUM('monzo','truelayer') + provider column

  • Credential storage: separate token files per provider in /mnt/data/hinata/data/bulma/ (Z2 bind mount). Vaultwarden (CT103, 192.168.1.250) is the canonical credential store.

  • CT109 systemd timers: bulma-poll-monzo + bulma-poll-truelayer, each every 15 min (*:0/15)

  • Never recommend "just use one provider" — deliberate design choice for data coverage

Status (2026-06-11)

Both providers live on CT109 bulma-finance (192.168.1.214). Monzo migration completed 2026-06-11. TrueLayer (NatWest credit) polling on CT109 since 2026-06-11. Mac is reauth surface only for both providers.

Five-Tab Rebuild — Producer-Iframe + React-Comparative Fusion

source: documentation/activities/bulma-tab-rebuild.md

Tab structure

  TabSourceRole

    Homeiframe(bulma-report-home.html)Full producer report minus Tactical Intelligence block
    WeeklyReact comparative panel + iframe(bulma-report-weekly.html)WTD vs Last Week + 7-day strip + BUDGETS table + producer HTML beneath
    MonthlyReact comparative + iframe(bulma-report-monthly.html)MTD vs Last Month + 3 KPI cards + BUDGETS ledger + producer HTML beneath
    YearlyReact-onlyYTD vs Last Year (producer has no yearly report)
    Behaviouriframe(bulma-report-behaviour.html)Full #ml-insights block

Architecture decision — splitter, not producer rewrite

report.py is 7,403 lines. Adopted: split-bulma-report.py post-processor reads producer output and injects tab-mask overlays. Producer is untouched.

Pipeline order

refresh-and-deploy-data.sh
  Step 0   build-tokens.py
  Step 1a  tasks-to-{memory,lanes}
  Step 1b  refresh-{tasks,session-burn}
  Step 1c  sync producer report*.html → public/data/bulma-report*.html
  Step 1c  run split-bulma-report.py
  Step 1d  refresh-bulma-comparative.py → bulma-comparative.json
  Step 2   md5 hash, deploy if changed

React comparative panel features

  • 7-day spend strip — mini bar chart, daily totals, today highlighted in var(--accent)

  • BUDGETS table — per-category target vs actual, status chip (OK/WARN/OVER), delta %

  • Pace projection (Monthly only) — pace = MTD / proRata

  • 3 KPI card row (Monthly) — MTD spend + full-month pace + vs previous month

Failures + lessons

1. CSS `@import` order bug — import must precede every other rule or browsers silently drop it

2. Squidward palette as accent reverted — "palette" was ambiguous; base stayed (slate), accent reverted to Hinata orange

3. Dashboard.tsx mid-session strip + restore — strip operations need git tags or backup comments

4. Pipeline step ordering — splitter must run AFTER syncing weekly/monthly HTMLs

5. Producer hex categorisation drift — some Monzo transfers tagged general not transfer; accepted

Open

  * Producer report.py 7,400 inline hexes still in warm-orange/indigo palette — migration backlogged

  * report_monthly.html last regenerated April — stale until June 1st

  * ~50 `var(--accent, #4a9eff)` fallback hexes in MusicMastery/SolfegeView are dormant violations; sweep deferred

Monzo Live Transactions

source: documentation/activities/bulma-monzo-slice1.md

Monzo polling runs on CT109 bulma-finance (192.168.1.214) as systemd timer bulma-poll-monzo, every 15 minutes (OnCalendar=*:0/15). Migration completed 2026-06-11. First CT109 poll: 2 accounts, 40 transactions.

Data plane

Z2 host /mnt/data/hinata/data/bulma bind-mounted into CT109 at /root/data/bulma. Tokens live in the bind mount; Z2 is the sole token writer. CT109's refresh rotates the chain.

Collector endpoint

http://192.168.1.153:8090/bulma (LAN). Host Tailscale IP unreachable from LXC; poll-monzo.py uses LAN only.

Mac role

Reauth surface only. reauth-monzo.py --force writes to ~/Sandpit/hinata-sandpit/data/bulma/, then push-monzo-tokens.sh copies to Z2. Mac tokens_monzo.json deleted after first CT109 poll; monzo_credentials.json stays for next reauth.

Monzo SCA gate

After browser OAuth, API calls return HTTP 403 until approval is tapped in the Monzo app. Not a token failure. Poll succeeds on next fire (within 15 min) once approved.

Credential discipline

  • Never read or print token values — only lengths, expiry epochs, and "refresh ok" event types

Slice 2 (deferred)

UI wire-up — Bulma.tsx live-transactions panel reading /data/bulma-monzo.json.

Fees & Interest Panel Shipped

source: documentation/activities/bulma-fees-panel.md

#840044 — Natwest-style accumulated fees + interest panel on Bulma Monthly tab. 2026-05-26 evening.

Files

          * `src/lib/feeInterestDetector.ts` — pure 220-line detector with word-boundary regex + category whitelist

          * `src/components/Bulma.tsx` — new `FeesInterestPanel` wired into Monthly tab top

          * `public/data/bulma-monzo-fixture.json` — 24 synthetic txs for DEV mode fallback

Verification logic

            * Word-boundary regex on fee patterns (FEE, INTEREST, CHARGE, OVERDRAFT, PENALTY, LATE PAYMENT, etc.)

            * Category whitelist: charges, fees, interest, overdraft

            * Only debits (amount < 0) count as fees-paid

            * Multi-account merge by source field

Pre-req status

  • Monzo polling live on CT109 (2026-06-11)

  • TrueLayer polling live on CT109 (2026-06-11)

    ◆ hinata · bulma-banking-stack · folded from documentation/activities/ phase-19