mirror of
https://github.com/pupperpowell/bibdle.git
synced 2026-08-22 22:32:28 -04:00
200 lines
12 KiB
Markdown
200 lines
12 KiB
Markdown
# CLAUDE.md
|
|
|
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
|
|
## Project Overview
|
|
|
|
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:
|
|
|
|
(Make sure you use the Svelte agent to execute these commands)
|
|
|
|
## Available MCP Tools:
|
|
|
|
### 1. list-sections
|
|
|
|
Use this FIRST to discover all available documentation sections. Returns a structured list with titles, use_cases, and paths.
|
|
When asked about Svelte or SvelteKit topics, ALWAYS use this tool at the start of the chat to find relevant sections.
|
|
|
|
### 2. get-documentation
|
|
|
|
Retrieves full documentation content for specific sections. Accepts single or multiple sections.
|
|
After calling the list-sections tool, you MUST analyze the returned documentation sections (especially the use_cases field) and then use the get-documentation tool to fetch ALL documentation sections that are relevant for the user's task.
|
|
|
|
### 3. svelte-autofixer
|
|
|
|
Analyzes Svelte code and returns issues and suggestions.
|
|
You MUST use this tool whenever writing Svelte code before sending it to the user. Keep calling it until no issues or suggestions are returned.
|
|
|
|
## Tech Stack
|
|
|
|
- **Framework**: SvelteKit 2 with Svelte 5 (uses 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
|
|
- **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
|
|
|
|
```bash
|
|
# Start development server
|
|
bun run dev
|
|
|
|
# Type checking
|
|
bun run check
|
|
bun run check:watch
|
|
|
|
# Run tests
|
|
bun test
|
|
bun test --watch
|
|
bun test tests/timezone-handling.test.ts # Run a single test file
|
|
|
|
# Build for production
|
|
bun run build
|
|
|
|
# Preview production build
|
|
bun run preview
|
|
|
|
# Database operations
|
|
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
|
|
```
|
|
|
|
## Critical: Date/Time Handling
|
|
|
|
**Bibdle is played by users across many timezones worldwide. The verse shown to a player must always be the verse for the calendar date at *their* location — not the server's timezone, not UTC. A user in Tokyo on Wednesday must see Wednesday's verse, even if the server (or a user in New York) is still on Tuesday.**
|
|
|
|
**NEVER use server time or UTC time for user-facing date calculations.**
|
|
|
|
- Get today's date client-side: `new Date().toLocaleDateString("en-CA")` → `YYYY-MM-DD`
|
|
- Pass the date to the server as a query param or POST body (`localDate`)
|
|
- Server-side date arithmetic must use UTC methods on the client-provided date string: `new Date(dateStr + 'T00:00:00Z')` + `setUTCDate`/`getUTCDate`
|
|
- `src/routes/+page.ts` has `ssr = false` so the load runs client-side with the true local date
|
|
- Never set the user-facing URL to include their date as a parameter. It should always be passed to an API route behind the scenes if needed.
|
|
|
|
### Streak Calculation
|
|
|
|
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.
|
|
- `/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), `googleId` (unique), `isPrivate`
|
|
- **session**: `id` (SHA-256 hash of token), `userId` (FK), `expiresAt`
|
|
- **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`, `Major Prophets`, `Minor Prophets`, `Gospels`, `Pauline Epistles`, `General Epistles`, `Apocalyptic`
|
|
- `order` (1-66, used for adjacency detection)
|
|
|
|
### 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 (`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
|
|
|
|
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`, `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: 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/`, `src/routes/progress/`)
|
|
|
|
- `/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 (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 (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/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; (commented-out) embeddings init
|
|
- `tests/` — Bun test suites: timezone, game, bible, stats, share, sign-in migration
|
|
|
|
## 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)
|
|
|
|
## 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.
|
|
|
|
## A Note
|
|
|
|
The main developer of this project is still learning a lot about developing full-stack applications. If they ask you to do something, make sure they understand how it will be implemented before proceeding.
|