Estimate: 26m · Depends on: 1.5.3, 1.6.4
The operator surface. When a workspace invite or password reset fails — or (post-Epic-7) a planning agent run fails — the workspace owner needs a place to see it, understand it, and replay it without going to Inngest's dashboard. This Subtask ships that place.
Route: app/(authed)/settings/workspace/jobs/page.tsx — hangs off the existing /settings/workspace structure from Story 1.2. The Story 1.5 sidebar gets one new sub-link "Job runs" under Settings.
Page structure:
email.send), Event (email.send or scheduled.{job_id}), Attempts, Started, Duration, Failure (short — first line of the error message, full payload reachable via a row-click that opens a Dialog with the JSON detail). 50 rows per page, ordered started_at desc. Filter: a status pill row at top (All / Succeeded / Failed / Running).owner role only (reuse Story 1.2's role check); other members see a disabled Replay button with a tooltip explaining the gate.process.env.PLATFORM_ADMIN_EMAIL (the documented pre-Epic-6 escape hatch from Subtask 1.6.3; tracked as a finding to replace with real platform-admin roles in Epic 6). Same shape as "Recent runs" but queries with the prodect.system_admin RLS context set.Data access:
lib/jobs/service.ts: listJobRuns({ workspaceId, status?, limit, offset }), listDLQ({ workspaceId, limit, offset }), countDLQ({ workspaceId }), replayDLQ(dlqId) (wraps 1.6.4's repo-layer replayDLQ with role gating + audit logging).Empty states: an empty "Recent runs" tab shows "No job runs yet — when a background job runs, it'll appear here." Empty "Dead letter" tab shows "Nothing in the dead-letter queue — every job has succeeded or is still retrying."
Realtime / refresh: NO websockets, NO polling in v1. A "Refresh" button reloads the page. Auto-refresh is deferred — a finding logs the future-work item ("dashboard could auto-refresh every 10s; defer until usage shows demand"). This avoids a half-done realtime story that Epic 6's reporting will revisit holistically.
Tokens: reuses existing pill / badge / table primitives. No new --el-* tokens. The status pill maps to existing success / warning / danger tokens.
app/(authed)/settings/workspace/jobs/page.tsx ships; sidebar's Settings section grows a "Job runs" sub-link routing to it.lib/jobs/service.ts ships listJobRuns, listDLQ, countDLQ, and a role-gated replayDLQ; the underlying repo functions are single-Prisma-op per the 4-layer rule.owner role; clicking it calls a Server Action that invokes replayDLQ, sets the row's replayed_at, re-emits the original event, and shows a success toast.process.env.PLATFORM_ADMIN_EMAIL; a finding logs the post-Epic-6 replacement plan./settings/workspace/jobs while the active workspace is B sees zero runs (verified via the existing isolation E2E pattern extended in 1.6.4).tests/jobs/service.test.ts cover: listJobRuns filters by status; countDLQ excludes replayed_at IS NOT NULL; replayDLQ rejects non-owner callers; replay re-emits the original event payload byte-for-byte.tests/e2e/jobs-dashboard.spec.ts covers: dashboard renders empty state for a fresh workspace; after seeding a failed run, the run appears in the Failed filter; DLQ tab badge increments; Replay button re-runs the job.motir-core/CLAUDE.md — 4-layer rule (auto-loaded)app/(authed)/settings/workspace/page.tsx + the workspace settings layout — the existing structure this Subtask extendsapp/(authed)/settings/project/page.tsx — exemplar for the owner-role gating patterncomponents/ui/AppLayout.tsx + Sidebar.tsx from Story 1.5 — the shell the new route renders insidelib/jobs/* from 1.6.2 / 1.6.3 / 1.6.4 — the runtime + DLQ this dashboard reads