Skip to content

feat: sync progression across devices, rebalance hunt XP - #10

Merged
Amayyas merged 5 commits into
mainfrom
feat/progression-sync
Jul 23, 2026
Merged

feat: sync progression across devices, rebalance hunt XP#10
Amayyas merged 5 commits into
mainfrom
feat/progression-sync

Conversation

@Amayyas

@Amayyas Amayyas commented Jul 22, 2026

Copy link
Copy Markdown
Owner

What

Delivers the cross-device progression the specification lists as deliverable 5, which M10 left unfinished: a signed-in player's XP, level, badges and statistics lived only in the browser, so a second device — or a cleared cache — showed a blank profile.

Changes

  • progression table (new migration): one row per account, private to its owner under RLS, holding XP, the statistics as one JSON document, and the unlocked badges. The worldwide leaderboard still ranks scores, so this row drives only its owner's own dashboard.
  • Drops the vestigial profiles.xp/profiles.level: added in M10 but never read (every level is derived from the store), and kept out of the client's reach by the M10 grants, so they could only ever be a second stale copy.
  • Sync layer (useProgressionSync): on sign-in the account's row is pulled; the first time there is none it is seeded from the guest progress so it is kept; afterwards the server is the source of truth and a later sign-in on any device restores it. Each change is written back, debounced.
  • Robustness: a failed pull is treated as unknown rather than as an empty account, so a transient error can never overwrite a real server copy with an empty local one. The pull awaits the session first, since the first request after sign-in can otherwise outrun the token and be rejected — the same race is fixed on the profile fetch.
  • Hunt XP rebalanced: one point per hundred scored (was per ten), capped near a battle win, so one lucky round no longer jumps several levels.

