Files
jiggablend/web/static/job_new.js
Justin Harms a3defe5cf6 Add tests for main package, manager, and various components
- 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.
2026-03-14 22:20:03 -05:00

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();
})();