1const DEFAULT_API_SLUG = 'flexylead'; 2const ALLOWED_APIS = new Set(['flexylead', 'flexylead-partner']); 3const LIVE_JOB_STATUSES = new Set(['pending', 'running', 'retrying', 'active']); 4const RETIRED_ROUTING_JOBS = new Set(['flexylead-dispatch-leadbyte-prospects']); 5const CANONICAL_JOBS = [ 6 { 7 name: 'flexylead-process-ai-ready-appels', 8 every_seconds: 60, 9 timeout_seconds: 300, 10 max_retries: 2, 11 backoff_seconds: 30, 12 function_name: 'process-appel-db-only', 13 event: { limit: 1, mode: 'sweep', source: 'jobs' }, 14 }, 15 { 16 name: 'flexylead-reconcile-sale-revenue', 17 every_seconds: 300, 18 timeout_seconds: 60, 19 max_retries: 2, 20 backoff_seconds: 30, 21 function_name: 'flexylead-reconcile-sale-revenue', 22 event: { limit: 500, source: 'jobs' }, 23 }, 24]; 25 26export default async function handler(event = {}, context = null) { 27 if (!context?.call) throw new Error('App context is unavailable.'); 28 29 const body = event?.body && typeof event.body === 'object' ? event.body : event; 30 const action = clean(body?.action || 'probe'); 31 32 if (action === 'probe') { 33 const result = await context.call('api', 'api_list', {}); 34 return { 35 ok: true, 36 gateway_available: true, 37 api_count: Array.isArray(result?.apis) ? result.apis.length : 0, 38 }; 39 } 40 41 if (action === 'configure') { 42 return configureGateway(context, body); 43 } 44 45 if (action === 'inventory') { 46 const [apis, functions, jobs, tables] = await Promise.all([ 47 context.call('api', 'api_list', {}), 48 context.call('functions', 'functions_list', {}), 49 context.call('jobs', 'jobs_list', {}), 50 context.call('tables', 'tables_list', {}), 51 ]); 52 return { 53 ok: true, 54 apis: arrayOf(apis?.apis).map((api) => pick(api, ['id', 'slug', 'name', 'status', 'auth_kind'])), 55 functions: arrayOf(functions?.functions).map((fn) => pick(fn, [ 56 'id', 'name', 'runtime', 'status', 'active_version', 'timeout_ms', 'updated_at', 57 ])), 58 jobs: Array.isArray(jobs?.jobs) ? jobs.jobs : Array.isArray(jobs) ? jobs : [], 59 tables: (Array.isArray(tables?.tables) ? tables.tables : Array.isArray(tables) ? tables : []) 60 .map((table) => ({ name: clean(table?.name), row_count: Number(table?.row_count || 0) })), 61 }; 62 } 63 64 if (action === 'reconcile_jobs') { 65 return reconcileJobs(context); 66 } 67 68 throw new Error('Unsupported gateway bootstrap action.'); 69} 70 71async function reconcileJobs(context) { 72 const listing = await context.call('jobs', 'jobs_list', { owner_app: 'flexylead' }); 73 const existing = Array.isArray(listing?.jobs) ? listing.jobs : Array.isArray(listing) ? listing : []; 74 const results = []; 75 for (const job of existing) { 76 if (!RETIRED_ROUTING_JOBS.has(clean(job?.name)) || !LIVE_JOB_STATUSES.has(clean(job?.status).toLowerCase())) continue; 77 await context.call('jobs', 'jobs_cancel', { id: Number(job.id) }); 78 results.push({ name: clean(job.name), id: Number(job.id), action: 'retired' }); 79 } 80 for (const definition of CANONICAL_JOBS) { 81 const matching = existing.filter((job) => clean(job?.name) === definition.name && LIVE_JOB_STATUSES.has(clean(job?.status).toLowerCase())); 82 const canonical = matching.find((job) => isCanonicalJob(job, definition)); 83 for (const job of matching) { 84 if (canonical && Number(job?.id) === Number(canonical?.id)) continue; 85 await context.call('jobs', 'jobs_cancel', { id: Number(job.id) }); 86 } 87 if (canonical) { 88 results.push({ name: definition.name, id: Number(canonical.id), action: 'kept' }); 89 continue; 90 } 91 const scheduled = await context.call('jobs', 'jobs_schedule', { 92 name: definition.name, 93 schedule: { kind: 'every', every_seconds: definition.every_seconds }, 94 timezone: 'Europe/Madrid', 95 target: canonicalTarget(definition), 96 owner_app: 'flexylead', 97 idempotency_key: `${definition.name}:native:v1`, 98 max_retries: definition.max_retries, 99 backoff_seconds: definition.backoff_seconds, 100 }); 101 const job = scheduled?.job || scheduled; 102 const id = Number(job?.id || 0); 103 if (!id) throw new Error(`Jobs did not return an id for ${definition.name}.`); 104 await context.call('jobs', 'jobs_run_now', { id }); 105 results.push({ name: definition.name, id, action: 'scheduled_and_started' }); 106 } 107 return { ok: true, jobs: results }; 108} 109 110function isCanonicalJob(job, definition) { 111 const target = job?.target || {}; 112 return clean(job?.schedule_kind) === 'every' 113 && Number(job?.every_seconds) === definition.every_seconds 114 && clean(target?.kind) === 'app_tool' 115 && clean(target?.app) === 'functions' 116 && clean(target?.tool) === 'functions_invoke' 117 && clean(target?.input?.name) === definition.function_name 118 && JSON.stringify(target?.input?.event || {}) === JSON.stringify(definition.event); 119} 120 121function canonicalTarget(definition) { 122 return { 123 kind: 'app_tool', 124 app: 'functions', 125 tool: 'functions_invoke', 126 input: { name: definition.function_name, event: definition.event },