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}
+
+
+ Debug + +
+ ${debugMarkup} +
+ `; + 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