From 04f1ff0c6bb95eefc8cd4d0b64c8715497ad166b Mon Sep 17 00:00:00 2001 From: mariosemes Date: Fri, 27 Mar 2026 07:36:25 +0100 Subject: [PATCH] =?UTF-8?q?Cache=20search=20results=20in=20shared=20links?= =?UTF-8?q?=20=E2=80=94=20no=20re-searching=20on=20open?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When sharing a search, the actual results are now saved alongside the search config. Opening a shared link shows the cached results instantly instead of re-running the search against all stores. - Add results_json column to shared_searches (migration 006) - Frontend sends current results array when creating a share - Share page loads cached results directly, shows date saved - "Search Live Prices" button available to re-run with fresh data - Falls back to live search if no cached results exist Co-Authored-By: Claude Opus 4.6 (1M context) --- src/client/src/lib/api.ts | 1 + src/client/src/routes/results/+page.svelte | 1 + src/client/src/routes/share/[id]/+page.svelte | 27 ++++++++++++++++--- .../db/migrations/006_add_shared_results.sql | 1 + src/server/models/shared-search.ts | 11 +++++--- src/server/routes/share.ts | 10 ++++++- 6 files changed, 43 insertions(+), 8 deletions(-) create mode 100644 src/server/db/migrations/006_add_shared_results.sql diff --git a/src/client/src/lib/api.ts b/src/client/src/lib/api.ts index ed05aea..7ee763a 100644 --- a/src/client/src/lib/api.ts +++ b/src/client/src/lib/api.ts @@ -133,6 +133,7 @@ export function saveSharedSearch(data: { excludedStores?: string; sortBy?: string; sortAsc?: boolean; + results?: any[]; }) { return api('/api/share', { method: 'POST', body: JSON.stringify(data) }); } diff --git a/src/client/src/routes/results/+page.svelte b/src/client/src/routes/results/+page.svelte index e3410fe..134f33a 100644 --- a/src/client/src/routes/results/+page.svelte +++ b/src/client/src/routes/results/+page.svelte @@ -135,6 +135,7 @@ excludedStores: excludedStores.size > 0 ? [...excludedStores].join(',') : undefined, sortBy, sortAsc, + results, }); const shareUrl = `${window.location.origin}/share/${shared.share_id}`; await navigator.clipboard.writeText(shareUrl); diff --git a/src/client/src/routes/share/[id]/+page.svelte b/src/client/src/routes/share/[id]/+page.svelte index 4af9098..c4372ec 100644 --- a/src/client/src/routes/share/[id]/+page.svelte +++ b/src/client/src/routes/share/[id]/+page.svelte @@ -34,8 +34,20 @@ excludedStores = new Set(shared.excluded_stores.split(',')); } - // Run the search with saved store selection - doSearch(shared.store_ids || undefined); + // Use cached results if available, otherwise re-search + if (shared.results && shared.results.length > 0) { + results = shared.results; + searchDone = true; + meta = { + totalResults: shared.results.length, + storeCount: new Set(shared.results.map((r) => r.storeName)).size, + duration: 0, + errors: [], + }; + loading = false; + } else { + doSearch(shared.store_ids || undefined); + } } catch { loadError = 'Shared search not found or has expired.'; loading = false; @@ -156,7 +168,12 @@ {#if meta} - ({meta.totalResults} results from {meta.storeCount} stores in {meta.duration}ms) + ({meta.totalResults} results from {meta.storeCount} stores{#if meta.duration > 0} in {meta.duration}ms{/if}) + + {/if} + {#if sharedConfig?.created_at} + + · saved {new Date(sharedConfig.created_at + 'Z').toLocaleDateString()} {/if} @@ -164,7 +181,9 @@ {#if shareMessage} {shareMessage} {/if} - Search Again + + Search Live Prices + New Search diff --git a/src/server/db/migrations/006_add_shared_results.sql b/src/server/db/migrations/006_add_shared_results.sql new file mode 100644 index 0000000..f6050b3 --- /dev/null +++ b/src/server/db/migrations/006_add_shared_results.sql @@ -0,0 +1 @@ +ALTER TABLE shared_searches ADD COLUMN results_json TEXT; diff --git a/src/server/models/shared-search.ts b/src/server/models/shared-search.ts index d385b6d..0108a8f 100644 --- a/src/server/models/shared-search.ts +++ b/src/server/models/shared-search.ts @@ -11,6 +11,7 @@ export interface SharedSearch { excluded_stores: string | null; sort_by: string; sort_asc: number; + results_json: string | null; created_at: string; } @@ -25,13 +26,14 @@ export function createSharedSearch(data: { excludedStores?: string; sortBy?: string; sortAsc?: boolean; + results?: any[]; }): SharedSearch { const db = getDatabase(); const shareId = generateShareId(); db.run(` - INSERT INTO shared_searches (share_id, query, store_ids, filter_text, excluded_stores, sort_by, sort_asc) - VALUES (?, ?, ?, ?, ?, ?, ?) + INSERT INTO shared_searches (share_id, query, store_ids, filter_text, excluded_stores, sort_by, sort_asc, results_json) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) `, [ shareId, data.query, @@ -40,6 +42,7 @@ export function createSharedSearch(data: { data.excludedStores || null, data.sortBy || 'price', data.sortAsc !== false ? 1 : 0, + data.results ? JSON.stringify(data.results) : null, ]); saveDatabase(); @@ -47,5 +50,7 @@ export function createSharedSearch(data: { } export function getSharedSearch(shareId: string): SharedSearch | undefined { - return queryOne('SELECT * FROM shared_searches WHERE share_id = ?', [shareId]); + const row = queryOne('SELECT * FROM shared_searches WHERE share_id = ?', [shareId]); + if (!row) return undefined; + return row; } diff --git a/src/server/routes/share.ts b/src/server/routes/share.ts index 8bf4984..d6ccaa5 100644 --- a/src/server/routes/share.ts +++ b/src/server/routes/share.ts @@ -24,6 +24,7 @@ export const shareRoutes: FastifyPluginAsync = async (app) => { excludedStores: { type: 'string' }, sortBy: { type: 'string' }, sortAsc: { type: 'boolean' }, + results: { type: 'array' }, }, }, }, @@ -36,6 +37,13 @@ export const shareRoutes: FastifyPluginAsync = async (app) => { app.get<{ Params: { id: string } }>('/share/:id', async (request, reply) => { const shared = getSharedSearch(request.params.id); if (!shared) return reply.code(404).send({ error: 'Shared search not found' }); - return shared; + + // Parse cached results JSON + let results = null; + if (shared.results_json) { + try { results = JSON.parse(shared.results_json); } catch { /* ignore */ } + } + + return { ...shared, results, results_json: undefined }; }); };