Skip to content

Commit dcffd4d

Browse files
pepperonasclaude
andcommitted
fix: auto-migrate pre-backfill achievement data (one-time per user)
Users whose unlocks predate the historical backfill still carried hundreds of achievements stamped on their first init/sync day (e.g. 216 on the multi-user first-sync date). A metadata flag (ach_backfill_v1_<userId>) now triggers exactly one historical recompute per user: - single-user: at startup (also covers existing distorted installs) - multi-user: on first GET /api/achievements after deploy - manual recompute endpoint sets the flag too Guarded on messageCount > 0 so an empty aggregator never wipes unlocks. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 85a747d commit dcffd4d

1 file changed

Lines changed: 20 additions & 1 deletion

File tree

server.js

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,15 +138,20 @@ if (!MULTI_USER) {
138138

139139
// DB helper for achievements module
140140
const achievementsDb = { getUnlockedAchievements, unlockAchievementsBatch, unlockAchievementsBatchAt, clearAchievementsForUser, replaceAchievementsForUser };
141+
// Metadata-flag prefix for the one-time historical achievements backfill (per user)
142+
const ACH_BACKFILL_FLAG = 'ach_backfill_v1_';
141143

142144
// 5. Check achievements on startup (single-user). A FRESH install with an
143145
// existing Claude history would bulk-unlock hundreds of achievements stamped
144146
// "now" — replay the history instead so unlock dates land on the day each
145147
// condition was actually first met (no distorted timeline).
146148
if (!MULTI_USER) {
147149
try {
148-
if (getUnlockedAchievements(0).length === 0 && aggregator.messageCount > 0) {
150+
const needsBackfill = aggregator.messageCount > 0 &&
151+
(getUnlockedAchievements(0).length === 0 || !getMetadata(ACH_BACKFILL_FLAG + 0));
152+
if (needsBackfill) {
149153
const res = achievements.backfillAchievements(aggregator, 0, achievementsDb);
154+
setMetadata(ACH_BACKFILL_FLAG + 0, new Date().toISOString());
150155
console.log(`Backfilled ${res.unlocked} achievements with historical dates across ${res.days} days (${res.from}${res.to})`);
151156
} else {
152157
const newAch = achievements.checkAchievements(aggregator, 0, achievementsDb);
@@ -1255,6 +1260,7 @@ const server = http.createServer((req, res) => {
12551260
const fullAgg = MULTI_USER ? aggregatorCache.get(user.id, null) : aggregator;
12561261
try {
12571262
const result = achievements.backfillAchievements(fullAgg, achUserId, achievementsDb);
1263+
setMetadata(ACH_BACKFILL_FLAG + achUserId, new Date().toISOString());
12581264
return sendJSON(res, { recomputed: true, ...result });
12591265
} catch (e) {
12601266
return sendJSON(res, { error: e.message }, 500);
@@ -1627,6 +1633,19 @@ const server = http.createServer((req, res) => {
16271633
// Achievements endpoint
16281634
if (pathname === '/api/achievements') {
16291635
const userId = MULTI_USER ? user.id : 0;
1636+
// One-time migration: users whose unlocks predate the historical backfill
1637+
// carry hundreds of achievements stamped on their first init/sync day.
1638+
// Recompute once from history, then never again (metadata flag).
1639+
if (!getMetadata(ACH_BACKFILL_FLAG + userId)) {
1640+
try {
1641+
const fullAgg = MULTI_USER ? aggregatorCache.get(user.id, null) : aggregator;
1642+
if (fullAgg.messageCount > 0) {
1643+
const res2 = achievements.backfillAchievements(fullAgg, userId, achievementsDb);
1644+
setMetadata(ACH_BACKFILL_FLAG + userId, new Date().toISOString());
1645+
console.log(`Achievements backfill migration (user ${userId}): ${res2.unlocked} unlocks re-dated across ${res2.days} days`);
1646+
}
1647+
} catch (e) { console.error('Achievements backfill migration failed:', e.message); }
1648+
}
16301649
return sendJSON(res, achievements.getAchievementsResponse(userId, achievementsDb));
16311650
}
16321651

0 commit comments

Comments
 (0)