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:
DevSoftTech Server 2026-08-03 16:47:41 +05:30
parent 37631c27a0
commit 89b862db31
4 changed files with 297 additions and 0 deletions

View File

@ -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');

View 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);
}
}

View 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>

View 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>