Bug: Milestone Achievements Use localStorage as Deduplication Guard — Causing Duplicate Activity Feed Spam Across Devices
Describe the bug
Milestone achievements (e.g. "🎉 Read 1000 pages!", "🏆 Completed 10 books!") are posted to the Supabase activity feed multiple times if the user logs in from a second device, clears browser storage, or uses incognito mode.
Root Cause
In apps/web/src/pages/Stats.jsx, the functions getAchievedMilestones() and markMilestoneAchieved() (lines 11–27) use localStorage under the key bookcat_achieved_milestones as the sole guard against re-triggering milestones. Since localStorage is browser/device-local, this deduplication does not persist across devices or sessions. Every new device or cleared browser will re-fire all milestones and call logMilestone() again, writing duplicate entries into Supabase.
Steps to Reproduce
- Use the app on Device A until a milestone is reached (e.g. complete 5 books) — milestone posts to activity feed
- Log into the same account on Device B (or clear localStorage on Device A)
- Open the Stats page
- The same milestone fires again and a duplicate entry appears in the activity feed
Expected Behavior
Each milestone should only ever be logged once per user, regardless of device or browser. The deduplication guard must live server-side, not in localStorage.
Suggested Fix
Move milestone deduplication to the database using one of these approaches:
Option A — Add a dedicated table:
Create a user_milestones table in Supabase with a unique constraint on (user_id, milestone_key). Before calling logMilestone(), insert into this table. If the insert fails due to the unique constraint, skip logging.
Option B — Query existing activities:
Before calling logMilestone(), query the activities table to check whether an activity with the same milestone_type and value metadata already exists for the user. Only log if no match is found.
localStorage can still be used as a fast client-side cache to avoid redundant DB lookups within a session, but the source of truth must be Supabase.
Files affected
apps/web/src/pages/Stats.jsx — getAchievedMilestones(), markMilestoneAchieved(), isMilestoneAchieved(), checkMilestones() (lines 11–60)
I am a GSSoC 2026 contributor and would love to work on this.
Bug: Milestone Achievements Use localStorage as Deduplication Guard — Causing Duplicate Activity Feed Spam Across Devices
Describe the bug
Milestone achievements (e.g. "🎉 Read 1000 pages!", "🏆 Completed 10 books!") are posted to the Supabase activity feed multiple times if the user logs in from a second device, clears browser storage, or uses incognito mode.
Root Cause
In
apps/web/src/pages/Stats.jsx, the functionsgetAchievedMilestones()andmarkMilestoneAchieved()(lines 11–27) uselocalStorageunder the keybookcat_achieved_milestonesas the sole guard against re-triggering milestones. SincelocalStorageis browser/device-local, this deduplication does not persist across devices or sessions. Every new device or cleared browser will re-fire all milestones and calllogMilestone()again, writing duplicate entries into Supabase.Steps to Reproduce
Expected Behavior
Each milestone should only ever be logged once per user, regardless of device or browser. The deduplication guard must live server-side, not in localStorage.
Suggested Fix
Move milestone deduplication to the database using one of these approaches:
Option A — Add a dedicated table:
Create a
user_milestonestable in Supabase with a unique constraint on(user_id, milestone_key). Before callinglogMilestone(), insert into this table. If the insert fails due to the unique constraint, skip logging.Option B — Query existing activities:
Before calling
logMilestone(), query theactivitiestable to check whether an activity with the samemilestone_typeandvaluemetadata already exists for the user. Only log if no match is found.localStorage can still be used as a fast client-side cache to avoid redundant DB lookups within a session, but the source of truth must be Supabase.
Files affected
apps/web/src/pages/Stats.jsx—getAchievedMilestones(),markMilestoneAchieved(),isMilestoneAchieved(),checkMilestones()(lines 11–60)I am a GSSoC 2026 contributor and would love to work on this.