import { json, error } from '@sveltejs/kit'; import type { RequestHandler } from './$types'; import { db } from '$lib/server/db'; import { dailyCompletions } from '$lib/server/db/schema'; import { desc } from 'drizzle-orm'; export const GET: RequestHandler = async ({ url }) => { const streakParam = url.searchParams.get('streak'); const localDate = url.searchParams.get('localDate'); if (!streakParam || !localDate) { error(400, 'Missing streak or localDate'); } const targetStreak = parseInt(streakParam, 10); if (isNaN(targetStreak) || targetStreak < 1) { error(400, 'Invalid streak'); } // Fetch all completions ordered by anonymous_id and date desc // so we can walk each user's history to compute their current streak. const rows = await db .select({ anonymousId: dailyCompletions.anonymousId, date: dailyCompletions.date, }) .from(dailyCompletions) .orderBy(desc(dailyCompletions.date)); // Group dates by user const byUser = new Map(); for (const row of rows) { const list = byUser.get(row.anonymousId); if (list) { list.push(row.date); } else { byUser.set(row.anonymousId, [row.date]); } } // Calculate the current streak for each user. // Start from today; if the user hasn't played today yet, try yesterday so // that streaks aren't zeroed out mid-day before the player has had a chance // to complete today's puzzle. const yesterday = new Date(`${localDate}T00:00:00`); yesterday.setDate(yesterday.getDate() - 1); const yesterdayStr = yesterday.toLocaleDateString('en-CA'); const thirtyDaysAgo = new Date(`${localDate}T00:00:00`); thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30); const thirtyDaysAgoStr = thirtyDaysAgo.toLocaleDateString('en-CA'); // For each user, compute their current streak and whether they've played // within the last 30 days. "Eligible players" = active streak OR recent play. const userStats: { streak: number; isEligible: boolean }[] = []; for (const [, dates] of byUser) { // dates are already desc-sorted const dateSet = new Set(dates); // Pick the most recent anchor: today if played, otherwise yesterday const anchor = dateSet.has(localDate) ? localDate : yesterdayStr; let streak = 0; let cursor = new Date(`${anchor}T00:00:00`); while (true) { const dateStr = cursor.toLocaleDateString('en-CA'); if (!dateSet.has(dateStr)) break; streak++; cursor.setDate(cursor.getDate() - 1); } const hasRecentPlay = dates.some((d) => d >= thirtyDaysAgoStr); userStats.push({ streak, isEligible: streak >= 1 || hasRecentPlay }); } const eligiblePlayers = userStats.filter((u) => u.isEligible); if (eligiblePlayers.length === 0) { console.log('[streak-percentile] No eligible players found, returning 100th percentile'); return json({ percentile: 100 }); } // Percentage of eligible players who have a streak >= targetStreak const atOrAbove = eligiblePlayers.filter((u) => u.streak >= targetStreak).length; const raw = (atOrAbove / eligiblePlayers.length) * 100; const percentile = raw < 1 ? Math.round(raw * 100) / 100 : Math.round(raw); console.log('[streak-percentile]', { localDate, targetStreak, totalUsers: byUser.size, totalRows: rows.length, eligiblePlayers: eligiblePlayers.length, activeStreaks: userStats.filter((u) => u.streak >= 1).length, recentPlayers: userStats.filter((u) => u.isEligible).length, atOrAbove, raw, percentile, }); return json({ percentile }); };