Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
d8e5508
feat(api): add isDueToday scheduler helper
Marfuen Apr 24, 2026
647bbfe
test(api): use .spec.ts suffix and fix misleading test title
Marfuen Apr 24, 2026
7f77a99
refactor(api): harden isDueToday exhaustive check and document UTC co…
Marfuen Apr 24, 2026
576aaa6
feat(db): add per-automation scheduleFrequency and lastRunAt
Marfuen Apr 24, 2026
250aa80
refactor(api): remove stale schedule field from browserbase DTOs and …
Marfuen Apr 24, 2026
fcfad8a
feat(api): respect integrationScheduleFrequency in integration orches…
Marfuen Apr 24, 2026
b7068d2
fix(api): retry integration task on transient execution error
Marfuen Apr 24, 2026
f161627
feat(api): respect scheduleFrequency in browser automation orchestrator
Marfuen Apr 24, 2026
75231f7
docs(api): clarify browser orchestrator retry comment
Marfuen Apr 24, 2026
e9f1d57
feat(api): accept scheduleFrequency in automation and task PATCH endp…
Marfuen Apr 24, 2026
5355653
feat(app): add SchedulePicker component
Marfuen Apr 24, 2026
d869d3e
feat(app): add schedule picker to browser automation config
Marfuen Apr 24, 2026
7da3cf0
feat(app): add schedule edit dialog for evidence automations
Marfuen Apr 24, 2026
1774371
feat(app): add integration schedule picker on task detail
Marfuen Apr 24, 2026
a447e34
feat(app): show schedule summary on automation cards
Marfuen Apr 24, 2026
5a44518
fix(app,api): repair test fixtures and guard missing schedule frequency
Marfuen Apr 24, 2026
e7a1a90
style(api): apply prettier to SALE-49 files
Marfuen Apr 24, 2026
fe921a8
fix: address cubic review findings on schedule UI and DTOs
Marfuen Apr 24, 2026
6afa8ad
fix(app): daily period is 1 day in TaskIntegrationChecks next-run calc
Marfuen Apr 24, 2026
e7b6b87
Merge branch 'main' into mariano/sale-49-custom-task-schedules
Marfuen Apr 25, 2026
f675836
fix(api): include integrationScheduleFrequency in createTask response
Marfuen Apr 25, 2026
9e4d513
fix(app): inline schedule picker on automation overview, render label…
Marfuen Apr 25, 2026
b09f55d
feat(app): metrics schedule + next-run reflect frequency in user's TZ
Marfuen Apr 25, 2026
089ad4a
fix(app): use @trycompai/ui Select in SchedulePicker
Marfuen Apr 25, 2026
b8f0081
fix(app): show next-run as next tick (not now + period) when never run
Marfuen Apr 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions apps/api/src/browserbase/browserbase.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ jest.mock('@db', () => ({
}
},
},
TaskFrequency: {
daily: 'daily',
weekly: 'weekly',
monthly: 'monthly',
quarterly: 'quarterly',
yearly: 'yearly',
},
}));

