Skip to content

Releases: budgets-au/budgets

v0.335.0 — Cashflow: Tagged views toggle

Choose a tag to compare

@budgets-au budgets-au released this 13 Aug 23:48

Container

ghcr.io/budgets-au/budgets:0.335.0
ghcr.io/budgets-au/budgets:latest

Added

  • Cashflow report: Tagged views show/hide toggle. The virtual-rows section below Total Expenses (one row per category tag) can now be toggled off from the report's filter bar. Only surfaces when at least one tag has activity in the window — no dead switch when tags aren't in use. Default on, so upgraders see no change; flip off to declutter when tags aren't in daily rotation.
  • Persisted via new `cashflowShowTaggedViews` display-pref.

Verify

  • tsc clean, `pnpm build` clean, pages-smoke 12/12 green.

Upgrade

Pull `ghcr.io/budgets-au/budgets:0.335.0`. No migrations — UI-only pass on top of 0.334.0.

v0.334.0 — scoped API keys + health/logs + OpenAPI docs

Choose a tag to compare

@budgets-au budgets-au released this 13 Aug 22:37

Container

ghcr.io/budgets-au/budgets:0.334.0
ghcr.io/budgets-au/budgets:latest

Added

  • Per-key API scopes. New `scope` field on API keys — pick when minting:
    • Full access (`full`) — default. Access to every route the key's role permits.
    • Ops (`ops`) — restricted to `/api/health`, `/api/logs`, `/api/version-check`, `/api/github-stats`, `/api/openapi.json`. Perfect for an assistant / home-automation integration that only needs infrastructure telemetry — payee / amount / balance endpoints refuse with `403 {"error":"Scope does not permit this endpoint"}`.
  • `GET /api/health` — version, uptime, `dbUnlocked`, active profile, memory, node version.
  • `GET /api/logs` — new in-process ring buffer (max 1000 lines) capturing every `console.*` call. Query `?limit=N&level=warn,error`. Documented privacy caveat: don't console.log sensitive strings.
  • API docs at `/docs/api` — Scalar-rendered interactive reference generated from Zod schemas via `@asteasolutions/zod-to-openapi`. Spec served at `GET /api/openapi.json` (in the ops allowlist so scoped consumers can discover their own endpoints). v1 documents ~15 endpoints (ops + admin control-plane + API-key CRUD + core financial reads); rest still work, get added over time.

Schema

  • Migration `0019_api_keys_scope` — adds `api_keys.scope TEXT NOT NULL DEFAULT 'full'`. Existing rows inherit `full` — behavioural no-op for old keys.

Verify

  • tsc clean, `pnpm build` clean, 751/761 unit pass (+10 vs 0.333), pages-smoke + proxy-auth 16/16 green.

Upgrade

Pull `ghcr.io/budgets-au/budgets:0.334.0`. Migration 0019 runs on first unlock. Existing keys keep working (`full` scope preserved).

v0.333.0 — API keys for programmatic access

Choose a tag to compare

@budgets-au budgets-au released this 13 Aug 22:07

Container

ghcr.io/budgets-au/budgets:0.333.0
ghcr.io/budgets-au/budgets:latest

Added

  • API keys — long-lived bearer tokens for programmatic access. Admin operators can now mint, list, and revoke API keys under Settings → Security → API keys. Present via `Authorization: Bearer bk_…` on any API call; the auth guard accepts either a session cookie OR a valid bearer.
    • Format: `bk_` prefix + 32 bytes of URL-safe base64 (43 chars). The prefix makes leaks eyeballable in grep and CI logs.
    • Storage: only the SHA-256 hex digest lives in the DB. The plaintext is shown ONCE at creation and never round-tripped again — an emerald banner in the panel highlights the value with a copy button, then dismisses.
    • Role: keys default to admin scope.
    • Revoke: hard delete of the row — a leaked key stops working immediately.
    • `lastUsedAt` bumps on every authenticated call, so operators can spot stale keys.
    • Guard behaviour: when a Bearer is present, guard does NOT fall back to session auth — a stale cookie can't override a freshly-revoked key on the same request.
  • Migration 0018 — `api_keys` table. Applied automatically at first unlock.

