fix: re-derive POST day keys from record timestamps at read time (#4168) - #4308
Merged
Conversation
A stored POST `date` is frozen in the timezone that was configured when it was written, so changing `settings.timezone` after accumulating history left the old day keys disagreeing with the new-zone readers — a session saved as one day read as "not today" after the move, breaking streaks, stats windows and history. The readers now key each record off the instant it happened (`startedAt`/`completedAt`/`timestamp`) via the new `recordDayKey` / `withDerivedDayKeys` in `server/lib/postStreak.js`, making the stored `date` a cache they ignore. This is the same read-time derivation the reminder path has always used (`meatspacePostReminder.js` `isOnLocalDay`). Derivation happens at the read boundaries — `getPostSessions`, `getAllTrainingEntries`, and `getMorseProgress` — so every server reader and the client see one timezone-current day key. The write paths are untouched, and a legacy record carrying no instant keeps its authored day.
getPostSession returned the raw record, so the detail view quoted the frozen write-time day while the history row beside it showed the re-derived one.
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.
Summary
A POST record's stored
dateis frozen in whatever timezone was configured when it was written. #2681 fixed the steady state (write and read both use the configured zone), but left a residual gap: once the user changessettings.timezone, every existing day key disagrees with the new-zone readers — a session saved as2026-07-15in one zone reads as "not today" after a move, breaking the streak, the stats window and the history list.The streak/stats/history readers now re-derive each record's day key from the instant it happened (
startedAt→completedAt→timestamp) instead of trusting the storeddate, which becomes a cache they ignore. This is the same read-time derivation the reminder path has always used (meatspacePostReminder.jsisOnLocalDay).server/lib/postStreak.js— newrecordDayKey(record, timezone)andwithDerivedDayKeys(records, timezone).computePostStreaks/computeUnifiedStreakkey off them, andcomputeUnifiedStreak's activity projection now carries the instants through instead of stripping them to a bare{ date }.getPostSessions(meatspacePost.js),getAllTrainingEntries(postTrainingLogStore.js), andgetMorseProgress(meatspacePostMorse.js).getPostSessionsalso re-sorts, since re-derivation can move a record across a day boundary.getPostStats,getPostProgress,getUnifiedActivityStreak,getTrainingStats,getMorseProgress, plus the mastery-window aggregations andpracticedTodayFromActivity) now callrecordDayKeydirectly rather thannormalizeYmd(x?.date, tz), so a caller passing raw records is covered too.loadSessions/loadTrainingLogstay raw so submits still round-trip the stored record. No on-disk change, so no migration.datestill wins — absent must not collapse into "UTC" and silently re-key a whole history.Closes #4168
Test plan
cd server && NODE_ENV=test npx vitest run— 1408 files / 29500 tests pass. The only 2 failures areroutes/health.test.js(gh-CLI forge reachability + PM2 process health), reproduced identically on a cleanmaincheckout, so they are pre-existing and environment-dependent.cd client && NODE_ENV=test npx vitest run— 650 files / 7938 tests pass.server/lib/postStreak.test.js—recordDayKeyfield precedence, epoch-ms instants, the no-instant and no-timezone fallbacks, non-mutation inwithDerivedDayKeys, and a unified-streak case where the same history reads correctly in both the original and the new zone.server/services/meatspacePost.test.js— a new#4168block holding LA-written history fixed and flipping only the setting:getPostSessionsre-keys,getPostStatsreportscompletedToday/todayScoreagainst the new zone, thefrom/torange filter matches the re-derived days, the same history still reads correctly back in the original zone, and a legacy no-instant record stays on its authored day.server/services/meatspacePostTraining.test.js—getTrainingStatsstreak/active-days/window andgetTrainingEntriesdisplay dates after a zone change.server/services/meatspacePostMorse.test.js—getMorseProgresswindow cutoff and emitted series re-keyed from each round'stimestamp.