Show the manual sync command it will actually run, and guard --delete
The preview ended in the literal text "…src host:dst", so the one thing worth checking before pressing Run — the real source and destination, and which of them has the trailing slash — was the one thing never shown. --delete sat unmarked among nine checkboxes with no confirmation on a card that can remove data on the far host. Confirm fires only for --delete without --dry-run, so it stays worth reading.
This commit is contained in:
@@ -186,7 +186,7 @@ require_once dirname(__DIR__) . '/include/ai_chat.php';
|
||||
<div>
|
||||
<div class="vv-ms-lbl" style="margin-bottom:5px;">Local source</div>
|
||||
<div style="display:flex;gap:5px;align-items:center;margin-bottom:4px;">
|
||||
<input class="vv-ms-inp" id="vv-ms-local" type="text" placeholder="/mnt/user/…"
|
||||
<input class="vv-ms-inp" id="vv-ms-local" type="text" placeholder="/mnt/user/…" oninput="vvMsUpdatePreview()"
|
||||
style="flex:1;min-width:0;" onkeydown="if(event.key==='Enter')vvMsBrowseLocal()">
|
||||
<button onclick="vvMsLocalNavUp()" title="Parent"
|
||||
style="background:#111;border:1px solid #222;color:#555;border-radius:3px;
|
||||
@@ -212,14 +212,14 @@ require_once dirname(__DIR__) . '/include/ai_chat.php';
|
||||
</div>
|
||||
<div style="flex:0 1 80px;">
|
||||
<div class="vv-ms-lbl" style="margin-bottom:3px;">User</div>
|
||||
<select class="vv-ms-sel" id="vv-ms-user">
|
||||
<select class="vv-ms-sel" id="vv-ms-user" onchange="vvMsUpdatePreview()">
|
||||
<option value="root">root</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="vv-ms-lbl" style="margin-bottom:5px;">Remote path</div>
|
||||
<div style="display:flex;gap:5px;align-items:center;margin-bottom:4px;">
|
||||
<input class="vv-ms-inp" id="vv-ms-rpath" type="text" placeholder="/mnt/user/…"
|
||||
<input class="vv-ms-inp" id="vv-ms-rpath" type="text" placeholder="/mnt/user/…" oninput="vvMsUpdatePreview()"
|
||||
style="flex:1;min-width:0;" onkeydown="if(event.key==='Enter')vvMsBrowse()">
|
||||
<button onclick="vvMsNavUp()" title="Parent"
|
||||
style="background:#111;border:1px solid #222;color:#555;border-radius:3px;
|
||||
@@ -256,8 +256,13 @@ require_once dirname(__DIR__) . '/include/ai_chat.php';
|
||||
<span style="font-size:9px;color:#2a2a2a;flex-shrink:0;">rsync</span>
|
||||
<span id="vv-ms-flags-preview"></span>
|
||||
<span id="vv-ms-bwlimit-preview" style="color:#2e4e2e;"></span>
|
||||
<span style="color:#2a2a2a;">…src host:dst</span>
|
||||
</div>
|
||||
<div style="margin-top:3px;background:#080808;border:1px solid #181818;border-radius:3px;
|
||||
padding:4px 8px;font-family:monospace;font-size:10px;color:#4a6a8a;
|
||||
word-break:break-all;" id="vv-ms-target-preview"></div>
|
||||
<div id="vv-ms-delete-warn" style="display:none;margin-top:3px;background:#1a0d0d;
|
||||
border:1px solid #3a1a1a;border-radius:3px;padding:4px 8px;font-size:10px;
|
||||
color:#ef5350;"></div>
|
||||
</div>
|
||||
|
||||
<!-- BW limit + SSH key -->
|
||||
@@ -1660,6 +1665,8 @@ function vvMsHostChanged() {
|
||||
const host = _vvMsHosts.find(h => h.slot === slot);
|
||||
const stat = document.getElementById('vv-ms-host-status');
|
||||
|
||||
vvMsUpdatePreview(); // the destination just changed, so the command shown must too
|
||||
|
||||
if (!host) { stat.textContent = ''; return; }
|
||||
|
||||
const onlineTxt = host.online === null ? 'status unknown'
|
||||
@@ -1720,6 +1727,14 @@ function vvMsBrowse() {
|
||||
});
|
||||
}
|
||||
|
||||
// The whole command, with the real paths in it, not "…src host:dst".
|
||||
//
|
||||
// Two reasons it has to resolve rather than gesture. The first is the trailing slash: this card
|
||||
// already warns that "/" means contents and no "/" means the folder, and a hint under a text box
|
||||
// is not where anyone looks at the moment they press Run — in the command it is unmissable. The
|
||||
// second is --delete, which is one checkbox among nine and is the only one that removes data at
|
||||
// the far end. Seeing it sit next to the actual destination is the difference between reading a
|
||||
// flag and understanding a consequence.
|
||||
function vvMsUpdatePreview() {
|
||||
const flags = Array.from(document.querySelectorAll('#vv-ms-card input[data-flag]:checked'))
|
||||
.map(el => el.dataset.flag).join(' ');
|
||||
@@ -1728,6 +1743,31 @@ function vvMsUpdatePreview() {
|
||||
const bwPrev= document.getElementById('vv-ms-bwlimit-preview');
|
||||
if (prev) prev.textContent = flags || '(none)';
|
||||
if (bwPrev) bwPrev.textContent = bw > 0 ? '--bwlimit=' + bw : '';
|
||||
|
||||
const local = document.getElementById('vv-ms-local')?.value.trim() || '';
|
||||
const rpath = document.getElementById('vv-ms-rpath')?.value.trim() || '';
|
||||
const user = document.getElementById('vv-ms-user')?.value || 'root';
|
||||
const hostEl = document.getElementById('vv-ms-host');
|
||||
const hostTxt = hostEl && hostEl.selectedIndex >= 0
|
||||
? (hostEl.options[hostEl.selectedIndex].textContent || '').trim() : '';
|
||||
const tgt = document.getElementById('vv-ms-target-preview');
|
||||
if (tgt) {
|
||||
tgt.textContent = (local || '<source>') + ' ' + user + '@' + (hostTxt || '<host>') + ':' + (rpath || '<dest>');
|
||||
}
|
||||
|
||||
// Destructive styling follows the checkbox, so the warning cannot be left on screen after the
|
||||
// flag is cleared — a stale danger marker is how a real one stops being read.
|
||||
const del = document.querySelector('#vv-ms-card input[data-flag="--delete"]');
|
||||
const dry = document.getElementById('vv-ms-dryrun');
|
||||
const warn = document.getElementById('vv-ms-delete-warn');
|
||||
if (warn) {
|
||||
const armed = del && del.checked && !(dry && dry.checked);
|
||||
warn.style.display = armed ? '' : 'none';
|
||||
if (armed) {
|
||||
warn.textContent = 'deletes anything at ' + (rpath || 'the destination')
|
||||
+ ' that is not in ' + (local || 'the source');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _vvMsRenderDirs(dirsElId, dirs, parent, navFn) {
|
||||
@@ -1815,7 +1855,7 @@ function vvMsBrowseLocal() {
|
||||
});
|
||||
}
|
||||
|
||||
function vvMsRun() {
|
||||
async function vvMsRun() {
|
||||
const local = document.getElementById('vv-ms-local').value.trim();
|
||||
const slot = document.getElementById('vv-ms-host').value;
|
||||
const rpath = document.getElementById('vv-ms-rpath').value.trim();
|
||||
@@ -1831,6 +1871,24 @@ function vvMsRun() {
|
||||
if (!slot) { vvAlert('Select a remote server.'); return; }
|
||||
if (!rpath) { vvAlert('Enter a remote destination path.'); return; }
|
||||
|
||||
// The only confirm on this card, and only for the one flag that destroys data at the far end.
|
||||
// Guarding every run would train the reflex that dismisses this one — a prompt that appears on
|
||||
// every safe action is not read by the time an unsafe one arrives.
|
||||
//
|
||||
// Skipped under --dry-run, because that is exactly the rehearsal this is asking for, and
|
||||
// refusing to let someone rehearse the dangerous case cheaply is the wrong lesson to teach.
|
||||
if (flags.includes('--delete') && !isDry) {
|
||||
const ok = await vvConfirm(
|
||||
'This deletes anything in ' + rpath + ' on ' + slot + ' that is not in ' + local + '.\n\n'
|
||||
+ (local.endsWith('/')
|
||||
? 'The source ends in "/", so its CONTENTS are compared against the destination.'
|
||||
: 'The source has no trailing "/", so the FOLDER ITSELF is copied into the destination — '
|
||||
+ 'a common cause of deleting the wrong level.')
|
||||
+ '\n\nRun a --dry-run first if you have not.',
|
||||
{ title: 'Run with --delete?', confirmText: 'Run it', type: 'warning' });
|
||||
if (!ok) return;
|
||||
}
|
||||
|
||||
const btn = document.getElementById('vv-ms-run-btn');
|
||||
const stat = document.getElementById('vv-ms-run-status');
|
||||
const out = document.getElementById('vv-ms-out');
|
||||
|
||||
Reference in New Issue
Block a user