Skip to content

Latest commit

 

History

History
161 lines (134 loc) · 13.8 KB

File metadata and controls

161 lines (134 loc) · 13.8 KB

CLAUDE.md

Guidance for Claude Code working on this repo.

Commands

Dev & Deploy

  • yarn preview — Full-stack local dev (Worker + React on :8787)
  • yarn deployyarn build && wrangler deploy to production
  • yarn deploy -- --env staging — Deploy to staging
  • yarn build — Build React app only
  • yarn format — Prettier
  • yarn format:check — Check formatting

Testing

  • yarn test — Vitest unit tests
  • yarn test:watch — Vitest watch mode
  • yarn test:visual — Playwright visual regression
  • yarn test:visual:update — Update Playwright snapshots

Database

  • yarn db:init — Init local DB schema
  • yarn db:seed — Load sample data
  • yarn db:local -- "SQL" — Run SQL on local D1
  • yarn db:remote -- "SQL" — Run SQL on remote D1
  • yarn db:local:file -- path.sql — Execute SQL file locally
  • yarn db:remote:file -- path.sql — Execute SQL file remotely

Book Management (TypeScript CLI)

  • yarn import-book -- --file="book.epub" --target="zh" — Import + translate
  • yarn list-books:local / yarn list-books:remote — List books
  • yarn remove-book:local -- --uuid="..." / yarn remove-book:remote -- --uuid="..." — Remove book
  • yarn sync-remote-book -- --uuid="..." — Sync local book to remote D1
  • yarn backfill-links -- --uuid="..." [--env=remote] [--dry-run] — Re-parse the original EPUB (from R2 uploads/{uuid}/original.epub) and rewrite raw_html so pre-footnote-support books get internal links/note popovers; verifies stored translation XPaths still resolve before writing

Architecture

TypeScript-first across frontend, backend, CLI, and translator service.

Components

  • React SPA (src/components/) — BookShelf (3D closet, with a legacy 2D wall fallback), BilingualReaderV2, ErrorBoundary
  • CF Worker (src/worker/) — API server: auth, book-handlers, credits, db, types
  • Railway Translator (services/translator/) — Long-running translation service (Hono + Sharp)
    • Receives webhook from Worker on EPUB upload
    • Translates via OpenAI-compatible API (default: gpt-4o-mini). All LLM work (paragraph batches, chapter titles, per-node retries) flows through one global worker pool spanning chapters (TRANSLATE_CONCURRENCY, default 8)
    • Reads/writes D1 via REST API. Checkpoint resume is chapter-level: a chapter counts as done only when every node has a translations_v2 row; partially written chapters are wiped and redone on resume (translations_v2 has no UNIQUE constraint, so this is what keeps resume idempotent)
    • Generates each book's cover + spine by compositing onto a pre-made blank cloth-hardcover template (pure Sharp, no AI at request time) — see cover-composer.ts. Spine width scales with book length.
  • CLI Scripts (scripts/) — import-book, list-books, remove-book, sync-remote-book, generate-blanks
  • D1 SQLite — Users, sessions, books/chapters/translations (v2 tables), translation jobs, credits, reading progress
  • R2 Storage — Cover images, spine images, in-book images (books/{uuid}/images/). CORS enabled (GET/HEAD from *) — the 3D shelf loads these as WebGL textures
  • PWA — Installable (manifest + iOS metas); src/sw-register.ts registers the service worker and shows a refresh toast on new deploys. iOS standalone quirk: use 100dvh/safe-area-inset-bottom for full-screen layouts, not bare 100vh

