Move Custom Scripts out of the repo and add an Import Script picker

Custom Scripts (the Scheduler page's inline editor) used to save into the
git-tracked Custom/ folder, so anything saved there would end up on GitHub.
They now live in /boot/config/plugins/user.scripts/Varaverk/Scripts, same
folder family as Unraid's own User Scripts plugin. Import Script lets you
browse the whole server and move an existing script in instead of only
creating new ones inline — always a move, never a copy, so no stray
duplicate is left where it came from.
This commit is contained in:
Gmer4Lfe
2026-07-03 10:57:35 -04:00
parent 93aa134aaa
commit 6fd22ae4ee
7 changed files with 352 additions and 10 deletions
+191 -2
View File
@@ -272,7 +272,7 @@ $runningScripts = array_unique($runningScripts);
</div>
<div class="vv-children" id="vv-custom-children" style="display:none;">
<?php if (empty($customs)): ?>
<p class="vv-custom-empty">No custom scripts yet — click <strong>+ Add Script</strong> to create one.</p>
<p class="vv-custom-empty">No custom scripts yet — click <strong>+ Create Script</strong> to create one.</p>
<?php else: ?>
<?php // ── Folder groups ──────────────────────────────────────────────
@@ -362,7 +362,8 @@ $runningScripts = array_unique($runningScripts);
</div><!-- /#vv-sched-cards -->
<div class="vv-sched-footer">
<button class="vv-save-btn vv-add-script-btn" onclick="vvAddScript()">+ Add Script</button>
<button class="vv-save-btn vv-add-script-btn" onclick="vvAddScript()">+ Create Script</button>
<button class="vv-save-btn vv-import-script-btn" onclick="vvImportScriptOpen()" title="Move an existing script from anywhere on the server into Custom Scripts">+ Import Script</button>
<button class="vv-save-btn vv-new-folder-btn" onclick="vvNewFolder()" title="Create a folder in Custom Scripts">+ Folder</button>
<button id="vv-arrange-btn" class="vv-save-btn" onclick="vvToggleArrange()" title="Drag scripts between orchestrators">Arrange</button>
<button id="vv-arrange-save-btn" class="vv-save-btn vv-arrange-save-btn" onclick="vvSaveArrange()" style="display:none">Save Arrangement</button>
@@ -1044,6 +1045,9 @@ function vvLH() {
let vvSetupConf = <?= json_encode($vv_setup_conf) ?>;
const vvLocalHostConf = <?= json_encode($_vv_local_host_conf) ?>;
// Where Custom Scripts (Create + Import) actually live — shown in the Import Script dialog
window.__vvCustomScriptsDir = <?= json_encode(CUSTOM_SCRIPTS_DIR) ?>;
if (vvSetupConf) {
// Auto-open the setup conf file once the page is ready
document.addEventListener('DOMContentLoaded', () => {
@@ -1718,6 +1722,191 @@ function vvDeleteScript() {
.catch(() => { btn.disabled = false; btn.textContent = '\u{1F5D1} Delete'; });
}
// ── Import Script — browse the whole server, move a chosen .sh into Custom Scripts ──
let _vvImpSelected = null; // full path of the currently-selected file, or null
function vvImportScriptOpen() {
let modal = document.getElementById('vv-import-modal');
if (!modal) modal = _vvImpBuildModal();
modal.style.display = 'flex';
_vvImpSelected = null;
_vvImpUpdateFooter();
_vvImpBrowse('/');
}
function vvImportScriptClose() {
const modal = document.getElementById('vv-import-modal');
if (modal) modal.style.display = 'none';
_vvImpSelected = null;
}
function _vvImpBuildModal() {
const modal = document.createElement('div');
modal.id = 'vv-import-modal';
modal.style.cssText = 'display:none;position:fixed;inset:0;z-index:9000;background:rgba(0,0,0,.6);'
+ 'align-items:center;justify-content:center;';
modal.addEventListener('mousedown', e => { if (e.target === modal) vvImportScriptClose(); });
const box = document.createElement('div');
box.style.cssText = 'background:#111;border:1px solid #222;border-radius:6px;width:560px;max-width:92vw;'
+ 'max-height:80vh;display:flex;flex-direction:column;overflow:hidden;';
box.innerHTML = `
<div style="padding:10px 14px;border-bottom:1px solid #1e1e1e;display:flex;align-items:center;justify-content:space-between;">
<span style="font-size:13px;font-weight:bold;color:#ccc;">Import Script</span>
<button onclick="vvImportScriptClose()" style="background:none;border:none;color:#666;cursor:pointer;font-size:14px;">&#10005;</button>
</div>
<div style="padding:10px 14px;border-bottom:1px solid #1e1e1e;">
<div style="font-size:10px;color:#555;margin-bottom:6px;">
Moves the selected script into Custom Scripts (<code>${_vvImpEsc(window.__vvCustomScriptsDir || '')}</code>).
The original is removed once the copy is verified.
</div>
<div style="display:flex;gap:6px;align-items:center;">
<input id="vv-imp-path" type="text" value="/" style="flex:1;min-width:0;background:#0a0a0a;border:1px solid #222;
color:#aaa;border-radius:3px;padding:4px 8px;font-size:11px;font-family:monospace;"
onkeydown="if(event.key==='Enter')_vvImpBrowse(document.getElementById('vv-imp-path').value.trim()||'/')">
<button onclick="_vvImpNavUp()" title="Parent" style="background:#111;border:1px solid #222;color:#555;
border-radius:3px;padding:4px 8px;cursor:pointer;font-size:12px;flex-shrink:0;">&#8593;</button>
<button onclick="_vvImpBrowse(document.getElementById('vv-imp-path').value.trim()||'/')" style="background:#111;
border:1px solid #222;color:#4a9eff;border-radius:3px;padding:4px 9px;cursor:pointer;font-size:11px;
white-space:nowrap;flex-shrink:0;">&#8635; Browse</button>
</div>
<span id="vv-imp-err" style="font-size:9px;color:#ef5350;display:none;margin-top:4px;"></span>
</div>
<div id="vv-imp-list" style="flex:1;overflow-y:auto;background:#080808;min-height:220px;"></div>
<div style="padding:10px 14px;border-top:1px solid #1e1e1e;display:flex;align-items:center;justify-content:space-between;gap:10px;">
<span id="vv-imp-selected" style="font-size:10px;color:#555;font-family:monospace;flex:1;min-width:0;
overflow:hidden;text-overflow:ellipsis;white-space:nowrap;"></span>
<div style="display:flex;gap:6px;flex-shrink:0;">
<button onclick="vvImportScriptClose()" class="vv-btn-sm">Cancel</button>
<button id="vv-imp-do-btn" class="vv-btn-sm vv-save-script-btn-style" disabled onclick="_vvImpDoImport()">Import</button>
</div>
</div>
`;
modal.appendChild(box);
document.body.appendChild(modal);
return modal;
}
function _vvImpEsc(s) {
const d = document.createElement('div');
d.textContent = s;
return d.innerHTML;
}
function _vvImpBrowse(path) {
const listEl = document.getElementById('vv-imp-list');
const errEl = document.getElementById('vv-imp-err');
const pathEl = document.getElementById('vv-imp-path');
errEl.style.display = 'none';
listEl.innerHTML = '<div style="padding:10px;color:#444;font-size:11px;">Loading…</div>';
fetch(`/plugins/varaverk/api/import_script.php?action=browse&path=${encodeURIComponent(path)}&_=${Date.now()}`)
.then(r => r.json())
.then(d => {
if (!d.ok) {
listEl.innerHTML = '';
errEl.textContent = d.error || 'Browse failed';
errEl.style.display = '';
return;
}
pathEl.value = d.path;
_vvImpRender(d);
})
.catch(e => {
listEl.innerHTML = '';
errEl.textContent = 'Request failed: ' + e;
errEl.style.display = '';
});
}
function _vvImpNavUp() {
const cur = (document.getElementById('vv-imp-path').value || '/').replace(/\/$/, '') || '/';
const up = cur === '/' ? '/' : (cur.substring(0, cur.lastIndexOf('/')) || '/');
_vvImpBrowse(up);
}
function _vvImpRender(d) {
const listEl = document.getElementById('vv-imp-list');
let html = '';
if (d.parent !== null) {
const pname = d.parent === '/' ? '/' : (d.parent.replace(/^.*\//, '') || d.parent) + '/';
html += `<div class="vv-imp-row" onclick="_vvImpBrowse(${JSON.stringify(d.parent)})"
style="padding:5px 12px;font-size:11px;color:#444;font-style:italic;cursor:pointer;font-family:monospace;">&#8593; ${pname}</div>`;
}
for (const dir of d.dirs) {
const name = dir.replace(/^.*\//, '') || dir;
html += `<div class="vv-imp-row" onclick="_vvImpBrowse(${JSON.stringify(dir)})" title="${_vvImpEsc(dir)}"
style="padding:5px 12px;font-size:11px;color:#666;cursor:pointer;font-family:monospace;">&#9654; ${_vvImpEsc(name)}</div>`;
}
for (const file of d.files) {
const name = file.replace(/^.*\//, '') || file;
const sel = file === _vvImpSelected;
html += `<div class="vv-imp-row vv-imp-file${sel ? ' vv-imp-file-sel' : ''}" data-path="${_vvImpEsc(file)}"
onclick="_vvImpSelectFile(${JSON.stringify(file)})" title="${_vvImpEsc(file)}"
style="padding:5px 12px;font-size:11px;cursor:pointer;font-family:monospace;
color:${sel ? '#4caf50' : '#4a9eff'};background:${sel ? '#0f1f0f' : 'transparent'};">&#128196; ${_vvImpEsc(name)}</div>`;
}
if (!d.dirs.length && !d.files.length) {
html += '<div style="padding:10px 12px;color:#333;font-size:11px;">— empty —</div>';
}
listEl.innerHTML = html;
}
function _vvImpSelectFile(path) {
_vvImpSelected = path;
_vvImpUpdateFooter();
// Re-render just the highlight without a re-fetch.
document.querySelectorAll('#vv-imp-list .vv-imp-file').forEach(el => {
const isSel = el.dataset.path === path;
el.classList.toggle('vv-imp-file-sel', isSel);
el.style.color = isSel ? '#4caf50' : '#4a9eff';
el.style.background = isSel ? '#0f1f0f' : 'transparent';
});
}
function _vvImpUpdateFooter() {
const sel = document.getElementById('vv-imp-selected');
const btn = document.getElementById('vv-imp-do-btn');
if (!sel || !btn) return;
sel.textContent = _vvImpSelected || '';
btn.disabled = !_vvImpSelected;
}
function _vvImpDoImport() {
if (!_vvImpSelected) return;
const dest = (window.__vvCustomScriptsDir || 'Custom Scripts') + '/' + _vvImpSelected.replace(/^.*\//, '');
if (!confirm('Move\n ' + _vvImpSelected + '\n→ ' + dest + '\n\nThe original will be deleted once the copy is verified. Continue?')) return;
const btn = document.getElementById('vv-imp-do-btn');
btn.disabled = true;
btn.textContent = 'Importing…';
vvPost('/plugins/varaverk/api/import_script.php', {action: 'import', path: _vvImpSelected})
.then(d => {
if (!d.ok) {
alert('Import failed: ' + (d.error || 'Unknown error'));
btn.disabled = false;
btn.textContent = 'Import';
return;
}
if (d.warning) alert(d.warning);
window.location.reload();
})
.catch(e => {
alert('Import failed: ' + e);
btn.disabled = false;
btn.textContent = 'Import';
});
}
function vvShowConfMode(title) {
document.getElementById('vv-suggestions').style.display = 'none';
document.getElementById('vv-log-pre').style.display = 'none';