Repository navigation
Load less and read again only what changed (app review batch 4) - #28
Merged
Merged
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
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
EmotiveImpact
force-pushed
the
claude/app-review-batch-4
branch
from
October 9, 2026 16:50
9627da6 to
beb52ba
Compare
Owner
Author
|
The Vercel preview for Generated by Claude Code |
EmotiveImpact
changed the base branch from
claude/app-review-batch-6a
to
main
October 12, 2026 02:00
EmotiveImpact
marked this pull request as ready for review
October 12, 2026 02:00
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. |
This branch had an error being deployed
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.
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
commune_expense_pageandcommune_expense_recordsserve older expenses, andcommune_summarygives 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.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_communerefuses an 11th commune in an hour, a 31st in a day and a 101st in all; communes the account has left still count.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
How it was checked
On the final branch:
run-local.sh, including the newlighter_loading.sqlandchange_mark_order.mjs; 2 more are skipped because they need a Neon branch.lighter-loading.mjs.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.mjsholds 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 thatindex.htmlnames the chunk the main bundle imports.🤖 Generated with Claude Code
https://claude.ai/code/session_01A6P7aTcMBccooYYr4xYnVS
Generated by Claude Code