Add frontend debug tracing

This commit is contained in:
Mikei386
2026-06-23 06:56:26 +02:00
parent 3fec83cf73
commit 068973ab9b
6 changed files with 197 additions and 26 deletions
+1 -1
View File
@@ -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.
+157 -23
View File
@@ -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,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) };
}
+37
View File
@@ -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
View File
@@ -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">