Skip to content

Load less and read again only what changed (app review batch 4) - #28

Merged
EmotiveImpact merged 11 commits into
mainfrom
claude/app-review-batch-4
Oct 12, 2026
Merged

EmotiveImpact merged 11 commits into
mainfrom
claude/app-review-batch-4

Conversation

@EmotiveImpact

Copy link
Copy Markdown
Owner

Before: every load sent a commune's whole history, about 1.8 MB for a busy household's year, and every open tab read every commune's full record again each minute. The Expenses screen drew every expense ever recorded. Reading the commune list checked membership row by row across every commune on the platform, and any account could create communes without limit. On phones the data client only started downloading once the main bundle had run. Inter came from Google Fonts on every page, a 63 KB font drew the eight letters of the wordmark, Home asked Unsplash for every default cover twice at up to 1600 pixels, and a cover that failed showed a broken image.

After: the same year's snapshot is 270 KB and takes 46 ms rather than 135 ms, and older expenses load a month at a time when someone asks for them. Each minute a tab that is visible and in use reads the commune list alone and reads again only the communes that changed. The commune list takes under 2 ms for a member of one commune among 5,000, and an account can create at most 100 communes. The data client downloads alongside the main bundle, the fonts come from the app itself, and covers are fetched at the size they are drawn and fall back to a calm tone.

This is batch 4 of the app review fixes and is built on #27 (batch 6a), so review that first; the diff here shows only this batch.

What changed, by finding

Finding Change
18 Migration 0036 gives each commune a change mark, moved by a trigger on every table a snapshot, the commune list or an invitation list shows. The snapshot holds open expenses, those created, corrected, voided or settled since the start of last month, and always the five newest that are not voided, so a commune that has gone quiet keeps its hub and activity. commune_expense_page and commune_expense_records serve older expenses, and commune_summary gives the overview screens only what they draw (150 KB for the same household). Each minute the client reads the commune list and reads again only the communes whose mark moved, and only while the tab is visible and someone has used it in the last ten minutes. Who is signed in is asked at start-up and whenever the tab is shown or focused again.
19 Expenses shows open expenses and this month's settled ones (or last month's while none has been settled this month). Show earlier adds a month at a time and says which months are not shown yet; voided expenses page on their own. A link to an older expense fetches it, and statements and both export files load every older expense first. Counts and empty states include the expenses still on the server.
37 my_communes() returns the caller's communes and their marks in one call, and the communes and members policies look up the caller's communes once per query: 58 ms and 231 ms became 1.7 ms and 1.8 ms for a member of one commune among 5,000. create_commune refuses an 11th commune in an hour, a 31st in a day and a 101st in all; communes the account has left still count.
35 Inter and the wordmark are served by the app. Inter's Latin file draws the interface, and the files for other scripts download only for a page that shows one of their characters, such as a member's name. The wordmark font is a 4.4 KB subset holding "commune.". Google Fonts leaves the page and the hosted content policy, and the privacy page no longer names Google. Default covers are offered at 480 to 1600 pixels with a size for each placement, so Home makes 3 requests instead of 6 and This Hub fetches 1200 pixels rather than 1600. A cover that cannot load leaves its commune type's tone, which also shows while it loads. Screenshots of the sign-in page, the expenses screen and every cover placement are pixel-identical before and after.
38 Production builds add a preload link for the data client, so it downloads with the main bundle. From the page request to the first data request on a phone (390 pixels wide, processor slowed 4x), medians of 7 runs: 2,053 to 1,876 ms on a slow connection and 811 to 694 ms on good 4G. The start-up session check reached the network once in every trace, as the SDK answers the second from memory, so that part needed no change.

Not in this batch: finding 17 (one cached formatter, memoised data, filter text kept in each screen and a split context) comes later. Covers still come from Unsplash until the three image files are bundled.

Releasing

Nothing hosted was touched. Migration 0036 is local only and follows 0035. A client from before 0036, including a tab left open, keeps working but sees only the expenses the snapshot now holds, so its statements and exports would leave older ones out: deploy the new client straight after applying 0036 and refreshing the Data API schema cache. 0036 replaces prune_delivery_records() in place, so the worker needs no redeploy; each run also deletes change rows older than two days.