jest.mock('../auth/auth.server', () => ({
Expand All @@ -35,7 +42,9 @@ import { PermissionGuard } from '../auth/permission.guard';

describe('BrowserbaseController.redirectToScreenshot', () => {
let controller: BrowserbaseController;
let service: jest.Mocked<Pick<BrowserbaseService, 'getScreenshotRedirectUrl'>>;
let service: jest.Mocked<
Pick<BrowserbaseService, 'getScreenshotRedirectUrl'>
>;

beforeEach(async () => {
service = {
Expand Down Expand Up @@ -73,7 +82,10 @@ describe('BrowserbaseController.redirectToScreenshot', () => {
organizationId: 'org_1',
download: false,
});
expect(res.redirect).toHaveBeenCalledWith(302, 'https://s3.example.com/fresh-signed');
expect(res.redirect).toHaveBeenCalledWith(
302,
'https://s3.example.com/fresh-signed',
);
});

it('passes download=true to the service when the query param is "true"', async () => {
Expand Down
89 changes: 88 additions & 1 deletion apps/api/src/browserbase/browserbase.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@ jest.mock('@db', () => ({
browserAutomationRun: {
findUnique: jest.fn(),
},
browserAutomation: {
create: jest.fn(),
update: jest.fn(),
},
},
TaskFrequency: {
daily: 'daily',
weekly: 'weekly',
monthly: 'monthly',
quarterly: 'quarterly',
yearly: 'yearly',
},
}));

Expand All @@ -17,7 +28,7 @@ jest.mock('@/app/s3', () => ({
BUCKET_NAME: 'test-bucket',
}));

import { db } from '@db';
import { db, TaskFrequency } from '@db';
import { getSignedUrl } from '@/app/s3';

describe('BrowserbaseService.getScreenshotRedirectUrl', () => {
Expand Down Expand Up @@ -126,3 +137,79 @@ describe('BrowserbaseService.getScreenshotRedirectUrl', () => {
);
});
});

describe('BrowserbaseService schedule frequency passthrough', () => {
let service: BrowserbaseService;

beforeEach(async () => {
jest.clearAllMocks();
const moduleRef = await Test.createTestingModule({
providers: [BrowserbaseService],
}).compile();
service = moduleRef.get(BrowserbaseService);
});

it('forwards scheduleFrequency when creating a browser automation', async () => {
(db.browserAutomation.create as jest.Mock).mockResolvedValue({
id: 'bau_1',
});

await service.createBrowserAutomation({
taskId: 'tsk_1',
name: 'name',
targetUrl: 'https://example.com',
instruction: 'click',
scheduleFrequency: TaskFrequency.weekly,
});

expect(db.browserAutomation.create).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({ scheduleFrequency: 'weekly' }),
}),
);
});

it('omits scheduleFrequency when creating without the field', async () => {
(db.browserAutomation.create as jest.Mock).mockResolvedValue({
id: 'bau_1',
});

await service.createBrowserAutomation({
taskId: 'tsk_1',
name: 'name',
targetUrl: 'https://example.com',
instruction: 'click',
});

const call = (db.browserAutomation.create as jest.Mock).mock.calls[0][0];
expect(call.data).not.toHaveProperty('scheduleFrequency');
});

it('forwards scheduleFrequency when updating a browser automation', async () => {
(db.browserAutomation.update as jest.Mock).mockResolvedValue({
id: 'bau_1',
});

await service.updateBrowserAutomation('bau_1', {
scheduleFrequency: TaskFrequency.monthly,
});

expect(db.browserAutomation.update).toHaveBeenCalledWith(
expect.objectContaining({
where: { id: 'bau_1' },
data: expect.objectContaining({ scheduleFrequency: 'monthly' }),
}),
);
});

it('omits scheduleFrequency when updating without the field', async () => {
(db.browserAutomation.update as jest.Mock).mockResolvedValue({
id: 'bau_1',
});

await service.updateBrowserAutomation('bau_1', { name: 'renamed' });

const call = (db.browserAutomation.update as jest.Mock).mock.calls[0][0];
expect(call.data).not.toHaveProperty('scheduleFrequency');
});
});
32 changes: 22 additions & 10 deletions apps/api/src/browserbase/browserbase.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@ import Browserbase from '@browserbasehq/sdk';
// Lazy-imported in createStagehand() to avoid Node v25 crash
// (SlowBuffer.prototype was removed — @browserbasehq/stagehand bundles buffer-equal-constant-time which uses it)
type Stagehand = import('@browserbasehq/stagehand').Stagehand;
import { db } from '@db';
import { db, TaskFrequency } from '@db';
import { z } from 'zod';
import { GetObjectCommand, PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
import {
GetObjectCommand,
PutObjectCommand,
S3Client,
} from '@aws-sdk/client-s3';
import { BUCKET_NAME, getSignedUrl, s3Client } from '@/app/s3';
import { renderOverlay } from './screenshot-overlay';
import { isNoPageError, toRunErrorMessage } from './run-error-formatter';
Expand Down Expand Up @@ -353,7 +357,7 @@ export class BrowserbaseService {
targetUrl: string;
instruction: string;
evaluationCriteria?: string;
schedule?: string;
scheduleFrequency?: TaskFrequency;
}) {
return db.browserAutomation.create({
data: {
Expand All @@ -363,8 +367,10 @@ export class BrowserbaseService {
targetUrl: data.targetUrl,
instruction: data.instruction,
evaluationCriteria: normalizeCriteria(data.evaluationCriteria),
schedule: data.schedule,
isEnabled: true, // Enable by default so scheduled runs work
...(data.scheduleFrequency !== undefined
? { scheduleFrequency: data.scheduleFrequency }
: {}),
},
});
}
Expand Down Expand Up @@ -402,18 +408,19 @@ export class BrowserbaseService {
targetUrl?: string;
instruction?: string;
evaluationCriteria?: string;
schedule?: string;
isEnabled?: boolean;
scheduleFrequency?: TaskFrequency;
},
) {
const { evaluationCriteria, ...rest } = data;
const { evaluationCriteria, scheduleFrequency, ...rest } = data;
return db.browserAutomation.update({
where: { id: automationId },
data: {
...rest,
...(evaluationCriteria !== undefined
? { evaluationCriteria: normalizeCriteria(evaluationCriteria) }
: {}),
...(scheduleFrequency !== undefined ? { scheduleFrequency } : {}),
},
});
}
Expand Down Expand Up @@ -848,10 +855,15 @@ export class BrowserbaseService {
capturedAt: new Date(),
});
} catch (overlayErr) {
this.logger.warn('Screenshot overlay render failed; uploading raw image', {
error:
overlayErr instanceof Error ? overlayErr.message : String(overlayErr),
});
this.logger.warn(
'Screenshot overlay render failed; uploading raw image',
{
error:
overlayErr instanceof Error
? overlayErr.message
: String(overlayErr),
},
);
}

// Optional evaluation: if the automation was configured with
Expand Down
27 changes: 16 additions & 11 deletions apps/api/src/browserbase/dto/browserbase.dto.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import {
IsEnum,
IsNotEmpty,
IsOptional,
IsString,
IsBoolean,
IsUrl,
} from 'class-validator';
import { TaskFrequency } from '@db';
import { IsSafeUrl } from '../validators/url-safety.validator';

// ===== Session DTOs =====
Expand Down Expand Up @@ -90,10 +92,13 @@ export class CreateBrowserAutomationDto {
@IsOptional()
evaluationCriteria?: string;

@ApiPropertyOptional({ description: 'Cron schedule expression' })
@IsString()
@ApiPropertyOptional({
enum: TaskFrequency,
description: 'Automation schedule cadence',
})
@IsEnum(TaskFrequency)
@IsOptional()
schedule?: string;
scheduleFrequency?: TaskFrequency;
}

export class UpdateBrowserAutomationDto {
Expand Down Expand Up @@ -127,15 +132,18 @@ export class UpdateBrowserAutomationDto {
@IsOptional()
evaluationCriteria?: string;

@ApiPropertyOptional({ description: 'Cron schedule expression' })
@IsString()
@IsOptional()
schedule?: string;

@ApiPropertyOptional({ description: 'Whether automation is enabled' })
@IsBoolean()
@IsOptional()
isEnabled?: boolean;

@ApiPropertyOptional({
enum: TaskFrequency,
description: 'Automation schedule cadence',
})
@IsEnum(TaskFrequency)
@IsOptional()
scheduleFrequency?: TaskFrequency;
}

// ===== Response DTOs =====
Expand Down Expand Up @@ -186,9 +194,6 @@ export class BrowserAutomationResponseDto {
@ApiProperty()
isEnabled: boolean;

@ApiPropertyOptional()
schedule?: string;

@ApiProperty()
createdAt: Date;

Expand Down
7 changes: 6 additions & 1 deletion apps/api/src/tasks/automations/automations.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,12 +87,17 @@ export class AutomationsService {
throw new NotFoundException('Automation not found');
}

const { scheduleFrequency, ...rest } = updateAutomationDto;

// Update the automation
const automation = await db.evidenceAutomation.update({
where: {
id: automationId,
},
data: updateAutomationDto,
data: {
...rest,
...(scheduleFrequency !== undefined ? { scheduleFrequency } : {}),
},
});

return {
Expand Down
13 changes: 11 additions & 2 deletions apps/api/src/tasks/automations/dto/update-automation.dto.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsString, IsOptional, IsBoolean } from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsString, IsOptional, IsBoolean, IsEnum } from 'class-validator';
import { TaskFrequency } from '@db';

export class UpdateAutomationDto {
@ApiProperty({
Expand Down Expand Up @@ -35,4 +36,12 @@ export class UpdateAutomationDto {
@IsString()
@IsOptional()
evaluationCriteria?: string;

@ApiPropertyOptional({
enum: TaskFrequency,
description: 'Automation schedule cadence',
})
@IsEnum(TaskFrequency)
@IsOptional()
scheduleFrequency?: TaskFrequency;
}
28 changes: 28 additions & 0 deletions apps/api/src/tasks/dto/swagger.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ export class CreateTaskDto {
})
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
frequency?: string;

@ApiProperty({
description:
'Cadence for running the integration check attached to this task',
enum: ['daily', 'weekly', 'monthly', 'quarterly', 'yearly'],
required: false,
})
integrationScheduleFrequency?: string;

@ApiProperty({
description: 'Department assignment',
enum: ['none', 'admin', 'gov', 'hr', 'it', 'itsm', 'qms'],
Expand Down Expand Up @@ -80,6 +88,14 @@ export class UpdateTaskDto {
})
frequency?: string;

@ApiProperty({
description:
'Cadence for running the integration check attached to this task',
enum: ['daily', 'weekly', 'monthly', 'quarterly', 'yearly'],
required: false,
})
integrationScheduleFrequency?: string;

@ApiProperty({
description: 'Department assignment',
enum: ['none', 'admin', 'gov', 'hr', 'it', 'itsm', 'qms'],
Expand Down Expand Up @@ -172,6 +188,18 @@ export class TaskResponseDto {
})
frequency: string | null;

@ApiProperty({
description: 'Cadence for running the integration check attached to this task',
enum: ['daily', 'weekly', 'monthly', 'quarterly', 'yearly'],
})
integrationScheduleFrequency: string;

@ApiProperty({
description: 'Last successful integration check run timestamp',
nullable: true,
})
integrationLastRunAt: Date | null;

@ApiProperty({
description: 'Department assignment',
enum: ['none', 'admin', 'gov', 'hr', 'it', 'itsm', 'qms'],
Expand Down
14 changes: 14 additions & 0 deletions apps/api/src/tasks/dto/task-responses.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,4 +85,18 @@ export class TaskResponseDto {
required: false,
})
taskTemplateId?: string | null;

@ApiProperty({
description: 'Cadence for running the integration check attached to this task',
enum: ['daily', 'weekly', 'monthly', 'quarterly', 'yearly'],
example: 'daily',
})
integrationScheduleFrequency: string;

@ApiProperty({
description: 'Last successful integration check run timestamp',
nullable: true,
required: false,
})
integrationLastRunAt?: Date | null;
}
Loading
Loading