- Introduced unit tests for the main package to ensure compilation. - Added tests for the manager, including validation of upload sessions and handling of Blender binary paths. - Implemented tests for job token generation and validation, ensuring security and integrity. - Created tests for configuration management and database schema to verify functionality. - Added tests for logger and runner components to enhance overall test coverage and reliability.
311 lines
11 KiB
JavaScript
311 lines
11 KiB
JavaScript
(function () {
|
|
const uploadForm = document.getElementById("upload-analyze-form");
|
|
const configForm = document.getElementById("job-config-form");
|
|
const fileInput = document.getElementById("source-file");
|
|
const statusEl = document.getElementById("upload-status");
|
|
const errorEl = document.getElementById("job-create-error");
|
|
const blendVersionEl = document.getElementById("blender-version");
|
|
const mainBlendWrapper = document.getElementById("main-blend-wrapper");
|
|
const mainBlendSelect = document.getElementById("main-blend-select");
|
|
const metadataPreview = document.getElementById("metadata-preview");
|
|
const configSection = document.getElementById("job-config-section");
|
|
const uploadSection = document.getElementById("job-upload-section");
|
|
const uploadSubmitBtn = uploadForm.querySelector('button[type="submit"]');
|
|
const stepUpload = document.getElementById("step-upload");
|
|
const stepConfig = document.getElementById("step-config");
|
|
const nameInput = document.getElementById("job-name");
|
|
const frameStartInput = document.getElementById("frame-start");
|
|
const frameEndInput = document.getElementById("frame-end");
|
|
const outputFormatInput = document.getElementById("output-format");
|
|
const unhideObjectsInput = document.getElementById("unhide-objects");
|
|
const enableExecutionInput = document.getElementById("enable-execution");
|
|
|
|
let sessionID = "";
|
|
let pollTimer = null;
|
|
let uploadInProgress = false;
|
|
|
|
function showError(msg) {
|
|
errorEl.textContent = msg || "";
|
|
errorEl.classList.toggle("hidden", !msg);
|
|
}
|
|
|
|
function showStatus(msg) {
|
|
statusEl.classList.remove("hidden");
|
|
statusEl.innerHTML = `<p>${msg}</p>`;
|
|
}
|
|
|
|
function setUploadBusy(busy) {
|
|
uploadInProgress = busy;
|
|
if (!uploadSubmitBtn) return;
|
|
uploadSubmitBtn.disabled = busy;
|
|
}
|
|
|
|
function setStep(step) {
|
|
const uploadActive = step === 1;
|
|
stepUpload.classList.toggle("active", uploadActive);
|
|
stepUpload.classList.toggle("complete", !uploadActive);
|
|
stepConfig.classList.toggle("active", !uploadActive);
|
|
uploadSection.classList.toggle("hidden", !uploadActive);
|
|
configSection.classList.toggle("hidden", uploadActive);
|
|
}
|
|
|
|
function fileNameToJobName(fileName) {
|
|
const stem = (fileName || "Render Job").replace(/\.[^/.]+$/, "");
|
|
return stem.trim() || "Render Job";
|
|
}
|
|
|
|
function prefillFromMetadata(status, fileName) {
|
|
const metadata = status.metadata || {};
|
|
const render = metadata.render_settings || {};
|
|
|
|
nameInput.value = fileNameToJobName(fileName || status.file_name);
|
|
frameStartInput.value = Number.isFinite(metadata.frame_start) ? metadata.frame_start : 1;
|
|
frameEndInput.value = Number.isFinite(metadata.frame_end) ? metadata.frame_end : 250;
|
|
|
|
if (render.output_format && outputFormatInput.querySelector(`option[value="${render.output_format}"]`)) {
|
|
outputFormatInput.value = render.output_format;
|
|
} else {
|
|
outputFormatInput.value = "EXR";
|
|
}
|
|
|
|
if (metadata.blender_version && blendVersionEl.querySelector(`option[value="${metadata.blender_version}"]`)) {
|
|
blendVersionEl.value = metadata.blender_version;
|
|
} else {
|
|
blendVersionEl.value = "";
|
|
}
|
|
|
|
unhideObjectsInput.checked = Boolean(metadata.unhide_objects);
|
|
enableExecutionInput.checked = Boolean(metadata.enable_execution);
|
|
|
|
const scenes = metadata.scene_info || {};
|
|
metadataPreview.innerHTML = `
|
|
<div class="metadata-grid">
|
|
<div><strong>Detected file:</strong> ${status.file_name || fileName || "-"}</div>
|
|
<div><strong>Frames:</strong> ${metadata.frame_start ?? "-"} - ${metadata.frame_end ?? "-"}</div>
|
|
<div><strong>Render engine:</strong> ${render.engine || "-"}</div>
|
|
<div><strong>Resolution:</strong> ${render.resolution_x || "-"} x ${render.resolution_y || "-"}</div>
|
|
<div><strong>Frame rate:</strong> ${render.frame_rate || "-"}</div>
|
|
<div><strong>Objects:</strong> ${scenes.object_count ?? "-"}</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
async function loadBlenderVersions() {
|
|
try {
|
|
const res = await fetch("/api/blender/versions", { credentials: "include" });
|
|
if (!res.ok) return;
|
|
const data = await res.json();
|
|
const versions = data.versions || [];
|
|
versions.slice(0, 30).forEach((v) => {
|
|
const option = document.createElement("option");
|
|
option.value = v.full;
|
|
option.textContent = v.full;
|
|
blendVersionEl.appendChild(option);
|
|
});
|
|
} catch (_) {}
|
|
}
|
|
|
|
function uploadFile(mainBlendFile) {
|
|
return new Promise((resolve, reject) => {
|
|
const file = fileInput.files[0];
|
|
if (!file) {
|
|
reject(new Error("Select a file first"));
|
|
return;
|
|
}
|
|
const lowerName = file.name.toLowerCase();
|
|
const isAccepted = lowerName.endsWith(".blend") || lowerName.endsWith(".zip");
|
|
if (!isAccepted) {
|
|
reject(new Error("Only .blend or .zip files are supported."));
|
|
return;
|
|
}
|
|
|
|
const fd = new FormData();
|
|
fd.append("file", file);
|
|
if (mainBlendFile) {
|
|
fd.append("main_blend_file", mainBlendFile);
|
|
}
|
|
|
|
const xhr = new XMLHttpRequest();
|
|
xhr.open("POST", "/api/jobs/upload", true);
|
|
xhr.withCredentials = true;
|
|
xhr.upload.addEventListener("progress", (e) => {
|
|
if (!e.lengthComputable) return;
|
|
const pct = Math.round((e.loaded / e.total) * 100);
|
|
showStatus(`Uploading: ${pct}%`);
|
|
});
|
|
xhr.onload = () => {
|
|
try {
|
|
const data = JSON.parse(xhr.responseText || "{}");
|
|
if (xhr.status >= 400) {
|
|
reject(new Error(data.error || "Upload failed"));
|
|
return;
|
|
}
|
|
resolve(data);
|
|
} catch (err) {
|
|
reject(err);
|
|
}
|
|
};
|
|
xhr.onerror = () => reject(new Error("Upload failed"));
|
|
xhr.send(fd);
|
|
});
|
|
}
|
|
|
|
async function pollUploadStatus() {
|
|
if (!sessionID) return null;
|
|
const res = await fetch(`/api/jobs/upload/status?session_id=${encodeURIComponent(sessionID)}`, { credentials: "include" });
|
|
const data = await res.json().catch(() => ({}));
|
|
if (!res.ok) {
|
|
throw new Error(data.error || "Upload status check failed");
|
|
}
|
|
return data;
|
|
}
|
|
|
|
async function createJob(payload) {
|
|
const res = await fetch("/api/jobs", {
|
|
method: "POST",
|
|
credentials: "include",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(payload),
|
|
});
|
|
const data = await res.json().catch(() => ({}));
|
|
if (!res.ok) {
|
|
const err = new Error(data.error || "Job creation failed");
|
|
if (data && typeof data.code === "string") {
|
|
err.code = data.code;
|
|
}
|
|
throw err;
|
|
}
|
|
return data;
|
|
}
|
|
|
|
function resetToUploadStep(message) {
|
|
sessionID = "";
|
|
clearInterval(pollTimer);
|
|
setUploadBusy(false);
|
|
mainBlendWrapper.classList.add("hidden");
|
|
metadataPreview.innerHTML = "";
|
|
configSection.classList.add("hidden");
|
|
setStep(1);
|
|
showStatus("Please upload the file again.");
|
|
showError(message);
|
|
}
|
|
|
|
async function runSubmission(mainBlendFile) {
|
|
showError("");
|
|
setStep(1);
|
|
configSection.classList.add("hidden");
|
|
metadataPreview.innerHTML = "";
|
|
|
|
const upload = await uploadFile(mainBlendFile);
|
|
sessionID = upload.session_id;
|
|
showStatus("Upload complete. Processing...");
|
|
|
|
clearInterval(pollTimer);
|
|
await new Promise((resolve, reject) => {
|
|
pollTimer = setInterval(async () => {
|
|
try {
|
|
const status = await pollUploadStatus();
|
|
if (!status) return;
|
|
showStatus(`${status.message || status.status} (${Math.round((status.progress || 0) * 100)}%)`);
|
|
|
|
if (status.status === "select_blend") {
|
|
clearInterval(pollTimer);
|
|
mainBlendSelect.innerHTML = "";
|
|
(status.blend_files || []).forEach((path) => {
|
|
const option = document.createElement("option");
|
|
option.value = path;
|
|
option.textContent = path;
|
|
mainBlendSelect.appendChild(option);
|
|
});
|
|
mainBlendWrapper.classList.remove("hidden");
|
|
reject(new Error("Select a main blend file and submit again."));
|
|
return;
|
|
}
|
|
|
|
if (status.status === "error") {
|
|
clearInterval(pollTimer);
|
|
reject(new Error(status.error || "Upload processing failed"));
|
|
return;
|
|
}
|
|
|
|
if (status.status === "completed") {
|
|
clearInterval(pollTimer);
|
|
prefillFromMetadata(status, fileInput.files[0]?.name || "");
|
|
setStep(2);
|
|
resolve();
|
|
}
|
|
} catch (err) {
|
|
clearInterval(pollTimer);
|
|
reject(err);
|
|
}
|
|
}, 1500);
|
|
});
|
|
|
|
}
|
|
|
|
async function submitJobConfig() {
|
|
if (!sessionID) {
|
|
throw new Error("Upload and analyze a file first.");
|
|
}
|
|
|
|
const fd = new FormData(configForm);
|
|
const jobName = String(fd.get("name") || "").trim();
|
|
if (!jobName) {
|
|
throw new Error("Job name is required.");
|
|
}
|
|
nameInput.value = jobName;
|
|
const payload = {
|
|
job_type: "render",
|
|
name: jobName,
|
|
frame_start: Number(fd.get("frame_start")),
|
|
frame_end: Number(fd.get("frame_end")),
|
|
output_format: fd.get("output_format"),
|
|
upload_session_id: sessionID,
|
|
unhide_objects: Boolean(fd.get("unhide_objects")),
|
|
enable_execution: Boolean(fd.get("enable_execution")),
|
|
};
|
|
const blenderVersion = fd.get("blender_version");
|
|
if (blenderVersion) payload.blender_version = blenderVersion;
|
|
|
|
const job = await createJob(payload);
|
|
showStatus(`Job created (#${job.id}). Redirecting...`);
|
|
window.location.href = `/jobs/${job.id}`;
|
|
}
|
|
|
|
uploadForm.addEventListener("submit", async (e) => {
|
|
e.preventDefault();
|
|
if (uploadInProgress) {
|
|
return;
|
|
}
|
|
try {
|
|
setUploadBusy(true);
|
|
const selected = mainBlendWrapper.classList.contains("hidden") ? "" : mainBlendSelect.value;
|
|
await runSubmission(selected);
|
|
} catch (err) {
|
|
showError(err.message || "Failed to create job");
|
|
setUploadBusy(false);
|
|
}
|
|
});
|
|
|
|
configForm.addEventListener("submit", async (e) => {
|
|
e.preventDefault();
|
|
try {
|
|
showError("");
|
|
await submitJobConfig();
|
|
} catch (err) {
|
|
if (err && err.code === "UPLOAD_SESSION_EXPIRED") {
|
|
resetToUploadStep(err.message || "Upload session expired. Please upload the file again.");
|
|
return;
|
|
}
|
|
if (err && err.code === "UPLOAD_SESSION_NOT_READY") {
|
|
showError(err.message || "Upload session is still processing. Please wait and try again.");
|
|
return;
|
|
}
|
|
showError(err.message || "Failed to create job");
|
|
}
|
|
});
|
|
|
|
setStep(1);
|
|
loadBlenderVersions();
|
|
})();
|