diff --git a/build-plugin.sh b/build-plugin.sh
index 13200c8..7ff10d0 100755
--- a/build-plugin.sh
+++ b/build-plugin.sh
@@ -2,7 +2,7 @@
set -eu
PLUGIN="u-navigator"
-VERSION="2026.06.23.r004"
+VERSION="2026.06.23.r005"
PKG_DIR="packages"
WORK_DIR=".plugin-build"
PKG_NAME="${PLUGIN}-${VERSION}.tgz"
diff --git a/packages/u-navigator-2026.06.23.r004.tgz b/packages/u-navigator-2026.06.23.r004.tgz
deleted file mode 100644
index d729826..0000000
Binary files a/packages/u-navigator-2026.06.23.r004.tgz and /dev/null differ
diff --git a/packages/u-navigator-2026.06.23.r005.tgz b/packages/u-navigator-2026.06.23.r005.tgz
new file mode 100644
index 0000000..f44e4ab
Binary files /dev/null and b/packages/u-navigator-2026.06.23.r005.tgz differ
diff --git a/server/public/app.js b/server/public/app.js
index 6880894..451c2d5 100644
--- a/server/public/app.js
+++ b/server/public/app.js
@@ -10,6 +10,8 @@ const state = {
draggedEntry: null,
pointerDrag: null,
suppressClickPath: null,
+ debug: [],
+ nextDebugId: 1,
jobs: new Map(),
nextWindowId: 1,
zIndex: 20
@@ -25,6 +27,18 @@ const icons = {
document.querySelector('#newExplorerButton').addEventListener('click', () => openExplorer());
document.querySelector('#newPropertiesButton').addEventListener('click', () => openProperties());
document.querySelector('#newQueueButton').addEventListener('click', () => openQueue());
+window.addEventListener('error', (event) => {
+ recordDebug('window.error', {
+ message: event.message,
+ source: event.filename,
+ line: event.lineno,
+ column: event.colno,
+ error: serializeError(event.error)
+ });
+});
+window.addEventListener('unhandledrejection', (event) => {
+ recordDebug('window.unhandledrejection', { reason: serializeError(event.reason) });
+});
init();
@@ -300,18 +314,35 @@ function renderProperties(body) {
function renderQueue(body) {
const jobs = [...state.jobs.values()].reverse();
- if (!jobs.length) {
- body.innerHTML = '
Keine Transfers.
';
- return;
- }
-
- body.innerHTML = `${jobs.map((job) => `
+ const jobMarkup = jobs.length ? jobs.map((job) => `
${escapeHtml(job.type)} #${escapeHtml(job.id)}
${escapeHtml(job.status)} · ${escapeHtml(job.message || '')}
- `).join('')}
`;
+ `).join('') : 'Keine Transfers.
';
+ const debugMarkup = state.debug.length ? state.debug.slice(-12).reverse().map((item) => `
+
+ #${item.id} ${escapeHtml(item.label)}
+
+ ${escapeHtml(JSON.stringify(item.data, null, 2))}
+
+ `).join('') : 'Noch keine Debug-Einträge.
';
+
+ body.innerHTML = `
+ ${jobMarkup}
+
+ `;
+ body.querySelector('[data-action="clear-debug"]')?.addEventListener('click', () => {
+ state.debug = [];
+ render();
+ });
}
async function loadExplorer(win, targetPath) {
@@ -332,8 +363,13 @@ async function loadExplorer(win, targetPath) {
}
async function handleDrop(event, win, targetPath) {
+ recordDebug('html.drop', {
+ targetPath,
+ types: dataTransferTypes(event),
+ files: getDroppedFiles(event).map((file) => ({ name: file.name, size: file.size }))
+ });
if (state.config.readOnly) {
- alert('Read-only mode ist aktiv.');
+ showError(new Error('Read-only mode ist aktiv.'));
return;
}
@@ -352,11 +388,13 @@ async function handleDrop(event, win, targetPath) {
}
async function createJob(type, source, destination) {
+ recordDebug('job.create.request', { type, source, destination });
const job = await api(apiUrl(`job.php?action=${encodeURIComponent(type)}`), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ source, destination })
});
+ recordDebug('job.create.response', job);
state.jobs.set(job.id, job);
pollJob(job.id);
render();
@@ -369,13 +407,15 @@ async function reloadExplorerWindows(ids) {
}
function hasDropPayload(event) {
- const types = [...(event.dataTransfer?.types || [])];
+ const types = dataTransferTypes(event);
return types.includes('Files') || getDroppedFiles(event).length > 0;
}
function showError(error) {
+ const debug = recordDebug('error.shown', serializeError(error));
const message = error?.message || String(error);
- alert(message);
+ const detail = debug ? `\n\nDebug #${debug.id}: ${debug.label}` : '';
+ alert(`${message}${detail}`);
}
function getDroppedFiles(event) {
@@ -386,17 +426,28 @@ function getDroppedFiles(event) {
}
}
+function dataTransferTypes(event) {
+ try {
+ return [...(event.dataTransfer?.types || [])];
+ } catch (error) {
+ recordDebug('dataTransfer.types.error', serializeError(error));
+ return [];
+ }
+}
+
function setEffectAllowed(event) {
try {
event.dataTransfer.effectAllowed = 'copyMove';
- } catch {}
+ } catch (error) {
+ recordDebug('dataTransfer.effectAllowed.error', serializeError(error));
+ }
}
function setDropEffect(event, effect) {
try {
event.dataTransfer.dropEffect = effect;
} catch {
- // Browser rejected the requested cursor hint; the app-state drop still works.
+ recordDebug('dataTransfer.dropEffect.error', { effect });
}
}
@@ -406,7 +457,7 @@ async function renameEntry(win, entry) {
return;
}
if (nextName.includes('/')) {
- alert('Der Name darf keinen Slash enthalten.');
+ showError(new Error('Der Name darf keinen Slash enthalten.'));
return;
}
await createJob('move', entry.path, `${dirname(entry.path)}/${nextName}`);
@@ -464,7 +515,7 @@ function renderContextMenu() {
try {
await action();
} catch (error) {
- alert(error.message);
+ showError(error);
}
});
menu.appendChild(button);
@@ -488,11 +539,16 @@ function renderContextMenu() {
}
async function pollJob(id) {
- const job = await api(apiUrl(`job.php?id=${encodeURIComponent(id)}`));
- state.jobs.set(id, job);
- render();
- if (['queued', 'running', 'cancel_requested'].includes(job.status)) {
- setTimeout(() => pollJob(id), 700);
+ try {
+ const job = await api(apiUrl(`job.php?id=${encodeURIComponent(id)}`));
+ state.jobs.set(id, job);
+ render();
+ if (['queued', 'running', 'cancel_requested'].includes(job.status)) {
+ setTimeout(() => pollJob(id), 700);
+ }
+ } catch (error) {
+ recordDebug('job.poll.error', { id, error: serializeError(error) });
+ showError(error);
}
}
@@ -540,6 +596,13 @@ function updatePropertiesWindows() {
function startItemPointerDrag(event, win, entry) {
if (event.button !== 0 || !entry) return;
+ recordDebug('pointer.start', {
+ windowId: win.id,
+ path: entry.path,
+ type: entry.type,
+ x: event.clientX,
+ y: event.clientY
+ });
state.pointerDrag = {
sourceWindowId: win.id,
path: entry.path,
@@ -595,6 +658,13 @@ async function finishItemPointerDrag(event, move, up) {
event.preventDefault();
state.suppressClickPath = drag.path;
const target = findPointerDropTarget(event);
+ recordDebug('pointer.finish', {
+ source: drag.path,
+ target: target ? { windowId: target.windowId, path: target.path, rowPath: target.row?.dataset?.path || null } : null,
+ copy: event.altKey,
+ x: event.clientX,
+ y: event.clientY
+ });
if (!target) {
return;
}
@@ -635,14 +705,35 @@ function clearPointerDropTarget() {
}
function findPointerDropTarget(event) {
- const element = document.elementFromPoint(event.clientX, event.clientY);
- const windowEl = element?.closest?.('.window');
+ let element;
+ try {
+ element = document.elementFromPoint(event.clientX, event.clientY);
+ } catch (error) {
+ recordDebug('elementFromPoint.error', {
+ x: event.clientX,
+ y: event.clientY,
+ error: serializeError(error)
+ });
+ return null;
+ }
+ let windowEl;
+ try {
+ windowEl = element?.closest?.('.window');
+ } catch (error) {
+ recordDebug('closest.window.error', { error: serializeError(error), element: element?.tagName });
+ return null;
+ }
if (!windowEl) return null;
const win = state.windows.find((item) => item.id === windowEl.dataset.windowId);
if (!win || win.kind !== 'explorer') return null;
- const row = element.closest?.('.file-row');
+ let row;
+ try {
+ row = element.closest?.('.file-row');
+ } catch (error) {
+ recordDebug('closest.row.error', { error: serializeError(error), element: element?.tagName });
+ }
if (row?.dataset.type === 'directory') {
return { windowId: win.id, path: row.dataset.path, row };
}
@@ -734,11 +825,18 @@ function startResizeWindow(event, win) {
async function api(url, options) {
const response = await fetchChecked(url, options);
- return response.json();
+ try {
+ return await response.json();
+ } catch (error) {
+ recordDebug('api.json.error', { url, error: serializeError(error) });
+ throw error;
+ }
}
async function fetchChecked(url, options) {
+ recordDebug('fetch.request', { url, method: options?.method || 'GET' });
const response = await fetch(url, options);
+ recordDebug('fetch.response', { url, status: response.status, ok: response.ok });
if (!response.ok) {
let message = `${response.status} ${response.statusText}`;
try {
@@ -797,3 +895,39 @@ function clamp(value, min, max) {
function apiUrl(path) {
return `${API_BASE.replace(/\/$/, '')}/${path.replace(/^\//, '')}`;
}
+
+function recordDebug(label, data = {}) {
+ const item = {
+ id: state.nextDebugId++,
+ time: new Date().toLocaleTimeString(),
+ label,
+ data
+ };
+ state.debug.push(item);
+ if (state.debug.length > 80) {
+ state.debug.splice(0, state.debug.length - 80);
+ }
+ updateQueueWindows();
+ return item;
+}
+
+function updateQueueWindows() {
+ for (const win of state.windows.filter((item) => item.kind === 'queue')) {
+ const body = workspace.querySelector(`.window[data-window-id="${win.id}"] .window-body`);
+ if (body) {
+ renderQueue(body);
+ }
+ }
+}
+
+function serializeError(error) {
+ if (!error) return null;
+ if (error instanceof Error || typeof error === 'object') {
+ return {
+ name: error.name,
+ message: error.message || String(error),
+ stack: error.stack
+ };
+ }
+ return { message: String(error) };
+}
diff --git a/server/public/styles.css b/server/public/styles.css
index eee438f..2dc191d 100644
--- a/server/public/styles.css
+++ b/server/public/styles.css
@@ -306,6 +306,43 @@
gap: 10px;
}
+.u-nav .debug-log {
+ border-top: 1px solid var(--line);
+ display: grid;
+ gap: 8px;
+ margin-top: 14px;
+ padding-top: 12px;
+}
+
+.u-nav .debug-log header {
+ align-items: center;
+ display: flex;
+ justify-content: space-between;
+}
+
+.u-nav .debug-entry {
+ background: var(--surface-2);
+ border: 1px solid var(--line);
+ border-radius: 6px;
+ padding: 8px;
+}
+
+.u-nav .debug-entry time {
+ color: var(--muted);
+ display: block;
+ font-size: 12px;
+}
+
+.u-nav .debug-entry pre {
+ color: var(--text);
+ font: 12px/1.35 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
+ margin: 6px 0 0;
+ max-height: 140px;
+ overflow: auto;
+ white-space: pre-wrap;
+ word-break: break-word;
+}
+
.u-nav .job {
border: 1px solid var(--line);
border-radius: 8px;
diff --git a/u-navigator.plg b/u-navigator.plg
index 0b08b0f..2bf3a14 100644
--- a/u-navigator.plg
+++ b/u-navigator.plg
@@ -1,7 +1,7 @@
-
+
]>
@@ -14,7 +14,7 @@
https://git.casaderoll.de/michael/Unraid-Navigator/raw/branch/main/packages/&package;
-da98c5f068d4f12dfddb0e7143a7aaa0
+1e5eb8df508f7a4e46a4c13336302d16