feat: transaction calendar with indefinitely projected schedules - #185
Open
dannywieser wants to merge 2 commits into
Open
feat: transaction calendar with indefinitely projected schedules#185dannywieser wants to merge 2 commits into
dannywieser wants to merge 2 commits into
Conversation
Adds a month-at-a-time calendar page listing each day's transactions, both logged and scheduled, backed by a new transaction-tracker service. - apps/transaction-tracker: GET /transactions?month= (or ?from=&to=) scans note frontmatter for transactions and returns the occurrences in the requested window plus in/out/net totals. Notes with a recurrence become schedules; their occurrences are expanded on demand for whatever window is asked for, so projection has no end horizon. Day/week rules seek to the first occurrence arithmetically and month/year rules seek by month distance, so a distant future month costs the same as the current one. Month rules clamp to shorter months instead of drifting. - packages/app-config: optional `transactions` block naming the frontmatter properties, currency, and an optional folder to scan; every field defaults, so the block can be omitted. - packages/markdown: FILE_ID_NAMESPACE moved here from notes-api so both services derive the same note ids and occurrences can link to notes. - packages/util: addMonthsToDate (clamping) and monthsBetween; toISODateString's timezone argument is now optional, defaulting to the runtime's for browser use. - packages/services: TransactionsResponse types and useTransactionsQuery, with a demo-mode path that narrows the static snapshot to one month. - apps/web: /calendar and /calendar/:month, with the month in the route so months are linkable and the back button steps through them. Adds positiveText/negativeText palette tokens for money in and out. - apps/demo-data: generates finance notes and snapshots a bounded window of occurrences, since a static demo cannot project on demand. - Docker, nginx, vite proxy and the publish matrix wire up the service. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PLNimtv3ZC5ZtoLQpMhzNS
Day cells grew to fit their transactions, so rows were all different heights. Each cell is now a fixed height and summarises the day instead of listing it. - The cell shows the day's logged and scheduled totals (the scheduled one marked with a repeat icon), then a count of money out and money in. - The transactions themselves move to TransactionDayDetail, a panel shown on hover or keyboard focus. It stays mounted so the entry links remain tabbable, and anchors to its own edge on the first and last columns so it doesn't spill off the page. - buildCalendarWeeks now derives loggedTotal, scheduledTotal, expenseCount and incomeCount per day, replacing the single net total, so a cell can summarise itself without walking its transactions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PLNimtv3ZC5ZtoLQpMhzNS
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a
/calendarpage showing a month at a time, with forward/back navigation, where every day block summarises that day's transactions — both logged and scheduled. Scheduled transactions are projected indefinitely into the future.There was no transaction concept in the repo before this, so the data model is new. It follows the
habit-trackerpattern: a new backend service reads from note frontmatter, driven by an optional config block.How a note becomes a transaction
Any note with a numeric value in the configured
amountPropertyis a transaction. Adding arecurrencemakes it a schedule:Every property name is configurable via a new optional
transactionsblock inapp.config.json, and every field defaults, so the block can be omitted entirely.folderoptionally restricts the scan to one vault directory.amountis signed (negative is money out) and tolerates$1,200.50,(45), and-45.datefalls back to the note's own date, so a daily-note vault needs no explicit date. Recurrence accepts named rules (daily,biweekly,monthly,quarterly,yearly, …) and an interval form (every 2 weeks,every 18 months). An unrecognised rule degrades to a one-off rather than failing the scan.On "projected indefinitely"
There is no horizon constant and nothing is precomputed. The calendar requests a month; recurrences are expanded for exactly that window. Day/week rules seek to the first occurrence arithmetically; month/year rules seek by month distance — so a distant future month costs the same as the current one rather than iterating from the anchor. Month rules clamp to shorter months instead of drifting (Jan 31 → Feb 28 → Mar 31). A single rule is capped at 1000 occurrences per request so an unusually wide
from/tocan't force an unbounded response; a month request is far below that.The day cell
Every cell is a fixed height regardless of how much falls on the day, so rows stay uniform. A cell shows the day's logged and scheduled totals (the scheduled one marked with a repeat icon, so a projection is never mistaken for something that happened), then a count of money out and money in rather than a row per transaction.
Hovering a day — or tabbing into it — opens a panel with that day's transactions in full, each linking to its source note. The panel stays mounted rather than appearing on hover, so those links remain tabbable, and it anchors to its own edge on the first and last columns so it never spills off the page.
API
GET /transactions— returns every transaction occurrence in a date window, with the window's totals.Query parameters (all optional):
month=YYYY-MM(what the calendar uses), orfrom=YYYY-MM-DD&to=YYYY-MM-DD, or neither to default to the month containing today.statusisloggedfor a note recording a transaction directly andscheduledfor a projected occurrence — it says where the entry came from, not whether its date has passed.idis unique per occurrence (<noteId>:<date>) so repeats of one note don't collide;noteIdmatches the idnotes-apigives the same note, so occurrences link back to their source.Success response:
200{ "currency": "USD", "from": "2026-08-01", "to": "2026-08-31", "totals": { "expense": -2019.17, "income": 5085, "logged": 35.31, "net": 3065.83, "scheduled": 3030.52 }, "transactions": [ { "amount": -1650, "category": "housing", "date": "2026-08-01", "description": "Rent", "id": "8f3c1d2e-...:2026-08-01", "noteId": "8f3c1d2e-...", "obsidianUrl": "obsidian://open?vault=notes&file=finance%2Frent", "recurrence": "monthly", "status": "scheduled" }, { "amount": -22.99, "category": "books", "date": "2026-08-04", "description": "Paperback", "id": "b1a0f5c7-...:2026-08-04", "noteId": "b1a0f5c7-...", "obsidianUrl": "obsidian://open?vault=notes&file=finance%2Fpaperback", "recurrence": null, "status": "logged" } ] }Error response:
400for a malformed window (badmonth, non-ISO or half-suppliedfrom/to, ortobeforefrom){ "error": "month must be a YYYY-MM value" }Error response:
500when the vault can't be scanned{ "error": "Unable to load transactions" }GET /health— same shape as the other services:200 {"status":"ok"}, or503 {"status":"error","error":"..."}when config can't be resolved or (inobsidianmode) the vault isn't readable.Changes by package
apps/transaction-tracker(new, port 3006) — the service above. Supports bothobsidianandbearnote sources, same dispatch ashabit-tracker.packages/app-config— optionaltransactionsblock;DEFAULT_TRANSACTIONS_CONFIGfills every unset field.packages/markdown—FILE_ID_NAMESPACEmoved here fromnotes-api(which re-exports it) so both services scanning the same vault derive identical note ids. That's what makes the occurrence → note links work.packages/util—addMonthsToDate(day-clamping) andmonthsBetween.toISODateString's timezone argument is now optional, defaulting to the runtime's, which is what a browser needs for "today".packages/services—TransactionsResponsetypes anduseTransactionsQuery.apps/web—/calendarand/calendar/:month. The month lives in the route so months are linkable and the back button steps through them.buildCalendarWeeksderives each day's logged/scheduled totals and money-out/in counts, so a cell summarises itself without walking its transactions. AddspositiveText/negativeTextpalette tokens to all 8 themes: there was no red token, andsuccessTextis a foreground-on-green colour, so reusing it would have been wrong.apps/demo-data— generates finance notes and snapshots the occurrences.Verification
npm run verifypasses — 59/59 tasks (lint, typecheck, build, test). 103 tests on the service, plus web and services coverage.Smoke-tested against the real service over the generated demo vault:
2026-08returns interleaved logged and scheduled entries with correct totals.2027-02,2031-06,2099-07,2150-12all resolve, scheduled-only, withbiweeklystaying in phase (paycheque count varies by month as it should).2150-12responded in 35ms vs 27ms for the current month — confirming the arithmetic seek, not iteration.400s and the explicitfrom/toform behave as documented.Also driven in Chromium in demo mode:
/calendarcrashed because I'd usedformatDate, which returns"2026.08.22 (Sat)"rather than ISO. The component test mocks its util, so it was blind to it. Fixed via the optional-timezonetoISODateString, with a regression test asserting the date passed into the grid builder matches the ISO pattern.Notes for review
apps/demo-data/README.md.Checklist
.changeset/*.mdentry for any user-visible change, or I confirm this PR contains only docs, tests, or CI changes and no changeset is needed.