@detiq/crawler 1.1.17 → 1.1.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@detiq/crawler",
3
- "version": "1.1.17",
3
+ "version": "1.1.19",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public",
package/src/job-queue.ts CHANGED
@@ -240,8 +240,25 @@ export async function claimNextQueuedJob(): Promise<ClaimedJob | null> {
240
240
 
241
241
  /** Requeues RUNNING jobs whose heartbeat has gone stale — the worker that claimed them
242
242
  * died (crash, restart, OOM) without ever marking them DONE/FAILED. Idempotent and
243
- * safe to run from every worker process; only affects rows past the timeout. */
243
+ * safe to run from every worker process; only affects rows past the timeout.
244
+ * API-in-process job types (GENERATE_SCRIPT etc.) are failed directly — requeueing them
245
+ * would cause the worker to claim and fail them with a confusing "claimed by worker" error. */
244
246
  export async function requeueStaleRunningJobs(): Promise<void> {
247
+ // Directly fail stale API-in-process jobs — worker cannot process these types.
248
+ const failedRows = await prismaAdmin.$queryRaw<Array<{ id: string }>>`
249
+ UPDATE "PlatformJob"
250
+ SET status = 'FAILED',
251
+ "finishedAt" = NOW(),
252
+ error = 'API process was restarted while this job was running — please retry from the UI.'
253
+ WHERE status = 'RUNNING'
254
+ AND type IN ('GENERATE_SCRIPT', 'GENERATE_DATA', 'GENERATE_TESTS', 'GENERATE_TEST_PLAN')
255
+ AND COALESCE("heartbeatAt", "startedAt") < NOW() - (${STALE_JOB_TIMEOUT_MINUTES} * interval '1 minute')
256
+ RETURNING id
257
+ `;
258
+ if (failedRows.length > 0) {
259
+ logger.warn({ jobIds: failedRows.map((r: any) => r.id), staleTimeoutMinutes: STALE_JOB_TIMEOUT_MINUTES }, '[job-worker] failed stale API-in-process job(s) — API restarted while running');
260
+ }
261
+
245
262
  const rows = await prismaAdmin.$queryRaw<Array<{ id: string }>>`
246
263
  UPDATE "PlatformJob"
247
264
  SET status = 'QUEUED',
@@ -253,6 +270,7 @@ export async function requeueStaleRunningJobs(): Promise<void> {
253
270
  true
254
271
  )
255
272
  WHERE status = 'RUNNING'
273
+ AND type NOT IN ('GENERATE_SCRIPT', 'GENERATE_DATA', 'GENERATE_TESTS', 'GENERATE_TEST_PLAN')
256
274
  AND COALESCE("heartbeatAt", "startedAt") < NOW() - (${STALE_JOB_TIMEOUT_MINUTES} * interval '1 minute')
257
275
  RETURNING id
258
276
  `;
@@ -1064,19 +1082,25 @@ async function processFigmaImportJob(jobId: string, config: any) {
1064
1082
  tx.platformJob.update({ where: { id: jobId }, data: { status: 'RUNNING', startedAt: new Date() } })
1065
1083
  );
1066
1084
  try {
1067
- const connector = await withTenant(tenantId, (tx) =>
1068
- (tx as any).connector.findFirst({ where: { projectId, kind: 'FIGMA', tenantId } })
1069
- ) as any;
1070
- if (!connector) throw new Error('Figma connector not configured. Add your Figma token in Project Settings → Integrations.');
1071
-
1072
- const cfg = connector.config as Record<string, string>;
1073
- const accessToken = cfg.accessToken_enc ? decrypt(cfg.accessToken_enc) : '';
1074
- if (!accessToken) throw new Error('Figma access token missing. Edit the Figma connector and add your Personal Access Token.');
1075
-
1076
- const resolvedFileKey = fileKey ?? cfg.fileKey ?? '';
1085
+ // Prefer OAuth token (connected via Figma OAuth flow), fall back to PAT connector.
1086
+ const oauthConn = await AppBrain.getOAuthConnection(tenantId, projectId, 'FIGMA');
1087
+ let creds: figma.FigmaCredentials;
1088
+ const resolvedFileKey = fileKey ?? '';
1077
1089
  if (!resolvedFileKey) throw new Error('No Figma file key. Paste the full Figma file URL as the App URL.');
1078
1090
 
1079
- const creds: figma.FigmaCredentials = { accessToken, fileKey: resolvedFileKey };
1091
+ if (oauthConn?.accessToken && oauthConn.status === 'CONNECTED') {
1092
+ // OAuth token must use Authorization: Bearer header (oauthToken field), not X-Figma-Token
1093
+ creds = { accessToken: '', oauthToken: oauthConn.accessToken, fileKey: resolvedFileKey };
1094
+ } else {
1095
+ const connector = await withTenant(tenantId, (tx) =>
1096
+ (tx as any).connector.findFirst({ where: { projectId, kind: 'FIGMA', tenantId } })
1097
+ ) as any;
1098
+ if (!connector) throw new Error('Figma connector not configured. Add your Figma token in Project Settings → Integrations.');
1099
+ const cfg = connector.config as Record<string, string>;
1100
+ const pat = cfg.accessToken_enc ? decrypt(cfg.accessToken_enc) : '';
1101
+ if (!pat) throw new Error('Figma access token missing. Edit the Figma connector and add your Personal Access Token.');
1102
+ creds = { accessToken: pat, fileKey: resolvedFileKey };
1103
+ }
1080
1104
 
1081
1105
  await updateJobProgress(tenantId, jobId, 5, 'Listing frames from Figma file…');
1082
1106
  const frames = await figma.listScreens(creds, resolvedFileKey);
@@ -1118,7 +1142,9 @@ async function processFigmaImportJob(jobId: string, config: any) {
1118
1142
  console.warn('[figma-import] cloud upload failed:', e);
1119
1143
  }
1120
1144
  }
1121
- await AppBrain.saveScreenshot(tenantId, projectId, screen.id, storagePath, 1, 'DESKTOP', cloudUrl);
1145
+ const w = (frame as any).boundingBox?.width ?? 1440;
1146
+ const viewport = w <= 480 ? 'MOBILE_PORTRAIT' : w <= 1024 ? 'TABLET' : 'DESKTOP';
1147
+ await AppBrain.saveScreenshot(tenantId, projectId, screen.id, storagePath, 1, viewport, cloudUrl);
1122
1148
  }
1123
1149
  }
