diff --git a/apps/web/src/pages/routine-detail-page.tsx b/apps/web/src/pages/routine-detail-page.tsx
index 161f6e5b0..b81e1b0b0 100644
--- a/apps/web/src/pages/routine-detail-page.tsx
+++ b/apps/web/src/pages/routine-detail-page.tsx
@@ -365,14 +365,26 @@ export function RoutineDetailPage({
subtitle={row.tenantName}
actions={
<>
-
-
+ {!row.routine.enabled || health.state === "paused" ? (
+
+ ) : (
+ <>
+
+
+ >
+ )}
diff --git a/apps/web/src/pages/routines-page.tsx b/apps/web/src/pages/routines-page.tsx
index 8fd0d2f60..c36de50f7 100644
--- a/apps/web/src/pages/routines-page.tsx
+++ b/apps/web/src/pages/routines-page.tsx
@@ -351,7 +351,7 @@ export function GlobalRoutinesList({
onToggleEnabled(row, enabled)}
/>
diff --git a/apps/web/test/routine-detail-page.test.tsx b/apps/web/test/routine-detail-page.test.tsx
index da56c0100..cf0307db5 100644
--- a/apps/web/test/routine-detail-page.test.tsx
+++ b/apps/web/test/routine-detail-page.test.tsx
@@ -173,6 +173,32 @@ describe("RoutineDetailPage", () => {
expect(actions).toContain("Run now");
expect(actions).toContain("Pause");
});
+
+ function topBarActions(markup: string): string {
+ const start = markup.indexOf('data-testid="stage-top-bar-actions"');
+ const end = markup.indexOf("", start);
+ return markup.slice(start, end === -1 ? undefined : end);
+ }
+
+ test("hides Run now when the routine is off, and offers Resume only", () => {
+ const actions = topBarActions(
+ renderPage({ routine: { ...routine, enabled: false } }),
+ );
+ expect(actions).toContain("Resume");
+ expect(actions).not.toContain("Run now");
+ expect(actions).not.toContain("Pause");
+ });
+
+ test("hides Run now when the routine is dead-lettered, and offers Resume only", () => {
+ const actions = topBarActions(
+ renderPage({
+ routine: { ...routine, deadLetteredAt: "2026-01-02T00:00:00.000Z" },
+ }),
+ );
+ expect(actions).toContain("Resume");
+ expect(actions).not.toContain("Run now");
+ expect(actions).not.toContain("Pause");
+ });
});
describe("RoutineDetailPage lifecycle actions", () => {
@@ -237,6 +263,21 @@ describe("RoutineDetailPage lifecycle actions", () => {
}
});
+ test("Resume on a dead-lettered routine asks for it to be enabled again", () => {
+ const calls: boolean[] = [];
+ const { container, root } = mount(
+ { onToggleEnabled: (enabled: boolean) => calls.push(enabled) },
+ { routine: { ...routine, deadLetteredAt: "2026-01-02T00:00:00.000Z" } },
+ );
+ try {
+ clickButton(container, "Resume");
+ expect(calls).toEqual([true]);
+ } finally {
+ act(() => root.unmount());
+ container.remove();
+ }
+ });
+
test("Run now triggers the run-now mutation", () => {
let runs = 0;
const { container, root } = mount({
@@ -254,6 +295,37 @@ describe("RoutineDetailPage lifecycle actions", () => {
}
});
+ test("Run now reads as starting while the request is in flight", async () => {
+ let resolveRun: () => void = () => undefined;
+ const pending = new Promise((resolve) => {
+ resolveRun = resolve;
+ });
+ const { container, root } = mount({
+ onRunNow: () => pending,
+ });
+ try {
+ clickButton(container, "Run now");
+ const button = [...container.querySelectorAll("button")].find(
+ (candidate) => candidate.textContent?.includes("Starting") === true,
+ );
+ expect(button).not.toBeUndefined();
+ expect(button?.disabled).toBe(true);
+
+ await act(async () => {
+ resolveRun();
+ await pending;
+ });
+ expect(
+ [...container.querySelectorAll("button")].some(
+ (candidate) => candidate.textContent?.includes("Starting") === true,
+ ),
+ ).toBe(false);
+ } finally {
+ act(() => root.unmount());
+ container.remove();
+ }
+ });
+
test("Edit opens the routine's editor", () => {
let edits = 0;
const { container, root } = mount({
diff --git a/apps/web/test/routines-page.test.tsx b/apps/web/test/routines-page.test.tsx
index 62e306bff..44b8f675e 100644
--- a/apps/web/test/routines-page.test.tsx
+++ b/apps/web/test/routines-page.test.tsx
@@ -262,6 +262,20 @@ describe("GlobalRoutinesList", () => {
expect(markup).toContain("—");
});
+ test("the On switch is named On, not Pause, while the routine is enabled", () => {
+ const markup = renderList([row()]);
+ expect(markup).toContain('aria-label="On Morning brief"');
+ expect(markup).not.toContain('aria-label="Pause Morning brief"');
+ });
+
+ test("the On switch is named Off when the routine is disabled", () => {
+ const markup = renderList([
+ row({ routine: { ...routine, enabled: false } }),
+ ]);
+ expect(markup).toContain('aria-label="Off Morning brief"');
+ expect(markup).not.toContain('aria-label="Resume Morning brief"');
+ });
+
test("Run now calls onRunNow with the row", async () => {
const calls: GlobalRoutineRow[] = [];
const container = document.createElement("div");
@@ -371,6 +385,16 @@ describe("GlobalRoutinesList", () => {
}
});
+ test("a dead-lettered routine's action reads Resume, not Run now", () => {
+ const markup = renderList([
+ row({
+ routine: { ...routine, deadLetteredAt: "2026-01-02T00:00:00.000Z" },
+ }),
+ ]);
+ expect(markup).toContain("Resume");
+ expect(markup).not.toContain("Run now");
+ });
+
test("Edit calls onEditRoutine with the row", () => {
const calls: GlobalRoutineRow[] = [];
const container = document.createElement("div");