From 2fdd8e8222f44bb10e774336e475bd1820157c16 Mon Sep 17 00:00:00 2001 From: vikingowl Date: Fri, 24 Apr 2026 13:37:39 +0200 Subject: [PATCH] feat(web): polish discovery admin page and drawer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Discovery drawer - Wrap each section in a rounded card so boundaries are visible without parsing the uppercase headers. - Header: N Quellen and enrichment_status become consistent pills, matching the existing konfidenz pill treatment. - Enrichment: replace the inline "(llm)"/"(crawl)" trailing text with a color-coded badge on the label side (purple = llm, sky = crawl). - Empty enrichment state now tells the operator how to trigger it. - Audit timestamp uses a local-time helper so the displayed time matches the browser timezone (was UTC-as-local). - Quellen list: prefix each URL with its hostname for scannability; long URLs truncated with full URL in the title attribute. ContributionsPanel - Amber border/background now only on conflict rows; every row previously got border-amber-100 unconditionally, which diluted the conflict signal. Rang 1 badge flipped to emerald so it reads as a positive "winner" marker, not a warning. Discovery page - Remove dead dateInputValue() function and the stale a11y_click_events_have_key_events suppression — both flagged by eslint after earlier refactors. - Render crawl/enrich timestamps in the browser's local timezone via a new fmtLocalStamp helper; the previous .slice(0,16).replace('T',' ') treated the ISO UTC string as if it were local time. --- web/src/routes/admin/discovery/+page.svelte | 29 +- .../admin/discovery/ContributionsPanel.svelte | 48 ++- .../admin/discovery/DetailDrawer.svelte | 327 +++++++++++------- 3 files changed, 241 insertions(+), 163 deletions(-) diff --git a/web/src/routes/admin/discovery/+page.svelte b/web/src/routes/admin/discovery/+page.svelte index 1654c38..ed703bc 100644 --- a/web/src/routes/admin/discovery/+page.svelte +++ b/web/src/routes/admin/discovery/+page.svelte @@ -111,9 +111,7 @@ // Scroll into view so the highlighted row stays visible during // keyboard-driven scanning through a long list. requestAnimationFrame(() => { - document - .querySelector(`[data-row-id="${selectedId}"]`) - ?.scrollIntoView({ block: 'nearest' }); + document.querySelector(`[data-row-id="${selectedId}"]`)?.scrollIntoView({ block: 'nearest' }); }); } @@ -269,11 +267,6 @@ const showingFrom = $derived(data.offset + 1); const showingTo = $derived(Math.min(data.offset + data.limit, data.total ?? 0)); - // 'YYYY-MM-DDTHH:mm:ssZ' → 'YYYY-MM-DD' for - function dateInputValue(iso: string | null): string { - return iso ? iso.slice(0, 10) : ''; - } - // 'YYYY-MM-DDTHH:mm:ssZ' → 'DD.MM.YYYY' (German) for display. function fmtDate(iso: string | null): string { if (!iso) return ''; @@ -289,6 +282,13 @@ return s || e || ''; } + // Backend emits UTC; render in the browser's local timezone. + function fmtLocalStamp(iso: string): string { + const d = new Date(iso); + const pad = (n: number) => String(n).padStart(2, '0'); + return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`; + } + function konfidenzClass(k: string): string { switch (k) { case 'hoch': @@ -578,9 +578,7 @@

Crawl-Ergebnis

{(s.duration_ms / 1000).toFixed(1)} s · {s.started_at - .slice(0, 16) - .replace('T', ' ')}{(s.duration_ms / 1000).toFixed(1)} s · {fmtLocalStamp(s.started_at)}
@@ -671,9 +669,7 @@

Crawl-Enrich-Ergebnis

{(es.duration_ms / 1000).toFixed(1)} s · {es.started_at - .slice(0, 16) - .replace('T', ' ')}{(es.duration_ms / 1000).toFixed(1)} s · {fmtLocalStamp(es.started_at)}
@@ -817,7 +813,6 @@ {#each queue as row (row.id)} - Keine Quelldaten vorhanden.

{:else}
-

- Rang 1 = höchste Priorität · Gewinnende Quelle: - {contributions[0].source_name} - · Amber = abweichende Werte zwischen Quellen -

+
+ + + Gewinnende Quelle: + {contributions[0].source_name} + + + + Abweichende Werte zwischen Quellen + +
{#each contributions as c, i} @@ -93,18 +103,18 @@ {#each fields as field} {@const conflict = hasConflict(field)} {#each contributions as c} diff --git a/web/src/routes/admin/discovery/DetailDrawer.svelte b/web/src/routes/admin/discovery/DetailDrawer.svelte index 4e01731..e7c5a32 100644 --- a/web/src/routes/admin/discovery/DetailDrawer.svelte +++ b/web/src/routes/admin/discovery/DetailDrawer.svelte @@ -177,6 +177,12 @@ const [y, m, day] = d.split('-'); return y && m && day ? `${day}.${m}.${y}` : d; } + // Backend emits UTC; render in the browser's local timezone. + function fmtLocalStamp(iso: string): string { + const d = new Date(iso); + const pad = (n: number) => String(n).padStart(2, '0'); + return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`; + } function konfidenzClass(k: string): string { switch (k) { case 'hoch': @@ -189,8 +195,58 @@ return 'bg-stone-100 text-stone-600 dark:bg-stone-800 dark:text-stone-400'; } } + function provenanceClass(src: string | undefined): string { + if (src === 'llm') { + return 'bg-purple-100 text-purple-700 dark:bg-purple-900/50 dark:text-purple-300'; + } + if (src === 'crawl') { + return 'bg-sky-100 text-sky-700 dark:bg-sky-900/50 dark:text-sky-300'; + } + return 'bg-stone-100 text-stone-500 dark:bg-stone-800 dark:text-stone-400'; + } + function hostnameOf(url: string): string { + try { + return new URL(url).hostname.replace(/^www\./, ''); + } catch { + return url; + } + } + function enrichmentStatusClass(s: string): string { + if (s === 'done') + return 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/50 dark:text-emerald-300'; + if (s === 'failed') return 'bg-red-100 text-red-700 dark:bg-red-900/50 dark:text-red-300'; + return 'bg-stone-100 text-stone-600 dark:bg-stone-800 dark:text-stone-300'; + } + + // Truthy iff the row has at least one enrichment value worth rendering. + const hasEnrichment = $derived.by(() => { + const en = row?.enrichment; + if (!en) return false; + return !!( + en.plz || + en.venue || + en.organizer || + en.category || + en.opening_hours || + en.description || + (en.lat != null && en.lng != null) + ); + }); +{#snippet sourceBadge(src: string | undefined)} + {#if src} + + {src} + + {/if} +{/snippet} + {#if row} @@ -203,7 +259,7 @@ >
Feld - {c.source_name} + {c.source_name} {#if i === 0} Rang 1Rang 1 {/if}
{field.label} {#if conflict} - + {/if}
+
- + @@ -515,23 +583,21 @@ {#each similarEntries as m} - - - - - - + + + + + -
Score Markt Stadt
{(m.score * 100).toFixed(0)}%{m.entry.markt_name}{m.entry.stadt}{fmtDate(m.entry.start_datum)} +
+ {(m.score * 100).toFixed(0)}% + {m.entry.markt_name}{m.entry.stadt}{fmtDate(m.entry.start_datum)} {m.entry.konfidenz || '—'} + {#if verdicts[m.entry.id]} {@const v = verdicts[m.entry.id]} - {v.same ? '✓ same' : '✗ diff'} {(v.confidence * 100).toFixed(0)}% + {v.same ? '✓ same' : '✗ diff'} + {(v.confidence * 100).toFixed(0)}% {:else}