Add frontend debug tracing
This commit is contained in:
+1
-1
@@ -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"
|
||||
|
||||
Binary file not shown.
Binary file not shown.
+152
-18
@@ -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 = '<p class="muted">Keine Transfers.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
body.innerHTML = `<div class="queue-list">${jobs.map((job) => `
|
||||
const jobMarkup = jobs.length ? jobs.map((job) => `
|
||||
<article class="job">
|
||||
<strong>${escapeHtml(job.type)} #${escapeHtml(job.id)}</strong>
|
||||
<div class="muted">${escapeHtml(job.status)} · ${escapeHtml(job.message || '')}</div>
|
||||
<div class="progress"><div style="width:${Number(job.progress) || 0}%"></div></div>
|
||||
</article>
|
||||
`).join('')}</div>`;
|
||||
`).join('') : '<p class="muted">Keine Transfers.</p>';
|
||||
const debugMarkup = state.debug.length ? state.debug.slice(-12).reverse().map((item) => `
|
||||
<article class="debug-entry">
|
||||
<strong>#${item.id} ${escapeHtml(item.label)}</strong>
|
||||
<time>${escapeHtml(item.time)}</time>
|
||||
<pre>${escapeHtml(JSON.stringify(item.data, null, 2))}</pre>
|
||||
</article>
|
||||
`).join('') : '<p class="muted">Noch keine Debug-Einträge.</p>';
|
||||
|
||||
body.innerHTML = `
|
||||
<div class="queue-list">${jobMarkup}</div>
|
||||
<section class="debug-log" aria-label="Debug-Protokoll">
|
||||
<header>
|
||||
<strong>Debug</strong>
|
||||
<button type="button" data-action="clear-debug">Leeren</button>
|
||||
</header>
|
||||
${debugMarkup}
|
||||
</section>
|
||||
`;
|
||||
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,12 +539,17 @@ function renderContextMenu() {
|
||||
}
|
||||
|
||||
async function pollJob(id) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
function selectEntry(win, entry, scrollBody = null) {
|
||||
@@ -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) };
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
<!DOCTYPE PLUGIN [
|
||||
<!ENTITY name "u-navigator">
|
||||
<!ENTITY author "michael">
|
||||
<!ENTITY version "2026.06.23.r004">
|
||||
<!ENTITY version "2026.06.23.r005">
|
||||
<!ENTITY package "&name;-&version;.tgz">
|
||||
]>
|
||||
<PLUGIN name="&name;" author="&author;" version="&version;" pluginURL="https://git.casaderoll.de/michael/Unraid-Navigator/raw/branch/main/u-navigator.plg">
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
<FILE Name="/boot/config/plugins/&name;/&package;">
|
||||
<URL>https://git.casaderoll.de/michael/Unraid-Navigator/raw/branch/main/packages/&package;</URL>
|
||||
<MD5>da98c5f068d4f12dfddb0e7143a7aaa0</MD5>
|
||||
<MD5>1e5eb8df508f7a4e46a4c13336302d16</MD5>
|
||||
</FILE>
|
||||
|
||||
<FILE Run="/bin/bash">
|
||||
|
||||
Reference in New Issue
Block a user