Load restore account dropdown from remote backups via AJAX

The WHM restore page now populates the account dropdown dynamically
from the selected remote, making terminated/removed accounts visible
and restorable. Accounts not on the local server show "(terminated)".

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
shuki
2026-03-05 03:43:00 +02:00
parent d069c490ae
commit 6ecba2ae43
3 changed files with 150 additions and 16 deletions

View File

@@ -702,6 +702,22 @@ cmd_list() {
validate_config || die "Invalid configuration" validate_config || die "Invalid configuration"
init_logging init_logging
# list accounts subcommand
if [[ "${1:-}" == "accounts" ]]; then
shift
local remote_flag=""
remote_flag=$(get_opt remote "$@" 2>/dev/null) || true
[[ -z "$remote_flag" ]] && die "Usage: gniza list accounts --remote=NAME"
local remotes; remotes=$(get_target_remotes "$remote_flag") || die "Invalid remote"
local rname; rname=$(head -1 <<< "$remotes")
_save_remote_globals
load_remote "$rname" || die "Failed to load remote: $rname"
_test_connection || die "Connection failed to remote: $rname"
list_remote_accounts
_restore_remote_globals
return 0
fi
local single_account="" local single_account=""
single_account=$(get_opt account "$@" 2>/dev/null) || true single_account=$(get_opt account "$@" 2>/dev/null) || true

View File

@@ -39,6 +39,7 @@ my @ALLOWED = (
{ cmd => 'restore', subcmd => 'list-ssl', args => [qr/^[a-z][a-z0-9_-]*$/] }, { cmd => 'restore', subcmd => 'list-ssl', args => [qr/^[a-z][a-z0-9_-]*$/] },
# list # list
{ cmd => 'list', subcmd => undef, args => [] }, { cmd => 'list', subcmd => undef, args => [] },
{ cmd => 'list', subcmd => 'accounts', args => [] },
); );
# Named option patterns (--key=value). # Named option patterns (--key=value).

View File

