\n};
# SVG icons for stat cards (24x24, stroke-based)
my $icon_accounts = '';
my $icon_backed_up = '';
my $icon_snapshots = '';
my $icon_remotes = '';
my $icon_schedules = '';
my $icon_last_backup = '';
# Card 1: cPanel Accounts
my $acct_count = scalar @cpanel_accounts;
print qq{
\n};
print qq{
\n};
print qq{
\n};
print qq{
cPanel Accounts
\n};
print qq{
$icon_accounts
\n};
print qq{
\n};
print qq{
$acct_count
\n};
print qq{
on this server
\n};
print qq{
\n};
print qq{
\n};
# Card 2: Backed Up Accounts
print qq{
\n};
print qq{
\n};
print qq{
\n};
print qq{
Backed Up Accounts
\n};
print qq{
$icon_backed_up
\n};
print qq{
\n};
print qq{
$backed_up
\n};
my $remote_count_desc = scalar @remotes;
print qq{
\n};
# Active schedules
my $schedules = Gniza4cpWHM::Cron::get_current_schedules();
print qq{
\n
\n};
print qq{
Active Cron Schedules
\n};
if (keys %$schedules) {
print qq{
\n};
print qq{
Schedule
Timing
Remote Destination(s)
\n};
print qq{\n};
for my $name (sort keys %$schedules) {
my $esc_name = Gniza4cpWHM::UI::esc($name);
my ($timing, $remotes) = Gniza4cpWHM::Cron::cron_to_human($schedules->{$name});
my $esc_timing = Gniza4cpWHM::UI::esc($timing);
my $esc_remotes = Gniza4cpWHM::UI::esc($remotes);
print qq{
$esc_name
$esc_timing
$esc_remotes
\n};
}
print qq{\n
\n};
} else {
print qq{
No active gniza4cp cron entries.
\n};
}
print qq{
\n
\n};
# Overview
my $version = Gniza4cpWHM::UI::get_gniza4cp_version();
print qq{
\n
\n};
print qq{
Overview
\n};
print qq{
\n};
print qq{
gniza4cp version
} . Gniza4cpWHM::UI::esc($version) . qq{
\n};
print qq{
\n};
print qq{
\n
\n};
# Inline JavaScript for live status polling
print qq{
};
print Gniza4cpWHM::UI::page_footer();
Whostmgr::HTMLInterface::footer();
# --- JSON status endpoint ---
sub handle_status_json {
print "Content-Type: application/json\r\n\r\n";
my $lock_file = '/var/run/gniza4cp.lock';
my $running = 0;
my $pid = '';
# Check lock file for running PID
if (-f $lock_file) {
if (open my $fh, '<', $lock_file) {
$pid = <$fh> // '';
chomp $pid;
close $fh;
if ($pid =~ /^\d+$/ && kill(0, $pid)) {
$running = 1;
} else {
$pid = '';
}
}
}
# Find LOG_DIR from main config
my $log_dir = '/var/log/gniza4cp';
my $main_conf_file = '/etc/gniza4cp/gniza4cp.conf';
if (-f $main_conf_file) {
my $cfg = Gniza4cpWHM::Config::parse($main_conf_file, 'main');
$log_dir = $cfg->{LOG_DIR} if $cfg->{LOG_DIR} && $cfg->{LOG_DIR} ne '';
}
# Find most recent gniza4cp-*.log by mtime
my $log_file = '';
my @lines;
if (opendir my $dh, $log_dir) {
my @logs = sort {
(stat("$log_dir/$b"))[9] <=> (stat("$log_dir/$a"))[9]
} grep { /^gniza4cp-\d{8}-\d{6}\.log$/ } readdir($dh);
closedir $dh;
$log_file = $logs[0] // '';
}
# Read last 15 lines if running and log exists
if ($log_file && $running) {
my $path = "$log_dir/$log_file";
if (open my $fh, '<', $path) {
my @all = <$fh>;
close $fh;
my $start = @all > 15 ? @all - 15 : 0;
@lines = @all[$start .. $#all];
chomp @lines;
}
}
# Build JSON manually (no JSON module dependency)
my $json = '{"running":' . ($running ? 'true' : 'false');
$json .= ',"pid":"' . _json_esc($pid) . '"';
$json .= ',"log_file":"' . _json_esc($log_file) . '"';
$json .= ',"lines":[';
my @json_lines;
for my $line (@lines) {
my $level = 'INFO';
if ($line =~ /\]\s+\[(\w+)\]/) {
$level = uc($1);
}
push @json_lines, '{"level":"' . _json_esc($level) . '","text":"' . _json_esc($line) . '"}';
}
$json .= join(',', @json_lines);
$json .= ']}';
print $json;
}
sub _json_esc {
my ($s) = @_;
$s //= '';
$s =~ s/\\/\\\\/g;
$s =~ s/"/\\"/g;
$s =~ s/\n/\\n/g;
$s =~ s/\r/\\r/g;
$s =~ s/\t/\\t/g;
# Escape control characters
$s =~ s/([\x00-\x1f])/sprintf("\\u%04x", ord($1))/ge;
return $s;
}