mirror of
https://github.com/pupperpowell/bibdle.git
synced 2026-08-22 22:32:28 -04:00
Updated documentation
This commit is contained in:
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
|
||||
## Project Overview
|
||||
|
||||
Bibdle is a daily Bible verse guessing game built with SvelteKit 5. Players read a verse and try to guess which book of the Bible it comes from. The game provides feedback hints (Testament match, Section match, Adjacent book, etc.) similar to Wordle-style games. Progress is stored locally in the browser and a new verse is generated daily.
|
||||
Bibdle is a daily Bible verse guessing game built with SvelteKit 2 / Svelte 5. Players read a verse and try to guess which book of the Bible it comes from. The game provides feedback hints (Testament match, Section match, Adjacent book, First letter, etc.) similar to Wordle-style games. Progress is stored locally in the browser and a new verse is generated daily.
|
||||
|
||||
You are able to use the Svelte MCP server, where you have access to comprehensive Svelte 5 and SvelteKit documentation. Here's how to use the available tools effectively:
|
||||
|
||||
@@ -29,12 +29,15 @@ You MUST use this tool whenever writing Svelte code before sending it to the use
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Framework**: SvelteKit 5 with Svelte 5 (uses runes: `$state`, `$derived`, `$effect`, `$props`)
|
||||
- **Framework**: SvelteKit 2 with Svelte 5 (uses runes: `$state`, `$derived`, `$effect`, `$props`)
|
||||
- **Styling**: Tailwind CSS 4
|
||||
- **Database**: SQLite with Drizzle ORM
|
||||
- **Auth**: Session-based authentication using Bun's built-in cryptographically secure functions
|
||||
- **Deployment**: Node.js adapter for production builds
|
||||
- **ML**: `@xenova/transformers` for verse embeddings (initialized in server hook) (currently disabled, was a test for a cancelled project)
|
||||
- **Database**: SQLite (`bun:sqlite`) with Drizzle ORM
|
||||
- **Auth**: Session-based — email/password (argon2id via `Bun.password`), plus Apple and Google OAuth
|
||||
- **Bible text**: Local NKJV XML (`EnglishNKJBible.xml`), parsed with `fast-xml-parser`; a Greek 1904 and Swedish 2000 translation are also bundled for alternate modes
|
||||
- **Deployment**: `@sveltejs/adapter-node` run under Bun, managed by a systemd service (`bibdle.service`)
|
||||
- **ML** (currently disabled): `@xenova/transformers` verse embeddings for a similarity search route
|
||||
|
||||
The package version is `3.0.0alpha`.
|
||||
|
||||
## Development Commands
|
||||
|
||||
@@ -81,84 +84,115 @@ bun run db:studio # Open Drizzle Studio GUI
|
||||
A streak counts consecutive calendar days (in the user's local timezone) on which the user completed the puzzle. The rules:
|
||||
|
||||
- The client passes its local date (`localDate`) to the streak API. The server never uses its own clock.
|
||||
- A streak is **active** if the user has completed today's puzzle *or* yesterday's puzzle (they still have time to play today).
|
||||
- Walk backwards from `localDate` through the `dailyCompletions` records, counting each day that has a completion. Stop as soon as a day is missing.
|
||||
- A streak of 1 (completed only today or only yesterday, with no prior consecutive days) is **not displayed** — the minimum shown streak is 2.
|
||||
- "Yesterday" and all date arithmetic on the server must use UTC methods on the client-provided date string to avoid timezone drift: `new Date(localDate + 'T00:00:00Z')`, then `setUTCDate`/`getUTCDate`.
|
||||
- `/api/streak` walks backwards from `localDate` through the `dailyCompletions` records, counting each completed day. It stops at the first missing day. It's called from the win screen, so today is typically completed.
|
||||
- `/api/streak-percentile` (which ranks all players) anchors on today-if-played-else-yesterday, so a player's streak isn't zeroed mid-day before they've had a chance to complete today's puzzle.
|
||||
- A streak of 1 (completed only today, with no prior consecutive days) is **not displayed** — `/api/streak` returns `0` for any streak < 2, and the minimum shown streak is 2.
|
||||
- All date arithmetic on the server must use UTC methods on the client-provided date string to avoid timezone drift: `new Date(localDate + 'T00:00:00Z')`, then `setUTCDate`/`getUTCDate`.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Database Schema (`src/lib/server/db/schema.ts`)
|
||||
|
||||
- **user**: `id`, `firstName`, `lastName`, `email` (unique), `passwordHash`, `appleId` (unique), `isPrivate`
|
||||
- **user**: `id`, `firstName`, `lastName`, `email` (unique), `passwordHash`, `appleId` (unique), `googleId` (unique), `isPrivate`
|
||||
- **session**: `id` (SHA-256 hash of token), `userId` (FK), `expiresAt`
|
||||
- **daily_verses**: Cached daily verses with book ID, verse text, reference, and date
|
||||
- **dailyCompletions**: Game results per user/date with guess count, grade, book; unique on `(userId, date)`
|
||||
- **dailyVerses** (table `daily_verses`): cached daily verse — `date` (unique), `bookId`, `verseText`, `reference`, `createdAt`
|
||||
- **dailyCompletions** (table `daily_completions`): one row per player/date — `anonymousId`, `date`, `guessCount`, `guesses` (JSON array of book IDs; nullable), `completedAt`. Unique on `(anonymousId, date)` to prevent duplicate submissions.
|
||||
|
||||
Sessions expire after 30 days and auto-renew when < 15 days remain.
|
||||
|
||||
**Identity model:** logged-in users' `anonymousId` *is* their `user.id` — `createUser()` inserts the user with `id = anonymousId` so existing stats carry over. Anonymous users get a client-generated UUID stored in `localStorage` (`bibdle-anonymous-id`).
|
||||
|
||||
### Bible Data (`src/lib/types/bible.ts`)
|
||||
|
||||
The `bibleBooks` array contains all 66 Bible books with metadata:
|
||||
- Testament (old/new), Section (Law, History, Wisdom, Prophets, Gospels, Epistles, Apocalyptic)
|
||||
- Order (1-66, used for adjacency detection)
|
||||
- `testament`: `old` | `new`
|
||||
- `section`: `Law`, `History`, `Wisdom`, `Major Prophets`, `Minor Prophets`, `Gospels`, `Pauline Epistles`, `General Epistles`, `Apocalyptic`
|
||||
- `order` (1-66, used for adjacency detection)
|
||||
|
||||
### Daily Verse System (`src/routes/+page.server.ts`)
|
||||
### Daily Verse System
|
||||
|
||||
`getTodayVerse()` checks the database for today's date, fetches a verse if missing, caches permanently, and returns verse with book metadata.
|
||||
`src/lib/server/daily-verse.ts` → `getVerseForDate(date)`: returns the cached verse for a date if present, otherwise fetches a random verse from the local XML Bible (`src/lib/server/xml-bible.ts`, wrapped by `src/lib/server/bible-api.ts`) and stores it permanently. The client calls `POST /api/daily-verse` with its local date; `src/routes/+page.server.ts` only loads `user`/`session` (the verse is fetched client-side because `+page.ts` sets `ssr = false`).
|
||||
|
||||
### Game Logic (`src/routes/+page.svelte`)
|
||||
### Game Logic
|
||||
|
||||
Core logic lives in `src/lib/utils/game.ts` and the reactive store `src/lib/stores/game-persistence.svelte.ts`; `src/routes/+page.svelte` wires them into the UI.
|
||||
|
||||
**State Management:**
|
||||
- `guesses` array stored in localStorage keyed by date: `bibdle-guesses-${date}`
|
||||
- Each guess tracks: book, testamentMatch, sectionMatch, adjacent
|
||||
- `guesses` array stored in `localStorage` keyed by date: `bibdle-guesses-${date}`
|
||||
- Each `Guess` tracks: `book`, `testamentMatch`, `sectionMatch`, `adjacent`, `firstLetterMatch`
|
||||
- `evaluateGuess()` includes a special case: numbered Epistles (e.g. "1 John") match on first letter against any other numbered Epistle
|
||||
- `isWon` derived from whether any guess matches the correct book
|
||||
- `getGrade()` maps guess count to a letter grade (S+ → C)
|
||||
|
||||
**Hint System, for share grid:**
|
||||
- ✅ Exact match | 🟩 Section match | 🟧 Testament match | ‼️ Adjacent book | 🟥 No match
|
||||
|
||||
### Authentication System (`src/lib/server/auth.ts`)
|
||||
|
||||
- Token generation: base64-encoded random bytes; stored as SHA-256 hash in DB
|
||||
- Cookie name: `auth-session`
|
||||
- Anonymous users: identified by a client-generated ID; stats migrate on sign-up via `migrateAnonymousStats()`
|
||||
- Apple Sign-In supported via `appleId` field
|
||||
- Token generation: base64url-encoded random bytes; stored as SHA-256 hash in DB. Cookie name: `auth-session`.
|
||||
- Anonymous users: identified by a client-generated UUID in `localStorage`; stats migrate on sign-up via `migrateAnonymousStats()` (re-attributes `dailyCompletions` rows from the anonymous ID to the new user ID; overlapping dates are dropped).
|
||||
- Three sign-in methods: email/password (argon2id via `Bun.password`), Apple Sign-In (`src/lib/server/apple-auth.ts`, `appleId` field), and Google Sign-In (`src/lib/server/google-auth.ts`, `googleId` field). The SvelteKit CSRF config trusts `https://appleid.apple.com` for the cross-origin `form_post` callback.
|
||||
|
||||
### Stats & Streak (`src/routes/stats/`)
|
||||
### Stats & Streak (`src/routes/stats/`, `src/routes/progress/`)
|
||||
|
||||
- Stats page requires auth; returns `requiresAuth: true` if unauthenticated
|
||||
- Streak calculated client-side by calling `GET /api/streak?userId=X&localDate=Y`
|
||||
- Streak walk-back: counts consecutive days backwards from `localDate` through completed dates
|
||||
- Minimum displayed streak is 2 (single-day streaks suppressed)
|
||||
- `/stats` and `/progress` require auth; the server load returns `requiresAuth: true` for unauthenticated visitors, and the page renders a sign-in modal.
|
||||
- The current streak is fetched from the server via `GET /api/streak?anonymousId=X&localDate=Y` (the server never uses its own clock).
|
||||
- Streak walk-back: counts consecutive days backwards from `localDate` through `dailyCompletions`; stops at the first missing day. Single-day streaks are reported as `0` — the minimum displayed streak is 2.
|
||||
- `/api/streak` walks from `localDate` only (it's called after a win, so today is completed). `/api/streak-percentile`, which ranks all players, anchors on today-if-played-else-yesterday so mid-day streaks aren't zeroed.
|
||||
- Achievements/milestones are computed server-side in `src/lib/server/milestones.ts`.
|
||||
|
||||
## API Endpoints
|
||||
|
||||
- `POST /api/daily-verse` — Fetch verse for a specific date
|
||||
- `POST /api/submit-completion` — Submit game result with stats
|
||||
- `GET /api/streak?userId=X&localDate=Y` — Current streak for user
|
||||
- `GET /api/streak-percentile` — Streak percentile ranking
|
||||
- `POST /api/daily-verse` — Fetch (and cache) the verse for a given `YYYY-MM-DD` date
|
||||
- `POST /api/submit-completion` — Submit a game result (`anonymousId`, `date`, `guessCount`, `guesses`); returns solve rank, guess rank, total solves, average guesses, ties, percentile. Unique on `(anonymousId, date)`.
|
||||
- `GET /api/streak?anonymousId=X&localDate=Y` — Current streak for a player
|
||||
- `GET /api/streak-percentile?streak=N&localDate=Y` — Streak percentile ranking across all players
|
||||
- `GET /api/stats` — Aggregated stats for the `/global` dashboard
|
||||
- `GET /api/imposter` — Generate a four-verse imposter-mode round
|
||||
- `POST /api/similar-verses` — Semantic verse search via embeddings
|
||||
- `POST /api/send-daily-verse` — Cron-only (bearer `CRON_SECRET`); posts today's verse to the Discord webhook
|
||||
- `POST /api/dev/seed-history` — Dev seeding helper
|
||||
|
||||
Other endpoints: `GET /feed.xml` (RSS) and `GET /sitemap.xml` (SEO).
|
||||
|
||||
## Key Files
|
||||
|
||||
- `src/routes/+page.svelte` — Main game UI and client-side logic
|
||||
- `src/routes/+page.server.ts` / `+page.ts` — Server load (verse) + client load (`ssr: false`)
|
||||
- `src/routes/stats/+page.svelte` / `+page.server.ts` — Stats UI and server calculations
|
||||
- `src/lib/server/auth.ts` — Session management, password hashing, anonymous migration
|
||||
- `src/lib/server/bible-api.ts` — Random verse fetching from local XML Bible
|
||||
- `src/routes/+page.server.ts` / `+page.ts` — Server load (user/session) + client load (`ssr: false`, fetches the daily verse)
|
||||
- `src/routes/+layout.svelte` — App shell, title animation, theme toggle, analytics injection
|
||||
- `src/routes/imposter/`, `/random`, `/greek-random`, `/similarity` — Alternate game/debug modes
|
||||
- `src/routes/about/`, `/global/`, `/progress/`, `/stats/`, `/dev/` — Supporting pages
|
||||
- `src/lib/server/auth.ts` — Session management, password hashing, anonymous→user migration
|
||||
- `src/lib/server/apple-auth.ts`, `google-auth.ts` — OAuth providers
|
||||
- `src/lib/server/daily-verse.ts` — Per-date verse caching/lookup
|
||||
- `src/lib/server/xml-bible.ts` — Local XML Bible parsing (NKJV / Greek / Swedish)
|
||||
- `src/lib/server/bible-api.ts` — Random verse fetching on top of the XML parser
|
||||
- `src/lib/server/bible.ts` — Bible book utility functions
|
||||
- `src/lib/types/bible.ts` — Bible books data and TypeScript types
|
||||
- `src/lib/server/milestones.ts` — Achievement/milestone calculation
|
||||
- `src/lib/utils/game.ts` — Guess evaluation and grading
|
||||
- `src/lib/utils/share.ts` — Share grid/text generation
|
||||
- `src/lib/utils/streak.ts`, `stats-client.ts`, `stats.ts` — Client-side streak/stats fetching and formatting
|
||||
- `src/lib/stores/game-persistence.svelte.ts` — Reactive localStorage-backed game state
|
||||
- `src/lib/types/bible.ts` — 66-book metadata and TypeScript types
|
||||
- `src/lib/server/db/schema.ts` — Drizzle ORM schema
|
||||
- `src/hooks.server.ts` — Session validation hook; initializes ML embeddings
|
||||
- `tests/` — Bun test suites (timezone, game, bible, stats, share, auth migration)
|
||||
- `src/hooks.server.ts` — Session validation hook; (commented-out) embeddings init
|
||||
- `tests/` — Bun test suites: timezone, game, bible, stats, share, sign-in migration
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Required in `.env`:
|
||||
- `DATABASE_URL` — Path to SQLite database file (e.g., `./local.db`)
|
||||
See `.env.example`. Required/used variables:
|
||||
|
||||
- `DATABASE_URL` — Path to the SQLite database file (e.g., `prod.db`)
|
||||
- `PUBLIC_SITE_URL` — Canonical site URL
|
||||
- `CRON_SECRET` — Bearer token protecting the cron-only `/api/send-daily-verse` endpoint
|
||||
- `DISCORD_DAILY_WEBHOOK` — Webhook for posting the daily verse to Discord
|
||||
- `AUTH_SECRET` — Secret for Apple Sign-In
|
||||
- `APPLE_ID` / `APPLE_TEAM_ID` / `APPLE_KEY_ID` / `APPLE_PRIVATE_KEY` — Apple Sign-In credentials
|
||||
- `SMTP_USERNAME` / `SMTP_TOKEN` / `SMTP_SERVER` / `SMTP_PORT` — Email (nodemailer)
|
||||
|
||||
## Deployment
|
||||
|
||||
Uses `@sveltejs/adapter-node`. See `bibdle.service` systemd configuration.
|
||||
Production uses `@sveltejs/adapter-node` run under Bun via a systemd service. `deploy.sh` pulls latest, installs deps, builds, and restarts `bibdle.service` (which runs `bun --bun build/index.js` on port 5173 with `DATABASE_URL=prod.db`). See `bibdle.service` for the unit file.
|
||||
|
||||
## A Note
|
||||
|
||||
|
||||
Reference in New Issue
Block a user