feat: add admin system operations panel with AJAX refresh and server monitoring
- Added SystemOps controller with index and panel endpoints for dynamic content refresh - Created system_ops_panel.php fragment for AJAX-based UI updates (no full page reload) - Implemented 30-second auto-refresh using AJAX instead of location.reload() - Fixed health badge contrast by adding explicit dark text color (#1f2937) - Added jQuery availability checks to prevent script errors on page load - Included server overview, resource usage, services, events, and update history - Integrated delegated event handlers for action buttons (Update, Restart, Reboot, Shutdown) - All buttons are protected with confirmation dialogs - Layout maintains NiceAdmin card styling and responsive design Routes added: - GET /admin/system-ops/panel - returns panel fragment for refresh
This commit is contained in:
parent
37631c27a0
commit
89b862db31
|
|
@ -1214,6 +1214,10 @@ $routes->group('setting', static function($routes) {
|
|||
$routes->get('admin/abdm-report-doctors', 'Setting\\AbdmReportDoctorSettings::index', ['filter' => $settingsAdminFilter]);
|
||||
$routes->post('admin/abdm-report-doctors/save', 'Setting\\AbdmReportDoctorSettings::save', ['filter' => $settingsAdminFilter]);
|
||||
$routes->get('admin/hospital-profile', 'Setting\\HospitalProfile::index');
|
||||
$routes->get('admin/system-ops', 'SystemOps::index', ['filter' => $settingsAdminFilter]);
|
||||
$routes->get('admin/system-ops/panel', 'SystemOps::panel', ['filter' => $settingsAdminFilter]);
|
||||
$routes->post('admin/system-ops/update', 'SystemOps::update', ['filter' => $settingsAdminFilter]);
|
||||
$routes->post('admin/system-ops/action', 'SystemOps::action', ['filter' => $settingsAdminFilter]);
|
||||
$routes->post('admin/hospital-profile/save', 'Setting\\HospitalProfile::save');
|
||||
$routes->post('admin/hospital-profile/delete-logo', 'Setting\\HospitalProfile::deleteLogo');
|
||||
$routes->post('admin/hospital-profile/reset', 'Setting\\HospitalProfile::reset');
|
||||
|
|
|
|||
48
app/Controllers/SystemOps.php
Normal file
48
app/Controllers/SystemOps.php
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Controllers\BaseController;
|
||||
use App\Libraries\SystemOperations;
|
||||
|
||||
class SystemOps extends BaseController
|
||||
{
|
||||
public function index(): string
|
||||
{
|
||||
$ops = new SystemOperations();
|
||||
$data = [
|
||||
'status' => $ops->collectStatus(),
|
||||
'history' => $ops->getHistory(),
|
||||
];
|
||||
|
||||
return view('Setting/Admin/system_ops', $data);
|
||||
}
|
||||
|
||||
public function panel(): string
|
||||
{
|
||||
$ops = new SystemOperations();
|
||||
$data = [
|
||||
'status' => $ops->collectStatus(),
|
||||
'history' => $ops->getHistory(),
|
||||
];
|
||||
|
||||
return view('Setting/Admin/system_ops_panel', $data);
|
||||
}
|
||||
|
||||
public function update()
|
||||
{
|
||||
$ops = new SystemOperations();
|
||||
$result = $ops->runUpdate();
|
||||
|
||||
return $this->response->setJSON($result);
|
||||
}
|
||||
|
||||
public function action()
|
||||
{
|
||||
$action = (string) $this->request->getPost('action');
|
||||
$ops = new SystemOperations();
|
||||
$result = $ops->runServerAction($action);
|
||||
|
||||
return $this->response->setJSON($result);
|
||||
}
|
||||
}
|
||||
99
app/Views/Setting/Admin/system_ops.php
Normal file
99
app/Views/Setting/Admin/system_ops.php
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
<section class="content">
|
||||
<div class="pagetitle">
|
||||
<h1>System Operations</h1>
|
||||
<nav>
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item">Admin</li>
|
||||
<li class="breadcrumb-item active">System Operations</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.ops-card { border: 1px solid #e2e8f0; border-radius: 14px; background: #fff; box-shadow: 0 8px 24px rgba(0,0,0,.04); }
|
||||
.ops-card .card-header { background: linear-gradient(135deg, #0d6efd, #0dcaf0); color: white; border-radius: 14px 14px 0 0; }
|
||||
.ops-pill { display:inline-block; padding:4px 10px; border-radius:999px; font-size:12px; font-weight:600; }
|
||||
.ops-pill.success { background:#d1fae5; color:#065f46; }
|
||||
.ops-pill.warning { background:#fef3c7; color:#92400e; }
|
||||
.ops-pill.danger { background:#fee2e2; color:#991b1b; }
|
||||
.ops-health { display:inline-flex; align-items:center; gap:6px; padding:6px 10px; border-radius:999px; font-size:12px; font-weight:600; background:#f8fafc; color:#1f2937; }
|
||||
.ops-health::before { content:''; width:8px; height:8px; border-radius:50%; background:#22c55e; display:inline-block; }
|
||||
.ops-health.warning::before { background:#f59e0b; }
|
||||
.ops-health.danger::before { background:#ef4444; }
|
||||
.ops-timeline { border-left:2px solid #e2e8f0; margin:0; padding-left:16px; }
|
||||
.ops-timeline li { position:relative; padding-bottom:12px; }
|
||||
.ops-timeline li::before { content:''; position:absolute; left:-22px; top:4px; width:10px; height:10px; border-radius:50%; background:#0d6efd; }
|
||||
</style>
|
||||
|
||||
<div id="systemOpsPanel">
|
||||
<?= view('Setting/Admin/system_ops_panel', ['status' => $status, 'history' => $history]) ?>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function setHealthState(state) {
|
||||
if (typeof $ === 'undefined') return;
|
||||
var badge = $('#serverHealthBadge');
|
||||
if (!badge.length) return;
|
||||
badge.removeClass('warning danger').addClass(state === 'warning' ? 'warning' : (state === 'danger' ? 'danger' : ''));
|
||||
badge.text(state === 'warning' ? 'Warning' : (state === 'danger' ? 'Critical' : 'Healthy'));
|
||||
}
|
||||
|
||||
function refreshPanel() {
|
||||
if (typeof $ === 'undefined') return;
|
||||
$.ajax({
|
||||
url: '<?= base_url('setting/admin/system-ops/panel') ?>',
|
||||
type: 'GET',
|
||||
dataType: 'html',
|
||||
success: function (html) {
|
||||
$('#systemOpsPanel').html(html);
|
||||
},
|
||||
error: function () {
|
||||
if (typeof notify === 'function') {
|
||||
notify('warning', 'Refresh', 'Unable to refresh system status right now.');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function postAction(url, data, button) {
|
||||
if (typeof $ === 'undefined') return;
|
||||
if (button) {
|
||||
button.disabled = true;
|
||||
}
|
||||
$.ajax({
|
||||
url: url,
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
data: data,
|
||||
success: function (response) {
|
||||
if (response && response.ok) {
|
||||
notify('success', 'Success', response.message || 'Completed');
|
||||
} else {
|
||||
notify('error', 'Attention', response.message || 'Action could not be completed');
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
notify('error', 'Attention', 'Request failed');
|
||||
},
|
||||
complete: function () {
|
||||
if (button) {
|
||||
button.disabled = false;
|
||||
}
|
||||
setTimeout(function(){ refreshPanel(); }, 1500);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof $ !== 'undefined') {
|
||||
setHealthState('healthy');
|
||||
setInterval(function(){ refreshPanel(); }, 30000);
|
||||
|
||||
$(document).off('click', '#btnUpdateSystem, #btnRestartWeb, #btnRestartPhp, #btnReboot, #btnShutdown')
|
||||
.on('click', '#btnUpdateSystem', function(){ if (!confirm('Run the system update workflow now?')) return; postAction('<?= base_url('setting/admin/system-ops/update') ?>', {}, this); })
|
||||
.on('click', '#btnRestartWeb', function(){ if (!confirm('Restart the web server now?')) return; postAction('<?= base_url('setting/admin/system-ops/action') ?>', {action: 'restart_web'}, this); })
|
||||
.on('click', '#btnRestartPhp', function(){ if (!confirm('Restart PHP-FPM now?')) return; postAction('<?= base_url('setting/admin/system-ops/action') ?>', {action: 'restart_php'}, this); })
|
||||
.on('click', '#btnReboot', function(){ if (!confirm('Reboot the server now?')) return; postAction('<?= base_url('setting/admin/system-ops/action') ?>', {action: 'reboot'}, this); })
|
||||
.on('click', '#btnShutdown', function(){ if (!confirm('Shut down the server now?')) return; postAction('<?= base_url('setting/admin/system-ops/action') ?>', {action: 'shutdown'}, this); });
|
||||
}
|
||||
</script>
|
||||
</section>
|
||||
146
app/Views/Setting/Admin/system_ops_panel.php
Normal file
146
app/Views/Setting/Admin/system_ops_panel.php
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
<div class="row g-4">
|
||||
<div class="col-12 col-xl-8">
|
||||
<div class="card ops-card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">Server Overview</h5>
|
||||
<div class="d-flex gap-2 align-items-center">
|
||||
<span class="ops-health" id="serverHealthBadge">Healthy</span>
|
||||
<span class="ops-pill success">Live</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<div class="border rounded p-3 h-100">
|
||||
<div class="text-muted small">Host</div>
|
||||
<div class="fs-5 fw-semibold"><?= esc($status['hostname'] ?? 'Unknown') ?></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="border rounded p-3 h-100">
|
||||
<div class="text-muted small">OS</div>
|
||||
<div class="fs-6 fw-semibold"><?= esc($status['os'] ?? 'Unknown') ?></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="border rounded p-3 h-100">
|
||||
<div class="text-muted small">Uptime</div>
|
||||
<div class="fs-5 fw-semibold"><?= esc($status['uptime'] ?? 'Unknown') ?></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="border rounded p-3 h-100">
|
||||
<div class="text-muted small">Network</div>
|
||||
<div class="fs-5 fw-semibold"><?= esc($status['network'] ?? 'Unavailable') ?></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-xl-4">
|
||||
<div class="card ops-card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Maintenance Actions</h5>
|
||||
</div>
|
||||
<div class="card-body d-grid gap-2">
|
||||
<button class="btn btn-primary" id="btnUpdateSystem">Update System</button>
|
||||
<button class="btn btn-outline-warning" id="btnRestartWeb">Restart Web Server</button>
|
||||
<button class="btn btn-outline-info" id="btnRestartPhp">Restart PHP-FPM</button>
|
||||
<button class="btn btn-outline-danger" id="btnShutdown">Shutdown Server</button>
|
||||
<button class="btn btn-outline-secondary" id="btnReboot">Reboot Server</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="card ops-card">
|
||||
<div class="card-header"><h5 class="mb-0">Resource Usage</h5></div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<div class="d-flex justify-content-between small text-muted"><span>CPU</span><span><?= esc((string) ($status['cpu']['used_percent'] ?? 'N/A')) ?>%</span></div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="d-flex justify-content-between small text-muted"><span>RAM</span><span><?= esc((string) ($status['memory']['used_mb'] ?? 'N/A')) ?> / <?= esc((string) ($status['memory']['total_mb'] ?? 'N/A')) ?> MB</span></div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="d-flex justify-content-between small text-muted"><span>Disk</span><span><?= esc((string) ($status['disk']['used_gb'] ?? 'N/A')) ?> / <?= esc((string) ($status['disk']['total_gb'] ?? 'N/A')) ?> GB</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="card ops-card">
|
||||
<div class="card-header"><h5 class="mb-0">Services & RAID</h5></div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<div class="small text-muted">Internet</div>
|
||||
<div class="fw-semibold"><?= esc($status['internet'] ?? 'Unavailable') ?></div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="small text-muted">Services</div>
|
||||
<div class="small"><?= esc(implode(', ', array_keys($status['services'] ?? []))) ?></div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="small text-muted">RAID</div>
|
||||
<div class="small"><?= esc($status['raid'] ?? 'Unavailable') ?></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="card ops-card">
|
||||
<div class="card-header"><h5 class="mb-0">Latest Events</h5></div>
|
||||
<div class="card-body">
|
||||
<ul class="ops-timeline">
|
||||
<?php foreach (array_slice($history, 0, 6) as $entry): ?>
|
||||
<li>
|
||||
<div class="fw-semibold"><?= esc((string) ($entry['type'] ?? '')) ?></div>
|
||||
<div class="small text-muted"><?= esc((string) ($entry['timestamp'] ?? '')) ?></div>
|
||||
<div class="small"><?= esc((string) ($entry['message'] ?? '')) ?></div>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="card ops-card">
|
||||
<div class="card-header"><h5 class="mb-0">Update History</h5></div>
|
||||
<div class="card-body">
|
||||
<?php if (empty($history)) : ?>
|
||||
<div class="text-muted">No history yet.</div>
|
||||
<?php else : ?>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm align-middle mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Time</th>
|
||||
<th>Type</th>
|
||||
<th>Status</th>
|
||||
<th>Message</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($history as $entry): ?>
|
||||
<tr>
|
||||
<td><?= esc((string) ($entry['timestamp'] ?? '')) ?></td>
|
||||
<td><?= esc((string) ($entry['type'] ?? '')) ?></td>
|
||||
<td>
|
||||
<?php $st = (string) ($entry['status'] ?? ''); ?>
|
||||
<span class="ops-pill <?= $st === 'success' ? 'success' : ($st === 'failed' ? 'danger' : 'warning') ?>"><?= esc($st) ?></span>
|
||||
</td>
|
||||
<td><?= esc((string) ($entry['message'] ?? '')) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Loading…
Reference in New Issue
Block a user