- 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>
224 lines
8.5 KiB
Perl
224 lines
8.5 KiB
Perl
#!/usr/local/cpanel/3rdparty/bin/perl
|
|
# gniza cPanel Plugin — Activity Logs
|
|
# Shows user-initiated restore actions and their results
|
|
use strict;
|
|
use warnings;
|
|
|
|
BEGIN {
|
|
my $base;
|
|
if ($0 =~ m{^(.*)/}) {
|
|
$base = $1;
|
|
} else {
|
|
$base = '.';
|
|
}
|
|
unshift @INC, "$base/lib";
|
|
}
|
|
|
|
use Cpanel::LiveAPI ();
|
|
use Cpanel::AdminBin::Call ();
|
|
use Cpanel::Form ();
|
|
use GnizaCPanel::UI;
|
|
|
|
my $cpanel = Cpanel::LiveAPI->new();
|
|
END { $cpanel->end() if $cpanel }
|
|
my $form = Cpanel::Form::parseform();
|
|
my $entry = $form->{'entry'} // '';
|
|
|
|
if ($entry ne '') {
|
|
show_entry($entry);
|
|
} else {
|
|
show_list();
|
|
}
|
|
|
|
exit;
|
|
|
|
# ── List View ────────────────────────────────────────────────
|
|
|
|
sub show_list {
|
|
print "Content-Type: text/html\r\n\r\n";
|
|
print $cpanel->header('');
|
|
print GnizaCPanel::UI::page_header('GNIZA Activity Log');
|
|
print GnizaCPanel::UI::render_nav('logs.live.cgi');
|
|
print GnizaCPanel::UI::render_flash();
|
|
|
|
my $raw = eval { Cpanel::AdminBin::Call::call('Gniza', 'Restore', 'LIST_LOGS') } // '';
|
|
|
|
if ($raw =~ /^ERROR: (.*)/) {
|
|
print qq{<div class="alert alert-error mb-4">} . GnizaCPanel::UI::esc($1) . qq{</div>\n};
|
|
print GnizaCPanel::UI::page_footer();
|
|
print $cpanel->footer();
|
|
return;
|
|
}
|
|
|
|
my @entries;
|
|
for my $line (split /\n/, $raw) {
|
|
next if $line eq '';
|
|
my ($idx, $date, $action, $details, $status) = split /\t/, $line;
|
|
push @entries, {
|
|
idx => $idx // '',
|
|
date => $date // '',
|
|
action => $action // '',
|
|
details => $details // '',
|
|
status => $status // '',
|
|
};
|
|
}
|
|
|
|
if (!@entries) {
|
|
print qq{<div class="alert alert-info mb-4">No restore activity yet. Actions you perform in the Restore section will appear here.</div>\n};
|
|
print GnizaCPanel::UI::page_footer();
|
|
print $cpanel->footer();
|
|
return;
|
|
}
|
|
|
|
# Pagination
|
|
my $per_page = 25;
|
|
my $total = scalar @entries;
|
|
my $page = int($form->{'page'} // 1);
|
|
$page = 1 if $page < 1;
|
|
my $total_pages = int(($total + $per_page - 1) / $per_page);
|
|
$page = $total_pages if $page > $total_pages;
|
|
my $start = ($page - 1) * $per_page;
|
|
my $end = $start + $per_page - 1;
|
|
$end = $#entries if $end > $#entries;
|
|
my @page_entries = @entries[$start .. $end];
|
|
|
|
print qq{<div class="overflow-x-auto rounded-box border border-base-content/5 bg-base-100">\n};
|
|
print qq{<table class="table">\n};
|
|
print qq{<thead><tr><th>Date (UTC)</th><th>Action</th><th>Details</th><th>Status</th><th></th></tr></thead>\n};
|
|
print qq{<tbody>\n};
|
|
|
|
for my $e (@page_entries) {
|
|
my $esc_date = GnizaCPanel::UI::esc($e->{date});
|
|
my $esc_action = GnizaCPanel::UI::esc($e->{action});
|
|
my $esc_details = GnizaCPanel::UI::esc($e->{details});
|
|
my $esc_status = GnizaCPanel::UI::esc($e->{status});
|
|
my $esc_idx = GnizaCPanel::UI::esc($e->{idx});
|
|
|
|
my $status_badge = $e->{status} eq 'Error' ? 'badge-error' : 'badge-success';
|
|
my $href = 'logs.live.cgi?entry=' . _uri_escape($e->{idx});
|
|
|
|
print qq{<tr>\n};
|
|
print qq{ <td class="whitespace-nowrap">$esc_date</td>\n};
|
|
print qq{ <td><span class="badge badge-info badge-sm">$esc_action</span></td>\n};
|
|
print qq{ <td class="text-sm">$esc_details</td>\n};
|
|
print qq{ <td><span class="badge $status_badge badge-sm">$esc_status</span></td>\n};
|
|
print qq{ <td><button type="button" class="btn btn-secondary btn-sm" onclick="location.href='$href'">View</button></td>\n};
|
|
print qq{</tr>\n};
|
|
}
|
|
|
|
print qq{</tbody>\n</table>\n</div>\n};
|
|
|
|
# Pagination controls
|
|
if ($total_pages > 1) {
|
|
print qq{<div class="flex items-center justify-center gap-2 mt-4">\n};
|
|
if ($page > 1) {
|
|
my $prev = $page - 1;
|
|
print qq{ <button type="button" class="btn btn-sm" onclick="location.href='logs.live.cgi?page=$prev'">« Prev</button>\n};
|
|
}
|
|
print qq{ <span class="text-sm">Page $page of $total_pages ($total entries)</span>\n};
|
|
if ($page < $total_pages) {
|
|
my $next = $page + 1;
|
|
print qq{ <button type="button" class="btn btn-sm" onclick="location.href='logs.live.cgi?page=$next'">Next »</button>\n};
|
|
}
|
|
print qq{</div>\n};
|
|
}
|
|
|
|
print GnizaCPanel::UI::page_footer();
|
|
print $cpanel->footer();
|
|
}
|
|
|
|
# ── Entry Detail View ────────────────────────────────────────
|
|
|
|
sub show_entry {
|
|
my ($entry_idx) = @_;
|
|
|
|
print "Content-Type: text/html\r\n\r\n";
|
|
print $cpanel->header('');
|
|
print GnizaCPanel::UI::page_header('GNIZA Activity Detail');
|
|
print GnizaCPanel::UI::render_nav('logs.live.cgi');
|
|
|
|
# Validate entry index (numeric only)
|
|
unless ($entry_idx =~ /^[0-9]+$/) {
|
|
print qq{<div class="alert alert-error mb-4">Invalid entry.</div>\n};
|
|
print qq{<p><a href="logs.live.cgi" class="link">← Back to activity log</a></p>\n};
|
|
print GnizaCPanel::UI::page_footer();
|
|
print $cpanel->footer();
|
|
return;
|
|
}
|
|
|
|
my $content = eval { Cpanel::AdminBin::Call::call('Gniza', 'Restore', 'GET_LOG', $entry_idx) } // '';
|
|
|
|
if ($content =~ /^ERROR: (.*)/) {
|
|
print qq{<div class="alert alert-error mb-4">} . GnizaCPanel::UI::esc($1) . qq{</div>\n};
|
|
print qq{<p><a href="logs.live.cgi" class="link">← Back to activity log</a></p>\n};
|
|
print GnizaCPanel::UI::page_footer();
|
|
print $cpanel->footer();
|
|
return;
|
|
}
|
|
|
|
# Parse header fields and output from entry content
|
|
my ($date, $action, $details, $status, $output) = ('', '', '', '', '');
|
|
my @lines = split /\n/, $content;
|
|
my @output_lines;
|
|
my $in_output = 0;
|
|
for my $line (@lines) {
|
|
if (!$in_output) {
|
|
if ($line =~ /^Date:\s+(.+)$/) { $date = $1; }
|
|
elsif ($line =~ /^Action:\s+(.+)$/) { $action = $1; }
|
|
elsif ($line =~ /^Details:\s+(.+)$/) { $details = $1; }
|
|
elsif ($line =~ /^Status:\s+(.+)$/) { $status = $1; $in_output = 1; }
|
|
} else {
|
|
push @output_lines, $line;
|
|
}
|
|
}
|
|
|
|
# Back link
|
|
print qq{<p class="mb-4"><a href="logs.live.cgi" class="link">← Back to activity log</a></p>\n};
|
|
|
|
# Entry info card
|
|
my $status_badge = $status eq 'Error' ? 'badge-error' : 'badge-success';
|
|
print qq{<div class="card bg-base-200 shadow-sm border border-base-300 mb-4">\n};
|
|
print qq{<div class="card-body py-3 px-4">\n};
|
|
print qq{<div class="flex flex-wrap gap-4 items-center text-sm">\n};
|
|
print qq{ <span><strong>Date:</strong> } . GnizaCPanel::UI::esc($date) . qq{</span>\n};
|
|
print qq{ <span><strong>Action:</strong> <span class="badge badge-info badge-sm">} . GnizaCPanel::UI::esc($action) . qq{</span></span>\n};
|
|
print qq{ <span><strong>Status:</strong> <span class="badge $status_badge badge-sm">} . GnizaCPanel::UI::esc($status) . qq{</span></span>\n};
|
|
print qq{</div>\n};
|
|
print qq{<div class="text-sm mt-2"><strong>Details:</strong> } . GnizaCPanel::UI::esc($details) . qq{</div>\n};
|
|
print qq{</div>\n</div>\n};
|
|
|
|
# Output section
|
|
if (@output_lines) {
|
|
print qq{<h3 class="text-sm font-bold mb-2">Command Output</h3>\n};
|
|
print qq{<pre class="bg-base-100 border border-base-300 rounded-lg p-4 text-sm font-mono overflow-x-auto leading-relaxed">};
|
|
for my $line (@output_lines) {
|
|
my $esc = GnizaCPanel::UI::esc($line);
|
|
if ($line =~ /\[ERROR\]/) {
|
|
print qq{<span class="text-error font-bold">$esc</span>\n};
|
|
} elsif ($line =~ /\[WARN\]/) {
|
|
print qq{<span class="text-warning">$esc</span>\n};
|
|
} elsif ($line =~ /\[DEBUG\]/) {
|
|
print qq{<span class="text-base-content/60">$esc</span>\n};
|
|
} else {
|
|
print "$esc\n";
|
|
}
|
|
}
|
|
print qq{</pre>\n};
|
|
} else {
|
|
print qq{<div class="alert alert-info">No output recorded for this action.</div>\n};
|
|
}
|
|
|
|
print GnizaCPanel::UI::page_footer();
|
|
print $cpanel->footer();
|
|
}
|
|
|
|
# ── Helpers ───────────────────────────────────────────────────
|
|
|
|
sub _uri_escape {
|
|
my $str = shift // '';
|
|
$str =~ s/([^A-Za-z0-9\-._~])/sprintf("%%%02X", ord($1))/ge;
|
|
return $str;
|
|
}
|
|
|
|
1;
|