Key Files

  • src/worker/index.ts — Main Worker entry, routing, middleware
  • src/worker/auth.ts — Google OAuth flow
  • src/worker/book-handlers.ts — Book CRUD, upload, chapter content
  • src/worker/credits.ts — Credit balance, Stripe checkout/webhooks
  • src/worker/db.ts — Database helpers, migrations
  • src/worker/translation/ — Cloudflare Workflows translation backend (jobs with backend='cf'): translate-core.ts mirrors the LLM-facing logic of services/translator/src/translate-worker.ts verbatim — keep the two in sync; orchestration is idempotent per-chapter Workflow steps (row-count check + partial wipe on entry). workflow.ts is the logic-free TranslateBookWorkflow entrypoint (sliding window of 5 chapter steps); d1-binding-client.ts adapts the D1 binding to the REST client's surface. Started only via secret-guarded POST /api/internal/translate-cf, which refuses jobs whose backend isn't 'cf'
  • src/components/BilingualReaderV2.tsx — Main reader (scroll nav, paragraph toggle, progress). Internal links resolved by the parser (a[data-ov-chapter][data-ov-xpath]) navigate in-app: note references (data-ov-note) open a bilingual footnote popover (cross-chapter notes fetched via the chapter cache), other links jump via loadChapter with a floating "return to reading" chip (stack lives in AppV2); note markers are re-appended after translated text so they stay tappable in translated view
  • src/components/BookShelf.tsx — Library UI: hosts the 3D closet (default). Falls back to a classic 2D wall when WebGL is unavailable, but that fallback is legacy/deprecated — it has no upload entry point (upload only happens by clicking an empty slot in the 3D closet) and is slated for removal
  • src/components/shelf3d/BookShelf3D.tsx — 3D closet view (three + @react-three/fiber, lazy-loaded): gaze/zoom camera, click-to-fly-out book with info panel, click-empty-slot-to-upload. Requires CORS on the R2 assets domain (configured on bucket ovid)
  • src/components/shelf3d/layout.ts — Pure shelf-packing math for the 3D view: books explicitly placed in a physical slot (shelf_row/shelf_col) render at that coordinate; everything else packs into a stable block of rows below the physical slots, grouped by shelf/ownership (adaptive case width, unit-tested)
  • src/components/ErrorBoundary.tsx — Error boundary wrapper
  • src/utils/translator.ts — Unified translation module (used by CLI scripts)
  • services/translator/src/index.ts — Railway service entry (Hono routes)
  • services/translator/src/translate-worker.ts — Translation logic
  • services/translator/src/d1-client.ts — D1 REST API client
  • services/translator/src/cover-composer.ts — Composes cover + spine onto blank cloth templates (Sharp): book-face detection, original-cover inset, title/author typesetting, length-based spine thickness
  • services/translator/src/book-parser.ts — EPUB parsing; also extracts the embedded cover (used as the cover inset). Chapter extraction is TOC-first: when the EPUB has a usable nav.xhtml/NCX, files it references take its titles; tiny nav-referenced half-title pages (bare part/chapter numbers) are dropped and donate their nav title to the next unreferenced file (donated nav title > own h1 > inheritance); substantial unreferenced files with a leading h1 are chapters the TOC skipped and take their own h1, headingless ones inherit the preceding entry's title (split-chapter continuations); tiny text-only pages outside the TOC's range (publisher ads/filler) are dropped, and linear="no" spine items are skipped per spec; untitled front/back matter falls back through OPF <guide> roles → filename patterns → headings → short-block derivation (prose/dialogue lines are rejected), with "Chapter N" as the true last resort. Resolves internal links across spine files to (chapter, xpath) coordinates (data-ov-chapter/data-ov-xpath attributes in raw_html) and classifies footnote references (data-ov-note) across the common EPUB shapes: EPUB3 epub:type="noteref" + <aside epub:type="footnote"> (aside hidden via data-ov-hidden, still translated), separate endnotes pages (heading + backlink-ratio heuristic), Gutenberg-style same-file anchor pairs (note-label echo heuristic), plus plain cross-references; note labels are stripped from the text sent to translation. Mirrored in src/utils/book-processor.ts (CLI fallback importer) — keep the two in sync
  • services/translator/src/image-processor.ts — Legacy cover/spine image processing (Sharp), used by the cover-preview debug page
  • services/translator/src/cover-preview.ts — Password-protected cover preview page

API Endpoints

Auth

  • GET /api/auth/google — Start OAuth flow
  • GET /api/auth/callback/google — OAuth callback
  • GET /api/auth/me — Current user
  • POST /api/auth/logout — Logout

