feat(streak): store the user's frozen rest days #85

Merged
raisondetredev merged 1 commit from refs/pull/85/head into main 2026-08-05 12:56:26 +02:00
raisondetredev commented 2026-08-05 12:56:18 +02:00 (Migrated from codeberg.org)

The Home streak counts four things: flashcard reviews, learning-path
step completions, hand-authored card creations, and the days the user
froze as rest days. The first three sync. The fourth had no server table
at all, so a rest day only ever existed on the device that set it.

The effect is a streak that disagrees across devices, and disagrees by a
lot rather than by one: a frozen day bridges a gap in the activity
record, so the device that knows about it counts straight through while
the other stops dead at the gap. A user with a single zero-activity day
three weeks back sees a three-week streak on the phone and a four-day
one on the web.

id is derived, not random: '<user_id>:<YYYY-MM-DD>', enforced by a
CHECK. A frozen day is set membership, so two devices freezing the same
date offline have to converge on one row. With client-generated UUIDs
they would each insert their own, and a later un-freeze would clear one
and leave the other — the day would stay frozen, and no amount of
syncing would fix it. The same natural-key trick studyset_study_settings
uses for user:studyset.

date is a DATE rather than a timestamp. The frozen unit is a local
calendar day as the user lived it; storing an instant would re-open the
timezone question on every device that reads it, and a rest day taken in
Berlin is that same day everywhere.

Un-freezing is a soft delete. Dropping the row would leave the other
devices still holding the day, and the next pull from one of them would
push it straight back — the delete has to be a change that propagates,
not an absence.

Realtime, for the reason reviews were opted in: the streak is a
dashboard surface, and a rest day marked on the phone should reach an
open web app rather than wait for a relaunch. RLS applies to Realtime
too, so each device receives only its own rows.

Signed-off-by: Tizian 「ティツィアーン」 raisondetredev@pm.me

The Home streak counts four things: flashcard reviews, learning-path step completions, hand-authored card creations, and the days the user froze as rest days. The first three sync. The fourth had no server table at all, so a rest day only ever existed on the device that set it. The effect is a streak that disagrees across devices, and disagrees by a lot rather than by one: a frozen day bridges a gap in the activity record, so the device that knows about it counts straight through while the other stops dead at the gap. A user with a single zero-activity day three weeks back sees a three-week streak on the phone and a four-day one on the web. `id` is derived, not random: `'<user_id>:<YYYY-MM-DD>'`, enforced by a CHECK. A frozen day is set membership, so two devices freezing the same date offline have to converge on one row. With client-generated UUIDs they would each insert their own, and a later un-freeze would clear one and leave the other — the day would stay frozen, and no amount of syncing would fix it. The same natural-key trick studyset_study_settings uses for `user:studyset`. `date` is a DATE rather than a timestamp. The frozen unit is a local calendar day as the user lived it; storing an instant would re-open the timezone question on every device that reads it, and a rest day taken in Berlin is that same day everywhere. Un-freezing is a soft delete. Dropping the row would leave the other devices still holding the day, and the next pull from one of them would push it straight back — the delete has to be a change that propagates, not an absence. Realtime, for the reason reviews were opted in: the streak is a dashboard surface, and a rest day marked on the phone should reach an open web app rather than wait for a relaunch. RLS applies to Realtime too, so each device receives only its own rows. Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
project-halkyon/halkyon-infra!85
No description provided.