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:
@@ -37,6 +37,20 @@
|
||||
let domains = $state<AppDomain[]>([]);
|
||||
let members = $state<AppMemberDto[]>([]);
|
||||
|
||||
/// v1.1.1 dead-letters surface — design notes §4 mandates the
|
||||
/// dashboard surface this since there's no default handler.
|
||||
let unresolvedDeadLetters = $state<number>(0);
|
||||
async function loadDeadLetterCount(idOrSlug: string) {
|
||||
try {
|
||||
const r = await api.deadLetters.count(idOrSlug);
|
||||
unresolvedDeadLetters = r.unresolved;
|
||||
} catch {
|
||||
// Non-fatal: the page renders fine without the badge if
|
||||
// the count endpoint is unreachable (e.g. older server).
|
||||
unresolvedDeadLetters = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Derive UI gates from the capabilities helper so the rules stay
|
||||
// in lockstep with the backend's `can()`. canAdminApp also covers
|
||||
// the Members + Settings + Domains-mutation tabs; canWriteApp
|
||||
@@ -107,7 +121,11 @@
|
||||
editName = app.name;
|
||||
editDescription = app.description ?? '';
|
||||
editSlug = app.slug;
|
||||
const loaders: Promise<unknown>[] = [loadScripts(app.id), loadDomains(app.id)];
|
||||
const loaders: Promise<unknown>[] = [
|
||||
loadScripts(app.id),
|
||||
loadDomains(app.id),
|
||||
loadDeadLetterCount(app.id)
|
||||
];
|
||||
if (canAdmin) {
|
||||
loaders.push(loadMembers(app.id), loadEligibleUsers());
|
||||
}
|
||||
@@ -421,6 +439,16 @@
|
||||
class:active={activeTab === 'settings'}
|
||||
onclick={() => (activeTab = 'settings')}>Settings</button
|
||||
>
|
||||
<a
|
||||
class="tab-link"
|
||||
href="{base}/apps/{slug}/dead-letters"
|
||||
title="Dead letters — replay or resolve events that exhausted their retry policy"
|
||||
>
|
||||
Dead letters
|
||||
{#if unresolvedDeadLetters > 0}
|
||||
<span class="dl-badge">{unresolvedDeadLetters}</span>
|
||||
{/if}
|
||||
</a>
|
||||
{/if}
|
||||
</nav>
|
||||
|
||||
@@ -871,6 +899,32 @@
|
||||
border-bottom-color: #38bdf8;
|
||||
}
|
||||
|
||||
.tabs .tab-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
color: #94a3b8;
|
||||
text-decoration: none;
|
||||
padding: 0.6rem 1rem;
|
||||
margin-left: auto;
|
||||
border-bottom: 2px solid transparent;
|
||||
font: inherit;
|
||||
}
|
||||
.tabs .tab-link:hover {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
|
||||
button {
|
||||
background: #38bdf8;
|
||||
color: #0b1220;
|
||||
|
||||
Reference in New Issue
Block a user