Books

  • GET /api/books (alias /api/v2/books) — List books (public + user's private)
  • POST /api/books/estimate — Parse an EPUB via Railway and return a translation cost estimate
  • POST /api/books/upload — Upload EPUB (auth required, deducts credits; Railway handles the rest via /upload-and-parse). Accepts an optional shelf target (shelfSlotId, or shelfRow/shelfCol to create one on the fly) from clicking an empty slot in the 3D closet. The handler picks the translation backend per user (chooseTranslationBackend: CF_TRANSLATION_ALLOWLIST emails → 'cf', CF_TRANSLATION_DEFAULT=1 → everyone; else 'railway') and passes it to Railway, which parses either way but for 'cf' jobs hands translation to the Workflow via POST /api/internal/translate-cf (falling back to translating on Railway — after flipping job ownership — if the trigger fails)
  • GET /api/book/:uuid/status — Parsing/translation progress (polled by the shelf)
  • GET /api/book/:uuid/chapters — Chapter list
  • GET /api/book/:uuid/chapter/:number — Chapter content (XPath-mapped paragraphs)
  • GET /api/book/:uuid/content — Full book content
  • DELETE /api/book/:uuid — Delete book (owner only)
  • POST /api/book/:uuid/share / GET /api/shared/:token/... — Share links
  • GET /api/shelf-slots — Physical shelf-slot grid (row/col/label) for the 3D closet's slot-based upload targets

Reading Progress

  • POST /api/book/:uuid/mark-complete — Mark book read/unread ({isCompleted: bool})
  • GET /api/book/:uuid/progress — Get reading progress
  • PUT|POST /api/book/:uuid/progress — Save reading position (POST used by sendBeacon on unload)
  • GET /api/progress — All of the user's per-book progress in one map

Credits & Payments

  • GET /api/credits — Balance + available packages
  • GET /api/credits/transactions — Purchase/usage history
  • POST /api/stripe/checkout — Create Stripe checkout session
  • GET /api/stripe/verify-session — Verify checkout (webhook fallback)
  • POST /api/stripe/webhook — Stripe webhook

Cover Preview

  • GET /api/cover-preview/:uuid — Password-protected cover/spine preview

Database Schema

Production runs the v2 schema (database/schema_v2.sql + database/migrations/); database/schema.sql is the legacy v1 layout.

Core Tables

  • usersid, google_id, email, name, picture, credits, created_at, updated_at
  • sessionsid, user_id, session_token, expires_at
  • books_v2id, uuid, title, original_title, author, language_pair, styles, book_cover_img_url, book_spine_img_url, user_id, status, display_order, created_at, updated_at
  • chapters_v2id, book_id, chapter_number, title, original_title, raw_html (original EPUB HTML), text_nodes_json, order_index
  • translations_v2id, chapter_id, xpath, original_text, original_html, translated_text, order_index (XPath-mapped onto the chapter's raw HTML)
  • translation_jobsbook_uuid, source/target_language, total/completed_chapters, current_chapter, current_item_offset, glossary_json, glossary_extracted, translated_title, status, error_message, backend (checkpoint + progress state per book; backend — 'railway' or 'cf' — names the only service allowed to write the job, and the Railway stalled-job scanner filters on it)

User Data

  • user_book_progressid, user_id, book_uuid, is_completed, completed_at, last_read_at, chapter_number, paragraph_xpath, show_original (UNIQUE user_id + book_uuid)
  • credit_transactionsid, user_id, amount, type (purchase/usage), stripe_payment_intent_id, balance_after, created_at

Shelf Layout

  • shelf_slotsid, shelf_id, row, col, sort_order, label (UNIQUE shelf_id+row+col, UNIQUE shelf_id+sort_order). Physical coordinates on the 3D closet wall; created on the fly when a user clicks an empty slot to upload, or pre-labeled (e.g. "Gutenberg books") for curated bays
  • book_shelf_slotsbook_id (PK), slot_id, position — links a book to the physical slot it was uploaded into
  • book_shelvesshelf_id, book_id, position — dormant/legacy named-shelf grouping (no current app writer; shelf3d/layout.ts still reads shelf_id off books for stable grouping of anything not yet in a physical slot)

Environment Variables

Worker Secrets (wrangler secret put)

  • GOOGLE_OAUTH_CLIENT_ID / GOOGLE_OAUTH_CLIENT_SECRET
  • OPENAI_API_KEY, OPENAI_API_BASE_URL, OPENAI_MODEL
  • STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, STRIPE_PUBLISHABLE_KEY
  • TRANSLATOR_SERVICE_URL, TRANSLATOR_SECRET

wrangler.toml vars

  • APP_URL — e.g. https://ovid.ink

Local .env (CLI scripts)

  • OPENAI_API_KEY, OPENAI_API_BASE_URL, OPENAI_MODEL
  • CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_API_TOKEN, CLOUDFLARE_D1_DATABASE_ID

Development Rules

  • Branch off latest main — Always git fetch origin && git checkout main && git pull before creating a new branch. Branching off a stale local main produces PRs that conflict with or revert recently merged work.
  • Rebase before opening a PR — After committing on the feature branch, run git fetch origin && git rebase origin/main and resolve conflicts before git push. Do this for every PR, even small fixes — main moves fast and yesterday's base is already stale.
  • Branch namingfeature/ or fix/ branches, PR back to main.
  • Never force push to main. Force-push to your own feature branch (after rebase) is fine and expected.
  • Tests required — Run yarn test before submitting. New features need new tests.
  • CI — Push to main auto-deploys via GitHub Actions
  • Railway — Translator service auto-deploys on git push separately