Fix JS syntax errors: convert qq{} to heredocs in restore.live.cgi

Perl's qq{} delimiter matches balanced braces, which conflicted with
JavaScript curly braces, producing empty function bodies. Converted
_print_step1_js and _print_step2_js to heredoc blocks.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
shuki
2026-03-05 02:16:58 +02:00
parent d26a327595
commit 8822f93438

View File

@@ -232,83 +232,85 @@ sub handle_step1 {
sub _print_step1_js {
my ($esc_type, $next_step) = @_;
print qq{<script>\n};
print qq{var gnizaType = '$esc_type';\n};
print qq{var gnizaNextStep = '$next_step';\n};
print qq{\n};
print qq{function gnizaLoadSnapshots() {\n};
print qq{ var remote = document.getElementById('remote').value;\n};
print qq{ var sel = document.getElementById('timestamp');\n};
print qq{ var btn = document.getElementById('next-btn');\n};
print qq{\n};
print qq{ if (!remote) {\n};
print qq{ _setSelectPlaceholder(sel, '-- Select remote first --');\n};
print qq{ sel.disabled = true;\n};
print qq{ btn.disabled = true;\n};
print qq{ return;\n};
print qq{ }\n};
print qq{\n};
print qq{ _setSelectPlaceholder(sel, 'Loading...');\n};
print qq{ sel.disabled = true;\n};
print qq{ btn.disabled = true;\n};
print qq{\n};
print qq{ var url = 'restore.live.cgi?step=fetch_snapshots&remote=' + encodeURIComponent(remote);\n};
print qq{ var xhr = new XMLHttpRequest();\n};
print qq{ xhr.open('GET', url, true);\n};
print qq{ xhr.onreadystatechange = function() {\n};
print qq{ if (xhr.readyState !== 4) return;\n};
print qq{ if (xhr.status === 200) {\n};
print qq{ try {\n};
print qq{ var data = JSON.parse(xhr.responseText);\n};
print qq{ if (data.error) {\n};
print qq{ _setSelectPlaceholder(sel, 'Error: ' + data.error);\n};
print qq{ } else if (data.snapshots && data.snapshots.length > 0) {\n};
print qq{ _populateSelect(sel, data.snapshots);\n};
print qq{ sel.disabled = false;\n};
print qq{ btn.disabled = false;\n};
print qq{ } else {\n};
print qq{ _setSelectPlaceholder(sel, 'No snapshots found');\n};
print qq{ }\n};
print qq{ } catch(e) {\n};
print qq{ _setSelectPlaceholder(sel, 'Failed to parse response');\n};
print qq{ }\n};
print qq{ } else {\n};
print qq{ _setSelectPlaceholder(sel, 'Request failed');\n};
print qq{ }\n};
print qq{ };\n};
print qq{ xhr.send();\n};
print qq{}\n};
print qq{\n};
print qq{function _setSelectPlaceholder(sel, text) {\n};
print qq{ while (sel.options.length) sel.remove(0);\n};
print qq{ var opt = document.createElement('option');\n};
print qq{ opt.value = '';\n};
print qq{ opt.textContent = text;\n};
print qq{ sel.appendChild(opt);\n};
print qq{}\n};
print qq{\n};
print qq{function _populateSelect(sel, values) {\n};
print qq{ while (sel.options.length) sel.remove(0);\n};
print qq{ for (var i = 0; i < values.length; i++) {\n};
print qq{ var opt = document.createElement('option');\n};
print qq{ opt.value = values[i];\n};
print qq{ opt.textContent = values[i];\n};
print qq{ sel.appendChild(opt);\n};
print qq{ }\n};
print qq{}\n};
print qq{\n};
print qq{function gnizaGoNext() {\n};
print qq{ var remote = document.getElementById('remote').value;\n};
print qq{ var timestamp = document.getElementById('timestamp').value;\n};
print qq{ if (!remote || !timestamp) return;\n};
print qq{\n};
print qq{ var url = 'restore.live.cgi?step=' + gnizaNextStep\n};
print qq{ + '&type=' + encodeURIComponent(gnizaType)\n};
print qq{ + '&remote=' + encodeURIComponent(remote)\n};
print qq{ + '&timestamp=' + encodeURIComponent(timestamp);\n};
print qq{ window.location.href = url;\n};
print qq{}\n};
print qq{</script>\n};
print <<"END_JS";
<script>
var gnizaType = '$esc_type';
var gnizaNextStep = '$next_step';
function gnizaLoadSnapshots() {
var remote = document.getElementById('remote').value;
var sel = document.getElementById('timestamp');
var btn = document.getElementById('next-btn');
if (!remote) {
_setSelectPlaceholder(sel, '-- Select remote first --');
sel.disabled = true;
btn.disabled = true;
return;
}
_setSelectPlaceholder(sel, 'Loading...');
sel.disabled = true;
btn.disabled = true;
var url = 'restore.live.cgi?step=fetch_snapshots&remote=' + encodeURIComponent(remote);
var xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.onreadystatechange = function() {
if (xhr.readyState !== 4) return;
if (xhr.status === 200) {
try {
var data = JSON.parse(xhr.responseText);
if (data.error) {
_setSelectPlaceholder(sel, 'Error: ' + data.error);
} else if (data.snapshots && data.snapshots.length > 0) {
_populateSelect(sel, data.snapshots);
sel.disabled = false;
btn.disabled = false;
} else {
_setSelectPlaceholder(sel, 'No snapshots found');
}
} catch(e) {
_setSelectPlaceholder(sel, 'Failed to parse response');
}
} else {
_setSelectPlaceholder(sel, 'Request failed');
}
};
xhr.send();
}
function _setSelectPlaceholder(sel, text) {
while (sel.options.length) sel.remove(0);
var opt = document.createElement('option');
opt.value = '';
opt.textContent = text;
sel.appendChild(opt);
}
function _populateSelect(sel, values) {
while (sel.options.length) sel.remove(0);
for (var i = 0; i < values.length; i++) {
var opt = document.createElement('option');
opt.value = values[i];
opt.textContent = values[i];
sel.appendChild(opt);
}
}
function gnizaGoNext() {
var remote = document.getElementById('remote').value;
var timestamp = document.getElementById('timestamp').value;
if (!remote || !timestamp) return;
var url = 'restore.live.cgi?step=' + gnizaNextStep
+ '&type=' + encodeURIComponent(gnizaType)
+ '&remote=' + encodeURIComponent(remote)
+ '&timestamp=' + encodeURIComponent(timestamp);
window.location.href = url;
}
</script>
END_JS
}
# ── Step 2: Select specific item ─────────────────────────────
@@ -409,280 +411,282 @@ sub _render_file_picker {
sub _print_step2_js {
my ($esc_type, $esc_remote, $esc_timestamp) = @_;
print qq{<script>\n};
print qq{var gnizaType = '$esc_type';\n};
print qq{var gnizaRemote = '$esc_remote';\n};
print qq{var gnizaTimestamp = '$esc_timestamp';\n};
print qq{var fbCache = {};\n};
print qq{var fbSelected = '';\n};
print qq{\n};
print qq{(function() {\n};
print qq{ var listTypes = ['database','dbusers','mailbox','domains','ssl','cron'];\n};
print qq{ if (listTypes.indexOf(gnizaType) >= 0) {\n};
print qq{ loadOptions();\n};
print qq{ }\n};
print qq{})();\n};
print qq{\n};
print qq{function loadOptions() {\n};
print qq{ var url = 'restore.live.cgi?step=fetch_options'\n};
print qq{ + '&remote=' + encodeURIComponent(gnizaRemote)\n};
print qq{ + '&timestamp=' + encodeURIComponent(gnizaTimestamp)\n};
print qq{ + '&type=' + encodeURIComponent(gnizaType);\n};
print qq{\n};
print qq{ var xhr = new XMLHttpRequest();\n};
print qq{ xhr.open('GET', url, true);\n};
print qq{ xhr.onreadystatechange = function() {\n};
print qq{ if (xhr.readyState !== 4) return;\n};
print qq{ var container = document.getElementById('item-list');\n};
print qq{ if (xhr.status === 200) {\n};
print qq{ try {\n};
print qq{ var data = JSON.parse(xhr.responseText);\n};
print qq{ if (data.error) {\n};
print qq{ container.textContent = 'Error: ' + data.error;\n};
print qq{ } else if (gnizaType === 'cron') {\n};
print qq{ populatePreview(container, data.options);\n};
print qq{ } else {\n};
print qq{ populateChecklist(container, data.options);\n};
print qq{ }\n};
print qq{ } catch(e) {\n};
print qq{ container.textContent = 'Failed to parse response';\n};
print qq{ }\n};
print qq{ } else {\n};
print qq{ container.textContent = 'Request failed';\n};
print qq{ }\n};
print qq{ };\n};
print qq{ xhr.send();\n};
print qq{}\n};
print qq{\n};
print qq{function populateChecklist(container, options) {\n};
print qq{ var hidden = document.getElementById('selected_items');\n};
print qq{ container.textContent = '';\n};
print qq{ if (!options || options.length === 0) {\n};
print qq{ container.textContent = '(none found)';\n};
print qq{ return;\n};
print qq{ }\n};
print qq{\n};
print qq{ var allLabels = {database:'All Databases',dbusers:'All Database Users',mailbox:'All Mailboxes',domains:'All Domains',ssl:'All Certificates'};\n};
print qq{ var allLabel = allLabels[gnizaType] || 'All';\n};
print qq{\n};
print qq{ var allRow = _makeCheckRow(allLabel, '', true);\n};
print qq{ allRow.querySelector('input').setAttribute('data-all', '1');\n};
print qq{ allRow.querySelector('input').onchange = function() { toggleAll(this.checked); };\n};
print qq{ allRow.querySelector('span').className = 'text-sm font-semibold';\n};
print qq{ container.appendChild(allRow);\n};
print qq{\n};
print qq{ for (var i = 0; i < options.length; i++) {\n};
print qq{ var row = _makeCheckRow(options[i], options[i], false);\n};
print qq{ row.querySelector('input').setAttribute('data-item', '1');\n};
print qq{ row.querySelector('input').onchange = function() { syncHidden(); };\n};
print qq{ container.appendChild(row);\n};
print qq{ }\n};
print qq{}\n};
print qq{\n};
print qq{function _makeCheckRow(labelText, value, isAll) {\n};
print qq{ var label = document.createElement('label');\n};
print qq{ label.className = 'flex items-center gap-2 cursor-pointer';\n};
print qq{ var cb = document.createElement('input');\n};
print qq{ cb.type = 'checkbox';\n};
print qq{ cb.className = 'checkbox checkbox-sm';\n};
print qq{ if (value) cb.value = value;\n};
print qq{ var span = document.createElement('span');\n};
print qq{ span.className = 'text-sm';\n};
print qq{ span.textContent = labelText;\n};
print qq{ label.appendChild(cb);\n};
print qq{ label.appendChild(span);\n};
print qq{ return label;\n};
print qq{}\n};
print qq{\n};
print qq{function toggleAll(checked) {\n};
print qq{ var container = document.getElementById('item-list');\n};
print qq{ var hidden = document.getElementById('selected_items');\n};
print qq{ var items = container.querySelectorAll('input[data-item]');\n};
print qq{ for (var i = 0; i < items.length; i++) {\n};
print qq{ items[i].disabled = checked;\n};
print qq{ if (checked) items[i].checked = false;\n};
print qq{ }\n};
print qq{ hidden.value = checked ? '__ALL__' : '';\n};
print qq{}\n};
print qq{\n};
print qq{function syncHidden() {\n};
print qq{ var container = document.getElementById('item-list');\n};
print qq{ var hidden = document.getElementById('selected_items');\n};
print qq{ var items = container.querySelectorAll('input[data-item]:checked');\n};
print qq{ var vals = [];\n};
print qq{ for (var i = 0; i < items.length; i++) {\n};
print qq{ vals.push(items[i].value);\n};
print qq{ }\n};
print qq{ hidden.value = vals.join(',');\n};
print qq{}\n};
print qq{\n};
print qq{function populatePreview(container, options) {\n};
print qq{ container.textContent = '';\n};
print qq{ if (!options || options.length === 0) {\n};
print qq{ container.textContent = '(none found)';\n};
print qq{ return;\n};
print qq{ }\n};
print qq{ var pre = document.createElement('pre');\n};
print qq{ pre.className = 'text-xs font-mono bg-base-200 p-3 rounded-lg overflow-x-auto';\n};
print qq{ pre.textContent = options.join('\\n');\n};
print qq{ container.appendChild(pre);\n};
print qq{}\n};
print qq{\n};
print qq{function gnizaGoConfirm() {\n};
print qq{ var url = 'restore.live.cgi?step=3'\n};
print qq{ + '&type=' + encodeURIComponent(gnizaType)\n};
print qq{ + '&remote=' + encodeURIComponent(gnizaRemote)\n};
print qq{ + '&timestamp=' + encodeURIComponent(gnizaTimestamp);\n};
print qq{\n};
print qq{ if (gnizaType === 'files') {\n};
print qq{ var path = document.getElementById('path') ? document.getElementById('path').value : '';\n};
print qq{ url += '&path=' + encodeURIComponent(path);\n};
print qq{ } else if (document.getElementById('selected_items')) {\n};
print qq{ url += '&items=' + encodeURIComponent(document.getElementById('selected_items').value);\n};
print qq{ }\n};
print qq{\n};
print qq{ window.location.href = url;\n};
print qq{}\n};
print qq{\n};
print qq{function gnizaOpenBrowser() {\n};
print qq{ fbSelected = '';\n};
print qq{ document.getElementById('fb-select-btn').disabled = true;\n};
print qq{ document.getElementById('fb-modal').showModal();\n};
print qq{ gnizaLoadDir('');\n};
print qq{}\n};
print qq{\n};
print qq{function gnizaLoadDir(path) {\n};
print qq{ var cacheKey = path;\n};
print qq{ if (fbCache[cacheKey]) {\n};
print qq{ gnizaRenderFileList(path, fbCache[cacheKey]);\n};
print qq{ return;\n};
print qq{ }\n};
print qq{\n};
print qq{ document.getElementById('fb-loading').hidden = false;\n};
print qq{ document.getElementById('fb-error').hidden = true;\n};
print qq{ document.getElementById('fb-tbody').textContent = '';\n};
print qq{\n};
print qq{ var url = 'restore.live.cgi?step=fetch_options'\n};
print qq{ + '&remote=' + encodeURIComponent(gnizaRemote)\n};
print qq{ + '&timestamp=' + encodeURIComponent(gnizaTimestamp)\n};
print qq{ + '&type=files'\n};
print qq{ + (path ? '&path=' + encodeURIComponent(path) : '');\n};
print qq{\n};
print qq{ var xhr = new XMLHttpRequest();\n};
print qq{ xhr.open('GET', url, true);\n};
print qq{ xhr.onreadystatechange = function() {\n};
print qq{ if (xhr.readyState !== 4) return;\n};
print qq{ document.getElementById('fb-loading').hidden = true;\n};
print qq{ if (xhr.status === 200) {\n};
print qq{ try {\n};
print qq{ var data = JSON.parse(xhr.responseText);\n};
print qq{ if (data.error) {\n};
print qq{ document.getElementById('fb-error').textContent = data.error;\n};
print qq{ document.getElementById('fb-error').hidden = false;\n};
print qq{ } else {\n};
print qq{ fbCache[cacheKey] = data.options;\n};
print qq{ gnizaRenderFileList(path, data.options);\n};
print qq{ }\n};
print qq{ } catch(e) {\n};
print qq{ document.getElementById('fb-error').textContent = 'Failed to parse response';\n};
print qq{ document.getElementById('fb-error').hidden = false;\n};
print qq{ }\n};
print qq{ }\n};
print qq{ };\n};
print qq{ xhr.send();\n};
print qq{}\n};
print qq{\n};
print qq{function gnizaRenderBreadcrumbs(path) {\n};
print qq{ var ul = document.createElement('ul');\n};
print qq{ var li = document.createElement('li');\n};
print qq{ var a = document.createElement('a');\n};
print qq{ a.textContent = 'homedir';\n};
print qq{ a.href = '#';\n};
print qq{ a.onclick = function(e) { e.preventDefault(); gnizaLoadDir(''); };\n};
print qq{ li.appendChild(a);\n};
print qq{ ul.appendChild(li);\n};
print qq{\n};
print qq{ if (path) {\n};
print qq{ var parts = path.replace(/\\/\$/, '').split('/');\n};
print qq{ var built = '';\n};
print qq{ for (var i = 0; i < parts.length; i++) {\n};
print qq{ built += (i > 0 ? '/' : '') + parts[i];\n};
print qq{ li = document.createElement('li');\n};
print qq{ if (i < parts.length - 1) {\n};
print qq{ a = document.createElement('a');\n};
print qq{ a.textContent = parts[i];\n};
print qq{ a.href = '#';\n};
print qq{ (function(p) { a.onclick = function(e) { e.preventDefault(); gnizaLoadDir(p); }; })(built);\n};
print qq{ li.appendChild(a);\n};
print qq{ } else {\n};
print qq{ li.textContent = parts[i];\n};
print qq{ }\n};
print qq{ ul.appendChild(li);\n};
print qq{ }\n};
print qq{ }\n};
print qq{\n};
print qq{ var bc = document.getElementById('fb-breadcrumbs');\n};
print qq{ bc.textContent = '';\n};
print qq{ bc.appendChild(ul);\n};
print qq{}\n};
print qq{\n};
print qq{function gnizaRenderFileList(currentPath, entries) {\n};
print qq{ gnizaRenderBreadcrumbs(currentPath);\n};
print qq{ fbSelected = '';\n};
print qq{ document.getElementById('fb-select-btn').disabled = true;\n};
print qq{\n};
print qq{ var tbody = document.getElementById('fb-tbody');\n};
print qq{ tbody.textContent = '';\n};
print qq{\n};
print qq{ if (!entries || entries.length === 0) {\n};
print qq{ var emptyTr = document.createElement('tr');\n};
print qq{ var emptyTd = document.createElement('td');\n};
print qq{ emptyTd.className = 'text-center text-base-content/60 py-4';\n};
print qq{ emptyTd.textContent = '(empty directory)';\n};
print qq{ emptyTr.appendChild(emptyTd);\n};
print qq{ tbody.appendChild(emptyTr);\n};
print qq{ return;\n};
print qq{ }\n};
print qq{\n};
print qq{ for (var i = 0; i < entries.length; i++) {\n};
print qq{ var entry = entries[i];\n};
print qq{ var isDir = entry.endsWith('/');\n};
print qq{ var fullPath = currentPath ? currentPath.replace(/\\/\$/, '') + '/' + entry : entry;\n};
print qq{\n};
print qq{ var tr = document.createElement('tr');\n};
print qq{ tr.className = 'cursor-pointer hover';\n};
print qq{ tr.setAttribute('data-path', fullPath);\n};
print qq{\n};
print qq{ var td = document.createElement('td');\n};
print qq{ td.className = 'py-1';\n};
print qq{ var icon = isDir ? '\\uD83D\\uDCC1 ' : '\\uD83D\\uDCC4 ';\n};
print qq{ td.textContent = icon + entry;\n};
print qq{ tr.appendChild(td);\n};
print qq{\n};
print qq{ (function(row, path, dir) {\n};
print qq{ row.onclick = function() {\n};
print qq{ var rows = document.getElementById('fb-tbody').querySelectorAll('tr');\n};
print qq{ for (var j = 0; j < rows.length; j++) rows[j].classList.remove('bg-primary/10');\n};
print qq{ row.classList.add('bg-primary/10');\n};
print qq{ fbSelected = path;\n};
print qq{ document.getElementById('fb-select-btn').disabled = false;\n};
print qq{ };\n};
print qq{ if (dir) {\n};
print qq{ row.ondblclick = function() { gnizaLoadDir(path.replace(/\\/\$/, '')); };\n};
print qq{ }\n};
print qq{ })(tr, fullPath, isDir);\n};
print qq{\n};
print qq{ tbody.appendChild(tr);\n};
print qq{ }\n};
print qq{}\n};
print qq{\n};
print qq{function gnizaSelectPath() {\n};
print qq{ if (fbSelected) {\n};
print qq{ document.getElementById('path').value = fbSelected;\n};
print qq{ }\n};
print qq{ document.getElementById('fb-modal').close();\n};
print qq{}\n};
print qq{</script>\n};
print <<"END_JS";
<script>
var gnizaType = '$esc_type';
var gnizaRemote = '$esc_remote';
var gnizaTimestamp = '$esc_timestamp';
var fbCache = {};
var fbSelected = '';
(function() {
var listTypes = ['database','dbusers','mailbox','domains','ssl','cron'];
if (listTypes.indexOf(gnizaType) >= 0) {
loadOptions();
}
})();
function loadOptions() {
var url = 'restore.live.cgi?step=fetch_options'
+ '&remote=' + encodeURIComponent(gnizaRemote)
+ '&timestamp=' + encodeURIComponent(gnizaTimestamp)
+ '&type=' + encodeURIComponent(gnizaType);
var xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.onreadystatechange = function() {
if (xhr.readyState !== 4) return;
var container = document.getElementById('item-list');
if (xhr.status === 200) {
try {
var data = JSON.parse(xhr.responseText);
if (data.error) {
container.textContent = 'Error: ' + data.error;
} else if (gnizaType === 'cron') {
populatePreview(container, data.options);
} else {
populateChecklist(container, data.options);
}
} catch(e) {
container.textContent = 'Failed to parse response';
}
} else {
container.textContent = 'Request failed';
}
};
xhr.send();
}
function populateChecklist(container, options) {
var hidden = document.getElementById('selected_items');
container.textContent = '';
if (!options || options.length === 0) {
container.textContent = '(none found)';
return;
}
var allLabels = {database:'All Databases',dbusers:'All Database Users',mailbox:'All Mailboxes',domains:'All Domains',ssl:'All Certificates'};
var allLabel = allLabels[gnizaType] || 'All';
var allRow = _makeCheckRow(allLabel, '', true);
allRow.querySelector('input').setAttribute('data-all', '1');
allRow.querySelector('input').onchange = function() { toggleAll(this.checked); };
allRow.querySelector('span').className = 'text-sm font-semibold';
container.appendChild(allRow);
for (var i = 0; i < options.length; i++) {
var row = _makeCheckRow(options[i], options[i], false);
row.querySelector('input').setAttribute('data-item', '1');
row.querySelector('input').onchange = function() { syncHidden(); };
container.appendChild(row);
}
}
function _makeCheckRow(labelText, value, isAll) {
var label = document.createElement('label');
label.className = 'flex items-center gap-2 cursor-pointer';
var cb = document.createElement('input');
cb.type = 'checkbox';
cb.className = 'checkbox checkbox-sm';
if (value) cb.value = value;
var span = document.createElement('span');
span.className = 'text-sm';
span.textContent = labelText;
label.appendChild(cb);
label.appendChild(span);
return label;
}
function toggleAll(checked) {
var container = document.getElementById('item-list');
var hidden = document.getElementById('selected_items');
var items = container.querySelectorAll('input[data-item]');
for (var i = 0; i < items.length; i++) {
items[i].disabled = checked;
if (checked) items[i].checked = false;
}
hidden.value = checked ? '__ALL__' : '';
}
function syncHidden() {
var container = document.getElementById('item-list');
var hidden = document.getElementById('selected_items');
var items = container.querySelectorAll('input[data-item]:checked');
var vals = [];
for (var i = 0; i < items.length; i++) {
vals.push(items[i].value);
}
hidden.value = vals.join(',');
}
function populatePreview(container, options) {
container.textContent = '';
if (!options || options.length === 0) {
container.textContent = '(none found)';
return;
}
var pre = document.createElement('pre');
pre.className = 'text-xs font-mono bg-base-200 p-3 rounded-lg overflow-x-auto';
pre.textContent = options.join('\\n');
container.appendChild(pre);
}
function gnizaGoConfirm() {
var url = 'restore.live.cgi?step=3'
+ '&type=' + encodeURIComponent(gnizaType)
+ '&remote=' + encodeURIComponent(gnizaRemote)
+ '&timestamp=' + encodeURIComponent(gnizaTimestamp);
if (gnizaType === 'files') {
var path = document.getElementById('path') ? document.getElementById('path').value : '';
url += '&path=' + encodeURIComponent(path);
} else if (document.getElementById('selected_items')) {
url += '&items=' + encodeURIComponent(document.getElementById('selected_items').value);
}
window.location.href = url;
}
function gnizaOpenBrowser() {
fbSelected = '';
document.getElementById('fb-select-btn').disabled = true;
document.getElementById('fb-modal').showModal();
gnizaLoadDir('');
}
function gnizaLoadDir(path) {
var cacheKey = path;
if (fbCache[cacheKey]) {
gnizaRenderFileList(path, fbCache[cacheKey]);
return;
}
document.getElementById('fb-loading').hidden = false;
document.getElementById('fb-error').hidden = true;
document.getElementById('fb-tbody').textContent = '';
var url = 'restore.live.cgi?step=fetch_options'
+ '&remote=' + encodeURIComponent(gnizaRemote)
+ '&timestamp=' + encodeURIComponent(gnizaTimestamp)
+ '&type=files'
+ (path ? '&path=' + encodeURIComponent(path) : '');
var xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.onreadystatechange = function() {
if (xhr.readyState !== 4) return;
document.getElementById('fb-loading').hidden = true;
if (xhr.status === 200) {
try {
var data = JSON.parse(xhr.responseText);
if (data.error) {
document.getElementById('fb-error').textContent = data.error;
document.getElementById('fb-error').hidden = false;
} else {
fbCache[cacheKey] = data.options;
gnizaRenderFileList(path, data.options);
}
} catch(e) {
document.getElementById('fb-error').textContent = 'Failed to parse response';
document.getElementById('fb-error').hidden = false;
}
}
};
xhr.send();
}
function gnizaRenderBreadcrumbs(path) {
var ul = document.createElement('ul');
var li = document.createElement('li');
var a = document.createElement('a');
a.textContent = 'homedir';
a.href = '#';
a.onclick = function(e) { e.preventDefault(); gnizaLoadDir(''); };
li.appendChild(a);
ul.appendChild(li);
if (path) {
var parts = path.replace(/\\/\$/, '').split('/');
var built = '';
for (var i = 0; i < parts.length; i++) {
built += (i > 0 ? '/' : '') + parts[i];
li = document.createElement('li');
if (i < parts.length - 1) {
a = document.createElement('a');
a.textContent = parts[i];
a.href = '#';
(function(p) { a.onclick = function(e) { e.preventDefault(); gnizaLoadDir(p); }; })(built);
li.appendChild(a);
} else {
li.textContent = parts[i];
}
ul.appendChild(li);
}
}
var bc = document.getElementById('fb-breadcrumbs');
bc.textContent = '';
bc.appendChild(ul);
}
function gnizaRenderFileList(currentPath, entries) {
gnizaRenderBreadcrumbs(currentPath);
fbSelected = '';
document.getElementById('fb-select-btn').disabled = true;
var tbody = document.getElementById('fb-tbody');
tbody.textContent = '';
if (!entries || entries.length === 0) {
var emptyTr = document.createElement('tr');
var emptyTd = document.createElement('td');
emptyTd.className = 'text-center text-base-content/60 py-4';
emptyTd.textContent = '(empty directory)';
emptyTr.appendChild(emptyTd);
tbody.appendChild(emptyTr);
return;
}
for (var i = 0; i < entries.length; i++) {
var entry = entries[i];
var isDir = entry.endsWith('/');
var fullPath = currentPath ? currentPath.replace(/\\/\$/, '') + '/' + entry : entry;
var tr = document.createElement('tr');
tr.className = 'cursor-pointer hover';
tr.setAttribute('data-path', fullPath);
var td = document.createElement('td');
td.className = 'py-1';
var icon = isDir ? '\\uD83D\\uDCC1 ' : '\\uD83D\\uDCC4 ';
td.textContent = icon + entry;
tr.appendChild(td);
(function(row, path, dir) {
row.onclick = function() {
var rows = document.getElementById('fb-tbody').querySelectorAll('tr');
for (var j = 0; j < rows.length; j++) rows[j].classList.remove('bg-primary/10');
row.classList.add('bg-primary/10');
fbSelected = path;
document.getElementById('fb-select-btn').disabled = false;
};
if (dir) {
row.ondblclick = function() { gnizaLoadDir(path.replace(/\\/\$/, '')); };
}
})(tr, fullPath, isDir);
tbody.appendChild(tr);
}
}
function gnizaSelectPath() {
if (fbSelected) {
document.getElementById('path').value = fbSelected;
}
document.getElementById('fb-modal').close();
}
</script>
END_JS
}
# ── Step 3: Confirmation ─────────────────────────────────────