Add frontend debug tracing
This commit is contained in:
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
PLUGIN="u-navigator"
|
PLUGIN="u-navigator"
|
||||||
VERSION="2026.06.23.r004"
|
VERSION="2026.06.23.r005"
|
||||||
PKG_DIR="packages"
|
PKG_DIR="packages"
|
||||||
WORK_DIR=".plugin-build"
|
WORK_DIR=".plugin-build"
|
||||||
PKG_NAME="${PLUGIN}-${VERSION}.tgz"
|
PKG_NAME="${PLUGIN}-${VERSION}.tgz"
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
+157
-23
@@ -10,6 +10,8 @@ const state = {
|
|||||||
draggedEntry: null,
|
draggedEntry: null,
|
||||||
pointerDrag: null,
|
pointerDrag: null,
|
||||||
suppressClickPath: null,
|
suppressClickPath: null,
|
||||||
|
debug: [],
|
||||||
|
nextDebugId: 1,
|
||||||
jobs: new Map(),
|
jobs: new Map(),
|
||||||
nextWindowId: 1,
|
nextWindowId: 1,
|
||||||
zIndex: 20
|
zIndex: 20
|
||||||
@@ -25,6 +27,18 @@ const icons = {
|
|||||||
document.querySelector('#newExplorerButton').addEventListener('click', () => openExplorer());
|
document.querySelector('#newExplorerButton').addEventListener('click', () => openExplorer());
|
||||||
document.querySelector('#newPropertiesButton').addEventListener('click', () => openProperties());
|
document.querySelector('#newPropertiesButton').addEventListener('click', () => openProperties());
|
||||||
document.querySelector('#newQueueButton').addEventListener('click', () => openQueue());
|
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();
|
init();
|
||||||
|
|
||||||
@@ -300,18 +314,35 @@ function renderProperties(body) {
|
|||||||
|
|
||||||
function renderQueue(body) {
|
function renderQueue(body) {
|
||||||
const jobs = [...state.jobs.values()].reverse();
|
const jobs = [...state.jobs.values()].reverse();
|
||||||
if (!jobs.length) {
|
const jobMarkup = jobs.length ? jobs.map((job) => `
|
||||||
body.innerHTML = '<p class="muted">Keine Transfers.</p>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.innerHTML = `<div class="queue-list">${jobs.map((job) => `
|
|
||||||
<article class="job">
|
<article class="job">
|
||||||
<strong>${escapeHtml(job.type)} #${escapeHtml(job.id)}</strong>
|
<strong>${escapeHtml(job.type)} #${escapeHtml(job.id)}</strong>
|
||||||
<div class="muted">${escapeHtml(job.status)} · ${escapeHtml(job.message || '')}</div>
|
<div class="muted">${escapeHtml(job.status)} · ${escapeHtml(job.message || '')}</div>
|
||||||
<div class="progress"><div style="width:${Number(job.progress) || 0}%"></div></div>
|
<div class="progress"><div style="width:${Number(job.progress) || 0}%"></div></div>
|
||||||
</article>
|
</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) {
|
async function loadExplorer(win, targetPath) {
|
||||||
@@ -332,8 +363,13 @@ async function loadExplorer(win, targetPath) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleDrop(event, 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) {
|
if (state.config.readOnly) {
|
||||||
alert('Read-only mode ist aktiv.');
|
showError(new Error('Read-only mode ist aktiv.'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -352,11 +388,13 @@ async function handleDrop(event, win, targetPath) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function createJob(type, source, destination) {
|
async function createJob(type, source, destination) {
|
||||||
|
recordDebug('job.create.request', { type, source, destination });
|
||||||
const job = await api(apiUrl(`job.php?action=${encodeURIComponent(type)}`), {
|
const job = await api(apiUrl(`job.php?action=${encodeURIComponent(type)}`), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ source, destination })
|
body: JSON.stringify({ source, destination })
|
||||||
});
|
});
|
||||||
|
recordDebug('job.create.response', job);
|
||||||
state.jobs.set(job.id, job);
|
state.jobs.set(job.id, job);
|
||||||
pollJob(job.id);
|
pollJob(job.id);
|
||||||
render();
|
render();
|
||||||
@@ -369,13 +407,15 @@ async function reloadExplorerWindows(ids) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function hasDropPayload(event) {
|
function hasDropPayload(event) {
|
||||||
const types = [...(event.dataTransfer?.types || [])];
|
const types = dataTransferTypes(event);
|
||||||
return types.includes('Files') || getDroppedFiles(event).length > 0;
|
return types.includes('Files') || getDroppedFiles(event).length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function showError(error) {
|
function showError(error) {
|
||||||
|
const debug = recordDebug('error.shown', serializeError(error));
|
||||||
const message = error?.message || String(error);
|
const message = error?.message || String(error);
|
||||||
alert(message);
|
const detail = debug ? `\n\nDebug #${debug.id}: ${debug.label}` : '';
|
||||||
|
alert(`${message}${detail}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDroppedFiles(event) {
|
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) {
|
function setEffectAllowed(event) {
|
||||||
try {
|
try {
|
||||||
event.dataTransfer.effectAllowed = 'copyMove';
|
event.dataTransfer.effectAllowed = 'copyMove';
|
||||||
} catch {}
|
} catch (error) {
|
||||||
|
recordDebug('dataTransfer.effectAllowed.error', serializeError(error));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setDropEffect(event, effect) {
|
function setDropEffect(event, effect) {
|
||||||
try {
|
try {
|
||||||
event.dataTransfer.dropEffect = effect;
|
event.dataTransfer.dropEffect = effect;
|
||||||
} catch {
|
} 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;
|
return;
|
||||||
}
|
}
|
||||||
if (nextName.includes('/')) {
|
if (nextName.includes('/')) {
|
||||||
alert('Der Name darf keinen Slash enthalten.');
|
showError(new Error('Der Name darf keinen Slash enthalten.'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await createJob('move', entry.path, `${dirname(entry.path)}/${nextName}`);
|
await createJob('move', entry.path, `${dirname(entry.path)}/${nextName}`);
|
||||||
@@ -464,7 +515,7 @@ function renderContextMenu() {
|
|||||||
try {
|
try {
|
||||||
await action();
|
await action();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert(error.message);
|
showError(error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
menu.appendChild(button);
|
menu.appendChild(button);
|
||||||
@@ -488,11 +539,16 @@ function renderContextMenu() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function pollJob(id) {
|
async function pollJob(id) {
|
||||||
const job = await api(apiUrl(`job.php?id=${encodeURIComponent(id)}`));
|
try {
|
||||||
state.jobs.set(id, job);
|
const job = await api(apiUrl(`job.php?id=${encodeURIComponent(id)}`));
|
||||||
render();
|
state.jobs.set(id, job);
|
||||||
if (['queued', 'running', 'cancel_requested'].includes(job.status)) {
|
render();
|
||||||
setTimeout(() => pollJob(id), 700);
|
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) {
|
function startItemPointerDrag(event, win, entry) {
|
||||||
if (event.button !== 0 || !entry) return;
|
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 = {
|
state.pointerDrag = {
|
||||||
sourceWindowId: win.id,
|
sourceWindowId: win.id,
|
||||||
path: entry.path,
|
path: entry.path,
|
||||||
@@ -595,6 +658,13 @@ async function finishItemPointerDrag(event, move, up) {
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
state.suppressClickPath = drag.path;
|
state.suppressClickPath = drag.path;
|
||||||
const target = findPointerDropTarget(event);
|
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) {
|
if (!target) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -635,14 +705,35 @@ function clearPointerDropTarget() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function findPointerDropTarget(event) {
|
function findPointerDropTarget(event) {
|
||||||
const element = document.elementFromPoint(event.clientX, event.clientY);
|
let element;
|
||||||
const windowEl = element?.closest?.('.window');
|
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;
|
if (!windowEl) return null;
|
||||||
|
|
||||||
const win = state.windows.find((item) => item.id === windowEl.dataset.windowId);
|
const win = state.windows.find((item) => item.id === windowEl.dataset.windowId);
|
||||||
if (!win || win.kind !== 'explorer') return null;
|
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') {
|
if (row?.dataset.type === 'directory') {
|
||||||
return { windowId: win.id, path: row.dataset.path, row };
|
return { windowId: win.id, path: row.dataset.path, row };
|
||||||
}
|
}
|
||||||
@@ -734,11 +825,18 @@ function startResizeWindow(event, win) {
|
|||||||
|
|
||||||
async function api(url, options) {
|
async function api(url, options) {
|
||||||
const response = await fetchChecked(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) {
|
async function fetchChecked(url, options) {
|
||||||
|
recordDebug('fetch.request', { url, method: options?.method || 'GET' });
|
||||||
const response = await fetch(url, options);
|
const response = await fetch(url, options);
|
||||||
|
recordDebug('fetch.response', { url, status: response.status, ok: response.ok });
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
let message = `${response.status} ${response.statusText}`;
|
let message = `${response.status} ${response.statusText}`;
|
||||||
try {
|
try {
|
||||||
@@ -797,3 +895,39 @@ function clamp(value, min, max) {
|
|||||||
function apiUrl(path) {
|
function apiUrl(path) {
|
||||||
return `${API_BASE.replace(/\/$/, '')}/${path.replace(/^\//, '')}`;
|
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;
|
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 {
|
.u-nav .job {
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
|||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
<!DOCTYPE PLUGIN [
|
<!DOCTYPE PLUGIN [
|
||||||
<!ENTITY name "u-navigator">
|
<!ENTITY name "u-navigator">
|
||||||
<!ENTITY author "michael">
|
<!ENTITY author "michael">
|
||||||
<!ENTITY version "2026.06.23.r004">
|
<!ENTITY version "2026.06.23.r005">
|
||||||
<!ENTITY package "&name;-&version;.tgz">
|
<!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">
|
<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;">
|
<FILE Name="/boot/config/plugins/&name;/&package;">
|
||||||
<URL>https://git.casaderoll.de/michael/Unraid-Navigator/raw/branch/main/packages/&package;</URL>
|
<URL>https://git.casaderoll.de/michael/Unraid-Navigator/raw/branch/main/packages/&package;</URL>
|
||||||
<MD5>da98c5f068d4f12dfddb0e7143a7aaa0</MD5>
|
<MD5>1e5eb8df508f7a4e46a4c13336302d16</MD5>
|
||||||
</FILE>
|
</FILE>
|
||||||
|
|
||||||
<FILE Run="/bin/bash">
|
<FILE Run="/bin/bash">
|
||||||
|
|||||||
Reference in New Issue
Block a user