1a69fcfd04
- Updated the installation script to clarify that production wrappers do not include fixed test secrets, improving user guidance for local testing. - Modified the jiggablend-manager and jiggablend-runner scripts to remove fixed test configurations, emphasizing the use of local test credentials via `make init-test`. - Enhanced error handling in the runner script to ensure that an API key is provided, improving security and user feedback. - Added new flags and options for the runner, including sandboxing capabilities and GPU ray tracing control, enhancing flexibility for users. - Improved README documentation to reflect changes in usage and configuration, ensuring users have clear instructions for setup and execution.
326 lines
11 KiB
JavaScript
326 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 detectedBlenderVersion = "";
|
|
let pollTimer = null;
|
|
let uploadInProgress = false;
|
|
|
|
function escapeHtml(value) {
|
|
return String(value ?? "")
|
|
.replaceAll("&", "&")
|
|
.replaceAll("<", "<")
|
|
.replaceAll(">", ">")
|
|
.replaceAll('"', """)
|
|
.replaceAll("'", "'");
|
|
}
|
|
|
|
function showError(msg) {
|
|
errorEl.textContent = msg || "";
|
|
errorEl.classList.toggle("hidden", !msg);
|
|
}
|
|
|
|
function showStatus(msg) {
|
|
statusEl.classList.remove("hidden");
|
|
statusEl.replaceChildren();
|
|
const p = document.createElement("p");
|
|
p.textContent = msg || "";
|
|
statusEl.appendChild(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";
|
|
}
|
|
|
|
detectedBlenderVersion = metadata.blender_version || "";
|
|
if (detectedBlenderVersion && blendVersionEl.querySelector(`option[value="${detectedBlenderVersion}"]`)) {
|
|
blendVersionEl.value = detectedBlenderVersion;
|
|
} 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> ${escapeHtml(status.file_name || fileName || "-")}</div>
|
|
<div><strong>Frames:</strong> ${escapeHtml(metadata.frame_start ?? "-")} - ${escapeHtml(metadata.frame_end ?? "-")}</div>
|
|
<div><strong>Render engine:</strong> ${escapeHtml(render.engine || "-")}</div>
|
|
<div><strong>Resolution:</strong> ${escapeHtml(render.resolution_x || "-")} x ${escapeHtml(render.resolution_y || "-")}</div>
|
|
<div><strong>Frame rate:</strong> ${escapeHtml(render.frame_rate || "-")}</div>
|
|
<div><strong>Objects:</strong> ${escapeHtml(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 = "";
|
|
detectedBlenderVersion = "";
|
|
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") || detectedBlenderVersion;
|
|
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();
|
|
})();
|