Added server health page in CP

This commit is contained in:
Pinga 2024-12-08 13:31:07 +02:00
parent d04d940a14
commit 0d8eda7ea8
7 changed files with 306 additions and 2 deletions

View file

@ -7,6 +7,7 @@ use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Container\ContainerInterface; use Psr\Container\ContainerInterface;
use Nyholm\Psr7\Stream; use Nyholm\Psr7\Stream;
use Utopia\System\System;
class ReportsController extends Controller class ReportsController extends Controller
{ {
@ -100,4 +101,125 @@ class ReportsController extends Controller
// Output the CSV content to the response body // Output the CSV content to the response body
return $response->withBody($stream); return $response->withBody($stream);
} }
public function serverHealth(Request $request, Response $response)
{
if ($_SESSION["auth_roles"] != 0) {
return $response->withHeader('Location', '/dashboard')->withStatus(302);
}
$csrfTokenName = $this->container->get('csrf')->getTokenName();
$csrfTokenValue = $this->container->get('csrf')->getTokenValue();
$system = new System();
$serverHealth = [
'getCPUCores' => $system->getCPUCores(),
'getCPUUsage' => $system->getCPUUsage(),
'getMemoryTotal' => $system->getMemoryTotal(),
'getMemoryFree' => $system->getMemoryFree(),
'getDiskTotal' => $system->getDiskTotal(),
'getDiskFree' => $system->getDiskFree()
];
$logFile = '/var/log/namingo/backup.log';
// Check if the file exists
if (!file_exists($logFile)) {
$backupSummary = "Backup log file not found.";
} else {
// Read and decode JSON file
$logData = json_decode(file_get_contents($logFile), true);
if (json_last_error() !== JSON_ERROR_NONE || !is_array($logData)) {
$backupSummary = "Invalid JSON format in backup log file.";
} else {
// Start building the summary
$backupSummary = "Backup Summary:\n";
$backupSummary .= "Timestamp: " . date('Y-m-d H:i:s', $logData['timestamp'] ?? time()) . "\n";
$backupSummary .= "Duration: " . round($logData['duration'] ?? 0, 2) . " seconds\n";
$backupSummary .= "Total Backups: " . ($logData['backupCount'] ?? 0) . "\n";
$backupSummary .= "Failed Backups: " . ($logData['backupFailed'] ?? 0) . "\n";
$backupSummary .= "Errors: " . ($logData['errorCount'] ?? 0) . "\n";
if (!empty($logData['backups'])) {
foreach ($logData['backups'] as $backup) {
$backupSummary .= "\nBackup: " . ($backup['name'] ?? 'Unknown') . "\n";
$backupSummary .= "- Status: " . (($backup['status'] ?? 1) === 0 ? 'Success' : 'Failed') . "\n";
$backupSummary .= "- Checks: " . ($backup['checks']['executed'] ?? 0) . " executed, " . ($backup['checks']['failed'] ?? 0) . " failed\n";
$backupSummary .= "- Syncs: " . ($backup['syncs']['executed'] ?? 0) . " executed, " . ($backup['syncs']['failed'] ?? 0) . " failed\n";
$backupSummary .= "- Cleanup: " . ($backup['cleanup']['executed'] ?? 0) . " executed, " . ($backup['cleanup']['failed'] ?? 0) . " failed\n";
}
}
if (!empty($logData['debug'])) {
$backupSummary .= "\nDebug Info (last 5 entries):\n";
$debugEntries = array_slice($logData['debug'], -5);
foreach ($debugEntries as $entry) {
$backupSummary .= "- $entry\n";
}
}
}
}
return $this->view->render($response, 'admin/reports/serverHealth.twig', [
'serverHealth' => $serverHealth,
'csrfTokenName' => $csrfTokenName,
'csrfTokenValue' => $csrfTokenValue,
'backupLog' => nl2br(htmlspecialchars($backupSummary)),
]);
}
public function clearCache(Request $request, Response $response): Response
{
if ($_SESSION["auth_roles"] != 0) {
return $response->withHeader('Location', '/dashboard')->withStatus(302);
}
$result = [
'success' => true,
'message' => 'Cache cleared successfully!',
];
$cacheDir = '/var/www/cp/cache';
try {
// Check if the cache directory exists
if (!is_dir($cacheDir)) {
throw new RuntimeException('Cache directory does not exist.');
}
// Iterate through the files and directories in the cache directory
$files = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($cacheDir, \RecursiveDirectoryIterator::SKIP_DOTS),
\RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($files as $fileinfo) {
// Check if the parent directory name is exactly two letters/numbers long
if (preg_match('/^[a-zA-Z0-9]{2}$/', $fileinfo->getFilename()) ||
preg_match('/^[a-zA-Z0-9]{2}$/', basename(dirname($fileinfo->getPathname())))) {
$action = ($fileinfo->isDir() ? 'rmdir' : 'unlink');
$action($fileinfo->getRealPath());
}
}
// Delete the two-letter/number directories themselves
$dirs = new \DirectoryIterator($cacheDir);
foreach ($dirs as $dir) {
if ($dir->isDir() && !$dir->isDot() && preg_match('/^[a-zA-Z0-9]{2}$/', $dir->getFilename())) {
rmdir($dir->getRealPath());
}
}
} catch (Exception $e) {
$result = [
'success' => false,
'message' => 'Error clearing cache: ' . $e->getMessage(),
];
}
// Respond with the result as JSON
$response->getBody()->write(json_encode($result));
return $response->withHeader('Content-Type', 'application/json')->withStatus(200);
}
} }

