feat: graphical RAID display, remove server action buttons

- Remove Restart Web, Restart PHP-FPM, Shutdown, Reboot buttons
- Remove runServerAction() library method and action() controller
- Remove /system-ops/action route (no longer needed)
- Parse /proc/mdstat into structured array (name, level, health, drives)
- Show each RAID array as a card with colored drive icons per-slot
- Green drive = healthy, red = failed, blue = spare
- Show rebuild progress bar when array is rebuilding
- Internet status shown as colored badge with wifi icon
- Services shown as colored badges (green=running, grey=other)
This commit is contained in:
DevSoftTech Server 2026-08-04 03:39:49 +05:30
parent eb81f1999f
commit 1073ab8aca
5 changed files with 183 additions and 71 deletions

View File

@ -1218,7 +1218,6 @@ $routes->group('setting', static function($routes) {
$routes->get('admin/system-ops/panel', 'SystemOps::panel', ['filter' => $settingsAdminFilter]);
$routes->get('admin/system-ops/diagnose', 'SystemOps::diagnose', ['filter' => $settingsAdminFilter]);
$routes->post('admin/system-ops/updateDirect', 'SystemOps::updateDirect', ['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

@ -37,15 +37,6 @@ class SystemOps extends BaseController
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);
}
public function diagnose()
{
$ops = new SystemOperations();

View File

@ -400,42 +400,6 @@ class SystemOperations
return $info;
}
public function runServerAction(string $action): array
{
$commands = [
'restart_web' => ['systemctl restart nginx', 'systemctl restart apache2', 'service nginx restart', 'service apache2 restart'],
'restart_php' => ['systemctl restart php-fpm', 'systemctl restart php8.3-fpm', 'service php-fpm restart', 'service php8.3-fpm restart'],
'reboot' => ['systemctl reboot'],
'shutdown' => ['systemctl poweroff'],
];
$options = $commands[$action] ?? [];
if ($options === []) {
return ['ok' => false, 'message' => 'Unknown action'];
}
$command = $this->findWorkingCommand($options);
if ($command === null) {
return ['ok' => false, 'message' => 'System control commands are not available on this server.'];
}
$result = $this->runCommand($command);
$status = $result['exit_code'] === 0 ? 'success' : 'failed';
$this->appendHistory([
'timestamp' => date('Y-m-d H:i:s'),
'type' => $action,
'status' => $status,
'message' => ucfirst(str_replace('_', ' ', $action)) . ' completed',
'detail' => $result['output'],
]);
return [
'ok' => $result['exit_code'] === 0,
'message' => $result['exit_code'] === 0 ? ucfirst(str_replace('_', ' ', $action)) . ' executed.' : 'Action failed.',
'output' => $result['output'],
];
}
public function getHistory(): array
{
if (! file_exists($this->historyPath)) {
@ -954,16 +918,97 @@ class SystemOperations
return $result;
}
private function readRaidStatus(): string
private function readRaidStatus(): array
{
if (file_exists('/proc/mdstat')) {
$content = file_get_contents('/proc/mdstat');
if (is_string($content) && trim($content) !== '') {
return trim($content);
}
if (!file_exists('/proc/mdstat')) {
return [];
}
return 'Not detected';
$content = @file_get_contents('/proc/mdstat');
if (!is_string($content) || trim($content) === '') {
return [];
}
$arrays = [];
$lines = explode("\n", $content);
$i = 0;
while ($i < count($lines)) {
$line = trim($lines[$i]);
// Match array definition: "md0 : active raid1 sda1[0] sdb1[1]"
if (preg_match('/^(md\d+)\s*:\s*(\w+)\s+(\w+)\s+(.+)$/', $line, $m)) {
$name = $m[1];
$state = $m[2]; // active / inactive
$level = $m[3]; // raid1, raid5, etc.
$devStr = $m[4];
// Parse devices: sda1[0], sdb1[1], sdc1[2](F) etc.
preg_match_all('/(\w+)\[(\d+)\](\(F\)|\(S\))?/', $devStr, $dm);
$devices = [];
foreach ($dm[1] as $k => $dev) {
$flag = $dm[3][$k] ?? '';
$devices[] = [
'name' => $dev,
'index' => (int) $dm[2][$k],
'failed' => $flag === '(F)',
'spare' => $flag === '(S)',
];
}
// Next line has block count and health: " 1953382400 blocks super 1.2 [2/2] [UU]"
$health = 'Unknown';
$total = 0;
$active = 0;
$healthStr = '';
if (isset($lines[$i + 1])) {
$next = trim($lines[$i + 1]);
// [total/active]
if (preg_match('/\[(\d+)\/(\d+)\]/', $next, $nm)) {
$total = (int) $nm[1];
$active = (int) $nm[2];
}
// [UU] or [U_UU] style
if (preg_match('/\[([U_]+)\]/', $next, $hm)) {
$healthStr = $hm[1];
}
// Rebuild progress line (line after next)
$rebuildProgress = null;
if (isset($lines[$i + 2]) && preg_match('/(\d+\.\d+)%/', $lines[$i + 2], $pm)) {
$rebuildProgress = (float) $pm[1];
}
}
$failedCount = substr_count($healthStr, '_');
if ($state !== 'active') {
$health = 'Inactive';
} elseif ($failedCount === 0 && $active === $total && $total > 0) {
$health = 'Healthy';
} elseif ($failedCount > 0) {
$health = 'Degraded';
} elseif ($total === 0) {
$health = 'Unknown';
} else {
$health = 'Rebuilding';
}
$arrays[] = [
'name' => $name,
'state' => $state,
'level' => strtoupper($level),
'health' => $health,
'health_str' => $healthStr,
'total_drives' => $total,
'active_drives' => $active,
'failed_drives' => $failedCount,
'devices' => $devices,
'rebuild_progress' => $rebuildProgress ?? null,
];
}
$i++;
}
return $arrays;
}
private function findWorkingCommand(array $options): ?string

View File

@ -88,12 +88,8 @@
setHealthState('healthy');
setInterval(function(){ refreshPanel(); }, 30000);
$(document).off('click', '#btnUpdateDirectGithub, #btnRestartWeb, #btnRestartPhp, #btnReboot, #btnShutdown')
.on('click', '#btnUpdateDirectGithub', function(){ if (!confirm('Deploy latest HMS code from GitHub?\n\nThis will download the latest code and update all files.')) return; postAction('<?= base_url('setting/admin/system-ops/updateDirect') ?>', {}, 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); });
$(document).off('click', '#btnUpdateDirectGithub')
.on('click', '#btnUpdateDirectGithub', function(){ if (!confirm('Deploy latest HMS code from GitHub?\n\nThis will download the latest code and update all files.')) return; postAction('<?= base_url('setting/admin/system-ops/updateDirect') ?>', {}, this); });
}
</script>
</section>

View File

@ -45,11 +45,6 @@
</div>
<div class="card-body d-grid gap-2">
<button class="btn btn-success" id="btnUpdateDirectGithub" title="Download and deploy latest from GitHub">Deploy from GitHub</button>
<hr class="my-2">
<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>
@ -106,16 +101,102 @@
<div class="card-body">
<div class="mb-3">
<div class="small text-muted">Internet</div>
<div class="fw-semibold"><?= esc($status['internet'] ?? 'Unavailable') ?></div>
<?php $inet = $status['internet'] ?? 'Unavailable'; ?>
<span class="badge <?= $inet === 'Online' ? 'bg-success' : 'bg-danger' ?> fs-6">
<i class="bi <?= $inet === 'Online' ? 'bi-wifi' : 'bi-wifi-off' ?>"></i>
<?= esc($inet) ?>
</span>
</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 class="small text-muted mb-1">Services</div>
<div class="d-flex flex-wrap gap-1">
<?php foreach ($status['services'] ?? [] as $svc => $state): ?>
<span class="badge <?= $state === 'running' ? 'bg-success' : 'bg-secondary' ?>"><?= esc($svc) ?></span>
<?php endforeach; ?>
</div>
</div>
<?php
$raidArrays = $status['raid'] ?? [];
?>
<div class="small text-muted mb-2">RAID Arrays</div>
<?php if (empty($raidArrays)): ?>
<div class="d-flex align-items-center gap-2 text-muted">
<i class="bi bi-hdd-stack fs-5"></i>
<span class="small">No RAID detected</span>
</div>
<?php else: ?>
<?php foreach ($raidArrays as $arr): ?>
<?php
$health = $arr['health'] ?? 'Unknown';
$healthColor = match($health) {
'Healthy' => 'success',
'Degraded' => 'danger',
'Rebuilding'=> 'warning',
default => 'secondary',
};
$healthIcon = match($health) {
'Healthy' => 'bi-shield-check',
'Degraded' => 'bi-shield-exclamation',
'Rebuilding'=> 'bi-arrow-repeat',
default => 'bi-question-circle',
};
$healthStr = $arr['health_str'] ?? '';
?>
<div class="border rounded p-2 mb-2" style="background: #f8f9fa;">
<div class="d-flex justify-content-between align-items-center mb-2">
<div>
<span class="fw-bold"><?= esc($arr['name']) ?></span>
<span class="badge bg-secondary ms-1"><?= esc($arr['level']) ?></span>
<span class="text-muted small ms-1"><?= esc($arr['state']) ?></span>
</div>
<span class="badge bg-<?= $healthColor ?>">
<i class="bi <?= $healthIcon ?>"></i> <?= esc($health) ?>
</span>
</div>
<!-- Drive visual indicators -->
<div class="d-flex gap-1 flex-wrap mb-1">
<?php
// Build per-slot health from health_str [UU_] etc.
$slots = str_split($healthStr);
foreach ($arr['devices'] ?? [] as $di => $dev):
$slotOk = ($slots[$dev['index']] ?? '_') === 'U';
if ($dev['spare']) {
$driveColor = 'bg-info'; $driveIcon = 'bi-hdd'; $driveTitle = 'Spare';
} elseif ($dev['failed']) {
$driveColor = 'bg-danger'; $driveIcon = 'bi-hdd'; $driveTitle = 'Failed';
} elseif ($slotOk) {
$driveColor = 'bg-success'; $driveIcon = 'bi-hdd-fill'; $driveTitle = 'Healthy';
} else {
$driveColor = 'bg-danger'; $driveIcon = 'bi-hdd'; $driveTitle = 'Failed';
}
?>
<div class="d-flex flex-column align-items-center" title="<?= esc($dev['name']) ?> — <?= $driveTitle ?>">
<span class="badge <?= $driveColor ?> p-2">
<i class="bi <?= $driveIcon ?> fs-6"></i>
</span>
<span class="text-muted" style="font-size:10px;"><?= esc($dev['name']) ?></span>
</div>
<?php endforeach; ?>
</div>
<!-- Drive count summary -->
<div class="small text-muted">
<?= esc((string)$arr['active_drives']) ?>/<?= esc((string)$arr['total_drives']) ?> drives active
<?php if (($arr['failed_drives'] ?? 0) > 0): ?>
&mdash; <span class="text-danger fw-semibold"><?= esc((string)$arr['failed_drives']) ?> failed</span>
<?php endif; ?>
<?php if ($arr['rebuild_progress'] !== null): ?>
&mdash; Rebuilding <?= esc((string)$arr['rebuild_progress']) ?>%
<div class="progress mt-1" style="height:6px;">
<div class="progress-bar bg-warning" style="width:<?= esc((string)$arr['rebuild_progress']) ?>%"></div>
</div>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
</div>
</div>