···1919.DS_Store
20202121# Node modules (if any)
2222-node_modules/2222+node_modules/
2323+2424+# test data
2525+testdata/
+66
CLAUDE.md
···11+# CLAUDE.md
22+33+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
44+55+## Project Overview
66+77+This is a web service that converts reading data between Storygraph and Goodreads CSV formats, hosted on Val Town. It provides bidirectional conversion with optional Goodreads API enrichment.
88+99+## Development Commands
1010+1111+**Quality Check (run before deployment):**
1212+```bash
1313+deno task quality
1414+```
1515+This runs formatting, linting with fixes, type checking, and all tests.
1616+1717+**Deploy to Val Town:**
1818+```bash
1919+deno task deploy
2020+```
2121+Runs quality checks first, then pushes to Val Town.
2222+2323+**Individual Commands:**
2424+- `deno task test` - Run all tests with necessary permissions
2525+- `deno task fmt` - Format code
2626+- `deno task lint` - Lint and fix issues
2727+- `deno task check` - Type check all TypeScript files
2828+2929+**Run a single test:**
3030+```bash
3131+deno test --allow-net --allow-read --allow-import backend/utils/converter.test.ts
3232+```
3333+3434+## Architecture
3535+3636+The codebase follows a clean separation pattern:
3737+3838+- **backend/**: Hono server and business logic
3939+ - `index.ts`: Main entry point, handles HTTP routes and SSE streaming
4040+ - `utils/`: Core conversion logic, file validation, and Goodreads API integration
4141+4242+- **frontend/**: React SPA with no build step
4343+ - Direct ESM imports from esm.sh
4444+ - TailwindCSS via Twind
4545+ - Server-Sent Events for progress tracking
4646+4747+- **shared/**: TypeScript interfaces used by both frontend and backend
4848+4949+## Key Technical Considerations
5050+5151+**Platform:** This runs on Val Town using Deno (not Node.js). Use Val Town's utilities like `serveFile`, `readFile`, and blob storage.
5252+5353+**Testing:** All business logic has comprehensive unit tests. Tests use dependency injection patterns - external services are mocked. Test files are co-located with source files (`.test.ts`).
5454+5555+**Security:** The file validator enforces strict limits on file types, sizes (1MB max), and content patterns. All user input is validated before processing.
5656+5757+**API Integration:** Goodreads API calls are rate-limited and cached. The enricher gracefully handles API failures and continues with basic conversion.
5858+5959+**Progress Tracking:** Long-running conversions use Server-Sent Events to stream progress updates to the frontend.
6060+6161+## Val Town Specifics
6262+6363+- Use `@std/` for Deno standard library imports
6464+- Blob storage is used for temporary file handling with 24-hour TTL
6565+- The cleanup cron job runs daily to remove expired files
6666+- Deploy with `vt push` after running quality checks
···121121 const normalizedTitle = normalizeTitle(book.Title || "");
122122 const normalizedAuthor = normalizeAuthor(book.Authors || "");
123123124124- // Create base enriched book with normalized data
124124+ // Create base enriched book preserving ALL original Storygraph data
125125 const enrichedBook: Record<string, string> = {
126126- ...book,
126126+ ...book, // Keep all original fields including Title, Authors, Star Rating, etc.
127127 "Book Id": "", // Leave empty as requested
128128- "Title": normalizedTitle,
129129- "Author": normalizedAuthor,
130128 "Author l-f": "", // Will be filled if we get Goodreads data
131129 "Additional Authors": "", // Will be filled if we get Goodreads data
132130 };
···135133 const goodreadsResult = await searchGoodreads(normalizedTitle);
136134137135 if (goodreadsResult) {
138138- // CRITICAL: Use the FULL title (with series info) to match BookHive's rawTitle field
139139- // BookHive stores rawTitle = result.title (full) and title = result.bookTitleBare (clean)
140140- // For CSV imports to match, we need to use the FULL title that BookHive stores as rawTitle
136136+ // ONLY update the specific fields we need from Goodreads
137137+ // This preserves all original Storygraph data while adding Goodreads enrichment
141138 enrichedBook["Book Id"] = goodreadsResult.bookId || "";
142142- enrichedBook["Title"] = goodreadsResult.title; // Use FULL title (matches BookHive's rawTitle)
143143- enrichedBook["Author"] = goodreadsResult.author.name || normalizedAuthor; // Use Goodreads author
139139+ enrichedBook["Title"] = goodreadsResult.title; // Use FULL title with series info
140140+ enrichedBook["Author"] = goodreadsResult.author.name || book.Authors || ""; // Use Goodreads author
144141 enrichedBook["Average Rating"] = goodreadsResult.avgRating || "";
145142 enrichedBook["Number of Pages"] = goodreadsResult.numPages?.toString() ||
146143 "";
···161158 }`,
162159 );
163160 console.log(
164164- ` 📚 Using FULL title for BookHive compatibility: "${goodreadsResult.title}"`,
161161+ ` 📚 Preserved all Storygraph data: Rating=${
162162+ book["Star Rating"]
163163+ }, Status=${book["Read Status"]}, Owned=${book["Owned?"]}`,
165164 );
166165 } else {
166166+ // No Goodreads match - just normalize the title and author
167167+ enrichedBook["Book Id"] = "";
168168+ enrichedBook["Title"] = normalizedTitle;
169169+ enrichedBook["Author"] = normalizedAuthor;
170170+ enrichedBook["Author l-f"] = "";
171171+ enrichedBook["Additional Authors"] = "";
167172 console.log(
168168- `📝 Using normalized data for "${book.Title}" → "${normalizedTitle}"`,
173173+ `📝 No Goodreads match for "${book.Title}" - using normalized title: "${normalizedTitle}"`,
169174 );
170175 }
171176