Files
gniza4cp/cpanel/gniza/index.live.cgi
shuki c20c019048 Add cPanel user activity logs page and WHM user log visibility
- Add per-user activity logging to AdminBin: every RESTORE_* action
  writes to /var/log/gniza/cpanel-<user>.log with action details and
  gniza command output
- New logs.live.cgi CGI with paginated activity list and detail view
- WHM logs.cgi now shows cpanel-*.log files with Owner column and
  structured activity entry viewer with expandable command output
- Add Logs nav item to cPanel plugin, update install.sh

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 19:37:08 +02:00

150 lines
4.9 KiB
Perl

#!/usr/local/cpanel/3rdparty/bin/perl
# gniza cPanel Plugin — Step 1: Select Remote + Snapshot
use strict;
use warnings;
BEGIN {
# Find our lib directory relative to this CGI
my $base;
if ($0 =~ m{^(.*)/}) {
$base = $1;
} else {
$base = '.';
}
unshift @INC, "$base/lib";
}
use Cpanel::LiveAPI ();
use Cpanel::AdminBin::Call ();
use GnizaCPanel::UI;
my $cpanel = Cpanel::LiveAPI->new();
print "Content-Type: text/html\r\n\r\n";
print $cpanel->header('');
# Get allowed remotes via AdminBin
my $remotes_raw = eval { Cpanel::AdminBin::Call::call('Gniza', 'Restore', 'LIST_ALLOWED_REMOTES') } // '';
my @remotes = grep { $_ ne '' } split /\n/, $remotes_raw;
print GnizaCPanel::UI::page_header('GNIZA Backups');
print GnizaCPanel::UI::render_nav('index.live.cgi');
print GnizaCPanel::UI::render_flash();
if (!@remotes) {
print qq{<div class="alert alert-info mb-4">No backup remotes are available for restore. Please contact your server administrator.</div>\n};
print GnizaCPanel::UI::page_footer();
print $cpanel->footer();
$cpanel->end();
exit;
}
print qq{<div class="card bg-white shadow-sm border border-base-300 mb-6">\n<div class="card-body">\n};
print qq{<h2 class="card-title text-sm">Select Backup Source</h2>\n};
# Remote dropdown
print qq{<div class="flex items-center gap-3 mb-2.5">\n};
print qq{ <label class="w-36 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" onchange="gnizaLoadSnapshots()">\n};
print qq{ <option value="">-- Select remote --</option>\n};
for my $r (@remotes) {
my $esc = GnizaCPanel::UI::esc($r);
print qq{ <option value="$esc">$esc</option>\n};
}
print qq{ </select>\n};
print qq{</div>\n};
# Snapshot dropdown (populated via AJAX)
print qq{<div class="flex items-center gap-3 mb-2.5">\n};
print qq{ <label class="w-36 font-medium text-sm" for="timestamp">Snapshot</label>\n};
print qq{ <select class="select select-bordered select-sm w-full max-w-xs" id="timestamp" name="timestamp" disabled>\n};
print qq{ <option value="">-- Select remote first --</option>\n};
print qq{ </select>\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="button" class="btn btn-primary btn-sm" id="next-btn" disabled onclick="gnizaGoNext()">Next</button>\n};
print qq{</div>\n};
# JavaScript for snapshot loading and navigation
print <<"END_JS";
<script>
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=2'
+ '&remote=' + encodeURIComponent(remote)
+ '&timestamp=' + encodeURIComponent(timestamp);
window.location.href = url;
}
</script>
END_JS
print GnizaCPanel::UI::page_footer();
print $cpanel->footer();
$cpanel->end();