diff --git a/apps/api/src/jobs/cancel.ts b/apps/api/src/jobs/cancel.ts index 05b8c60c..e96a65ba 100644 --- a/apps/api/src/jobs/cancel.ts +++ b/apps/api/src/jobs/cancel.ts @@ -16,7 +16,7 @@ import type Redis from "ioredis"; import { db, schema } from "../db/index.js"; import { createRedisConnection, sharedRedis } from "./connection.js"; import { getQueue } from "./queues.js"; -import { bullPrefix, POOLS, type Pool } from "./types.js"; +import { bullPrefix, POOLS } from "./types.js"; // ── Per-worker cancel registry ────────────────────────────────── diff --git a/apps/api/src/jobs/connection.ts b/apps/api/src/jobs/connection.ts index fd38e38e..12597874 100644 --- a/apps/api/src/jobs/connection.ts +++ b/apps/api/src/jobs/connection.ts @@ -4,6 +4,7 @@ * Uses ioredis with settings compatible with BullMQ's requirements * (maxRetriesPerRequest: null for blocking commands). */ +import type { ConnectionOptions } from "bullmq"; import Redis from "ioredis"; import { env } from "../config.js"; @@ -19,6 +20,11 @@ export function createRedisConnection(): Redis { }); } +// ioredis 5.11 vs BullMQ's bundled 5.10 type mismatch +export function createBullMQConnection(): ConnectionOptions { + return createRedisConnection() as unknown as ConnectionOptions; +} + let _shared: Redis | null = null; /** diff --git a/apps/api/src/jobs/enqueue.ts b/apps/api/src/jobs/enqueue.ts index 7d3261fe..3e962a10 100644 --- a/apps/api/src/jobs/enqueue.ts +++ b/apps/api/src/jobs/enqueue.ts @@ -10,9 +10,9 @@ import { FlowProducer, type Job, QueueEvents } from "bullmq"; import { eq } from "drizzle-orm"; import { env } from "../config.js"; import { db, schema } from "../db/index.js"; -import { createRedisConnection } from "./connection.js"; +import { createBullMQConnection } from "./connection.js"; import { getQueue } from "./queues.js"; -import { POOLS, type Pool, queueName, type ToolJobData, type ToolJobResult } from "./types.js"; +import { type Pool, queueName, type ToolJobData, type ToolJobResult } from "./types.js"; // ── QueueEvents (one per pool, lazy) ──────────────────────────── @@ -22,7 +22,7 @@ function getQueueEvents(pool: Pool): QueueEvents { let qe = queueEventsMap.get(pool); if (!qe) { qe = new QueueEvents(queueName(pool), { - connection: createRedisConnection(), + connection: createBullMQConnection(), }); queueEventsMap.set(pool, qe); } @@ -42,7 +42,7 @@ let _flowProducer: FlowProducer | null = null; export function getFlowProducer(): FlowProducer { if (!_flowProducer) { _flowProducer = new FlowProducer({ - connection: createRedisConnection(), + connection: createBullMQConnection(), }); } return _flowProducer; diff --git a/apps/api/src/jobs/queues.ts b/apps/api/src/jobs/queues.ts index 4a0d8f10..ca8a73bd 100644 --- a/apps/api/src/jobs/queues.ts +++ b/apps/api/src/jobs/queues.ts @@ -5,7 +5,7 @@ * default job options (retry policy, TTL-based cleanup). */ import { Queue } from "bullmq"; -import { createRedisConnection } from "./connection.js"; +import { createBullMQConnection } from "./connection.js"; import { POOLS, type Pool, queueName, type ToolJobData, type ToolJobResult } from "./types.js"; const queues = new Map>(); @@ -15,7 +15,7 @@ export function getQueue(pool: Pool): Queue { let q = queues.get(pool); if (!q) { q = new Queue(queueName(pool), { - connection: createRedisConnection(), + connection: createBullMQConnection(), defaultJobOptions: { attempts: pool === "ai" ? 1 : 2, backoff: { type: "exponential", delay: 1000 }, diff --git a/apps/api/src/jobs/siem-forward.ts b/apps/api/src/jobs/siem-forward.ts index 44940e1a..841c1769 100644 --- a/apps/api/src/jobs/siem-forward.ts +++ b/apps/api/src/jobs/siem-forward.ts @@ -35,10 +35,10 @@ async function readSettingValue(key: string): Promise { return row?.value ?? null; } -export async function runSiemForward(): Promise<{ forwarded: number } | void> { +export async function runSiemForward(): Promise<{ forwarded: number } | undefined> { // 1. Read SIEM config const config = await readSiemConfig(); - if (!config || !config.enabled || !config.webhookUrl) { + if (!config?.enabled || !config.webhookUrl) { return; } diff --git a/apps/api/src/jobs/worker.ts b/apps/api/src/jobs/worker.ts index e931fc66..ae0a02d2 100644 --- a/apps/api/src/jobs/worker.ts +++ b/apps/api/src/jobs/worker.ts @@ -43,8 +43,8 @@ import { import { hasAiJobHandler, runAiToolJob } from "./ai-handlers.js"; import { recordChildOutcome } from "./batch-progress.js"; import { registerCancelable, unregisterCancelable } from "./cancel.js"; -import { createRedisConnection } from "./connection.js"; -import { autoSaveToLibrary, buildOutputName, generatePreview } from "./postprocess.js"; +import { createBullMQConnection } from "./connection.js"; +import { buildOutputName, generatePreview } from "./postprocess.js"; import { runSystemJob } from "./system-jobs.js"; import { POOLS, type Pool, queueName, type ToolJobData, type ToolJobResult } from "./types.js"; @@ -578,7 +578,7 @@ async function processPipelineFinalize(job: Job): Promise/ so // the legacy download URL /api/v1/download//... works. const lastOutputBuffer = await getObjectBuffer(lastOutputRef); - const outFilename = lastOutputRef.split("/").pop()!; + const outFilename = lastOutputRef.split("/").pop() ?? "output"; const parentKey = `outputs/${data.jobId}/${outFilename}`; await putObject(parentKey, lastOutputBuffer); @@ -636,13 +636,15 @@ async function processPipelineFinalize(job: Job): Promise): Promise { + const parentId = job.data.parentId ?? ""; + const totalFiles = job.data.totalFiles ?? 0; try { const result = await processToolJob(job); - await recordChildOutcome(job.data.parentId!, job.data.totalFiles!, job.data.filename); + await recordChildOutcome(parentId, totalFiles, job.data.filename); return result; } catch (err) { const error = stripInternalPaths(err instanceof Error ? err.message : String(err)); - await recordChildOutcome(job.data.parentId!, job.data.totalFiles!, job.data.filename, error); + await recordChildOutcome(parentId, totalFiles, job.data.filename, error); // Return a completed job with a failure marker so the parent runs. return { outputRefs: [], @@ -687,7 +689,7 @@ async function processBatchFinalize(job: Job): Promise(queueName(pool), systemProcessor, { - connection: createRedisConnection(), + connection: createBullMQConnection(), concurrency: workerConcurrency, stalledInterval: 30_000, }); @@ -754,7 +756,7 @@ export function startWorkers(): void { }; const worker = new Worker(queueName(pool), processor, { - connection: createRedisConnection(), + connection: createBullMQConnection(), concurrency: workerConcurrency, stalledInterval: 30_000, }); diff --git a/apps/api/src/lib/output-format.ts b/apps/api/src/lib/output-format.ts index 64e55361..99286f37 100644 --- a/apps/api/src/lib/output-format.ts +++ b/apps/api/src/lib/output-format.ts @@ -1,25 +1,25 @@ import sharp from "sharp"; +type SharpFormat = keyof sharp.FormatEnum | "avif"; + export interface OutputFormat { - format: keyof sharp.FormatEnum; + format: SharpFormat; extension: string; contentType: string; quality: number; } -const FORMAT_MAP: Record< - string, - { format: keyof sharp.FormatEnum; extension: string; contentType: string } -> = { - jpeg: { format: "jpeg", extension: "jpg", contentType: "image/jpeg" }, - png: { format: "png", extension: "png", contentType: "image/png" }, - webp: { format: "webp", extension: "webp", contentType: "image/webp" }, - gif: { format: "gif", extension: "gif", contentType: "image/gif" }, - tiff: { format: "tiff", extension: "tiff", contentType: "image/tiff" }, - avif: { format: "avif", extension: "avif", contentType: "image/avif" }, - heif: { format: "avif", extension: "avif", contentType: "image/avif" }, - jxl: { format: "jxl" as keyof sharp.FormatEnum, extension: "jxl", contentType: "image/jxl" }, -}; +const FORMAT_MAP: Record = + { + jpeg: { format: "jpeg", extension: "jpg", contentType: "image/jpeg" }, + png: { format: "png", extension: "png", contentType: "image/png" }, + webp: { format: "webp", extension: "webp", contentType: "image/webp" }, + gif: { format: "gif", extension: "gif", contentType: "image/gif" }, + tiff: { format: "tiff", extension: "tiff", contentType: "image/tiff" }, + avif: { format: "avif", extension: "avif", contentType: "image/avif" }, + heif: { format: "avif", extension: "avif", contentType: "image/avif" }, + jxl: { format: "jxl", extension: "jxl", contentType: "image/jxl" }, + }; const DEFAULT_QUALITY = 95; const PNG_FALLBACK = FORMAT_MAP.png; diff --git a/apps/api/src/lib/webhook-delivery.ts b/apps/api/src/lib/webhook-delivery.ts index 8e850825..534819da 100644 --- a/apps/api/src/lib/webhook-delivery.ts +++ b/apps/api/src/lib/webhook-delivery.ts @@ -26,7 +26,7 @@ export async function deliverWebhook( }); const headers: Record = { "Content-Type": "application/json" }; - if (authHeader) headers["Authorization"] = authHeader; + if (authHeader) headers.Authorization = authHeader; let lastError: string | undefined; for (let attempt = 0; attempt <= maxRetries; attempt++) { diff --git a/apps/api/src/routes/tools/compose.ts b/apps/api/src/routes/tools/compose.ts index 7e346812..786c910c 100644 --- a/apps/api/src/routes/tools/compose.ts +++ b/apps/api/src/routes/tools/compose.ts @@ -142,7 +142,7 @@ export function registerCompose(app: FastifyInstance) { input: processedOverlay, top: settings.y, left: settings.x, - blend: settings.blendMode as import("sharp").Blend, + blend: settings.blendMode as sharp.Blend, }, ]) .toBuffer(); diff --git a/apps/api/src/routes/tools/convert.ts b/apps/api/src/routes/tools/convert.ts index 3a5f2152..a133d852 100644 --- a/apps/api/src/routes/tools/convert.ts +++ b/apps/api/src/routes/tools/convert.ts @@ -120,9 +120,7 @@ export function registerConvert(app: FastifyInstance) { } const inputExt = extname(filename).toLowerCase().replace(".", ""); - const sharpOpts: import("sharp").SharpOptions = isSvgBuffer(inputBuffer) - ? { density: 300 } - : {}; + const sharpOpts: sharp.SharpOptions = isSvgBuffer(inputBuffer) ? { density: 300 } : {}; // Preserve animation frames when both input and output are animatable formats if (ANIMATABLE_FORMATS.has(inputExt) && ANIMATABLE_FORMATS.has(settings.format)) { sharpOpts.animated = true; diff --git a/apps/api/src/routes/tools/split.ts b/apps/api/src/routes/tools/split.ts index 5681b0e2..98eadb1f 100644 --- a/apps/api/src/routes/tools/split.ts +++ b/apps/api/src/routes/tools/split.ts @@ -24,14 +24,16 @@ const settingsSchema = z.object({ quality: z.number().min(1).max(100).default(90), }); +type SharpFormat = keyof sharp.FormatEnum | "avif"; + function resolveOutputFormat( outputFormat: string, originalExt: string, -): { sharpFormat: keyof sharp.FormatEnum | null; ext: string } { +): { sharpFormat: SharpFormat | null; ext: string } { if (outputFormat === "original") { return { sharpFormat: null, ext: originalExt }; } - const map: Record = { + const map: Record = { png: { sharpFormat: "png", ext: ".png" }, jpg: { sharpFormat: "jpeg", ext: ".jpg" }, webp: { sharpFormat: "webp", ext: ".webp" }, diff --git a/apps/api/src/routes/tools/strip-metadata.ts b/apps/api/src/routes/tools/strip-metadata.ts index 89edac43..73ca2846 100644 --- a/apps/api/src/routes/tools/strip-metadata.ts +++ b/apps/api/src/routes/tools/strip-metadata.ts @@ -209,7 +209,7 @@ export function registerStripMetadata(app: FastifyInstance) { case "webp": result.webp({ quality: 85 }); break; - case "avif": + case "heif": result.avif({ quality: 50 }); break; case "tiff": diff --git a/apps/web/src/components/layout/app-layout.tsx b/apps/web/src/components/layout/app-layout.tsx index fee64429..464df068 100644 --- a/apps/web/src/components/layout/app-layout.tsx +++ b/apps/web/src/components/layout/app-layout.tsx @@ -37,10 +37,7 @@ export function AppLayout({ children, breadcrumb, navVariant }: AppLayoutProps) /> {/* Main content area */} -
+
{children}
diff --git a/apps/web/src/hooks/use-recent-tools.ts b/apps/web/src/hooks/use-recent-tools.ts index b4159007..1f684fc7 100644 --- a/apps/web/src/hooks/use-recent-tools.ts +++ b/apps/web/src/hooks/use-recent-tools.ts @@ -37,7 +37,7 @@ export function recordRecentTool(toolId: string) { const updated = [toolId, ...current.filter((id) => id !== toolId)].slice(0, MAX_RECENT); localStorage.setItem(STORAGE_KEY, JSON.stringify(updated)); cachedRaw = null; - listeners.forEach((l) => l()); + for (const l of listeners) l(); } export function useRecentTools(): string[] { diff --git a/packages/image-engine/src/engine.ts b/packages/image-engine/src/engine.ts index 99a850dc..976a1699 100644 --- a/packages/image-engine/src/engine.ts +++ b/packages/image-engine/src/engine.ts @@ -109,7 +109,7 @@ export async function processImage( if (!sharpFormat) { throw new Error(`Unsupported output format: ${outputFormat}`); } - image = image.toFormat(sharpFormat as keyof import("sharp").FormatEnum); + image = image.toFormat(sharpFormat as keyof sharp.FormatEnum); } const buffer = await image.toBuffer(); diff --git a/packages/image-engine/src/operations/compress.ts b/packages/image-engine/src/operations/compress.ts index f84f65b4..e31d9a1d 100644 --- a/packages/image-engine/src/operations/compress.ts +++ b/packages/image-engine/src/operations/compress.ts @@ -31,7 +31,7 @@ export async function compress(image: Sharp, options: CompressOptions): Promise< const safeDetected = NO_ENCODER.has(detected) ? "png" : detected; const outputFormat = (FORMAT_MAP[format ?? ""] ?? FORMAT_MAP[safeDetected] ?? - safeDetected) as keyof import("sharp").FormatEnum; + safeDetected) as keyof sharp.FormatEnum; if (targetSizeBytes !== undefined) { if (targetSizeBytes <= 0) { @@ -52,7 +52,7 @@ export async function compress(image: Sharp, options: CompressOptions): Promise< async function findBestQuality( inputBuffer: Buffer, resize: { width: number; height: number } | null, - format: keyof import("sharp").FormatEnum, + format: keyof sharp.FormatEnum, targetBytes: number, ): Promise { let low = 1; @@ -82,7 +82,7 @@ async function findBestQuality( async function compressToTargetSize( inputBuffer: Buffer, - format: keyof import("sharp").FormatEnum, + format: keyof sharp.FormatEnum, targetBytes: number, ): Promise { const quality = await findBestQuality(inputBuffer, null, format, targetBytes); diff --git a/packages/image-engine/src/operations/convert.ts b/packages/image-engine/src/operations/convert.ts index c566178e..1460ed7d 100644 --- a/packages/image-engine/src/operations/convert.ts +++ b/packages/image-engine/src/operations/convert.ts @@ -1,3 +1,4 @@ +import type sharp from "sharp"; import type { ConvertOptions, Sharp } from "../types.js"; /** @@ -31,5 +32,5 @@ export async function convert(image: Sharp, options: ConvertOptions): Promise