Add Test All Stores button with live per-store results

Click 'Test All' to enter a search query, then each enabled store
is tested sequentially. A 'Test Result' column appears showing:
- Spinner while testing
- Green checkmark with product count and duration on success
- Red X with error on failure
- Disabled stores show dash

Tests run one at a time so results stream in visibly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
mariosemes
2026-03-26 23:04:22 +01:00
parent 4ea48b3303
commit b3647be434

View File

@@ -1,11 +1,17 @@
<script>
import { getStores, toggleStore, deleteStore, exportStores, syncStores } from '$lib/api';
import { getStores, toggleStore, deleteStore, exportStores, syncStores, testStore } from '$lib/api';
import { onMount } from 'svelte';
let stores = $state([]);
let loading = $state(true);
let syncMessage = $state('');
// Test all state
let testRunning = $state(false);
let testQuery = $state('');
let showTestPrompt = $state(false);
let testResults = $state(new Map());
onMount(async () => {
try {
stores = await getStores();
@@ -45,6 +51,57 @@
syncMessage = `Exported ${result.exported} stores to ${result.directory}/`;
setTimeout(() => syncMessage = '', 5000);
}
function startTestAll() {
testQuery = '';
testResults = new Map();
showTestPrompt = true;
}
async function runTestAll() {
if (!testQuery.trim()) return;
showTestPrompt = false;
testRunning = true;
testResults = new Map();
const enabledStores = stores.filter((s) => s.enabled);
// Mark all as pending
for (const store of enabledStores) {
testResults.set(store.id, { status: 'pending' });
}
testResults = new Map(testResults);
// Test each store sequentially
for (const store of enabledStores) {
testResults.set(store.id, { status: 'testing' });
testResults = new Map(testResults);
try {
const result = await testStore(store.id, testQuery.trim());
testResults.set(store.id, {
status: result.success ? 'success' : 'error',
resultCount: result.parsedProducts?.length || 0,
duration: result.duration,
error: result.error,
});
} catch (err) {
testResults.set(store.id, {
status: 'error',
resultCount: 0,
duration: 0,
error: err.message || 'Request failed',
});
}
testResults = new Map(testResults);
}
testRunning = false;
}
function getTestResult(storeId) {
return testResults.get(storeId);
}
</script>
<div class="h-full flex flex-col">
@@ -52,6 +109,9 @@
<div class="flex-shrink-0 border-b border-surface-border px-6 py-3 flex items-center justify-between">
<h1 class="text-sm font-semibold text-text-primary">Stores</h1>
<div class="flex gap-2">
<button onclick={startTestAll} class="btn-secondary text-xs py-1" title="Test all enabled stores" disabled={testRunning}>
{testRunning ? 'Testing...' : 'Test All'}
</button>
<button onclick={handleSync} class="btn-secondary text-xs py-1" title="Reload from YAML files">
Sync from Files
</button>
@@ -68,6 +128,24 @@
</div>
{/if}
<!-- Test All Prompt -->
{#if showTestPrompt}
<div class="mx-6 mt-3 card p-4">
<p class="text-xs text-text-secondary mb-2">Enter a search term to test all enabled stores:</p>
<form onsubmit={(e) => { e.preventDefault(); runTestAll(); }} class="flex gap-2">
<input
type="text"
bind:value={testQuery}
placeholder="e.g. logitech m"
autofocus
class="input-field flex-1"
/>
<button type="submit" class="btn-primary text-xs" disabled={!testQuery.trim()}>Run Tests</button>
<button type="button" onclick={() => showTestPrompt = false} class="btn-secondary text-xs">Cancel</button>
</form>
</div>
{/if}
<!-- Content -->
<div class="flex-1 overflow-y-auto">
{#if loading}
@@ -92,11 +170,15 @@
<th class="px-6 py-2.5 text-2xs font-medium text-text-tertiary uppercase tracking-wider">URL</th>
<th class="px-6 py-2.5 text-2xs font-medium text-text-tertiary uppercase tracking-wider">Category</th>
<th class="px-6 py-2.5 text-2xs font-medium text-text-tertiary uppercase tracking-wider text-center">Status</th>
{#if testResults.size > 0}
<th class="px-6 py-2.5 text-2xs font-medium text-text-tertiary uppercase tracking-wider text-center">Test Result</th>
{/if}
<th class="px-6 py-2.5 text-2xs font-medium text-text-tertiary uppercase tracking-wider text-right">Actions</th>
</tr>
</thead>
<tbody>
{#each stores as store}
{@const test = getTestResult(store.id)}
<tr class="border-b border-surface-border hover:bg-surface-hover/50 transition-colors duration-100">
<td class="px-6 py-2.5">
<span class="text-sm font-medium text-text-primary">{store.name}</span>
@@ -123,6 +205,35 @@
{store.enabled ? 'Active' : 'Disabled'}
</button>
</td>
{#if testResults.size > 0}
<td class="px-6 py-2.5 text-center whitespace-nowrap">
{#if !test || test.status === 'pending'}
<span class="text-2xs text-text-tertiary"></span>
{:else if test.status === 'testing'}
<span class="inline-flex items-center gap-1.5 text-2xs text-accent-text">
<svg class="w-3 h-3 animate-spin" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Testing...
</span>
{:else if test.status === 'success'}
<span class="inline-flex items-center gap-1.5 text-2xs font-medium text-emerald-400">
<svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" />
</svg>
{test.resultCount} products · {test.duration}ms
</span>
{:else}
<span class="inline-flex items-center gap-1.5 text-2xs font-medium text-red-400" title={test.error}>
<svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
Failed
</span>
{/if}
</td>
{/if}
<td class="px-6 py-2.5 text-right">
<div class="flex gap-1 justify-end">
<a href="/admin/stores/{store.id}" class="btn-secondary text-xs py-0.5 px-2">Edit</a>