View file

@ -266,6 +266,9 @@ $csrfMiddleware = function ($request, $handler) use ($container) {
if ($path && $path === '/create-crypto-payment') { if ($path && $path === '/create-crypto-payment') {
return $handler->handle($request); return $handler->handle($request);
} }
if ($path && $path === '/clear-cache') {
return $handler->handle($request);
}
// If not skipped, apply the CSRF Guard // If not skipped, apply the CSRF Guard
return $csrf->process($request, $handler); return $csrf->process($request, $handler);

View file

@ -45,7 +45,8 @@
"giggsey/libphonenumber-for-php-lite": "^8.13", "giggsey/libphonenumber-for-php-lite": "^8.13",
"egulias/email-validator": "^4.0", "egulias/email-validator": "^4.0",
"utopia-php/messaging": "^0.12.0", "utopia-php/messaging": "^0.12.0",
"brick/postcode": "^0.3.3" "brick/postcode": "^0.3.3",
"utopia-php/system": "^0.9.0"
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {

View file

@ -0,0 +1,169 @@
{% extends "layouts/app.twig" %}
{% block title %}{{ __('Server Health') }}{% endblock %}
{% block content %}
<link href="/assets/css/sweetalert2.min.css" rel="stylesheet">
<div class="page-wrapper">
<!-- Page header -->
<div class="page-header d-print-none">
<div class="container-xl">
<div class="row g-2 align-items-center">
<div class="col">
<!-- Page pre-title -->
<div class="page-pretitle">
{{ __('Overview') }}
</div>
<h2 class="page-title">
{{ __('Server Health') }}
</h2>
</div>
<!-- Page title actions -->
<div class="col-auto ms-auto d-print-none">
<div class="btn-list">
<button onclick="clearSystemCache()" class="btn btn-primary d-none d-sm-inline-block">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M19 20h-10.5l-4.21 -4.3a1 1 0 0 1 0 -1.41l10 -10a1 1 0 0 1 1.41 0l5 5a1 1 0 0 1 0 1.41l-9.2 9.3" /><path d="M18 13.3l-6.3 -6.3" /></svg>
{{ __('Clear Cache') }}
</button>
<button onclick="clearSystemCache()" class="btn btn-primary d-sm-none btn-icon" aria-label="{{ __('Clear Cache') }}">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M19 20h-10.5l-4.21 -4.3a1 1 0 0 1 0 -1.41l10 -10a1 1 0 0 1 1.41 0l5 5a1 1 0 0 1 0 1.41l-9.2 9.3" /><path d="M18 13.3l-6.3 -6.3" /></svg>
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Page body -->
<div class="page-body">
<div class="container-xl">
<div class="col-12">
<div class="row">
<!-- CPU and Memory Card -->
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h3 class="card-title">{{ __('CPU and Memory') }}</h3>
</div>
<div class="card-body">
<div class="d-flex align-items-center mb-3">
<div>
<h4 class="m-0">{{ __('CPU Cores') }}</h4>
<div class="text-muted">{{ serverHealth.getCPUCores }}</div>
</div>
<div class="ms-auto">
<span class="badge bg-primary text-primary-fg">{{ serverHealth.getCPUUsage }}%</span>
</div>
</div>
<div class="progress progress-xl">
<div class="progress-bar bg-primary" style="width: {{ serverHealth.getCPUUsage }}%;" role="progressbar" aria-valuenow="{{ serverHealth.getCPUUsage }}" aria-valuemin="0" aria-valuemax="100">
{{ serverHealth.getCPUUsage }}%
</div>
</div>
<hr>
<div class="d-flex align-items-center">
<div>
<h4 class="m-0">{{ __('Memory') }}</h4>
<div class="text-muted">{{ serverHealth.getMemoryFree }} {{ __('MB free of') }} {{ serverHealth.getMemoryTotal }} MB</div>
</div>
<div class="ms-auto">
<span class="badge bg-green text-green-fg">{{ (serverHealth.getMemoryFree / serverHealth.getMemoryTotal * 100)|round(1) }}%</span>
</div>
</div>
<div class="progress progress-xl">
<div class="progress-bar bg-success" style="width: {{ (serverHealth.getMemoryFree / serverHealth.getMemoryTotal * 100)|round(1) }}%;" role="progressbar" aria-valuenow="{{ (serverHealth.getMemoryFree / serverHealth.getMemoryTotal * 100)|round(1) }}" aria-valuemin="0" aria-valuemax="100">
{{ (serverHealth.getMemoryFree / serverHealth.getMemoryTotal * 100)|round(1) }}%
</div>
</div>
</div>
</div>
</div>
<!-- Disk and Network Card -->
<div class="col-md-6 mt-md-0 mt-2">
<div class="card">
<div class="card-header">
<h3 class="card-title">{{ __('Disk and Network') }}</h3>
</div>
<div class="card-body">
<div class="d-flex align-items-center mb-3">
<div>
<h4 class="m-0">{{ __('Disk Usage') }}</h4>
<div class="text-muted">{{ serverHealth.getDiskFree }} {{ __('GB free of') }} {{ serverHealth.getDiskTotal }} GB</div>
</div>
<div class="ms-auto">
<span class="badge bg-warning text-warning-fg">{{ (serverHealth.getDiskFree / serverHealth.getDiskTotal * 100)|round(1) }}%</span>
</div>
</div>
<div class="progress progress-xl">
<div class="progress-bar bg-warning" style="width: {{ (serverHealth.getDiskFree / serverHealth.getDiskTotal * 100)|round(1) }}%;" role="progressbar" aria-valuenow="{{ (serverHealth.getDiskFree / serverHealth.getDiskTotal * 100)|round(1) }}" aria-valuemin="0" aria-valuemax="100">
{{ (serverHealth.getDiskFree / serverHealth.getDiskTotal * 100)|round(1) }}%
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row mt-2">
<div class="col-md-12">
<div class="card">
<div class="card-header">
<h3 class="card-title">{{ __('Backup Log') }}</h3>
</div>
<div class="card-body">
<pre>{{ backupLog }}</pre>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% include 'partials/footer.twig' %}
</div>
<script>
var csrfTokenName = "{{ csrfTokenName }}";
var csrfTokenValue = "{{ csrfTokenValue }}";
function clearSystemCache() {
fetch('/clear-cache', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
[csrfTokenName]: csrfTokenValue, // Include CSRF token in headers
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
Swal.fire({
icon: 'success',
title: 'Success',
text: data.message,
confirmButtonText: 'OK'
}).then(() => {
window.location.reload(); // Reload the page after user acknowledges
});
} else {
Swal.fire({
icon: 'error',
title: 'Error',
text: data.message,
confirmButtonText: 'OK'
});
}
})
.catch(err => {
Swal.fire({
icon: 'error',
title: 'Error',
text: 'Error clearing cache: ' + err.message,
confirmButtonText: 'OK'
});
});
}
</script>
{% endblock %}

