Upload updates
This commit is contained in:
@@ -15,6 +15,7 @@ import { ConfirmDialog } from "@/components/ConfirmDialog";
|
||||
import { StatusFooter } from "@/components/StatusFooter";
|
||||
import { StatusPill } from "@/components/StatusPill";
|
||||
import { Toasts } from "@/components/Toasts";
|
||||
import { UploadIndicator } from "@/components/UploadIndicator";
|
||||
import { api } from "@/lib/api";
|
||||
import { hasBackend } from "@/lib/backend";
|
||||
import { cn } from "@/lib/cn";
|
||||
@@ -153,6 +154,7 @@ export function AppShell() {
|
||||
<div className="ml-auto flex items-center gap-3">
|
||||
{backend ? (
|
||||
<>
|
||||
<UploadIndicator />
|
||||
<StatusPill status={status} />
|
||||
{user ? (
|
||||
<>
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import { cn } from "@/lib/cn";
|
||||
import { useUploadStatus } from "@/lib/uploadStatus";
|
||||
|
||||
const PHASE_LABELS: Record<string, string> = {
|
||||
md5: "e621 MD5",
|
||||
visual: "Visual similarity",
|
||||
iqdb: "IQDB",
|
||||
};
|
||||
|
||||
/**
|
||||
* Shell-wide progress for the server-side upload pipeline.
|
||||
*
|
||||
* Staged uploads keep processing after the Upload page is closed, so this
|
||||
* pill is the "leave and keep an eye on it" view: it lives in the header on
|
||||
* every page and links back to the board.
|
||||
*/
|
||||
export function UploadIndicator() {
|
||||
const { data: status } = useUploadStatus();
|
||||
if (!status || !status.active) return null;
|
||||
|
||||
const total = Math.max(status.total, status.processed + status.failed);
|
||||
const done = status.processed + status.failed;
|
||||
const percent = total > 0 ? Math.min(100, Math.round((done / total) * 100)) : 0;
|
||||
const broken = status.status === "paused" || status.status === "error";
|
||||
const label =
|
||||
status.status === "paused"
|
||||
? "uploads paused"
|
||||
: status.status === "error"
|
||||
? "uploads failed"
|
||||
: (PHASE_LABELS[status.phase] ?? "processing uploads");
|
||||
const count = total > 0 ? `${done}/${total}` : `${status.outstanding} left`;
|
||||
|
||||
return (
|
||||
<Link
|
||||
to="/upload"
|
||||
title={status.error || "Staged uploads are being processed in the background"}
|
||||
className={cn(
|
||||
"flex items-center gap-2 rounded-md border px-2 py-1 font-mono text-[10px] transition",
|
||||
broken
|
||||
? "border-ctp-red/40 text-ctp-red hover:bg-ctp-red/10"
|
||||
: "border-ctp-surface1 text-ctp-subtext0 hover:bg-ctp-surface0 hover:text-ctp-text",
|
||||
)}
|
||||
>
|
||||
<span className="hidden sm:inline">{label}</span>
|
||||
<span>{count}</span>
|
||||
<span className="h-1 w-12 overflow-hidden rounded-full bg-ctp-surface0">
|
||||
<span
|
||||
className={cn("block h-full", broken ? "bg-ctp-red" : "bg-ctp-teal")}
|
||||
style={{ width: `${percent}%` }}
|
||||
/>
|
||||
</span>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -355,12 +355,17 @@ export interface TempUpload {
|
||||
status: "pending" | "visual_match" | "completed" | "error";
|
||||
resolution: "" | "auto_md5" | "duplicate" | "linked" | "custom";
|
||||
e621_post_id: number | null;
|
||||
e621_data: E621StoredPost | null;
|
||||
/** Full post payload: only present on the detail endpoint. */
|
||||
e621_data?: E621StoredPost | null;
|
||||
custom_rating: Rating;
|
||||
custom_tags: string[];
|
||||
custom_notes: string;
|
||||
iqdb_data: E621IqdbCandidate[] | null;
|
||||
visual_matches:
|
||||
/** Only present on the detail endpoint. */
|
||||
custom_tags?: string[];
|
||||
/** Only present on the detail endpoint. */
|
||||
custom_notes?: string;
|
||||
/** Only present on the detail endpoint. */
|
||||
iqdb_data?: E621IqdbCandidate[] | null;
|
||||
/** Only present on the detail endpoint. */
|
||||
visual_matches?:
|
||||
| {
|
||||
j_id: string;
|
||||
filename: string;
|
||||
@@ -371,10 +376,33 @@ export interface TempUpload {
|
||||
library_j_id: string | null;
|
||||
file_url: string | null;
|
||||
preview_url: string | null;
|
||||
/** Background pipeline state (see the UploadRun status endpoint). */
|
||||
pipeline_error: string;
|
||||
attempts: number;
|
||||
md5_checked?: boolean;
|
||||
visual_checked?: boolean;
|
||||
iqdb_checked?: boolean;
|
||||
processing?: boolean;
|
||||
similar_count?: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
/** Progress of the server-side staged-upload pipeline. */
|
||||
export interface UploadStatus {
|
||||
status: "idle" | "running" | "paused" | "error";
|
||||
active: boolean;
|
||||
phase: "" | "md5" | "visual" | "iqdb";
|
||||
total: number;
|
||||
processed: number;
|
||||
matched: number;
|
||||
failed: number;
|
||||
error: string;
|
||||
outstanding: number;
|
||||
waiting: { md5: number; visual: number; iqdb: number };
|
||||
updated_at: string | null;
|
||||
}
|
||||
|
||||
export interface StatJob {
|
||||
kind: "download" | "match";
|
||||
id: string;
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { api } from "@/lib/api";
|
||||
import { hasBackend } from "@/lib/backend";
|
||||
import type { UploadStatus } from "@/lib/types";
|
||||
import { useAuth } from "@/store/auth";
|
||||
|
||||
export const uploadStatusQueryKey = ["upload-status"] as const;
|
||||
|
||||
/**
|
||||
* Poll the server-side upload pipeline while it has work and stop once it is
|
||||
* idle. Shared by the Upload page and the shell indicator so both show the
|
||||
* same state without extra requests.
|
||||
*/
|
||||
export function useUploadStatus(enabled = true) {
|
||||
const user = useAuth((state) => state.user);
|
||||
return useQuery({
|
||||
queryKey: uploadStatusQueryKey,
|
||||
queryFn: () => api<UploadStatus>("/api/uploads/status/"),
|
||||
enabled: enabled && hasBackend() && Boolean(user?.can_upload),
|
||||
// TanStack takes the smallest interval across the observers, so the
|
||||
// Upload page and the shell pill can safely share this query.
|
||||
refetchInterval: (query) => (query.state.data?.active ? 2_000 : false),
|
||||
});
|
||||
}
|
||||
|
||||
/** Ask the server to (re)start processing the caller's staged uploads. */
|
||||
export function postProcessUploads(): Promise<UploadStatus> {
|
||||
return api<UploadStatus>("/api/uploads/process/", { method: "POST" });
|
||||
}
|
||||
Reference in New Issue
Block a user