Needs the owner

  • The cap of 100 communes per account sits against the old Agency plan's "unlimited". Say if you want different numbers.
  • The three default cover photos, to bundle them with the app. Until then the hosted content policy keeps Unsplash for images, and the three cover tones are new colours for you to approve.
  • The privacy page change (Google is no longer named as the font provider) is part of the legal wording you are already reviewing for finding 13.

How it was checked

On the final branch:

  • 24 database tests through run-local.sh, including the new lighter_loading.sql and change_mark_order.mjs; 2 more are skipped because they need a Neon branch.
  • 18 local API suites on a disposable database and port, including the new lighter-loading.mjs.
  • 458 web tests in 43 files.
  • The reminder worker's unit tests and 4 database suites, including the prune of old change rows and a delivery moving the mark.
  • Typechecks for the web app, local API and worker, and a production build.

The snapshot figures come from a household with a busy year of records and the list figures from a database of 5,000 communes, each measured before and after on the same machine. In a browser: screenshots of the sign-in page, the expenses screen and every cover placement before and after the font and cover changes, and names in Polish, Russian, Kazakh, Greek and Vietnamese drawn in Inter with only the subsets they need downloaded.

How

The change mark is today's UTC date with the count and highest id of the commune's change rows since the start of yesterday, read in the same statement as the snapshot, so it moves with every commit, including one that commits after a later write; change_mark_order.mjs holds such a commit open to prove it. A trigger adds at most one change row per commune per transaction. Older expenses fetched by the client sit in a store per commune that survives the snapshot being read again; the snapshot's copy of an expense wins, and the store is dropped when a new month moves the window. The preload link is added by a small Vite plugin, and a test builds the app and checks that index.html names the chunk the main bundle imports.

🤖 Generated with Claude Code

https://claude.ai/code/session_01A6P7aTcMBccooYYr4xYnVS


Generated by Claude Code

@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 4:50pm UTC

claude added 11 commits October 9, 2026 16:50
Findings 18, 19 and 37 from the app review: the database side.

Migration 0036 adds commune_private.commune_changes. A trigger on every
table that a snapshot, the commune list or an invitation list shows adds
at most one row per commune per transaction. A commune's change mark
(today's UTC date, then the count and highest id of its rows since the
start of yesterday) moves with every commit, including one that commits
after a later write. The worker's scheduled prune drops rows older than
2 days.

public.my_communes() returns the caller's communes with their marks in
one call. The communes and commune_members row checks look the caller's
communes up once per query instead of once per row: reading them as a
member of one commune among 5,000 took 58 ms and 231 ms, and now takes
1.7 ms and 1.8 ms. create_commune refuses an 11th commune in an hour, a
31st in a day and a 101st in all.

The snapshot now carries the open expenses and those created, corrected,
voided or settled since the start of last month (UTC), with
expenseHistory counting what it left out and changeMark read in the same
statement. For a busy household's year it went from 1.8 MB and 135 ms to
270 KB and 46 ms. Older expenses come from public.commune_expense_page a
month or more at a time and named ones from
public.commune_expense_records (at most 50). public.commune_summary is
the snapshot without the detail only a commune's own screens use
(150 KB for the same household).

infra/neon/tests/lighter_loading.sql and change_mark_order.mjs cover
them, the grants test lists the new functions, and the worker
integration test checks the prune and that a delivery outcome moves the
mark.

Apply 0036 before releasing the web app that uses it. The worker needs
no redeploy.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A6P7aTcMBccooYYr4xYnVS
The commune list now comes from public.my_communes(), so it carries each
commune's change mark and is read the same way as on the hosted app. The
snapshot route takes ?detail=summary for the overview screens. Two new
routes page through expenses older than the snapshot holds and fetch named
expenses for links, with the same limits the database enforces.

lighter-loading.mjs covers marks, the summary, paging, named records and
the limit on creating communes against an isolated API port.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A6P7aTcMBccooYYr4xYnVS
…ient

