diff --git a/apps/web/src/components/settings/automations/AutomationsSettings.render.client.test.tsx b/apps/web/src/components/settings/automations/AutomationsSettings.render.client.test.tsx index b1bfb95e1..b6a9b053e 100644 --- a/apps/web/src/components/settings/automations/AutomationsSettings.render.client.test.tsx +++ b/apps/web/src/components/settings/automations/AutomationsSettings.render.client.test.tsx @@ -16,8 +16,8 @@ const state = vi.hoisted(() => ({ name: string; prompt: string; enabled: boolean; - scheduleMode: 'weekly'; - cronExpression: null; + scheduleMode: 'weekly' | 'cron'; + cronExpression: string | null; model: null; environmentId: string; target: { @@ -25,7 +25,7 @@ const state = vi.hoisted(() => ({ externalRef: string; metadata?: Record; }; - lastRunAt: null; + lastRunAt: Date | null; lastSucceededAt: null; lastFailedAt: null; lastError: null; @@ -925,11 +925,9 @@ describe('AutomationsSettings', () => { name: 'Toggle Weekly flaky-test scan', }), ).toBeChecked(); - expect( - screen.getByText( - 'Weekly · Production · slack:#roomote-managers · Created by Ada', - ), - ).toBeInTheDocument(); + expect(screen.getByText('Weekly, in Production →')).toBeInTheDocument(); + expect(screen.getByText('Slack #roomote-managers')).toBeInTheDocument(); + expect(screen.getByText('Created by Ada')).toBeInTheDocument(); expect( screen.getByRole('button', { name: 'Run Weekly flaky-test scan now' }), ).toBeEnabled(); @@ -973,6 +971,43 @@ describe('AutomationsSettings', () => { ).not.toBeInTheDocument(); }); + it('humanizes custom schedules and shows the last run when available', async () => { + state.environments = [{ id: 'env-1', name: 'Production' }]; + state.customAutomations = [ + { + id: 'automation-1', + name: 'Weekday scan', + prompt: 'Find flaky tests.', + enabled: true, + scheduleMode: 'cron', + cronExpression: '0 9 * * 1-5', + model: null, + environmentId: 'env-1', + target: { provider: 'slack', externalRef: 'C123MANAGER' }, + lastRunAt: new Date(), + lastSucceededAt: null, + lastFailedAt: null, + lastError: null, + lastLaunchedTaskId: null, + createdByName: 'Ada', + createdAt: new Date('2026-01-01T00:00:00Z'), + updatedAt: new Date('2026-01-01T00:00:00Z'), + }, + ]; + + render(); + + expect( + await screen.findByText( + 'At 09:00 AM, Monday through Friday, in Production →', + ), + ).toBeInTheDocument(); + expect(screen.getByText(/Created by Ada/)).toHaveTextContent( + /Created by Ada · Last run \d+s ago/, + ); + expect(screen.queryByText('0 9 * * 1-5')).not.toBeInTheDocument(); + }); + it('only offers connected providers as custom automation destinations', async () => { state.settingsQuery.data.capabilities.slackConnected = false; state.settingsQuery.data.capabilities.discordConnected = true; diff --git a/apps/web/src/components/settings/automations/CustomAutomationsSection.tsx b/apps/web/src/components/settings/automations/CustomAutomationsSection.tsx index ed750eb55..1d24f7950 100644 --- a/apps/web/src/components/settings/automations/CustomAutomationsSection.tsx +++ b/apps/web/src/components/settings/automations/CustomAutomationsSection.tsx @@ -9,13 +9,14 @@ import { } from '@roomote/types'; import { tryParseCronSchedule } from '@/lib/cron-schedule'; -import { formatTimeZone } from '@/lib/formatters'; +import { formatDistanceToNowCompact, formatTimeZone } from '@/lib/formatters'; import { useTRPC } from '@/trpc/client'; import type { CustomAutomationListItem } from '@/trpc/commands/automations'; import { Button, BasicTooltip, + BrandIcon, Card, CardContent, Dialog, @@ -107,6 +108,17 @@ function scheduleLabel(mode: CustomAutomationScheduleMode): string { ); } +function cadenceLabel(row: CustomAutomationListItem): string { + if (row.scheduleMode !== 'cron') { + return scheduleLabel(row.scheduleMode); + } + + return row.cronExpression + ? (tryParseCronSchedule(row.cronExpression, 'UTC')?.summary ?? + 'Custom schedule') + : 'Custom schedule'; +} + function targetFromRow(row: CustomAutomationListItem): { provider: CustomAutomationFormState['targetProvider']; channelId: string; @@ -977,9 +989,13 @@ export function CustomAutomationsSection() { (environment) => environment.id === row.environmentId, )?.name ?? 'Environment missing'; const target = targetFromRow(row); + const destinationName = + DESTINATION_OPTIONS.find( + (option) => option.value === target.provider, + )?.label ?? 'No report channel'; const destinationLabel = target.provider === 'none' - ? 'no report channel' + ? '' : target.provider === 'slack' ? (slackOptions.find( (option) => @@ -1012,15 +1028,37 @@ export function CustomAutomationsSection() { />

{row.name}

+

+ + {cadenceLabel(row)}, in {environmentName} → + + {target.provider !== 'none' ? ( + + ) : null} + + {destinationName} + {destinationLabel ? ` ${destinationLabel}` : ''} + +

- {row.scheduleMode === 'cron' - ? row.cronExpression - : scheduleLabel(row.scheduleMode)}{' '} - · {environmentName} ·{' '} - {target.provider === 'none' - ? destinationLabel - : `${target.provider}:${destinationLabel}`}{' '} - · Created by {row.createdByName ?? 'Unknown'} + Created by {row.createdByName ?? 'Unknown'} + {row.lastRunAt ? ( + <> + {' · Last run '} + + {formatDistanceToNowCompact( + new Date(row.lastRunAt), + { addSuffix: true }, + )} + + + ) : null}