Endpoints

  • `POST /api/settings/api-keys` — create (returns plaintext once).
  • `GET /api/settings/api-keys` — list metadata (no plaintext).
  • `DELETE /api/settings/api-keys/[id]` — revoke.

Usage

curl -H "Authorization: Bearer bk_..." https://your-host/api/accounts

Verify

  • tsc clean, `pnpm build` clean, 741/751 unit pass (+13 vs 0.332), pages-smoke + proxy-auth 16/16 green.

Upgrade

Pull `ghcr.io/budgets-au/budgets:0.333.0`. The 0018 migration runs on first unlock. Existing user sessions unchanged.

v0.332.0 — Cashflow Diff column sign tint

Choose a tag to compare

@budgets-au budgets-au released this 13 Aug 21:56

Container

ghcr.io/budgets-au/budgets:0.332.0
ghcr.io/budgets-au/budgets:latest

Changed

  • Cashflow report: Diff column background-tinted by sign.
    • Positive Diff (surplus / under-spend) → muted emerald fill (`bg-emerald-500/10`).
    • Negative Diff (shortfall / overspend) → muted rose fill (`bg-rose-500/10`).
    • Zero / no-plan cells keep the existing computed-column muted background.
    • The current-month indigo cell-highlight still wins when both apply — signalling "this month" beats sign for the current column.
  • Applied to every Diff cell: per-month sub-cells and the row-end aggregates (Leaf / SubParent / GrandParent / GrandparentHeader chrome).

Implementation

New `signTint?: boolean` prop on the shared `AmountCell`. Cheap to add elsewhere — any signed net/delta column can opt in.

Verify

  • tsc clean, `pnpm build` clean, pages-smoke 12/12 green.

Upgrade

Pull `ghcr.io/budgets-au/budgets:0.332.0`. No migrations — UI-only pass on top of 0.331.0.

v0.331.0 — server-restart API + Settings button

Choose a tag to compare

@budgets-au budgets-au released this 13 Aug 21:36

Container

ghcr.io/budgets-au/budgets:0.331.0
ghcr.io/budgets-au/budgets:latest

Added

  • POST /api/restart — admin-only server restart endpoint. Exits the Node process cleanly (process.exit(0)) after a 250 ms fudge factor so the JSON response flushes first. The container orchestrator (Docker / podman / k8s) relaunches when a restart policy is set — restart: unless-stopped on the Docker side is the typical choice. Without a restart policy the container stays stopped; response body documents the expectation.
  • Gated by withAdminAuth: a restart drops every live LAN session and flushes the SQLCipher key from memory, so it's a household-wide action, not a personal one.
  • Settings → Maintenance → Server → Restart button. New card next to the Transfer + Database sections. Confirm-dialog before the POST, spinner stays lit through the exit so the operator sees "Restarting…" until the tab loses connection and needs a manual reload.

Verify

  • tsc clean, pnpm build clean, pages-smoke 12/12 green.

Upgrade

Pull ghcr.io/budgets-au/budgets:0.331.0 and roll. To use the endpoint outside the UI:

curl -X POST -b 'authjs.session-token=<your session cookie>' https://your-host/api/restart

Ensure your docker-compose.yml (or equivalent) declares restart: unless-stopped on the budgets service.

v0.330.0 — treemap depth selector

Choose a tag to compare

@budgets-au budgets-au released this 13 Aug 20:45

Container

ghcr.io/budgets-au/budgets:0.330.0
ghcr.io/budgets-au/budgets:latest

