MotirBuilding in public
MOTIR · moooon
onMotir
You’re viewing a public project. Anyone can view it — no account needed. Sign in to submit, upvote, or comment on requests.View-only — you can’t edit work items
MOTIR-64

1.6.5 Operator dashboard: `/settings/workspace/jobs` — runs table + DLQ tab + replay action

Done
Description

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.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).
  • 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 owner role 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 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).
  • 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.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.
  • 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 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.
  • 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/jobs while 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.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.
  • Playwright spec 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.
  • 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 extends
  • app/(authed)/settings/project/page.tsx — exemplar for the owner-role gating pattern
  • components/ui/AppLayout.tsx + Sidebar.tsx from Story 1.5 — the shell the new route renders inside
  • lib/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