Skip to content
Merged
6 changes: 6 additions & 0 deletions apps/desktop/src/main/database/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -663,6 +663,12 @@ export const MIGRATIONS: Migration[] = [

CREATE UNIQUE INDEX IF NOT EXISTS idx_workspace_memory_key ON workspace_memory(workspaceId, scope, key);
`
},
{
name: '025_add_contacts_tags',
up: `
ALTER TABLE contacts ADD COLUMN tags TEXT;
`
}
];

Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/main/ipc/campaigns-ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,7 @@ export function registerCampaignsIpc(): void {
.prepare(
`
SELECT
j.id,
j.id as jobId,
j.type,
j.status,
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/main/ipc/observability-ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ export function registerObservabilityIpc() {
WHERE type = ? AND status = 'completed' AND durationMs IS NOT NULL
`
)
.get() as any;
.get(type) as any;
return Math.round(row?.avgVal || 0);
};

Expand Down
71 changes: 55 additions & 16 deletions apps/desktop/src/main/workers/plugins/automation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { randomUUID } from 'crypto';
import nodemailer from 'nodemailer';
import { AIRuntime, PromptsLibrary } from '@leadforge/ai';
import type { JobContext } from '../../../shared/types/job';
import { decryptSecret } from '../../lib/crypto';

// ── Types ─────────────────────────────────────────────────────────────────────

Expand Down Expand Up @@ -914,7 +915,7 @@ export async function executeAutomationWorkflow(ctx: JobContext): Promise<any> {
`Automation workflow: sequence "${sequenceId}" not found in workspace "${ctx.workspaceId}".`
);
}
if (sequence.status !== 'active') {
if (sequence.status?.toLowerCase() !== 'active') {
throw new Error(
`Automation workflow: sequence "${sequence.name}" is not active (status: "${sequence.status}").`
);
Expand Down Expand Up @@ -1827,48 +1828,86 @@ async function handleSendEmailStep(
const renderedBody = resolveVariables(tpl.body, renderCtx);

const settings = loadSettings(db, workspaceId);
const account = db
.prepare(
`
SELECT id, email, name
FROM email_accounts
WHERE workspaceId = ? AND status = 'connected' AND deletedAt IS NULL
ORDER BY createdAt ASC LIMIT 1
`
)
.get(workspaceId) as { id: string; email: string; name: string } | undefined;

let host = resolveSettingValue(ctx.payload._secrets, settings, 'smtp.host', 'smtpHost', 'host');
let accountId: string | undefined = undefined;
try {
const execRow = db
.prepare('SELECT campaignId FROM sequence_executions WHERE id = ?')
.get(execCtx.execution.id) as { campaignId: string | null } | undefined;
if (execRow?.campaignId) {
const campaignRow = db
.prepare('SELECT sendingAccountId FROM campaigns WHERE id = ? AND deletedAt IS NULL')
.get(execRow.campaignId) as { sendingAccountId: string } | undefined;
accountId = campaignRow?.sendingAccountId;
}
} catch {}

let account: any = undefined;
if (accountId) {
account = db
.prepare(
`
SELECT id, email, name, smtpHost, smtpPort, smtpSecure, smtpUsername, smtpPassword
FROM email_accounts
WHERE id = ? AND workspaceId = ? AND status = 'connected' AND deletedAt IS NULL
`
)
.get(accountId, workspaceId);
}

if (!account) {
account = db
.prepare(
`
SELECT id, email, name, smtpHost, smtpPort, smtpSecure, smtpUsername, smtpPassword
FROM email_accounts
WHERE workspaceId = ? AND status = 'connected' AND deletedAt IS NULL
ORDER BY createdAt ASC LIMIT 1
`
)
.get(workspaceId);
}

let accountPassword = '';
if (account?.smtpPassword) {
try {
accountPassword = decryptSecret(account.smtpPassword);
} catch {
accountPassword = account.smtpPassword;
}
}

let host = resolveSettingValue(ctx.payload._secrets, settings, 'smtp.host', 'smtpHost', 'host') || account?.smtpHost;
let portStr = resolveSettingValue(
ctx.payload._secrets,
settings,
'smtp.port',
'smtpPort',
'port'
);
) || (account?.smtpPort ? String(account.smtpPort) : undefined);
let secureStr = resolveSettingValue(
ctx.payload._secrets,
settings,
'smtp.secure',
'smtpSecure',
'secure'
);
) || (account?.smtpSecure ? String(account.smtpSecure) : undefined);
let username = resolveSettingValue(
ctx.payload._secrets,
settings,
'smtp.username',
'smtp.user',
'smtpUsername',
'username'
);
) || account?.smtpUsername;
let password = resolveSettingValue(
ctx.payload._secrets,
settings,
'smtp.password',
'smtp.pass',
'smtpPassword',
'password'
);
) || accountPassword;
let senderName =
resolveSettingValue(
ctx.payload._secrets,
Expand Down
8 changes: 7 additions & 1 deletion apps/desktop/src/preload/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,13 @@ contextBridge.exposeInMainWorld('ipc', {
'updater:download',
'updater:install',
'agent:execute',
'agent:workflow:execute'
'agent:workflow:execute',
'campaigns:enroll',
'campaigns:enrollments:list',
'campaigns:bulk-pause-enrollments',
'campaigns:bulk-resume-enrollments',
'campaigns:bulk-remove-enrollments',
'scheduler:queue:list'
];
if (validChannels.includes(channel as string)) {
return ipcRenderer.invoke(channel, payload);
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/src/renderer/screens/CampaignsScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -837,7 +837,7 @@ export default function CampaignsScreen() {
{sequence?.name || '—'}
</div>
<span className="block text-[9px] text-muted-foreground">
Steps: {sequence?.steps ? JSON.parse(sequence.steps).length : 0} steps
Steps: {sequence?.steps ? (typeof sequence.steps === 'string' ? JSON.parse(sequence.steps).length : sequence.steps.length) : 0} steps
</span>
</div>

Expand Down Expand Up @@ -1179,7 +1179,7 @@ export default function CampaignsScreen() {
{getEnrollmentStatusBadge(selectedEnrollment.status)}
<span className="font-mono">
Step {selectedEnrollment.currentStepIndex || 0} of{' '}
{sequence?.steps ? JSON.parse(sequence.steps).length : 0}
{sequence?.steps ? (typeof sequence.steps === 'string' ? JSON.parse(sequence.steps).length : sequence.steps.length) : 0}
</span>
</div>
{selectedEnrollment.status === 'replied' && (
Expand Down
Loading