diff --git a/frontend/src/components/ScheduledOperationsView.tsx b/frontend/src/components/ScheduledOperationsView.tsx index fb304467..956c608b 100644 --- a/frontend/src/components/ScheduledOperationsView.tsx +++ b/frontend/src/components/ScheduledOperationsView.tsx @@ -619,13 +619,28 @@ export default function ScheduledOperationsView({ filterNodeId, onClearFilter, p
{task.cron_expression}
- {task.last_status === 'success' ? ( - Success - ) : task.last_status === 'failure' ? ( - Failed - ) : ( - Never run - )} +
+ {task.last_status === 'success' ? ( + Success + ) : task.last_status === 'failure' ? ( + Failed + ) : ( + Never run + )} + {task.delete_after_run === 1 && ( + + One-shot + + )} +
{formatTimestamp(task.next_run_at)} diff --git a/frontend/src/components/__tests__/ScheduledOperationsView.test.tsx b/frontend/src/components/__tests__/ScheduledOperationsView.test.tsx index 95347906..873fe65e 100644 --- a/frontend/src/components/__tests__/ScheduledOperationsView.test.tsx +++ b/frontend/src/components/__tests__/ScheduledOperationsView.test.tsx @@ -79,6 +79,37 @@ describe('ScheduledOperationsView', () => { expect(await screen.findByText('nightly-prune')).toBeInTheDocument(); }); + it('marks a one-shot task with a chip and leaves recurring tasks unmarked', async () => { + tasksFixture = [ + makeTask({ id: 1, name: 'recurring-task' }), + makeTask({ id: 2, name: 'one-shot-task', delete_after_run: 1 }), + ]; + render(); + + await userEvent.click(await screen.findByRole('button', { name: /All tasks/ })); + await screen.findByText('recurring-task'); + + // A single One-shot chip, scoped to the one-shot row. + const chips = screen.getAllByText('One-shot'); + expect(chips).toHaveLength(1); + // A pending one-shot carries the plain lifecycle tooltip, not the failure copy. + expect(chips[0]).toHaveAttribute('title', expect.stringContaining('Deletes itself after a successful run')); + expect(chips[0]).toHaveAttribute('title', expect.not.stringContaining('kept after a failed run')); + const oneShotRow = screen.getByText('one-shot-task').closest('tr'); + expect(oneShotRow).toContainElement(chips[0]); + const recurringRow = screen.getByText('recurring-task').closest('tr'); + expect(recurringRow?.textContent).not.toContain('One-shot'); + }); + + it('explains a failed one-shot was kept to debug', async () => { + tasksFixture = [makeTask({ id: 1, name: 'failed-one-shot', delete_after_run: 1, last_status: 'failure' })]; + render(); + + await userEvent.click(await screen.findByRole('button', { name: /All tasks/ })); + const chip = await screen.findByText('One-shot'); + expect(chip).toHaveAttribute('title', expect.stringContaining('kept after a failed run')); + }); + it('opens the create modal from a prefill and consumes it once', async () => { const onPrefillConsumed = vi.fn(); render(