Skip to content

fix(bring): sync the Bring! list into the shopping list, and renew the token (#407) - #408

Merged
svenger87 merged 3 commits into
mainfrom
fix/bring-pull-sync-407
Oct 16, 2026
Merged

svenger87 merged 3 commits into
mainfrom
fix/bring-pull-sync-407

Conversation

@svenger87

@svenger87 svenger87 commented Oct 16, 2026 •

Copy link
Copy Markdown
Owner

Summary

Fixes #407. Bring! sync only went one way, Kinboard → Bring!. Nothing ever read the Bring! list back into shopping_items, so items added in the Bring! app never showed up and items bought there stayed open. The access token also ran out after about a week, and pushes then failed silently.

  • Pull sync (lib/bring-sync.ts): planBringSync is a pure planner, and syncBringFamily applies its plan. The network and the database are injected so the logic can be tested without either. Two pieces of per-item state make the reconcile unambiguous:
    • bring_item_id links an item to its Bring! key, which is the German catalogue name, e.g. Milch. Ticks and deletes on the shopping page now send that key, which fixes the Milk vs Milch gap from the issue.
    • bring_checked is a new column holding the ticked state last agreed with Bring!. If checked differs from it, Kinboard changed the item since, so Kinboard's change is pushed. This covers ticks from shop mode, assistants and the offline queue, which never told Bring! and would otherwise be undone. If the two match, any difference comes from Bring!, and Bring! wins.
  • Deletes from any path: bring_removals is a server-only table that a trigger fills. It records open, linked items deleted in Kinboard, so the next sync takes them off Bring! instead of reading them back in. The trigger skips the family-delete cascade; I tested that against Postgres 16.
  • When the sync runs: /api/cron/sync-bring runs every 2 minutes via ofelia, for families with Auto sync on. /api/bring/sync runs for Sync now and when the shopping page opens. A per-family queue stops the two from inserting the same item twice.
  • Token renewal: every Bring! call goes through withBringAccess. On a 401 it calls bringauth/token with the stored refresh token, saves the new token in integration_secrets, and retries once.
  • Language: pulled catalogue items show in the family's language, using catalog.<bcp47>.json.
  • Two-way sync off: Kinboard mirrors Bring!. Nothing is pushed, and Bring! wins any disagreement. This matches the wiki.
  • The wiki's "How merge conflicts resolve" section described behaviour that didn't exist. It's now "How the sync works".

Found in review, fixed here

  • Picking another Bring! list ticked off the whole open list. Every open item was still linked to the old list, and none of them is on the new one, so the sync took them as bought. Links now record their list (shopping_items.bring_list_id, also on bring_removals). A link to another list counts as no link: the item is pushed (two-way) or left alone (one-way), and a deletion recorded against the old list is not taken off the new one.
  • Past 1000 rows, the same Bring! item was added every two minutes. The sync read the oldest 1000 rows, and ticked items are never cleared. It now reads every open item, and refuses rather than plan from part of the list past 1000, plus the newest 1000 ticked ones.
  • The first sync for a family that already has a list deletes nothing and ticks nothing off. Unlinked open items are pushed (two-way) or kept (one-way).

Verification: three new cases. bring-sync + rls-coverage 81/81 on desktop, mobile and webkit. With the list check switched off, the list-switch case fails in all three. The migration applies twice on a local stack, the trigger records the list, and both PostgREST reads answer 200.

Type of change

  • Bug fix
  • New feature
  • Refactor (no user-facing change)
  • Documentation
  • Translation
  • Plugin / integration

Test plan

  • e2e/bring-sync.spec.ts, 22 cases: added, bought and re-added in the app; translations; a tick from another screen is pushed, not undone; untick; delete; one-way mode; a failed push leaves the rows alone; two syncs of one family don't overlap; wiring
  • Full stack-free spec suite as CI runs it: everything passes after rls-coverage learned about bring_removals
  • Migration applied twice against a local Postgres 16. As anon, a delete records only open, linked rows, and anon cannot read bring_removals. Deleting a family still cascades.
  • npm run lint, npx tsc --noEmit
  • Not exercised: a real Bring! account (no credentials here). Worth a manual check: add an item in the Bring! app and see it on /shopping within 2 minutes, buy it in the app and see it ticked off, wait past token expiry and see a push still go through.

Screenshots

No UI changes beyond the Sync now spinner covering the sync.

Checklist

  • npm run lint passes
  • New strings added to both messages/en.json and messages/de.json (no new strings)
  • Schema changes ship as a new webapp/docker/migration*.sql file (idempotent — IF NOT EXISTS)
  • No secrets, real API keys, or personal hostnames in the diff
  • I agree to the licensing of contributions
  • CHANGELOG.md updated under [Unreleased] (Fixed)
  • Commit follows Conventional Commits

Comment thread webapp/src/lib/bring-server.ts Fixed
Comment thread webapp/src/lib/bring-server.ts Fixed
…e token (#407)

Bring! sync only ever pushed. Items added in the Bring! app never reached
shopping_items and items bought there stayed open in Kinboard.

- lib/bring-sync.ts: a pure planner plus apply step that reconciles the
  selected Bring! list with shopping_items. bring_item_id links an item to
  its Bring! key (the German catalogue name); the new bring_checked column
  records the ticked state last agreed with Bring!, so a tick from shop
  mode, an assistant or the offline queue is pushed instead of undone.
- migration_bring_pull_sync.sql: bring_checked, plus a server-only
  bring_removals table filled by a trigger, so an open item deleted from
  any path is taken off Bring! rather than read back in.
- /api/cron/sync-bring every 2 minutes (families with Auto sync on),
  /api/bring/sync for "Sync now" and the shopping page on open.
- Access token renewal via bringauth/token on a 401, for every Bring! call.
- Pulled items show in the family's language; ticks and deletes on the
  shopping page use the Bring! key.
…the URL

The session routes take listId from the request and it becomes part of
the Bring! URL path (CodeQL: server-side request forgery).
@svenger87
svenger87 force-pushed the fix/bring-pull-sync-407 branch from 566abbd to 65d3a77 Compare October 16, 2026 15:31
@svenger87
svenger87 marked this pull request as ready for review October 16, 2026 15:31
… read

Review of the pull sync found two ways it could go wrong for a family.

Picking another Bring! list (or signing in to another account) left every
open item linked to the old list. None of them is on the new one, so the next
sync took them as bought there and ticked the whole open list off. Links now
record their list (shopping_items.bring_list_id, and bring_removals too); a
link to another list counts as no link, so the item is pushed (two-way) or
left alone (one-way), and a deletion recorded against the old list is not
taken off the new one.

The sync read the oldest 1000 rows. Ticked items are never cleared, so a
household past 1000 rows had its new items fall outside that window: every
item open on Bring! looked new and was inserted again on every run, every two
minutes. It now reads every open item (refusing, rather than planning from
part of the list, past 1000) plus the newest 1000 ticked ones.

Verification: bring-sync.spec 3 new cases, 81/81 with rls-coverage across
desktop, mobile and webkit; with the list check switched off the list-switch
case fails in all three. Migration applied twice on a local stack without
error; the trigger records the list; both PostgREST queries answer 200.
tsc and eslint clean.
@svenger87
svenger87 merged commit b80f043 into main Oct 16, 2026
14 checks passed
@svenger87
svenger87 deleted the fix/bring-pull-sync-407 branch October 16, 2026 16:07
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.

Bring! sync is push-only: items added in the Bring! app never appear in Kinboard's shopping list

2 participants