From f6652e59a7a2848ea914fee8847a5753f7aec746 Mon Sep 17 00:00:00 2001 From: George Powell Date: Thu, 12 Feb 2026 20:24:38 -0500 Subject: [PATCH] fixed weird signin bug --- scripts/deduplicate-completions.ts | 75 ++++++++ src/lib/server/db/schema.ts | 2 + src/routes/+page.svelte | 71 ++++---- src/routes/auth/signin/+page.server.ts | 64 +++---- src/routes/stats/+page.svelte | 242 ++++++++++++++++++++----- 5 files changed, 335 insertions(+), 119 deletions(-) create mode 100644 scripts/deduplicate-completions.ts diff --git a/scripts/deduplicate-completions.ts b/scripts/deduplicate-completions.ts new file mode 100644 index 0000000..2372ca2 --- /dev/null +++ b/scripts/deduplicate-completions.ts @@ -0,0 +1,75 @@ +import Database from 'bun:sqlite'; + +// Database path - adjust if your database is located elsewhere +const dbPath = Bun.env.DATABASE_URL || './local.db'; +console.log(`Connecting to database: ${dbPath}`); +const db = new Database(dbPath); + +interface DuplicateGroup { + anonymous_id: string; + date: string; + count: number; +} + +interface Completion { + id: string; + anonymous_id: string; + date: string; + guess_count: number; + completed_at: number; +} + +console.log('Finding duplicates...\n'); + +// Find all (anonymous_id, date) pairs with duplicates +const duplicatesQuery = db.query(` + SELECT anonymous_id, date, COUNT(*) as count + FROM daily_completions + GROUP BY anonymous_id, date + HAVING count > 1 +`); + +const duplicates = duplicatesQuery.all(); +console.log(`Found ${duplicates.length} duplicate groups\n`); + +if (duplicates.length === 0) { + console.log('No duplicates to clean up!'); + db.close(); + process.exit(0); +} + +let totalDeleted = 0; + +// Process each duplicate group +for (const dup of duplicates) { + // Get all completions for this (anonymous_id, date) pair + const completionsQuery = db.query(` + SELECT id, anonymous_id, date, guess_count, completed_at + FROM daily_completions + WHERE anonymous_id = ? AND date = ? + ORDER BY completed_at ASC + `); + + const completions = completionsQuery.all(dup.anonymous_id, dup.date); + console.log(` ${dup.anonymous_id} on ${dup.date}: ${completions.length} entries`); + + // Keep the first (earliest completion), delete the rest + const toKeep = completions[0]; + const toDelete = completions.slice(1); + + console.log(` Keeping: ${toKeep.id} (completed at ${new Date(toKeep.completed_at * 1000).toISOString()})`); + + const deleteQuery = db.query('DELETE FROM daily_completions WHERE id = ?'); + + for (const comp of toDelete) { + console.log(` Deleting: ${comp.id} (completed at ${new Date(comp.completed_at * 1000).toISOString()})`); + deleteQuery.run(comp.id); + totalDeleted++; + } +} + +console.log(`\nāœ… Deduplication complete!`); +console.log(`Total records deleted: ${totalDeleted}`); +console.log(`Unique completions preserved: ${duplicates.length}`); + +db.close(); diff --git a/src/lib/server/db/schema.ts b/src/lib/server/db/schema.ts index 15eebeb..5915e30 100644 --- a/src/lib/server/db/schema.ts +++ b/src/lib/server/db/schema.ts @@ -41,6 +41,8 @@ export const dailyCompletions = sqliteTable('daily_completions', { anonymousIdDateIndex: index('anonymous_id_date_idx').on(table.anonymousId, table.date), dateIndex: index('date_idx').on(table.date), dateGuessIndex: index('date_guess_idx').on(table.date, table.guessCount), + // Ensures schema matches the database migration and prevents duplicate submissions + uniqueAnonymousIdDate: unique('daily_completions_anonymous_id_date_unique').on(table.anonymousId, table.date), })); export type DailyCompletion = typeof dailyCompletions.$inferSelect; diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index c4a6fca..533c888 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -181,10 +181,17 @@ // Initialize anonymous ID $effect(() => { if (!browser) return; - anonymousId = getOrCreateAnonymousId(); + + // CRITICAL: If user is logged in, ALWAYS use their user ID + // Never use the localStorage anonymous ID for authenticated users + if (user) { + anonymousId = user.id; + } else { + anonymousId = getOrCreateAnonymousId(); + } + if ((window as any).umami) { - // Use user id if logged in, otherwise use anonymous id - (window as any).umami.identify(user ? user.id : anonymousId); + (window as any).umami.identify(anonymousId); } const statsKey = `bibdle-stats-submitted-${dailyVerse.date}`; statsSubmitted = localStorage.getItem(statsKey) === "true"; @@ -278,7 +285,7 @@ (async () => { try { const response = await fetch( - `/api/submit-completion?anonymousId=${user ? user.id : anonymousId}&date=${dailyVerse.date}`, + `/api/submit-completion?anonymousId=${anonymousId}&date=${dailyVerse.date}`, ); const result = await response.json(); console.log("Stats response:", result); @@ -308,7 +315,7 @@ async function submitStats() { try { const payload = { - anonymousId: user ? user.id : anonymousId, + anonymousId: anonymousId, // Already set correctly in $effect above date: dailyVerse.date, guessCount: guesses.length, }; @@ -477,32 +484,34 @@ {:else} - { - chapterGuessCompleted = true; - const key = `bibdle-chapter-guess-${dailyVerse.reference}`; - const saved = localStorage.getItem(key); - if (saved) { - const data = JSON.parse(saved); - const match = - dailyVerse.reference.match(/\s(\d+):/); - const correctChapter = match - ? parseInt(match[1], 10) - : 1; - chapterCorrect = - data.selectedChapter === correctChapter; - } - }} - /> +
+ { + chapterGuessCompleted = true; + const key = `bibdle-chapter-guess-${dailyVerse.reference}`; + const saved = localStorage.getItem(key); + if (saved) { + const data = JSON.parse(saved); + const match = + dailyVerse.reference.match(/\s(\d+):/); + const correctChapter = match + ? parseInt(match[1], 10) + : 1; + chapterCorrect = + data.selectedChapter === correctChapter; + } + }} + /> +
{/if}
diff --git a/src/routes/auth/signin/+page.server.ts b/src/routes/auth/signin/+page.server.ts index 394f096..02c1a7d 100644 --- a/src/routes/auth/signin/+page.server.ts +++ b/src/routes/auth/signin/+page.server.ts @@ -42,54 +42,42 @@ export const actions: Actions = { // Migrate anonymous stats if different anonymous ID if (anonymousId && anonymousId !== user.id) { try { - // Update all daily completions from the local anonymous ID to the user's ID - await db - .update(dailyCompletions) - .set({ anonymousId: user.id }) + // Get completions for both the anonymous ID and the user ID + const anonCompletions = await db + .select() + .from(dailyCompletions) .where(eq(dailyCompletions.anonymousId, anonymousId)); - - console.log(`Migrated stats from ${anonymousId} to ${user.id}`); - // Deduplicate any entries for the same date after migration - const allUserCompletions = await db + const userCompletions = await db .select() .from(dailyCompletions) .where(eq(dailyCompletions.anonymousId, user.id)); - // Group by date to find duplicates - const dateGroups = new Map(); - for (const completion of allUserCompletions) { - const date = completion.date; - if (!dateGroups.has(date)) { - dateGroups.set(date, []); - } - dateGroups.get(date)!.push(completion); - } + // Create a set of dates the user already has completions for + const userDates = new Set(userCompletions.map(c => c.date)); - // Process dates with duplicates - const duplicateIds: string[] = []; - for (const [date, completions] of dateGroups) { - if (completions.length > 1) { - // Sort by completedAt timestamp (earliest first) - completions.sort((a, b) => a.completedAt.getTime() - b.completedAt.getTime()); - - // Keep the first (earliest), mark the rest for deletion - const toDelete = completions.slice(1); - duplicateIds.push(...toDelete.map(c => c.id)); - - console.log(`Found ${completions.length} duplicates for date ${date}, keeping earliest, deleting ${toDelete.length}`); + let migrated = 0; + let skipped = 0; + + // Migrate only non-conflicting completions + for (const completion of anonCompletions) { + if (!userDates.has(completion.date)) { + // No conflict - safe to migrate + await db + .update(dailyCompletions) + .set({ anonymousId: user.id }) + .where(eq(dailyCompletions.id, completion.id)); + migrated++; + } else { + // Conflict exists - delete the anonymous completion (keep user's existing one) + await db + .delete(dailyCompletions) + .where(eq(dailyCompletions.id, completion.id)); + skipped++; } } - // Delete duplicate entries - if (duplicateIds.length > 0) { - await db - .delete(dailyCompletions) - .where(inArray(dailyCompletions.id, duplicateIds)); - - console.log(`Deleted ${duplicateIds.length} duplicate completion entries`); - } - + console.log(`Migration complete: ${migrated} moved, ${skipped} duplicates removed`); } catch (error) { console.error('Error migrating anonymous stats:', error); // Don't fail the signin if stats migration fails diff --git a/src/routes/stats/+page.svelte b/src/routes/stats/+page.svelte index 67a3ce5..bf25f69 100644 --- a/src/routes/stats/+page.svelte +++ b/src/routes/stats/+page.svelte @@ -2,7 +2,7 @@ import { browser } from "$app/environment"; import { goto } from "$app/navigation"; import { onMount } from "svelte"; - import { enhance } from '$app/forms'; + import { enhance } from "$app/forms"; import AuthModal from "$lib/components/AuthModal.svelte"; import Container from "$lib/components/Container.svelte"; import { bibleBooks } from "$lib/types/bible"; @@ -11,7 +11,7 @@ formatDate, getStreakMessage, getPerformanceMessage, - type UserStats + type UserStats, } from "$lib/utils/stats"; interface PageData { @@ -47,7 +47,7 @@ } function getBookName(bookId: string): string { - return bibleBooks.find(b => b.id === bookId)?.name || bookId; + return bibleBooks.find((b) => b.id === bookId)?.name || bookId; } $inspect(data); @@ -55,15 +55,24 @@ Stats | Bibdle - + -
+
-

Your Stats

-

Track your Bibdle performance over time

+

+ Your Stats +

+

+ Track your Bibdle performance over time +

-
+

Loading your stats...

{:else if data.requiresAuth}
-
-

Authentication Required

-

You must be logged in to see your stats.

+
+

+ Authentication Required +

+

+ You must be logged in to see your stats. +

{:else if data.error}
-
+ {#if stats.totalSolves > 0} -
+
{#if stats.worstDay}
šŸ˜…
-
Worst Day
-
{stats.worstDay.guessCount} guesses
-
{formatDate(stats.worstDay.date)}
+
+ Worst Day +
+
+ {stats.worstDay.guessCount} guesses +
+
+ {formatDate(stats.worstDay.date)} +
@@ -188,9 +257,22 @@
šŸ†
-
Best Book
-
{getBookName(stats.bestBook.bookId)}
-
{stats.bestBook.avgGuesses} avg guesses ({stats.bestBook.count}x)
+
+ Best Book +
+
+ {getBookName(stats.bestBook.bookId)} +
+
+ {stats.bestBook.avgGuesses} avg guesses ({stats + .bestBook.count}x) +
@@ -202,9 +284,24 @@
šŸ“–
-
Most Seen Book
-
{getBookName(stats.mostSeenBook.bookId)}
-
{stats.mostSeenBook.count} time{stats.mostSeenBook.count === 1 ? '' : 's'}
+
+ Most Seen Book +
+
+ {getBookName(stats.mostSeenBook.bookId)} +
+
+ {stats.mostSeenBook.count} time{stats + .mostSeenBook.count === 1 + ? "" + : "s"} +
@@ -215,11 +312,20 @@
šŸ“š
-
Unique Books
-
- {stats.totalBooksSeenOT + stats.totalBooksSeenNT} +
+ Unique Books +
+
+ {stats.totalBooksSeenOT + + stats.totalBooksSeenNT} +
+
+ OT: {stats.totalBooksSeenOT} / NT: {stats.totalBooksSeenNT}
-
OT: {stats.totalBooksSeenOT} / NT: {stats.totalBooksSeenNT}
@@ -227,18 +333,33 @@ -

Grade Distribution

+

+ Grade Distribution +

{#each Object.entries(stats.gradeDistribution) as [grade, count] (grade)} - {@const percentage = getGradePercentage(count, stats.totalSolves)} + {@const percentage = getGradePercentage( + count, + stats.totalSolves, + )}
- + {grade}
-
{count}
-
{percentage}%
+
+ {count} +
+
+ {percentage}% +
{/each}
@@ -247,16 +368,37 @@ {#if stats.recentCompletions.length > 0} -

Recent Performance

+

+ Recent Performance +

- {#each stats.recentCompletions as completion (completion.date)} -
+ {#each stats.recentCompletions as completion, idx (`${completion.date}-${idx}`)} +
- {formatDate(completion.date)} + {formatDate(completion.date)}
-
- {completion.guessCount} guess{completion.guessCount === 1 ? '' : 'es'} - +
+ {completion.guessCount} guess{completion.guessCount === + 1 + ? "" + : "es"} + {completion.grade}
@@ -270,4 +412,4 @@
- \ No newline at end of file +