fix(discovery): render empty queue as [] not null (500 on empty prod)
Go's nil slice marshals as JSON null, not [], which crashed the Svelte
page's .length access on fresh installs where no discovery tick has
happened yet. Reproduced in production: /admin/discovery → 500 because
data.queue was null and {queue.length} dereferenced it.
Backend: initialize every returning slice in repository.go via
make([]T, 0) so zero rows serialize as [] consistently. Also applies to
PickStaleBuckets, ListSeriesByCity, and Stats.RecentErrors.
Web: coalesce data.queue / data.stats.recent_errors at the top of the
Svelte script with `?? []` so future nil-slice regressions don't take
the whole page down.
This commit is contained in:
@@ -57,7 +57,7 @@ LIMIT $2`
|
||||
return nil, fmt.Errorf("pick buckets: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []Bucket
|
||||
out := make([]Bucket, 0)
|
||||
for rows.Next() {
|
||||
var b Bucket
|
||||
if err := rows.Scan(&b.ID, &b.Land, &b.Region, &b.YearMonth, &b.LastQueriedAt, &b.LastError, &b.CreatedAt); err != nil {
|
||||
@@ -89,7 +89,7 @@ func (r *pgRepository) ListSeriesByCity(ctx context.Context, cityNormalized stri
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []SeriesCandidate
|
||||
out := make([]SeriesCandidate, 0)
|
||||
for rows.Next() {
|
||||
var c SeriesCandidate
|
||||
if err := rows.Scan(&c.ID, &c.Name, &c.City); err != nil {
|
||||
@@ -155,7 +155,7 @@ LIMIT $2 OFFSET $3`, status, limit, offset)
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []DiscoveredMarket
|
||||
out := make([]DiscoveredMarket, 0)
|
||||
for rows.Next() {
|
||||
var d DiscoveredMarket
|
||||
if err := rows.Scan(
|
||||
@@ -249,6 +249,7 @@ LIMIT $1`, recentErrorsLimit)
|
||||
return Stats{}, fmt.Errorf("stats recent_errors: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
s.RecentErrors = make([]BucketError, 0)
|
||||
for rows.Next() {
|
||||
var e BucketError
|
||||
if err := rows.Scan(&e.Land, &e.Region, &e.YearMonth, &e.LastError, &e.LastQueriedAt); err != nil {
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
import { enhance } from '$app/forms';
|
||||
let { data } = $props();
|
||||
|
||||
// Coalesce nullable list fields (Go encodes nil slices as null).
|
||||
const queue = $derived(data.queue ?? []);
|
||||
const recentErrors = $derived(data.stats.recent_errors ?? []);
|
||||
|
||||
const lastTickLabel = $derived.by(() => {
|
||||
if (!data.stats.last_tick_at) return 'nie';
|
||||
const ts = new Date(data.stats.last_tick_at).getTime();
|
||||
@@ -64,16 +68,16 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if data.stats.recent_errors && data.stats.recent_errors.length > 0}
|
||||
{#if recentErrors.length > 0}
|
||||
<details
|
||||
class="mt-3 rounded border border-red-200 bg-red-50 p-2 text-sm dark:border-red-800 dark:bg-red-950"
|
||||
>
|
||||
<summary class="cursor-pointer text-red-700 dark:text-red-300">
|
||||
{data.stats.recent_errors.length} bucket{data.stats.recent_errors.length === 1 ? '' : 's'}
|
||||
{recentErrors.length} bucket{recentErrors.length === 1 ? '' : 's'}
|
||||
with errors
|
||||
</summary>
|
||||
<ul class="mt-2 space-y-1 text-xs">
|
||||
{#each data.stats.recent_errors as e}
|
||||
{#each recentErrors as e}
|
||||
<li class="font-mono">
|
||||
<span class="text-stone-500">{e.last_queried_at?.slice(0, 16)}</span>
|
||||
<span class="ml-2">{e.land} / {e.region} / {e.year_month}</span>
|
||||
@@ -86,10 +90,10 @@
|
||||
|
||||
<h2 class="mt-8 text-lg font-semibold">Queue</h2>
|
||||
<p class="mt-1 text-sm text-stone-500">
|
||||
{data.queue.length} pending · showing from offset {data.offset}
|
||||
{queue.length} pending · showing from offset {data.offset}
|
||||
</p>
|
||||
|
||||
{#if data.queue.length === 0}
|
||||
{#if queue.length === 0}
|
||||
<p class="mt-8 rounded border border-stone-200 bg-stone-50 p-6 text-center text-stone-600">
|
||||
Keine Einträge in der Warteschlange.
|
||||
</p>
|
||||
@@ -109,7 +113,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each data.queue as row (row.id)}
|
||||
{#each queue as row (row.id)}
|
||||
<tr class="border-b border-stone-100">
|
||||
<td class="py-2">{row.land}</td>
|
||||
<td>{row.bundesland}</td>
|
||||
|
||||
Reference in New Issue
Block a user