mirror of
https://github.com/pupperpowell/bibdle.git
synced 2026-08-22 22:32:28 -04:00
5.4 KiB
5.4 KiB
Community Verse Submissions — Implementation Plan
Source of truth: spec.md. Each step is independently verifiable and builds on the previous.
Step 1 — Schema & migration
- Add
verse_submissionstable tosrc/lib/server/db/schema.ts:idtext pk (Bun.randomUUIDv7())user_idtext not null, FK →user.idON DELETE SET NULL (nullable column to allow that)scheduled_datetext not null uniqueselected_book_idtext not nullselected_chapterinteger not nullselected_verseinteger not nullsubmitted_atinteger not null (server UTC millis; raw integer, drives 7-day cooldown)- indexes:
user_id, unique onscheduled_date(the column-level unique handles this)
bun run db:pushto dev.db.- Verify:
sqlite3 dev.db ".schema verse_submissions".
Note: spec says
user_idFK should be SET NULL on account deletion, so the column is nullable despite "not null" in the table sketch (the edge-cases section overrides: "FK should be ON DELETE SET NULL"). Resolution: makeuser_idnullable + ON DELETE SET NULL. The admin LEFT JOIN renders "(deleted user)" when null. This is the spec-intended behavior.
Step 2 — Bible structure + windowing helpers
- Export
getChapterCount,getVerseCountfromxml-bible.ts(currently private). - Add
composeVerseWindow(bookNumber, chapter, verse):- Computes the selected verse's index
vwithin the book (contiguous 1-based across chapters),bookStart=1,bookEnd=total verses in book. - Default window
[v-2, v-1, v]; ifv-2 < 1fall forward to[v, v+1, v+2]. - Returns
{ bookId, bookName, verses: string[], startChapter, startVerse, endChapter, endVerse, selectedVerse }by callingextractVersesper chapter and concatenating for cross-chapter windows.
- Computes the selected verse's index
- Add
formatWindowReference(bookName, startChapter, startVerse, endChapter, endVerse): same-chapter →Book C:S-E; cross-chapter →Book C1:S1–C2:E2. - Unit tests in
tests/verse-window.test.ts.
Step 3 — Public APIs
GET /api/bible/structure→[{ bookId, chapters: number[] }, ...]for all 66 books (verse counts per chapter). Cached in-memory.GET /api/verse-window?bookId=gen&chapter=1&verse=1→{ windowVerses, reference, bookId, selectedVerse }viacomposeVerseWindow.- Verify with curl.
Step 4 — Admin module + /scheduled-verses page
src/lib/server/admin.tsexportingADMIN_EMAIL = 'geohpowell@gmail.com'.src/routes/scheduled-verses/+page.server.ts: requirelocals.user?.email === ADMIN_EMAILelse auth redirect / 403 access-denied page.+page.svelte: table joiningverse_submissionsLEFT JOINdaily_verses(onscheduled_date = date) LEFT JOINuser(onuser_id), sorted byscheduled_dateasc. Columns per spec. "(deleted user)" when user null.- Not linked from nav. Verify by direct URL.
Step 5 — Submit backend + lazy gap-fill
POST /api/submit-verse(auth required): body{ bookId, chapter, verse, localDate }.- Require
locals.user(401). - Solved-today gate:
dailyCompletionsrow(anonymousId=user.id, date=localDate)else 403. - Cooldown: most recent
verse_submissions.submitted_atfor user; if< 7dago → 429{ cooldownEndsAt }. - Structural validation: bookId valid, chapter in
[1, getChapterCount], verse in[1, getVerseCount]. - Compute window via
composeVerseWindow. - Scheduling scan: fetch all
daily_verseswithdate > today(server UTC) ordered; walk day-by-day from tomorrow; validity = empty + no back-to-back with D-1/D+2 committed neighbors + no same-window-ref row within ±60 days (bounded lookup). Find earliest validD. - Transaction: insert
daily_verses{ id, date: D, bookId, reference, verseText, createdAt: now }+verse_submissions{ id, user_id, scheduled_date: D, selected_book_id, selected_chapter, selected_verse, submitted_at: now }. OnSQLITE_CONSTRAINT_UNIQUEretry scan fromD+1, up to 5 attempts. - Return
{ scheduledDate: D, reference, windowText }. Emit rybbit event.
- Require
GET /api/submit-verse/status?localDate=YYYY-MM-DD(auth required):{ canSubmit, cooldownEndsAt, lastSubmission, upcoming }.- Modify
getVerseForDatelazy path: re-rollgetRandomVerses()until book differs from committed D-1 and D+1 neighbors (cap ~20 retries). - Verify with curl end-to-end.
Step 6 — Frontend SubmitVerse.svelte
- New component rendered in
WinScreen.sveltenear the progress button, sharing.progress-btnstyling. - States: logged-out (Apple/Google dropdown mirroring progress button), cooldown (greyed +
CountdownTimer-style countdown tocooldownEndsAt), can-submit (cascading Book→Chapter→Verse<select>s from/api/bible/structure, debounced 250ms preview via/api/verse-window, Submit button, success confirmation with scheduled date, inline 429/403 errors). - Pass
isLoggedIn+anonymousIdthrough (already on WinScreen props). - Verify in browser.
Step 7 — Tests
tests/verse-window.test.ts: book-start fall-forward (Gen 1:1 → [1,2,3]), cross-chapter window, same-chapter window, end-of-book safety.tests/scheduling.test.ts(or unit-test the pure scan function against an in-memory fixture): no-back-to-back, 60-day repeat skip, earliest-valid-date selection.- Cooldown arithmetic test.
Step 8 — Docs
- Update
README.mdschema table (addverse_submissions) and routes/API tables (new endpoints +/scheduled-verses). - Note
ADMIN_EMAILconstant location.