Skip to content

Write amounts and dates one way, and re-render only what changes (app review batch H) - #34

Merged
EmotiveImpact merged 7 commits into
mainfrom
claude/app-review-dates-speed
Oct 12, 2026
Merged

EmotiveImpact merged 7 commits into
mainfrom
claude/app-review-dates-speed

Conversation

@EmotiveImpact

Copy link
Copy Markdown
Owner

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/core was 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/core is 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

Finding Change
50 formatMoney in packages/core/src/format.ts makes 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.
47 formatDay writes "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 writes search_commune_records out in full so each result also carries label (its words without the date) and date (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 old meta text stays for clients released before this one.
17 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.
64 packages/core is imported as @commune/core through a path in apps/web/tsconfig.json and an alias in vite.config.ts and vitest.config.ts. It is not a workspace package, because that needs a lockfile change, which the network here blocks. Its tests now live in packages/core/tests and 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 made readMoneyInput the one parser, so Add expense, corrections, funds, pledges, receipts, recurring bills and saved splits accept and refuse the same amounts; money.test.ts pins them.
65 (from #33) 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. meta can be dropped by a later migration once no released client reads it.

Needs the owner

  • Say when to drop meta from search results: once no released app version reads it.

How it was checked

On this branch:

  • 642 web tests in 64 files, packages/core's among them; the web, local API and worker typechecks; and a production build.
  • 28 database tests through run-local.sh (2 more need a Neon branch), one of them new for 0040, then dump-schema.sh --check.
  • 22 local API suites on a disposable database and port, one of them new (search dates), and the reminder worker's 7 unit tests and 4 database suites.
  • The secret scan over the files and every commit.

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 text listed above changed and nothing else did: "$32.10" on Studio East's Expenses, Settle and Home (was "US$32.10"); "Due 29 Sept" in the expense window; "due 6 Oct" on Settle and in the fund window; "9 Oct" on notices and contributions (was "09/10/2026"); "Due 11 Oct" and "Event · 18 Oct" in the finder. September reads "Sept", as the app's existing dates already write it. Pound and euro amounts read as before.
  • The Expenses filter and its chip, the Notes filter and full search's words survive opening and closing a window, and browser Back returns to the same search.
  • Typing, with the CPU slowed four times: three alternating runs of each, timed from key press to the next frame painted.
Typing in Loaded Before: typical, slowest After: typical, slowest
Notes filter on opening 208 ms, 352 ms 16 ms or less, 104 ms
Notes filter a whole year 480 ms, 1,120 ms 16 ms or less, 80 ms
Expenses filter on opening (84 cards) 24 ms, 312 ms 24 ms, 320 ms
Expenses filter a whole year (800 cards) 56 ms, 2,152 ms 44 ms, 2,216 ms

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's useSyncExternalStore re-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.sql was regenerated with 0040; its diff shows the whole change to search_commune_records.

🤖 Generated with Claude Code

https://claude.ai/code/session_01A6P7aTcMBccooYYr4xYnVS


Generated by Claude Code

claude added 7 commits October 9, 2026 20:38
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
@vercel

vercel Bot commented Oct 9, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
commune-web Error Error Oct 9, 2026 9:11pm UTC

Copy link
Copy Markdown
Owner Author

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 main in April failed the same way. It could also have been a passing GitHub fault. I can't re-run jobs from here (the API returns 403), so this needs the owner: press "Re-run all jobs" on the run, and if the jobs fail again within seconds, check Actions usage under Settings, Billing and plans.

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

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-12T02:02:22.034338Z dc69a71 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

This branch had an error being deployed

1 failed deployment
Preview — dc69a71a Deployed Oct 9, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants