Refactor web build process and update documentation
- Removed Node.js build artifacts from .gitignore and adjusted Makefile to reflect changes in web UI build process, now using server-rendered Go templates instead of React. - Updated README to clarify the new web UI architecture and output formats, emphasizing the removal of the Node.js build step. - Added a command to set the number of frames per render task in manager configuration, enhancing user control over rendering settings. - Improved Gitea workflow by removing unnecessary npm install step, streamlining the CI process.
This commit is contained in:
286
web/static/job_new.js
Normal file
286
web/static/job_new.js
Normal file
@@ -0,0 +1,286 @@
|
||||
(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) {
|
||||
throw new Error(data.error || "Job creation failed");
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
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) {
|
||||
showError(err.message || "Failed to create job");
|
||||
}
|
||||
});
|
||||
|
||||
setStep(1);
|
||||
loadBlenderVersions();
|
||||
})();
|
||||
Reference in New Issue
Block a user