diff --git a/CLAUDE.md b/CLAUDE.md index 3a6887d..21b09ea 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 diff --git a/README.md b/README.md index ce19680..5c82a0a 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,177 @@ -# sv +# Bibdle -Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli). +A daily Bible verse guessing game. Each day a verse is shown and players try to identify which of the 66 books of the Bible it comes from, receiving Wordle-style feedback (Testament match, Section match, Adjacent book, First letter, etc.) after each guess. A new verse is generated daily and progress is tracked across timezones. -## Creating a project +Live at [bibdle.com](https://bibdle.com). -If you're seeing this, you've probably already done this step. Congrats! +## Tech Stack -```sh -# create a new project in the current directory -bunx sv create +- **Framework**: SvelteKit 2 with Svelte 5 (runes: `$state`, `$derived`, `$effect`, `$props`) +- **Styling**: Tailwind CSS 4 +- **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 +- **ML** (currently disabled): `@xenova/transformers` verse embeddings for a similarity search route +- **Deployment**: `@sveltejs/adapter-node`, run under Bun, managed by a systemd service (see `bibdle.service`) -# create a new project in my-app -bunx sv create my-app -``` +## Getting Started -## Developing +```bash +bun install -Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: - -```sh +# Start the dev server (Vite, Bun runtime) bun run dev -# or start the server and open the app in a new browser tab -bun run dev -- --open -``` +# Type checking +bun run check -## Building +# Tests (Bun test) +bun test +bun test tests/timezone-handling.test.ts # single file +bun test --watch -To create a production version of your app: - -```sh +# Production build & preview bun run build +bun run preview ``` -You can preview the production build with `bun run preview`. +### Database -> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment. +```bash +bun run db:push # push schema changes directly (avoid in prod) +bun run db:generate # generate migrations +bun run db:migrate # run migrations +bun run db:studio # open Drizzle Studio GUI +``` + +### Environment Variables + +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) + +## Architecture + +### Database Schema (`src/lib/server/db/schema.ts`) + +- **user** — `id`, `firstName`, `lastName`, `email` (unique), `passwordHash`, `appleId` (unique), `googleId` (unique), `isPrivate` +- **session** — `id` (SHA-256 hash of token), `userId` (FK), `expiresAt` +- **dailyVerses** — cached daily verse: `date` (unique), `bookId`, `verseText`, `reference`, `createdAt` +- **dailyCompletions** — one row per player/date: `anonymousId`, `date`, `guessCount`, `guesses` (JSON of book IDs, nullable), `completedAt`. Unique on `(anonymousId, date)` to prevent duplicate submissions. + +Sessions expire after 30 days and auto-renew when fewer than 15 days remain. + +### Bible Data (`src/lib/types/bible.ts`) + +The `bibleBooks` array lists all 66 books with metadata: + +- `testament`: `old` | `new` +- `section`: `Law`, `History`, `Wisdom`, `Major Prophets`, `Minor Prophets`, `Gospels`, `Pauline Epistles`, `General Epistles`, `Apocalyptic` +- `order` (1–66, used for adjacency detection) + +### Game Logic + +- `src/lib/utils/game.ts` — `evaluateGuess()` compares a guess to the target book and returns `testamentMatch`, `sectionMatch`, `adjacent`, and `firstLetterMatch` flags. `getGrade()` maps guess count to a letter grade (S+ down to C). Includes a special-case so that numbered Epistles (e.g. "1 John") match on first letter against any other numbered Epistle. +- `src/lib/stores/game-persistence.svelte.ts` — reactive store that keeps guesses and per-day flags in sync with `localStorage`, keyed by date (`bibdle-guesses-${date}`). Resolves the player identity (logged-in user ID, or a locally generated anonymous UUID) and restores state. +- `src/lib/utils/share.ts` — generates the share grid and text. Hint emojis: ✅ exact · 🟩 section · 🟧 testament · ‼️ adjacent · 🟥 no match. + +### Daily Verse System + +`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 and stores it permanently. The XML Bible is read and parsed in `src/lib/server/xml-bible.ts`; `src/lib/server/bible-api.ts` wraps it to produce a verse with a validated `bookId`, `reference`, and `verseText`. + +### Authentication (`src/lib/server/auth.ts`) + +- Token: base64url-encoded random bytes; stored as a SHA-256 hash in the DB. Cookie name: `auth-session`. +- Anonymous users are identified by a client-generated UUID in `localStorage`. On sign-up, `migrateAnonymousStats()` re-attributes the user's `dailyCompletions` rows from the anonymous ID to the new user ID (duplicates for overlapping dates are dropped). +- Apple Sign-In (`src/lib/server/apple-auth.ts`) and Google Sign-In (`src/lib/server/google-auth.ts`) are supported; the SvelteKit CSRF config trusts `https://appleid.apple.com` for the cross-origin `form_post` callback. + +## Routes + +### Pages + +| Route | Description | +| --- | --- | +| `/` | Main game (classic mode). Verse display, search input, guesses table, win screen, streak/percentile. `+page.ts` sets `ssr = false` so the load runs client-side with the true local date, then POSTs that date to `/api/daily-verse`. | +| `/imposter` | Imposter Mode — four verses are shown; three come from one book and one from a different book. Pick the one that doesn't belong. | +| `/random` | Debug page showing a random verse from the NKJV. | +| `/greek-random` | Debug page showing a random verse in parallel Greek (1904) / English (NKJV). | +| `/similarity` | Search a sentence and return the most similar Bible verses via the (currently disabled) embeddings model. | +| `/about` | About page with the project's backstory and social links. | +| `/global` | Public stats dashboard: completions today/all-time, unique & weekly & monthly players, active streak distribution, 14-day completions trend, retention/return-rate metrics. | +| `/progress` | Personal progress page (requires auth): activity calendar, 66-book grid with mastery tiers, insights, and achievements/milestones. | +| `/stats` | Personal stats page (requires auth); returns `requiresAuth: true` for unauthenticated visitors and renders a sign-in modal. | +| `/dev` | Local-time / countdown debug page. | + +### API Endpoints + +| Endpoint | Description | +| --- | --- | +| `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, and percentile. Unique on `(anonymousId, date)`. | +| `GET /api/streak?anonymousId=X&localDate=Y` | Current streak: walks backwards from the client's local date through completed dates, counting consecutive days. Single-day streaks are reported as 0 (minimum displayed streak is 2). | +| `GET /api/streak-percentile?streak=N&localDate=Y` | Streak percentile ranking computed across all players' current streaks. | +| `GET /api/stats` | Aggregated stats used by the `/global` dashboard. | +| `GET /api/imposter` | Generates 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 feed of daily verses. +- `GET /sitemap.xml` — XML sitemap for SEO. + +## Critical: Date/Time Handling + +Bibdle is played across many timezones. The verse shown must always be the verse for the calendar date at **the player's location** — never the server's timezone, never UTC. + +- The client computes today's date with `new Date().toLocaleDateString("en-CA")` (`YYYY-MM-DD`) and sends it to the server. +- Server-side date arithmetic always uses UTC methods on the client-provided date string (`new Date(dateStr + 'T00:00:00Z')` + `setUTCDate`/`getUTCDate`) to avoid timezone drift — see `/api/streak` and `/api/send-daily-verse`. +- `/` has `ssr = false` so the load runs client-side with the real local date. +- The main page also reloads itself if the tab regains focus on a new calendar day. +- The user's local date is never placed in the URL; it is only ever sent to API routes. + +### Streak Calculation + +A streak counts consecutive calendar days (in the player's local timezone) on which the puzzle was completed: + +- The client passes `localDate`; the server never uses its own clock. +- `/api/streak` walks backwards from `localDate` through `dailyCompletions`, counting each completed day; stops at the first missing day. It's called from the win screen, so today is typically completed. Single-day streaks are reported as `0` — the minimum displayed streak is 2. +- `/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. + +## Key Files + +| File | Purpose | +| --- | --- | +| `src/routes/+page.svelte` | Main game UI and client-side logic | +| `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/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/server/milestones.ts` | Achievement/milestone calculation (set-completion, streak, etc.) | +| `src/lib/types/bible.ts` | 66-book metadata and TypeScript types | +| `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/server/db/schema.ts` | Drizzle ORM schema | +| `src/hooks.server.ts` | Session validation hook; (commented-out) embeddings init | +| `tests/` | Bun test suites: timezone, game, bible, stats, share, sign-in migration | + +## Deployment + +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. + +## Background + +Bibdle was created as a small, daily nudge to read the Bible — inspired by the Wordle story of a personal project that grew organically through word of mouth. The full backstory lives on the `/about` page.