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:
16
bin/gniza
16
bin/gniza
@@ -702,6 +702,22 @@ cmd_list() {
|
||||
validate_config || die "Invalid configuration"
|
||||
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=""
|
||||
single_account=$(get_opt account "$@" 2>/dev/null) || true
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ my @ALLOWED = (
|
||||
{ cmd => 'restore', subcmd => 'list-ssl', args => [qr/^[a-z][a-z0-9_-]*$/] },
|
||||
# list
|
||||
{ cmd => 'list', subcmd => undef, args => [] },
|
||||
{ cmd => 'list', subcmd => 'accounts', args => [] },
|
||||
);
|
||||
|
||||
# Named option patterns (--key=value).
|
||||
|
||||
@@ -31,6 +31,7 @@ my %TYPE_LABELS = (
|
||||
my %SIMPLE_TYPES = map { $_ => 1 } qw(account cron cpconfig);
|
||||
|
||||
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 '3') { handle_step3() }
|
||||
elsif ($step eq '4') { handle_step4() }
|
||||
@@ -106,6 +107,48 @@ sub handle_fetch_options {
|
||||
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 ─────────────────────────
|
||||
|
||||
sub handle_step1 {
|
||||
@@ -125,7 +168,7 @@ sub handle_step1 {
|
||||
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{<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
|
||||
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{ <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) {
|
||||
my $esc = GnizaWHM::UI::esc($r);
|
||||
print qq{ <option value="$esc">$esc</option>\n};
|
||||
@@ -142,31 +188,102 @@ sub handle_step1 {
|
||||
print qq{ </select>\n};
|
||||
print qq{</div>\n};
|
||||
|
||||
# Account input
|
||||
my @accounts = GnizaWHM::UI::get_cpanel_accounts();
|
||||
# Account dropdown (populated via AJAX)
|
||||
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};
|
||||
if (@accounts) {
|
||||
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 account --</option>\n};
|
||||
for my $a (@accounts) {
|
||||
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{ <select class="select select-bordered select-sm w-full max-w-xs" id="account" name="account" required disabled>\n};
|
||||
print qq{ <option value="">-- Select remote first --</option>\n};
|
||||
print qq{ </select>\n};
|
||||
print qq{ <span class="loading loading-spinner loading-sm" id="gnizaAccountSpinner" style="display:none"></span>\n};
|
||||
print qq{</div>\n};
|
||||
|
||||
print qq{</div>\n</div>\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{</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();
|
||||
Whostmgr::HTMLInterface::footer();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user