1.6.5 Operator dashboard: `/settings/workspace/jobs` — runs table + DLQ tab + replay action
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:
- Tabs: "Recent runs" (default) and "Dead letter" — the latter badged with the current DLQ row count when non-zero.
- Recent runs table: columns — Status (pill: succeeded / failed / running), Function (e.g.,
email.send), Event (email.sendorscheduled.{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, orderedstarted_at desc. Filter: a status pill row at top (All / Succeeded / Failed / Running). - Dead letter table: columns — Function, Event, Attempts, First failed, Last failed, Replayed (timestamp or "—"), Actions (Replay button). Row-click opens the same JSON-detail Dialog (now also showing the replayable event payload). Replay button is gated to
ownerrole only (reuse Story 1.2's role check); other members see a disabled Replay button with a tooltip explaining the gate. - System tab: visible only when the request user's email matches
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 theprodect.system_adminRLS 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-layerreplayDLQwith role gating + audit logging).- Page is a Server Component; it calls services directly. The replay action is a Server Action declared in the same file (Next 16 pattern from Story 1.5).
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.
Acceptance criteria
app/(authed)/settings/workspace/jobs/page.tsxships; sidebar's Settings section grows a "Job runs" sub-link routing to it.lib/jobs/service.tsshipslistJobRuns,listDLQ,countDLQ, and a role-gatedreplayDLQ; the underlying repo functions are single-Prisma-op per the 4-layer rule.- Page renders two tabs ("Recent runs", "Dead letter") with the columns + behaviors in the description; DLQ tab badge shows the current count when non-zero.
- Replay button is enabled only for
ownerrole; clicking it calls a Server Action that invokesreplayDLQ, sets the row'sreplayed_at, re-emits the original event, and shows a success toast. - System tab visible only when the request user's email matches
process.env.PLATFORM_ADMIN_EMAIL; a finding logs the post-Epic-6 replacement plan. - RLS holds: a member of workspace A querying
/settings/workspace/jobswhile the active workspace is B sees zero runs (verified via the existing isolation E2E pattern extended in 1.6.4). - Vitest specs in
tests/jobs/service.test.tscover:listJobRunsfilters by status;countDLQexcludesreplayed_at IS NOT NULL;replayDLQrejects non-owner callers; replay re-emits the original event payload byte-for-byte. - Playwright spec
tests/e2e/jobs-dashboard.spec.tscovers: 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. - All quality gates green; existing tests + E2E stay green; Story 1.5's a11y spec extends to cover the new route (zero axe violations).
Context refs
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.tsxfrom 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- The Story 1.2 workspace-membership role-check helper