Add storage mode migration: internal NVMe vs USB flash, Settings tab, fix hardcoded /boot/ paths
This commit is contained in:
@@ -24,9 +24,9 @@ unset($_master, $_h1m, $_host1_blank, $_my_hostid, $_conf_missing);
|
||||
|
||||
// Determine active tab
|
||||
$tab = $_GET['tab'] ?? 'monitor';
|
||||
$validTabs = ['monitor', 'scheduler', 'docker', 'watchdog', 'partnership', 'fallback', 'arrs', 'rsync'];
|
||||
$validTabs = ['monitor', 'scheduler', 'docker', 'watchdog', 'partnership', 'fallback', 'arrs', 'rsync', 'settings'];
|
||||
if (!in_array($tab, $validTabs)) $tab = 'monitor';
|
||||
$tabLabels = ['monitor' => 'Monitor', 'scheduler' => 'Scheduler', 'docker' => 'Docker', 'watchdog' => 'Watchdog', 'partnership' => 'Partnership', 'fallback' => 'FallBack', 'arrs' => 'Arrs', 'rsync' => 'Rsync'];
|
||||
$tabLabels = ['monitor' => 'Monitor', 'scheduler' => 'Scheduler', 'docker' => 'Docker', 'watchdog' => 'Watchdog', 'partnership' => 'Partnership', 'fallback' => 'FallBack', 'arrs' => 'Arrs', 'rsync' => 'Rsync', 'settings' => 'Settings'];
|
||||
?>
|
||||
|
||||
<link rel="stylesheet" href="/plugins/<?=$plugin?>/css/varaverk.css">
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
require_once dirname(__DIR__) . '/include/config.php';
|
||||
|
||||
$action = $_GET['action'] ?? $_POST['action'] ?? '';
|
||||
|
||||
// ── Boot device detection ─────────────────────────────────────────────────────
|
||||
function vv_storage_detect_transport(): string {
|
||||
$part = trim(shell_exec("findmnt -n -o SOURCE /boot 2>/dev/null") ?: '');
|
||||
if (!$part) return 'unknown';
|
||||
$disk = trim(shell_exec("lsblk -no pkname " . escapeshellarg($part) . " 2>/dev/null") ?: '');
|
||||
if (!$disk) return 'unknown';
|
||||
return strtolower(trim(shell_exec("lsblk -dno TRAN /dev/" . escapeshellarg($disk) . " 2>/dev/null") ?: 'unknown'));
|
||||
}
|
||||
|
||||
// ── Current mode status ───────────────────────────────────────────────────────
|
||||
if ($action === 'status') {
|
||||
$transport = vv_storage_detect_transport();
|
||||
$detected = ($transport === 'usb') ? 'flash' : 'internal';
|
||||
$currentDir = SCRIPTS_DIR;
|
||||
$internalDir = '/boot/config/plugins/varaverk';
|
||||
$flashDir = '/mnt/user/appdata/Varaverk';
|
||||
$currentMode = ($currentDir === $internalDir) ? 'internal'
|
||||
: ($currentDir === $flashDir ? 'flash' : 'custom');
|
||||
|
||||
$myHost = vv_detect_host();
|
||||
$vars = vv_conf_vars();
|
||||
$confKey = strtoupper($myHost) . '_STORAGE_MODE_INTERNAL';
|
||||
$confVal = $vars[$confKey] ?? null;
|
||||
|
||||
// Boot device name for display
|
||||
$bootPart = trim(shell_exec("findmnt -n -o SOURCE /boot 2>/dev/null") ?: '');
|
||||
$bootDisk = $bootPart ? trim(shell_exec("lsblk -no pkname " . escapeshellarg($bootPart) . " 2>/dev/null") ?: '') : '';
|
||||
|
||||
echo json_encode([
|
||||
'ok' => true,
|
||||
'current_mode' => $currentMode,
|
||||
'current_dir' => $currentDir,
|
||||
'internal_dir' => $internalDir,
|
||||
'flash_dir' => $flashDir,
|
||||
'transport' => $transport,
|
||||
'detected' => $detected,
|
||||
'boot_disk' => $bootDisk ? '/dev/' . $bootDisk : 'unknown',
|
||||
'conf_key' => $confKey,
|
||||
'conf_val' => $confVal,
|
||||
'array_started'=> is_dir('/mnt/user') && count(scandir('/mnt/user')) > 2,
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// ── Run migration ─────────────────────────────────────────────────────────────
|
||||
if ($action === 'migrate' && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$to = trim($_POST['to'] ?? '');
|
||||
if (!in_array($to, ['internal', 'flash'], true)) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Invalid target: must be internal or flash']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$script = SCRIPTS_DIR . '/Tools/storage_migrate.sh';
|
||||
if (!file_exists($script)) {
|
||||
echo json_encode(['ok' => false, 'error' => 'storage_migrate.sh not found']);
|
||||
exit;
|
||||
}
|
||||
|
||||
set_time_limit(300);
|
||||
$output = [];
|
||||
$exit = 0;
|
||||
exec('bash ' . escapeshellarg($script) . ' --to=' . escapeshellarg($to) . ' 2>&1', $output, $exit);
|
||||
|
||||
echo json_encode([
|
||||
'ok' => $exit === 0,
|
||||
'exit' => $exit,
|
||||
'output' => implode("\n", $output),
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// ── Auto-detect and write to conf ─────────────────────────────────────────────
|
||||
if ($action === 'detect' && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$transport = vv_storage_detect_transport();
|
||||
$detected = ($transport === 'usb') ? 'false' : 'true';
|
||||
$myHost = vv_detect_host();
|
||||
$confKey = strtoupper($myHost) . '_STORAGE_MODE_INTERNAL';
|
||||
$confFile = $myHost . '.conf';
|
||||
|
||||
$results = vv_conf_write_changes([[
|
||||
'file' => $confFile,
|
||||
'key' => $confKey,
|
||||
'value' => $detected,
|
||||
'type' => 'scalar',
|
||||
]]);
|
||||
|
||||
$ok = !in_array(false, $results, true);
|
||||
echo json_encode(['ok' => $ok, 'detected' => $detected, 'transport' => $transport]);
|
||||
exit;
|
||||
}
|
||||
|
||||
echo json_encode(['ok' => false, 'error' => 'Unknown action']);
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
require_once __DIR__ . '/config.php';
|
||||
|
||||
define('VV_DOCKER_JSON', '/boot/config/plugins/varaverk/docker_folders.json');
|
||||
define('VV_DOCKER_JSON', SCRIPTS_DIR . '/docker_folders.json');
|
||||
define('VV_FV3_JSON', '/boot/config/plugins/folder.view3/docker.json');
|
||||
|
||||
// ── JSON helpers ──────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
require_once __DIR__ . '/config.php';
|
||||
require_once __DIR__ . '/confform.php';
|
||||
|
||||
define('SCHEDULE_FILE', '/boot/config/plugins/varaverk/schedule.json');
|
||||
define('CRON_FILE', '/boot/config/plugins/varaverk/varaverk.cron');
|
||||
define('SCHEDULE_FILE', SCRIPTS_DIR . '/schedule.json');
|
||||
define('CRON_FILE', '/boot/config/plugins/varaverk/varaverk.cron'); // must stay in /boot — update_cron scans there
|
||||
|
||||
function vv_pretty_label(string $slug): string {
|
||||
return ucwords(str_replace('_', ' ', $slug));
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
<?php
|
||||
$_myHost = vv_detect_host();
|
||||
$_vars = vv_conf_vars();
|
||||
$_confKey = strtoupper($_myHost) . '_STORAGE_MODE_INTERNAL';
|
||||
$_confVal = $_vars[$_confKey] ?? null;
|
||||
?>
|
||||
<style>
|
||||
.vv-set-card { background:#161616;border:1px solid #2a2a2a;border-radius:6px;padding:14px 16px;margin-bottom:14px; }
|
||||
.vv-set-hdr { font-size:11px;font-weight:700;color:#666;text-transform:uppercase;letter-spacing:.07em;margin-bottom:12px; }
|
||||
.vv-set-row { display:flex;justify-content:space-between;align-items:baseline;gap:8px;margin:4px 0; }
|
||||
.vv-set-lbl { font-size:11px;color:#444; }
|
||||
.vv-set-val { font-size:12px;color:#888;text-align:right;font-family:monospace; }
|
||||
.vv-set-badge { display:inline-block;font-size:10px;padding:2px 8px;border-radius:3px;font-weight:700;letter-spacing:.04em; }
|
||||
.vv-set-badge.internal { background:#0d1f0d;color:#4caf50;border:1px solid #1a3a1a; }
|
||||
.vv-set-badge.flash { background:#1a1200;color:#ffb74d;border:1px solid #3a2800; }
|
||||
.vv-set-badge.custom { background:#0a1a2a;color:#4a9eff;border:1px solid #1a3a5a; }
|
||||
.vv-set-sep { border:none;border-top:1px solid #1e1e1e;margin:10px 0; }
|
||||
.vv-set-btn { background:#1a1a1a;border:1px solid #333;color:#888;font-size:11px;padding:5px 14px;
|
||||
border-radius:3px;cursor:pointer;transition:border-color .15s,color .15s; }
|
||||
.vv-set-btn:hover { border-color:#555;color:#ccc; }
|
||||
.vv-set-btn:disabled { opacity:.4;cursor:default; }
|
||||
.vv-set-btn.primary { background:#1a2a1a;border-color:#2d4a2d;color:#4caf50; }
|
||||
.vv-set-btn.primary:hover { background:#223a22; }
|
||||
.vv-set-btn.warn { background:#1f1500;border-color:#3a2800;color:#ffb74d; }
|
||||
.vv-set-btn.warn:hover { background:#2a1e00; }
|
||||
.vv-set-out { background:#080808;border:1px solid #1a1a1a;border-radius:3px;padding:10px 12px;
|
||||
font-family:monospace;font-size:10px;color:#555;white-space:pre-wrap;word-break:break-all;
|
||||
max-height:320px;overflow-y:auto;margin-top:10px;display:none;
|
||||
scrollbar-width:none; }
|
||||
.vv-set-out::-webkit-scrollbar { display:none; }
|
||||
.vv-set-info { font-size:11px;color:#444;line-height:1.6;margin-bottom:10px; }
|
||||
.vv-set-warn-box { background:#1a1200;border:1px solid #3a2800;border-radius:4px;
|
||||
padding:8px 12px;font-size:11px;color:#ffb74d;margin-top:8px;display:none; }
|
||||
</style>
|
||||
|
||||
<div style="max-width:740px;margin:0 auto;">
|
||||
|
||||
<!-- Storage Location card -->
|
||||
<div class="vv-set-card" id="vv-stor-card">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
|
||||
<span class="vv-set-hdr" style="margin-bottom:0;">Storage Location</span>
|
||||
<span id="vv-stor-badge" class="vv-set-badge">…</span>
|
||||
</div>
|
||||
|
||||
<div id="vv-stor-info" style="font-size:11px;color:#3a3a3a;">Loading…</div>
|
||||
|
||||
<hr class="vv-set-sep">
|
||||
|
||||
<div class="vv-set-info">
|
||||
Varaverk stores all scripts, configuration, state files, and the git repository in
|
||||
<code style="font-size:10px;color:#4a7a4a;background:#080808;padding:1px 4px;border-radius:2px;">SCRIPTS_DIR</code>.
|
||||
<br>
|
||||
<strong style="color:#555;">Internal NVMe/SSD</strong> — direct access, zero write-wear concern, git pull/push from boot volume.<br>
|
||||
<strong style="color:#555;">USB Flash</strong> — preserves flash lifetime. Requires array to be started. git pull syncs
|
||||
<code style="font-size:10px;color:#4a7a4a;background:#080808;padding:1px 4px;border-radius:2px;">Plugin/</code>
|
||||
back to <code style="font-size:10px;color:#4a7a4a;background:#080808;padding:1px 4px;border-radius:2px;">/boot/</code>
|
||||
after each pull so the webUI always stays current.
|
||||
</div>
|
||||
|
||||
<div id="vv-stor-warn-flash" class="vv-set-warn-box">
|
||||
Array must be started for Varaverk to function in flash mode. The plugin tab is always accessible.
|
||||
</div>
|
||||
<div id="vv-stor-warn-noarray" class="vv-set-warn-box">
|
||||
Array is not started — cannot migrate to flash mode right now. Start the array first.
|
||||
</div>
|
||||
|
||||
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-top:10px;">
|
||||
<button id="vv-stor-migrate-btn" class="vv-set-btn" onclick="vvStorMigrate()" style="display:none;"></button>
|
||||
<button class="vv-set-btn" onclick="vvStorLoad()" style="font-size:10px;padding:4px 10px;">↻ Refresh</button>
|
||||
<span id="vv-stor-fb" style="font-size:11px;color:#444;"></span>
|
||||
</div>
|
||||
|
||||
<pre class="vv-set-out" id="vv-stor-out"></pre>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var _vvStorData = null;
|
||||
|
||||
function vvStorLoad() {
|
||||
const info = document.getElementById('vv-stor-info');
|
||||
const badge = document.getElementById('vv-stor-badge');
|
||||
if (info) info.textContent = 'Loading…';
|
||||
|
||||
fetch('/plugins/varaverk/api/storage.php?action=status&_=' + Date.now())
|
||||
.then(r => r.json())
|
||||
.then(d => {
|
||||
_vvStorData = d;
|
||||
if (!d.ok) { if (info) info.textContent = 'Error loading status'; return; }
|
||||
_vvStorRender(d);
|
||||
})
|
||||
.catch(() => { if (info) info.textContent = 'Request failed'; });
|
||||
}
|
||||
|
||||
function _vvStorRender(d) {
|
||||
const badge = document.getElementById('vv-stor-badge');
|
||||
const info = document.getElementById('vv-stor-info');
|
||||
const btn = document.getElementById('vv-stor-migrate-btn');
|
||||
const wFlash = document.getElementById('vv-stor-warn-flash');
|
||||
const wArr = document.getElementById('vv-stor-warn-noarray');
|
||||
|
||||
// Badge
|
||||
if (badge) {
|
||||
badge.textContent = d.current_mode.toUpperCase();
|
||||
badge.className = 'vv-set-badge ' + d.current_mode;
|
||||
}
|
||||
|
||||
// Info grid
|
||||
const modeLabel = d.current_mode === 'internal' ? 'Internal NVMe/SSD' : d.current_mode === 'flash' ? 'USB Flash' : 'Custom';
|
||||
const detLabel = d.detected === 'internal' ? 'internal NVMe/SSD' : 'USB flash';
|
||||
const confBadge = d.conf_val === null ? '<span style="color:#3a3a3a;">not set</span>'
|
||||
: d.conf_val === 'true' ? '<span style="color:#4caf50;">true (internal)</span>'
|
||||
: '<span style="color:#ffb74d;">false (flash)</span>';
|
||||
const matchIcon = (d.current_mode === d.detected || d.current_mode === 'custom') ? '' :
|
||||
' <span style="color:#ef5350;">⚠ mismatch with conf</span>';
|
||||
|
||||
if (info) info.innerHTML = `
|
||||
<div style="display:grid;grid-template-columns:auto 1fr;gap:3px 16px;align-items:baseline;">
|
||||
<span style="color:#333;">Mode</span> <span style="color:#888;">${modeLabel}${matchIcon}</span>
|
||||
<span style="color:#333;">SCRIPTS_DIR</span> <code style="font-size:10px;color:#4a7a4a;">${d.current_dir}</code>
|
||||
<span style="color:#333;">Boot device</span> <span style="color:#555;">${d.boot_disk} <span style="color:#2a2a2a;">(${d.transport})</span></span>
|
||||
<span style="color:#333;">Auto-detect</span> <span style="color:#555;">${detLabel}</span>
|
||||
<span style="color:#333;">${d.conf_key}</span> <span>${confBadge}</span>
|
||||
</div>`;
|
||||
|
||||
// Warnings
|
||||
if (wFlash) wFlash.style.display = (d.current_mode !== 'flash' && d.detected === 'usb') ? '' : 'none';
|
||||
if (wArr) wArr.style.display = 'none';
|
||||
|
||||
// Migrate button
|
||||
if (btn) {
|
||||
if (d.current_mode === 'internal') {
|
||||
btn.textContent = 'Migrate to Flash (appdata)';
|
||||
btn.className = 'vv-set-btn warn';
|
||||
btn.dataset.to = 'flash';
|
||||
btn.style.display = '';
|
||||
} else if (d.current_mode === 'flash') {
|
||||
btn.textContent = 'Migrate to Internal (/boot)';
|
||||
btn.className = 'vv-set-btn primary';
|
||||
btn.dataset.to = 'internal';
|
||||
btn.style.display = '';
|
||||
} else {
|
||||
btn.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function vvStorMigrate() {
|
||||
const btn = document.getElementById('vv-stor-migrate-btn');
|
||||
const out = document.getElementById('vv-stor-out');
|
||||
const fb = document.getElementById('vv-stor-fb');
|
||||
const wArr = document.getElementById('vv-stor-warn-noarray');
|
||||
const to = btn?.dataset.to;
|
||||
if (!to) return;
|
||||
|
||||
// Flash guard: array must be started
|
||||
if (to === 'flash' && _vvStorData && !_vvStorData.array_started) {
|
||||
if (wArr) wArr.style.display = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const label = to === 'flash' ? 'Flash (appdata)' : 'Internal (/boot)';
|
||||
if (!confirm(`Migrate Varaverk storage to ${label}?\n\nThis will:\n• Copy all scripts, conf, and git repo to the new location\n• Update varaverk.cfg and master.conf\n• Delete the old location\n\nThe page will need a reload after migration.`)) return;
|
||||
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Migrating…';
|
||||
if (fb) fb.textContent = '';
|
||||
if (out) { out.textContent = 'Starting migration…\n'; out.style.display = ''; }
|
||||
|
||||
const fd = new FormData();
|
||||
fd.append('action', 'migrate');
|
||||
fd.append('to', to);
|
||||
|
||||
fetch('/plugins/varaverk/api/storage.php', { method: 'POST', body: fd })
|
||||
.then(r => r.json())
|
||||
.then(d => {
|
||||
btn.disabled = false;
|
||||
if (out) {
|
||||
out.textContent = d.output || '(no output)';
|
||||
out.scrollTop = out.scrollHeight;
|
||||
}
|
||||
if (d.ok) {
|
||||
btn.textContent = '✓ Done — reload page';
|
||||
btn.className = 'vv-set-btn primary';
|
||||
btn.onclick = () => location.reload();
|
||||
if (fb) { fb.style.color = '#4caf50'; fb.textContent = 'Migration complete — reload to apply'; }
|
||||
} else {
|
||||
btn.textContent = btn.dataset.to === 'flash' ? 'Migrate to Flash (appdata)' : 'Migrate to Internal (/boot)';
|
||||
if (fb) { fb.style.color = '#ef5350'; fb.textContent = 'Migration failed — see output above'; }
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
btn.disabled = false;
|
||||
btn.textContent = btn.dataset.to === 'flash' ? 'Migrate to Flash (appdata)' : 'Migrate to Internal (/boot)';
|
||||
if (fb) { fb.style.color = '#ef5350'; fb.textContent = 'Request failed: ' + e; }
|
||||
});
|
||||
}
|
||||
|
||||
vvStorLoad();
|
||||
</script>
|
||||
@@ -44,7 +44,9 @@ if [[ "$MANUAL" == false && -f "$MANUAL_TS_FILE" ]]; then
|
||||
# Only suppress for interval-based crons (*/N * * * *).
|
||||
# Static schedules like "30 2 * * 0" run at their appointed time and are never suppressed.
|
||||
INTERVAL=0
|
||||
SCHEDULE_FILE="/boot/config/plugins/varaverk/schedule.json"
|
||||
_VV_CFG="/boot/config/plugins/varaverk/varaverk.cfg"
|
||||
_SCRIPTS_DIR=$(grep -m1 '^SCRIPTS_DIR' "$_VV_CFG" 2>/dev/null | cut -d= -f2 | tr -d '"'"'" | tr -d '[:space:]')
|
||||
SCHEDULE_FILE="${_SCRIPTS_DIR:-/boot/config/plugins/varaverk}/schedule.json"
|
||||
if [[ -f "$SCHEDULE_FILE" ]]; then
|
||||
CRON_EXPR=$(php -r "
|
||||
\$s = json_decode(file_get_contents('$SCHEDULE_FILE'), true) ?: [];
|
||||
|
||||
Reference in New Issue
Block a user