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:
@@ -1,11 +1,17 @@
|
|||||||
<script>
|
<script>
|
||||||
import { getStores, toggleStore, deleteStore, exportStores, syncStores } from '$lib/api';
|
import { getStores, toggleStore, deleteStore, exportStores, syncStores, testStore } from '$lib/api';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
let stores = $state([]);
|
let stores = $state([]);
|
||||||
let loading = $state(true);
|
let loading = $state(true);
|
||||||
let syncMessage = $state('');
|
let syncMessage = $state('');
|
||||||
|
|
||||||
|
// Test all state
|
||||||
|
let testRunning = $state(false);
|
||||||
|
let testQuery = $state('');
|
||||||
|
let showTestPrompt = $state(false);
|
||||||
|
let testResults = $state(new Map());
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
try {
|
try {
|
||||||
stores = await getStores();
|
stores = await getStores();
|
||||||
@@ -45,6 +51,57 @@
|
|||||||
syncMessage = `Exported ${result.exported} stores to ${result.directory}/`;
|
syncMessage = `Exported ${result.exported} stores to ${result.directory}/`;
|
||||||
setTimeout(() => syncMessage = '', 5000);
|
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>
|
</script>
|
||||||
|
|
||||||
<div class="h-full flex flex-col">
|
<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">
|
<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>
|
<h1 class="text-sm font-semibold text-text-primary">Stores</h1>
|
||||||
<div class="flex gap-2">
|
<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">
|
<button onclick={handleSync} class="btn-secondary text-xs py-1" title="Reload from YAML files">
|
||||||
Sync from Files
|
Sync from Files
|
||||||
</button>
|
</button>
|
||||||
@@ -68,6 +128,24 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/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 -->
|
<!-- Content -->
|
||||||
<div class="flex-1 overflow-y-auto">
|
<div class="flex-1 overflow-y-auto">
|
||||||
{#if loading}
|
{#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">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">Category</th>
|
||||||
<th class="px-6 py-2.5 text-2xs font-medium text-text-tertiary uppercase tracking-wider text-center">Status</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>
|
<th class="px-6 py-2.5 text-2xs font-medium text-text-tertiary uppercase tracking-wider text-right">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{#each stores as store}
|
{#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">
|
<tr class="border-b border-surface-border hover:bg-surface-hover/50 transition-colors duration-100">
|
||||||
<td class="px-6 py-2.5">
|
<td class="px-6 py-2.5">
|
||||||
<span class="text-sm font-medium text-text-primary">{store.name}</span>
|
<span class="text-sm font-medium text-text-primary">{store.name}</span>
|
||||||
@@ -123,6 +205,35 @@
|
|||||||
{store.enabled ? 'Active' : 'Disabled'}
|
{store.enabled ? 'Active' : 'Disabled'}
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</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">
|
<td class="px-6 py-2.5 text-right">
|
||||||
<div class="flex gap-1 justify-end">
|
<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>
|
<a href="/admin/stores/{store.id}" class="btn-secondary text-xs py-0.5 px-2">Edit</a>
|
||||||
|
|||||||
Reference in New Issue
Block a user