View file

@ -206,7 +206,7 @@
</a> </a>
</div> </div>
</li> </li>
<li {{ is_current_url('epphistory') or is_current_url('poll') or is_current_url('log') or is_current_url('registry') or is_current_url('reports') or is_current_url('listTlds') or is_current_url('createTld') or 'tld' in currentUri or 'reserved' in currentUri or 'tokens' in currentUri or (roles != 0 and 'registrar' in currentUri) ? 'class="nav-item dropdown active"' : 'class="nav-item dropdown"' }}> <li {{ is_current_url('epphistory') or is_current_url('poll') or is_current_url('log') or is_current_url('registry') or is_current_url('reports') or is_current_url('serverHealth') or is_current_url('listTlds') or is_current_url('createTld') or 'tld' in currentUri or 'reserved' in currentUri or 'tokens' in currentUri or (roles != 0 and 'registrar' in currentUri) ? 'class="nav-item dropdown active"' : 'class="nav-item dropdown"' }}>
<a class="nav-link dropdown-toggle" href="#" data-bs-toggle="dropdown" data-bs-auto-close="outside" role="button" aria-expanded="false"> <a class="nav-link dropdown-toggle" href="#" data-bs-toggle="dropdown" data-bs-auto-close="outside" role="button" aria-expanded="false">
<span class="nav-link-icon d-md-none d-lg-inline-block"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M10.325 4.317c.426 -1.756 2.924 -1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543 -.94 3.31 .826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.756 .426 1.756 2.924 0 3.35a1.724 1.724 0 0 0 -1.066 2.573c.94 1.543 -.826 3.31 -2.37 2.37a1.724 1.724 0 0 0 -2.572 1.065c-.426 1.756 -2.924 1.756 -3.35 0a1.724 1.724 0 0 0 -2.573 -1.066c-1.543 .94 -3.31 -.826 -2.37 -2.37a1.724 1.724 0 0 0 -1.065 -2.572c-1.756 -.426 -1.756 -2.924 0 -3.35a1.724 1.724 0 0 0 1.066 -2.573c-.94 -1.543 .826 -3.31 2.37 -2.37c1 .608 2.296 .07 2.572 -1.065z"></path> <path d="M9 12a3 3 0 1 0 6 0a3 3 0 0 0 -6 0"></path></svg> <span class="nav-link-icon d-md-none d-lg-inline-block"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M10.325 4.317c.426 -1.756 2.924 -1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543 -.94 3.31 .826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.756 .426 1.756 2.924 0 3.35a1.724 1.724 0 0 0 -1.066 2.573c.94 1.543 -.826 3.31 -2.37 2.37a1.724 1.724 0 0 0 -2.572 1.065c-.426 1.756 -2.924 1.756 -3.35 0a1.724 1.724 0 0 0 -2.573 -1.066c-1.543 .94 -3.31 -.826 -2.37 -2.37a1.724 1.724 0 0 0 -1.065 -2.572c-1.756 -.426 -1.756 -2.924 0 -3.35a1.724 1.724 0 0 0 1.066 -2.573c-.94 -1.543 .826 -3.31 2.37 -2.37c1 .608 2.296 .07 2.572 -1.065z"></path> <path d="M9 12a3 3 0 1 0 6 0a3 3 0 0 0 -6 0"></path></svg>
</span> </span>
@ -227,6 +227,9 @@
<a class="dropdown-item" href="{{route('reports')}}"> <a class="dropdown-item" href="{{route('reports')}}">
{{ __('Reports') }} {{ __('Reports') }}
</a> </a>
<a class="dropdown-item" href="{{route('serverHealth')}}">
{{ __('Server Health') }}
</a>
<div class="dropdown-divider"></div>{% endif %} <div class="dropdown-divider"></div>{% endif %}
<a class="dropdown-item" href="{{route('poll')}}"> <a class="dropdown-item" href="{{route('poll')}}">
{{ __('Message Queue') }} {{ __('Message Queue') }}
@ -307,6 +310,8 @@
{% include 'partials/js-tlds.twig' %} {% include 'partials/js-tlds.twig' %}
{% elseif route_is('profile') %} {% elseif route_is('profile') %}
{% include 'partials/js-profile.twig' %} {% include 'partials/js-profile.twig' %}
{% elseif route_is('server') %}
{% include 'partials/js-server.twig' %}
{% else %} {% else %}
{% include 'partials/js.twig' %} {% include 'partials/js.twig' %}
{% endif %} {% endif %}

View file

@ -0,0 +1,2 @@
<script src="/assets/js/sweetalert2.min.js" defer></script>
<script src="/assets/js/tabler.min.js" defer></script>

View file

@ -108,6 +108,8 @@ $app->group('', function ($route) {
$route->get('/reports', ReportsController::class .':view')->setName('reports'); $route->get('/reports', ReportsController::class .':view')->setName('reports');
$route->get('/export', ReportsController::class .':exportDomains')->setName('exportDomains'); $route->get('/export', ReportsController::class .':exportDomains')->setName('exportDomains');
$route->get('/server', ReportsController::class .':serverHealth')->setName('serverHealth');
$route->post('/clear-cache', ReportsController::class .':clearCache')->setName('clearCache');
$route->get('/invoices', FinancialsController::class .':invoices')->setName('invoices'); $route->get('/invoices', FinancialsController::class .':invoices')->setName('invoices');
$route->get('/invoice/{invoice}', FinancialsController::class . ':viewInvoice')->setName('viewInvoice'); $route->get('/invoice/{invoice}', FinancialsController::class . ':viewInvoice')->setName('viewInvoice');