diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 53cb7009..74ecae24 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -239,7 +239,7 @@ jobs: platforms: ${{ matrix.platform }} build-args: | SNAPOTTER_ANALYTICS=on - SNAPOTTER_POSTHOG_KEY=${{ secrets.SNAPOTTER_POSTHOG_KEY }} + SNAPOTTER_POSTHOG_PROJECT_ID=${{ secrets.SNAPOTTER_POSTHOG_KEY }} SNAPOTTER_SENTRY_DSN=${{ secrets.SNAPOTTER_SENTRY_DSN }} SENTRY_RELEASE=${{ needs.release.outputs.new_version }} secrets: | diff --git a/apps/api/src/jobs/cancel.ts b/apps/api/src/jobs/cancel.ts index e96a65ba..c746892f 100644 --- a/apps/api/src/jobs/cancel.ts +++ b/apps/api/src/jobs/cancel.ts @@ -14,7 +14,7 @@ import { eq } from "drizzle-orm"; import type Redis from "ioredis"; import { db, schema } from "../db/index.js"; -import { createRedisConnection, sharedRedis } from "./connection.js"; +import { createRedisSubscriberConnection, sharedRedis } from "./connection.js"; import { getQueue } from "./queues.js"; import { bullPrefix, POOLS } from "./types.js"; @@ -39,7 +39,7 @@ const CANCEL_CHANNEL = () => `${bullPrefix()}:cancel`; let subscriber: Redis | null = null; export async function startCancelListener(): Promise { - subscriber = createRedisConnection(); + subscriber = createRedisSubscriberConnection(); subscriber.on("error", (err) => { console.error("Cancel listener subscriber error", err); }); diff --git a/apps/api/src/jobs/connection.ts b/apps/api/src/jobs/connection.ts index 12597874..263cb240 100644 --- a/apps/api/src/jobs/connection.ts +++ b/apps/api/src/jobs/connection.ts @@ -20,6 +20,18 @@ export function createRedisConnection(): Redis { }); } +/** + * Create a Redis connection used only for pub/sub subscriptions. + * Subscriber sockets cannot run regular commands once subscribed, so disable + * ioredis ready checks that issue INFO during reconnects. + */ +export function createRedisSubscriberConnection(): Redis { + return new Redis(env.REDIS_URL, { + maxRetriesPerRequest: null, + enableReadyCheck: false, + }); +} + // ioredis 5.11 vs BullMQ's bundled 5.10 type mismatch export function createBullMQConnection(): ConnectionOptions { return createRedisConnection() as unknown as ConnectionOptions; diff --git a/apps/api/src/lib/analytics-gate.ts b/apps/api/src/lib/analytics-gate.ts index f4cd2037..cfab5b51 100644 --- a/apps/api/src/lib/analytics-gate.ts +++ b/apps/api/src/lib/analytics-gate.ts @@ -95,8 +95,8 @@ const CHANNEL = async () => { /** Subscribe so a setting change on any replica refreshes this process's cache. */ export async function startAnalyticsGateListener(): Promise { - const { createRedisConnection } = await import("../jobs/connection.js"); - gateSubscriber = createRedisConnection(); + const { createRedisSubscriberConnection } = await import("../jobs/connection.js"); + gateSubscriber = createRedisSubscriberConnection(); gateSubscriber.on("error", (err) => console.error("Analytics gate subscriber error", err)); await gateSubscriber.subscribe(await CHANNEL()); gateSubscriber.on("message", () => { diff --git a/apps/api/src/routes/async-response.ts b/apps/api/src/routes/async-response.ts new file mode 100644 index 00000000..4cff658e --- /dev/null +++ b/apps/api/src/routes/async-response.ts @@ -0,0 +1,24 @@ +export type AsyncAcceptedPayload = { + jobId: string; + async: true; + progressJobId?: string; + artifactJobId?: string; +}; + +export function buildAsyncAcceptedPayload( + artifactJobId: string, + clientJobId?: string | null, +): AsyncAcceptedPayload { + const progressJobId = clientJobId && clientJobId.length > 0 ? clientJobId : artifactJobId; + + if (progressJobId === artifactJobId) { + return { jobId: artifactJobId, async: true }; + } + + return { + jobId: progressJobId, + progressJobId, + artifactJobId, + async: true, + }; +} diff --git a/apps/api/src/routes/progress.ts b/apps/api/src/routes/progress.ts index 04e3e1ae..f049bac8 100644 --- a/apps/api/src/routes/progress.ts +++ b/apps/api/src/routes/progress.ts @@ -13,7 +13,7 @@ import { eq } from "drizzle-orm"; import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; import { db, schema } from "../db/index.js"; -import { createRedisConnection, sharedRedis } from "../jobs/connection.js"; +import { createRedisSubscriberConnection, sharedRedis } from "../jobs/connection.js"; import { bullPrefix } from "../jobs/types.js"; import { getSecurityHeaders } from "../lib/csp.js"; @@ -228,11 +228,11 @@ export function publishEphemeral( type FrameCallback = (json: string) => void; const sseListeners = new Map>(); -let sseSubscriber: ReturnType | null = null; +let sseSubscriber: ReturnType | null = null; function ensureSubscriber(): void { if (sseSubscriber) return; - sseSubscriber = createRedisConnection(); + sseSubscriber = createRedisSubscriberConnection(); // ioredis auto-resubscribes after reconnects; the handler keeps connection // errors observable without crashing (ioredis silentEmits, but be explicit). sseSubscriber.on("error", (err) => { diff --git a/apps/api/src/routes/tool-factory.ts b/apps/api/src/routes/tool-factory.ts index 48862b39..8a95983d 100644 --- a/apps/api/src/routes/tool-factory.ts +++ b/apps/api/src/routes/tool-factory.ts @@ -25,6 +25,7 @@ import { InputValidationError } from "../modality/contract.js"; import { inputHandlerFor } from "../modality/input-handler.js"; import { MediaInputHandler, type MediaInputKind } from "../modality/media-input.js"; import { requireToolAccess } from "../permissions.js"; +import { buildAsyncAcceptedPayload } from "./async-response.js"; import { updateSingleFileProgress } from "./progress.js"; /** Context passed to tool process functions for cooperative cancellation, scratch storage, and progress. */ @@ -553,7 +554,7 @@ export function createToolRoute(app: FastifyInstance, config: ToolRouteConfig // Long tools never block the HTTP request (spec 4.5): straight to SSE. if (shouldSkipSyncWindow(toolMeta?.executionHint)) { - return reply.status(202).send({ jobId: clientJobId || jobId, async: true }); + return reply.status(202).send(buildAsyncAcceptedPayload(jobId, clientJobId)); } try { @@ -590,7 +591,7 @@ export function createToolRoute(app: FastifyInstance, config: ToolRouteConfig ...result.resultPayload, }); } - return reply.status(202).send({ jobId: clientJobId || jobId, async: true }); + return reply.status(202).send(buildAsyncAcceptedPayload(jobId, clientJobId)); } catch (err) { // Keep the full error (incl. raw ffmpeg/tool stderr) in server logs, // but return only a user-safe detail to the client. diff --git a/apps/api/src/routes/tools/ai-canvas-expand.ts b/apps/api/src/routes/tools/ai-canvas-expand.ts index adc578e3..0a5daa7c 100644 --- a/apps/api/src/routes/tools/ai-canvas-expand.ts +++ b/apps/api/src/routes/tools/ai-canvas-expand.ts @@ -20,6 +20,7 @@ import { getObjectBuffer, putObject } from "../../lib/object-storage.js"; import { resolveOutputFormat } from "../../lib/output-format.js"; import { receiveUpload } from "../../lib/upload-stream.js"; import { getAuthUser } from "../../plugins/auth.js"; +import { buildAsyncAcceptedPayload } from "../async-response.js"; import { registerToolProcessFn } from "../tool-factory.js"; const settingsSchema = z.object({ @@ -241,8 +242,6 @@ export function registerAiCanvasExpand(app: FastifyInstance) { await putObject(inputKey, fileBuffer); } - const progressJobId = clientJobId || jobId; - await enqueueToolJob({ jobId, toolId, @@ -256,7 +255,7 @@ export function registerAiCanvasExpand(app: FastifyInstance) { kind: "ai-tool", }); - return reply.status(202).send({ jobId: progressJobId, async: true }); + return reply.status(202).send(buildAsyncAcceptedPayload(jobId, clientJobId)); }, ); diff --git a/apps/api/src/routes/tools/auto-subtitles.ts b/apps/api/src/routes/tools/auto-subtitles.ts index 477141a4..1569a060 100644 --- a/apps/api/src/routes/tools/auto-subtitles.ts +++ b/apps/api/src/routes/tools/auto-subtitles.ts @@ -13,6 +13,7 @@ import { isToolInstalled } from "../../lib/feature-status.js"; import { type TranscriptSegment, toSrt, toVtt } from "../../lib/subtitle-format.js"; import { receiveUpload } from "../../lib/upload-stream.js"; import { getAuthUser } from "../../plugins/auth.js"; +import { buildAsyncAcceptedPayload } from "../async-response.js"; const settingsSchema = z.object({ language: z @@ -157,8 +158,6 @@ export function registerAutoSubtitles(app: FastifyInstance) { return reply.status(400).send({ error: "Settings must be valid JSON" }); } - const progressJobId = clientJobId || jobId; - await enqueueToolJob({ jobId, toolId, @@ -172,7 +171,7 @@ export function registerAutoSubtitles(app: FastifyInstance) { kind: "ai-tool", }); - return reply.status(202).send({ jobId: progressJobId, async: true }); + return reply.status(202).send(buildAsyncAcceptedPayload(jobId, clientJobId)); }, ); } diff --git a/apps/api/src/routes/tools/background-replace.ts b/apps/api/src/routes/tools/background-replace.ts index 8bd6c60b..2da5a6d4 100644 --- a/apps/api/src/routes/tools/background-replace.ts +++ b/apps/api/src/routes/tools/background-replace.ts @@ -15,6 +15,7 @@ import { decodeToSharpCompat, needsCliDecode } from "../../lib/format-decoders.j import { decodeHeic } from "../../lib/heic-converter.js"; import { receiveUpload } from "../../lib/upload-stream.js"; import { getAuthUser } from "../../plugins/auth.js"; +import { buildAsyncAcceptedPayload } from "../async-response.js"; const HEX_RE = /^#[0-9a-fA-F]{6}$/; @@ -216,8 +217,6 @@ export function registerBackgroundReplace(app: FastifyInstance) { await putObject(inputKey, fileBuffer); } - const progressJobId = clientJobId || jobId; - await enqueueToolJob({ jobId, toolId, @@ -231,7 +230,7 @@ export function registerBackgroundReplace(app: FastifyInstance) { kind: "ai-tool", }); - return reply.status(202).send({ jobId: progressJobId, async: true }); + return reply.status(202).send(buildAsyncAcceptedPayload(jobId, clientJobId)); }, ); } diff --git a/apps/api/src/routes/tools/blur-background.ts b/apps/api/src/routes/tools/blur-background.ts index e63862d1..148d43f5 100644 --- a/apps/api/src/routes/tools/blur-background.ts +++ b/apps/api/src/routes/tools/blur-background.ts @@ -15,6 +15,7 @@ import { decodeToSharpCompat, needsCliDecode } from "../../lib/format-decoders.j import { decodeHeic } from "../../lib/heic-converter.js"; import { receiveUpload } from "../../lib/upload-stream.js"; import { getAuthUser } from "../../plugins/auth.js"; +import { buildAsyncAcceptedPayload } from "../async-response.js"; const settingsSchema = z.object({ intensity: z.number().int().min(1).max(100).default(50), @@ -186,8 +187,6 @@ export function registerBlurBackground(app: FastifyInstance) { await putObject(inputKey, fileBuffer); } - const progressJobId = clientJobId || jobId; - await enqueueToolJob({ jobId, toolId, @@ -201,7 +200,7 @@ export function registerBlurBackground(app: FastifyInstance) { kind: "ai-tool", }); - return reply.status(202).send({ jobId: progressJobId, async: true }); + return reply.status(202).send(buildAsyncAcceptedPayload(jobId, clientJobId)); }, ); } diff --git a/apps/api/src/routes/tools/blur-faces.ts b/apps/api/src/routes/tools/blur-faces.ts index 358c7d31..256be892 100644 --- a/apps/api/src/routes/tools/blur-faces.ts +++ b/apps/api/src/routes/tools/blur-faces.ts @@ -19,6 +19,7 @@ import { getObjectBuffer, putObject } from "../../lib/object-storage.js"; import { resolveOutputFormat } from "../../lib/output-format.js"; import { receiveUpload } from "../../lib/upload-stream.js"; import { getAuthUser } from "../../plugins/auth.js"; +import { buildAsyncAcceptedPayload } from "../async-response.js"; import { registerToolProcessFn } from "../tool-factory.js"; const settingsSchema = z.object({ @@ -163,8 +164,6 @@ export function registerBlurFaces(app: FastifyInstance) { await putObject(inputKey, fileBuffer); } - const progressJobId = clientJobId || jobId; - await enqueueToolJob({ jobId, toolId, @@ -178,7 +177,7 @@ export function registerBlurFaces(app: FastifyInstance) { kind: "ai-tool", }); - return reply.status(202).send({ jobId: progressJobId, async: true }); + return reply.status(202).send(buildAsyncAcceptedPayload(jobId, clientJobId)); }, ); diff --git a/apps/api/src/routes/tools/colorize.ts b/apps/api/src/routes/tools/colorize.ts index f27d0fe9..e585c553 100644 --- a/apps/api/src/routes/tools/colorize.ts +++ b/apps/api/src/routes/tools/colorize.ts @@ -19,6 +19,7 @@ import { getObjectBuffer, putObject } from "../../lib/object-storage.js"; import { resolveOutputFormat } from "../../lib/output-format.js"; import { receiveUpload } from "../../lib/upload-stream.js"; import { getAuthUser } from "../../plugins/auth.js"; +import { buildAsyncAcceptedPayload } from "../async-response.js"; import { registerToolProcessFn } from "../tool-factory.js"; const settingsSchema = z.object({ @@ -162,8 +163,6 @@ export function registerColorize(app: FastifyInstance) { await putObject(inputKey, fileBuffer); } - const progressJobId = clientJobId || jobId; - await enqueueToolJob({ jobId, toolId, @@ -177,7 +176,7 @@ export function registerColorize(app: FastifyInstance) { kind: "ai-tool", }); - return reply.status(202).send({ jobId: progressJobId, async: true }); + return reply.status(202).send(buildAsyncAcceptedPayload(jobId, clientJobId)); }); // Register in the pipeline/batch registry diff --git a/apps/api/src/routes/tools/enhance-faces.ts b/apps/api/src/routes/tools/enhance-faces.ts index 62710847..91afb0aa 100644 --- a/apps/api/src/routes/tools/enhance-faces.ts +++ b/apps/api/src/routes/tools/enhance-faces.ts @@ -17,6 +17,7 @@ import { decodeHeic } from "../../lib/heic-converter.js"; import { getObjectBuffer, putObject } from "../../lib/object-storage.js"; import { receiveUpload } from "../../lib/upload-stream.js"; import { getAuthUser } from "../../plugins/auth.js"; +import { buildAsyncAcceptedPayload } from "../async-response.js"; import { registerToolProcessFn } from "../tool-factory.js"; const settingsSchema = z.object({ @@ -156,8 +157,6 @@ export function registerEnhanceFaces(app: FastifyInstance) { await putObject(inputKey, fileBuffer); } - const progressJobId = clientJobId || jobId; - await enqueueToolJob({ jobId, toolId, @@ -171,7 +170,7 @@ export function registerEnhanceFaces(app: FastifyInstance) { kind: "ai-tool", }); - return reply.status(202).send({ jobId: progressJobId, async: true }); + return reply.status(202).send(buildAsyncAcceptedPayload(jobId, clientJobId)); }, ); diff --git a/apps/api/src/routes/tools/erase-object.ts b/apps/api/src/routes/tools/erase-object.ts index f8130e7a..6d4bd214 100644 --- a/apps/api/src/routes/tools/erase-object.ts +++ b/apps/api/src/routes/tools/erase-object.ts @@ -16,6 +16,7 @@ import { getObjectBuffer, putObject } from "../../lib/object-storage.js"; import { resolveOutputFormat } from "../../lib/output-format.js"; import { receiveUpload } from "../../lib/upload-stream.js"; import { getAuthUser } from "../../plugins/auth.js"; +import { buildAsyncAcceptedPayload } from "../async-response.js"; const settingsSchema = z.object({ format: z @@ -157,8 +158,6 @@ export function registerEraseObject(app: FastifyInstance) { await putObject(imageKey, imageBuffer); } - const progressJobId = clientJobId || jobId; - // Enqueue with both image and mask as inputRefs; the worker handler // reads them via getObjectBuffer. await enqueueToolJob({ @@ -174,7 +173,7 @@ export function registerEraseObject(app: FastifyInstance) { kind: "ai-tool", }); - return reply.status(202).send({ jobId: progressJobId, async: true }); + return reply.status(202).send(buildAsyncAcceptedPayload(jobId, clientJobId)); }, ); } diff --git a/apps/api/src/routes/tools/noise-removal.ts b/apps/api/src/routes/tools/noise-removal.ts index 35ed3ea5..7359bebe 100644 --- a/apps/api/src/routes/tools/noise-removal.ts +++ b/apps/api/src/routes/tools/noise-removal.ts @@ -17,6 +17,7 @@ import { decodeHeic } from "../../lib/heic-converter.js"; import { getObjectBuffer, putObject } from "../../lib/object-storage.js"; import { receiveUpload } from "../../lib/upload-stream.js"; import { getAuthUser } from "../../plugins/auth.js"; +import { buildAsyncAcceptedPayload } from "../async-response.js"; import { registerToolProcessFn } from "../tool-factory.js"; const settingsSchema = z.object({ @@ -167,8 +168,6 @@ export function registerNoiseRemoval(app: FastifyInstance) { await putObject(inputKey, fileBuffer); } - const progressJobId = clientJobId || jobId; - await enqueueToolJob({ jobId, toolId, @@ -182,7 +181,7 @@ export function registerNoiseRemoval(app: FastifyInstance) { kind: "ai-tool", }); - return reply.status(202).send({ jobId: progressJobId, async: true }); + return reply.status(202).send(buildAsyncAcceptedPayload(jobId, clientJobId)); }, ); diff --git a/apps/api/src/routes/tools/ocr-pdf.ts b/apps/api/src/routes/tools/ocr-pdf.ts index e538b254..54a038b2 100644 --- a/apps/api/src/routes/tools/ocr-pdf.ts +++ b/apps/api/src/routes/tools/ocr-pdf.ts @@ -11,6 +11,7 @@ import { formatZodErrors, stripInternalPaths } from "../../lib/errors.js"; import { isToolInstalled } from "../../lib/feature-status.js"; import { receiveUpload } from "../../lib/upload-stream.js"; import { getAuthUser } from "../../plugins/auth.js"; +import { buildAsyncAcceptedPayload } from "../async-response.js"; const settingsSchema = z.object({ quality: z.enum(["fast", "balanced", "best"]).default("balanced"), @@ -122,8 +123,6 @@ export function registerOcrPdf(app: FastifyInstance) { return reply.status(400).send({ error: "Settings must be valid JSON" }); } - const progressJobId = clientJobId || jobId; - await enqueueToolJob({ jobId, toolId, @@ -137,6 +136,6 @@ export function registerOcrPdf(app: FastifyInstance) { kind: "ai-tool", }); - return reply.status(202).send({ jobId: progressJobId, async: true }); + return reply.status(202).send(buildAsyncAcceptedPayload(jobId, clientJobId)); }); } diff --git a/apps/api/src/routes/tools/red-eye-removal.ts b/apps/api/src/routes/tools/red-eye-removal.ts index ea31f7d4..d53eef4e 100644 --- a/apps/api/src/routes/tools/red-eye-removal.ts +++ b/apps/api/src/routes/tools/red-eye-removal.ts @@ -17,6 +17,7 @@ import { decodeHeic } from "../../lib/heic-converter.js"; import { getObjectBuffer, putObject } from "../../lib/object-storage.js"; import { receiveUpload } from "../../lib/upload-stream.js"; import { getAuthUser } from "../../plugins/auth.js"; +import { buildAsyncAcceptedPayload } from "../async-response.js"; import { registerToolProcessFn } from "../tool-factory.js"; const settingsSchema = z.object({ @@ -155,8 +156,6 @@ export function registerRedEyeRemoval(app: FastifyInstance) { await putObject(inputKey, fileBuffer); } - const progressJobId = clientJobId || jobId; - await enqueueToolJob({ jobId, toolId, @@ -170,7 +169,7 @@ export function registerRedEyeRemoval(app: FastifyInstance) { kind: "ai-tool", }); - return reply.status(202).send({ jobId: progressJobId, async: true }); + return reply.status(202).send(buildAsyncAcceptedPayload(jobId, clientJobId)); }, ); diff --git a/apps/api/src/routes/tools/remove-background.ts b/apps/api/src/routes/tools/remove-background.ts index 337dcf7d..97ddd1b8 100644 --- a/apps/api/src/routes/tools/remove-background.ts +++ b/apps/api/src/routes/tools/remove-background.ts @@ -23,6 +23,7 @@ import { decodeHeic } from "../../lib/heic-converter.js"; import { getObjectBuffer, putObject } from "../../lib/object-storage.js"; import { receiveUpload } from "../../lib/upload-stream.js"; import { getAuthUser } from "../../plugins/auth.js"; +import { buildAsyncAcceptedPayload } from "../async-response.js"; import { registerToolProcessFn } from "../tool-factory.js"; const settingsSchema = z.object({ @@ -206,8 +207,6 @@ export function registerRemoveBackground(app: FastifyInstance) { await putObject(inputKey, fileBuffer); } - const progressJobId = clientJobId || jobId; - // Enqueue on the AI pool await enqueueToolJob({ jobId, @@ -223,7 +222,7 @@ export function registerRemoveBackground(app: FastifyInstance) { }); // AI tools always return 202 (no sync window) - return reply.status(202).send({ jobId: progressJobId, async: true }); + return reply.status(202).send(buildAsyncAcceptedPayload(jobId, clientJobId)); }, ); diff --git a/apps/api/src/routes/tools/restore-photo.ts b/apps/api/src/routes/tools/restore-photo.ts index 13ff86d4..4b1eebf0 100644 --- a/apps/api/src/routes/tools/restore-photo.ts +++ b/apps/api/src/routes/tools/restore-photo.ts @@ -19,6 +19,7 @@ import { getObjectBuffer, putObject } from "../../lib/object-storage.js"; import { resolveOutputFormat } from "../../lib/output-format.js"; import { receiveUpload } from "../../lib/upload-stream.js"; import { getAuthUser } from "../../plugins/auth.js"; +import { buildAsyncAcceptedPayload } from "../async-response.js"; import { registerToolProcessFn } from "../tool-factory.js"; const settingsSchema = z.object({ @@ -187,8 +188,6 @@ export function registerRestorePhoto(app: FastifyInstance) { await putObject(inputKey, fileBuffer); } - const progressJobId = clientJobId || jobId; - await enqueueToolJob({ jobId, toolId: "restore-photo", @@ -202,7 +201,7 @@ export function registerRestorePhoto(app: FastifyInstance) { kind: "ai-tool", }); - return reply.status(202).send({ jobId: progressJobId, async: true }); + return reply.status(202).send(buildAsyncAcceptedPayload(jobId, clientJobId)); }, ); diff --git a/apps/api/src/routes/tools/sign-pdf.ts b/apps/api/src/routes/tools/sign-pdf.ts index 9d12e86d..d3d0d5c8 100644 --- a/apps/api/src/routes/tools/sign-pdf.ts +++ b/apps/api/src/routes/tools/sign-pdf.ts @@ -14,6 +14,7 @@ import { getObjectBuffer } from "../../lib/object-storage.js"; import { receiveUpload } from "../../lib/upload-stream.js"; import { inputHandlerFor } from "../../modality/input-handler.js"; import { getAuthUser } from "../../plugins/auth.js"; +import { buildAsyncAcceptedPayload } from "../async-response.js"; const TOOL_ID = "sign-pdf"; const MAX_PLACEMENTS = 100; @@ -158,7 +159,7 @@ export function registerSignPdf(app: FastifyInstance) { savedFileId: result.savedFileId, }); } - return reply.status(202).send({ jobId: clientJobId || jobId, async: true }); + return reply.status(202).send(buildAsyncAcceptedPayload(jobId, clientJobId)); } catch (err) { request.log.error({ err, toolId: TOOL_ID }, "sign-pdf processing failed"); return reply.status(422).send({ diff --git a/apps/api/src/routes/tools/transcribe-audio.ts b/apps/api/src/routes/tools/transcribe-audio.ts index 18163911..492ca48b 100644 --- a/apps/api/src/routes/tools/transcribe-audio.ts +++ b/apps/api/src/routes/tools/transcribe-audio.ts @@ -12,6 +12,7 @@ import { isToolInstalled } from "../../lib/feature-status.js"; import { type TranscriptSegment, toSrt, toVtt } from "../../lib/subtitle-format.js"; import { receiveUpload } from "../../lib/upload-stream.js"; import { getAuthUser } from "../../plugins/auth.js"; +import { buildAsyncAcceptedPayload } from "../async-response.js"; const settingsSchema = z.object({ language: z @@ -145,8 +146,6 @@ export function registerTranscribeAudio(app: FastifyInstance) { return reply.status(400).send({ error: "Settings must be valid JSON" }); } - const progressJobId = clientJobId || jobId; - await enqueueToolJob({ jobId, toolId, @@ -160,7 +159,7 @@ export function registerTranscribeAudio(app: FastifyInstance) { kind: "ai-tool", }); - return reply.status(202).send({ jobId: progressJobId, async: true }); + return reply.status(202).send(buildAsyncAcceptedPayload(jobId, clientJobId)); }, ); } diff --git a/apps/api/src/routes/tools/transparency-fixer.ts b/apps/api/src/routes/tools/transparency-fixer.ts index ba289742..7654d784 100644 --- a/apps/api/src/routes/tools/transparency-fixer.ts +++ b/apps/api/src/routes/tools/transparency-fixer.ts @@ -18,6 +18,7 @@ import { decodeHeic } from "../../lib/heic-converter.js"; import { getObjectBuffer, putObject } from "../../lib/object-storage.js"; import { receiveUpload } from "../../lib/upload-stream.js"; import { getAuthUser } from "../../plugins/auth.js"; +import { buildAsyncAcceptedPayload } from "../async-response.js"; import { registerToolProcessFn } from "../tool-factory.js"; const TOOL_ID = "transparency-fixer"; @@ -252,8 +253,6 @@ export function registerTransparencyFixer(app: FastifyInstance) { await putObject(inputKey, fileBuffer); } - const progressJobId = clientJobId || jobId; - await enqueueToolJob({ jobId, toolId: TOOL_ID, @@ -267,7 +266,7 @@ export function registerTransparencyFixer(app: FastifyInstance) { kind: "ai-tool", }); - return reply.status(202).send({ jobId: progressJobId, async: true }); + return reply.status(202).send(buildAsyncAcceptedPayload(jobId, clientJobId)); }, ); diff --git a/apps/api/src/routes/tools/upscale.ts b/apps/api/src/routes/tools/upscale.ts index e47ecfc7..54383116 100644 --- a/apps/api/src/routes/tools/upscale.ts +++ b/apps/api/src/routes/tools/upscale.ts @@ -20,6 +20,7 @@ import { getObjectBuffer, putObject } from "../../lib/object-storage.js"; import { resolveOutputFormat } from "../../lib/output-format.js"; import { receiveUpload } from "../../lib/upload-stream.js"; import { getAuthUser } from "../../plugins/auth.js"; +import { buildAsyncAcceptedPayload } from "../async-response.js"; import { registerToolProcessFn } from "../tool-factory.js"; const settingsSchema = z.object({ @@ -214,8 +215,6 @@ export function registerUpscale(app: FastifyInstance) { await putObject(inputKey, fileBuffer); } - const progressJobId = clientJobId || jobId; - await enqueueToolJob({ jobId, toolId, @@ -229,7 +228,7 @@ export function registerUpscale(app: FastifyInstance) { kind: "ai-tool", }); - return reply.status(202).send({ jobId: progressJobId, async: true }); + return reply.status(202).send(buildAsyncAcceptedPayload(jobId, clientJobId)); }); // Register in the pipeline/batch registry so this tool can be used diff --git a/apps/landing/src/components/CategoryCards.astro b/apps/landing/src/components/CategoryCards.astro index ade50c68..6b0f1ad2 100644 --- a/apps/landing/src/components/CategoryCards.astro +++ b/apps/landing/src/components/CategoryCards.astro @@ -1,4 +1,5 @@ --- +// biome-ignore-all lint/correctness/noUnusedVariables: Astro template consumes frontmatter values. import { TOOLS, toolSection } from "@snapotter/shared"; import * as lucideIcons from "lucide"; diff --git a/apps/landing/src/components/EnterpriseSection.astro b/apps/landing/src/components/EnterpriseSection.astro index c2154a13..18ad134c 100644 --- a/apps/landing/src/components/EnterpriseSection.astro +++ b/apps/landing/src/components/EnterpriseSection.astro @@ -1,4 +1,5 @@ --- +// biome-ignore-all lint/correctness/noUnusedVariables: Astro template consumes frontmatter values. const features = [ { tag: "Identity", diff --git a/apps/landing/src/components/FeatureHighlights.astro b/apps/landing/src/components/FeatureHighlights.astro index 60290f95..0ed9cc5d 100644 --- a/apps/landing/src/components/FeatureHighlights.astro +++ b/apps/landing/src/components/FeatureHighlights.astro @@ -1,4 +1,5 @@ --- +// biome-ignore-all lint/correctness/noUnusedVariables: Astro template consumes frontmatter values. const aiTools = [ "Remove Background", "Image Upscaling", diff --git a/apps/landing/src/components/Footer.astro b/apps/landing/src/components/Footer.astro index 8f9ec729..030e5563 100644 --- a/apps/landing/src/components/Footer.astro +++ b/apps/landing/src/components/Footer.astro @@ -1,4 +1,5 @@ --- +// biome-ignore-all lint/correctness/noUnusedVariables: Astro template consumes frontmatter values. const year = new Date().getFullYear(); const columns = [ diff --git a/apps/landing/src/components/Hero.astro b/apps/landing/src/components/Hero.astro index 55c12e49..db824f25 100644 --- a/apps/landing/src/components/Hero.astro +++ b/apps/landing/src/components/Hero.astro @@ -1,4 +1,6 @@ --- +// biome-ignore-all lint/correctness/noUnusedImports: Astro template consumes component imports. +// biome-ignore-all lint/correctness/noUnusedVariables: Astro template consumes frontmatter values. import CategoryCards from "./CategoryCards.astro"; import HeroSearch from "./HeroSearch.astro"; import TrustSignals from "./TrustSignals.astro"; diff --git a/apps/landing/src/components/HeroSearch.astro b/apps/landing/src/components/HeroSearch.astro index 006b33b2..39ee7d71 100644 --- a/apps/landing/src/components/HeroSearch.astro +++ b/apps/landing/src/components/HeroSearch.astro @@ -1,4 +1,6 @@ --- +// biome-ignore-all lint/correctness/noUnusedImports: Astro template consumes component imports. +// biome-ignore-all lint/correctness/noUnusedVariables: Astro template consumes frontmatter values. import { TOOLS, toolSection } from "@snapotter/shared"; import * as lucideIcons from "lucide"; diff --git a/apps/landing/src/components/JsonLd.astro b/apps/landing/src/components/JsonLd.astro index 23f0cf0c..b6443b4e 100644 --- a/apps/landing/src/components/JsonLd.astro +++ b/apps/landing/src/components/JsonLd.astro @@ -1,4 +1,5 @@ --- +// biome-ignore-all lint/correctness/noUnusedVariables: Astro template consumes frontmatter values. interface Props { data: Record | Record[]; } @@ -8,5 +9,5 @@ const schemas = Array.isArray(data) ? data : [data]; --- {schemas.map((schema) => ( -