diff --git a/apps/desktop/src/main/database/runner.ts b/apps/desktop/src/main/database/runner.ts index c291dfc..8138bff 100644 --- a/apps/desktop/src/main/database/runner.ts +++ b/apps/desktop/src/main/database/runner.ts @@ -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; + ` } ]; diff --git a/apps/desktop/src/main/ipc/campaigns-ipc.ts b/apps/desktop/src/main/ipc/campaigns-ipc.ts index 2f55e24..c5801b0 100644 --- a/apps/desktop/src/main/ipc/campaigns-ipc.ts +++ b/apps/desktop/src/main/ipc/campaigns-ipc.ts @@ -300,6 +300,7 @@ export function registerCampaignsIpc(): void { .prepare( ` SELECT + j.id, j.id as jobId, j.type, j.status, diff --git a/apps/desktop/src/main/ipc/observability-ipc.ts b/apps/desktop/src/main/ipc/observability-ipc.ts index 7d5c914..19ba18e 100644 --- a/apps/desktop/src/main/ipc/observability-ipc.ts +++ b/apps/desktop/src/main/ipc/observability-ipc.ts @@ -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); }; diff --git a/apps/desktop/src/main/workers/plugins/automation.ts b/apps/desktop/src/main/workers/plugins/automation.ts index ba782e2..3853968 100644 --- a/apps/desktop/src/main/workers/plugins/automation.ts +++ b/apps/desktop/src/main/workers/plugins/automation.ts @@ -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 ───────────────────────────────────────────────────────────────────── @@ -914,7 +915,7 @@ export async function executeAutomationWorkflow(ctx: JobContext): Promise { `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}").` ); @@ -1827,32 +1828,70 @@ 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, @@ -1860,7 +1899,7 @@ async function handleSendEmailStep( 'smtp.user', 'smtpUsername', 'username' - ); + ) || account?.smtpUsername; let password = resolveSettingValue( ctx.payload._secrets, settings, @@ -1868,7 +1907,7 @@ async function handleSendEmailStep( 'smtp.pass', 'smtpPassword', 'password' - ); + ) || accountPassword; let senderName = resolveSettingValue( ctx.payload._secrets, diff --git a/apps/desktop/src/preload/index.ts b/apps/desktop/src/preload/index.ts index a4a1f0e..4514ecb 100644 --- a/apps/desktop/src/preload/index.ts +++ b/apps/desktop/src/preload/index.ts @@ -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); diff --git a/apps/desktop/src/renderer/screens/CampaignsScreen.tsx b/apps/desktop/src/renderer/screens/CampaignsScreen.tsx index d6e5b6d..086e628 100644 --- a/apps/desktop/src/renderer/screens/CampaignsScreen.tsx +++ b/apps/desktop/src/renderer/screens/CampaignsScreen.tsx @@ -837,7 +837,7 @@ export default function CampaignsScreen() { {sequence?.name || '—'} - 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 @@ -1179,7 +1179,7 @@ export default function CampaignsScreen() { {getEnrollmentStatusBadge(selectedEnrollment.status)} 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} {selectedEnrollment.status === 'replied' && (