diff --git a/docs/development/CENTRAL.md b/docs/development/CENTRAL.md index 3920ca5..7968fa6 100644 --- a/docs/development/CENTRAL.md +++ b/docs/development/CENTRAL.md @@ -28,3 +28,4 @@ - [Persist Metadata](./devlog/2025-12-16-230000_persist_metadata.md): Completed. Implemented IndexedDB persistence for Scryfall metadata to ensure offline availability and reduce API calls. - [Bulk Parse Feedback](./devlog/2025-12-16-231500_bulk_parse_feedback.md): Completed. Updated `CubeManager` to handle metadata generation properly and provide feedback on missing cards. - [Full Metadata Passthrough](./devlog/2025-12-16-234500_full_metadata_passthrough.md): Completed. `DraftCard` now includes a `definition` property containing the complete, raw Scryfall object for future-proofing and advanced algorithm usage. +- [Server-Side Caching](./devlog/2025-12-16-235900_server_side_caching.md): Completed. Implemented logic to cache images and metadata on the server upon bulk parsing, and updated client to use local assets. diff --git a/docs/development/devlog/2025-12-16-235900_server_side_caching.md b/docs/development/devlog/2025-12-16-235900_server_side_caching.md new file mode 100644 index 0000000..e5cf0e3 --- /dev/null +++ b/docs/development/devlog/2025-12-16-235900_server_side_caching.md @@ -0,0 +1,25 @@ +# Plan: Server-Side Caching of Bulk Data + +## Objective +Implement server-side caching of both card images and metadata upon bulk parsing, ensuring the application relies on local assets rather than external Scryfall URLs. + +## Implementation Steps + +1. **Refactor Server Architecture (`CardService.ts`)** + * Update storage paths to `public/cards/images` (previously `public/cards`) and `public/cards/metadata`. + * Implement `cacheMetadata` to save JSON files alongside images. + +2. **Update API Endpoint (`index.ts`)** + * Modify `POST /api/cards/cache` to handle metadata saving in addition to image downloading. + * Update static file serving to map `/cards` to `public/cards`, making images accessible at `/cards/images/{id}.jpg`. + +3. **Update Client Logic (`CubeManager.tsx`, `PackGeneratorService.ts`, `LobbyManager.tsx`)** + * **Generation**: Pass a flag (`useLocalImages`) to the generator service. + * **Url Construction**: Generator now produces URLs like `${origin}/cards/images/{id}.jpg` when the flag is set. + * **Triggers**: `CubeManager` immediately sends parsed data to the server for caching before generating packs. + * **Consistency**: `LobbyManager` updated to look for images in the new `/cards/images` path for multiplayer sessions. + +## Impact +* **Performance**: Initial "Parse Bulk" takes slightly longer (due to server cache call), but subsequent interactions are instant and local. +* **Reliability**: Application works offline or without Scryfall after initial parse. +* **Precision**: Metadata is now persisted as individual JSONs on the backend, ready for future complex backend algorithms. diff --git a/src/client/src/modules/cube/CubeManager.tsx b/src/client/src/modules/cube/CubeManager.tsx index 298f320..6fcfa14 100644 --- a/src/client/src/modules/cube/CubeManager.tsx +++ b/src/client/src/modules/cube/CubeManager.tsx @@ -111,7 +111,8 @@ export const CubeManager: React.FC = ({ packs, setPacks, onGoT // --- Effects --- useEffect(() => { if (rawScryfallData) { - const result = generatorService.processCards(rawScryfallData, filters); + // Use local images: true + const result = generatorService.processCards(rawScryfallData, filters, true); setProcessedData(result); } }, [filters, rawScryfallData]); @@ -189,6 +190,20 @@ export const CubeManager: React.FC = ({ packs, setPacks, onGoT } setRawScryfallData(expandedCards); + + // Cache to server + if (expandedCards.length > 0) { + setProgress('Loading...'); + // Deduplicate for shipping to server + const uniqueCards = Array.from(new Map(expandedCards.map(c => [c.id, c])).values()); + + await fetch('/api/cards/cache', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ cards: uniqueCards }) // Send full metadata + }); + } + setLoading(false); setProgress(''); diff --git a/src/client/src/modules/lobby/LobbyManager.tsx b/src/client/src/modules/lobby/LobbyManager.tsx index 73331fd..34beafb 100644 --- a/src/client/src/modules/lobby/LobbyManager.tsx +++ b/src/client/src/modules/lobby/LobbyManager.tsx @@ -78,7 +78,7 @@ export const LobbyManager: React.FC = ({ generatedPacks }) => // Transform packs to use local URLs // Note: For multiplayer, clients need to access this URL. - const baseUrl = `${window.location.protocol}//${window.location.host}/cards`; + const baseUrl = `${window.location.protocol}//${window.location.host}/cards/images`; const updatedPacks = generatedPacks.map(pack => ({ ...pack, diff --git a/src/client/src/services/PackGeneratorService.ts b/src/client/src/services/PackGeneratorService.ts index 3c91cd7..426135a 100644 --- a/src/client/src/services/PackGeneratorService.ts +++ b/src/client/src/services/PackGeneratorService.ts @@ -79,7 +79,7 @@ export interface PackGenerationSettings { export class PackGeneratorService { - processCards(cards: ScryfallCard[], filters: { ignoreBasicLands: boolean, ignoreCommander: boolean, ignoreTokens: boolean }): { pools: ProcessedPools, sets: SetsMap } { + processCards(cards: ScryfallCard[], filters: { ignoreBasicLands: boolean, ignoreCommander: boolean, ignoreTokens: boolean }, useLocalImages: boolean = false): { pools: ProcessedPools, sets: SetsMap } { const pools: ProcessedPools = { commons: [], uncommons: [], rares: [], mythics: [], lands: [], tokens: [] }; const setsMap: SetsMap = {}; @@ -104,7 +104,9 @@ export class PackGeneratorService { typeLine: typeLine, layout: layout, colors: cardData.colors || [], - image: cardData.image_uris?.normal || cardData.card_faces?.[0]?.image_uris?.normal || '', + image: useLocalImages + ? `${window.location.origin}/cards/images/${cardData.id}.jpg` + : (cardData.image_uris?.normal || cardData.card_faces?.[0]?.image_uris?.normal || ''), set: cardData.set_name, setCode: cardData.set, setType: setType, diff --git a/src/server/index.ts b/src/server/index.ts index 418ab8c..9462687 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -28,7 +28,7 @@ const PORT = process.env.PORT || 3000; app.use(express.json({ limit: '50mb' })); // Increase limit for large card lists -// Serve static images +// Serve static images (Nested) app.use('/cards', express.static(path.join(__dirname, 'public/cards'))); // API Routes @@ -51,9 +51,10 @@ app.post('/api/cards/cache', async (req: Request, res: Response) => { return; } - console.log(`Caching images for ${cards.length} cards...`); - const count = await cardService.cacheImages(cards); - res.json({ success: true, downloaded: count }); + console.log(`Caching images and metadata for ${cards.length} cards...`); + const imgCount = await cardService.cacheImages(cards); + const metaCount = await cardService.cacheMetadata(cards); + res.json({ success: true, downloadedImages: imgCount, savedMetadata: metaCount }); } catch (err: any) { console.error('Error in cache route:', err); res.status(500).json({ error: err.message }); diff --git a/src/server/services/CardService.ts b/src/server/services/CardService.ts index cbed3a1..1120a86 100644 --- a/src/server/services/CardService.ts +++ b/src/server/services/CardService.ts @@ -8,9 +8,18 @@ const __dirname = path.dirname(__filename); const CARDS_DIR = path.join(__dirname, '../public/cards'); export class CardService { + private imagesDir: string; + private metadataDir: string; + constructor() { - if (!fs.existsSync(CARDS_DIR)) { - fs.mkdirSync(CARDS_DIR, { recursive: true }); + this.imagesDir = path.join(CARDS_DIR, 'images'); + this.metadataDir = path.join(CARDS_DIR, 'metadata'); + + if (!fs.existsSync(this.imagesDir)) { + fs.mkdirSync(this.imagesDir, { recursive: true }); + } + if (!fs.existsSync(this.metadataDir)) { + fs.mkdirSync(this.metadataDir, { recursive: true }); } } @@ -38,7 +47,7 @@ export class CardService { if (!imageUrl) continue; - const filePath = path.join(CARDS_DIR, `${uuid}.jpg`); + const filePath = path.join(this.imagesDir, `${uuid}.jpg`); if (fs.existsSync(filePath)) { // Already cached @@ -67,4 +76,21 @@ export class CardService { return downloadedCount; } + + async cacheMetadata(cards: any[]): Promise { + let cachedCount = 0; + for (const card of cards) { + if (!card.id) continue; + const filePath = path.join(this.metadataDir, `${card.id}.json`); + if (!fs.existsSync(filePath)) { + try { + fs.writeFileSync(filePath, JSON.stringify(card, null, 2)); + cachedCount++; + } catch (e) { + console.error(`Failed to save metadata for ${card.id}`, e); + } + } + } + return cachedCount; + } }