feat(v1.1.1-dead-letters): dashboard badge + list view
Design notes §4 makes the dashboard surface load-bearing — with no
default DL handler, users wouldn't know dead letters exist
otherwise.
New route: `apps/[slug]/dead-letters/+page.svelte` — list view
columns per the design notes:
- `created_at`, `source`, `op`, `script_id`, `attempt_count`,
`first/last_attempt_at`, `last_error` (truncated; clickable)
- per-row Replay + Mark resolved buttons
- expandable row detail panel showing full payload (JSON) +
full last_error
- unresolved-only filter (default on); refresh button
Per-app detail page (`apps/[slug]/+page.svelte`) grows a "Dead
letters" link in the tabs nav, with a red unresolved-count pill
when > 0. Loaded in parallel with the existing app loaders so it
doesn't slow the page.
Apps list (`apps/+page.svelte`) shows the same red pill next to
each app's name when its unresolved count > 0. Counts fetched in
parallel after the apps list lands; failures here are non-fatal
(just no badge).
API client wiring: `api.deadLetters.{count,list,get,replay,resolve}`
mirrors the v1.1.1 admin endpoints. `DeadLetterRow` type added to
the dashboard's API shape declarations.
dashboard's svelte-check passes (369 files, 0 errors, 0 warnings).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,26 @@
|
||||
let listError = $state<string | null>(null);
|
||||
let loading = $state(true);
|
||||
|
||||
/// Unresolved-dead-letter count per app (v1.1.1). Loaded in
|
||||
/// parallel after the app list. Failures here are non-fatal —
|
||||
/// missing counts just don't render a badge.
|
||||
let unresolvedDl = $state<Record<string, number>>({});
|
||||
async function loadDlCounts(appList: App[]) {
|
||||
const results = await Promise.all(
|
||||
appList.map(async (a) => {
|
||||
try {
|
||||
const r = await api.deadLetters.count(a.id);
|
||||
return [a.id, r.unresolved] as const;
|
||||
} catch {
|
||||
return [a.id, 0] as const;
|
||||
}
|
||||
})
|
||||
);
|
||||
const next: Record<string, number> = {};
|
||||
for (const [id, count] of results) next[id] = count;
|
||||
unresolvedDl = next;
|
||||
}
|
||||
|
||||
let showCreate = $state(false);
|
||||
let createSlug = $state('');
|
||||
let createName = $state('');
|
||||
@@ -49,6 +69,9 @@
|
||||
listError = null;
|
||||
try {
|
||||
apps = await api.apps.list();
|
||||
if (apps && apps.length > 0) {
|
||||
void loadDlCounts(apps);
|
||||
}
|
||||
} catch (e) {
|
||||
listError = e instanceof Error ? e.message : String(e);
|
||||
apps = null;
|
||||
@@ -201,6 +224,12 @@
|
||||
<div class="primary">
|
||||
<strong>{app.name}</strong>
|
||||
<span class="muted">/{app.slug}</span>
|
||||
{#if unresolvedDl[app.id] > 0}
|
||||
<span
|
||||
class="dl-badge"
|
||||
title="Unresolved dead letters in this app"
|
||||
>{unresolvedDl[app.id]}</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="secondary muted">
|
||||
{app.description ?? '—'}
|
||||
@@ -246,6 +275,19 @@
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.dl-badge {
|
||||
display: inline-block;
|
||||
min-width: 1.25rem;
|
||||
padding: 0.1rem 0.4rem;
|
||||
background: #ef4444;
|
||||
color: #fff;
|
||||
border-radius: 999px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user