fix: recover failed Gaussian project builds

This commit is contained in:
DCCONSTRUCTIONS
2026-08-26 09:34:23 +03:00
parent cb1e1494af
commit 385082c8ca
10 changed files with 224 additions and 27 deletions
@@ -111,7 +111,7 @@ export function SimulationProjectWindow({
footer={(
<>
<span className="simulation-project-window__footer-state">
{pending ? <><ActivityIndicator /><span>{project ? "Сохраняем" : "Загружаем источник"}</span></> : null}
{pending && project ? <><ActivityIndicator /><span>Сохраняем</span></> : null}
</span>
<WindowFooterActions>
<Button disabled={pending} onClick={onClose}>Отмена</Button>
@@ -223,6 +223,15 @@ export async function updateSimulationProject(
return parseProject(await jsonResponse(response));
}
export async function retrySimulationProject(projectId: string): Promise<SimulationProject> {
const response = await fetch(`${API_ROOT}/${encodeURIComponent(projectId)}/build`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: "{}",
});
return parseProject(await jsonResponse(response));
}
export async function deleteSimulationProject(projectId: string): Promise<void> {
const response = await fetch(`${API_ROOT}/${encodeURIComponent(projectId)}`, { method: "DELETE" });
if (!response.ok) await jsonResponse(response);
+31 -17
View File
@@ -60,28 +60,30 @@
}
.simulation-catalog {
overflow: hidden;
border: 1px solid var(--station-hairline);
border-radius: 1rem;
background: rgb(255 255 255 / 0.022);
display: grid;
gap: 0.55rem;
min-width: 0;
}
.simulation-catalog__summary {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 1px;
border-bottom: 1px solid var(--station-hairline);
background: var(--station-hairline);
gap: 0.45rem;
}
.simulation-catalog__summary > div {
display: grid;
gap: 0.24rem;
background: var(--nodedc-canvas);
padding: 0.85rem 1rem;
gap: 0.28rem;
border-radius: 0.85rem;
background: rgb(255 255 255 / 0.03);
padding: 0.85rem;
}
.simulation-catalog__summary span {
color: var(--nodedc-text-muted);
font-size: 0.58rem;
}
.simulation-catalog__summary span,
.simulation-catalog__table th {
color: var(--nodedc-text-muted);
font-size: 0.52rem;
@@ -92,11 +94,13 @@
.simulation-catalog__summary strong {
color: var(--nodedc-text-primary);
font-size: 0.88rem;
font-size: 1.15rem;
}
.simulation-catalog__table-wrap {
overflow: auto;
border-radius: 1rem;
background: rgb(255 255 255 / 0.025);
}
.simulation-catalog__table {
@@ -178,7 +182,6 @@
height: 2rem;
flex: 0 0 auto;
place-items: center;
border: 1px solid var(--station-hairline);
border-radius: 0.62rem;
background: rgb(var(--nodedc-accent-rgb) / 0.07);
color: var(--nodedc-text-secondary);
@@ -195,6 +198,8 @@
justify-content: center;
gap: 0.75rem;
color: var(--nodedc-text-muted);
border-radius: 1rem;
background: rgb(255 255 255 / 0.025);
font-size: 0.65rem;
}
@@ -275,7 +280,8 @@
.simulation-upload-progress > span {
position: relative;
overflow: hidden;
width: 8rem;
min-width: 12rem;
flex: 1 1 auto;
height: 0.28rem;
border-radius: 999px;
background: rgb(255 255 255 / 0.08);
@@ -290,7 +296,9 @@
.simulation-upload-progress > div {
display: grid;
min-width: 0;
min-width: 9rem;
max-width: 13rem;
flex: 0 1 13rem;
gap: 0.14rem;
}
@@ -328,7 +336,6 @@
min-height: 0;
grid-template-rows: auto minmax(0, 1fr) auto;
overflow: hidden;
border: 1px solid var(--station-hairline);
border-radius: 1rem;
background: #07090d;
}
@@ -408,12 +415,19 @@
gap: 0.8rem;
}
.simulation-workspace__processing > div {
.simulation-workspace__processing-copy {
display: grid;
max-width: 40rem;
gap: 0.25rem;
}
.simulation-workspace__processing-actions {
display: flex;
flex: 0 0 auto;
align-items: center;
gap: 0.55rem;
}
.simulation-workspace__processing strong {
color: var(--nodedc-text-primary);
font-size: 0.75rem;
@@ -14,6 +14,7 @@ import { SimulationViewport } from "../../components/simulation/SimulationViewpo
import {
deleteSimulationProject,
fetchSimulationProjects,
retrySimulationProject,
type SimulationProject,
type SimulationProjectStatus,
} from "../../core/simulation/projects";
@@ -28,6 +29,7 @@ export function SimulationWorkspace() {
const [windowOpen, setWindowOpen] = useState(false);
const [editing, setEditing] = useState<SimulationProject | null>(null);
const [deleting, setDeleting] = useState<SimulationProject | null>(null);
const [retryingId, setRetryingId] = useState<string | null>(null);
const load = useCallback(async (signal?: AbortSignal) => {
try {
@@ -84,6 +86,21 @@ export function SimulationWorkspace() {
setDeleting(null);
};
const retry = async (project: SimulationProject) => {
setRetryingId(project.projectId);
setError(null);
try {
const queued = await retrySimulationProject(project.projectId);
setProjects((current) => current.map((item) => (
item.projectId === queued.projectId ? queued : item
)));
} catch (caught) {
setError(caught instanceof Error ? caught.message : "Не удалось повторить сборку.");
} finally {
setRetryingId(null);
}
};
if (selected) {
return (
<div className="simulation-workspace simulation-workspace--scene">
@@ -109,13 +126,28 @@ export function SimulationWorkspace() {
) : (
<GlassSurface className="simulation-workspace__processing" padding="lg">
{selected.status === "failed" ? <Icon name="alert" size={20} /> : <ActivityIndicator label="Сборка мира" />}
<div>
<div className="simulation-workspace__processing-copy">
<strong>{selected.status === "failed" ? "Сборка остановлена" : "Worker 006 собирает мир"}</strong>
<p>{selected.error ?? processingMessage(selected)}</p>
<p>{error ?? selected.error ?? processingMessage(selected)}</p>
</div>
<div className="simulation-workspace__processing-actions">
<StatusBadge tone={selected.status === "failed" ? "warning" : "accent"}>
{selected.provider.state ?? selected.status}
</StatusBadge>
{selected.status === "failed" ? (
<Button
size="compact"
variant="secondary"
disabled={retryingId === selected.projectId}
icon={retryingId === selected.projectId
? <ActivityIndicator size="compact" />
: <Icon name="refresh" size={14} />}
onClick={() => void retry(selected)}
>
Повторить сборку
</Button>
) : null}
</div>
<StatusBadge tone={selected.status === "failed" ? "warning" : "accent"}>
{selected.provider.state ?? selected.status}
</StatusBadge>
</GlassSurface>
)}
</div>
@@ -39,6 +39,18 @@ test("browser upload remains same-origin, resumable and token-free", async () =>
assert.match(window, /webkitdirectory/);
assert.match(window, /webkitGetAsEntry/);
assert.match(window, /ровно одна сцена \.lcc или \.lcc2/);
assert.doesNotMatch(window, /Загружаем источник/);
});
test("failed simulation can restart from its retained Mission Core source", async () => {
const [core, workspace] = await Promise.all([
read("core/simulation/projects.ts"),
read("workspaces/simulation/SimulationWorkspace.tsx"),
]);
assert.match(core, /retrySimulationProject/);
assert.match(core, /\$\{API_ROOT\}\/\$\{encodeURIComponent\(projectId\)\}\/build/);
assert.match(workspace, /Повторить сборку/);
});
test("PlayCanvas owns the realtime scene graph without an iframe or React entity tree", async () => {