Added

  • Depth selector on the Expenses / Income treemap. A Levels 1 · 2 · 3 control in the card header caps how many category levels the treemap subdivides below its current root.
    • Why it was needed. The tree was always built three deep, but Recharts paints child tiles over their parents — so an unpruned hierarchy effectively rendered only the leaves, and there was no way to see the grandparent or parent split without drilling in and losing the whole-picture view. "3" reproduces the previous rendering exactly, so it's the default and nothing changes until you reach for the control.
    • Values are preserved when pruning. Each node's value is already own + rolled-up children, so a capped parent still shows its full total — it just stops being subdivided. A pruned treemap covers the same area as an unpruned one.
    • Pruned tiles stay drillable. The click-to-drill affordance keys off a hasKids flag captured from the source tree rather than children.length, which the prune empties. Without that, choosing "1" would strand you with no way into level 2 — the one thing the selector must not break.
    • Depth is relative to whatever node the drilldown is currently rooted at, so it composes with the existing drill-in / Up / category-filter navigation.
    • Persisted as displayPrefs.treemapDepth rather than local component state, so it follows you across devices.

Verify

  • tsc clean, pnpm build clean.
  • 740/750 unit — seven new cases covering pruneToDepth: each level, over-asking, value preservation, hasKids survival, and input immutability.
  • pages-smoke 12/12, reports-tabs 15/15.

Upgrade

Pull ghcr.io/budgets-au/budgets:0.330.0 and roll. No migrations. The treemap renders exactly as before until you change the Levels control.

v0.329.0 — UI review release D: shared async states + clipped-header fix

Choose a tag to compare

@budgets-au budgets-au released this 13 Aug 12:44

Container

ghcr.io/budgets-au/budgets:0.329.0
ghcr.io/budgets-au/budgets:latest

Last of four UI-consistency releases — shared async states — plus the fix for the clipped table header reported against 0.328.

