Initial import of borg-restore

This commit is contained in:
Marek Miklewicz
2026-07-04 11:20:44 +02:00
commit bd66a92f30
24 changed files with 11365 additions and 0 deletions
+787
View File
@@ -0,0 +1,787 @@
#!/usr/local/bin/php -nc/usr/local/directadmin/plugins/borg-restore/php.ini
<?php
function h(string $value): string
{
return htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
}
function parse_sh_value(string $value): string
{
$value = trim($value);
if ($value === '') {
return '';
}
if ($value[0] === "'" && substr($value, -1) === "'") {
return substr($value, 1, -1);
}
if ($value[0] === '"' && substr($value, -1) === '"') {
return stripcslashes(substr($value, 1, -1));
}
$value = preg_replace('/\s+#.*$/', '', $value);
return trim((string)$value);
}
function parse_bool(string $value, bool $default = false): bool
{
$normalized = strtolower(trim($value));
if ($normalized === '') {
return $default;
}
if (in_array($normalized, ['1', 'true', 'yes', 'y', 'on'], true)) {
return true;
}
if (in_array($normalized, ['0', 'false', 'no', 'n', 'off'], true)) {
return false;
}
return $default;
}
function read_params(): array
{
$params = [];
if (!empty($_GET)) {
$params = array_merge($params, $_GET);
}
if (!empty($_POST)) {
$params = array_merge($params, $_POST);
}
$get_raw = getenv('GET') ?: getenv('get') ?: '';
if ($get_raw !== '') {
parse_str($get_raw, $parsed);
$params = array_merge($parsed, $params);
}
$qs = $_SERVER['QUERY_STRING'] ?? '';
if ($qs !== '') {
parse_str($qs, $parsed);
$params = array_merge($parsed, $params);
}
return $params;
}
function load_settings(string $path): array
{
$settings = [
'BB_PATH' => '/opt/bb',
'BORG_VARIABLES_FILE' => '/opt/bb/bbvars.sh',
'ADMIN_MODE_ENABLE' => 'TRUE',
'RESULTS_PER_PAGE' => '10',
'DEBUG_MODE' => 'FALSE',
];
if (!is_readable($path)) {
return $settings;
}
$lines = file($path, FILE_IGNORE_NEW_LINES);
if ($lines === false) {
return $settings;
}
foreach ($lines as $line) {
$line = trim($line);
if ($line === '' || $line[0] === '#') {
continue;
}
if (preg_match('/^(BB_PATH|BORG_VARIABLES_FILE|ADMIN_MODE_ENABLE|RESULTS_PER_PAGE|DEBUG_MODE)=(.*)$/', $line, $m)) {
$settings[$m[1]] = parse_sh_value($m[2]);
}
}
return $settings;
}
function load_borg_vars(string $path): array
{
$vars = [
'BORG_PASSPHRASE' => null,
'REPO' => null,
];
if (!is_readable($path)) {
return $vars;
}
$lines = file($path, FILE_IGNORE_NEW_LINES);
if ($lines === false) {
return $vars;
}
foreach ($lines as $line) {
$line = trim($line);
if ($line === '' || $line[0] === '#') {
continue;
}
if (preg_match('/^(?:export\\s+)?(BORG_PASSPHRASE|REPO)=(.*)$/', $line, $m)) {
$vars[$m[1]] = parse_sh_value($m[2]);
}
}
return $vars;
}
function load_user_skin(string $user): string
{
$path = '/usr/local/directadmin/data/users/' . $user . '/user.conf';
if (!is_readable($path)) {
return 'enhanced';
}
$lines = file($path, FILE_IGNORE_NEW_LINES);
if ($lines === false) {
return 'enhanced';
}
foreach ($lines as $line) {
$line = trim($line);
if ($line === '' || $line[0] === '#') {
continue;
}
if (preg_match('/^skin=(.+)$/', $line, $m)) {
$skin = strtolower(trim($m[1]));
return $skin !== '' ? $skin : 'enhanced';
}
}
return 'enhanced';
}
function load_job_vars(string $path): array
{
$vars = [
'DA_USER' => '',
'REPONAME' => '',
'USERRESTOREPATH' => '',
'TARGETPATH' => '',
'RESTOREMODE' => '',
'RESTORETYPE' => '',
'START_TS' => '',
];
if (!is_readable($path)) {
return $vars;
}
$lines = file($path, FILE_IGNORE_NEW_LINES);
if ($lines === false) {
return $vars;
}
foreach ($lines as $line) {
$line = trim($line);
if ($line === '' || $line[0] === '#') {
continue;
}
if (preg_match('/^(DA_USER|REPONAME|USERRESTOREPATH|TARGETPATH|RESTOREMODE|RESTORETYPE|START_TS|USERLANG)=(.*)$/', $line, $m)) {
$vars[$m[1]] = parse_sh_value($m[2]);
}
}
return $vars;
}
function job_id_from_file(string $file): string
{
$base = basename($file);
if (substr($base, -3) === '.ok') {
$base = substr($base, 0, -3);
} elseif (substr($base, -5) === '.fail') {
$base = substr($base, 0, -5);
} elseif (substr($base, -7) === '.cancel') {
$base = substr($base, 0, -7);
}
if (substr($base, -4) === '.env') {
$base = substr($base, 0, -4);
}
return $base;
}
function find_job_file(string $job_base, string $file, string $job_id): string
{
$candidates = [
$job_base . '/processing/' . $file,
$job_base . '/pending/' . $file,
$job_base . '/done/' . $file,
];
if ($job_id !== '') {
$candidates[] = $job_base . '/processing/' . $job_id . '.env';
$candidates[] = $job_base . '/pending/' . $job_id . '.env';
$candidates[] = $job_base . '/done/' . $job_id . '.ok';
$candidates[] = $job_base . '/done/' . $job_id . '.fail';
$candidates[] = $job_base . '/done/' . $job_id . '.cancel';
$candidates[] = $job_base . '/done/' . $job_id . '.env.ok';
$candidates[] = $job_base . '/done/' . $job_id . '.env.fail';
$candidates[] = $job_base . '/done/' . $job_id . '.env.cancel';
}
foreach ($candidates as $path) {
if (is_file($path)) {
return $path;
}
}
return '';
}
function read_log_tail(string $path, int $max_bytes = 200000): string
{
if (!is_readable($path)) {
return '';
}
$size = filesize($path);
if ($size === false || $size <= $max_bytes) {
return (string)file_get_contents($path);
}
$fp = fopen($path, 'rb');
if ($fp === false) {
return '';
}
fseek($fp, -$max_bytes, SEEK_END);
$data = fread($fp, $max_bytes);
fclose($fp);
return (string)$data;
}
function normalize_log(string $text): string
{
$text = str_replace("\r", "\n", $text);
$lines = preg_split('/\n/', $text);
if (!is_array($lines)) {
return $text;
}
$filtered = [];
foreach ($lines as $line) {
$trimmed = trim($line);
if ($trimmed !== '' && stripos($trimmed, 'Permanently added') !== false && stripos($trimmed, 'known host') !== false) {
continue;
}
$filtered[] = $line;
}
return implode("\n", $filtered);
}
function last_non_empty_line(string $text): string
{
$lines = preg_split('/\r\n|\n|\r/', $text);
if (!is_array($lines)) {
return '';
}
for ($i = count($lines) - 1; $i >= 0; $i--) {
$line = trim((string)$lines[$i]);
if ($line !== '') {
return $line;
}
}
return '';
}
function is_calculating_line(string $line): bool
{
return (bool)preg_match('/calculat/i', $line);
}
function extract_progress(string $line): ?int
{
if (preg_match('/\b(\d{1,3})%\b/', $line, $m)) {
$p = (int)$m[1];
if ($p < 0) {
$p = 0;
} elseif ($p > 100) {
$p = 100;
}
return $p;
}
return null;
}
function get_job_status(string $job_id, string $job_base): string
{
if ($job_id === '') {
return 'unknown';
}
if (is_file($job_base . '/processing/' . $job_id . '.env')) {
return 'processing';
}
if (is_file($job_base . '/pending/' . $job_id . '.env')) {
return 'pending';
}
if (is_file($job_base . '/done/' . $job_id . '.ok') || is_file($job_base . '/done/' . $job_id . '.env.ok')) {
return 'done';
}
if (is_file($job_base . '/done/' . $job_id . '.fail') || is_file($job_base . '/done/' . $job_id . '.env.fail')) {
return 'failed';
}
if (is_file($job_base . '/done/' . $job_id . '.cancel') || is_file($job_base . '/done/' . $job_id . '.env.cancel')) {
return 'canceled';
}
return 'unknown';
}
function display_status(string $base_status, string $last_line): string
{
if ($base_status === 'processing' && $last_line !== '' && is_calculating_line($last_line)) {
return 'calculating';
}
return $base_status;
}
function normalize_admin_status(string $status): string
{
if (in_array($status, ['pending', 'processing', 'calculating'], true)) {
return 'processing';
}
if ($status === 'done') {
return 'done';
}
if (in_array($status, ['failed', 'canceled'], true)) {
return 'failed';
}
return 'failed';
}
function admin_status_label(string $status): string
{
if ($status === 'done') {
return 'Zakończone';
}
if ($status === 'processing') {
return 'W trakcie';
}
return 'Błąd';
}
function admin_status_sort(string $status): int
{
if ($status === 'processing') {
return 1;
}
if ($status === 'done') {
return 2;
}
return 3;
}
function restore_mode_label(string $mode): string
{
if ($mode === 'overwrite') {
return 'Nadpisz aktualne dane';
}
if ($mode === 'update') {
return 'Scalenie';
}
if ($mode === 'location') {
return 'Przywracanie do lokalizacji';
}
return '-';
}
function restore_type_label(string $type): string
{
if ($type === 'mail') {
return 'Zawartość skrzynki pocztowej';
}
if ($type === 'web') {
return 'Pliki www';
}
return '-';
}
function display_restore_path(string $value): string
{
$trimmed = trim($value);
if ($trimmed === '') {
return '-';
}
if ($trimmed[0] !== '/') {
$trimmed = '/' . $trimmed;
}
return $trimmed;
}
function log_missing_include_explanation(string $log_text, string $restore_path_label = ''): string
{
if (stripos($log_text, 'W przywracanej kopii zapasowej nie odnaleziono wskazanej ścieżki') !== false) {
return '';
}
if (stripos($log_text, 'The specified restore path was not found in the backup') !== false) {
return '';
}
if (preg_match("/Include pattern '.*' never matched\\./i", $log_text)) {
$suffix = '';
if ($restore_path_label !== '' && $restore_path_label !== '-') {
$suffix = ' (' . $restore_path_label . ')';
}
return 'W przywracanej kopii zapasowej nie odnaleziono wskazanej ścieżki' . $suffix . '. Prosimy spróbować przywrócić dane z innego archiwum, w którym te pliki mogły się znajdować.';
}
return '';
}
function collect_jobs(string $job_base, string $log_dir): array
{
$rows = [];
$sources = [
['dir' => $job_base . '/pending', 'pattern' => '*.env', 'status' => 'pending'],
['dir' => $job_base . '/processing', 'pattern' => '*.env', 'status' => 'processing'],
['dir' => $job_base . '/done', 'pattern' => '*.ok', 'status' => 'done'],
['dir' => $job_base . '/done', 'pattern' => '*.env.ok', 'status' => 'done'],
['dir' => $job_base . '/done', 'pattern' => '*.fail', 'status' => 'failed'],
['dir' => $job_base . '/done', 'pattern' => '*.env.fail', 'status' => 'failed'],
['dir' => $job_base . '/done', 'pattern' => '*.cancel', 'status' => 'canceled'],
['dir' => $job_base . '/done', 'pattern' => '*.env.cancel', 'status' => 'canceled'],
];
foreach ($sources as $source) {
foreach (glob($source['dir'] . '/' . $source['pattern']) ?: [] as $path) {
$file = basename($path);
if (isset($rows[$file])) {
continue;
}
$vars = load_job_vars($path);
$job_id = job_id_from_file($file);
if ($job_id === '') {
continue;
}
$log_path = $log_dir . '/' . $job_id . '.log';
$base_status = $source['status'];
if ($base_status === 'processing' && is_readable($log_path)) {
$tail = normalize_log(read_log_tail($log_path, 20000));
$last = last_non_empty_line($tail);
$base_status = display_status($base_status, $last);
}
$status = normalize_admin_status($base_status);
$start_ts = is_numeric($vars['START_TS'] ?? '') ? (int)$vars['START_TS'] : (int)(@filemtime($path) ?: 0);
if ($start_ts <= 0) {
$start_ts = time();
}
$rows[$file] = [
'file' => $file,
'job_id' => $job_id,
'user' => (string)($vars['DA_USER'] ?? ''),
'start_ts' => $start_ts,
'start_text' => date('Y-m-d H:i:s', $start_ts),
'type' => (string)($vars['RESTORETYPE'] ?? ''),
'type_label' => restore_type_label((string)($vars['RESTORETYPE'] ?? '')),
'mode' => (string)($vars['RESTOREMODE'] ?? ''),
'mode_label' => restore_mode_label((string)($vars['RESTOREMODE'] ?? '')),
'status' => $status,
'status_label' => admin_status_label($status),
'status_sort' => admin_status_sort($status),
];
}
}
return array_values($rows);
}
function compare_rows(array $a, array $b, string $sort, string $dir): int
{
$cmp = 0;
switch ($sort) {
case 'user':
$cmp = strcasecmp((string)$a['user'], (string)$b['user']);
break;
case 'type':
$cmp = strcasecmp((string)$a['type_label'], (string)$b['type_label']);
break;
case 'mode':
$cmp = strcasecmp((string)$a['mode_label'], (string)$b['mode_label']);
break;
case 'status':
$cmp = ((int)$a['status_sort']) <=> ((int)$b['status_sort']);
break;
case 'start':
default:
$cmp = ((int)$a['start_ts']) <=> ((int)$b['start_ts']);
break;
}
if ($cmp === 0) {
$cmp = ((int)$a['start_ts']) <=> ((int)$b['start_ts']);
}
if ($cmp === 0) {
$cmp = strcmp((string)$a['job_id'], (string)$b['job_id']);
}
return $dir === 'asc' ? $cmp : -$cmp;
}
function build_admin_url(array $base, array $override = []): string
{
$query = array_merge($base, $override);
foreach ($query as $key => $value) {
if ($value === null || $value === '') {
unset($query[$key]);
}
}
$qs = http_build_query($query);
return '/CMD_PLUGINS_ADMIN/borg-restore/index.html' . ($qs !== '' ? ('?' . $qs) : '');
}
$params = read_params();
$action = (string)($params['action'] ?? '');
$username = getenv('USERNAME');
if ($username === false || $username === '') {
$username = getenv('LOGNAME') ?: getenv('USER') ?: 'admin';
}
$plugin_dir = realpath(__DIR__ . '/..') ?: '/usr/local/directadmin/plugins/borg-restore';
$settings_path = $plugin_dir . '/plugin-settings.conf';
$settings = load_settings($settings_path);
$admin_mode_enabled = parse_bool((string)($settings['ADMIN_MODE_ENABLE'] ?? 'TRUE'), true);
$debug_mode = parse_bool((string)($settings['DEBUG_MODE'] ?? 'FALSE'), false);
$bb_path = trim((string)($settings['BB_PATH'] ?? '/opt/bb'));
$borg_vars_file = trim((string)($settings['BORG_VARIABLES_FILE'] ?? '/opt/bb/bbvars.sh'));
$bbvars_path = $borg_vars_file !== '' ? $borg_vars_file : $bb_path;
if ($bbvars_path !== '' && substr($bbvars_path, -3) !== '.sh') {
$bbvars_path = rtrim($bbvars_path, '/') . '/bbvars.sh';
}
$bbvars = load_borg_vars($bbvars_path);
$admin_missing_passphrase = $debug_mode && is_readable($bbvars_path) && trim((string)($bbvars['BORG_PASSPHRASE'] ?? '')) === '';
$results_per_page = (int)trim((string)($settings['RESULTS_PER_PAGE'] ?? '10'));
if ($results_per_page <= 0) {
$results_per_page = 10;
}
if ($results_per_page > 200) {
$results_per_page = 200;
}
$skin = load_user_skin($username);
$css_file = $skin === 'evolution' ? 'evolution.css' : 'enhanced.css';
$inline_css = '';
$css_path = $plugin_dir . '/user/' . $css_file;
if (is_readable($css_path)) {
$inline_css = (string)file_get_contents($css_path);
}
$job_base = '/usr/local/directadmin/plugins/borg-restore/data/jobs';
$log_dir = '/usr/local/directadmin/plugins/borg-restore/data/logs';
$allowed_sorts = ['user', 'start', 'type', 'mode', 'status'];
$sort = strtolower((string)($params['sort'] ?? 'start'));
if (!in_array($sort, $allowed_sorts, true)) {
$sort = 'start';
}
$dir = strtolower((string)($params['dir'] ?? 'desc')) === 'asc' ? 'asc' : 'desc';
$page = (int)($params['page'] ?? 1);
if ($page < 1) {
$page = 1;
}
$base_link_params = ['sort' => $sort, 'dir' => $dir, 'page' => $page];
$jobs = [];
$total = 0;
$total_pages = 1;
$page_start = 0;
$page_end = 0;
$page_jobs = [];
if ($admin_mode_enabled) {
$jobs = collect_jobs($job_base, $log_dir);
usort($jobs, static function (array $a, array $b) use ($sort, $dir): int {
return compare_rows($a, $b, $sort, $dir);
});
$total = count($jobs);
$total_pages = max(1, (int)ceil($total / $results_per_page));
if ($page > $total_pages) {
$page = $total_pages;
$base_link_params['page'] = $page;
}
$offset = ($page - 1) * $results_per_page;
$page_jobs = array_slice($jobs, $offset, $results_per_page);
if ($total > 0) {
$page_start = $offset + 1;
$page_end = min($offset + count($page_jobs), $total);
}
}
$view_log = null;
if ($admin_mode_enabled && $action === 'view_log') {
$file = basename((string)($params['job'] ?? ''));
if ($file !== '' && preg_match('/^[-A-Za-z0-9_.]+$/', $file)) {
$job_id = job_id_from_file($file);
$job_file_path = find_job_file($job_base, $file, $job_id);
if ($job_file_path !== '') {
$vars = load_job_vars($job_file_path);
$log_path = $log_dir . '/' . $job_id . '.log';
$log_message = is_readable($log_path) ? normalize_log((string)file_get_contents($log_path)) : 'Log nie istnieje.';
$restore_path_label = display_restore_path((string)($vars['USERRESTOREPATH'] ?? ''));
$explanation = log_missing_include_explanation($log_message, $restore_path_label);
if ($explanation !== '') {
$log_message = rtrim($log_message) . "\n\n" . $explanation;
}
$base_status = get_job_status($job_id, $job_base);
$last_line = last_non_empty_line($log_message);
$progress = extract_progress($last_line);
$display = display_status($base_status, $last_line);
$status = normalize_admin_status($display);
if ($status === 'done') {
$progress_text = '100%';
} elseif ($status === 'failed') {
$progress_text = '0%';
} else {
$progress_text = $progress !== null ? ($progress . '%') : ($last_line !== '' ? $last_line : '-');
}
$start_ts = is_numeric($vars['START_TS'] ?? '') ? (int)$vars['START_TS'] : (int)(@filemtime($job_file_path) ?: 0);
if ($start_ts <= 0) {
$start_ts = time();
}
$view_log = [
'job' => $file,
'user' => (string)($vars['DA_USER'] ?? '-'),
'start' => date('Y-m-d H:i:s', $start_ts),
'type' => restore_type_label((string)($vars['RESTORETYPE'] ?? '')),
'mode' => restore_mode_label((string)($vars['RESTOREMODE'] ?? '')),
'path' => $restore_path_label,
'target' => display_restore_path((string)($vars['TARGETPATH'] ?? '')),
'status' => admin_status_label($status),
'progress' => $progress_text,
'content' => $log_message,
];
} else {
$view_log = [
'error' => 'Nie znaleziono wskazanego zadania.',
];
}
} else {
$view_log = [
'error' => 'Nieprawidłowy identyfikator zadania.',
];
}
}
$sort_labels = [
'user' => 'Nazwa użytkownika',
'start' => 'Data rozpoczęcia',
'type' => 'Rodzaj przywracanych danych',
'mode' => 'Tryb przywracania',
'status' => 'Status',
];
$sort_arrow = static function (string $field) use ($sort, $dir): string {
if ($field !== $sort) {
return '';
}
return $dir === 'asc' ? ' ▲' : ' ▼';
};
$sort_link = static function (string $field) use ($sort, $dir, $base_link_params): string {
$next_dir = ($sort === $field && $dir === 'asc') ? 'desc' : 'asc';
return build_admin_url($base_link_params, ['sort' => $field, 'dir' => $next_dir, 'page' => 1]);
};
$view_back_url = build_admin_url($base_link_params, ['action' => null, 'job' => null]);
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>BorgBackup - Przywracanie plików (Admin)</title>
<?php if ($inline_css !== '') { ?>
<style><?php echo $inline_css; ?></style>
<?php } ?>
<style>
.br-admin-subtitle { margin: 0 0 8px; color: var(--br-muted); }
.br-sort-link { color: inherit; text-decoration: none; }
.br-sort-link:hover { text-decoration: underline; }
.br-col-log { width: 130px; text-align: center; }
.br-pagination {
margin-top: 12px;
display: flex;
justify-content: space-between;
align-items: center;
gap: 10px;
}
.br-pagination .br-page-meta { color: var(--br-muted); font-size: 13px; }
.br-pagination .br-page-actions { display: flex; gap: 8px; }
.br-log-content { max-height: 58vh; overflow: auto; }
</style>
</head>
<body>
<div class="br-container">
<div class="br-header">
<div class="br-title">BorgBackup - Przywracanie plików (Admin)</div>
</div>
<?php if (!$admin_mode_enabled) { ?>
<div class="br-card">
<div class="br-alert br-alert-warn">Poziom administratora został wyłączony</div>
</div>
<?php } else { ?>
<?php if ($view_log !== null) { ?>
<div class="br-card">
<div class="br-actions" style="justify-content:flex-start; margin-bottom: 10px;">
<a class="br-btn br-btn-ghost" href="<?php echo h($view_back_url); ?>">Powrót do zestawienia</a>
</div>
<?php if (!empty($view_log['error'])) { ?>
<div class="br-alert br-alert-error"><?php echo h((string)$view_log['error']); ?></div>
<?php } else { ?>
<h3 class="m-0" style="margin-bottom:10px;">Log zadania</h3>
<div class="br-log-meta">
<div><strong>Nazwa użytkownika:</strong> <?php echo h((string)$view_log['user']); ?></div>
<div><strong>Data rozpoczęcia:</strong> <?php echo h((string)$view_log['start']); ?></div>
<div><strong>Rodzaj przywracanych danych:</strong> <?php echo h((string)$view_log['type']); ?></div>
<div><strong>Tryb przywracania:</strong> <?php echo h((string)$view_log['mode']); ?></div>
<div><strong>Ścieżka do przywrócenia:</strong> <?php echo h((string)$view_log['path']); ?></div>
<div><strong>Lokalizacja docelowa:</strong> <?php echo h((string)$view_log['target']); ?></div>
</div>
<div class="br-status-line">Status: <strong><?php echo h((string)$view_log['status']); ?></strong></div>
<div class="br-status-line">Postęp: <strong><?php echo h((string)$view_log['progress']); ?></strong></div>
<pre class="br-log-content" id="log-content"><?php echo h((string)$view_log['content']); ?></pre>
<?php } ?>
</div>
<?php } ?>
<div class="br-card">
<h3 style="margin-bottom:6px;">Zestawienie operacji przywracania</h3>
<p class="br-admin-subtitle">Wyniki na stronę: <?php echo h((string)$results_per_page); ?></p>
<?php if ($admin_missing_passphrase) { ?>
<div class="br-alert br-alert-warn">
Brak zmiennej <strong>BORG_PASSPHRASE</strong> w pliku <?php echo h($bbvars_path); ?>.
</div>
<?php } ?>
<?php if (empty($jobs)) { ?>
<p class="br-muted">Brak operacji do wyświetlenia.</p>
<?php } else { ?>
<table class="br-table" border="1" cellpadding="6" cellspacing="0">
<thead>
<tr>
<?php foreach ($sort_labels as $field => $label) { ?>
<th>
<a class="br-sort-link" href="<?php echo h($sort_link($field)); ?>">
<?php echo h($label . $sort_arrow($field)); ?>
</a>
</th>
<?php } ?>
<th class="br-col-log">Logi</th>
</tr>
</thead>
<tbody>
<?php foreach ($page_jobs as $job) { ?>
<tr>
<td><?php echo h((string)$job['user']); ?></td>
<td><?php echo h((string)$job['start_text']); ?></td>
<td><?php echo h((string)$job['type_label']); ?></td>
<td><?php echo h((string)$job['mode_label']); ?></td>
<td><?php echo h((string)$job['status_label']); ?></td>
<td class="br-col-log">
<a class="br-btn br-btn-ghost br-btn-small" href="<?php echo h(build_admin_url($base_link_params, ['action' => 'view_log', 'job' => (string)$job['file']])); ?>">Pokaż logi</a>
</td>
</tr>
<?php } ?>
</tbody>
</table>
<div class="br-pagination">
<div class="br-page-meta">
<?php echo h('Strona ' . $page . ' z ' . $total_pages . ' (wyniki ' . $page_start . '-' . $page_end . ' z ' . $total . ')'); ?>
</div>
<div class="br-page-actions">
<?php if ($page > 1) { ?>
<a class="br-btn br-btn-ghost br-btn-small" href="<?php echo h(build_admin_url($base_link_params, ['page' => $page - 1])); ?>">Poprzednia</a>
<?php } ?>
<?php if ($page < $total_pages) { ?>
<a class="br-btn br-btn-ghost br-btn-small" href="<?php echo h(build_admin_url($base_link_params, ['page' => $page + 1])); ?>">Następna</a>
<?php } ?>
</div>
</div>
<?php } ?>
</div>
<?php } ?>
</div>
</body>
</html>