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:
@@ -186,6 +186,23 @@ export interface UpdateScriptInput {
|
||||
sandbox?: ScriptSandbox;
|
||||
}
|
||||
|
||||
export interface DeadLetterRow {
|
||||
id: string;
|
||||
app_id: string;
|
||||
source: string;
|
||||
op: string;
|
||||
trigger_id: string | null;
|
||||
script_id: string | null;
|
||||
payload: unknown;
|
||||
attempt_count: number;
|
||||
first_attempt_at: string;
|
||||
last_attempt_at: string;
|
||||
last_error: string;
|
||||
created_at: string;
|
||||
resolved_at: string | null;
|
||||
resolution: 'replayed' | 'ignored' | 'handled_by_script' | 'handler_failed' | null;
|
||||
}
|
||||
|
||||
export interface ExecutionResult {
|
||||
status: number;
|
||||
headers: Record<string, string>;
|
||||
@@ -516,6 +533,37 @@ export const api = {
|
||||
)
|
||||
},
|
||||
|
||||
deadLetters: {
|
||||
count: (idOrSlug: string) =>
|
||||
adminRequest<{ unresolved: number }>(
|
||||
`/api/v1/admin/apps/${encodeURIComponent(idOrSlug)}/dead_letters/count`
|
||||
),
|
||||
list: (idOrSlug: string, opts: { unresolved?: boolean; limit?: number; offset?: number } = {}) => {
|
||||
const params = new URLSearchParams();
|
||||
if (opts.unresolved) params.set('unresolved', 'true');
|
||||
if (opts.limit !== undefined) params.set('limit', String(opts.limit));
|
||||
if (opts.offset !== undefined) params.set('offset', String(opts.offset));
|
||||
const qs = params.toString();
|
||||
return adminRequest<{ dead_letters: DeadLetterRow[] }>(
|
||||
`/api/v1/admin/apps/${encodeURIComponent(idOrSlug)}/dead_letters${qs ? `?${qs}` : ''}`
|
||||
);
|
||||
},
|
||||
get: (idOrSlug: string, dlId: string) =>
|
||||
adminRequest<DeadLetterRow>(
|
||||
`/api/v1/admin/apps/${encodeURIComponent(idOrSlug)}/dead_letters/${dlId}`
|
||||
),
|
||||
replay: (idOrSlug: string, dlId: string) =>
|
||||
adminRequest<null>(
|
||||
`/api/v1/admin/apps/${encodeURIComponent(idOrSlug)}/dead_letters/${dlId}/replay`,
|
||||
{ method: 'POST' }
|
||||
),
|
||||
resolve: (idOrSlug: string, dlId: string, reason: string) =>
|
||||
adminRequest<null>(
|
||||
`/api/v1/admin/apps/${encodeURIComponent(idOrSlug)}/dead_letters/${dlId}/resolve`,
|
||||
{ method: 'POST', body: JSON.stringify({ reason }) }
|
||||
)
|
||||
},
|
||||
|
||||
execute: async (
|
||||
id: string,
|
||||
body: unknown,
|
||||
|
||||
Reference in New Issue
Block a user