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:
@@ -133,6 +133,7 @@ export function saveSharedSearch(data: {
|
|||||||
excludedStores?: string;
|
excludedStores?: string;
|
||||||
sortBy?: string;
|
sortBy?: string;
|
||||||
sortAsc?: boolean;
|
sortAsc?: boolean;
|
||||||
|
results?: any[];
|
||||||
}) {
|
}) {
|
||||||
return api<any>('/api/share', { method: 'POST', body: JSON.stringify(data) });
|
return api<any>('/api/share', { method: 'POST', body: JSON.stringify(data) });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -135,6 +135,7 @@
|
|||||||
excludedStores: excludedStores.size > 0 ? [...excludedStores].join(',') : undefined,
|
excludedStores: excludedStores.size > 0 ? [...excludedStores].join(',') : undefined,
|
||||||
sortBy,
|
sortBy,
|
||||||
sortAsc,
|
sortAsc,
|
||||||
|
results,
|
||||||
});
|
});
|
||||||
const shareUrl = `${window.location.origin}/share/${shared.share_id}`;
|
const shareUrl = `${window.location.origin}/share/${shared.share_id}`;
|
||||||
await navigator.clipboard.writeText(shareUrl);
|
await navigator.clipboard.writeText(shareUrl);
|
||||||
|
|||||||
@@ -34,8 +34,20 @@
|
|||||||
excludedStores = new Set(shared.excluded_stores.split(','));
|
excludedStores = new Set(shared.excluded_stores.split(','));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run the search with saved store selection
|
// 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);
|
doSearch(shared.store_ids || undefined);
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
loadError = 'Shared search not found or has expired.';
|
loadError = 'Shared search not found or has expired.';
|
||||||
loading = false;
|
loading = false;
|
||||||
@@ -156,7 +168,12 @@
|
|||||||
</h1>
|
</h1>
|
||||||
{#if meta}
|
{#if meta}
|
||||||
<span class="text-2xs text-text-tertiary">
|
<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">
|
||||||
|
· saved {new Date(sharedConfig.created_at + 'Z').toLocaleDateString()}
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
@@ -164,7 +181,9 @@
|
|||||||
{#if shareMessage}
|
{#if shareMessage}
|
||||||
<span class="text-2xs text-emerald-400">{shareMessage}</span>
|
<span class="text-2xs text-emerald-400">{shareMessage}</span>
|
||||||
{/if}
|
{/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>
|
<a href="/" class="btn-secondary text-xs py-1">New Search</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
1
src/server/db/migrations/006_add_shared_results.sql
Normal file
1
src/server/db/migrations/006_add_shared_results.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE shared_searches ADD COLUMN results_json TEXT;
|
||||||
@@ -11,6 +11,7 @@ export interface SharedSearch {
|
|||||||
excluded_stores: string | null;
|
excluded_stores: string | null;
|
||||||
sort_by: string;
|
sort_by: string;
|
||||||
sort_asc: number;
|
sort_asc: number;
|
||||||
|
results_json: string | null;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,13 +26,14 @@ export function createSharedSearch(data: {
|
|||||||
excludedStores?: string;
|
excludedStores?: string;
|
||||||
sortBy?: string;
|
sortBy?: string;
|
||||||
sortAsc?: boolean;
|
sortAsc?: boolean;
|
||||||
|
results?: any[];
|
||||||
}): SharedSearch {
|
}): SharedSearch {
|
||||||
const db = getDatabase();
|
const db = getDatabase();
|
||||||
const shareId = generateShareId();
|
const shareId = generateShareId();
|
||||||
|
|
||||||
db.run(`
|
db.run(`
|
||||||
INSERT INTO shared_searches (share_id, query, store_ids, filter_text, excluded_stores, sort_by, sort_asc)
|
INSERT INTO shared_searches (share_id, query, store_ids, filter_text, excluded_stores, sort_by, sort_asc, results_json)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
`, [
|
`, [
|
||||||
shareId,
|
shareId,
|
||||||
data.query,
|
data.query,
|
||||||
@@ -40,6 +42,7 @@ export function createSharedSearch(data: {
|
|||||||
data.excludedStores || null,
|
data.excludedStores || null,
|
||||||
data.sortBy || 'price',
|
data.sortBy || 'price',
|
||||||
data.sortAsc !== false ? 1 : 0,
|
data.sortAsc !== false ? 1 : 0,
|
||||||
|
data.results ? JSON.stringify(data.results) : null,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
saveDatabase();
|
saveDatabase();
|
||||||
@@ -47,5 +50,7 @@ export function createSharedSearch(data: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getSharedSearch(shareId: string): SharedSearch | undefined {
|
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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export const shareRoutes: FastifyPluginAsync = async (app) => {
|
|||||||
excludedStores: { type: 'string' },
|
excludedStores: { type: 'string' },
|
||||||
sortBy: { type: 'string' },
|
sortBy: { type: 'string' },
|
||||||
sortAsc: { type: 'boolean' },
|
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) => {
|
app.get<{ Params: { id: string } }>('/share/:id', async (request, reply) => {
|
||||||
const shared = getSharedSearch(request.params.id);
|
const shared = getSharedSearch(request.params.id);
|
||||||
if (!shared) return reply.code(404).send({ error: 'Shared search not found' });
|
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 };
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user