diff --git a/build-plugin.sh b/build-plugin.sh
index 54ab595..7315f46 100755
--- a/build-plugin.sh
+++ b/build-plugin.sh
@@ -2,7 +2,7 @@
set -eu
PLUGIN="u-navigator"
-VERSION="2026.06.23.r049"
+VERSION="2026.06.24.r001"
PKG_DIR="packages"
WORK_DIR=".plugin-build"
PKG_NAME="${PLUGIN}-${VERSION}.tgz"
diff --git a/packages/u-navigator-2026.06.23.r049.tgz b/packages/u-navigator-2026.06.23.r049.tgz
deleted file mode 100644
index 5fdc6dc..0000000
Binary files a/packages/u-navigator-2026.06.23.r049.tgz and /dev/null differ
diff --git a/packages/u-navigator-2026.06.24.r001.tgz b/packages/u-navigator-2026.06.24.r001.tgz
new file mode 100644
index 0000000..41e0f36
Binary files /dev/null and b/packages/u-navigator-2026.06.24.r001.tgz differ
diff --git a/server/public/app.js b/server/public/app.js
index 07a95a5..4071a0a 100644
--- a/server/public/app.js
+++ b/server/public/app.js
@@ -19,6 +19,8 @@ const state = {
debug: [],
nextDebugId: 1,
jobs: new Map(),
+ uploadTransfers: new Map(),
+ nextUploadTransferId: 1,
jobRefresh: new Map(),
sizeJobs: new Map(),
sizeResults: new Map(),
@@ -634,13 +636,18 @@ function renderQueue(body) {
function queueMarkup() {
const jobs = [...state.jobs.values()].reverse();
- const jobMarkup = jobs.length ? jobs.map((job) => `
+ const uploads = [...state.uploadTransfers.values()].reverse();
+ const transferItems = [
+ ...uploads.map(uploadTransferMarkup),
+ ...jobs.map((job) => `
${escapeHtml(job.type)} #${escapeHtml(job.id)}
${escapeHtml(job.status)} · ${escapeHtml(job.message || '')}
- `).join('') : '
Keine Transfers.
';
+ `)
+ ];
+ const jobMarkup = transferItems.length ? transferItems.join('') : 'Keine Transfers.
';
const debugSection = state.config?.debug ? debugMarkup() : '';
return `
@@ -649,6 +656,22 @@ function queueMarkup() {
`;
}
+function uploadTransferMarkup(upload) {
+ const progress = Math.max(0, Math.min(100, Number(upload.progress) || 0));
+ const fileText = `${upload.completedFiles + upload.skippedFiles}/${upload.totalFiles} Dateien`;
+ const byteText = `${formatBytes(upload.uploadedBytes)} / ${formatBytes(upload.totalBytes)}`;
+ const detail = upload.currentFile ? ` · ${upload.currentFile}` : '';
+ const skipped = upload.skippedFiles ? ` · ${upload.skippedFiles} übersprungen` : '';
+ const overwritten = upload.overwrittenFiles ? ` · ${upload.overwrittenFiles} überschrieben` : '';
+ return `
+
+ Upload #${escapeHtml(upload.id)}
+ ${escapeHtml(upload.status)} · ${escapeHtml(fileText)} · ${escapeHtml(byteText)}${escapeHtml(detail)}${escapeHtml(skipped)}${escapeHtml(overwritten)}
+
+
+ `;
+}
+
function debugMarkup() {
const debugMarkup = state.debug.length ? state.debug.slice(-12).reverse().map((item) => `
@@ -726,7 +749,9 @@ function scheduleTransferAutoHide() {
}
function activeTransferCount() {
- return [...state.jobs.values()].filter((job) => job.type !== 'size' && ['queued', 'running', 'cancel_requested'].includes(job.status)).length;
+ const activeJobs = [...state.jobs.values()].filter((job) => job.type !== 'size' && ['queued', 'running', 'cancel_requested'].includes(job.status)).length;
+ const activeUploads = [...state.uploadTransfers.values()].filter((upload) => ['queued', 'running'].includes(upload.status)).length;
+ return activeJobs + activeUploads;
}
function renderTransferPanel() {
@@ -902,29 +927,68 @@ async function uploadFileList(win, files, targetPath) {
}
if (!files.length) return;
const uploadItems = normalizeUploadItems(files);
+ const transfer = createUploadTransfer(targetPath, uploadItems);
for (const [index, item] of uploadItems.entries()) {
+ updateUploadTransfer(transfer, {
+ status: 'running',
+ currentFile: item.path,
+ currentIndex: index + 1
+ });
recordDebug('upload.file.request', { path: item.path, size: item.file.size, index: index + 1, total: uploadItems.length });
try {
- await uploadFileChunked(targetPath, item.file, item.path, false);
+ await uploadFileChunked(targetPath, item.file, item.path, false, (bytes) => {
+ addUploadProgress(transfer, bytes);
+ });
+ updateUploadTransfer(transfer, {
+ completedFiles: transfer.completedFiles + 1,
+ currentFile: item.path,
+ progress: uploadProgressPercent(transfer)
+ });
recordDebug('upload.file.done', { path: item.path, index: index + 1, total: uploadItems.length, overwrite: false });
} catch (error) {
if (error.status !== 409) {
+ updateUploadTransfer(transfer, {
+ status: 'failed',
+ message: error.message || 'Upload fehlgeschlagen',
+ currentFile: item.path,
+ progress: uploadProgressPercent(transfer)
+ }, true);
throw error;
}
const overwrite = window.confirm(`"${item.path}" existiert bereits.\n\nVorhandene Datei überschreiben?`);
recordDebug('upload.overwrite.prompt', { path: item.path, overwrite });
if (!overwrite) {
+ updateUploadTransfer(transfer, {
+ skippedFiles: transfer.skippedFiles + 1,
+ currentFile: item.path,
+ progress: uploadProgressPercent(transfer)
+ });
recordDebug('upload.file.skipped', { path: item.path, index: index + 1, total: uploadItems.length });
continue;
}
- await uploadFileChunked(targetPath, item.file, item.path, true);
+ await uploadFileChunked(targetPath, item.file, item.path, true, (bytes) => {
+ addUploadProgress(transfer, bytes);
+ });
+ updateUploadTransfer(transfer, {
+ completedFiles: transfer.completedFiles + 1,
+ overwrittenFiles: transfer.overwrittenFiles + 1,
+ currentFile: item.path,
+ progress: uploadProgressPercent(transfer)
+ });
recordDebug('upload.file.done', { path: item.path, index: index + 1, total: uploadItems.length, overwrite: true });
}
}
+ updateUploadTransfer(transfer, {
+ status: 'completed',
+ message: 'Abgeschlossen',
+ currentFile: '',
+ progress: 100
+ }, true);
+ scheduleTransferAutoHide();
await loadExplorer(win, targetPath);
}
-async function uploadFileChunked(targetPath, file, relativePath, overwrite = false) {
+async function uploadFileChunked(targetPath, file, relativePath, overwrite = false, onChunkUploaded = null) {
const chunkSize = 256 * 1024;
const totalChunks = Math.max(1, Math.ceil(file.size / chunkSize));
let finalPayload = null;
@@ -963,6 +1027,7 @@ async function uploadFileChunked(targetPath, file, relativePath, overwrite = fal
error.path = relativePath;
throw error;
}
+ onChunkUploaded?.(chunk.size);
try {
finalPayload = JSON.parse(text);
} catch (error) {
@@ -974,6 +1039,59 @@ async function uploadFileChunked(targetPath, file, relativePath, overwrite = fal
}
}
+function createUploadTransfer(targetPath, uploadItems) {
+ const transfer = {
+ id: `upload-${state.nextUploadTransferId++}`,
+ status: 'queued',
+ message: 'Wartet',
+ targetPath,
+ totalFiles: uploadItems.length,
+ completedFiles: 0,
+ skippedFiles: 0,
+ overwrittenFiles: 0,
+ totalBytes: uploadItems.reduce((sum, item) => sum + Number(item.file?.size || 0), 0),
+ uploadedBytes: 0,
+ currentFile: '',
+ currentIndex: 0,
+ progress: 0,
+ updatedAt: Date.now(),
+ lastRenderAt: 0
+ };
+ state.uploadTransfers.set(transfer.id, transfer);
+ showTransferPanel();
+ updateTransferPanel();
+ return transfer;
+}
+
+function updateUploadTransfer(transfer, patch, force = false) {
+ Object.assign(transfer, patch, {
+ message: patch.message ?? `${transfer.currentIndex || 0}/${transfer.totalFiles}`,
+ updatedAt: Date.now()
+ });
+ const now = Date.now();
+ if (force || now - transfer.lastRenderAt > 180) {
+ transfer.lastRenderAt = now;
+ updateTransferPanel();
+ }
+}
+
+function addUploadProgress(transfer, bytes) {
+ transfer.uploadedBytes += Number(bytes || 0);
+ updateUploadTransfer(transfer, {
+ progress: uploadProgressPercent(transfer)
+ });
+}
+
+function uploadProgressPercent(transfer) {
+ if (transfer.totalBytes > 0) {
+ return (transfer.uploadedBytes / transfer.totalBytes) * 100;
+ }
+ if (transfer.totalFiles > 0) {
+ return ((transfer.completedFiles + transfer.skippedFiles) / transfer.totalFiles) * 100;
+ }
+ return 0;
+}
+
function arrayBufferToBase64(buffer) {
const bytes = new Uint8Array(buffer);
let binary = '';
diff --git a/server/public/styles.css b/server/public/styles.css
index fad81f4..a20eaab 100644
--- a/server/public/styles.css
+++ b/server/public/styles.css
@@ -809,6 +809,14 @@
padding: 10px;
}
+.u-nav .job .muted {
+ overflow-wrap: anywhere;
+}
+
+.u-nav .upload-job {
+ border-color: color-mix(in srgb, var(--accent-2) 45%, var(--line));
+}
+
.u-nav .progress {
background: var(--surface-2);
border-radius: 999px;
diff --git a/u-navigator.plg b/u-navigator.plg
index 7d958bf..a533996 100644
--- a/u-navigator.plg
+++ b/u-navigator.plg
@@ -1,7 +1,7 @@
-
+
@@ -24,7 +24,7 @@
https://git.casaderoll.de/michael/Unraid-Navigator/raw/branch/main/packages/&package;
-3bf3f4752ed9cd8b86fd27f9e17fbe14
+5d36280f152114104067f4d3e3a97234