Repository navigation
Write amounts and dates one way, and re-render only what changes (app review batch H) - #34
Conversation
The web app reached the shared rules through relative paths, and one test's path pointed outside the repository. A TypeScript path alias, a Vite alias and the same alias in Vitest now resolve "@commune/core" to packages/core/src/index.ts, without a package file or a lockfile change. packages/core/tests runs with the web app's tests, in Node: the split arithmetic (moved from apps/web/tests, with checks that every split adds up to its total), reading amounts people type, and balances read from either side. The two test files that imported modules that no longer exist, and the onboarding copy that nothing imports, are removed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A6P7aTcMBccooYYr4xYnVS
Money was formatted by a new Intl.NumberFormat on every call, in four
copies, and US dollar communes read "US$32.10". Dates came in three
styles: "Due 2026-10-06" in the expense window, "09/10/2026" for notices
and fund contributions, and "9 Oct" elsewhere.
@commune/core now has formatMoney, which makes one formatter per currency
and keeps it, with each offered currency's narrow symbol ("$32.10"); a
currency that shares a symbol with an offered one keeps its code-prefixed
form ("CA$"). formatDay writes a calendar date as "9 Oct", with the year
when it is not this year, never moved by a time zone. Every screen formats
through them: the live model, the expense window, Settle, notices, funds
and pledges, search results, receipts, announcements, task history and the
activity log. The reminder worker writes amounts the same way through its
own copy, checked against the shared one.
Tests that compared dates with a fixed year now compare with the app's own
date text, so they hold in any year.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A6P7aTcMBccooYYr4xYnVS
…creen
The quick finder showed the database's text, "Due 2026-11-04" for a
recurring bill and "Task · 2026-10-20" for a task or event. Migration 0040
writes out search_commune_records in full with two more fields on each
result: label, its words without the date, and date, the date on its own
(YYYY-MM-DD, whatever the session's date style) or null. meta keeps the
old text unchanged for app versions released before this one, and can be
dropped once none reads it.
The app shows the label with the date written as everywhere else ("Due
4 Nov", "Task · 20 Oct"), and falls back to meta while a database sends
neither field. The local API passes search results through unchanged.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A6P7aTcMBccooYYr4xYnVS
The app's state was one context value rebuilt on every change, so typing in a filter, the minute check or a notice count in another commune re-rendered every screen, card and icon, and rebuilt every commune's views. - One store replaces the context. useApp keeps its shape, and each component follows only the fields it reads (useSyncExternalStore), so a change re-renders only what shows it. - A read that finds nothing new keeps the data it had, field by field, so the minute check changes nothing. Derived values keep their objects while what they are built from is the same, and a commune's views are built once per snapshot (readyPlace), for the member's date. - Commands keep their identity between renders (they read the records on screen when they run). - Notes keeps its search text in the Notes screen and Search keeps its own words; the app keeps a copy so Back to Search finds the same search, and forgets it when the account signs out. The finder opens Search with its words through openFullSearch. - Expense cards and note tiles are memoised, so typing in a filter does not re-render them. Render counts (components rendered, measured by tests/render-counts): typing in the Expenses filter 217 to 5; a minute check that finds nothing new 312 to 0; a notice count in another commune 312 to 6; typing in the Notes filter 124 to 5. New tests check the filters survive a window opened over their screen, and that Back to Search keeps its words. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A6P7aTcMBccooYYr4xYnVS
A Node benchmark builds the Hub and expense rows from a deterministic year of a four-person household: 800 expenses with repayments and corrections, 300 activity events, tasks, notes and pledges. Run it with cd apps/web && node node_modules/vitest/vitest.mjs bench --run --reporter=verbose tests/derive-snapshot.bench.ts On this machine, from a snapshot not seen before: a3db9b2 took 279.9 ms on average (median 220.0 ms), now 11.2 ms (median 9.9 ms), with one cached money formatter and date label. Before, every render of the app rebuilt this for every commune; now a render that is not a new read of the commune reuses what was built (0.0002 ms). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A6P7aTcMBccooYYr4xYnVS
equalShares in live/model.ts repeated packages/core's allocateExpense, which every split already goes through, and nothing imported it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A6P7aTcMBccooYYr4xYnVS
The release order, the local migration list and the hosted cutover now run to 0040, which adds label and date to search results. CLAUDE.md and the handover describe @commune/core, formatMoney and formatDay, and the store behind useApp. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A6P7aTcMBccooYYr4xYnVS
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
CI did not run on this commit. All three jobs (Web app; Database, local API and worker; Secret scan) were marked failed two seconds after they were queued, before a runner was assigned: no step ran and there are no logs. The workflow file is the same as on #33, where the same jobs passed at 20:10 UTC. GitHub fails jobs this way when the account cannot start Actions, for example when the month's included minutes or the spending limit are used up; the push runs on On this commit, everything CI runs passes locally: 642 web tests, the web, local API and worker typechecks, the production build, 28 database tests and the schema check, 22 local API suites, the worker's tests, and the secret scan over the files and the whole history. The Vercel preview error is the account issue already noted on #24 to #33, not this change. Generated by Claude Code |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
Before: US dollar communes showed "US$32.10". Dates came in several styles: most screens said "9 Oct", but the expense window said "Due 2026-10-06", Settle and pledges "due 2026-10-06", notices and fund contributions "09/10/2026", the quick finder "Due 2026-11-04" and "Task · 2026-10-20", and reminder and receipt dates always carried the year. Each amount built its own number formatter, and money was formatted in five places (the email worker included). The whole app shared one context, so in the render-count test a minute check that found nothing new re-rendered 312 components, a letter typed in the Notes filter re-rendered the whole app (124 components), and every render rebuilt every commune's views from its records (about 280 ms for a busy year). A letter typed in the Expenses filter re-rendered every expense card (217 components for 40 expenses).
packages/corewas reached by relative paths, its tests ran nowhere, and two of its test files imported modules that do not exist.After: amounts read "£12.50", "$32.10" and "€8.00", and every plain date reads "9 Oct", with the year when it is not this year ("6 Oct 2025"), the finder included. One cached formatter writes every amount and one writes every date, and reminder emails write amounts the same way. Typing in the Expenses or Notes filter re-renders only that screen (5 components); in Chromium with the CPU slowed four times, a keystroke in the Notes filter reaches the screen in 16 ms or less, against about 200 ms before (480 ms with a year of expenses loaded). A minute check that finds nothing new re-renders nothing, and a commune's views are built once per read of it: a busy year now takes about 11 ms, 25 times faster.
packages/coreis imported as@commune/core, and its tests run in CI with the web tests.This is app review batch H, built on #33 (tidying), so review that first; the diff here shows only this part. Besides the money and date text listed above, the one visible change is that the Notes filter clears when you leave Notes, as the Expenses filter always has.
What changed, by finding
formatMoneyinpackages/core/src/format.tsmakes one formatter per currency and keeps it. The three currencies the app offers use their narrow symbol; any other keeps the code-prefixed form ("CA$", "A$") so it cannot be mistaken for one of them. Every amount in the web app goes through it: the live model, the expense window, Settle, Expenses, search results, funds, receipts, announcements, task history and the activity log. The reminder worker keeps a copy (apps/reminder-worker/src/money.ts, since it deploys on its own and imports nothing from outside its folder), and a test checks the two agree.formatDaywrites "9 Oct", with the year when it is not this year, and never moves a date across a time zone. The expense window, Settle, notices, pledges, fund contributions, receipts and reminder days use it. Migration 0040 writessearch_commune_recordsout in full so each result also carrieslabel(its words without the date) anddate(YYYY-MM-DD or null); the finder writes "Due 4 Nov" and "Task · 20 Oct" from them, and shows the old text if the database has no 0040 yet. The oldmetatext stays for clients released before this one.useApp()keeps its shape, so no screen changed how it reads state, but behind it is now a store: each component re-renders only when a field it read has changed. A minute check that finds nothing new keeps the objects it already had. Each commune's views are built once per read (readyPlace), and rebuilt when the date changes, so "Overdue" moves on at midnight. The Notes search text now lives in Notes (Expenses' already did), so like the Expenses filter it clears when you leave the screen or switch commune; Search keeps its own words and the app keeps a copy so browser Back finds the same search, and the expense and note cards are memoised. Commands keep their identity between renders and read the latest records when they run.packages/coreis imported as@commune/corethrough a path inapps/web/tsconfig.jsonand an alias invite.config.tsandvitest.config.ts. It is not a workspace package, because that needs a lockfile change, which the network here blocks. Its tests now live inpackages/core/testsand run in Node in the web test command, so CI runs them. The two test files that imported missing modules and an unused onboarding module are deleted, and the expense split tests moved into core and check that every split adds up. Batch 3 had already madereadMoneyInputthe one parser, so Add expense, corrections, funds, pledges, receipts, recurring bills and saved splits accept and refuse the same amounts;money.test.tspins them.equalShares, an unused copy of core's equal split, is deleted.Not in this pull request: the minute check's download size (finding 18) and drawing every expense card (finding 19). Other date styles stay as they are, because they say something else: day headings in Activity ("Thursday 9 October"), the Hub week list, task and month headings, and times of day.
Releasing
Nothing hosted was touched. Migration 0040 follows 0039 and only adds two fields to search results, so the previous client keeps working. This client works with or without it: without 0040 the finder shows the old date text. Apply it and refresh the Data API schema cache.
metacan be dropped by a later migration once no released client reads it.Needs the owner
metafrom search results: once no released app version reads it.How it was checked
On this branch:
packages/core's among them; the web, local API and worker typechecks; and a production build.run-local.sh(2 more need a Neon branch), one of them new for 0040, thendump-schema.sh --check.In Chromium, production builds of #33 and of this branch (pointed at the local API) ran against one disposable database: the three sample communes, in pounds, euros and dollars, and a household with 800 expenses over a year.
The Expenses filter was already local to its screen, so it gains least: with a year loaded, the slowest tenth of keystrokes fell from 360 ms to 136 ms. Its slowest keystroke is clearing the filter with a year loaded, which draws all 800 cards again; that is finding 19, left for a later pull request.
How
The store holds the state the provider computes on each render.
useApp()returns a view of it that notes which fields a component reads, and React'suseSyncExternalStorere-renders that component only when one of those fields has changed. Derived values keep their objects while what they are built from is unchanged, so a field changes only when what it shows does.infra/neon/schema.sqlwas regenerated with 0040; its diff shows the whole change tosearch_commune_records.🤖 Generated with Claude Code
https://claude.ai/code/session_01A6P7aTcMBccooYYr4xYnVS
Generated by Claude Code