Repository navigation
fix(web): keep typed percents, survive offline saves, validate search params - #36
Merged
Merged
Conversation
Typing a percent used to rebalance everyone else in proportion, and the fields only committed on blur or Enter, so 50 / 30 / 20 could save as 48.7 / 31.3 / 20 and fast filling as 0 / 80 / 20, all while the status read 100%. Each field now passes on every keystroke and changes only that person; the status says how much is left to assign or how much too much, and saving waits for 100%. Cents are only worked out from the percents, by the same largest remainder rule as shares and adjustments, now pinned by shared cases in core and pgTAP. Co-authored-by: Ferrin <177692050+Ferrin-Y@users.noreply.github.com> Co-authored-by: noorelmosrati <182049385+noorelmosrati@users.noreply.github.com>
/search?q=30 took the whole app down: the router reads the address as JSON, the validator dropped the number, and TanStack lays validated params over the raw ones, so q still reached the screen as 30 and crashed on trim(). Text params now take numbers and booleans back as text and every validator returns all of its keys, undefined when dropped, so a raw value can never show through. A screen that throws now shows our own page with Try again and Go home, inside the app frame, instead of the router's bare "Something went wrong!". Co-authored-by: Ferrin <177692050+Ferrin-Y@users.noreply.github.com> Co-authored-by: noorelmosrati <182049385+noorelmosrati@users.noreply.github.com>
A small banner shows on every screen while the browser is offline. The expense and payment forms turn their save button off with a note that what was typed stays put, and the tab asks before closing while that entry would be lost. A save already on its way when the connection drops is paused by React Query; it now says it will go through once the connection is back instead of spinning with no word. React Query also starts from navigator.onLine rather than assuming it is online. Co-authored-by: Ferrin <177692050+Ferrin-Y@users.noreply.github.com> Co-authored-by: noorelmosrati <182049385+noorelmosrati@users.noreply.github.com>
The category row (and its subcategory row) is now a proper radio group: one Tab stop on the picked chip, arrow keys, Home and End to move and pick, instead of sixteen Tab presses. The split chips, rows and slider list you first and then everyone in member order, as the by item chips already did, and the by item chips use the same avatar size, so initials no longer switch between "O" and "OO" when the split type changes. Co-authored-by: Ferrin <177692050+Ferrin-Y@users.noreply.github.com> Co-authored-by: noorelmosrati <182049385+noorelmosrati@users.noreply.github.com>
The count sat inside the label element, so the field read "Receipts and photos1 of 5" to screen readers and in copied text. The label is now just the label, the count is separate text with a real space before it, and the list takes the count as its description. Co-authored-by: Ferrin <177692050+Ferrin-Y@users.noreply.github.com> Co-authored-by: noorelmosrati <182049385+noorelmosrati@users.noreply.github.com>
Co-authored-by: Ferrin <177692050+Ferrin-Y@users.noreply.github.com> Co-authored-by: noorelmosrati <182049385+noorelmosrati@users.noreply.github.com>
Co-authored-by: Ferrin <177692050+Ferrin-Y@users.noreply.github.com> Co-authored-by: noorelmosrati <182049385+noorelmosrati@users.noreply.github.com>
Co-authored-by: Ferrin <177692050+Ferrin-Y@users.noreply.github.com> Co-authored-by: noorelmosrati <182049385+noorelmosrati@users.noreply.github.com>
Co-authored-by: Ferrin <177692050+Ferrin-Y@users.noreply.github.com> Co-authored-by: noorelmosrati <182049385+noorelmosrati@users.noreply.github.com>
musa-kal
force-pushed
the
fix/entry-search-offline
branch
from
November 14, 2026 07:52
3582d66 to
bad17f9
Compare
This branch was successfully 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.
Intent
Fixes from the full-app dogfood pass:
Must keep: money in integer minor units, i18n for every string, no em or en dashes.
What Changed
typePercentreplaces the rebalancingsetPercent). The status shows how much is left or over, and the form will not save until the total is exactly 100%. People appear viewer first, in the same order with the same initials, in every split mode. Category and subcategory chips are each a single Tab stop and use arrow keys to move. The receipts count is now separate text that describes the list, not part of the label. The leftover cent rule (largest remainder, ties go to the smaller user id) is pinned by matching cases inpackages/core(split-remainder.test.ts) and pgTAP (31_split_rounding.test.sql). No SQL logic changed.useOnlineanduseWarnBeforeUnloadhooks (lib/online.ts). An app-wideOfflineBannersits in the root layout. React Query'sonlineManageris set fromnavigator.onLineat startup. The expense and settle forms disable Save while offline and keep what was typed. They ask before the tab closes while an unsaved entry is at risk. A save that fails with a network error showsOfflineSaveNotewith Try again and refetches group money, and is not retried automatically. Newapp.offline.*strings are added in en, de, es, fr and pt.stringParamandbooleanParamnow return every key they own and turn numbers and booleans back into text, so/search?q=30searches for "30" instead of crashing. The expenses, insights and new-plan search params use the same validators. A new styledRouteErrorpage with Try again and Go home is the root and default error component. Unit tests cover all of this, and the expense-entry, expenses and insights e2e specs plus the docs are updated.🤖 Generated with Claude Code
Risk Assessment
✅ Low: Client-only UI changes with no SQL logic change. Round 2 now classifies a failure once in onError and invalidates the group's money keys (settlements and expenses sit under moneyKeys.group), and I traced it as correct for both forms. The percent, search-param, category and route-error changes behave as the intent and the later recorded decisions require. The only remaining item is a stale comment.
Testing
Pipeline
Updates from git push no-mistakes
✅ **Intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
apps/web/src/features/expenses/form/expense-form.tsx:557- Intent item 3 says 'a save caught by a dropped connection says it will go through when back', but the new paused path is almost never reachable.useOnline()and React Query'sonlineManagerfollow the same window online/offline events, and both forms now return early when!online(expense-form.tsx:563, settle-flow.tsx:370). Somutate()only starts paused in the sub-frame race between the offline event and the re-render. The real sequence is different: the connection drops while the create_expense/record-payment request is in flight,fetchrejects, and mutations have no retry (query-client.ts only setsqueries.retry). The mutation then errors instead of pausing,isPausedstays false, andOfflineSaveNotenever showsapp.offline.paused. The user gets the pre-existingerrors.networktoast ('We will retry when you are back'), and that claim is also false: nothing retries the mutation. The entry is kept and the user can save again by hand. The unit test 'says a save caught by the connection dropping will go through' (expense-form.test.tsx:507) only passes because it callsonlineManager.setOnline(false)without the browser event, a state real usage does not produce. The same gap exists at settle-flow.tsx:366 (pay.isPaused). The smallest honest fix would retry non-idempotent creates on network errors, which needs idempotency keys or server-side dedupe to avoid duplicate expenses or payments. That fix extends scope, so it needs the author's decision. The alternative is to narrow the claim and the copy to what actually happens.🔧 Fix applied.
3 issues (1 warning, 2 infos) still open:
apps/web/src/features/expenses/form/expense-form.tsx:557- Intent item 3 says 'a save caught by a dropped connection says it will go through when back', but the new paused path is almost never reachable.useOnline()and React Query'sonlineManagerfollow the same window online/offline events, and both forms now return early when!online(expense-form.tsx:563, settle-flow.tsx:370). Somutate()only starts paused in the sub-frame race between the offline event and the re-render. The real sequence is different: the connection drops while the create_expense/record-payment request is in flight,fetchrejects, and mutations have no retry (query-client.ts only setsqueries.retry). The mutation then errors instead of pausing,isPausedstays false, andOfflineSaveNotenever showsapp.offline.paused. The user gets the pre-existingerrors.networktoast ('We will retry when you are back'), and that claim is also false: nothing retries the mutation. The entry is kept and the user can save again by hand. The unit test 'says a save caught by the connection dropping will go through' (expense-form.test.tsx:507) only passes because it callsonlineManager.setOnline(false)without the browser event, a state real usage does not produce. The same gap exists at settle-flow.tsx:366 (pay.isPaused). The smallest honest fix would retry non-idempotent creates on network errors, which needs idempotency keys or server-side dedupe to avoid duplicate expenses or payments. That fix extends scope, so it needs the author's decision. The alternative is to narrow the claim and the copy to what actually happens.apps/web/src/features/expenses/form/expense-form.tsx:560- Introduced by fix round 1.unreachedis worked out on every render asm.isError && isNetworkError(m.error).isNetworkErrorreturns true for any error whilenavigator.onLine === false(errors.ts:52), so a mutation that failed for a server reason gets relabeled later. Example: Save is refused withsplit_member_not_in_groupand the toast shows. The phone then loses signal, the offline event re-renders the form, andunreachedbecomes true. The note now says 'Could not reach Potluck... try again' with a disabled Try again, instead of the plainapp.offline.formtext, and beforeunload is armed for that reason. When the connection comes back the note disappears again. Sibling site: settle-flow.tsx:369 (pay.isError && isNetworkError(pay.error)). Fix: classify the failure once, when it happens. For example, set a flag inonError(cleared inonMutate), or check only the error's shape (FETCH_FAILURES) for the failed state, so a refusal stays a refusal after going offline.apps/web/src/features/shell/offline.tsx:51- This is a tradeoff left by the fix the user chose in round 1. A fetch that rejects after the server committedcreate_expense/record_payment(for example, the connection drops while the response is coming back) still shows 'Could not reach Potluck. What you typed is still here'. Try again (expense-form.tsx:597, settle-flow.tsx:428) then creates a second expense or payment. The docs added in development.md:303 say saves are not retried automatically because a request that did arrive would be saved twice, but a manual Try again carries the same risk. Without idempotency keys this is acceptable. No change needed unless the author wants softer copy, such as 'may not have saved; check before trying again'.🔧 Fix applied.
1 info still open:
apps/web/src/lib/online.ts:13- Stale comments left by fix round 1, which removed the paused save path. TheuseOnlinedoc says React Query's onlineManager follows the same events "so a save that would pause offline and this flag agree", and query-client.ts:18 says opening offline should "pause queries and saves". Saves no longer pause: both forms return early while offline (expense-form.tsx onSubmit, settle-flow.tsx submit), and a failed save is classified in onError. The onlineManager seeding still matters for queries. Fix: reword both comments to cover queries only, so nobody brings back the removed paused branch on the strength of them.⏭️ **Test** - skipped
Step was skipped.
✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.