From 5ab08babd3d2e3f293be3b547ac15d81c7d8e8b8 Mon Sep 17 00:00:00 2001 From: mariosemes Date: Fri, 27 Mar 2026 07:45:16 +0100 Subject: [PATCH] Add avg response time on search page + click-to-retry store badges 1. Search page: each store now shows its average response time (e.g., "~850ms") pulled from scrape_logs. Shows "untested" or "failing" for stores without successful scrapes. 2. Results page: store progress badges are now clickable buttons. Click a completed or failed store to re-search just that store. Old results from that store are removed, new ones stream in. Useful for retrying stores that had connection errors. Hover tooltip shows "Click to retry" or "Click to refresh". Co-Authored-By: Claude Opus 4.6 (1M context) --- src/client/src/routes/+page.svelte | 14 ++++--- src/client/src/routes/results/+page.svelte | 46 +++++++++++++++++++--- src/server/models/store.ts | 3 +- 3 files changed, 51 insertions(+), 12 deletions(-) diff --git a/src/client/src/routes/+page.svelte b/src/client/src/routes/+page.svelte index dec6c13..9b0084e 100644 --- a/src/client/src/routes/+page.svelte +++ b/src/client/src/routes/+page.svelte @@ -183,11 +183,15 @@ : selectedStoreIds.has(store.id) ? 'text-text-primary' : 'text-text-tertiary'}">{store.name} - {#if isBroken} - failing - {:else if store.last_scrape_ok === null} - untested - {/if} + + {#if isBroken} + failing + {:else if store.last_scrape_ok === null} + untested + {:else if store.avg_duration_ms} + ~{store.avg_duration_ms}ms + {/if} + {/each} diff --git a/src/client/src/routes/results/+page.svelte b/src/client/src/routes/results/+page.svelte index 134f33a..fe8ef6d 100644 --- a/src/client/src/routes/results/+page.svelte +++ b/src/client/src/routes/results/+page.svelte @@ -123,6 +123,37 @@ excludedStores = next; } + function retryStore(storeId, storeName) { + // Mark store as searching again + storeProgress = storeProgress.map((s) => + s.id === storeId ? { ...s, status: 'searching', error: null, resultCount: 0, duration: 0 } : s + ); + + // Remove old results from this store + results = results.filter((r) => r.storeId !== storeId); + + // Re-search just this store + searchProductsStream(query, { stores: String(storeId) }, (event) => { + switch (event.type) { + case 'store_complete': + results = [...results, ...event.results]; + storeProgress = storeProgress.map((s) => + s.id === event.storeId + ? { ...s, status: 'done', resultCount: event.resultCount, duration: event.duration } + : s + ); + break; + case 'store_error': + storeProgress = storeProgress.map((s) => + s.id === event.storeId + ? { ...s, status: 'error', error: event.error, duration: event.duration } + : s + ); + break; + } + }); + } + let shareMessage = $state(''); async function handleShare() { @@ -221,14 +252,17 @@
{#each storeProgress as store} -
+ ? 'bg-emerald-500/10 border-emerald-500/20 text-emerald-400 hover:bg-emerald-500/20 cursor-pointer' + : 'bg-red-500/10 border-red-500/20 text-red-400 hover:bg-red-500/20 cursor-pointer'}"> - {#if store.status === 'searching'} @@ -254,9 +288,9 @@ {store.resultCount} products {store.duration}ms {:else} - {store.error} + {store.error} {/if} -
+ {/each}
diff --git a/src/server/models/store.ts b/src/server/models/store.ts index 25025a9..84b235a 100644 --- a/src/server/models/store.ts +++ b/src/server/models/store.ts @@ -63,7 +63,8 @@ function slugify(text: string): string { export function getAllStores(): StoreWithCategory[] { return queryAll(` - SELECT s.*, c.name as category_name, c.color as category_color + SELECT s.*, c.name as category_name, c.color as category_color, + (SELECT ROUND(AVG(duration_ms)) FROM scrape_logs WHERE store_id = s.id AND success = 1) as avg_duration_ms FROM stores s LEFT JOIN categories c ON s.category_id = c.id ORDER BY s.name