@@ -31,6 +31,7 @@ my %TYPE_LABELS = (
my %SIMPLE_TYPES = map { $_ => 1 } qw(account cron cpconfig); my %SIMPLE_TYPES = map { $_ => 1 } qw(account cron cpconfig);
if ($step eq 'fetch_options') { handle_fetch_options() } if ($step eq 'fetch_options') { handle_fetch_options() }
elsif ($step eq 'fetch_accounts') { handle_fetch_accounts() }
elsif ($step eq '2') { handle_step2() } elsif ($step eq '2') { handle_step2() }
elsif ($step eq '3') { handle_step3() } elsif ($step eq '3') { handle_step3() }
elsif ($step eq '4') { handle_step4() } elsif ($step eq '4') { handle_step4() }
@@ -106,6 +107,48 @@ sub handle_fetch_options {
print qq({"options":[$json_arr]}); print qq({"options":[$json_arr]});
} }
# ── JSON endpoint: fetch accounts from remote ──────────────────
sub handle_fetch_accounts {
my $remote = $form->{'remote'} // '';
print "Content-Type: application/json\r\n\r\n";
if ($remote eq '') {
print qq({"error":"Remote is required"});
return;
}
my ($ok, $stdout, $stderr) = GnizaWHM::Runner::run(
'list', 'accounts', [], { remote => $remote }
);
unless ($ok) {
my $msg = $stderr // 'Failed to list accounts';
$msg =~ s/\\/\\\\/g;
$msg =~ s/"/\\"/g;
$msg =~ s/\n/\\n/g;
$msg =~ s/\r/\\r/g;
print qq({"error":"$msg"});
return;
}
# Get local cPanel accounts for terminated detection
my %local_accounts = map { $_ => 1 } GnizaWHM::UI::get_cpanel_accounts();
my @accounts;
for my $line (split /\n/, $stdout) {
$line =~ s/^\s+|\s+$//g;
next unless $line =~ /^[a-z][a-z0-9_-]*$/;
my $terminated = $local_accounts{$line} ? 0 : 1;
$line =~ s/\\/\\\\/g;
$line =~ s/"/\\"/g;
push @accounts, qq({"name":"$line","terminated":$terminated});
}
print '{"accounts":[' . join(',', @accounts) . ']}';
}
# ── Step 1: Select Account + Remote ───────────────────────── # ── Step 1: Select Account + Remote ─────────────────────────
sub handle_step1 { sub handle_step1 {
@@ -125,7 +168,7 @@ sub handle_step1 {
return; return;
} }
print qq{<form method="GET" action="restore.cgi">\n}; print qq{<form method="GET" action="restore.cgi" id="gnizaStep1Form">\n};
print qq{<input type="hidden" name="restore_step" value="2">\n}; print qq{<input type="hidden" name="restore_step" value="2">\n};
print qq{<div class="card bg-white shadow-sm border border-base-300 mb-6">\n<div class="card-body">\n}; print qq{<div class="card bg-white shadow-sm border border-base-300 mb-6">\n<div class="card-body">\n};
@@ -134,7 +177,10 @@ sub handle_step1 {
# Remote dropdown # Remote dropdown
print qq{<div class="flex items-center gap-3 mb-2.5">\n}; print qq{<div class="flex items-center gap-3 mb-2.5">\n};
print qq{ <label class="w-44 font-medium text-sm" for="remote">Remote</label>\n}; print qq{ <label class="w-44 font-medium text-sm" for="remote">Remote</label>\n};
print qq{ <select class="select select-bordered select-sm w-full max-w-xs" id="remote" name="remote" required>\n}; print qq{ <select class="select select-bordered select-sm w-full max-w-xs" id="remote" name="remote" required onchange="gnizaLoadAccounts()">\n};
if (@remotes > 1) {
print qq{ <option value="">-- Select remote --</option>\n};
}
for my $r (@remotes) { for my $r (@remotes) {
my $esc = GnizaWHM::UI::esc($r); my $esc = GnizaWHM::UI::esc($r);
print qq{ <option value="$esc">$esc</option>\n}; print qq{ <option value="$esc">$esc</option>\n};
@@ -142,31 +188,102 @@ sub handle_step1 {
print qq{ </select>\n}; print qq{ </select>\n};
print qq{</div>\n}; print qq{</div>\n};
# Account input # Account dropdown (populated via AJAX)
my @accounts = GnizaWHM::UI::get_cpanel_accounts();
print qq{<div class="flex items-center gap-3 mb-2.5">\n}; print qq{<div class="flex items-center gap-3 mb-2.5">\n};
print qq{ <label class="w-44 font-medium text-sm" for="account">Account</label>\n}; print qq{ <label class="w-44 font-medium text-sm" for="account">Account</label>\n};
if (@accounts) { print qq{ <select class="select select-bordered select-sm w-full max-w-xs" id="account" name="account" required disabled>\n};
print qq{ <select class="select select-bordered select-sm w-full max-w-xs" id="account" name="account" required>\n}; print qq{ <option value="">-- Select remote first --</option>\n};
print qq{ <option value="">-- Select account --</option>\n}; print qq{ </select>\n};
for my $a (@accounts) { print qq{ <span class="loading loading-spinner loading-sm" id="gnizaAccountSpinner" style="display:none"></span>\n};
my $esc = GnizaWHM::UI::esc($a);
print qq{ <option value="$esc">$esc</option>\n};
}
print qq{ </select>\n};
} else {
print qq{ <input type="text" class="input input-bordered input-sm w-full max-w-xs" id="account" name="account" required placeholder="Username">\n};
}
print qq{</div>\n}; print qq{</div>\n};
print qq{</div>\n</div>\n}; print qq{</div>\n</div>\n};
print qq{<div class="flex items-center gap-2">\n}; print qq{<div class="flex items-center gap-2">\n};
print qq{ <button type="submit" class="btn btn-primary btn-sm">Load Snapshots</button>\n}; print qq{ <button type="submit" class="btn btn-primary btn-sm" id="gnizaSubmitBtn" disabled>Load Snapshots</button>\n};
print qq{</div>\n}; print qq{</div>\n};
print qq{</form>\n}; print qq{</form>\n};
# JavaScript for dynamic account loading
my $single_remote = (@remotes == 1) ? 'true' : 'false';
print <<'JSEOF';
<script>
function gnizaLoadAccounts() {
var sel = document.getElementById('remote');
var remote = sel.value;
var acctSel = document.getElementById('account');
var spinner = document.getElementById('gnizaAccountSpinner');
var submitBtn = document.getElementById('gnizaSubmitBtn');
if (!remote) {
acctSel.innerHTML = '<option value="">-- Select remote first --</option>';
acctSel.disabled = true;
submitBtn.disabled = true;
return;
}
acctSel.innerHTML = '<option value="">Loading...</option>';
acctSel.disabled = true;
submitBtn.disabled = true;
spinner.style.display = '';
function gnizaSetAcctMsg(msg) {
acctSel.innerHTML = '';
var opt = document.createElement('option');
opt.value = '';
opt.textContent = msg;
acctSel.appendChild(opt);
}
function gnizaDone() { spinner.style.display = 'none'; }
var xhr = new XMLHttpRequest();
xhr.timeout = 30000;
xhr.open('GET', 'restore.cgi?restore_step=fetch_accounts&remote=' + encodeURIComponent(remote));
xhr.onload = function() {
gnizaDone();
if (xhr.status === 200) {
try {
var data = JSON.parse(xhr.responseText);
if (data.error) {
gnizaSetAcctMsg('Error: ' + data.error);
return;
}
acctSel.innerHTML = '';
var placeholder = document.createElement('option');
placeholder.value = '';
placeholder.textContent = '-- Select account --';
acctSel.appendChild(placeholder);
for (var i = 0; i < data.accounts.length; i++) {
var a = data.accounts[i];
var opt = document.createElement('option');
opt.value = a.name;
opt.textContent = a.name + (a.terminated ? ' (terminated)' : '');
acctSel.appendChild(opt);
}
if (data.accounts.length === 0) {
gnizaSetAcctMsg('No accounts found');
} else {
acctSel.disabled = false;
submitBtn.disabled = false;
}
} catch(e) {
gnizaSetAcctMsg('Failed to parse response');
}
} else {
gnizaSetAcctMsg('Request failed');
}
};
xhr.onerror = function() { gnizaDone(); gnizaSetAcctMsg('Connection error'); };
xhr.ontimeout = function() { gnizaDone(); gnizaSetAcctMsg('Request timed out'); };
xhr.send();
}
JSEOF
print "if ($single_remote) { gnizaLoadAccounts(); }\n";
print "</script>\n";
print GnizaWHM::UI::page_footer(); print GnizaWHM::UI::page_footer();
Whostmgr::HTMLInterface::footer(); Whostmgr::HTMLInterface::footer();
} }