rsync window cards: edit/add/remove scripts and sync shares with browse and profile picker
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
require_once dirname(__DIR__) . '/include/config.php';
|
||||
require_once dirname(__DIR__) . '/include/confform.php';
|
||||
require_once dirname(__DIR__) . '/include/common.php';
|
||||
|
||||
$action = $_GET['action'] ?? $_POST['action'] ?? '';
|
||||
|
||||
// ── Script library — all .sh files grouped by folder ─────────────────────────
|
||||
if ($action === 'list_scripts') {
|
||||
$base = rtrim(SCRIPTS_DIR, '/') . '/';
|
||||
$exclude = ['Plugin', '.git', 'Orchestrators', 'Custom', 'Configurations',
|
||||
'Deployment', 'State_Files', 'data', 'Old_Arch_Still_Works', 'Kernel'];
|
||||
$groups = [];
|
||||
try {
|
||||
$ri = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator(SCRIPTS_DIR, RecursiveDirectoryIterator::SKIP_DOTS)
|
||||
);
|
||||
foreach ($ri as $rf) {
|
||||
if (!$rf->isFile() || strtolower($rf->getExtension()) !== 'sh') continue;
|
||||
$rel = ltrim(str_replace($base, '', $rf->getPathname()), '/');
|
||||
$parts = explode('/', $rel);
|
||||
if (count($parts) < 2 || in_array($parts[0], $exclude)) continue;
|
||||
$folder = $parts[0];
|
||||
$label = str_replace('_', ' ', basename($rel, '.sh'));
|
||||
$groups[$folder][] = ['id' => $rel, 'label' => $label];
|
||||
}
|
||||
} catch (Exception $e) {}
|
||||
ksort($groups);
|
||||
foreach ($groups as &$g) usort($g, fn($a, $b) => strcmp($a['id'], $b['id']));
|
||||
echo json_encode(['ok' => true, 'groups' => $groups]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// ── Profile names from master.conf ────────────────────────────────────────────
|
||||
if ($action === 'list_profiles') {
|
||||
$raw = vv_read_conf_raw('master.conf');
|
||||
preg_match_all('/^\s*PROFILE_([A-Z0-9_]+)_RSYNC_OPTS\s*=/m', $raw, $m);
|
||||
$profiles = array_values(array_unique(
|
||||
array_map(fn($n) => strtolower(str_replace('_', '-', $n)), $m[1] ?? [])
|
||||
));
|
||||
echo json_encode(['ok' => true, 'profiles' => $profiles]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// ── Save scripts + shares for a window ───────────────────────────────────────
|
||||
if ($action === 'save' && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$winKey = trim($_POST['win_key'] ?? '');
|
||||
$scripts = json_decode($_POST['scripts'] ?? 'null', true);
|
||||
$shares = json_decode($_POST['shares'] ?? 'null', true);
|
||||
|
||||
$winMap = [
|
||||
'critical' => ['CRITICAL_MAINTENANCE_SCRIPTS', 'CRITICAL_SYNC_SHARES'],
|
||||
'intermediate' => ['INTERMEDIATE_MAINTENANCE_SCRIPTS', 'INTERMEDIATE_SYNC_SHARES'],
|
||||
'daily' => ['DAILY_MAINTENANCE_SCRIPTS', 'DAILY_SYNC_SHARES'],
|
||||
'weekly' => ['WEEKLY_MAINTENANCE_SCRIPTS', 'WEEKLY_SYNC_SHARES'],
|
||||
];
|
||||
|
||||
if (!isset($winMap[$winKey])) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Invalid window']); exit;
|
||||
}
|
||||
|
||||
[$scriptsVar, $sharesBase] = $winMap[$winKey];
|
||||
$myId = strtoupper(vv_detect_host());
|
||||
$sharesVar = "{$myId}_{$sharesBase}";
|
||||
$errors = [];
|
||||
|
||||
// ── Scripts → master.conf ─────────────────────────────────────────────────
|
||||
if (is_array($scripts)) {
|
||||
$confPath = CONF_DIR . '/master.conf';
|
||||
$lines = file($confPath, FILE_KEEP_BLANK_LINES) ?: [];
|
||||
$esc = preg_quote($scriptsVar, '/');
|
||||
$blockStart = $blockEnd = null;
|
||||
$depth = 0;
|
||||
$origLines = [];
|
||||
|
||||
foreach ($lines as $i => $line) {
|
||||
if ($blockStart === null) {
|
||||
if (preg_match('/^\s*' . $esc . '\s*=\s*\(/', $line)) { $blockStart = $i; $depth = 1; }
|
||||
continue;
|
||||
}
|
||||
$depth += substr_count($line, '(') - substr_count($line, ')');
|
||||
if ($depth <= 0) { $blockEnd = $i; break; }
|
||||
if (preg_match('/^\s*(?:#\s*)?"([^"]+)"/', $line, $m)) {
|
||||
$path = explode(' ', trim($m[1]))[0];
|
||||
if (str_ends_with($path, '.sh') && !isset($origLines[$path]))
|
||||
$origLines[$path] = '"' . $m[1] . '"';
|
||||
}
|
||||
}
|
||||
|
||||
if ($blockStart !== null && $blockEnd !== null) {
|
||||
$newBlock = [$lines[$blockStart]];
|
||||
foreach ($scripts as $item) {
|
||||
$id = trim((string)($item['id'] ?? ''));
|
||||
$enabled = !isset($item['enabled']) || (bool)$item['enabled'];
|
||||
if (!$id || str_contains($id, '..') || !preg_match('/^[A-Za-z0-9_.\-\/]+\.sh$/', $id)) continue;
|
||||
$entry = $origLines[$id] ?? '"' . $id . '"';
|
||||
$prefix = $enabled ? ' ' : ' #';
|
||||
$newBlock[] = $prefix . $entry . "\n";
|
||||
}
|
||||
$newBlock[] = $lines[$blockEnd];
|
||||
array_splice($lines, $blockStart, $blockEnd - $blockStart + 1, $newBlock);
|
||||
if (file_put_contents($confPath, implode('', $lines)) === false) $errors[] = 'scripts write failed';
|
||||
} else {
|
||||
$errors[] = "Array $scriptsVar not found in master.conf";
|
||||
}
|
||||
}
|
||||
|
||||
// ── Shares → host*.conf ───────────────────────────────────────────────────
|
||||
if (is_array($shares)) {
|
||||
$confFile = strtolower($myId) . '.conf';
|
||||
$inner = '';
|
||||
foreach ($shares as $item) {
|
||||
$path = trim((string)($item['path'] ?? ''));
|
||||
$profile = trim((string)($item['profile'] ?? ''));
|
||||
if (!$path || str_contains($path, '..') || !str_starts_with($path, '/')) continue;
|
||||
$val = $profile ? "{$path}|{$profile}" : $path;
|
||||
$inner .= ' "' . $val . '"' . "\n";
|
||||
}
|
||||
$results = vv_conf_write_changes([[
|
||||
'file' => $confFile,
|
||||
'key' => $sharesVar,
|
||||
'value' => rtrim($inner),
|
||||
'type' => 'array',
|
||||
]]);
|
||||
if (in_array(false, $results, true)) $errors[] = 'shares write failed';
|
||||
}
|
||||
|
||||
echo json_encode(['ok' => empty($errors), 'errors' => $errors]);
|
||||
exit;
|
||||
}
|
||||
|
||||
echo json_encode(['ok' => false, 'error' => 'Unknown action']);
|
||||
+335
-36
@@ -468,6 +468,57 @@
|
||||
</div><!-- /layer -->
|
||||
|
||||
<script>
|
||||
// Shared state — globals so IIFE internals and onclick handlers can both reach them
|
||||
var _vvRyWinOpen = new Set(); // which window panels are expanded
|
||||
var _vvRyLastData = null; // last successful poll response
|
||||
var _vvRyEditMode = false; // true = skip poll re-renders
|
||||
var _vvRyWinEditing = null; // key of window currently in edit mode
|
||||
var _vvRyWinEState = {}; // {key: {scripts:[{id,enabled}], shares:[{path,profile}]}}
|
||||
var _vvRyScriptLib = null; // cached {Folder:[{id,label}]} — null = not yet loaded
|
||||
var _vvRyProfiles = null; // cached profile name list
|
||||
|
||||
// View-mode panel inner HTML — shared by _windowsRow (inside IIFE) and vvRyWinCancel (outside)
|
||||
function _vvRyViewPanelInner(key, cfg) {
|
||||
const scripts = cfg.scripts || [];
|
||||
const shares = cfg.shares || [];
|
||||
let inner = '';
|
||||
|
||||
if (scripts.length) {
|
||||
inner += `<div style="font-size:9px;font-weight:700;color:#2a2a2a;text-transform:uppercase;letter-spacing:.07em;margin-bottom:4px;">Scripts</div>`;
|
||||
for (const s of scripts) {
|
||||
const parts = s.split('/');
|
||||
const folder = parts.length > 1 ? parts.slice(0, -1).join('/') : '';
|
||||
const name = parts[parts.length - 1].replace(/\.sh$/, '');
|
||||
inner += `<div style="font-size:10px;color:#555;padding:1px 0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">
|
||||
${folder ? `<span style="color:#2a2a2a;">${folder}/</span>` : ''}<span>${name}</span>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
if (shares.length) {
|
||||
if (scripts.length) inner += `<div style="border-top:1px solid #161616;margin:6px 0;"></div>`;
|
||||
inner += `<div style="font-size:9px;font-weight:700;color:#2a2a2a;text-transform:uppercase;letter-spacing:.07em;margin-bottom:4px;">Sync shares</div>`;
|
||||
for (const sh of shares) {
|
||||
const [rawPath, profile] = sh.split('|');
|
||||
const basename = rawPath.trim().split('/').pop();
|
||||
inner += `<div style="font-size:10px;color:#555;padding:1px 0;display:flex;align-items:center;gap:5px;min-width:0;">
|
||||
<span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;" title="${rawPath.trim()}">${basename}</span>
|
||||
${profile ? `<span style="font-size:9px;color:#2a3a2a;flex-shrink:0;font-family:monospace;">${profile.trim()}</span>` : ''}
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
if (scripts.length || shares.length) {
|
||||
inner += `<div style="margin-top:7px;padding-top:6px;border-top:1px solid #161616;">
|
||||
<button onclick="event.stopPropagation();vvRyWinEdit('${key}')"
|
||||
style="background:#0a1a2a;border:1px solid #1a3a5a;color:#4a9eff;font-size:10px;
|
||||
padding:2px 10px;border-radius:3px;cursor:pointer;">⚙ Edit</button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
return inner;
|
||||
}
|
||||
|
||||
(function() {
|
||||
|
||||
const WIN_META = {
|
||||
@@ -591,40 +642,10 @@ function _windowsRow(data) {
|
||||
<div class="vv-ry-win-dur">${_rel(ls.ts)}${ls.duration ? ' · ' + _dur(ls.duration) : ''}</div>`;
|
||||
}
|
||||
|
||||
let panelHtml = '';
|
||||
if (scripts.length || shares.length) {
|
||||
let inner = '';
|
||||
|
||||
if (scripts.length) {
|
||||
inner += `<div style="font-size:9px;font-weight:700;color:#2a2a2a;text-transform:uppercase;letter-spacing:.07em;margin-bottom:4px;">Scripts</div>`;
|
||||
for (const s of scripts) {
|
||||
const parts = s.split('/');
|
||||
const folder = parts.length > 1 ? parts.slice(0, -1).join('/') : '';
|
||||
const name = parts[parts.length - 1].replace(/\.sh$/, '');
|
||||
inner += `<div style="font-size:10px;color:#555;padding:1px 0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">
|
||||
${folder ? `<span style="color:#2a2a2a;">${folder}/</span>` : ''}<span>${name}</span>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
if (shares.length) {
|
||||
if (scripts.length) inner += `<div style="border-top:1px solid #161616;margin:6px 0;"></div>`;
|
||||
inner += `<div style="font-size:9px;font-weight:700;color:#2a2a2a;text-transform:uppercase;letter-spacing:.07em;margin-bottom:4px;">Sync shares</div>`;
|
||||
for (const sh of shares) {
|
||||
const [rawPath, profile] = sh.split('|');
|
||||
const path = rawPath.trim();
|
||||
const basename = path.split('/').pop();
|
||||
inner += `<div style="font-size:10px;color:#555;padding:1px 0;display:flex;align-items:center;gap:5px;min-width:0;">
|
||||
<span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;" title="${path}">${basename}</span>
|
||||
${profile ? `<span style="font-size:9px;color:#2a3a2a;flex-shrink:0;font-family:monospace;">${profile.trim()}</span>` : ''}
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
panelHtml = `<div class="vv-ry-win-panel" style="display:none;margin-top:9px;padding-top:8px;border-top:1px solid #1e1e1e;">${inner}</div>`;
|
||||
}
|
||||
|
||||
const expandable = !!(scripts.length || shares.length);
|
||||
const panelHtml = expandable
|
||||
? `<div class="vv-ry-win-panel" style="display:none;margin-top:9px;padding-top:8px;border-top:1px solid #1e1e1e;">${_vvRyViewPanelInner(key, cfg)}</div>`
|
||||
: '';
|
||||
|
||||
return `<div class="vv-ry-win ${cls}" data-win-key="${key}"
|
||||
${expandable ? 'onclick="vvRyWinExpand(this)" style="cursor:pointer;"' : ''}>
|
||||
@@ -787,11 +808,11 @@ function _settingsSection(data) {
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// Track which window panels are open so they survive poll re-renders
|
||||
const _vvRyWinOpen = new Set();
|
||||
|
||||
// ── Main render ───────────────────────────────────────────────────────────────
|
||||
function _render(data) {
|
||||
_vvRyLastData = data;
|
||||
if (_vvRyEditMode) return; // don't clobber the edit UI during a poll
|
||||
|
||||
let html = '';
|
||||
html += _statusCard(data);
|
||||
html += _lastSyncCard(data);
|
||||
@@ -842,6 +863,284 @@ function vvRyWinExpand(card) {
|
||||
if (key) open ? _vvRyWinOpen.delete(key) : _vvRyWinOpen.add(key);
|
||||
}
|
||||
|
||||
// ── Window card edit ──────────────────────────────────────────────────────────
|
||||
|
||||
function vvRyWinEdit(key) {
|
||||
const cfg = (_vvRyLastData?.win_arrays || {})[key] || {};
|
||||
_vvRyWinEState[key] = {
|
||||
scripts: (cfg.scripts || []).map(s => ({id: s, enabled: true})),
|
||||
shares: (cfg.shares || []).map(s => {
|
||||
const [p, pr] = s.split('|');
|
||||
return {path: p.trim(), profile: (pr || '').trim()};
|
||||
}),
|
||||
};
|
||||
_vvRyEditMode = true;
|
||||
_vvRyWinEditing = key;
|
||||
|
||||
const go = () => _vvRyBuildEditPanel(key);
|
||||
if (!_vvRyScriptLib) {
|
||||
fetch('/plugins/varaverk/api/rsync_win_arrays.php?action=list_scripts&_=' + Date.now())
|
||||
.then(r => r.json()).then(d => { if (d.ok) _vvRyScriptLib = d.groups; go(); }).catch(go);
|
||||
} else { go(); }
|
||||
}
|
||||
|
||||
function vvRyWinCancel(key) {
|
||||
_vvRyEditMode = false;
|
||||
_vvRyWinEditing = null;
|
||||
const cfg = (_vvRyLastData?.win_arrays || {})[key] || {};
|
||||
const panel = document.querySelector(`.vv-ry-win[data-win-key="${key}"] .vv-ry-win-panel`);
|
||||
if (panel) { panel.innerHTML = _vvRyViewPanelInner(key, cfg); panel.style.display = ''; }
|
||||
const chev = document.querySelector(`.vv-ry-win[data-win-key="${key}"] .vv-ry-win-chev`);
|
||||
if (chev) chev.textContent = '▾';
|
||||
}
|
||||
|
||||
function vvRyWinSave(key) {
|
||||
const state = _vvRyWinEState[key];
|
||||
if (!state) return;
|
||||
const btn = document.getElementById('vv-ry-esave-' + key);
|
||||
const fb = document.getElementById('vv-ry-efb-' + key);
|
||||
if (btn) { btn.disabled = true; btn.textContent = 'Saving…'; }
|
||||
if (fb) fb.textContent = '';
|
||||
|
||||
const fd = new FormData();
|
||||
fd.append('action', 'save');
|
||||
fd.append('win_key', key);
|
||||
fd.append('scripts', JSON.stringify(state.scripts));
|
||||
fd.append('shares', JSON.stringify(state.shares));
|
||||
|
||||
fetch('/plugins/varaverk/api/rsync_win_arrays.php', {method:'POST', body:fd})
|
||||
.then(r => r.json())
|
||||
.then(d => {
|
||||
if (d.ok) {
|
||||
// Update cached data so cancel/re-render shows the new values
|
||||
if (_vvRyLastData) {
|
||||
_vvRyLastData.win_arrays ??= {};
|
||||
_vvRyLastData.win_arrays[key] = {
|
||||
scripts: state.scripts.map(s => s.id),
|
||||
shares: state.shares.map(s => s.profile ? `${s.path}|${s.profile}` : s.path),
|
||||
};
|
||||
}
|
||||
vvRyWinCancel(key);
|
||||
} else {
|
||||
if (btn) { btn.disabled = false; btn.textContent = 'Save changes'; }
|
||||
if (fb) { fb.style.color = '#ef5350'; fb.textContent = (d.errors || ['Save failed']).join(', '); }
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
if (btn) { btn.disabled = false; btn.textContent = 'Save changes'; }
|
||||
if (fb) { fb.style.color = '#ef5350'; fb.textContent = 'Request failed'; }
|
||||
});
|
||||
}
|
||||
|
||||
// ── Edit panel builder ────────────────────────────────────────────────────────
|
||||
function _vvRyBuildEditPanel(key) {
|
||||
const panel = document.querySelector(`.vv-ry-win[data-win-key="${key}"] .vv-ry-win-panel`);
|
||||
if (!panel) return;
|
||||
panel.style.display = '';
|
||||
const state = _vvRyWinEState[key] || {scripts:[], shares:[]};
|
||||
const scripts = state.scripts;
|
||||
const shares = state.shares;
|
||||
let h = '';
|
||||
|
||||
// ── Scripts list ─────────────────────────────────────────────────────────────
|
||||
h += `<div style="font-size:9px;font-weight:700;color:#2a2a2a;text-transform:uppercase;letter-spacing:.07em;margin-bottom:5px;">Scripts</div>`;
|
||||
for (let i = 0; i < scripts.length; i++) {
|
||||
const s = scripts[i];
|
||||
const parts = s.id.split('/');
|
||||
const dir = parts.length > 1 ? parts.slice(0,-1).join('/') : '';
|
||||
const name = parts[parts.length-1].replace(/\.sh$/,'');
|
||||
h += `<div style="display:flex;align-items:center;gap:3px;padding:2px 0;border-bottom:1px solid #111;">
|
||||
<button onclick="event.stopPropagation();vvRyERemScript('${key}',${i})"
|
||||
style="background:none;border:none;color:#4a1a1a;cursor:pointer;padding:0 3px;font-size:13px;line-height:1;flex-shrink:0;">×</button>
|
||||
<div style="display:flex;flex-direction:column;flex-shrink:0;">
|
||||
<button onclick="event.stopPropagation();vvRyEMvScript('${key}',${i},-1)" ${i===0?'disabled':''}
|
||||
style="background:none;border:none;color:${i===0?'#1e1e1e':'#3a5a3a'};cursor:${i===0?'default':'pointer'};padding:0;font-size:8px;line-height:1.2;">▲</button>
|
||||
<button onclick="event.stopPropagation();vvRyEMvScript('${key}',${i},1)" ${i===scripts.length-1?'disabled':''}
|
||||
style="background:none;border:none;color:${i===scripts.length-1?'#1e1e1e':'#3a5a3a'};cursor:${i===scripts.length-1?'default':'pointer'};padding:0;font-size:8px;line-height:1.2;">▼</button>
|
||||
</div>
|
||||
<span style="font-size:10px;color:#666;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;">
|
||||
${dir?`<span style="color:#2a2a2a;">${dir}/</span>`:''}${name}
|
||||
</span>
|
||||
</div>`;
|
||||
}
|
||||
// Script add row
|
||||
const existIds = new Set(scripts.map(s => s.id));
|
||||
h += `<div style="display:flex;gap:4px;margin-top:5px;align-items:center;" onclick="event.stopPropagation()">
|
||||
<select id="vv-ry-esel-${key}"
|
||||
style="background:#0d0d0d;border:1px solid #1e1e1e;border-radius:3px;color:#666;
|
||||
font-size:10px;padding:3px 5px;flex:1;outline:none;min-width:0;">
|
||||
<option value="">— add script —</option>`;
|
||||
if (_vvRyScriptLib) {
|
||||
for (const [folder, items] of Object.entries(_vvRyScriptLib)) {
|
||||
const avail = items.filter(it => !existIds.has(it.id));
|
||||
if (!avail.length) continue;
|
||||
h += `<optgroup label="${folder}">`;
|
||||
for (const it of avail) h += `<option value="${it.id}">${it.label}</option>`;
|
||||
h += `</optgroup>`;
|
||||
}
|
||||
}
|
||||
h += `</select>
|
||||
<button onclick="event.stopPropagation();vvRyEAddScript('${key}')"
|
||||
style="background:#0a1a0a;border:1px solid #1a3a1a;color:#4caf50;font-size:10px;
|
||||
padding:3px 9px;border-radius:3px;cursor:pointer;flex-shrink:0;">+ Add</button>
|
||||
</div>`;
|
||||
|
||||
// ── Shares list ───────────────────────────────────────────────────────────────
|
||||
h += `<div style="border-top:1px solid #161616;margin:8px 0;"></div>
|
||||
<div style="font-size:9px;font-weight:700;color:#2a2a2a;text-transform:uppercase;letter-spacing:.07em;margin-bottom:5px;">Sync shares</div>`;
|
||||
for (let i = 0; i < shares.length; i++) {
|
||||
const sh = shares[i];
|
||||
const bn = sh.path.split('/').pop();
|
||||
h += `<div style="display:flex;align-items:center;gap:3px;padding:2px 0;border-bottom:1px solid #111;">
|
||||
<button onclick="event.stopPropagation();vvRyERemShare('${key}',${i})"
|
||||
style="background:none;border:none;color:#4a1a1a;cursor:pointer;padding:0 3px;font-size:13px;line-height:1;flex-shrink:0;">×</button>
|
||||
<div style="display:flex;flex-direction:column;flex-shrink:0;">
|
||||
<button onclick="event.stopPropagation();vvRyEMvShare('${key}',${i},-1)" ${i===0?'disabled':''}
|
||||
style="background:none;border:none;color:${i===0?'#1e1e1e':'#3a5a3a'};cursor:${i===0?'default':'pointer'};padding:0;font-size:8px;line-height:1.2;">▲</button>
|
||||
<button onclick="event.stopPropagation();vvRyEMvShare('${key}',${i},1)" ${i===shares.length-1?'disabled':''}
|
||||
style="background:none;border:none;color:${i===shares.length-1?'#1e1e1e':'#3a5a3a'};cursor:${i===shares.length-1?'default':'pointer'};padding:0;font-size:8px;line-height:1.2;">▼</button>
|
||||
</div>
|
||||
<span style="font-size:10px;color:#666;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="${sh.path}">${bn}</span>
|
||||
${sh.profile?`<span style="font-size:9px;color:#2a3a2a;flex-shrink:0;font-family:monospace;">${sh.profile}</span>`:''}
|
||||
</div>`;
|
||||
}
|
||||
// Share add form
|
||||
h += `<div style="margin-top:6px;" onclick="event.stopPropagation()">
|
||||
<div style="display:flex;gap:4px;margin-bottom:3px;">
|
||||
<input id="vv-ry-epath-${key}" type="text" placeholder="/mnt/user/…" autocomplete="off"
|
||||
style="background:#0d0d0d;border:1px solid #1e1e1e;border-radius:3px;color:#888;
|
||||
font-size:10px;padding:3px 6px;flex:1;min-width:0;font-family:monospace;outline:none;"
|
||||
onkeydown="if(event.key==='Enter')vvRyEBrowse('${key}')">
|
||||
<button onclick="vvRyEBrowse('${key}')"
|
||||
style="background:#0a1a2a;border:1px solid #1a3a5a;color:#4a9eff;font-size:10px;
|
||||
padding:3px 8px;border-radius:3px;cursor:pointer;flex-shrink:0;white-space:nowrap;">⟳ Browse</button>
|
||||
</div>
|
||||
<div id="vv-ry-ebrowse-${key}" class="vv-ms-dirs" style="display:none;max-height:120px;" data-last-path=""></div>
|
||||
<div style="display:flex;gap:4px;margin-top:4px;align-items:center;">
|
||||
<select id="vv-ry-eprof-${key}"
|
||||
style="background:#0d0d0d;border:1px solid #1e1e1e;border-radius:3px;color:#666;
|
||||
font-size:10px;padding:3px 5px;flex:1;min-width:0;outline:none;">
|
||||
<option value="">no profile</option>
|
||||
</select>
|
||||
<button onclick="vvRyEAddShare('${key}')"
|
||||
style="background:#0a1a0a;border:1px solid #1a3a1a;color:#4caf50;font-size:10px;
|
||||
padding:3px 9px;border-radius:3px;cursor:pointer;flex-shrink:0;white-space:nowrap;">+ Add</button>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
// ── Save / Cancel ─────────────────────────────────────────────────────────────
|
||||
h += `<div style="display:flex;gap:6px;align-items:center;margin-top:10px;padding-top:8px;border-top:1px solid #1e1e1e;">
|
||||
<button id="vv-ry-esave-${key}" onclick="event.stopPropagation();vvRyWinSave('${key}')"
|
||||
style="background:#1a2a1a;border:1px solid #2d4a2d;color:#4caf50;font-size:11px;
|
||||
padding:4px 14px;border-radius:3px;cursor:pointer;">Save changes</button>
|
||||
<button onclick="event.stopPropagation();vvRyWinCancel('${key}')"
|
||||
style="background:#111;border:1px solid #222;color:#555;font-size:11px;
|
||||
padding:4px 12px;border-radius:3px;cursor:pointer;">Cancel</button>
|
||||
<span id="vv-ry-efb-${key}" style="font-size:10px;color:#ef5350;"></span>
|
||||
</div>`;
|
||||
|
||||
panel.innerHTML = h;
|
||||
_vvRyLoadProfiles(key);
|
||||
}
|
||||
|
||||
// ── Script mutations ──────────────────────────────────────────────────────────
|
||||
function vvRyEMvScript(key, idx, dir) {
|
||||
const s = _vvRyWinEState[key]?.scripts; if (!s) return;
|
||||
const n = idx + dir; if (n < 0 || n >= s.length) return;
|
||||
[s[idx], s[n]] = [s[n], s[idx]]; _vvRyBuildEditPanel(key);
|
||||
}
|
||||
function vvRyERemScript(key, idx) {
|
||||
const s = _vvRyWinEState[key]?.scripts; if (!s) return;
|
||||
s.splice(idx, 1); _vvRyBuildEditPanel(key);
|
||||
}
|
||||
function vvRyEAddScript(key) {
|
||||
const sel = document.getElementById('vv-ry-esel-' + key);
|
||||
if (!sel?.value) return;
|
||||
const s = _vvRyWinEState[key]?.scripts; if (!s) return;
|
||||
if (s.find(x => x.id === sel.value)) return;
|
||||
s.push({id: sel.value, enabled: true}); _vvRyBuildEditPanel(key);
|
||||
}
|
||||
|
||||
// ── Share mutations ───────────────────────────────────────────────────────────
|
||||
function vvRyEMvShare(key, idx, dir) {
|
||||
const s = _vvRyWinEState[key]?.shares; if (!s) return;
|
||||
const n = idx + dir; if (n < 0 || n >= s.length) return;
|
||||
[s[idx], s[n]] = [s[n], s[idx]]; _vvRyBuildEditPanel(key);
|
||||
}
|
||||
function vvRyERemShare(key, idx) {
|
||||
const s = _vvRyWinEState[key]?.shares; if (!s) return;
|
||||
s.splice(idx, 1); _vvRyBuildEditPanel(key);
|
||||
}
|
||||
function vvRyEAddShare(key) {
|
||||
const path = document.getElementById('vv-ry-epath-' + key)?.value.trim();
|
||||
const prof = document.getElementById('vv-ry-eprof-' + key)?.value.trim() || '';
|
||||
if (!path || !path.startsWith('/')) return;
|
||||
const s = _vvRyWinEState[key]?.shares; if (!s) return;
|
||||
s.push({path, profile: prof});
|
||||
document.getElementById('vv-ry-epath-' + key).value = '';
|
||||
const dirsEl = document.getElementById('vv-ry-ebrowse-' + key);
|
||||
if (dirsEl) dirsEl.style.display = 'none';
|
||||
_vvRyBuildEditPanel(key);
|
||||
}
|
||||
|
||||
// ── Share path browser ────────────────────────────────────────────────────────
|
||||
function vvRyEBrowse(key) {
|
||||
const pathEl = document.getElementById('vv-ry-epath-' + key);
|
||||
const dirsEl = document.getElementById('vv-ry-ebrowse-' + key);
|
||||
if (!pathEl || !dirsEl) return;
|
||||
const path = pathEl.value.trim() || '/mnt/user';
|
||||
|
||||
if (dirsEl.dataset.lastPath === path && dirsEl.style.display !== 'none') {
|
||||
dirsEl.style.display = 'none'; dirsEl.dataset.lastPath = ''; return;
|
||||
}
|
||||
dirsEl.innerHTML = '<div style="color:#444;font-size:10px;padding:4px;">Loading…</div>';
|
||||
dirsEl.style.display = '';
|
||||
|
||||
fetch(`/plugins/varaverk/api/manual_sync.php?action=browse_local&path=${encodeURIComponent(path)}&_=${Date.now()}`)
|
||||
.then(r => r.json())
|
||||
.then(d => {
|
||||
if (!d.ok) { dirsEl.innerHTML = `<div class="vv-ms-dir" style="color:#ef5350;cursor:default;">${d.error}</div>`; return; }
|
||||
if (pathEl) pathEl.value = d.path;
|
||||
dirsEl.dataset.lastPath = d.path;
|
||||
let h = '';
|
||||
if (d.parent) {
|
||||
const pn = d.parent === '/' ? '/' : (d.parent.replace(/^.*\//,'') || d.parent) + '/';
|
||||
h += `<div class="vv-ms-dir parent" onclick="vvRyENavTo('${key}',${JSON.stringify(d.parent)})">↑ ${pn}</div>`;
|
||||
}
|
||||
if (!d.dirs.length) {
|
||||
h += '<div class="vv-ms-dir" style="color:#2a2a2a;cursor:default;">— empty —</div>';
|
||||
} else {
|
||||
for (const dir of d.dirs) {
|
||||
const name = dir.replace(/^.*\//,'') || dir;
|
||||
h += `<div class="vv-ms-dir" onclick="vvRyENavTo('${key}',${JSON.stringify(dir)})" title="${dir}">▶ ${name}</div>`;
|
||||
}
|
||||
}
|
||||
dirsEl.innerHTML = h;
|
||||
})
|
||||
.catch(() => { dirsEl.innerHTML = '<div class="vv-ms-dir" style="color:#ef5350;cursor:default;">Browse failed</div>'; });
|
||||
}
|
||||
|
||||
function vvRyENavTo(key, path) {
|
||||
const pathEl = document.getElementById('vv-ry-epath-' + key);
|
||||
if (pathEl) pathEl.value = path;
|
||||
vvRyEBrowse(key);
|
||||
}
|
||||
|
||||
// ── Profile dropdown loader ───────────────────────────────────────────────────
|
||||
function _vvRyLoadProfiles(key) {
|
||||
const sel = document.getElementById('vv-ry-eprof-' + key);
|
||||
if (!sel) return;
|
||||
const populate = () => {
|
||||
sel.innerHTML = '<option value="">no profile</option>' +
|
||||
(_vvRyProfiles || []).map(p => `<option value="${p}">${p}</option>`).join('');
|
||||
};
|
||||
if (_vvRyProfiles !== null) { populate(); return; }
|
||||
fetch('/plugins/varaverk/api/rsync_win_arrays.php?action=list_profiles&_=' + Date.now())
|
||||
.then(r => r.json())
|
||||
.then(d => { _vvRyProfiles = d.ok ? (d.profiles || []) : []; populate(); })
|
||||
.catch(() => { _vvRyProfiles = []; populate(); });
|
||||
}
|
||||
|
||||
// ── Toggle a boolean flag — called by inline onclick ──────────────────────────
|
||||
function vvRyToggle(trackEl, name) {
|
||||
const on = !trackEl.classList.contains('on');
|
||||
|
||||
Reference in New Issue
Block a user