Files
asepharyana-hub-tools/frontend/src/hooks/use-job-status.ts
T
asepharyanaandKilo d2bfcb83a0 fix: audit perbaikan NATS subject, worker spawn, frontend options, pipeline PDF output
Bug fixes:
1. NATS subject doubled prefix — gateway publish ke tools.tools.scan.*
   padahal workers subscribe ke tools.scan.*. Fix: pakai group() method
2. Worker subscriptions pake tokio::select! — cuma satu subscription
   yang diproses, sisanya di-cancel. Fix: ganti ke tokio::spawn + loop
3. WebSocket URL hardcoded ke localhost:3001 — gak jalan di prod.
   Fix: infer dari window.location atau env var
4. Frontend scan options hardcoded — user gak bisa atur OCR/enhance/format.
   Fix: interactive toggles, selects, slider
5. Frontend compress quality hardcoded — user gak bisa atur kualitas.
   Fix: quality range slider
6. Pipeline cuma output PNG — gak ada PDF/OCR.
   Fix: generate searchable PDF kalo tesseract feature enabled

Enhancements:
- Tool::group() method added — returns 'scan', 'image', 'pdf' dll
- Dockerfile: --features tesseract pas cargo build
- leptess OCR: proper API usage (set_image_from_mem, recognize, etc.)

Co-Authored-By: Kilo <kilo@kilo.ai>
2026-07-24 18:24:22 +07:00

104 lines
2.8 KiB
TypeScript

"use client";
import { useState, useEffect, useRef, useCallback } from "react";
export type JobStatusType = "queued" | "processing" | "completed" | "failed";
interface JobProgress {
type: "progress" | "complete" | "error" | "status" | "ping";
job_id: string;
status: JobStatusType;
progress: number;
stage: string;
message: string;
result?: {
download_url: string;
file_name: string;
file_size: number;
preview_url?: string;
ocr_text?: string;
};
error?: string;
}
interface UseJobStatusOptions {
onComplete?: (result: JobProgress["result"]) => void;
onError?: (error: string) => void;
wsBaseUrl?: string; // optional override, e.g. "wss://tools.asepharyana.my.id"
}
export function useJobStatus(jobId: string | null, options?: UseJobStatusOptions) {
const [progress, setProgress] = useState(0);
const [stage, setStage] = useState("queued");
const [message, setMessage] = useState("");
const [status, setStatus] = useState<JobStatusType>("queued");
const [result, setResult] = useState<JobProgress["result"] | null>(null);
const [error, setError] = useState<string | null>(null);
const wsRef = useRef<WebSocket | null>(null);
const retryCount = useRef(0);
const maxRetries = 3;
const connect = useCallback(() => {
if (!jobId) return;
// Determine WS base URL: from options, NEXT_PUBLIC_WS_URL, or infer from page location
const wsBase = options?.wsBaseUrl
?? process.env.NEXT_PUBLIC_WS_URL
?? (typeof window !== "undefined"
? `${window.location.protocol === "https:" ? "wss:" : "ws:"}//${window.location.host}`
: "ws://localhost:3002");
const url = `${wsBase}/api/job/${jobId}/ws`;
const ws = new WebSocket(url);
wsRef.current = ws;
ws.onopen = () => {
retryCount.current = 0;
};
ws.onmessage = (event) => {
try {
const data: JobProgress = JSON.parse(event.data);
if (data.type === "ping") return;
setStatus(data.status);
setProgress(data.progress);
setStage(data.stage);
setMessage(data.message);
if (data.type === "complete") {
setResult(data.result ?? null);
options?.onComplete?.(data.result);
}
if (data.type === "error") {
setError(data.error ?? "Unknown error");
options?.onError?.(data.error ?? "Unknown error");
}
} catch {
// Ignore parse errors
}
};
ws.onclose = () => {
if (retryCount.current < maxRetries) {
retryCount.current++;
setTimeout(connect, 1000 * retryCount.current);
}
};
ws.onerror = () => {
ws.close();
};
}, [jobId, options]);
useEffect(() => {
connect();
return () => {
wsRef.current?.close();
};
}, [connect]);
return { progress, stage, message, status, result, error };
}