1124
1150
 
@@ -2502,7 +2528,7 @@ jobDispatchTable = {
2502
2528
  BATCH_GENERATE_ALIGNED: processBatchGenerateAlignedJob,
2503
2529
  CREATE_PR: processCreatePRJob,
2504
2530
  NEW_PAGES_PIPELINE: processNewPagesPipelineJob,
2505
- // GENERATE_TESTS is processed in-process by the API (never QUEUED).
2531
+ // The following job types are processed in-process by the API (never QUEUED).
2506
2532
  // If the worker claims one, the API crashed after creating it — mark failed so it's visible.
2507
2533
  GENERATE_TESTS: async (jobId: string, _config: any) => {
2508
2534
  logger.error({ jobId }, '[job-worker] GENERATE_TESTS claimed by worker — API did not process it in-process');
@@ -2511,6 +2537,27 @@ jobDispatchTable = {
2511
2537
  data: { status: 'FAILED', error: 'Test generation was not completed by the API — please retry from the UI.', finishedAt: new Date() },
2512
2538
  }).catch(() => {});
2513
2539
  },
2540
+ GENERATE_DATA: async (jobId: string, _config: any) => {
2541
+ logger.error({ jobId }, '[job-worker] GENERATE_DATA claimed by worker — API did not process it in-process');
2542
+ await prismaAdmin.platformJob.update({
2543
+ where: { id: jobId },
2544
+ data: { status: 'FAILED', error: 'Data generation was not completed by the API — please retry from the UI.', finishedAt: new Date() },
2545
+ }).catch(() => {});
2546
+ },
2547
+ GENERATE_SCRIPT: async (jobId: string, _config: any) => {
2548
+ logger.error({ jobId }, '[job-worker] GENERATE_SCRIPT claimed by worker — API did not process it in-process');
2549
+ await prismaAdmin.platformJob.update({
2550
+ where: { id: jobId },
2551
+ data: { status: 'FAILED', error: 'Script generation was not completed by the API — please retry from the UI.', finishedAt: new Date() },
2552
+ }).catch(() => {});
2553
+ },
2554
+ GENERATE_TEST_PLAN: async (jobId: string, _config: any) => {
2555
+ logger.error({ jobId }, '[job-worker] GENERATE_TEST_PLAN claimed by worker — API did not process it in-process');
2556
+ await prismaAdmin.platformJob.update({
2557
+ where: { id: jobId },
2558
+ data: { status: 'FAILED', error: 'Test plan generation was not completed by the API — please retry from the UI.', finishedAt: new Date() },
2559
+ }).catch(() => {});
2560
+ },
2514
2561
  };
2515
2562
 
2516
2563
  async function processRepoScanJob(jobId: string, config: any) {