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"
|
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
|
||||||
|
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|||||||
@@ -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};
|
|
||||||
for my $a (@accounts) {
|
|
||||||
my $esc = GnizaWHM::UI::esc($a);
|
|
||||||
print qq{ <option value="$esc">$esc</option>\n};
|
|
||||||
}
|
|
||||||
print qq{ </select>\n};
|
print qq{ </select>\n};
|
||||||
} else {
|
print qq{ <span class="loading loading-spinner loading-sm" id="gnizaAccountSpinner" style="display:none"></span>\n};
|
||||||
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();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user