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) <noreply@anthropic.com>
This commit is contained in:
mariosemes
2026-03-27 07:45:16 +01:00
parent 88eaa68a85
commit 5ab08babd3
3 changed files with 51 additions and 12 deletions

View File

@@ -183,11 +183,15 @@
: selectedStoreIds.has(store.id)
? 'text-text-primary'
: 'text-text-tertiary'}">{store.name}</span>
<span class="ml-auto text-2xs {isBroken ? 'text-red-400/50' : 'text-text-tertiary/40'}">
{#if isBroken}
<span class="text-2xs text-red-400/50">failing</span>
failing
{:else if store.last_scrape_ok === null}
<span class="text-2xs text-text-tertiary/50">untested</span>
untested
{:else if store.avg_duration_ms}
~{store.avg_duration_ms}ms
{/if}
</span>
</button>
{/each}
</div>

View File

@@ -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 @@
</div>
<div class="flex flex-wrap gap-2">
{#each storeProgress as store}
<div class="flex items-center gap-2 text-xs px-3 py-1.5 rounded-lg border transition-all duration-300
<button
onclick={() => store.status !== 'searching' && retryStore(store.id, store.name)}
disabled={store.status === 'searching'}
title={store.status === 'searching' ? 'Searching...' : store.status === 'error' ? `Click to retry — ${store.error}` : `Click to refresh ${store.name}`}
class="flex items-center gap-2 text-xs px-3 py-1.5 rounded-lg border transition-all duration-300
{store.status === 'searching'
? 'bg-surface-raised border-surface-border text-text-secondary'
: store.status === 'done'
? 'bg-emerald-500/10 border-emerald-500/20 text-emerald-400'
: 'bg-red-500/10 border-red-500/20 text-red-400'}">
? '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'}">
<!-- Status indicator -->
{#if store.status === 'searching'}
<svg class="w-3.5 h-3.5 animate-spin text-accent-text" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
@@ -254,9 +288,9 @@
<span>{store.resultCount} products</span>
<span class="text-emerald-600">{store.duration}ms</span>
{:else}
<span class="truncate max-w-[150px]" title={store.error}>{store.error}</span>
<span class="truncate max-w-[150px]">{store.error}</span>
{/if}
</div>
</button>
{/each}
</div>
</div>

View File

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