Fixed

  • Report table headers were clipped by the filter bar. On /reports two filter bars now stack (the date-range + report picker row, then the active report's own toggle bar), so real vertical chrome above a table is ~200px where --table-chrome reserved 180. The page scrolled by the difference, sliding the table's sticky top-0 header up underneath the sticky filter bar — and because both sat at z-10, the bar won and cut the month labels in half. The z-30 corner cell escaped, which is why "Category" stayed readable while "Jan '26" didn't.
    • /reports declares its own --table-chrome: 15rem, which is what the token was added for. The page no longer scrolls, so the overlap doesn't arise.
    • The z-ladder is re-cut so it degrades gracefully if a page ever does scroll: header corner z-40, rest of the thead z-20 (clear of the bar's z-10), sticky first-column body cells z-10. Documented as an explicit contract in ui/table-scroller.tsx so the next table to adopt it doesn't rediscover this.
    • accounts-cashflow-report's header cells were also semi-transparent — now opaque, same reason the frozen column needed it in 0.328.
  • Failed report fetches no longer masquerade as an empty ledger. Cashflow and Category checked !data but never error, so a failed GET fell through to "No data for this period." — telling you your ledger is empty when the request had actually failed. Both now check error first and offer a retry.

Added

  • ui/state-message.tsx — LoadingState, EmptyState, ErrorState.
    • LoadingState replaces 24 hand-written Loading… paragraphs across 23 files, which had drifted across nine padding combinations and two type sizes. Carries aria-live="polite" / aria-busy so screen readers announce the transition rather than sitting on a silently stale region.
    • EmptyState standardises the container and typography but not the copy. "No transactions in this category" and "No accounts yet" answer different questions — one says your filter matched nothing, the other says you haven't created anything. An optional action slot covers the second case.
    • ErrorState is new behaviour, not a consolidation: read failures were silent almost everywhere. Toasts remain the pattern for write failures (99 call sites, user-initiated). A read failure needs a persistent marker in the region that failed, with a retry.
    • Deliberately not skeletons — these sit above tables and charts whose height isn't known until the fetch lands, so a shimmer block of the wrong size reads worse than a line of text.

Verify

  • tsc clean, pnpm build clean, 733/743 unit.
  • 42 e2e: pages-smoke 12/12, reports-tabs 15/15, monkey-goals 15/15.

Upgrade

Pull ghcr.io/budgets-au/budgets:0.329.0 and roll. No migrations. The visible change is that report table headers stay fully readable while you scroll, and a failed report fetch now says so instead of claiming you have no data.

v0.328.0 — UI review release C: chrome + tables, plus Cashflow scroll fixes

Choose a tag to compare

@budgets-au budgets-au released this 13 Aug 11:27

Container

ghcr.io/budgets-au/budgets:0.328.0
ghcr.io/budgets-au/budgets:latest

Third of four UI-consistency releases — page chrome and tables — plus three Cashflow-report fixes reported during review.

Fixed

  • Cashflow's frozen category column was painted over by the data cells. All nine sticky first-column cells had no z-index while the thead cells had z-10/z-30, so during horizontal scroll the unpositioned numeric cells rendered on top of the frozen column. Two of those cells were also semi-transparent (bg-muted/30, bg-muted/40), so content bled through even where paint order was right. All nine now carry z-10 and an opaque background.
  • The 15-tab report strip is now a grouped dropdown at the right of the filter bar. Fifteen triggers in a row overflowed the content width, putting a horizontal scrollbar on the whole page that fought the report tables' own horizontal scroll — two scrollbars competing for the same gesture. Removing the strip also reclaims ~40px of vertical chrome, which brings page height back under the viewport so the tables' bounded scroll is the only vertical scroller. Options are grouped (Overview / By category / Accounts & flow / Detail). REPORT_TABS is now derived from the group definition so URL validation can't drift from what the picker offers. Deep links (/reports?tab=sankey) are unchanged.

Added

  • ui/filter-bar.tsx — the app's one sticky filter strip, extracted from the identical class string in the Cashflow and Category reports and rolled out to Transactions and Reports. Replaces three unrelated families.
    • sticky is opt-out, because two strips at top-14 overlap. On /transactions the bulk-selection bar owns that slot; on /reports the active tab's own filter bar does. Those pages still take the shared chrome without the pinning.
  • ui/table-scroller.tsx — the viewport-bounded scroller from 0.319, extracted and applied to six tables in priority order: accounts-cashflow-report (had a sticky first column but no sticky header and no bound — the half-migration that reads as broken on a long window), transactions-view (longest list in the app, previously no sticky header at all), cashflow-report, category-report, yoy-report, envelope-report. --cashflow-chrome renamed --table-chrome now that it's shared.
  • ui/back-link.tsx — up-navigation for /scheduled/[id] and /accounts/[id], which had no way out except the sidebar since the Topbar shows the record's own name.

Changed

  • Topbar titles consistently Title Case (two outliers fixed).
  • Removed six inert space-y-0 overrides from dashboard CardHeaders — left over from when the primitive was a flex container; it's been a grid since.

Audit notes

One planned item was dropped: normalising the settings panels' raw <h2> onto CardTitle. Those panels don't use the Card primitive at all — they hand-roll rounded-xl border bg-card — and all 19 use an identical heading class, so that surface is internally consistent. Converting it is a refactor of a distinct surface class (dense admin list vs data card), not a consistency fix.

Known issue

monkey: Scheduled in the exploratory crawl times out at 120s. Verified pre-existing by stashing this release and reproducing on 0.327.0 — it's state-dependent (the crawl runs after monkey-goals populates the DB, and the persisted app-map accrues across runs, so that page's control surface grows). Not a regression. The other 24 crawl tests pass.

Verify

  • tsc clean, pnpm build clean, 733/743 unit.
  • pages-smoke 12/12, reports-tabs 15/15.

Upgrade

Pull ghcr.io/budgets-au/budgets:0.328.0 and roll. No migrations. The Reports page now picks its report from a dropdown at the top right instead of a 15-tab strip; Transactions and the report tables keep their headers pinned while you scroll; and detail pages have a back link.

v0.327.0 — UI review release B: one segmented control, one money colour

Choose a tag to compare

@budgets-au budgets-au released this 13 Aug 08:35

Container

ghcr.io/budgets-au/budgets:0.327.0
ghcr.io/budgets-au/budgets:latest

Second of four UI-consistency releases — the "one source of truth" pass.

Added

  • ui/segmented-control.tsx replaces four hand-rolled variants across 17 call sites in 15 files. The variants had drifted into: two that shared markup but disagreed on the active fill (indigo vs near-black), one "inset tray" look, and the real ui/tabs.tsx used in two places. Padding came in three sizes and exactly one site had any ARIA at all.
    • Look: the majority treatment — joined hairline border with a bg-indigo-600 active fill, matching the sidebar's active nav item and the project's indigo accent.
    • Two scales, one language. size="compact" for controls that sit inside a CardHeader beside a CardTitle, where the default scale crowds the heading. Only type size and padding differ, so the two read as one control rather than two components.
    • Accessibility: role="radiogroup" / role="radio" / aria-checked with a roving tabindex and full arrow-key / Home / End navigation. That's the correct ARIA for "pick exactly one of N" — the calendar's range chips already had it, and the other 16 sites are brought up to match. Deliberately not role="tablist" (promises a tabpanel, and these reshape data in place) and not bare aria-pressed (describes independent toggles). ui/tabs.tsx remains for genuine page-level tab bars.

Changed

  • amountClass is the only money colour in the app now. Gained exported companions — MONEY_POSITIVE_CLASS, MONEY_NEGATIVE_CLASS, MONEY_NEUTRAL_CLASS — for callers that colour by semantic rather than by sign. The motivating case: accounts-cashflow's "debit" column is always a positive number but reads as an outflow.
    • Migrated every hand-rolled equivalent. Negatives had been appearing as text-rose-600, text-rose-400, text-rose-700 or text-red-500 depending on the file. Files touched: accounts-cashflow-report (6 sites), envelope-report, accounts-list, cashflow-calendar, transaction-row, reports-view, and stocks-summary-card — which had a wrapper colour duplicating the amountClass call on its own child.
    • Left alone deliberately: transfer-in / transfer-out columns (sky / amber are a genuinely different semantic to income and expense), and the envelope report's Income / Expenses section headings (they tint by section type, not by amount).

Fixed

  • The e2e suite had been hanging on networkidle since the Next 16.3 bump in 0.315.0. Next's RSC route-prefetch loop keeps the network permanently non-idle, so waitForLoadState("networkidle") never resolves. 0.315.0 fixed this in pages-smoke.spec.ts only — the other 12 specs kept the old wait and were quietly failing or burning their full timeout. All 24 call sites now wait on load.
    • The monkey crawl went from 9 of 9 route tests failing at 19.1 minutes to 25/25 passing in 2.0 minutes.
    • Charts-drawn / RGL-settled detection in screenshots.spec.ts and dashboard-visual.spec.ts is unaffected — those key off real completion signals, which is why they were already the right pattern.

Verify

  • tsc clean, pnpm build clean, 732/742 unit.
  • pages-smoke 12/12, monkey-goals 15/15, monkey crawl 25/25.

Upgrade

Pull ghcr.io/budgets-au/budgets:0.327.0 and roll. No migrations. Visible changes: the Category-report and Forward-balance-widget toggles now use the indigo active fill like every other toggle; the report toolbars that used the grey inset-tray look now match too; negatives are one consistent red everywhere; and all segmented controls are keyboard-navigable with arrow keys.

v0.326.0 — UI review release A: real defects + dead-token deletion

Choose a tag to compare

@budgets-au budgets-au released this 13 Aug 08:00

Container

ghcr.io/budgets-au/budgets:0.326.0
ghcr.io/budgets-au/budgets:latest

First of four releases from a full UI-consistency review (three parallel audits over layout/navigation, design tokens/primitives, and page interaction patterns). This one is the "clear the decks" pass — real defects plus dead-token deletion, chosen because none of it has a visual consequence worth reviewing. Colour/control consolidation, chrome/table normalisation, and shared loading/empty states follow in 0.327–0.329.

Fixed

  • Transactions bulk-action bar was invisible while scrolling. sticky top-0 z-10 under a sticky top-0 z-20 h-14 Topbar — same offset, lower z-index, so it slid underneath the moment the page scrolled. Now top-14.
  • amountClass is now genuinely the single source of money colour.
    • Zero is muted, not green — 0 >= 0 put it in the positive branch, so a $0.00 row read as a gain.
    • dark: variants baked in. The helper had none while ~7 hand-rolled call sites added their own, so the two families diverged in dark mode only.
    • Nullish / non-finite input returns the muted tone, so callers can pass a possibly-absent average without a guard.
    • This let the forked local copy in category-report.tsx go. It was also emitting text-rose-600 where every other table used text-red-500 — so the Category report had been rendering negatives in a different hue since 0.320.
  • font-mono did nothing. --font-mono aliased --font-geist-mono, defined nowhere, so all 12 files using it for code / import hashes / FITIDs silently inherited the sans body font. Alias dropped; Tailwind's built-in monospace stack now applies.
  • data-size="lg" on a Card was a silent no-op — ui/card.tsx implements sm only. Dropped from the Forward balance widget (0.325).
  • --cashflow-chrome was a phantom token — consumed by two bounded-height table wrappers, defined nowhere, always falling back. Now defined in :root so the offset is tunable in one place.
  • Print leaked dark-mode money colours onto paper. The print block overrides the -500/-600 status tints with darker -700 shades but had no -400 rule, which amountClass now emits under .dark.

Changed

  • Mobile navigation drawer is dismissible and modal. Escape closes it, the page behind no longer scrolls under it, and it announces itself with role="dialog" / aria-modal / aria-label while open — conditionally, since at lg it's permanent page furniture where a dialog role would be a lie. Hamburger gained aria-expanded / aria-controls. A proper focus trap is a larger lift and is not included.

Removed

  • 13 dead design tokens — --chart-1…--chart-5 and all eight --sidebar-*, plus their 13 @theme aliases, in both light and dark blocks. Zero references anywhere in src/**: charts read hex from lib/colours.ts (Recharts can't consume var(), so the chart tokens could never have worked as written) and the sidebar styles itself with bg-background / bg-indigo-600 directly. .dark --sidebar-primary was the only chromatic value in the entire neutral token set — a leftover shadcn blue.

Audit notes

Three findings were investigated and rejected rather than actioned — worth recording so they don't get "fixed" later:

  • The sample-data / orphan-categories panels reportedly hung on "Checking…" after a failed fetch. They already branch on SWR error before the loading check, and returning null is deliberate: non-admins get a 401 and the panel should hide for them.
  • edit-investment-dialog reportedly deleted a vest without confirmation. That delete is the consequence of an explicit Save on a form the user already confirmed, not a direct destructive action — a second modal mid-save would be worse. (It did surface a genuine unrelated gap: three unchecked fetch calls in that sync path fail silently. Tracked separately.)
  • The print block's hard-coded hex reportedly duplicated tokens. It deliberately does not match them — print needs more contrast than the on-screen tokens provide, which is the entire reason those rules exist.

Verify

  • tsc clean, pnpm build clean.
  • 731/741 unit tests. The amountClass suite was rewritten against the new contract; the two failures it produced first were the old behaviour being correctly pinned.
  • pages-smoke 12/12 green.

Upgrade

Pull ghcr.io/budgets-au/budgets:0.326.0 and roll. No migrations. The visible changes are: $0.00 amounts render grey instead of green, negatives are slightly brighter in dark mode, font-mono text is actually monospace, and the Transactions bulk-selection bar stays on screen while you scroll.