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>
This commit is contained in:
@@ -108,6 +108,64 @@ sub _run_gniza {
|
|||||||
|
|
||||||
# ── Action dispatch ───────────────────────────────────────────
|
# ── Action dispatch ───────────────────────────────────────────
|
||||||
|
|
||||||
|
# ── Per-user activity logging ─────────────────────────────────
|
||||||
|
|
||||||
|
my $ACTIVITY_ENTRY_RE = qr/^[0-9]+$/;
|
||||||
|
|
||||||
|
sub _get_log_dir {
|
||||||
|
my $log_dir = '/var/log/gniza';
|
||||||
|
if (open my $fh, '<', $MAIN_CONFIG) {
|
||||||
|
while (my $line = <$fh>) {
|
||||||
|
if ($line =~ /^LOG_DIR=(?:"([^"]*)"|'([^']*)'|(\S*))$/) {
|
||||||
|
my $val = defined $1 ? $1 : (defined $2 ? $2 : ($3 // ''));
|
||||||
|
$log_dir = $val if $val ne '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
close $fh;
|
||||||
|
}
|
||||||
|
return $log_dir;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub _activity_log_path {
|
||||||
|
my ($user) = @_;
|
||||||
|
my $log_dir = _get_log_dir();
|
||||||
|
return "$log_dir/cpanel-$user.log";
|
||||||
|
}
|
||||||
|
|
||||||
|
my %ACTION_LABELS = (
|
||||||
|
RESTORE_ACCOUNT => 'Full Account',
|
||||||
|
RESTORE_FILES => 'Home Directory',
|
||||||
|
RESTORE_DATABASE => 'Database',
|
||||||
|
RESTORE_MAILBOX => 'Email',
|
||||||
|
RESTORE_CRON => 'Cron Jobs',
|
||||||
|
RESTORE_DBUSERS => 'DB Users',
|
||||||
|
RESTORE_DOMAINS => 'Domains',
|
||||||
|
RESTORE_SSL => 'SSL Certificates',
|
||||||
|
);
|
||||||
|
|
||||||
|
sub _log_activity {
|
||||||
|
my ($user, $action, $details, $status, $output) = @_;
|
||||||
|
my $log_file = _activity_log_path($user);
|
||||||
|
my $log_dir = _get_log_dir();
|
||||||
|
mkdir $log_dir, 0700 unless -d $log_dir;
|
||||||
|
|
||||||
|
my @t = gmtime(time);
|
||||||
|
my $ts = sprintf('%04d-%02d-%02d %02d:%02d:%02d',
|
||||||
|
$t[5]+1900, $t[4]+1, $t[3], $t[2], $t[1], $t[0]);
|
||||||
|
my $label = $ACTION_LABELS{$action} // $action;
|
||||||
|
|
||||||
|
if (open my $fh, '>>', $log_file) {
|
||||||
|
print $fh "--- ENTRY ---\n";
|
||||||
|
print $fh "Date: $ts\n";
|
||||||
|
print $fh "Action: $label\n";
|
||||||
|
print $fh "Details: $details\n";
|
||||||
|
print $fh "Status: $status\n";
|
||||||
|
print $fh $output if defined $output && $output ne '';
|
||||||
|
print $fh "--- END ---\n";
|
||||||
|
close $fh;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
sub _actions {
|
sub _actions {
|
||||||
return qw(
|
return qw(
|
||||||
LIST_ALLOWED_REMOTES
|
LIST_ALLOWED_REMOTES
|
||||||
@@ -119,6 +177,8 @@ sub _actions {
|
|||||||
LIST_CRON
|
LIST_CRON
|
||||||
LIST_DNS
|
LIST_DNS
|
||||||
LIST_SSL
|
LIST_SSL
|
||||||
|
LIST_LOGS
|
||||||
|
GET_LOG
|
||||||
RESTORE_ACCOUNT
|
RESTORE_ACCOUNT
|
||||||
RESTORE_FILES
|
RESTORE_FILES
|
||||||
RESTORE_DATABASE
|
RESTORE_DATABASE
|
||||||
@@ -130,6 +190,81 @@ sub _actions {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sub LIST_LOGS {
|
||||||
|
my ($self) = @_;
|
||||||
|
my $user = $self->get_caller_username() // '';
|
||||||
|
return "ERROR: Invalid user" unless $user =~ $ACCOUNT_RE;
|
||||||
|
|
||||||
|
my $log_file = _activity_log_path($user);
|
||||||
|
return '' unless -f $log_file && !-l $log_file;
|
||||||
|
|
||||||
|
# Parse entries from the activity log (newest first)
|
||||||
|
my @entries;
|
||||||
|
if (open my $fh, '<', $log_file) {
|
||||||
|
my $in_entry = 0;
|
||||||
|
my %cur;
|
||||||
|
while (my $line = <$fh>) {
|
||||||
|
chomp $line;
|
||||||
|
if ($line eq '--- ENTRY ---') {
|
||||||
|
$in_entry = 1;
|
||||||
|
%cur = ();
|
||||||
|
} elsif ($line eq '--- END ---' && $in_entry) {
|
||||||
|
push @entries, { %cur } if $cur{date};
|
||||||
|
$in_entry = 0;
|
||||||
|
} elsif ($in_entry) {
|
||||||
|
if ($line =~ /^Date:\s+(.+)$/) { $cur{date} = $1; }
|
||||||
|
elsif ($line =~ /^Action:\s+(.+)$/) { $cur{action} = $1; }
|
||||||
|
elsif ($line =~ /^Details:\s+(.+)$/) { $cur{details} = $1; }
|
||||||
|
elsif ($line =~ /^Status:\s+(.+)$/) { $cur{status} = $1; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
close $fh;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Return newest first, one line per entry: index\tdate\taction\tdetails\tstatus
|
||||||
|
my @lines;
|
||||||
|
for my $i (reverse 0 .. $#entries) {
|
||||||
|
my $e = $entries[$i];
|
||||||
|
push @lines, join("\t", $i, $e->{date} // '', $e->{action} // '',
|
||||||
|
$e->{details} // '', $e->{status} // '');
|
||||||
|
}
|
||||||
|
return join("\n", @lines);
|
||||||
|
}
|
||||||
|
|
||||||
|
sub GET_LOG {
|
||||||
|
my ($self, $entry_idx) = @_;
|
||||||
|
my $user = $self->get_caller_username() // '';
|
||||||
|
return "ERROR: Invalid user" unless $user =~ $ACCOUNT_RE;
|
||||||
|
return "ERROR: Invalid entry" unless defined $entry_idx && $entry_idx =~ $ACTIVITY_ENTRY_RE;
|
||||||
|
|
||||||
|
my $log_file = _activity_log_path($user);
|
||||||
|
return "ERROR: No activity log" unless -f $log_file && !-l $log_file;
|
||||||
|
|
||||||
|
# Parse the Nth entry
|
||||||
|
my $idx = int($entry_idx);
|
||||||
|
my @entries;
|
||||||
|
if (open my $fh, '<', $log_file) {
|
||||||
|
my $in_entry = 0;
|
||||||
|
my @cur_lines;
|
||||||
|
while (my $line = <$fh>) {
|
||||||
|
chomp $line;
|
||||||
|
if ($line eq '--- ENTRY ---') {
|
||||||
|
$in_entry = 1;
|
||||||
|
@cur_lines = ();
|
||||||
|
} elsif ($line eq '--- END ---' && $in_entry) {
|
||||||
|
push @entries, join("\n", @cur_lines);
|
||||||
|
$in_entry = 0;
|
||||||
|
} elsif ($in_entry) {
|
||||||
|
push @cur_lines, $line;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
close $fh;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "ERROR: Entry not found" if $idx < 0 || $idx > $#entries;
|
||||||
|
return $entries[$idx];
|
||||||
|
}
|
||||||
|
|
||||||
sub LIST_ALLOWED_REMOTES {
|
sub LIST_ALLOWED_REMOTES {
|
||||||
my ($self) = @_;
|
my ($self) = @_;
|
||||||
my @remotes = _get_filtered_remotes();
|
my @remotes = _get_filtered_remotes();
|
||||||
@@ -267,7 +402,12 @@ sub RESTORE_ACCOUNT {
|
|||||||
push @opts, "--exclude=$exclude";
|
push @opts, "--exclude=$exclude";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
my $details = "remote=$remote snapshot=$timestamp";
|
||||||
|
$details .= " exclude=$exclude" if defined $exclude && $exclude ne '';
|
||||||
|
|
||||||
my ($ok, $stdout, $stderr) = _run_gniza('restore', 'account', $user, @opts);
|
my ($ok, $stdout, $stderr) = _run_gniza('restore', 'account', $user, @opts);
|
||||||
|
_log_activity($user, 'RESTORE_ACCOUNT', $details,
|
||||||
|
$ok ? 'OK' : 'Error', $ok ? $stdout : $stderr);
|
||||||
return $ok ? "OK\n$stdout" : "ERROR: $stderr";
|
return $ok ? "OK\n$stdout" : "ERROR: $stderr";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -290,7 +430,13 @@ sub RESTORE_FILES {
|
|||||||
push @opts, "--exclude=$exclude";
|
push @opts, "--exclude=$exclude";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
my $details = "remote=$remote snapshot=$timestamp";
|
||||||
|
$details .= " path=$path" if defined $path && $path ne '';
|
||||||
|
$details .= " exclude=$exclude" if defined $exclude && $exclude ne '';
|
||||||
|
|
||||||
my ($ok, $stdout, $stderr) = _run_gniza('restore', 'files', $user, @opts);
|
my ($ok, $stdout, $stderr) = _run_gniza('restore', 'files', $user, @opts);
|
||||||
|
_log_activity($user, 'RESTORE_FILES', $details,
|
||||||
|
$ok ? 'OK' : 'Error', $ok ? $stdout : $stderr);
|
||||||
return $ok ? "OK\n$stdout" : "ERROR: $stderr";
|
return $ok ? "OK\n$stdout" : "ERROR: $stderr";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,8 +455,13 @@ sub RESTORE_DATABASE {
|
|||||||
push @args, $dbname;
|
push @args, $dbname;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
my $details = "remote=$remote snapshot=$timestamp";
|
||||||
|
$details .= " database=$dbname" if defined $dbname && $dbname ne '';
|
||||||
|
|
||||||
my ($ok, $stdout, $stderr) = _run_gniza('restore', 'database', @args,
|
my ($ok, $stdout, $stderr) = _run_gniza('restore', 'database', @args,
|
||||||
"--remote=$remote", "--timestamp=$timestamp");
|
"--remote=$remote", "--timestamp=$timestamp");
|
||||||
|
_log_activity($user, 'RESTORE_DATABASE', $details,
|
||||||
|
$ok ? 'OK' : 'Error', $ok ? $stdout : $stderr);
|
||||||
return $ok ? "OK\n$stdout" : "ERROR: $stderr";
|
return $ok ? "OK\n$stdout" : "ERROR: $stderr";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,8 +480,13 @@ sub RESTORE_MAILBOX {
|
|||||||
push @args, $email;
|
push @args, $email;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
my $details = "remote=$remote snapshot=$timestamp";
|
||||||
|
$details .= " email=$email" if defined $email && $email ne '';
|
||||||
|
|
||||||
my ($ok, $stdout, $stderr) = _run_gniza('restore', 'mailbox', @args,
|
my ($ok, $stdout, $stderr) = _run_gniza('restore', 'mailbox', @args,
|
||||||
"--remote=$remote", "--timestamp=$timestamp");
|
"--remote=$remote", "--timestamp=$timestamp");
|
||||||
|
_log_activity($user, 'RESTORE_MAILBOX', $details,
|
||||||
|
$ok ? 'OK' : 'Error', $ok ? $stdout : $stderr);
|
||||||
return $ok ? "OK\n$stdout" : "ERROR: $stderr";
|
return $ok ? "OK\n$stdout" : "ERROR: $stderr";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -343,8 +499,12 @@ sub RESTORE_CRON {
|
|||||||
return "ERROR: Invalid timestamp" unless defined $timestamp && $timestamp =~ $TS_RE;
|
return "ERROR: Invalid timestamp" unless defined $timestamp && $timestamp =~ $TS_RE;
|
||||||
return "ERROR: Remote not allowed" unless _is_remote_allowed($remote);
|
return "ERROR: Remote not allowed" unless _is_remote_allowed($remote);
|
||||||
|
|
||||||
|
my $details = "remote=$remote snapshot=$timestamp";
|
||||||
|
|
||||||
my ($ok, $stdout, $stderr) = _run_gniza('restore', 'cron', $user,
|
my ($ok, $stdout, $stderr) = _run_gniza('restore', 'cron', $user,
|
||||||
"--remote=$remote", "--timestamp=$timestamp");
|
"--remote=$remote", "--timestamp=$timestamp");
|
||||||
|
_log_activity($user, 'RESTORE_CRON', $details,
|
||||||
|
$ok ? 'OK' : 'Error', $ok ? $stdout : $stderr);
|
||||||
return $ok ? "OK\n$stdout" : "ERROR: $stderr";
|
return $ok ? "OK\n$stdout" : "ERROR: $stderr";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -363,8 +523,13 @@ sub RESTORE_DBUSERS {
|
|||||||
push @args, $dbuser;
|
push @args, $dbuser;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
my $details = "remote=$remote snapshot=$timestamp";
|
||||||
|
$details .= " dbuser=$dbuser" if defined $dbuser && $dbuser ne '';
|
||||||
|
|
||||||
my ($ok, $stdout, $stderr) = _run_gniza('restore', 'dbusers', @args,
|
my ($ok, $stdout, $stderr) = _run_gniza('restore', 'dbusers', @args,
|
||||||
"--remote=$remote", "--timestamp=$timestamp");
|
"--remote=$remote", "--timestamp=$timestamp");
|
||||||
|
_log_activity($user, 'RESTORE_DBUSERS', $details,
|
||||||
|
$ok ? 'OK' : 'Error', $ok ? $stdout : $stderr);
|
||||||
return $ok ? "OK\n$stdout" : "ERROR: $stderr";
|
return $ok ? "OK\n$stdout" : "ERROR: $stderr";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -383,8 +548,13 @@ sub RESTORE_DOMAINS {
|
|||||||
push @args, $domain;
|
push @args, $domain;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
my $details = "remote=$remote snapshot=$timestamp";
|
||||||
|
$details .= " domain=$domain" if defined $domain && $domain ne '';
|
||||||
|
|
||||||
my ($ok, $stdout, $stderr) = _run_gniza('restore', 'domains', @args,
|
my ($ok, $stdout, $stderr) = _run_gniza('restore', 'domains', @args,
|
||||||
"--remote=$remote", "--timestamp=$timestamp");
|
"--remote=$remote", "--timestamp=$timestamp");
|
||||||
|
_log_activity($user, 'RESTORE_DOMAINS', $details,
|
||||||
|
$ok ? 'OK' : 'Error', $ok ? $stdout : $stderr);
|
||||||
return $ok ? "OK\n$stdout" : "ERROR: $stderr";
|
return $ok ? "OK\n$stdout" : "ERROR: $stderr";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -403,8 +573,13 @@ sub RESTORE_SSL {
|
|||||||
push @args, $domain;
|
push @args, $domain;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
my $details = "remote=$remote snapshot=$timestamp";
|
||||||
|
$details .= " domain=$domain" if defined $domain && $domain ne '';
|
||||||
|
|
||||||
my ($ok, $stdout, $stderr) = _run_gniza('restore', 'ssl', @args,
|
my ($ok, $stdout, $stderr) = _run_gniza('restore', 'ssl', @args,
|
||||||
"--remote=$remote", "--timestamp=$timestamp");
|
"--remote=$remote", "--timestamp=$timestamp");
|
||||||
|
_log_activity($user, 'RESTORE_SSL', $details,
|
||||||
|
$ok ? 'OK' : 'Error', $ok ? $stdout : $stderr);
|
||||||
return $ok ? "OK\n$stdout" : "ERROR: $stderr";
|
return $ok ? "OK\n$stdout" : "ERROR: $stderr";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 369 KiB After Width: | Height: | Size: 685 B |
@@ -20,13 +20,14 @@ use GnizaCPanel::UI;
|
|||||||
|
|
||||||
my $cpanel = Cpanel::LiveAPI->new();
|
my $cpanel = Cpanel::LiveAPI->new();
|
||||||
print "Content-Type: text/html\r\n\r\n";
|
print "Content-Type: text/html\r\n\r\n";
|
||||||
print $cpanel->header('GNIZA Backups');
|
print $cpanel->header('');
|
||||||
|
|
||||||
# Get allowed remotes via AdminBin
|
# Get allowed remotes via AdminBin
|
||||||
my $remotes_raw = eval { Cpanel::AdminBin::Call::call('Gniza', 'Restore', 'LIST_ALLOWED_REMOTES') } // '';
|
my $remotes_raw = eval { Cpanel::AdminBin::Call::call('Gniza', 'Restore', 'LIST_ALLOWED_REMOTES') } // '';
|
||||||
my @remotes = grep { $_ ne '' } split /\n/, $remotes_raw;
|
my @remotes = grep { $_ ne '' } split /\n/, $remotes_raw;
|
||||||
|
|
||||||
print GnizaCPanel::UI::page_header('GNIZA Backups');
|
print GnizaCPanel::UI::page_header('GNIZA Backups');
|
||||||
|
print GnizaCPanel::UI::render_nav('index.live.cgi');
|
||||||
print GnizaCPanel::UI::render_flash();
|
print GnizaCPanel::UI::render_flash();
|
||||||
|
|
||||||
if (!@remotes) {
|
if (!@remotes) {
|
||||||
|
|||||||
223
cpanel/gniza/logs.live.cgi
Normal file
223
cpanel/gniza/logs.live.cgi
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
#!/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;
|
||||||
@@ -195,8 +195,9 @@ sub handle_step2 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
print "Content-Type: text/html\r\n\r\n";
|
print "Content-Type: text/html\r\n\r\n";
|
||||||
print $cpanel->header('GNIZA Backups');
|
print $cpanel->header('');
|
||||||
print GnizaCPanel::UI::page_header('Restore Options');
|
print GnizaCPanel::UI::page_header('Restore Options');
|
||||||
|
print GnizaCPanel::UI::render_nav('restore.live.cgi');
|
||||||
print GnizaCPanel::UI::render_flash();
|
print GnizaCPanel::UI::render_flash();
|
||||||
|
|
||||||
my $esc_remote = GnizaCPanel::UI::esc($remote);
|
my $esc_remote = GnizaCPanel::UI::esc($remote);
|
||||||
@@ -901,8 +902,9 @@ sub handle_step3 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
print "Content-Type: text/html\r\n\r\n";
|
print "Content-Type: text/html\r\n\r\n";
|
||||||
print $cpanel->header('GNIZA Backups');
|
print $cpanel->header('');
|
||||||
print GnizaCPanel::UI::page_header('Restore: Confirm');
|
print GnizaCPanel::UI::page_header('Restore: Confirm');
|
||||||
|
print GnizaCPanel::UI::render_nav('restore.live.cgi');
|
||||||
print GnizaCPanel::UI::render_flash();
|
print GnizaCPanel::UI::render_flash();
|
||||||
|
|
||||||
my $esc_remote = GnizaCPanel::UI::esc($remote);
|
my $esc_remote = GnizaCPanel::UI::esc($remote);
|
||||||
@@ -1020,8 +1022,9 @@ sub handle_step4 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
print "Content-Type: text/html\r\n\r\n";
|
print "Content-Type: text/html\r\n\r\n";
|
||||||
print $cpanel->header('GNIZA Backups');
|
print $cpanel->header('');
|
||||||
print GnizaCPanel::UI::page_header('Restore Results');
|
print GnizaCPanel::UI::page_header('Restore Results');
|
||||||
|
print GnizaCPanel::UI::render_nav('restore.live.cgi');
|
||||||
|
|
||||||
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};
|
||||||
print qq{<h2 class="card-title text-sm">Restore Results</h2>\n};
|
print qq{<h2 class="card-title text-sm">Restore Results</h2>\n};
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ if [[ -d "$CPANEL_BASE" ]]; then
|
|||||||
mkdir -p "$CPANEL_BASE/gniza/lib/GnizaCPanel" "$CPANEL_BASE/gniza/assets"
|
mkdir -p "$CPANEL_BASE/gniza/lib/GnizaCPanel" "$CPANEL_BASE/gniza/assets"
|
||||||
cp "$SOURCE_DIR/cpanel/gniza/index.live.cgi" "$CPANEL_BASE/gniza/"
|
cp "$SOURCE_DIR/cpanel/gniza/index.live.cgi" "$CPANEL_BASE/gniza/"
|
||||||
cp "$SOURCE_DIR/cpanel/gniza/restore.live.cgi" "$CPANEL_BASE/gniza/"
|
cp "$SOURCE_DIR/cpanel/gniza/restore.live.cgi" "$CPANEL_BASE/gniza/"
|
||||||
|
cp "$SOURCE_DIR/cpanel/gniza/logs.live.cgi" "$CPANEL_BASE/gniza/"
|
||||||
chmod +x "$CPANEL_BASE/gniza/"*.cgi
|
chmod +x "$CPANEL_BASE/gniza/"*.cgi
|
||||||
cp "$SOURCE_DIR/cpanel/gniza/lib/GnizaCPanel/UI.pm" "$CPANEL_BASE/gniza/lib/GnizaCPanel/"
|
cp "$SOURCE_DIR/cpanel/gniza/lib/GnizaCPanel/UI.pm" "$CPANEL_BASE/gniza/lib/GnizaCPanel/"
|
||||||
cp "$SOURCE_DIR/cpanel/gniza/assets/gniza-whm.css" "$CPANEL_BASE/gniza/assets/"
|
cp "$SOURCE_DIR/cpanel/gniza/assets/gniza-whm.css" "$CPANEL_BASE/gniza/assets/"
|
||||||
|
|||||||
@@ -52,17 +52,27 @@ sub show_list {
|
|||||||
my @files;
|
my @files;
|
||||||
if (opendir my $dh, $log_dir) {
|
if (opendir my $dh, $log_dir) {
|
||||||
while (my $entry = readdir $dh) {
|
while (my $entry = readdir $dh) {
|
||||||
next unless $entry =~ /^gniza-\d{8}-\d{6}\.log$/;
|
next unless $entry =~ /^gniza-\d{8}-\d{6}\.log$/ || $entry =~ /^cpanel-[a-z][a-z0-9_-]*\.log$/;
|
||||||
my $path = "$log_dir/$entry";
|
my $path = "$log_dir/$entry";
|
||||||
next unless -f $path;
|
next unless -f $path;
|
||||||
my @stat = stat($path);
|
my @stat = stat($path);
|
||||||
my $status = _detect_log_status($path);
|
my ($type, $status, $owner);
|
||||||
|
if ($entry =~ /^cpanel-([a-z][a-z0-9_-]*)\.log$/) {
|
||||||
|
$type = 'User';
|
||||||
|
$owner = $1;
|
||||||
|
$status = _detect_activity_status($path);
|
||||||
|
} else {
|
||||||
|
$type = _detect_log_type($path);
|
||||||
|
$status = _detect_log_status($path);
|
||||||
|
$owner = '';
|
||||||
|
}
|
||||||
push @files, {
|
push @files, {
|
||||||
name => $entry,
|
name => $entry,
|
||||||
size => $stat[7] // 0,
|
size => $stat[7] // 0,
|
||||||
mtime => $stat[9] // 0,
|
mtime => $stat[9] // 0,
|
||||||
type => _detect_log_type($path),
|
type => $type,
|
||||||
status => $status,
|
status => $status,
|
||||||
|
owner => $owner,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
closedir $dh;
|
closedir $dh;
|
||||||
@@ -91,13 +101,14 @@ sub show_list {
|
|||||||
|
|
||||||
print qq{<div class="overflow-x-auto rounded-box border border-base-content/5 bg-base-100">\n};
|
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{<table class="table">\n};
|
||||||
print qq{<thead><tr><th>Filename</th><th>Type</th><th>Status</th><th>Date</th><th>Size</th><th></th></tr></thead>\n};
|
print qq{<thead><tr><th>Filename</th><th>Type</th><th>Owner</th><th>Status</th><th>Date</th><th>Size</th><th></th></tr></thead>\n};
|
||||||
print qq{<tbody>\n};
|
print qq{<tbody>\n};
|
||||||
|
|
||||||
for my $f (@page_files) {
|
for my $f (@page_files) {
|
||||||
my $esc_name = GnizaWHM::UI::esc($f->{name});
|
my $esc_name = GnizaWHM::UI::esc($f->{name});
|
||||||
my $badge = $f->{type} eq 'Cron' ? 'badge-neutral'
|
my $badge = $f->{type} eq 'Cron' ? 'badge-neutral'
|
||||||
: $f->{type} eq 'System' ? 'badge-warning'
|
: $f->{type} eq 'System' ? 'badge-warning'
|
||||||
|
: $f->{type} eq 'User' ? 'badge-secondary'
|
||||||
: 'badge-info';
|
: 'badge-info';
|
||||||
my $date = _format_time($f->{mtime});
|
my $date = _format_time($f->{mtime});
|
||||||
my $size = _human_size($f->{size});
|
my $size = _human_size($f->{size});
|
||||||
@@ -106,6 +117,8 @@ sub show_list {
|
|||||||
print qq{<tr>\n};
|
print qq{<tr>\n};
|
||||||
print qq{ <td><code>$esc_name</code></td>\n};
|
print qq{ <td><code>$esc_name</code></td>\n};
|
||||||
print qq{ <td><span class="badge $badge badge-sm">$f->{type}</span></td>\n};
|
print qq{ <td><span class="badge $badge badge-sm">$f->{type}</span></td>\n};
|
||||||
|
my $esc_owner = GnizaWHM::UI::esc($f->{owner} // '');
|
||||||
|
print qq{ <td>$esc_owner</td>\n};
|
||||||
my $status_badge = $f->{status} eq 'Error' ? 'badge-error'
|
my $status_badge = $f->{status} eq 'Error' ? 'badge-error'
|
||||||
: $f->{status} eq 'Warning' ? 'badge-warning'
|
: $f->{status} eq 'Warning' ? 'badge-warning'
|
||||||
: 'badge-success';
|
: 'badge-success';
|
||||||
@@ -181,6 +194,12 @@ sub show_file {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Detect activity log vs standard log
|
||||||
|
if ($filename =~ /^cpanel-([a-z][a-z0-9_-]*)\.log$/) {
|
||||||
|
_show_activity_file($filename, $filepath, $1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
# Read file
|
# Read file
|
||||||
my @lines;
|
my @lines;
|
||||||
if (open my $fh, '<', $filepath) {
|
if (open my $fh, '<', $filepath) {
|
||||||
@@ -313,6 +332,133 @@ sub show_file {
|
|||||||
print GnizaWHM::UI::page_footer();
|
print GnizaWHM::UI::page_footer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ── Activity Log Viewer (cpanel-*.log) ───────────────────────
|
||||||
|
|
||||||
|
sub _show_activity_file {
|
||||||
|
my ($filename, $filepath, $owner) = @_;
|
||||||
|
|
||||||
|
my $esc_name = GnizaWHM::UI::esc($filename);
|
||||||
|
my $esc_owner = GnizaWHM::UI::esc($owner);
|
||||||
|
my @stat = stat($filepath);
|
||||||
|
my $file_size = _human_size($stat[7] // 0);
|
||||||
|
my $file_date = _format_time($stat[9] // 0);
|
||||||
|
|
||||||
|
# Back link
|
||||||
|
print qq{<p class="mb-4"><a href="logs.cgi" class="link">← Back to logs</a></p>\n};
|
||||||
|
|
||||||
|
# File info card
|
||||||
|
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-3 items-center text-sm">\n};
|
||||||
|
print qq{ <span><strong>User:</strong> $esc_owner</span>\n};
|
||||||
|
print qq{ <span><strong>File:</strong> <code>$esc_name</code></span>\n};
|
||||||
|
print qq{ <span><strong>Size:</strong> $file_size</span>\n};
|
||||||
|
print qq{ <span><strong>Last Modified:</strong> $file_date</span>\n};
|
||||||
|
print qq{</div>\n</div>\n</div>\n};
|
||||||
|
|
||||||
|
# Parse activity entries
|
||||||
|
my @entries;
|
||||||
|
if (open my $fh, '<', $filepath) {
|
||||||
|
my $in_entry = 0;
|
||||||
|
my (%cur, @cur_output);
|
||||||
|
while (my $line = <$fh>) {
|
||||||
|
chomp $line;
|
||||||
|
if ($line eq '--- ENTRY ---') {
|
||||||
|
$in_entry = 1;
|
||||||
|
%cur = ();
|
||||||
|
@cur_output = ();
|
||||||
|
} elsif ($line eq '--- END ---' && $in_entry) {
|
||||||
|
$cur{output} = join("\n", @cur_output) if @cur_output;
|
||||||
|
push @entries, { %cur } if $cur{date};
|
||||||
|
$in_entry = 0;
|
||||||
|
} elsif ($in_entry) {
|
||||||
|
if ($line =~ /^Date:\s+(.+)$/) { $cur{date} = $1; }
|
||||||
|
elsif ($line =~ /^Action:\s+(.+)$/) { $cur{action} = $1; }
|
||||||
|
elsif ($line =~ /^Details:\s+(.+)$/) { $cur{details} = $1; }
|
||||||
|
elsif ($line =~ /^Status:\s+(.+)$/) { $cur{status} = $1; }
|
||||||
|
else { push @cur_output, $line; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
close $fh;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Show newest first
|
||||||
|
@entries = reverse @entries;
|
||||||
|
|
||||||
|
if (!@entries) {
|
||||||
|
print qq{<div class="alert alert-info">No activity entries found.</div>\n};
|
||||||
|
print GnizaWHM::UI::page_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];
|
||||||
|
|
||||||
|
for my $e (@page_entries) {
|
||||||
|
my $status_badge = ($e->{status} // '') eq 'Error' ? 'badge-error' : 'badge-success';
|
||||||
|
my $esc_date = GnizaWHM::UI::esc($e->{date} // '');
|
||||||
|
my $esc_action = GnizaWHM::UI::esc($e->{action} // '');
|
||||||
|
my $esc_details = GnizaWHM::UI::esc($e->{details} // '');
|
||||||
|
my $esc_status = GnizaWHM::UI::esc($e->{status} // '');
|
||||||
|
|
||||||
|
print qq{<div class="card bg-base-100 border border-base-300 mb-3">\n};
|
||||||
|
print qq{<div class="card-body py-3 px-4">\n};
|
||||||
|
print qq{<div class="flex flex-wrap gap-3 items-center text-sm mb-1">\n};
|
||||||
|
print qq{ <span>$esc_date</span>\n};
|
||||||
|
print qq{ <span class="badge badge-info badge-sm">$esc_action</span>\n};
|
||||||
|
print qq{ <span class="badge $status_badge badge-sm">$esc_status</span>\n};
|
||||||
|
print qq{</div>\n};
|
||||||
|
print qq{<div class="text-sm text-base-content/70">$esc_details</div>\n};
|
||||||
|
|
||||||
|
if ($e->{output} && $e->{output} ne '') {
|
||||||
|
print qq{<details class="mt-2">\n};
|
||||||
|
print qq{<summary class="cursor-pointer text-sm font-medium">Command Output</summary>\n};
|
||||||
|
print qq{<pre class="bg-base-200 rounded p-3 mt-1 text-xs font-mono overflow-x-auto leading-relaxed">};
|
||||||
|
for my $oline (split /\n/, $e->{output}) {
|
||||||
|
my $esc = GnizaWHM::UI::esc($oline);
|
||||||
|
if ($oline =~ /\[ERROR\]/) {
|
||||||
|
print qq{<span class="text-error font-bold">$esc</span>\n};
|
||||||
|
} elsif ($oline =~ /\[WARN\]/) {
|
||||||
|
print qq{<span class="text-warning">$esc</span>\n};
|
||||||
|
} else {
|
||||||
|
print "$esc\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
print qq{</pre>\n};
|
||||||
|
print qq{</details>\n};
|
||||||
|
}
|
||||||
|
|
||||||
|
print qq{</div>\n</div>\n};
|
||||||
|
}
|
||||||
|
|
||||||
|
# Pagination controls
|
||||||
|
if ($total_pages > 1) {
|
||||||
|
my $base = 'logs.cgi?file=' . _uri_escape($filename);
|
||||||
|
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='$base&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='$base&page=$next'">Next »</button>\n};
|
||||||
|
}
|
||||||
|
print qq{</div>\n};
|
||||||
|
}
|
||||||
|
|
||||||
|
print GnizaWHM::UI::page_footer();
|
||||||
|
}
|
||||||
|
|
||||||
# ── Helpers ───────────────────────────────────────────────────
|
# ── Helpers ───────────────────────────────────────────────────
|
||||||
|
|
||||||
sub _valid_log_filename {
|
sub _valid_log_filename {
|
||||||
@@ -320,9 +466,26 @@ sub _valid_log_filename {
|
|||||||
return 0 unless defined $name && $name ne '';
|
return 0 unless defined $name && $name ne '';
|
||||||
return 1 if $name =~ /^gniza-\d{8}-\d{6}\.log$/;
|
return 1 if $name =~ /^gniza-\d{8}-\d{6}\.log$/;
|
||||||
return 1 if $name =~ /^cron-[a-zA-Z0-9_-]+\.log$/;
|
return 1 if $name =~ /^cron-[a-zA-Z0-9_-]+\.log$/;
|
||||||
|
return 1 if $name =~ /^cpanel-[a-z][a-z0-9_-]*\.log$/;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sub _detect_activity_status {
|
||||||
|
my ($path) = @_;
|
||||||
|
return 'Success' unless -f $path;
|
||||||
|
my $has_error = 0;
|
||||||
|
if (open my $fh, '<', $path) {
|
||||||
|
while (my $line = <$fh>) {
|
||||||
|
if ($line =~ /^Status:\s+Error$/) {
|
||||||
|
$has_error = 1;
|
||||||
|
last;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
close $fh;
|
||||||
|
}
|
||||||
|
return $has_error ? 'Error' : 'Success';
|
||||||
|
}
|
||||||
|
|
||||||
sub _detect_log_type {
|
sub _detect_log_type {
|
||||||
my ($path) = @_;
|
my ($path) = @_;
|
||||||
if (open my $fh, '<', $path) {
|
if (open my $fh, '<', $path) {
|
||||||
|
|||||||
Reference in New Issue
Block a user