diff --git a/build-plugin.sh b/build-plugin.sh
index b98b2b9..b1ad44c 100755
--- a/build-plugin.sh
+++ b/build-plugin.sh
@@ -2,7 +2,7 @@
set -eu
PLUGIN="u-navigator"
-VERSION="2026.06.23.r026"
+VERSION="2026.06.23.r027"
PKG_DIR="packages"
WORK_DIR=".plugin-build"
PKG_NAME="${PLUGIN}-${VERSION}.tgz"
diff --git a/packages/u-navigator-2026.06.23.r026.tgz b/packages/u-navigator-2026.06.23.r026.tgz
deleted file mode 100644
index 33ab2c0..0000000
Binary files a/packages/u-navigator-2026.06.23.r026.tgz and /dev/null differ
diff --git a/packages/u-navigator-2026.06.23.r027.tgz b/packages/u-navigator-2026.06.23.r027.tgz
new file mode 100644
index 0000000..ace2b04
Binary files /dev/null and b/packages/u-navigator-2026.06.23.r027.tgz differ
diff --git a/plugin-root/usr/local/emhttp/plugins/u-navigator/U-Navigator.page b/plugin-root/usr/local/emhttp/plugins/u-navigator/U-Navigator.page
index a71a65b..a533eeb 100644
--- a/plugin-root/usr/local/emhttp/plugins/u-navigator/U-Navigator.page
+++ b/plugin-root/usr/local/emhttp/plugins/u-navigator/U-Navigator.page
@@ -17,6 +17,7 @@ $version = "__VERSION__";
+
diff --git a/plugin-root/usr/local/emhttp/plugins/u-navigator/api/common.php b/plugin-root/usr/local/emhttp/plugins/u-navigator/api/common.php
index d7bb740..7661240 100644
--- a/plugin-root/usr/local/emhttp/plugins/u-navigator/api/common.php
+++ b/plugin-root/usr/local/emhttp/plugins/u-navigator/api/common.php
@@ -2,6 +2,39 @@
const UNAV_DEFAULT_ROOT = '/mnt/user';
const UNAV_JOB_DIR = '/tmp/u-navigator/jobs';
+const UNAV_SETTINGS_FILE = '/boot/config/plugins/u-navigator/settings.json';
+
+function unav_settings(): array {
+ $defaults = [
+ 'readOnly' => true,
+ 'debug' => false,
+ ];
+ if (!is_file(UNAV_SETTINGS_FILE)) {
+ return $defaults;
+ }
+ $settings = json_decode(file_get_contents(UNAV_SETTINGS_FILE) ?: '{}', true);
+ if (!is_array($settings)) {
+ return $defaults;
+ }
+ return array_merge($defaults, [
+ 'readOnly' => filter_var($settings['readOnly'] ?? $defaults['readOnly'], FILTER_VALIDATE_BOOLEAN),
+ 'debug' => filter_var($settings['debug'] ?? $defaults['debug'], FILTER_VALIDATE_BOOLEAN),
+ ]);
+}
+
+function unav_settings_write(array $settings): array {
+ $current = unav_settings();
+ $next = [
+ 'readOnly' => filter_var($settings['readOnly'] ?? $current['readOnly'], FILTER_VALIDATE_BOOLEAN),
+ 'debug' => filter_var($settings['debug'] ?? $current['debug'], FILTER_VALIDATE_BOOLEAN),
+ ];
+ $dir = dirname(UNAV_SETTINGS_FILE);
+ if (!is_dir($dir) && !mkdir($dir, 0777, true) && !is_dir($dir)) {
+ unav_error(500, 'Could not create settings directory');
+ }
+ file_put_contents(UNAV_SETTINGS_FILE, json_encode($next, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT), LOCK_EX);
+ return $next;
+}
function unav_roots(): array {
$configured = getenv('U_NAV_ROOTS') ?: UNAV_DEFAULT_ROOT;
@@ -29,7 +62,14 @@ function unav_roots(): array {
}
function unav_read_only(): bool {
- return getenv('U_NAV_READ_ONLY') === '1';
+ $env = getenv('U_NAV_READ_ONLY');
+ if ($env === '1') {
+ return true;
+ }
+ if ($env === '0') {
+ return false;
+ }
+ return unav_settings()['readOnly'];
}
function unav_function_available(string $name): bool {
diff --git a/plugin-root/usr/local/emhttp/plugins/u-navigator/api/config.php b/plugin-root/usr/local/emhttp/plugins/u-navigator/api/config.php
index 9bab61b..d645605 100644
--- a/plugin-root/usr/local/emhttp/plugins/u-navigator/api/config.php
+++ b/plugin-root/usr/local/emhttp/plugins/u-navigator/api/config.php
@@ -1,7 +1,19 @@
$settings['readOnly'],
+ 'debug' => $settings['debug'],
+ 'roots' => unav_roots(),
+ ]);
+}
+
+$settings = unav_settings();
unav_json([
'readOnly' => unav_read_only(),
+ 'debug' => $settings['debug'],
'roots' => unav_roots(),
]);
diff --git a/server/public/app.js b/server/public/app.js
index 7b21e9d..1ab5453 100644
--- a/server/public/app.js
+++ b/server/public/app.js
@@ -2,6 +2,7 @@ const workspace = document.querySelector('#workspace');
const appShell = document.querySelector('.u-nav');
const statusText = document.querySelector('#statusText');
const transferButton = document.querySelector('#newQueueButton');
+const settingsButton = document.querySelector('#settingsButton');
const API_BASE = window.UNAV_API_BASE || '/api';
const APP_VERSION = window.UNAV_VERSION || 'dev';
const state = {
@@ -23,6 +24,7 @@ const state = {
transferPanelVisible: false,
transferPanelClosing: false,
transferAutoHideTimer: null,
+ settingsPanelVisible: false,
nextWindowId: 1,
zIndex: 20
};
@@ -37,6 +39,7 @@ const icons = {
document.querySelector('#newExplorerButton').addEventListener('click', () => openExplorer());
document.querySelector('#newPropertiesButton').addEventListener('click', () => openProperties());
transferButton.addEventListener('click', () => toggleTransferPanel());
+settingsButton.addEventListener('click', () => toggleSettingsPanel());
window.addEventListener('error', (event) => {
recordDebug('window.error', {
message: event.message,
@@ -58,7 +61,7 @@ async function init() {
if (!state.config.roots?.length) {
throw new Error('No U-Navigator roots configured');
}
- statusText.textContent = `${state.config.roots.length} Root, ${state.config.readOnly ? 'Read-only' : 'Schreibzugriff aktiv'} · ${APP_VERSION}`;
+ updateStatusText();
openExplorer(state.config.roots[0].path);
} catch (error) {
statusText.textContent = error.message;
@@ -143,6 +146,7 @@ function render() {
workspace.appendChild(renderContextMenu());
}
renderTransferPanel();
+ renderSettingsPanel();
}
function renderWindow(win) {
@@ -213,11 +217,11 @@ function renderExplorer(body, win) {
-
-
+
+
-
-
+
+
@@ -378,6 +382,15 @@ function queueMarkup() {
`).join('') : '
Keine Transfers.
';
+ const debugSection = state.config?.debug ? debugMarkup() : '';
+
+ return `
+
${jobMarkup}
+ ${debugSection}
+ `;
+}
+
+function debugMarkup() {
const debugMarkup = state.debug.length ? state.debug.slice(-12).reverse().map((item) => `
#${item.id} ${escapeHtml(item.label)}
@@ -387,7 +400,6 @@ function queueMarkup() {
`).join('') : 'Noch keine Debug-Einträge.
';
return `
- ${jobMarkup}
Debug
@@ -495,6 +507,100 @@ function renderTransferPanel() {
transferButton.setAttribute('aria-expanded', 'true');
}
+function toggleSettingsPanel() {
+ state.settingsPanelVisible = !state.settingsPanelVisible;
+ renderSettingsPanel();
+}
+
+function renderSettingsPanel() {
+ const existing = appShell.querySelector('.settings-panel');
+ if (!state.settingsPanelVisible) {
+ existing?.remove();
+ settingsButton.classList.remove('active');
+ settingsButton.setAttribute('aria-expanded', 'false');
+ return;
+ }
+
+ const panel = existing || document.createElement('aside');
+ panel.className = 'settings-panel';
+ panel.setAttribute('aria-label', 'Einstellungen');
+ panel.innerHTML = `
+
+
+
+
+
+ `;
+ if (!existing) {
+ appShell.appendChild(panel);
+ }
+
+ const buttonRect = settingsButton.getBoundingClientRect();
+ const shellRect = appShell.getBoundingClientRect();
+ panel.style.right = `${Math.max(16, shellRect.right - buttonRect.right)}px`;
+ panel.style.top = `${buttonRect.bottom - shellRect.top + 8}px`;
+
+ panel.querySelector('[data-action="close-settings"]').addEventListener('click', () => {
+ state.settingsPanelVisible = false;
+ renderSettingsPanel();
+ });
+ panel.querySelector('[data-setting="debug"]').addEventListener('change', (event) => {
+ saveSettings({ debug: event.currentTarget.checked });
+ });
+ panel.querySelector('[data-setting="write"]').addEventListener('change', (event) => {
+ saveSettings({ readOnly: !event.currentTarget.checked });
+ });
+ settingsButton.classList.add('active');
+ settingsButton.setAttribute('aria-expanded', 'true');
+}
+
+async function saveSettings(changes) {
+ const body = new URLSearchParams();
+ const readOnly = changes.readOnly ?? state.config.readOnly;
+ const debug = changes.debug ?? state.config.debug;
+ body.set('readOnly', readOnly ? '1' : '0');
+ body.set('debug', debug ? '1' : '0');
+ appendCsrf(body);
+ try {
+ state.config = await api(apiUrl('config.php'), {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
+ body
+ });
+ updateStatusText();
+ if (!state.config.debug) {
+ state.debug = [];
+ }
+ render();
+ } catch (error) {
+ showError(error);
+ }
+}
+
+function updateStatusText() {
+ if (!state.config) return;
+ statusText.textContent = `${state.config.roots.length} Root, ${state.config.readOnly ? 'Read-only' : 'Schreibzugriff aktiv'} · Debug ${state.config.debug ? 'aktiv' : 'aus'} · ${APP_VERSION}`;
+}
+
+function isReadOnly() {
+ return Boolean(state.config?.readOnly);
+}
+
async function loadExplorer(win, targetPath) {
win.data.loading = true;
win.data.error = null;
@@ -539,6 +645,10 @@ async function handleDrop(event, win, targetPath) {
}
async function createJob(type, source, destination, refreshWindowIds = []) {
+ if (['move', 'copy'].includes(type) && isReadOnly()) {
+ showError(new Error('Read-only mode ist aktiv.'));
+ return null;
+ }
recordDebug('job.create.request', { type, source, destination, refreshWindowIds });
const body = new URLSearchParams();
body.set('source', source);
@@ -631,6 +741,10 @@ function setDropEffect(event, effect) {
}
async function renameEntry(win, entry) {
+ if (isReadOnly()) {
+ showError(new Error('Read-only mode ist aktiv.'));
+ return;
+ }
const nextName = prompt('Neuer Name', entry.name);
if (!nextName || nextName === entry.name) {
return;
@@ -643,6 +757,10 @@ async function renameEntry(win, entry) {
}
async function promptTransfer(win, entry, type) {
+ if (isReadOnly()) {
+ showError(new Error('Read-only mode ist aktiv.'));
+ return;
+ }
const targetDir = prompt(type === 'copy' ? 'Kopieren nach Ordner' : 'Verschieben nach Ordner', win.data.path);
if (!targetDir) {
return;
@@ -681,9 +799,11 @@ function renderContextMenu() {
actions.push(['Öffnen', () => loadExplorer(win, entry.path)]);
}
actions.push(['Eigenschaften', () => showPropertiesForEntry(win, entry)]);
- actions.push(['Umbenennen', () => renameEntry(win, entry)]);
- actions.push(['Kopieren nach...', () => promptTransfer(win, entry, 'copy')]);
- actions.push(['Verschieben nach...', () => promptTransfer(win, entry, 'move')]);
+ if (!isReadOnly()) {
+ actions.push(['Umbenennen', () => renameEntry(win, entry)]);
+ actions.push(['Kopieren nach...', () => promptTransfer(win, entry, 'copy')]);
+ actions.push(['Verschieben nach...', () => promptTransfer(win, entry, 'move')]);
+ }
} else {
actions.push(['Neu laden', () => loadExplorer(win, win.data.path)]);
}
@@ -1234,6 +1354,9 @@ function getCsrfToken() {
}
function recordDebug(label, data = {}) {
+ if (!state.config?.debug) {
+ return null;
+ }
const item = {
id: state.nextDebugId++,
time: new Date().toLocaleTimeString(),
diff --git a/server/public/styles.css b/server/public/styles.css
index e425c0d..620f78a 100644
--- a/server/public/styles.css
+++ b/server/public/styles.css
@@ -193,12 +193,12 @@
}
.u-nav .pathbar .up-button {
- flex: 0 0 38px;
+ flex: 0 0 32px;
font-size: 18px;
line-height: 1;
}
-.u-nav .pathbar > button:not(.icon),
+.u-nav .pathbar > button.icon,
.u-nav .pathbar .view-toggle {
flex: 0 0 auto;
}
@@ -225,7 +225,7 @@
.u-nav .view-toggle button {
border-radius: 0;
flex: 0 0 auto;
- min-width: 88px;
+ min-width: 32px;
overflow: visible;
}
@@ -535,6 +535,61 @@
padding: 10px;
}
+.u-nav .settings-panel {
+ animation: transfer-slide-down 160ms ease-out both;
+ background: var(--surface);
+ border: 1px solid var(--line);
+ border-radius: 8px;
+ box-shadow: var(--shadow);
+ min-width: 320px;
+ overflow: hidden;
+ position: absolute;
+ transform-origin: top right;
+ width: min(380px, calc(100% - 32px));
+ z-index: 10010;
+}
+
+.u-nav .settings-panel-header {
+ align-items: center;
+ background: var(--surface-2);
+ border-bottom: 1px solid var(--line);
+ display: flex;
+ justify-content: space-between;
+ min-height: 40px;
+ padding: 5px 8px 5px 12px;
+}
+
+.u-nav .settings-panel-body {
+ display: grid;
+ gap: 2px;
+ padding: 8px;
+}
+
+.u-nav .setting-row {
+ align-items: center;
+ border-radius: 6px;
+ display: grid;
+ gap: 14px;
+ grid-template-columns: 1fr auto;
+ padding: 10px;
+}
+
+.u-nav .setting-row:hover {
+ background: var(--surface-2);
+}
+
+.u-nav .setting-row small {
+ color: var(--muted);
+ display: block;
+ margin-top: 2px;
+}
+
+.u-nav .setting-row input {
+ accent-color: var(--accent);
+ height: 18px;
+ width: 18px;
+}
+
@keyframes transfer-slide-down {
from {
opacity: 0;
@@ -675,4 +730,11 @@
right: 10px !important;
width: auto !important;
}
+
+ .u-nav .settings-panel {
+ left: 10px !important;
+ min-width: 0;
+ right: 10px !important;
+ width: auto !important;
+ }
}
diff --git a/u-navigator.plg b/u-navigator.plg
index c17474e..d101dd9 100644
--- a/u-navigator.plg
+++ b/u-navigator.plg
@@ -1,7 +1,7 @@
-
+
@@ -24,7 +24,7 @@
https://git.casaderoll.de/michael/Unraid-Navigator/raw/branch/main/packages/&package;
-9af5add68cb11a97e1c88afae8621497
+145d13dcb4bf35ced2981b0e219cb3a0