Cache search results in shared links — no re-searching on open

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) <noreply@anthropic.com>
This commit is contained in:
mariosemes
2026-03-27 07:36:25 +01:00
parent 226fa36016
commit 04f1ff0c6b
6 changed files with 43 additions and 8 deletions

View File

@@ -133,6 +133,7 @@ export function saveSharedSearch(data: {
excludedStores?: string;
sortBy?: string;
sortAsc?: boolean;
results?: any[];
}) {
return api<any>('/api/share', { method: 'POST', body: JSON.stringify(data) });
}

View File

@@ -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);

View File

@@ -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 @@
</h1>
{#if meta}
<span class="text-2xs text-text-tertiary">
({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})
</span>
{/if}
{#if sharedConfig?.created_at}
<span class="text-2xs text-text-tertiary">
&middot; saved {new Date(sharedConfig.created_at + 'Z').toLocaleDateString()}
</span>
{/if}
</div>
@@ -164,7 +181,9 @@
{#if shareMessage}
<span class="text-2xs text-emerald-400">{shareMessage}</span>
{/if}
<a href="/results?q={encodeURIComponent(query)}" class="btn-secondary text-xs py-1">Search Again</a>
<a href="/results?q={encodeURIComponent(query)}" class="btn-secondary text-xs py-1">
Search Live Prices
</a>
<a href="/" class="btn-secondary text-xs py-1">New Search</a>
</div>
</div>

View File

@@ -0,0 +1 @@
ALTER TABLE shared_searches ADD COLUMN results_json TEXT;

View File

@@ -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;
}

View File

@@ -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 };
});
};