fix: timezone-safe streak continuity with 48h elapsed-time window
Some checks failed
CI / update (push) Has been cancelled

Use local dates instead of UTC for day boundaries, and store an epoch
timestamp alongside the date string. Streak alive check uses real
elapsed time (<48h) which covers dateline crossings. Old data without
timestamps falls back to date-string comparison so existing streaks
are preserved.
This commit is contained in:
2026-04-06 00:36:02 +02:00
parent 082202b71c
commit c5710ff72d
6 changed files with 141 additions and 49 deletions

View File

@@ -19,6 +19,7 @@ export const GET: RequestHandler = async ({ locals }) => {
return json({
streak: streak?.streak ?? 0,
lastComplete: streak?.lastComplete ?? null,
lastCompleteTs: streak?.lastCompleteTs ?? null,
todayPrayed: streak?.todayPrayed ?? 0,
todayDate: streak?.todayDate ?? null
});
@@ -34,7 +35,7 @@ export const POST: RequestHandler = async ({ request, locals }) => {
throw error(401, 'Authentication required');
}
const { streak, lastComplete, todayPrayed, todayDate } = await request.json();
const { streak, lastComplete, lastCompleteTs, todayPrayed, todayDate } = await request.json();
if (typeof streak !== 'number' || streak < 0) {
throw error(400, 'Valid streak required');
@@ -55,15 +56,21 @@ export const POST: RequestHandler = async ({ request, locals }) => {
await dbConnect();
try {
const updateFields: Record<string, unknown> = { streak, lastComplete, todayPrayed, todayDate };
if (typeof lastCompleteTs === 'number') {
updateFields.lastCompleteTs = lastCompleteTs;
}
const updated = await AngelusStreak.findOneAndUpdate(
{ username: session.user.nickname },
{ streak, lastComplete, todayPrayed, todayDate },
updateFields,
{ upsert: true, new: true }
).lean() as any;
return json({
streak: updated?.streak ?? 0,
lastComplete: updated?.lastComplete ?? null,
lastCompleteTs: updated?.lastCompleteTs ?? null,
todayPrayed: updated?.todayPrayed ?? 0,
todayDate: updated?.todayDate ?? null
});

View File

@@ -18,7 +18,8 @@ export const GET: RequestHandler = async ({ locals }) => {
return json({
length: streak?.length ?? 0,
lastPrayed: streak?.lastPrayed ?? null
lastPrayed: streak?.lastPrayed ?? null,
lastPrayedTs: streak?.lastPrayedTs ?? null
});
} catch (e) {
throw error(500, 'Failed to fetch rosary streak');
@@ -32,7 +33,7 @@ export const POST: RequestHandler = async ({ request, locals }) => {
throw error(401, 'Authentication required');
}
const { length, lastPrayed } = await request.json();
const { length, lastPrayed, lastPrayedTs } = await request.json();
if (typeof length !== 'number' || length < 0) {
throw error(400, 'Valid streak length required');
@@ -45,15 +46,21 @@ export const POST: RequestHandler = async ({ request, locals }) => {
await dbConnect();
try {
const updateFields: Record<string, unknown> = { length, lastPrayed };
if (typeof lastPrayedTs === 'number') {
updateFields.lastPrayedTs = lastPrayedTs;
}
const updated = await RosaryStreak.findOneAndUpdate(
{ username: session.user.nickname },
{ length, lastPrayed },
updateFields,
{ upsert: true, new: true }
).lean() as any;
return json({
length: updated?.length ?? 0,
lastPrayed: updated?.lastPrayed ?? null
lastPrayed: updated?.lastPrayed ?? null,
lastPrayedTs: updated?.lastPrayedTs ?? null
});
} catch (e) {
throw error(500, 'Failed to update rosary streak');