The hosted app reads its commune list with one call to my_communes instead
of selecting from communes and commune_members, which the set-based
policies in 0036 now answer quickly. Each listed commune carries its change
mark. loadSnapshot can ask for the overview summary, and a full read
refuses a summary in its place. Two loaders page through older expenses and
fetch named ones, mapped to the new database functions with the same
limits.

The contract tests follow the single list call and cover the summary, the
paging and named-record calls, their refusals and malformed answers.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A6P7aTcMBccooYYr4xYnVS
The minute timer used to ask who was signed in, then reload the commune
list, the reminder counts, every admin commune's invitations and the open
commune's whole record, and on the overview screens every other commune's
too. Now each check reads the commune list alone and compares each
commune's change mark with the one it holds. A commune whose mark moved is
read again, with its invitations; the reminder counts are read when any
mark moved or a reminder slot has opened. Joins, departures and role
changes come from the list.

The overview screens read other communes as summaries; a commune's own
screens wait for its whole record. The check runs only while the tab is
visible and someone has used it in the last ten minutes, and starts again
with the next use. Who is signed in is asked at start-up and when the tab
is shown or focused again; a refused session still signs the tab out.
Screens can only ask for a full refresh.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A6P7aTcMBccooYYr4xYnVS
The snapshot now holds open expenses and those created or changed since
the start of last month. Older ones are fetched into a store per commune
that survives the snapshot being read again; the snapshot's copy of an
expense wins, and the store is dropped when a new month moves the window.

The Expenses screen shows open expenses, then settled ones from this
month, or from last month while none has been settled this month. Show
earlier adds the latest earlier month that has one, fetching as it passes
what is loaded, and says plainly which months are not shown yet. Voided
expenses page on their own. A link to an expense older than what is loaded
fetches that one expense behind the existing loading state; a refusal shows
the existing not-found line and a failed connection offers Try again. The
statement and both export files load every older expense first, with a
short loading line. Counts and empty states include the expenses left on
the server.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A6P7aTcMBccooYYr4xYnVS
With only the window, a commune that had gone quiet (a finished trip, a
household that last spent three months ago) lost its hub's recent expenses
and its activity, and the hub could list an old open expense as recent
while newer settled ones were missing. snapshot_expense_ids now also takes
the five newest expenses that are not voided, by created_at and then id, so
the snapshot, expenseHistory, the pages and the summary still agree on which
ones are left out.

The SQL test adds a quiet commune: its five newest are kept however old, a
voided one takes no place, a tie for fifth goes by id, a new expense pushes
the fifth out, and the counts and pages leave out the same ones. The web
tests check that a quiet commune's expenses start at its newest settled
month and page back past the snapshot's start, and that its hub and
activity list its latest. The busy household's snapshot is unchanged: its
five newest are already in the window.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A6P7aTcMBccooYYr4xYnVS
The local list and the Neon release order now run to 0036. The release
note says what 0036 changes for an older client and the worker, and that it
caps commune creation at 10 an hour, 30 a day and 100 in all. The handoff
summarises the speed fixes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A6P7aTcMBccooYYr4xYnVS
The app loaded Inter from Google Fonts, so Google saw every visit, and
it shipped the whole 63 KB DM Sans variable font to draw the eight
letters of the wordmark.

- Inter: every file fonts.gstatic.com served for the old stylesheet
  link, in the same order with the same weight range and unicode ranges,
  so text renders exactly as before. Latin (48 KB) draws the interface.
  Latin Extended, Cyrillic, Cyrillic Extended, Greek, Greek Extended and
  Vietnamese (10 to 85 KB each) download only for a page that shows one
  of their characters, such as a member's name. font-display: swap as
  before.
- Wordmark: a 4.4 KB subset of the same DM Sans file holding only
  "commune." with both variation axes, under its own family name.
- The OFL licence sits beside each font.
- index.html drops the Google preconnects and stylesheet; the hosted
  policy drops fonts.googleapis.com and fonts.gstatic.com, and the build
  never inlines a font as a data: URL, which font-src 'self' would block.
- The privacy page no longer names Google as the font provider.

Screenshots of the sign-in page, the expenses screen and every cover
placement are pixel-identical before and after.

