feat(studio): recover matching Nitro sources and persist import phases
CI / check (push) Successful in 2m5s
CI / deploy (push) Successful in 1m8s
CI / publish-container (push) Successful in 1m4s

This commit is contained in:
Simo committed 2026-09-11 10:22:06 +02:00
1 parent 00e33623f7
commit 13bd3695cb
45 files changed
+903 -28

No files matched your search

@@ -20,7 +20,7 @@ import {
uuid as zUuid,
} from "zod/mini";
import { adminFetch } from "@/lib/admin-fetch";
import type { ImportJob } from "@/lib/furni/import-job";
import { IMPORT_PHASES, type ImportJob } from "@/lib/furni/import-job";
const endpoint = "/api/admin/studio/import-jobs";
const historyTimeoutMs = 15_000;
@@ -55,6 +55,16 @@ const jobSchema = zLooseObject({
"cancelled",
"interrupted",
]),
phase: zOptional(zEnum(IMPORT_PHASES)),
phaseStartedAt: zOptional(zIso.datetime({ offset: true })),
sourceAttempt: zOptional(zString()),
recoveredSource: zOptional(
zObject({
sourceId: zString(),
sourceName: zString(),
revision: zNumber(),
}),
),
error: zOptional(zString()),
warnings: zOptional(zArray(zString())),
itemId: zOptional(zNumber()),
@@ -15,6 +15,7 @@ export function FurnitureJobHistory({
}) {
const t = useTranslations("pages.admin.importHistory");
const format = useFormatter();
const progressT = useTranslations("pages.admin.importProgress");
const [limits, setLimits] = useState<Record<string, number>>({});
return (
<details
@@ -94,6 +95,29 @@ export function FurnitureJobHistory({
<li key={item.classname}>
<strong>{item.classname}</strong> · {t(item.state)}
{item.itemId ? ` · ID ${item.itemId}` : ""}
{item.phase &&
(item.state === "running" ||
item.state === "failed" ||
item.state === "interrupted") && (
<p className="text-xs text-[var(--admin-text-muted)]">
{progressT(
item.state === "running" ? "current" : "last",
{ phase: progressT(item.phase) },
)}
{item.state === "running" &&
item.phaseStartedAt &&
` · ${progressT("elapsed", { seconds: Math.max(0, Math.floor((Date.now() - Date.parse(item.phaseStartedAt)) / 1000)) })}`}
{item.sourceAttempt && ` · ${item.sourceAttempt}`}
</p>
)}
{item.recoveredSource && (
<p className="break-words text-xs text-[var(--admin-text-muted)]">
{progressT("recovered", {
source: item.recoveredSource.sourceName,
revision: item.recoveredSource.revision,
})}
</p>
)}
{item.error && (
<p className="text-[var(--admin-warning)]">
{item.error}
@@ -66,6 +66,7 @@ export function ImportReview({
setUploading(null);
}
}
const progressT = useTranslations("pages.admin.importProgress");
const inspection = useFurnitureInspection(
items.map((item) => item.classname),
);
@@ -166,8 +167,7 @@ export function ImportReview({
Source files not found by preliminary check: {check.classname}
</p>
<p className="text-xs text-[var(--admin-text-muted)]">
You can continue. The importer will try the original furniture
files and verify them before creating its SQL entry.
{progressT("recoveryHint")}
</p>
</section>
))}