Implement distributed cluster upscaling, custom output dirs, date-based naming, active preemption, and SMB mappings
- Add Output Destination folder selector with file browser and fallback to outputs/ - Apply YYYY-MM-DD start-date suffix naming rule to upscaled outputs - Implement active job preemption (demoting active running job back to queue) via reorder - Implement Coordinator / Worker roles with automatic pull-based chunk processing - Support Shared Storage mode and HTTP-based frame upload/download fallback - Add SMB mapped share configuration and background CIFS mount support - Update index.html and app.js with the new Settings sections and modals
This commit is contained in:
+423
-12
@@ -6,6 +6,16 @@ let socket = null;
|
||||
let sliderInitialized = false;
|
||||
|
||||
// Error formatting helper
|
||||
function getDownloadUrl(filePath) {
|
||||
if (!filePath) return "";
|
||||
if (filePath.startsWith("http://") || filePath.startsWith("https://")) return filePath;
|
||||
if (filePath.startsWith("/")) {
|
||||
const filename = filePath.split("/").pop();
|
||||
return `/api/download/${encodeURIComponent(filename)}?path=${encodeURIComponent(filePath)}`;
|
||||
}
|
||||
return `/api/download/${encodeURIComponent(filePath)}`;
|
||||
}
|
||||
|
||||
function formatFetchError(err, defaultMsg) {
|
||||
if (!err) return defaultMsg;
|
||||
if (typeof err === "string") return err;
|
||||
@@ -1000,8 +1010,8 @@ function updateProgressUI(data) {
|
||||
vidOriginal.style.display = "block";
|
||||
vidUpscaledWrapper.style.display = "block";
|
||||
|
||||
vidOriginal.src = `/api/download/${data.original_preview_file}`;
|
||||
vidUpscaled.src = `/api/download/${data.output_file}`;
|
||||
vidOriginal.src = getDownloadUrl(data.original_preview_file);
|
||||
vidUpscaled.src = getDownloadUrl(data.output_file);
|
||||
|
||||
vidOriginal.load();
|
||||
vidUpscaled.load();
|
||||
@@ -1060,7 +1070,7 @@ function updateProgressUI(data) {
|
||||
if (socket) socket.close();
|
||||
stepProgress.classList.remove("active");
|
||||
stepFinished.classList.add("active");
|
||||
downloadLink.href = `/api/download/${data.output_file}`;
|
||||
downloadLink.href = getDownloadUrl(data.output_file);
|
||||
loadGallery();
|
||||
}, 1500);
|
||||
|
||||
@@ -1302,7 +1312,7 @@ if (finishedPlayComparisonBtn) {
|
||||
if (job) {
|
||||
const originalFilename = job.video_path.split("/").pop();
|
||||
const originalUrl = `/api/uploads/${originalFilename}`;
|
||||
const upscaledUrl = `/api/download/${job.output_file.split("/").pop()}`;
|
||||
const upscaledUrl = getDownloadUrl(job.output_file);
|
||||
openVideoComparisonModal(originalUrl, upscaledUrl);
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -1521,10 +1531,16 @@ async function loadQueue() {
|
||||
let actionsHtml = "";
|
||||
let reorderHtml = "";
|
||||
|
||||
if (job.status === "queued" && job.queue_position !== null && job.queue_position !== undefined) {
|
||||
const queuedJobs = displayJobs.filter(j => j.status === "queued");
|
||||
const isFirst = job.queue_position === 0;
|
||||
const isLast = job.queue_position === queuedJobs.length - 1;
|
||||
const isActiveOrQueued = ["queued", "analyzing", "extracting", "upscaling", "restoring_faces", "interpolating", "assembling"].includes(job.status);
|
||||
if (isActiveOrQueued && job.queue_position !== null && job.queue_position !== undefined) {
|
||||
const activeQueuedJobs = displayJobs.filter(j =>
|
||||
["queued", "analyzing", "extracting", "upscaling", "restoring_faces", "interpolating", "assembling"].includes(j.status)
|
||||
).filter(j => j.queue_position !== null && j.queue_position !== undefined);
|
||||
|
||||
const minPos = Math.min(...activeQueuedJobs.map(j => j.queue_position));
|
||||
const maxPos = Math.max(...activeQueuedJobs.map(j => j.queue_position));
|
||||
const isFirst = job.queue_position === minPos;
|
||||
const isLast = job.queue_position === maxPos;
|
||||
|
||||
reorderHtml = `
|
||||
<button class="btn btn-secondary btn-small btn-move-up" data-id="${job.job_id}" style="padding: 0.25rem 0.5rem; width: 30px; height: 30px; border-radius: 8px; font-size: 0.8rem; display: inline-flex; align-items: center; justify-content: center; ${isFirst ? 'visibility: hidden;' : ''}" title="Move Up">
|
||||
@@ -1698,7 +1714,7 @@ async function loadQueue() {
|
||||
document.querySelectorAll(".step-container").forEach(el => el.classList.remove("active"));
|
||||
if (job.status === "completed") {
|
||||
document.getElementById("step-finished").classList.add("active");
|
||||
document.getElementById("download-link").href = `/api/download/${job.output_file}`;
|
||||
document.getElementById("download-link").href = getDownloadUrl(job.output_file);
|
||||
} else {
|
||||
document.getElementById("step-progress").classList.add("active");
|
||||
document.getElementById("job-id-display").textContent = currentJobId;
|
||||
@@ -2128,8 +2144,9 @@ async function initializeApp() {
|
||||
const defaultTempDirInput = document.getElementById("default-temp-dir-input");
|
||||
const tempDirStatus = document.getElementById("temp-dir-status");
|
||||
|
||||
// Load saved default temp dir on init
|
||||
// Load saved default temp dir and cluster settings on init
|
||||
loadDefaultTempDir();
|
||||
loadClusterSettings();
|
||||
|
||||
if (saveTempDirBtn) {
|
||||
saveTempDirBtn.addEventListener("click", async () => {
|
||||
@@ -2139,10 +2156,19 @@ async function initializeApp() {
|
||||
saveTempDirBtn.disabled = true;
|
||||
saveTempDirBtn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> Saving...';
|
||||
try {
|
||||
// Fetch current settings to preserve other values
|
||||
const currentRes = await fetch("/api/settings");
|
||||
const currentSettings = currentRes.ok ? await currentRes.json() : {};
|
||||
|
||||
const updatedSettings = {
|
||||
...currentSettings,
|
||||
default_temp_dir: newDir
|
||||
};
|
||||
|
||||
const res = await fetch("/api/settings", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ default_temp_dir: newDir })
|
||||
body: JSON.stringify(updatedSettings)
|
||||
});
|
||||
if (res.ok) {
|
||||
tempDirStatus.style.color = "var(--success, #4caf50)";
|
||||
@@ -2172,9 +2198,67 @@ async function initializeApp() {
|
||||
});
|
||||
}
|
||||
|
||||
// Default output directory setting
|
||||
const saveOutputDirBtn = document.getElementById("save-output-dir-btn");
|
||||
const defaultOutputDirInput = document.getElementById("default-output-dir-input");
|
||||
const outputDirStatus = document.getElementById("output-dir-status");
|
||||
|
||||
if (saveOutputDirBtn) {
|
||||
saveOutputDirBtn.addEventListener("click", async () => {
|
||||
const newDir = defaultOutputDirInput ? defaultOutputDirInput.value.trim() : "";
|
||||
outputDirStatus.textContent = "";
|
||||
outputDirStatus.style.color = "";
|
||||
saveOutputDirBtn.disabled = true;
|
||||
saveOutputDirBtn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> Saving...';
|
||||
try {
|
||||
// Fetch current settings to preserve other values
|
||||
const currentRes = await fetch("/api/settings");
|
||||
const currentSettings = currentRes.ok ? await currentRes.json() : {};
|
||||
|
||||
const updatedSettings = {
|
||||
...currentSettings,
|
||||
default_output_dir: newDir
|
||||
};
|
||||
|
||||
const res = await fetch("/api/settings", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(updatedSettings)
|
||||
});
|
||||
if (res.ok) {
|
||||
outputDirStatus.style.color = "var(--success, #4caf50)";
|
||||
outputDirStatus.textContent = newDir
|
||||
? `\u2713 Default output directory saved: ${newDir}`
|
||||
: "\u2713 Reset to project default outputs/ directory.";
|
||||
const perJobInput = document.getElementById("output-dir-input");
|
||||
if (perJobInput && !perJobInput.value.trim()) {
|
||||
perJobInput.placeholder = newDir
|
||||
? `Default: ${newDir} (override here)`
|
||||
: "e.g. /mnt/fast_ssd/outputs (Leave empty for default outputs/)";
|
||||
}
|
||||
} else {
|
||||
const errData = await res.json().catch(() => ({}));
|
||||
outputDirStatus.style.color = "var(--danger, #f44336)";
|
||||
outputDirStatus.textContent = `\u2717 ${errData.detail || "Failed to save setting."}`;
|
||||
}
|
||||
} catch (err) {
|
||||
outputDirStatus.style.color = "var(--danger, #f44336)";
|
||||
outputDirStatus.textContent = "\u2717 Network error saving setting.";
|
||||
console.error(err);
|
||||
} finally {
|
||||
saveOutputDirBtn.disabled = false;
|
||||
saveOutputDirBtn.innerHTML = '<i class="fa-solid fa-floppy-disk"></i> Save';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Wire up folder browser modals
|
||||
const browseJobTempBtn = document.getElementById("browse-job-temp-btn");
|
||||
const browseDefaultTempBtn = document.getElementById("browse-default-temp-btn");
|
||||
const browseJobOutputBtn = document.getElementById("browse-job-output-btn");
|
||||
const browseDefaultOutputBtn = document.getElementById("browse-default-output-btn");
|
||||
const browseSharedStorageBtn = document.getElementById("browse-shared-storage-btn");
|
||||
const browseSmbMountBtn = document.getElementById("browse-smb-mount-btn");
|
||||
const folderModalCloseBtn = document.getElementById("folder-modal-close-btn");
|
||||
const folderCancelBtn = document.getElementById("folder-cancel-btn");
|
||||
const folderSelectBtn = document.getElementById("folder-select-btn");
|
||||
@@ -2193,6 +2277,27 @@ async function initializeApp() {
|
||||
openFolderBrowser(document.getElementById("default-temp-dir-input"));
|
||||
});
|
||||
}
|
||||
|
||||
if (browseJobOutputBtn) {
|
||||
browseJobOutputBtn.addEventListener("click", () => {
|
||||
openFolderBrowser(document.getElementById("output-dir-input"));
|
||||
});
|
||||
}
|
||||
if (browseDefaultOutputBtn) {
|
||||
browseDefaultOutputBtn.addEventListener("click", () => {
|
||||
openFolderBrowser(document.getElementById("default-output-dir-input"));
|
||||
});
|
||||
}
|
||||
if (browseSharedStorageBtn) {
|
||||
browseSharedStorageBtn.addEventListener("click", () => {
|
||||
openFolderBrowser(document.getElementById("shared-storage-path-input"));
|
||||
});
|
||||
}
|
||||
if (browseSmbMountBtn) {
|
||||
browseSmbMountBtn.addEventListener("click", () => {
|
||||
openFolderBrowser(document.getElementById("smb-mount-path"));
|
||||
});
|
||||
}
|
||||
|
||||
if (folderModalCloseBtn) {
|
||||
folderModalCloseBtn.addEventListener("click", closeFolderBrowser);
|
||||
@@ -2247,6 +2352,183 @@ async function initializeApp() {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Cluster and SMB UI Settings
|
||||
const nodeRoleSelect = document.getElementById("node-role-select");
|
||||
const useSharedStorageCheckbox = document.getElementById("use-shared-storage-checkbox");
|
||||
const coordUrlGroup = document.getElementById("coord-url-group");
|
||||
const workerUrlGroup = document.getElementById("worker-url-group");
|
||||
const sharedStoragePathGroup = document.getElementById("shared-storage-path-group");
|
||||
|
||||
// Toggle fields based on selects
|
||||
if (nodeRoleSelect) {
|
||||
nodeRoleSelect.addEventListener("change", (e) => {
|
||||
const role = e.target.value;
|
||||
coordUrlGroup.style.display = role === "worker" ? "block" : "none";
|
||||
workerUrlGroup.style.display = role === "worker" || role === "coordinator" ? "block" : "none";
|
||||
});
|
||||
}
|
||||
|
||||
if (useSharedStorageCheckbox) {
|
||||
useSharedStorageCheckbox.addEventListener("change", (e) => {
|
||||
sharedStoragePathGroup.style.display = e.target.checked ? "flex" : "none";
|
||||
});
|
||||
}
|
||||
|
||||
// SMB Modals
|
||||
const addSmbShareBtn = document.getElementById("add-smb-share-btn");
|
||||
const smbModal = document.getElementById("smb-modal");
|
||||
const smbModalCloseBtn = document.getElementById("smb-modal-close-btn");
|
||||
const smbCancelBtn = document.getElementById("smb-cancel-btn");
|
||||
const smbSaveBtn = document.getElementById("smb-save-btn");
|
||||
const smbModalStatus = document.getElementById("smb-modal-status");
|
||||
|
||||
if (addSmbShareBtn) {
|
||||
addSmbShareBtn.addEventListener("click", () => {
|
||||
if (smbModal) {
|
||||
document.getElementById("smb-name").value = "";
|
||||
document.getElementById("smb-server").value = "";
|
||||
document.getElementById("smb-share-name").value = "";
|
||||
document.getElementById("smb-username").value = "";
|
||||
document.getElementById("smb-password").value = "";
|
||||
document.getElementById("smb-mount-path").value = "";
|
||||
smbModalStatus.textContent = "";
|
||||
smbModalStatus.style.color = "";
|
||||
smbModal.style.display = "flex";
|
||||
document.body.style.overflow = "hidden";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const closeSmbModal = () => {
|
||||
if (smbModal) {
|
||||
smbModal.style.display = "none";
|
||||
document.body.style.overflow = "";
|
||||
}
|
||||
};
|
||||
|
||||
if (smbModalCloseBtn) smbModalCloseBtn.addEventListener("click", closeSmbModal);
|
||||
if (smbCancelBtn) smbCancelBtn.addEventListener("click", closeSmbModal);
|
||||
|
||||
if (smbModal) {
|
||||
smbModal.addEventListener("click", (e) => {
|
||||
if (e.target === smbModal) {
|
||||
closeSmbModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (smbSaveBtn) {
|
||||
smbSaveBtn.addEventListener("click", async () => {
|
||||
const name = document.getElementById("smb-name").value.trim();
|
||||
const server = document.getElementById("smb-server").value.trim();
|
||||
const share = document.getElementById("smb-share-name").value.trim();
|
||||
const username = document.getElementById("smb-username").value.trim();
|
||||
const password = document.getElementById("smb-password").value;
|
||||
const mountPath = document.getElementById("smb-mount-path").value.trim();
|
||||
|
||||
if (!name || !server || !share || !mountPath) {
|
||||
smbModalStatus.style.color = "var(--danger)";
|
||||
smbModalStatus.textContent = "Please fill in all required fields.";
|
||||
return;
|
||||
}
|
||||
|
||||
smbModalStatus.style.color = "var(--text-muted)";
|
||||
smbModalStatus.textContent = "Attempting to mount SMB share...";
|
||||
|
||||
try {
|
||||
// Client-side SMB share management
|
||||
const getRes = await fetch("/api/settings");
|
||||
if (!getRes.ok) throw new Error("Failed to load settings.");
|
||||
const settings = await getRes.json();
|
||||
|
||||
if (!settings.smb_shares) {
|
||||
settings.smb_shares = [];
|
||||
}
|
||||
const newShare = { name, server, share, username, password, mount_path: mountPath };
|
||||
const idx = settings.smb_shares.findIndex(s => s.name === name);
|
||||
if (idx !== -1) {
|
||||
settings.smb_shares[idx] = newShare;
|
||||
} else {
|
||||
settings.smb_shares.push(newShare);
|
||||
}
|
||||
|
||||
const saveRes = await fetch("/api/settings", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(settings)
|
||||
});
|
||||
|
||||
if (saveRes.ok) {
|
||||
closeSmbModal();
|
||||
loadClusterSettings(); // Reload SMB list
|
||||
} else {
|
||||
const err = await saveRes.json().catch(() => ({}));
|
||||
smbModalStatus.style.color = "var(--danger)";
|
||||
smbModalStatus.textContent = `\u2717 ${err.detail || "Mount failed."}`;
|
||||
}
|
||||
} catch (ex) {
|
||||
smbModalStatus.style.color = "var(--danger)";
|
||||
smbModalStatus.textContent = "\u2717 Network error mapping share.";
|
||||
console.error(ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const saveClusterSettingsBtn = document.getElementById("save-cluster-settings-btn");
|
||||
const clusterSettingsStatus = document.getElementById("cluster-settings-status");
|
||||
|
||||
if (saveClusterSettingsBtn) {
|
||||
saveClusterSettingsBtn.addEventListener("click", async () => {
|
||||
const role = nodeRoleSelect ? nodeRoleSelect.value : "standalone";
|
||||
const coordinatorUrl = document.getElementById("coordinator-url-input") ? document.getElementById("coordinator-url-input").value.trim() : "";
|
||||
const workerNodeUrl = document.getElementById("worker-url-input") ? document.getElementById("worker-url-input").value.trim() : "";
|
||||
const useSharedStorage = useSharedStorageCheckbox ? useSharedStorageCheckbox.checked : false;
|
||||
const sharedStoragePath = document.getElementById("shared-storage-path-input") ? document.getElementById("shared-storage-path-input").value.trim() : "";
|
||||
|
||||
clusterSettingsStatus.textContent = "";
|
||||
clusterSettingsStatus.style.color = "";
|
||||
saveClusterSettingsBtn.disabled = true;
|
||||
saveClusterSettingsBtn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> Saving...';
|
||||
|
||||
try {
|
||||
// Fetch current settings to preserve other values
|
||||
const currentRes = await fetch("/api/settings");
|
||||
const currentSettings = currentRes.ok ? await currentRes.json() : {};
|
||||
|
||||
const updatedSettings = {
|
||||
...currentSettings,
|
||||
role,
|
||||
coordinator_url: coordinatorUrl,
|
||||
worker_node_url: workerNodeUrl,
|
||||
use_shared_storage: useSharedStorage,
|
||||
shared_storage_path: sharedStoragePath
|
||||
};
|
||||
|
||||
const res = await fetch("/api/settings", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(updatedSettings)
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
clusterSettingsStatus.style.color = "var(--success, #4caf50)";
|
||||
clusterSettingsStatus.textContent = "\u2713 Cluster settings saved successfully.";
|
||||
} else {
|
||||
const errData = await res.json().catch(() => ({}));
|
||||
clusterSettingsStatus.style.color = "var(--danger, #f44336)";
|
||||
clusterSettingsStatus.textContent = `\u2717 ${errData.detail || "Failed to save setting."}`;
|
||||
}
|
||||
} catch (err) {
|
||||
clusterSettingsStatus.style.color = "var(--danger, #f44336)";
|
||||
clusterSettingsStatus.textContent = "\u2717 Network error saving setting.";
|
||||
console.error(err);
|
||||
} finally {
|
||||
saveClusterSettingsBtn.disabled = false;
|
||||
saveClusterSettingsBtn.innerHTML = '<i class="fa-solid fa-floppy-disk"></i> Save Cluster Settings';
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------
|
||||
@@ -2483,19 +2765,148 @@ async function loadTempInfo() {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadClusterSettings() {
|
||||
const nodeRoleSelect = document.getElementById("node-role-select");
|
||||
const coordinatorUrlInput = document.getElementById("coordinator-url-input");
|
||||
const workerUrlInput = document.getElementById("worker-url-input");
|
||||
const useSharedStorageCheckbox = document.getElementById("use-shared-storage-checkbox");
|
||||
const sharedStoragePathInput = document.getElementById("shared-storage-path-input");
|
||||
const smbSharesList = document.getElementById("smb-shares-list");
|
||||
|
||||
const coordUrlGroup = document.getElementById("coord-url-group");
|
||||
const workerUrlGroup = document.getElementById("worker-url-group");
|
||||
const sharedStoragePathGroup = document.getElementById("shared-storage-path-group");
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/settings");
|
||||
if (!res.ok) return;
|
||||
const settings = await res.json();
|
||||
|
||||
// Node role
|
||||
if (nodeRoleSelect) {
|
||||
nodeRoleSelect.value = settings.role || "standalone";
|
||||
const role = nodeRoleSelect.value;
|
||||
if (coordUrlGroup) coordUrlGroup.style.display = role === "worker" ? "block" : "none";
|
||||
if (workerUrlGroup) workerUrlGroup.style.display = role === "worker" || role === "coordinator" ? "block" : "none";
|
||||
}
|
||||
|
||||
if (coordinatorUrlInput) coordinatorUrlInput.value = settings.coordinator_url || "";
|
||||
if (workerUrlInput) workerUrlInput.value = settings.worker_node_url || "http://localhost:8000";
|
||||
|
||||
// Shared Storage
|
||||
if (useSharedStorageCheckbox) {
|
||||
useSharedStorageCheckbox.checked = !!settings.use_shared_storage;
|
||||
if (sharedStoragePathGroup) sharedStoragePathGroup.style.display = settings.use_shared_storage ? "flex" : "none";
|
||||
}
|
||||
if (sharedStoragePathInput) sharedStoragePathInput.value = settings.shared_storage_path || "";
|
||||
|
||||
// Render SMB Shares
|
||||
if (smbSharesList) {
|
||||
const shares = settings.smb_shares || [];
|
||||
if (shares.length === 0) {
|
||||
smbSharesList.innerHTML = `
|
||||
<div style="text-align: center; padding: 1rem; border: 1px dashed var(--border-color); border-radius: 8px; color: var(--text-muted); font-size: 0.8rem;">
|
||||
No mapped network shares.
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
smbSharesList.innerHTML = "";
|
||||
shares.forEach(share => {
|
||||
const item = document.createElement("div");
|
||||
item.className = "queue-item";
|
||||
item.style.padding = "0.75rem";
|
||||
item.style.display = "flex";
|
||||
item.style.justifyContent = "space-between";
|
||||
item.style.alignItems = "center";
|
||||
item.style.gap = "1rem";
|
||||
|
||||
item.innerHTML = `
|
||||
<div style="flex: 1; min-width: 0;">
|
||||
<div style="font-weight: 600; font-size: 0.88rem; display: flex; align-items: center; gap: 0.4rem;">
|
||||
<i class="fa-solid fa-folder-open" style="color: var(--primary);"></i> ${share.name}
|
||||
</div>
|
||||
<div style="font-size: 0.78rem; color: var(--text-muted); margin-top: 0.2rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
|
||||
<span><i class="fa-solid fa-server"></i> //${share.server}/${share.share}</span>
|
||||
<span style="margin-left: 0.75rem;"><i class="fa-solid fa-right-to-bracket"></i> Mount: ${share.mount_path}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<button class="btn btn-danger btn-small btn-delete-smb" data-name="${share.name}" style="padding: 0.25rem 0.5rem; font-size: 0.75rem;">
|
||||
<i class="fa-solid fa-trash"></i> Unmap
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const deleteBtn = item.querySelector(".btn-delete-smb");
|
||||
if (deleteBtn) {
|
||||
deleteBtn.addEventListener("click", async (e) => {
|
||||
e.stopPropagation();
|
||||
if (confirm(`Are you sure you want to unmap SMB share "${share.name}"?`)) {
|
||||
await deleteSmbShare(share.name);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
smbSharesList.appendChild(item);
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error loading cluster settings:", err);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteSmbShare(shareName) {
|
||||
try {
|
||||
const getRes = await fetch("/api/settings");
|
||||
if (!getRes.ok) throw new Error("Failed to load settings.");
|
||||
const settings = await getRes.json();
|
||||
|
||||
if (settings.smb_shares) {
|
||||
settings.smb_shares = settings.smb_shares.filter(s => s.name !== shareName);
|
||||
}
|
||||
|
||||
const saveRes = await fetch("/api/settings", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(settings)
|
||||
});
|
||||
|
||||
if (saveRes.ok) {
|
||||
loadClusterSettings();
|
||||
} else {
|
||||
const err = await saveRes.json().catch(() => ({}));
|
||||
alert(`Failed to delete SMB share: ${err.detail || "Unknown error"}`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error deleting SMB share:", err);
|
||||
alert("Network error deleting SMB share.");
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDefaultTempDir() {
|
||||
try {
|
||||
const res = await fetch("/api/settings");
|
||||
if (!res.ok) return;
|
||||
const settings = await res.json();
|
||||
|
||||
// Temp dir settings
|
||||
const savedDir = settings.default_temp_dir || "";
|
||||
const defaultInput = document.getElementById("default-temp-dir-input");
|
||||
if (defaultInput) defaultInput.value = savedDir;
|
||||
// Update per-job input placeholder
|
||||
const perJobInput = document.getElementById("temp-dir-input");
|
||||
if (perJobInput && savedDir && !perJobInput.value.trim()) {
|
||||
perJobInput.placeholder = `Default: ${savedDir} (override here)`;
|
||||
}
|
||||
|
||||
// Output dir settings
|
||||
const savedOutputDir = settings.default_output_dir || "";
|
||||
const defaultOutputDirInput = document.getElementById("default-output-dir-input");
|
||||
if (defaultOutputDirInput) defaultOutputDirInput.value = savedOutputDir;
|
||||
const perJobOutputDirInput = document.getElementById("output-dir-input");
|
||||
if (perJobOutputDirInput && savedOutputDir && !perJobOutputDirInput.value.trim()) {
|
||||
perJobOutputDirInput.placeholder = `Default: ${savedOutputDir} (override here)`;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to load settings:", err);
|
||||
}
|
||||
|
||||
@@ -290,6 +290,20 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Custom Output Directory -->
|
||||
<div class="form-group">
|
||||
<label for="output-dir-input"><i class="fa-solid fa-folder-closed" style="color: var(--primary); margin-right: 0.25rem;"></i> Custom Output Directory</label>
|
||||
<div style="display: flex; gap: 0.5rem; align-items: stretch;">
|
||||
<input type="text" id="output-dir-input" placeholder="e.g. /mnt/fast_ssd/outputs (Leave empty for default outputs/)" style="flex: 1;">
|
||||
<button type="button" id="browse-job-output-btn" class="btn btn-secondary" style="padding: 0 1rem;" title="Browse files">
|
||||
<i class="fa-solid fa-folder-open"></i>
|
||||
</button>
|
||||
</div>
|
||||
<p style="font-size: 0.72rem; color: var(--text-muted); margin-top: 4px;">
|
||||
Specify a custom location to save the final upscaled video file.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Pre-filters selection checkboxes -->
|
||||
<div class="filter-group">
|
||||
<span style="font-weight: 600; font-size: 0.85rem; display: block; color: var(--primary);">Pre-Filters & Enhancing</span>
|
||||
@@ -609,6 +623,95 @@
|
||||
<div id="temp-dir-status" style="font-size: 0.78rem; margin-top: 0.5rem; min-height: 1.2em;"></div>
|
||||
</div>
|
||||
|
||||
<!-- Default Output Directory Setting -->
|
||||
<div class="output-dir-setting" style="margin-bottom: 1.5rem; padding: 1.25rem; border-radius: 12px; background: rgba(255,255,255,0.02); border: 1px solid var(--border-color);">
|
||||
<div style="display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.75rem;">
|
||||
<i class="fa-solid fa-folder-closed" style="color: var(--primary); font-size: 1.1rem;"></i>
|
||||
<span style="font-weight: 600; font-size: 0.95rem;">Default Output Directory</span>
|
||||
</div>
|
||||
<p style="font-size: 0.82rem; color: var(--text-muted); margin-bottom: 0.75rem;">
|
||||
Set a persistent default directory where completed upscale videos will be saved. This is applied automatically unless overridden per-job in Advanced Options.
|
||||
</p>
|
||||
<div style="display: flex; gap: 0.5rem; align-items: stretch;">
|
||||
<input type="text" id="default-output-dir-input"
|
||||
placeholder="e.g. /mnt/fast_ssd/upscaled_outputs (Leave empty for default outputs/)"
|
||||
style="flex: 1; padding: 0.6rem 0.85rem; border-radius: 8px; background: var(--bg-card); border: 1px solid var(--border-color); color: var(--text-primary); font-size: 0.88rem; font-family: 'JetBrains Mono', monospace;">
|
||||
<button type="button" id="browse-default-output-btn" class="btn btn-secondary" style="padding: 0 1rem;" title="Browse files">
|
||||
<i class="fa-solid fa-folder-open"></i>
|
||||
</button>
|
||||
<button type="button" id="save-output-dir-btn" class="btn btn-primary btn-small" style="white-space: nowrap; padding: 0.6rem 1.2rem;">
|
||||
<i class="fa-solid fa-floppy-disk"></i> Save
|
||||
</button>
|
||||
</div>
|
||||
<div id="output-dir-status" style="font-size: 0.78rem; margin-top: 0.5rem; min-height: 1.2em;"></div>
|
||||
</div>
|
||||
|
||||
<!-- Cluster & Network Settings -->
|
||||
<div class="cluster-settings" style="margin-bottom: 1.5rem; padding: 1.25rem; border-radius: 12px; background: rgba(255,255,255,0.02); border: 1px solid var(--border-color);">
|
||||
<div style="display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.75rem;">
|
||||
<i class="fa-solid fa-network-wired" style="color: var(--primary); font-size: 1.1rem;"></i>
|
||||
<span style="font-weight: 600; font-size: 0.95rem;">Distributed Processing & Node Roles</span>
|
||||
</div>
|
||||
<p style="font-size: 0.82rem; color: var(--text-muted); margin-bottom: 0.75rem;">
|
||||
Link multiple video upscaler servers together to accelerate upscaling tasks by distributing frame processing workload.
|
||||
</p>
|
||||
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin-bottom: 1rem;">
|
||||
<div class="form-group" style="margin-bottom: 0;">
|
||||
<label for="node-role-select" style="font-size: 0.8rem; font-weight: 600; display: block; margin-bottom: 0.25rem;">Server Node Role</label>
|
||||
<select id="node-role-select" style="padding: 0.5rem; border-radius: 6px; background: var(--bg-card); border: 1px solid var(--border-color); color: var(--text-main); font-size: 0.85rem; width: 100%;">
|
||||
<option value="standalone">Standalone Node (Default)</option>
|
||||
<option value="coordinator">Coordinator Node (Distribute tasks)</option>
|
||||
<option value="worker">Worker Node (upscale assigned chunks)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="coord-url-group" style="display: none; margin-bottom: 0;">
|
||||
<label for="coordinator-url-input" style="font-size: 0.8rem; font-weight: 600; display: block; margin-bottom: 0.25rem;">Coordinator API URL</label>
|
||||
<input type="text" id="coordinator-url-input" placeholder="e.g. http://192.168.1.100:8000" style="padding: 0.5rem; border-radius: 6px; background: var(--bg-card); border: 1px solid var(--border-color); color: var(--text-main); font-size: 0.85rem; width: 100%;">
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="worker-url-group" style="display: none; margin-bottom: 0;">
|
||||
<label for="worker-url-input" style="font-size: 0.8rem; font-weight: 600; display: block; margin-bottom: 0.25rem;">My Public Worker URL</label>
|
||||
<input type="text" id="worker-url-input" placeholder="e.g. http://192.168.1.101:8000" style="padding: 0.5rem; border-radius: 6px; background: var(--bg-card); border: 1px solid var(--border-color); color: var(--text-main); font-size: 0.85rem; width: 100%;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Shared Storage toggle -->
|
||||
<div style="margin-bottom: 1rem; padding: 0.75rem; border-radius: 8px; background: rgba(255,255,255,0.01); border: 1px solid var(--border-color);">
|
||||
<label class="checkbox-container" style="font-size: 0.85rem; display: flex; align-items: center; gap: 0.5rem; cursor: pointer; user-select: none;">
|
||||
<input type="checkbox" id="use-shared-storage-checkbox"> Use Shared Network Storage (Fastest — skips uploads/downloads)
|
||||
</label>
|
||||
<div id="shared-storage-path-group" style="display: none; margin-top: 0.75rem; display: flex; gap: 0.5rem; align-items: stretch;">
|
||||
<input type="text" id="shared-storage-path-input" placeholder="Local path to mounted share, e.g. /mnt/shared_temp" style="flex: 1; padding: 0.5rem; border-radius: 6px; background: var(--bg-card); border: 1px solid var(--border-color); color: var(--text-main); font-size: 0.85rem; font-family: monospace;">
|
||||
<button type="button" id="browse-shared-storage-btn" class="btn btn-secondary" style="padding: 0 0.75rem;" title="Browse files">
|
||||
<i class="fa-solid fa-folder-open"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SMB Shares section -->
|
||||
<div style="margin-bottom: 1.25rem;">
|
||||
<h4 style="font-size: 0.85rem; margin-bottom: 0.5rem; display: flex; justify-content: space-between; align-items: center;">
|
||||
<span><i class="fa-solid fa-share-nodes"></i> SMB Network Shares Mapping</span>
|
||||
<button type="button" id="add-smb-share-btn" class="btn btn-secondary btn-small" style="padding: 0.25rem 0.5rem; font-size: 0.75rem;">
|
||||
<i class="fa-solid fa-plus"></i> Add Share
|
||||
</button>
|
||||
</h4>
|
||||
<div id="smb-shares-list" style="display: flex; flex-direction: column; gap: 0.5rem;">
|
||||
<!-- Mapped shares render dynamically -->
|
||||
<div style="text-align: center; padding: 1rem; border: 1px dashed var(--border-color); border-radius: 8px; color: var(--text-muted); font-size: 0.8rem;">
|
||||
No mapped network shares.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="button" id="save-cluster-settings-btn" class="btn btn-primary btn-small" style="padding: 0.6rem 1.5rem;">
|
||||
<i class="fa-solid fa-floppy-disk"></i> Save Cluster Settings
|
||||
</button>
|
||||
<div id="cluster-settings-status" style="font-size: 0.78rem; margin-top: 0.5rem; min-height: 1.2em;"></div>
|
||||
</div>
|
||||
|
||||
<div class="temp-stats-summary" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin-bottom: 1.5rem;">
|
||||
<div class="glass-card" style="padding: 1rem; background: rgba(255,255,255,0.01); text-align: center;">
|
||||
<span style="font-size: 0.75rem; color: var(--text-muted); text-transform: uppercase; display: block;">Total Temp Size</span>
|
||||
@@ -768,6 +871,56 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SMB Share Modal -->
|
||||
<div id="smb-modal" class="modal-overlay" style="display: none; z-index: 1200;">
|
||||
<div class="modal-card" style="max-width: 500px;">
|
||||
<div class="modal-header">
|
||||
<h3><i class="fa-solid fa-folder-plus"></i> Map SMB Network Share</h3>
|
||||
<button type="button" class="close-btn" id="smb-modal-close-btn">×</button>
|
||||
</div>
|
||||
<div class="modal-body" style="padding: 1.5rem; display: flex; flex-direction: column; gap: 1rem;">
|
||||
<div class="form-group">
|
||||
<label for="smb-name" style="font-size: 0.85rem; font-weight: 600;">Share Name (Identifier)</label>
|
||||
<input type="text" id="smb-name" placeholder="e.g. video_share" style="padding: 0.5rem; border-radius: 6px; background: var(--bg-card); border: 1px solid var(--border-color); color: var(--text-main); width: 100%;">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="smb-server" style="font-size: 0.85rem; font-weight: 600;">SMB Server (IP or Hostname)</label>
|
||||
<input type="text" id="smb-server" placeholder="e.g. 192.168.1.10" style="padding: 0.5rem; border-radius: 6px; background: var(--bg-card); border: 1px solid var(--border-color); color: var(--text-main); width: 100%;">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="smb-share-name" style="font-size: 0.85rem; font-weight: 600;">Share Folder Name</label>
|
||||
<input type="text" id="smb-share-name" placeholder="e.g. videos" style="padding: 0.5rem; border-radius: 6px; background: var(--bg-card); border: 1px solid var(--border-color); color: var(--text-main); width: 100%;">
|
||||
</div>
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin-bottom: 0;">
|
||||
<div class="form-group" style="margin-bottom: 0;">
|
||||
<label for="smb-username" style="font-size: 0.85rem; font-weight: 600;">Username (Optional)</label>
|
||||
<input type="text" id="smb-username" placeholder="Guest" style="padding: 0.5rem; border-radius: 6px; background: var(--bg-card); border: 1px solid var(--border-color); color: var(--text-main); width: 100%;">
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom: 0;">
|
||||
<label for="smb-password" style="font-size: 0.85rem; font-weight: 600;">Password (Optional)</label>
|
||||
<input type="password" id="smb-password" placeholder="••••••••" style="padding: 0.5rem; border-radius: 6px; background: var(--bg-card); border: 1px solid var(--border-color); color: var(--text-main); width: 100%;">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="smb-mount-path" style="font-size: 0.85rem; font-weight: 600;">Local Mount Path</label>
|
||||
<div style="display: flex; gap: 0.5rem; align-items: stretch;">
|
||||
<input type="text" id="smb-mount-path" placeholder="e.g. /home/david/smb_mount/videos" style="flex: 1; padding: 0.5rem; border-radius: 6px; background: var(--bg-card); border: 1px solid var(--border-color); color: var(--text-main); font-family: monospace; font-size: 0.85rem;">
|
||||
<button type="button" id="browse-smb-mount-btn" class="btn btn-secondary" style="padding: 0 0.75rem;" title="Browse files">
|
||||
<i class="fa-solid fa-folder-open"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="smb-modal-status" style="font-size: 0.78rem; min-height: 1.2em;"></div>
|
||||
|
||||
<div style="display: flex; justify-content: flex-end; gap: 0.75rem; border-top: 1px solid var(--border-color); padding-top: 1rem;">
|
||||
<button type="button" id="smb-cancel-btn" class="btn btn-secondary">Cancel</button>
|
||||
<button type="button" id="smb-save-btn" class="btn btn-primary">Map & Save Share</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Javascript Orchestration -->
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user