@fontsource-variable/dm-sans stays in package.json and the lockfile:
pnpm cannot rewrite the lockfile without resolving the whole workspace,
which fails here on a mobile dependency fetched from GitHub. Nothing
imports it any more.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A6P7aTcMBccooYYr4xYnVS
…ey fail

Home asked Unsplash for every default cover twice, 1200 and 1600 pixels
wide, even for the small cards; This Hub and the settings preview loaded
the 1600-pixel photo; and a cover that failed showed the browser's
broken-image icon on This Hub.

- data/covers.ts picks each cover: a photo chosen in this browser as it
  is, otherwise the commune type's Unsplash default offered at 480, 800,
  1200 and 1600 pixels, with a sizes value for each placement (Home's
  first card, the smaller cards, This Hub, the settings preview), so the
  browser fetches the width it needs.
- ui/CoverImage.tsx draws it as an image that removes itself if it cannot
  load. Its container carries the commune type's tone (new
  --cover-household, --cover-workspace and --cover-trip tokens), which
  shows while the photo loads and stays if it fails.
- Home's cards draw the photo as an image rather than a CSS background,
  so one request serves a commune before and after its records arrive.
  While loading, the first card no longer shows the household photo for
  every type.
- This Hub's loading placeholder is unchanged: plain sage, or a photo
  chosen in this browser.
- Unused cover styles that still named the photos are removed.

Requests on a 1280-pixel desktop: Home 6 (three at 1200, three at 1600)
to 3 (1200, 480, 480); This Hub 1600 to 1200; settings preview 1600 to
480. On a phone at 3x: Home 6 to 3, each 1200. With the photos loaded
the screens look as before. Member photos already fall back to initials.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A6P7aTcMBccooYYr4xYnVS
A production build puts src/live/neon.ts and the Neon SDK in a chunk of
their own, which live/api.ts imports on first use. Every visit needs it
straight away for the session check, but the browser only learned of it
once the main bundle had downloaded and run.

A small build plugin in vite.config.ts now adds a modulepreload link for
that chunk (and any chunk it shares, none today) to index.html, so it
downloads alongside the main bundle. api.ts is unchanged. A test builds
the app and checks that index.html carries the link and that it names
the chunk the main bundle imports.

Cold start on a phone (390 pixels at 3x, processor slowed 4x), medians
of 7 runs on 9f3077a with the font and cover changes, from the page
request to the first data request:
- slow mobile connection (150 ms latency, 1.6 Mbps): 2,053 to 1,876 ms
- good 4G (40 ms latency, 9 Mbps): 811 to 694 ms
Importing the client early from main.tsx instead gave 2,017 and 727 ms,
so the preload link is the one kept. Neither file changes size. Zod,
which the Neon SDK uses, makes up about two thirds of the data client.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A6P7aTcMBccooYYr4xYnVS
- HANDOFF.md: a Start here entry after the money accuracy fixes. Inter
  and the wordmark are served from the app, default covers are sized and
  fall back to a tone (bundling them needs Edem's three files), and
  production builds preload the data client.
- DESIGN-LANGUAGE.md: Inter is self-hosted with every subset; the
  wordmark's font holds only "commune."; the three cover tones.
- HOSTED-RELEASE-CHECKLIST.md: the preview walk checks the narrower font
  and style policy, and img-src keeps Unsplash until covers are bundled.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A6P7aTcMBccooYYr4xYnVS

Copy link
Copy Markdown
Owner Author

The Vercel preview for commune-web shows Error here, as it has on every branch since 6 October. It is the project or account issue explained on #24 (app review finding 45), not this diff: the Web app check, which builds the app, passes on this head (beb52ba). It needs someone with the Vercel login to read a failed deployment's log.


Generated by Claude Code

@EmotiveImpact
EmotiveImpact changed the base branch from claude/app-review-batch-6a to main October 12, 2026 02:00
@EmotiveImpact
EmotiveImpact marked this pull request as ready for review October 12, 2026 02:00
@EmotiveImpact
EmotiveImpact merged commit 3673db4 into main Oct 12, 2026
6 of 7 checks passed
@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:01:56.875515Z beb52ba 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 — beb52ba2 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