Verification

  • Migration + RLS run against a local Postgres: seed, private reads (a player cannot read another's row), upsert-not-duplicate, cross-account isolation, guest denied.
  • Full end-to-end in a headless browser against the local stack: guest progress seeded on first sign-in, then fully restored on a device with its localStorage wiped — proving both the write and the read round-trips — with no console errors.
  • npm run ci green; 255 unit tests pass.

Summary by CodeRabbit

  • New Features
    • Progression now syncs across devices for signed-in accounts (XP, statistics, and unlocked badges).
    • Hunt XP rewards now use a per-100 scoring model and are capped at 60.
  • Bug Fixes
    • Improved authentication profile loading: waits for session readiness and avoids clearing the current profile when the fetch fails.
  • Documentation
    • Updated database setup guidance to run migration files in timestamp order and expanded the RLS verification to include the progression table.
  • Chores
    • Updated local secret ignore rules for Supabase/Google OAuth files.

Amayyas added 4 commits July 23, 2026 00:09
M10 shipped accounts, but a player's XP, level, badges and statistics
still lived only in the browser, so signing in on a second device showed
a blank profile — short of deliverable 5, which asks for progression
synced across devices.

Add a `progression` table, one row per account, private to its owner
under RLS. It holds the XP, the statistics as one JSON document, and the
unlocked badges. The worldwide leaderboard still ranks the `scores`
table, so this row drives only its owner's own dashboard.

Drop the now-single-source vestigial profiles.xp/level: they were added
in M10 but never read — every level shown is derived from the store —
and the M10 grants deliberately kept them out of the client's reach, so
nothing could keep them up to date anyway.

Also ignore the local Google credentials (supabase/.env, client_secret).
Mirror the progression store to the new table for a signed-in player.
On sign-in the account's row is pulled: the first time there is none it
is seeded from whatever was built as a guest, so that progress is kept;
after that the server is the source of truth and a later sign-in on any
device restores it. While signed in, each change is written back,
debounced into one request.

A failed pull is treated as unknown rather than as an empty account, so
a transient error can never overwrite a real server copy with the empty
local one. The pull also awaits the session first, since the very first
request after sign-in can otherwise outrun the token and be rejected —
the same guard is applied to the profile fetch, which had the same race.
The hunt paid one XP per ten points, so a strong sixty-second round was
worth several hundred XP — more than a dozen wins against the engine —
and jumped the player several levels at once. Pay one per hundred points
instead, capped near a battle win, so a round is worth about a game
rather than a whole evening of them.
The setup named only the init migration; with a second one added, tell
the reader to run each file in order and note that re-running is how a
later migration reaches a project that already has the earlier ones.
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f887c530-1fc1-4ee9-b628-734f3fe4fdea

📥 Commits

Reviewing files that changed from the base of the PR and between aa44849 and 5b991b8.

📒 Files selected for processing (1)
  • src/features/progression/useProgressionSync.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/features/progression/useProgressionSync.ts

📝 Walkthrough

Walkthrough

Adds a Supabase progression table with RLS, synchronizes signed-in progression between local state and Supabase, normalizes progression data, preserves profiles on fetch errors, and changes hunt XP to per-hundred scoring with a cap.

Changes

Progression synchronization

Layer / File(s) Summary
Progression database contract
supabase/migrations/*, src/lib/supabase.ts, README.md, .gitignore
Adds the progression table and owner-only RLS policies, updates shared types and migration instructions, removes obsolete profile XP fields, and ignores local OAuth secrets.
Snapshot translation and hydration
src/store/useProgressionStore.ts, src/features/progression/sync.ts, src/features/progression/sync.test.ts
Defines synced progression snapshots, normalizes row data, maps rows for upsert, hydrates synced store fields, and tests these conversions.
Application-level authenticated sync
src/features/progression/useProgressionSync.ts, src/App.tsx, src/store/useAuthStore.ts
Pulls or seeds progression on sign-in, debounces subsequent upserts, integrates the hook at the app root, and preserves profiles when profile loading fails.
Hunt XP scaling and cap
src/features/progression/levels.ts, src/features/progression/levels.test.ts
Changes hunt scoring to per-hundred XP and caps exceptional rounds at 60 XP, with updated tests.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant App
  participant useProgressionSync
  participant useProgressionStore
  participant Supabase
  App->>useProgressionSync: mount hook
  useProgressionSync->>Supabase: refresh session and fetch progression
  Supabase-->>useProgressionSync: return row or no row
  useProgressionSync->>useProgressionStore: hydrate snapshot
  useProgressionStore-->>useProgressionSync: emit progression changes
  useProgressionSync->>Supabase: debounced upsert
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly captures the two main changes: cross-device progression sync and hunt XP rebalancing.
Docstring Coverage ✅ Passed Docstring coverage is 83.33% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/progression-sync

Comment @coderabbitai help to get the list of available commands.

@Amayyas Amayyas self-assigned this Jul 22, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/features/progression/useProgressionSync.ts`:
- Around line 47-97: The progression sync flow must not enable writes after a
failed pull, since an unhydrated guest snapshot could overwrite server data;
keep `ready` false until the baseline is successfully read or a retry
establishes it. Update `write` and the subscription flow to serialize upserts so
only one write is in flight, and add version/conflict handling that prevents an
older full snapshot from being sent or committed after a newer local state.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d5a1c6ac-1b99-4505-8d0d-34300b425bbc

📥 Commits

Reviewing files that changed from the base of the PR and between e32247b and aa44849.

📒 Files selected for processing (12)
  • .gitignore
  • README.md
  • src/App.tsx
  • src/features/progression/levels.test.ts
  • src/features/progression/levels.ts
  • src/features/progression/sync.test.ts
  • src/features/progression/sync.ts
  • src/features/progression/useProgressionSync.ts
  • src/lib/supabase.ts
  • src/store/useAuthStore.ts
  • src/store/useProgressionStore.ts
  • supabase/migrations/20260722215455_progression.sql

Comment thread src/features/progression/useProgressionSync.ts Outdated
A write sends the whole progression row, so sending a stale one replaces
good server data — the earlier "upsert merges rather than replaces" note
was simply wrong. Two holes are closed:

A failed pull used to enable writes anyway, so the next local change
could upsert an unhydrated guest snapshot over a real server row. The
pull is now retried, and writes stay blocked until the baseline is
actually read or confirmed absent; a pull that keeps failing leaves the
session read-only rather than risk a clobber.

Writes were also unserialised, so two in-flight upserts could finish out
of order and an older snapshot could land last. Only one upsert now runs
at a time, looping once more on the freshest state if the store moved
while it ran, so the server always converges on the newest snapshot.

Two devices active in the very same second still resolve last-write-wins
on the whole row, which matches the "server is the source of truth on
sign-in" model and does not warrant per-field version vectors here.
@Amayyas
Amayyas merged commit acfb50d into main Jul 23, 2026
6 checks passed
@Amayyas
Amayyas deleted the feat/progression-sync branch July 23, 2026 06:23
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.

1 participant