fix: harden Docker image and async job responses

Harden Docker runtime packaging, preserve async job response semantics, fix Redis subscriber startup connections, clear lint warnings, and harden enterprise S3 object body handling.
This commit is contained in:
SnapOtter
2026-07-01 12:32:33 +08:00
committed by GitHub
parent 6683ee8c30
commit f3342a1e57
89 changed files with 635 additions and 159 deletions
@@ -42,14 +42,15 @@ export function DocumentView({ inputOnly = false }: { inputOnly?: boolean } = {}
// F22: when processedUrl is available, use URL-based loading instead of
// entry.file (which is the original non-PDF input and would fail pdf.js)
const file = hasProcessedUrl ? undefined : entry?.file;
if (!file && !src) return;
const url = src;
if (!file && !url) return;
let cancelled = false;
let doc: pdfjs.PDFDocumentProxy | undefined;
let renderTask: pdfjs.RenderTask | undefined;
(async () => {
try {
const source = file ? { data: new Uint8Array(await file.arrayBuffer()) } : { url: src! };
const source = file ? { data: new Uint8Array(await file.arrayBuffer()) } : { url };
doc = await pdfjs.getDocument(source).promise;
if (cancelled) return;
setPageCount(doc.numPages);
@@ -1,5 +1,4 @@
import { ArrowLeft, ChevronLeft, ChevronRight, Crown, Search } from "lucide-react";
import { useTranslation } from "@/contexts/i18n-context";
import { formatFileSize } from "@/lib/download";
import type { DuplicateFileInfo } from "@/stores/duplicate-store";
import { useDuplicateStore } from "@/stores/duplicate-store";
@@ -251,7 +250,6 @@ function DetailComparison() {
}
export function FindDuplicatesResults() {
const { t } = useTranslation();
const { results, scanning, viewMode } = useDuplicateStore();
if (scanning) {
@@ -1,6 +1,5 @@
import { Download, FolderArchive, Loader2 } from "lucide-react";
import { useCallback, useEffect, useRef, useState } from "react";
import { useTranslation } from "@/contexts/i18n-context";
import { formatHeaders } from "@/lib/api";
import { formatFileSize } from "@/lib/download";
import type { DuplicateResult } from "@/stores/duplicate-store";
@@ -16,7 +15,6 @@ const PRESET_DESCRIPTIONS: Record<Preset, string> = {
};
export function FindDuplicatesSettings() {
const { t } = useTranslation();
const { files } = useFileStore();
const {
results,
@@ -217,7 +215,7 @@ export function FindDuplicatesSettings() {
URL.revokeObjectURL(url);
}, [files, results]);
const handleDownloadAll = useCallback(async () => {
const _handleDownloadAll = useCallback(async () => {
const { zipSync } = await import("fflate");
const zipData: Record<string, Uint8Array> = {};
@@ -27,7 +27,6 @@ export interface GifToolsControlsProps {
}
export function GifToolsControls({ settings: initialSettings, onChange }: GifToolsControlsProps) {
const { t } = useTranslation();
const { info, loading: infoLoading } = useGifInfo();
const isAnimated = (info?.pages ?? 0) > 1;
@@ -24,9 +24,11 @@ export function HtmlToImageResults() {
);
}
const resultUrl = store.resultUrl;
const handleDownload = () => {
const a = document.createElement("a");
a.href = store.resultUrl!;
a.href = resultUrl;
a.download = `screenshot.${store.format}`;
a.click();
};
@@ -35,7 +37,7 @@ export function HtmlToImageResults() {
<div className="flex h-full flex-col">
<div className="flex-1 overflow-auto p-4">
<img
src={store.resultUrl}
src={resultUrl}
alt="Captured screenshot"
className="mx-auto max-w-full rounded-lg border border-border shadow-sm"
/>
@@ -49,6 +51,7 @@ export function HtmlToImageResults() {
: ""}
</span>
<button
type="button"
onClick={handleDownload}
className="inline-flex items-center gap-2 rounded-lg bg-primary px-4 py-2 text-sm font-medium text-primary-foreground"
>
@@ -45,8 +45,11 @@ export function HtmlToImageSettings() {
{store.mode === "url" && (
<div>
<label className="mb-1 block text-sm font-medium">{ts.url}</label>
<label htmlFor="html-to-image-url" className="mb-1 block text-sm font-medium">
{ts.url}
</label>
<input
id="html-to-image-url"
type="url"
value={store.url}
onChange={(e) => store.setUrl(e.target.value)}
@@ -58,8 +61,11 @@ export function HtmlToImageSettings() {
{store.mode === "html" && (
<div>
<label className="mb-1 block text-sm font-medium">{ts.htmlFile}</label>
<label htmlFor="html-to-image-file" className="mb-1 block text-sm font-medium">
{ts.htmlFile}
</label>
<input
id="html-to-image-file"
type="file"
accept=".html,.htm"
onChange={(e) => {
@@ -74,8 +80,11 @@ export function HtmlToImageSettings() {
)}
<div>
<label className="mb-1 block text-sm font-medium">{ts.format}</label>
<label htmlFor="html-to-image-format" className="mb-1 block text-sm font-medium">
{ts.format}
</label>
<select
id="html-to-image-format"
value={store.format}
onChange={(e) => store.setFormat(e.target.value as "jpg" | "png" | "webp")}
className="w-full rounded-lg border border-border bg-background px-3 py-2 text-sm"
@@ -88,10 +97,11 @@ export function HtmlToImageSettings() {
{store.format !== "png" && (
<div>
<label className="mb-1 block text-sm font-medium">
<label htmlFor="html-to-image-quality" className="mb-1 block text-sm font-medium">
{ts.quality}: {store.quality}%
</label>
<input
id="html-to-image-quality"
type="range"
min={1}
max={100}
@@ -103,8 +113,11 @@ export function HtmlToImageSettings() {
)}
<div>
<label className="mb-1 block text-sm font-medium">{ts.devicePreset}</label>
<label htmlFor="html-to-image-device-preset" className="mb-1 block text-sm font-medium">
{ts.devicePreset}
</label>
<select
id="html-to-image-device-preset"
value={store.devicePreset}
onChange={(e) =>
store.setDevicePreset(e.target.value as "desktop" | "tablet" | "mobile" | "custom")
@@ -121,8 +134,14 @@ export function HtmlToImageSettings() {
{store.devicePreset === "custom" && (
<div className="grid grid-cols-2 gap-3">
<div>
<label className="mb-1 block text-sm font-medium">{ts.viewportWidth}</label>
<label
htmlFor="html-to-image-viewport-width"
className="mb-1 block text-sm font-medium"
>
{ts.viewportWidth}
</label>
<input
id="html-to-image-viewport-width"
type="number"
min={320}
max={3840}
@@ -132,8 +151,14 @@ export function HtmlToImageSettings() {
/>
</div>
<div>
<label className="mb-1 block text-sm font-medium">{ts.viewportHeight}</label>
<label
htmlFor="html-to-image-viewport-height"
className="mb-1 block text-sm font-medium"
>
{ts.viewportHeight}
</label>
<input
id="html-to-image-viewport-height"
type="number"
min={320}
max={2160}
@@ -146,8 +171,11 @@ export function HtmlToImageSettings() {
)}
<div className="flex items-center justify-between">
<label className="text-sm font-medium">{ts.fullPage}</label>
<label htmlFor="html-to-image-full-page" className="text-sm font-medium">
{ts.fullPage}
</label>
<button
id="html-to-image-full-page"
type="button"
role="switch"
aria-checked={store.fullPage}
@@ -13,7 +13,6 @@ import { useEffect, useRef, useState } from "react";
import { ProgressCard } from "@/components/common/progress-card";
import { useTranslation } from "@/contexts/i18n-context";
import { useToolProcessor } from "@/hooks/use-tool-processor";
import { format } from "@/lib/format";
import { useFileStore } from "@/stores/file-store";
type EnhancementMode = "auto" | "portrait" | "landscape" | "low-light" | "food" | "document";
@@ -1,6 +1,5 @@
import { Check, ClipboardCopy, Download, FileJson, FileText, Loader2 } from "lucide-react";
import { useCallback, useState } from "react";
import { useTranslation } from "@/contexts/i18n-context";
import type { Base64Result } from "@/stores/base64-store";
import { useBase64Store } from "@/stores/base64-store";
import { useFileStore } from "@/stores/file-store";
@@ -169,7 +168,6 @@ function FileResult({ result }: { result: Base64Result }) {
// -- Main ResultsPanel ------------------------------------------------------
export function ImageToBase64Results() {
const { t } = useTranslation();
const { results, errors, processing, progress } = useBase64Store();
const { entries, selectedIndex, originalBlobUrl, selectedFileName } = useFileStore();
@@ -1,8 +1,6 @@
import { Loader2 } from "lucide-react";
import { useState } from "react";
import { useTranslation } from "@/contexts/i18n-context";
import { formatHeaders } from "@/lib/api";
import { format } from "@/lib/format";
import { useBase64Store } from "@/stores/base64-store";
import { useFileStore } from "@/stores/file-store";
@@ -16,7 +14,6 @@ const OUTPUT_FORMATS = [
] as const;
export function ImageToBase64Settings() {
const { t } = useTranslation();
const { files } = useFileStore();
const { processing, setProcessing, setProgress, addResult, addError, reset } = useBase64Store();
@@ -94,7 +94,7 @@ export function InfoSettings() {
cacheRef.current.clear();
autoFetchRef.current = false;
setInfo(null);
}, [files.length]);
}, []);
useEffect(() => {
if (!autoFetchRef.current || files.length === 0) return;
@@ -8,7 +8,6 @@ import {
Sparkles,
} from "lucide-react";
import { useCallback } from "react";
import { useTranslation } from "@/contexts/i18n-context";
import { cn } from "@/lib/utils";
import {
FONT_OPTIONS,
@@ -328,7 +327,6 @@ function ResultSettings() {
// ── Main Settings Component ─────────────────────────────────────────
export function MemeGeneratorSettings() {
const { t } = useTranslation();
const phase = useMemeStore((s) => s.phase);
if (phase === "gallery") return <GallerySettings />;
@@ -20,7 +20,6 @@ import {
import { useCallback, useEffect, useRef, useState } from "react";
import { create } from "zustand";
import { ProgressCard } from "@/components/common/progress-card";
import { useTranslation } from "@/contexts/i18n-context";
import { useToolProcessor } from "@/hooks/use-tool-processor";
import { formatHeaders } from "@/lib/api";
import { useFileStore } from "@/stores/file-store";
@@ -337,7 +336,6 @@ function CountryOption({
// ── Settings panel (left side) ─────────────────────────────────────
export function PassportPhotoSettings() {
const { t } = useTranslation();
const { files } = useFileStore();
const { error } = useToolProcessor("passport-photo");
@@ -896,7 +894,6 @@ export function PassportPhotoSettings() {
// ── Preview panel (right side) ────────────────────────────────────
export function PassportPhotoPreview() {
const { t } = useTranslation();
const {
analyzeResult,
countryCode,
@@ -13,7 +13,6 @@ import {
import QRCodeStyling from "qr-code-styling";
import { useCallback, useRef } from "react";
import { CollapsibleSection } from "@/components/common/collapsible-section";
import { useTranslation } from "@/contexts/i18n-context";
import {
type ContentType,
type CornerDotType,
@@ -346,7 +345,6 @@ function PillButton({
// ── Main settings component ──────────────────────────────────────────
export function QrGenerateSettings() {
const { t } = useTranslation();
const store = useQrStore();
const logoInputRef = useRef<HTMLInputElement>(null);
@@ -104,6 +104,7 @@ export function ResizeControls({ settings: initialSettings, onChange }: ResizeCo
blurRadius,
sobelThreshold,
squareMode,
contentAware,
]);
const handlePreset = (preset: (typeof SOCIAL_MEDIA_PRESETS)[number]) => {
@@ -15,7 +15,6 @@ export function RestorePhotoControls({
settings: initialSettings,
onChange,
}: RestorePhotoControlsProps) {
const { t } = useTranslation();
const [scratchRemoval, setScratchRemoval] = useState(true);
const [faceEnhancement, setFaceEnhancement] = useState(true);
const [fidelity, setFidelity] = useState(70);
@@ -4,7 +4,6 @@ import { useEffect, useRef, useState } from "react";
import { ProgressCard } from "@/components/common/progress-card";
import { useTranslation } from "@/contexts/i18n-context";
import { useToolProcessor } from "@/hooks/use-tool-processor";
import { format } from "@/lib/format";
import { useFileStore } from "@/stores/file-store";
type CropMode = "subject" | "face" | "trim";
@@ -551,7 +550,6 @@ export function SmartCropControls({ settings: initialSettings, onChange }: Smart
}
export function SmartCropSettings() {
const { t } = useTranslation();
const { files } = useFileStore();
const { processFiles, processAllFiles, processing, error, progress } =
useToolProcessor("smart-crop");
@@ -1,7 +1,6 @@
import { Download, Loader2, PackageOpen } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { CollapsibleSection } from "@/components/common/collapsible-section";
import { useTranslation } from "@/contexts/i18n-context";
import { formatHeaders } from "@/lib/api";
import { useFileStore } from "@/stores/file-store";
import type { SplitMode } from "@/stores/split-store";
@@ -36,7 +35,6 @@ const OUTPUT_FORMATS = [
const LOSSY_FORMATS = new Set(["jpg", "webp", "avif", "jxl"]);
export function SplitSettings() {
const { t } = useTranslation();
const { files, processing: fileStoreProcessing } = useFileStore();
const {
mode,
@@ -1,6 +1,5 @@
import { Download, Loader2 } from "lucide-react";
import { useState } from "react";
import { useTranslation } from "@/contexts/i18n-context";
import { formatHeaders } from "@/lib/api";
import { useFileStore } from "@/stores/file-store";
@@ -10,7 +9,6 @@ type Alignment = "start" | "center" | "end";
type OutputFormat = "png" | "jpeg" | "webp" | "avif" | "jxl";
export function StitchSettings() {
const { t } = useTranslation();
const { files, processing, error, setProcessing, setError, setProcessedUrl, setSizes, setJobId } =
useFileStore();
@@ -19,7 +19,6 @@ export function TransparencyFixerControls({
settings: _settings,
onChange,
}: TransparencyFixerControlsProps) {
const { t } = useTranslation();
const [defringe, setDefringe] = useState(30);
const [outputFormat, setOutputFormat] = useState<OutputFormat>("png");
const [removeWatermark, setRemoveWatermark] = useState(false);
@@ -3,7 +3,6 @@ import { useEffect, useRef, useState } from "react";
import { ProgressCard } from "@/components/common/progress-card";
import { useTranslation } from "@/contexts/i18n-context";
import { useToolProcessor } from "@/hooks/use-tool-processor";
import { format } from "@/lib/format";
import { useFileStore } from "@/stores/file-store";
type Position = "center" | "top-left" | "top-right" | "bottom-left" | "bottom-right" | "tiled";