Add views directory and UI improvements
- Add all view templates (servers, auth, dashboard, admin, layouts, errors, terminal) - Fix SSH double decryption bug in SSHService - Fix router parameter type casting for strict_types - Add missing error views (401, 403, 404) - Add nginx manager with file editing - Add SSL certificates management - Add database manager with phpMyAdmin-like interface - Add sidebar server accordion
This commit is contained in:
90
views/admin/audit.php
Executable file
90
views/admin/audit.php
Executable file
@@ -0,0 +1,90 @@
|
|||||||
|
<?php
|
||||||
|
$logs = $logs ?? [];
|
||||||
|
$action = $action ?? '';
|
||||||
|
$userId = $userId ?? '';
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="page-header">
|
||||||
|
<h1 class="page-title">Audit Logs</h1>
|
||||||
|
<p class="page-subtitle">Comprehensive activity tracking and security monitoring</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-filters">
|
||||||
|
<select id="actionFilter" class="form-select" onchange="applyFilters()">
|
||||||
|
<option value="">All Actions</option>
|
||||||
|
<option value="login_success" <?= $action === 'login_success' ? 'selected' : '' ?>>Login Success</option>
|
||||||
|
<option value="login_failed" <?= $action === 'login_failed' ? 'selected' : '' ?>>Login Failed</option>
|
||||||
|
<option value="server_created" <?= $action === 'server_created' ? 'selected' : '' ?>>Server Created</option>
|
||||||
|
<option value="server_deleted" <?= $action === 'server_deleted' ? 'selected' : '' ?>>Server Deleted</option>
|
||||||
|
<option value="credential_access" <?= $action === 'credential_access' ? 'selected' : '' ?>>Credential Access</option>
|
||||||
|
<option value="command_executed" <?= $action === 'command_executed' ? 'selected' : '' ?>>Command Executed</option>
|
||||||
|
<option value="user_created" <?= $action === 'user_created' ? 'selected' : '' ?>>User Created</option>
|
||||||
|
<option value="user_deleted" <?= $action === 'user_deleted' ? 'selected' : '' ?>>User Deleted</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Time</th>
|
||||||
|
<th>User</th>
|
||||||
|
<th>Action</th>
|
||||||
|
<th>Entity</th>
|
||||||
|
<th>Details</th>
|
||||||
|
<th>IP Address</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($logs['data'] as $log): ?>
|
||||||
|
<tr class="audit-row <?= str_contains($log['action'] ?? '', 'failed') ? 'audit-row-danger' : '' ?>">
|
||||||
|
<td><?= htmlspecialchars($log['created_at'] ?? '') ?></td>
|
||||||
|
<td><?= htmlspecialchars($log['actor_name'] ?? $log['username'] ?? 'System') ?></td>
|
||||||
|
<td>
|
||||||
|
<span class="badge badge-<?= match(true) {
|
||||||
|
str_contains($log['action'] ?? '', 'failed') => 'danger',
|
||||||
|
str_contains($log['action'] ?? '', 'success') => 'success',
|
||||||
|
str_contains($log['action'] ?? '', 'deleted') => 'danger',
|
||||||
|
str_contains($log['action'] ?? '', 'created') => 'success',
|
||||||
|
default => 'info',
|
||||||
|
} ?>">
|
||||||
|
<?= htmlspecialchars(str_replace('_', ' ', $log['action'] ?? '')) ?>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td><?= htmlspecialchars(($log['entity_type'] ?? '') . ' #' . ($log['entity_id'] ?? '')) ?></td>
|
||||||
|
<td>
|
||||||
|
<?php if ($log['details'] ?? false): ?>
|
||||||
|
<pre class="audit-details"><?= htmlspecialchars($log['details']) ?></pre>
|
||||||
|
<?php else: ?>
|
||||||
|
-
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<td><code><?= htmlspecialchars($log['ip_address'] ?? '') ?></code></td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($logs['total_pages'] > 1): ?>
|
||||||
|
<div class="pagination">
|
||||||
|
<?php for ($i = 1; $i <= $logs['total_pages']; $i++): ?>
|
||||||
|
<a href="?page=<?= $i ?>&action=<?= urlencode($action) ?>&user_id=<?= urlencode($userId) ?>"
|
||||||
|
class="page-link <?= $i === ($logs['page'] ?? 1) ? 'active' : '' ?>">
|
||||||
|
<?= $i ?>
|
||||||
|
</a>
|
||||||
|
<?php endfor; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function applyFilters() {
|
||||||
|
const action = document.getElementById('actionFilter').value;
|
||||||
|
window.location.href = '/admin/audit?action=' + encodeURIComponent(action);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
123
views/admin/security.php
Executable file
123
views/admin/security.php
Executable file
@@ -0,0 +1,123 @@
|
|||||||
|
<div class="page-header">
|
||||||
|
<h1 class="page-title">Security Settings</h1>
|
||||||
|
<p class="page-subtitle">Configure security parameters and view status</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dashboard-grid">
|
||||||
|
<div class="dashboard-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2><i class="fas fa-lock"></i> Encryption Status</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="security-status">
|
||||||
|
<div class="security-item">
|
||||||
|
<span>Master Encryption Key</span>
|
||||||
|
<?php
|
||||||
|
$config = \ServerManager\Core\App::getConfig();
|
||||||
|
$keyConfigured = !empty($config['encryption']['master_key']) ||
|
||||||
|
(isset($config['encryption']['master_key_file']) && file_exists($config['encryption']['master_key_file']));
|
||||||
|
?>
|
||||||
|
<span class="badge badge-<?= $keyConfigured ? 'success' : 'danger' ?>">
|
||||||
|
<?= $keyConfigured ? 'Configured' : 'Not Configured' ?>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="security-item">
|
||||||
|
<span>Password Hashing</span>
|
||||||
|
<span class="badge badge-success">Argon2id</span>
|
||||||
|
</div>
|
||||||
|
<div class="security-item">
|
||||||
|
<span>AES-256-CBC Encryption</span>
|
||||||
|
<span class="badge badge-success">Active</span>
|
||||||
|
</div>
|
||||||
|
<div class="security-item">
|
||||||
|
<span>Session Configuration</span>
|
||||||
|
<span class="badge badge-<?= $config['session']['secure'] ? 'success' : 'warning' ?>">
|
||||||
|
<?= $config['session']['secure'] ? 'Secure' : 'HTTP' ?>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dashboard-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2><i class="fas fa-shield-alt"></i> Security Features</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="security-status">
|
||||||
|
<div class="security-item">
|
||||||
|
<span>CSRF Protection</span>
|
||||||
|
<span class="badge badge-success">Enabled</span>
|
||||||
|
</div>
|
||||||
|
<div class="security-item">
|
||||||
|
<span>XSS Prevention</span>
|
||||||
|
<span class="badge badge-success">Enabled</span>
|
||||||
|
</div>
|
||||||
|
<div class="security-item">
|
||||||
|
<span>Rate Limiting</span>
|
||||||
|
<span class="badge badge-success">Enabled (<?= $config['rate_limit']['max_attempts'] ?> attempts/<?= $config['rate_limit']['decay_minutes'] ?>min)</span>
|
||||||
|
</div>
|
||||||
|
<div class="security-item">
|
||||||
|
<span>Input Validation</span>
|
||||||
|
<span class="badge badge-success">Enabled</span>
|
||||||
|
</div>
|
||||||
|
<div class="security-item">
|
||||||
|
<span>Audit Logging</span>
|
||||||
|
<span class="badge badge-success">Enabled</span>
|
||||||
|
</div>
|
||||||
|
<div class="security-item">
|
||||||
|
<span>Session Security</span>
|
||||||
|
<span class="badge badge-success">HTTP Only + SameSite</span>
|
||||||
|
</div>
|
||||||
|
<div class="security-item">
|
||||||
|
<span>API Authentication</span>
|
||||||
|
<span class="badge badge-success">Bearer Token</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dashboard-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2><i class="fas fa-key"></i> Master Key Management</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
<i class="fas fa-exclamation-triangle"></i>
|
||||||
|
<strong>Important:</strong> The master encryption key is stored outside the database.
|
||||||
|
If lost, ALL encrypted SSH credentials will be unrecoverable.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Key File Location</label>
|
||||||
|
<code><?= htmlspecialchars($config['encryption']['master_key_file'] ?? '/etc/servermanager/master.key') ?></code>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-actions" style="margin-top: 1rem;">
|
||||||
|
<button class="btn btn-warning" onclick="generateKey()">
|
||||||
|
<i class="fas fa-key"></i> Generate New Master Key
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function generateKey() {
|
||||||
|
if (!confirm('WARNING: Generating a new master key will make all existing encrypted credentials UNREADABLE. ' +
|
||||||
|
'You will need to re-enter all SSH credentials. Continue?')) return;
|
||||||
|
|
||||||
|
fetch('/admin/security/generate-key', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]')?.content || '',
|
||||||
|
},
|
||||||
|
body: '_csrf_token=' + encodeURIComponent(document.querySelector('meta[name="csrf-token"]')?.content || ''),
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
showToast(data.success ? 'success' : 'error', data.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
201
views/admin/users.php
Executable file
201
views/admin/users.php
Executable file
@@ -0,0 +1,201 @@
|
|||||||
|
<?php $users = $users ?? []; ?>
|
||||||
|
|
||||||
|
<div class="page-header">
|
||||||
|
<div class="page-header-left">
|
||||||
|
<h1 class="page-title">User Management</h1>
|
||||||
|
<p class="page-subtitle">Manage administrator accounts</p>
|
||||||
|
</div>
|
||||||
|
<div class="page-header-right">
|
||||||
|
<button class="btn btn-primary" onclick="showCreateUserModal()">
|
||||||
|
<i class="fas fa-plus"></i> Add User
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Username</th>
|
||||||
|
<th>Email</th>
|
||||||
|
<th>Role</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Last Login</th>
|
||||||
|
<th>Created</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($users['data'] as $user): ?>
|
||||||
|
<tr>
|
||||||
|
<td><?= $user['id'] ?></td>
|
||||||
|
<td><?= htmlspecialchars($user['username']) ?></td>
|
||||||
|
<td><?= htmlspecialchars($user['email']) ?></td>
|
||||||
|
<td>
|
||||||
|
<span class="badge badge-<?= $user['role'] === 'super_admin' ? 'danger' : ($user['role'] === 'admin' ? 'warning' : 'info') ?>">
|
||||||
|
<?= htmlspecialchars(ucfirst(str_replace('_', ' ', $user['role']))) ?>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="status-badge <?= $user['is_active'] ? 'status-online' : 'status-offline' ?>">
|
||||||
|
<?= $user['is_active'] ? 'Active' : 'Inactive' ?>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td><?= $user['last_login_at'] ?? 'Never' ?></td>
|
||||||
|
<td><?= date('Y-m-d', strtotime($user['created_at'] ?? '')) ?></td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-xs" onclick="editUser(<?= $user['id'] ?>, '<?= htmlspecialchars(addslashes($user['username'])) ?>', '<?= htmlspecialchars(addslashes($user['email'])) ?>', '<?= $user['role'] ?>', <?= $user['is_active'] ?>)">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-xs btn-danger" onclick="deleteUser(<?= $user['id'] ?>, '<?= htmlspecialchars(addslashes($user['username'])) ?>')">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($users['total_pages'] > 1): ?>
|
||||||
|
<div class="pagination">
|
||||||
|
<?php for ($i = 1; $i <= $users['total_pages']; $i++): ?>
|
||||||
|
<a href="?page=<?= $i ?>" class="page-link <?= $i === ($users['page'] ?? 1) ? 'active' : '' ?>">
|
||||||
|
<?= $i ?>
|
||||||
|
</a>
|
||||||
|
<?php endfor; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal" id="userModal">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3 id="modalTitle">Create User</h3>
|
||||||
|
<button class="modal-close" onclick="closeModal()">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form id="userForm">
|
||||||
|
<input type="hidden" name="_csrf_token" value="<?= $this->csrfToken() ?>">
|
||||||
|
<input type="hidden" id="userId" value="">
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="modalUsername">Username</label>
|
||||||
|
<input type="text" id="modalUsername" name="username" class="form-input" required minlength="3">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="modalEmail">Email</label>
|
||||||
|
<input type="email" id="modalEmail" name="email" class="form-input" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="modalPassword">Password</label>
|
||||||
|
<input type="password" id="modalPassword" name="password" class="form-input" minlength="8">
|
||||||
|
<small class="form-hint" id="passwordHint">Minimum 8 characters</small>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="modalRole">Role</label>
|
||||||
|
<select id="modalRole" name="role" class="form-select" required>
|
||||||
|
<option value="operator">Operator</option>
|
||||||
|
<option value="admin">Administrator</option>
|
||||||
|
<option value="super_admin">Super Administrator</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input type="checkbox" id="modalActive" name="is_active" value="1" checked>
|
||||||
|
<span>Active</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-secondary" onclick="closeModal()">Cancel</button>
|
||||||
|
<button class="btn btn-primary" id="saveUserBtn" onclick="saveUser()">Save</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function showCreateUserModal() {
|
||||||
|
document.getElementById('modalTitle').textContent = 'Create User';
|
||||||
|
document.getElementById('userId').value = '';
|
||||||
|
document.getElementById('modalUsername').value = '';
|
||||||
|
document.getElementById('modalEmail').value = '';
|
||||||
|
document.getElementById('modalPassword').value = '';
|
||||||
|
document.getElementById('modalPassword').required = true;
|
||||||
|
document.getElementById('passwordHint').style.display = 'block';
|
||||||
|
document.getElementById('modalRole').value = 'operator';
|
||||||
|
document.getElementById('modalActive').checked = true;
|
||||||
|
document.getElementById('saveUserBtn').textContent = 'Create';
|
||||||
|
document.getElementById('userModal').style.display = 'flex';
|
||||||
|
}
|
||||||
|
|
||||||
|
function editUser(id, username, email, role, isActive) {
|
||||||
|
document.getElementById('modalTitle').textContent = 'Edit User';
|
||||||
|
document.getElementById('userId').value = id;
|
||||||
|
document.getElementById('modalUsername').value = username;
|
||||||
|
document.getElementById('modalEmail').value = email;
|
||||||
|
document.getElementById('modalPassword').value = '';
|
||||||
|
document.getElementById('modalPassword').required = false;
|
||||||
|
document.getElementById('passwordHint').style.display = 'none';
|
||||||
|
document.getElementById('modalRole').value = role;
|
||||||
|
document.getElementById('modalActive').checked = !!isActive;
|
||||||
|
document.getElementById('saveUserBtn').textContent = 'Update';
|
||||||
|
document.getElementById('userModal').style.display = 'flex';
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeModal() {
|
||||||
|
document.getElementById('userModal').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveUser() {
|
||||||
|
const id = document.getElementById('userId').value;
|
||||||
|
const formData = new FormData(document.getElementById('userForm'));
|
||||||
|
|
||||||
|
const isEdit = !!id;
|
||||||
|
const url = isEdit ? '/admin/users/' + id + '/update' : '/admin/users/create';
|
||||||
|
|
||||||
|
fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]')?.content || '',
|
||||||
|
},
|
||||||
|
body: new URLSearchParams(formData),
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
showToast('success', data.message);
|
||||||
|
setTimeout(() => location.reload(), 1000);
|
||||||
|
} else {
|
||||||
|
showToast('error', data.message);
|
||||||
|
}
|
||||||
|
closeModal();
|
||||||
|
})
|
||||||
|
.catch(err => showToast('error', 'Failed to save user'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteUser(id, username) {
|
||||||
|
if (!confirm('Are you sure you want to delete user "' + username + '"?')) return;
|
||||||
|
|
||||||
|
fetch('/admin/users/' + id + '/delete', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]')?.content || '',
|
||||||
|
},
|
||||||
|
body: '_csrf_token=' + encodeURIComponent(document.querySelector('meta[name="csrf-token"]')?.content || ''),
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) location.reload();
|
||||||
|
else showToast('error', data.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
47
views/auth/login.php
Executable file
47
views/auth/login.php
Executable file
@@ -0,0 +1,47 @@
|
|||||||
|
<div class="auth-card">
|
||||||
|
<div class="auth-header">
|
||||||
|
<i class="fas fa-server auth-logo"></i>
|
||||||
|
<h1>ServerManager</h1>
|
||||||
|
<p>Linux Server Management Platform</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($flash = \ServerManager\Core\Session::getFlash('error')): ?>
|
||||||
|
<div class="alert alert-error">
|
||||||
|
<i class="fas fa-exclamation-circle"></i>
|
||||||
|
<?= htmlspecialchars($flash[0]) ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<form method="POST" action="/login" class="auth-form" id="loginForm">
|
||||||
|
<input type="hidden" name="_csrf_token" value="<?= $this->csrfToken() ?>">
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="username">
|
||||||
|
<i class="fas fa-user"></i> Username
|
||||||
|
</label>
|
||||||
|
<input type="text" id="username" name="username" class="form-input"
|
||||||
|
placeholder="Enter your username" required autofocus
|
||||||
|
autocomplete="username">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password">
|
||||||
|
<i class="fas fa-lock"></i> Password
|
||||||
|
</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="password" id="password" name="password" class="form-input"
|
||||||
|
placeholder="Enter your password" required
|
||||||
|
autocomplete="current-password">
|
||||||
|
<button type="button" class="input-toggle-password" tabindex="-1">
|
||||||
|
<i class="fas fa-eye"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<button type="submit" class="btn btn-primary btn-block">
|
||||||
|
<i class="fas fa-sign-in-alt"></i> Sign In
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
64
views/auth/profile.php
Executable file
64
views/auth/profile.php
Executable file
@@ -0,0 +1,64 @@
|
|||||||
|
<?php $user = $user ?? []; ?>
|
||||||
|
|
||||||
|
<div class="page-header">
|
||||||
|
<h1 class="page-title">My Profile</h1>
|
||||||
|
<p class="page-subtitle">Manage your account settings</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card" style="max-width: 600px;">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2><i class="fas fa-user-circle"></i> Account Information</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<dl class="details-list">
|
||||||
|
<dt>Username</dt>
|
||||||
|
<dd><?= htmlspecialchars($user['username'] ?? '') ?></dd>
|
||||||
|
<dt>Role</dt>
|
||||||
|
<dd><span class="badge badge-info"><?= htmlspecialchars(ucfirst($user['role'] ?? '')) ?></span></dd>
|
||||||
|
<dt>Last Login</dt>
|
||||||
|
<dd><?= htmlspecialchars($user['last_login_at'] ?? 'Never') ?></dd>
|
||||||
|
<dt>Created</dt>
|
||||||
|
<dd><?= htmlspecialchars($user['created_at'] ?? '') ?></dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card" style="max-width: 600px; margin-top: 1rem;">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2><i class="fas fa-edit"></i> Update Profile</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="POST" action="/profile" class="form">
|
||||||
|
<input type="hidden" name="_csrf_token" value="<?= $this->csrfToken() ?>">
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="email">Email Address</label>
|
||||||
|
<input type="email" id="email" name="email" class="form-input"
|
||||||
|
value="<?= htmlspecialchars($user['email'] ?? '') ?>" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-section">
|
||||||
|
<h3>Change Password</h3>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="current_password">Current Password</label>
|
||||||
|
<input type="password" id="current_password" name="current_password" class="form-input" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="new_password">New Password (leave blank to keep current)</label>
|
||||||
|
<input type="password" id="new_password" name="new_password" class="form-input"
|
||||||
|
placeholder="Minimum 8 characters" minlength="8">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="confirm_password">Confirm New Password</label>
|
||||||
|
<input type="password" id="confirm_password" name="confirm_password" class="form-input">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="fas fa-save"></i> Update Profile
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
152
views/dashboard/index.php
Executable file
152
views/dashboard/index.php
Executable file
@@ -0,0 +1,152 @@
|
|||||||
|
<?php
|
||||||
|
$stats = $stats ?? [];
|
||||||
|
$servers = $servers ?? [];
|
||||||
|
$recentActivity = $recentActivity ?? [];
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="page-header">
|
||||||
|
<h1 class="page-title">Dashboard</h1>
|
||||||
|
<p class="page-subtitle">Server overview and system status</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stats-grid">
|
||||||
|
<div class="stat-card stat-card-primary">
|
||||||
|
<div class="stat-icon"><i class="fas fa-hdd"></i></div>
|
||||||
|
<div class="stat-info">
|
||||||
|
<span class="stat-value" id="statTotalServers"><?= $stats['total_servers'] ?? 0 ?></span>
|
||||||
|
<span class="stat-label">Total Servers</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card stat-card-success">
|
||||||
|
<div class="stat-icon"><i class="fas fa-check-circle"></i></div>
|
||||||
|
<div class="stat-info">
|
||||||
|
<span class="stat-value" id="statOnlineServers"><?= $stats['online_servers'] ?? 0 ?></span>
|
||||||
|
<span class="stat-label">Online</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card stat-card-danger">
|
||||||
|
<div class="stat-icon"><i class="fas fa-times-circle"></i></div>
|
||||||
|
<div class="stat-info">
|
||||||
|
<span class="stat-value" id="statOfflineServers"><?= $stats['offline_servers'] ?? 0 ?></span>
|
||||||
|
<span class="stat-label">Offline</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card stat-card-info">
|
||||||
|
<div class="stat-icon"><i class="fas fa-microchip"></i></div>
|
||||||
|
<div class="stat-info">
|
||||||
|
<span class="stat-value" id="statAvgCpu"><?= $stats['avg_cpu'] ?? 0 ?>%</span>
|
||||||
|
<span class="stat-label">Avg CPU</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card stat-card-warning">
|
||||||
|
<div class="stat-icon"><i class="fas fa-memory"></i></div>
|
||||||
|
<div class="stat-info">
|
||||||
|
<span class="stat-value" id="statAvgRam"><?= $stats['avg_ram'] ?? 0 ?>%</span>
|
||||||
|
<span class="stat-label">Avg RAM</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card stat-card-purple">
|
||||||
|
<div class="stat-icon"><i class="fas fa-hdd"></i></div>
|
||||||
|
<div class="stat-info">
|
||||||
|
<span class="stat-value" id="statAvgDisk"><?= $stats['avg_disk'] ?? 0 ?>%</span>
|
||||||
|
<span class="stat-label">Avg Disk</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dashboard-grid">
|
||||||
|
<div class="dashboard-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2><i class="fas fa-server"></i> Servers</h2>
|
||||||
|
<a href="/servers" class="btn btn-sm">View All</a>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<?php if (empty($servers)): ?>
|
||||||
|
<div class="empty-state">
|
||||||
|
<i class="fas fa-server"></i>
|
||||||
|
<p>No servers configured yet.</p>
|
||||||
|
<a href="/servers/create" class="btn btn-primary">Add Server</a>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-sm">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>IP</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>CPU</th>
|
||||||
|
<th>RAM</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach (array_slice($servers, 0, 8) as $server): ?>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a href="/servers/<?= $server['id'] ?>">
|
||||||
|
<?= htmlspecialchars($server['name']) ?>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td><code><?= htmlspecialchars($server['ip_address']) ?>:<?= $server['ssh_port'] ?></code></td>
|
||||||
|
<td>
|
||||||
|
<span class="status-badge status-<?= $server['current_status'] ?? 'unknown' ?>">
|
||||||
|
<?= ucfirst($server['current_status'] ?? 'unknown') ?>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td><?= $server['cpu_usage'] ?? '-' ?>%</td>
|
||||||
|
<td><?= $server['ram_usage'] ?? '-' ?>%</td>
|
||||||
|
<td>
|
||||||
|
<a href="/servers/<?= $server['id'] ?>" class="btn btn-xs" title="View">
|
||||||
|
<i class="fas fa-eye"></i>
|
||||||
|
</a>
|
||||||
|
<a href="/terminal/<?= $server['id'] ?>" class="btn btn-xs" title="Terminal">
|
||||||
|
<i class="fas fa-terminal"></i>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dashboard-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2><i class="fas fa-history"></i> Recent Activity</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<?php if (empty($recentActivity)): ?>
|
||||||
|
<div class="empty-state">
|
||||||
|
<i class="fas fa-history"></i>
|
||||||
|
<p>No recent activity.</p>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="activity-list">
|
||||||
|
<?php foreach ($recentActivity as $activity): ?>
|
||||||
|
<div class="activity-item">
|
||||||
|
<div class="activity-icon activity-<?= str_contains($activity['action'] ?? '', 'failed') ? 'danger' : (str_contains($activity['action'] ?? '', 'created') ? 'success' : 'info') ?>">
|
||||||
|
<i class="fas fa-<?= match(true) {
|
||||||
|
str_contains($activity['action'] ?? '', 'login') => 'sign-in-alt',
|
||||||
|
str_contains($activity['action'] ?? '', 'server') => 'server',
|
||||||
|
str_contains($activity['action'] ?? '', 'command') => 'terminal',
|
||||||
|
str_contains($activity['action'] ?? '', 'user') => 'user',
|
||||||
|
default => 'circle',
|
||||||
|
} ?>"></i>
|
||||||
|
</div>
|
||||||
|
<div class="activity-details">
|
||||||
|
<span class="activity-action"><?= htmlspecialchars(str_replace('_', ' ', $activity['action'] ?? '')) ?></span>
|
||||||
|
<span class="activity-meta">
|
||||||
|
<?= htmlspecialchars($activity['actor_name'] ?? $activity['username'] ?? 'System') ?>
|
||||||
|
· <?= date('H:i', strtotime($activity['created_at'] ?? '')) ?>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
18
views/errors/401.php
Normal file
18
views/errors/401.php
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
$code = $code ?? 401;
|
||||||
|
$message = $message ?? 'Unauthorized';
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="error-page">
|
||||||
|
<div class="error-card">
|
||||||
|
<div class="error-code"><?= $code ?></div>
|
||||||
|
<div class="error-message"><?= htmlspecialchars($message) ?></div>
|
||||||
|
<p class="error-description">
|
||||||
|
Please log in to access this page.
|
||||||
|
</p>
|
||||||
|
<div class="error-actions">
|
||||||
|
<a href="/login" class="btn btn-primary">Log In</a>
|
||||||
|
<button class="btn btn-secondary" onclick="history.back()">Go Back</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
18
views/errors/403.php
Normal file
18
views/errors/403.php
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
$code = $code ?? 403;
|
||||||
|
$message = $message ?? 'Forbidden';
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="error-page">
|
||||||
|
<div class="error-card">
|
||||||
|
<div class="error-code"><?= $code ?></div>
|
||||||
|
<div class="error-message"><?= htmlspecialchars($message) ?></div>
|
||||||
|
<p class="error-description">
|
||||||
|
You do not have permission to access this resource.
|
||||||
|
</p>
|
||||||
|
<div class="error-actions">
|
||||||
|
<a href="/dashboard" class="btn btn-primary">Go to Dashboard</a>
|
||||||
|
<button class="btn btn-secondary" onclick="history.back()">Go Back</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
18
views/errors/404.php
Normal file
18
views/errors/404.php
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
$code = $code ?? 404;
|
||||||
|
$message = $message ?? 'Not Found';
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="error-page">
|
||||||
|
<div class="error-card">
|
||||||
|
<div class="error-code"><?= $code ?></div>
|
||||||
|
<div class="error-message"><?= htmlspecialchars($message) ?></div>
|
||||||
|
<p class="error-description">
|
||||||
|
The page you are looking for could not be found.
|
||||||
|
</p>
|
||||||
|
<div class="error-actions">
|
||||||
|
<a href="/dashboard" class="btn btn-primary">Go to Dashboard</a>
|
||||||
|
<button class="btn btn-secondary" onclick="history.back()">Go Back</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
26
views/errors/500.php
Executable file
26
views/errors/500.php
Executable file
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
$code = $code ?? 500;
|
||||||
|
$message = $message ?? 'Internal Server Error';
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="error-page">
|
||||||
|
<div class="error-card">
|
||||||
|
<div class="error-code"><?= $code ?></div>
|
||||||
|
<div class="error-message"><?= htmlspecialchars($message) ?></div>
|
||||||
|
<p class="error-description">
|
||||||
|
<?php if ($code === 404): ?>
|
||||||
|
The page you are looking for could not be found.
|
||||||
|
<?php elseif ($code === 403): ?>
|
||||||
|
You do not have permission to access this resource.
|
||||||
|
<?php elseif ($code === 401): ?>
|
||||||
|
Please log in to access this page.
|
||||||
|
<?php else: ?>
|
||||||
|
An unexpected error occurred. The administrators have been notified.
|
||||||
|
<?php endif; ?>
|
||||||
|
</p>
|
||||||
|
<div class="error-actions">
|
||||||
|
<a href="/dashboard" class="btn btn-primary">Go to Dashboard</a>
|
||||||
|
<button class="btn btn-secondary" onclick="history.back()">Go Back</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
18
views/layouts/auth.php
Executable file
18
views/layouts/auth.php
Executable file
@@ -0,0 +1,18 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" data-theme="dark">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title><?= htmlspecialchars($title ?? 'ServerManager') ?></title>
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/assets/img/server.svg">
|
||||||
|
<link rel="stylesheet" href="/assets/css/style.css">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||||
|
</head>
|
||||||
|
<body class="auth-page">
|
||||||
|
<div class="auth-container">
|
||||||
|
<?= $content ?? '' ?>
|
||||||
|
</div>
|
||||||
|
<div id="toastContainer" class="toast-container"></div>
|
||||||
|
<script src="/assets/js/app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
177
views/layouts/main.php
Executable file
177
views/layouts/main.php
Executable file
@@ -0,0 +1,177 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" data-theme="dark">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta name="csrf-token" content="<?= $_SESSION['csrf_token'] ?? '' ?>">
|
||||||
|
<title><?= htmlspecialchars($title ?? 'ServerManager') ?></title>
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/assets/img/server.svg">
|
||||||
|
<link rel="stylesheet" href="/assets/css/style.css">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="app-container">
|
||||||
|
<aside class="sidebar" id="sidebar">
|
||||||
|
<div class="sidebar-header">
|
||||||
|
<a href="/dashboard" class="sidebar-logo">
|
||||||
|
<i class="fas fa-server"></i>
|
||||||
|
<span class="logo-text">ServerManager</span>
|
||||||
|
</a>
|
||||||
|
<button class="sidebar-toggle" id="sidebarToggle" title="Toggle sidebar">
|
||||||
|
<i class="fas fa-bars"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<nav class="sidebar-nav">
|
||||||
|
<ul class="nav-list">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/dashboard" class="nav-link <?= str_starts_with($_SERVER['REQUEST_URI'] ?? '', '/dashboard') ? 'active' : '' ?>">
|
||||||
|
<i class="fas fa-tachometer-alt"></i>
|
||||||
|
<span>Dashboard</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item nav-item-accordion <?= str_starts_with($_SERVER['REQUEST_URI'] ?? '', '/servers') ? 'active' : '' ?>">
|
||||||
|
<a href="#" class="nav-link" onclick="toggleServerList(event)">
|
||||||
|
<i class="fas fa-hdd"></i>
|
||||||
|
<span>Servers</span>
|
||||||
|
<i class="fas fa-chevron-right nav-chevron"></i>
|
||||||
|
</a>
|
||||||
|
<div class="nav-submenu" id="serverList">
|
||||||
|
<div class="nav-submenu-loading"><i class="fas fa-spinner fa-spin"></i></div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li class="nav-divider"></li>
|
||||||
|
<li class="nav-section">Administration</li>
|
||||||
|
<?php if (in_array($_SESSION['user_role'] ?? '', ['super_admin', 'admin'])): ?>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/users" class="nav-link <?= str_starts_with($_SERVER['REQUEST_URI'] ?? '', '/admin/users') ? 'active' : '' ?>">
|
||||||
|
<i class="fas fa-users"></i>
|
||||||
|
<span>Users</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/audit" class="nav-link <?= str_starts_with($_SERVER['REQUEST_URI'] ?? '', '/admin/audit') ? 'active' : '' ?>">
|
||||||
|
<i class="fas fa-history"></i>
|
||||||
|
<span>Audit Logs</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($_SESSION['user_role'] ?? '' === 'super_admin'): ?>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/security" class="nav-link <?= str_starts_with($_SERVER['REQUEST_URI'] ?? '', '/admin/security') ? 'active' : '' ?>">
|
||||||
|
<i class="fas fa-shield-alt"></i>
|
||||||
|
<span>Security</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<?php endif; ?>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
<div class="sidebar-footer">
|
||||||
|
<div class="user-info">
|
||||||
|
<div class="user-avatar">
|
||||||
|
<?= strtoupper(substr($_SESSION['username'] ?? '?', 0, 2)) ?>
|
||||||
|
</div>
|
||||||
|
<div class="user-details">
|
||||||
|
<span class="user-name"><?= htmlspecialchars($_SESSION['username'] ?? 'Guest') ?></span>
|
||||||
|
<span class="user-role"><?= htmlspecialchars(ucfirst(str_replace('_', ' ', $_SESSION['user_role'] ?? 'guest'))) ?></span>
|
||||||
|
</div>
|
||||||
|
<div class="user-menu">
|
||||||
|
<a href="/profile" title="Profile"><i class="fas fa-user-cog"></i></a>
|
||||||
|
<a href="/logout" title="Logout"><i class="fas fa-sign-out-alt"></i></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<main class="main-content">
|
||||||
|
<header class="top-bar">
|
||||||
|
<div class="top-bar-left">
|
||||||
|
<button class="mobile-toggle" id="mobileToggle">
|
||||||
|
<i class="fas fa-bars"></i>
|
||||||
|
</button>
|
||||||
|
<div class="breadcrumb">
|
||||||
|
<?= htmlspecialchars($title ?? 'ServerManager') ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="top-bar-right">
|
||||||
|
<div class="top-bar-actions">
|
||||||
|
<button class="btn-icon" id="refreshBtn" title="Refresh">
|
||||||
|
<i class="fas fa-sync-alt"></i>
|
||||||
|
</button>
|
||||||
|
<button class="btn-icon" id="themeToggle" title="Toggle theme">
|
||||||
|
<i class="fas fa-moon"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="content-area">
|
||||||
|
<?php if ($flash = \ServerManager\Core\Session::getAllFlashes()): ?>
|
||||||
|
<?php foreach ($flash as $type => $messages): ?>
|
||||||
|
<?php foreach ($messages as $message): ?>
|
||||||
|
<div class="toast toast-<?= htmlspecialchars($type) ?>" data-autohide="true">
|
||||||
|
<div class="toast-body">
|
||||||
|
<i class="fas fa-<?= $type === 'success' ? 'check-circle' : ($type === 'error' ? 'times-circle' : 'info-circle') ?>"></i>
|
||||||
|
<?= htmlspecialchars($message) ?>
|
||||||
|
</div>
|
||||||
|
<button class="toast-close">×</button>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?= $content ?? '' ?>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="toastContainer" class="toast-container"></div>
|
||||||
|
|
||||||
|
<script src="/assets/js/app.js"></script>
|
||||||
|
<?php if (isset($scripts)): ?>
|
||||||
|
<?php foreach ($scripts as $script): ?>
|
||||||
|
<script src="<?= htmlspecialchars($script) ?>"></script>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
<script>
|
||||||
|
let serverListLoaded = false;
|
||||||
|
function toggleServerList(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const item = e.currentTarget.closest('.nav-item-accordion');
|
||||||
|
const sub = document.getElementById('serverList');
|
||||||
|
const chevron = item.querySelector('.nav-chevron');
|
||||||
|
if (item.classList.contains('expanded')) {
|
||||||
|
item.classList.remove('expanded');
|
||||||
|
sub.style.maxHeight = '0';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
item.classList.add('expanded');
|
||||||
|
if (!serverListLoaded) {
|
||||||
|
serverListLoaded = true;
|
||||||
|
fetch('/sidebar/servers')
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(d => {
|
||||||
|
if (d.success && d.data) {
|
||||||
|
let html = '<a href="/servers" class="nav-sub-item"><i class="fas fa-list"></i> All Servers</a>';
|
||||||
|
d.data.forEach(s => {
|
||||||
|
const status = s.current_status === 'online' ? 'success' : s.current_status === 'offline' ? 'danger' : 'muted';
|
||||||
|
html += '<a href="/servers/' + s.id + '" class="nav-sub-item">'
|
||||||
|
+ '<span class="status-dot-sm status-' + status + '"></span> '
|
||||||
|
+ s.name + '</a>';
|
||||||
|
});
|
||||||
|
sub.innerHTML = html;
|
||||||
|
} else {
|
||||||
|
sub.innerHTML = '<div class="nav-sub-item" style="color:var(--text-muted)">No servers</div>';
|
||||||
|
}
|
||||||
|
sub.style.maxHeight = sub.scrollHeight + 'px';
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
sub.innerHTML = '<div class="nav-sub-item" style="color:var(--danger)">Failed to load</div>';
|
||||||
|
sub.style.maxHeight = sub.scrollHeight + 'px';
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
sub.style.maxHeight = sub.scrollHeight + 'px';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
119
views/servers/create.php
Executable file
119
views/servers/create.php
Executable file
@@ -0,0 +1,119 @@
|
|||||||
|
<?php $groups = $groups ?? []; ?>
|
||||||
|
|
||||||
|
<div class="page-header">
|
||||||
|
<h1 class="page-title">Add Server</h1>
|
||||||
|
<p class="page-subtitle">Register a new Linux server</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="POST" action="/servers/create" class="form" id="serverForm">
|
||||||
|
<input type="hidden" name="_csrf_token" value="<?= $this->csrfToken() ?>">
|
||||||
|
|
||||||
|
<div class="form-section">
|
||||||
|
<h3><i class="fas fa-info-circle"></i> General Information</h3>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="name">Server Name *</label>
|
||||||
|
<input type="text" id="name" name="name" class="form-input"
|
||||||
|
placeholder="e.g., Production Web Server" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="group_name">Group / Category</label>
|
||||||
|
<input type="text" id="group_name" name="group_name" class="form-input"
|
||||||
|
placeholder="e.g., Production, Staging"
|
||||||
|
list="groupList">
|
||||||
|
<datalist id="groupList">
|
||||||
|
<?php foreach ($groups as $group): ?>
|
||||||
|
<option value="<?= htmlspecialchars($group) ?>">
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</datalist>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="description">Description</label>
|
||||||
|
<textarea id="description" name="description" class="form-input" rows="3"
|
||||||
|
placeholder="Optional description of the server"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-section">
|
||||||
|
<h3><i class="fas fa-plug"></i> Connection Details</h3>
|
||||||
|
<div class="form-row form-row-3">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="ip_address">IP Address / Hostname *</label>
|
||||||
|
<input type="text" id="ip_address" name="ip_address" class="form-input"
|
||||||
|
placeholder="e.g., 192.168.1.100 or server.example.com" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="ssh_port">SSH Port *</label>
|
||||||
|
<input type="number" id="ssh_port" name="ssh_port" class="form-input"
|
||||||
|
value="22" min="1" max="65535" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="ssh_user">SSH User *</label>
|
||||||
|
<input type="text" id="ssh_user" name="ssh_user" class="form-input"
|
||||||
|
placeholder="e.g., root or admin" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-section">
|
||||||
|
<h3><i class="fas fa-key"></i> Authentication</h3>
|
||||||
|
<div class="auth-method-tabs">
|
||||||
|
<button type="button" class="tab active" data-method="password">Password</button>
|
||||||
|
<button type="button" class="tab" data-method="key">Private Key</button>
|
||||||
|
</div>
|
||||||
|
<div class="auth-method-content" id="passwordMethod">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="ssh_password">SSH Password</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="password" id="ssh_password" name="ssh_password" class="form-input"
|
||||||
|
placeholder="Leave blank if using key-based auth">
|
||||||
|
<button type="button" class="input-toggle-password" tabindex="-1">
|
||||||
|
<i class="fas fa-eye"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<small class="form-hint">Credentials are encrypted with AES-256 before storage.</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="auth-method-content" id="keyMethod" style="display: none;">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="ssh_key">SSH Private Key</label>
|
||||||
|
<textarea id="ssh_key" name="ssh_key" class="form-input font-mono" rows="6"
|
||||||
|
placeholder="Paste the private key content (RSA/Ed25519)"></textarea>
|
||||||
|
<small class="form-hint">Private key is encrypted before storage. Passphrase-protected keys supported.</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-section">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input type="checkbox" name="is_active" value="1" checked>
|
||||||
|
<span>Server active</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="fas fa-save"></i> Save Server
|
||||||
|
</button>
|
||||||
|
<a href="/servers" class="btn btn-secondary">Cancel</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.querySelectorAll('.auth-method-tabs .tab').forEach(tab => {
|
||||||
|
tab.addEventListener('click', function() {
|
||||||
|
document.querySelectorAll('.auth-method-tabs .tab').forEach(t => t.classList.remove('active'));
|
||||||
|
this.classList.add('active');
|
||||||
|
const method = this.dataset.method;
|
||||||
|
document.getElementById('passwordMethod').style.display = method === 'password' ? 'block' : 'none';
|
||||||
|
document.getElementById('keyMethod').style.display = method === 'key' ? 'block' : 'none';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
413
views/servers/database.php
Normal file
413
views/servers/database.php
Normal file
@@ -0,0 +1,413 @@
|
|||||||
|
<?php
|
||||||
|
$server = $server ?? [];
|
||||||
|
$db = $db ?? [];
|
||||||
|
$installed = $db['installed'] ?? false;
|
||||||
|
$dbType = $db['db_type'] ?? '';
|
||||||
|
$databases = $db['databases'] ?? [];
|
||||||
|
$totalSize = $db['total_size_mb'] ?? 0;
|
||||||
|
$status = $db['status'] ?? 'inactive';
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="page-header">
|
||||||
|
<div class="page-header-left">
|
||||||
|
<div class="back-link">
|
||||||
|
<a href="/servers/<?= $server['id'] ?>"><i class="fas fa-arrow-left"></i> Back to Server</a>
|
||||||
|
</div>
|
||||||
|
<h1 class="page-title"><i class="fas fa-database"></i> <?= htmlspecialchars($dbType) ?> Manager</h1>
|
||||||
|
<p class="page-subtitle"><?= htmlspecialchars($server['name']) ?> · <?= count($databases) ?> databases · <?= $totalSize ?> MB</p>
|
||||||
|
</div>
|
||||||
|
<div class="page-header-right">
|
||||||
|
<span class="status-badge status-<?= $status === 'active' ? 'online' : 'offline' ?>" style="font-size:0.8rem"><?= ucfirst($status) ?></span>
|
||||||
|
<button class="btn btn-secondary" onclick="location.reload()"><i class="fas fa-sync-alt"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if (!$installed): ?>
|
||||||
|
<div class="card"><div class="card-body"><div class="empty-state">
|
||||||
|
<i class="fas fa-database empty-icon" style="font-size:3rem;opacity:0.4"></i>
|
||||||
|
<h3>No database server found</h3>
|
||||||
|
<p>Neither MySQL nor MariaDB is installed on this server.</p>
|
||||||
|
<p class="text-muted">Install with: <code>sudo apt install mysql-server</code> or <code>sudo apt install mariadb-server</code></p>
|
||||||
|
</div></div></div>
|
||||||
|
<?php else: ?>
|
||||||
|
|
||||||
|
<div class="pma-layout">
|
||||||
|
<aside class="pma-sidebar">
|
||||||
|
<div class="pma-sidebar-header" style="justify-content:space-between">
|
||||||
|
<span><i class="fas fa-database"></i> Databases</span>
|
||||||
|
<span class="badge badge-info"><?= count($databases) ?></span>
|
||||||
|
</div>
|
||||||
|
<div class="pma-sidebar-filter"><input type="text" class="form-input" placeholder="Filter..." oninput="filterSidebar(this.value)" style="font-size:0.8rem"></div>
|
||||||
|
<div class="pma-sidebar-list" id="sidebarList">
|
||||||
|
<?php foreach ($databases as $d): ?>
|
||||||
|
<div class="pma-accordion-item" data-db="<?= htmlspecialchars($d['name']) ?>">
|
||||||
|
<div class="pma-accordion-header" onclick="toggleDb('<?= htmlspecialchars($d['name']) ?>')">
|
||||||
|
<i class="fas fa-database"></i>
|
||||||
|
<span class="pma-db-name"><?= htmlspecialchars($d['name']) ?></span>
|
||||||
|
<span class="pma-db-size"><?= $d['size_mb'] ?> MB</span>
|
||||||
|
<i class="fas fa-chevron-right pma-chevron"></i>
|
||||||
|
</div>
|
||||||
|
<div class="pma-accordion-body" id="tables_<?= htmlspecialchars(md5($d['name'])) ?>">
|
||||||
|
<div class="pma-loading-tables" style="display:none"><i class="fas fa-spinner fa-spin"></i> Loading...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<div style="padding:0.5rem;border-top:1px solid var(--border-color);display:flex;gap:0.5rem">
|
||||||
|
<button class="btn btn-xs btn-secondary" style="flex:1" onclick="switchTab('sql')"><i class="fas fa-terminal"></i> SQL</button>
|
||||||
|
<button class="btn btn-xs btn-secondary" style="flex:1" onclick="switchTab('users');loadUsers()"><i class="fas fa-users"></i> Users</button>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<main class="pma-content">
|
||||||
|
<div class="pma-tabs">
|
||||||
|
<button class="pma-tab" id="tabBrowse" onclick="switchTab('browse')" style="display:none"><i class="fas fa-table"></i> Browse</button>
|
||||||
|
<button class="pma-tab" id="tabStructure" onclick="switchTab('structure')" style="display:none"><i class="fas fa-list"></i> Structure</button>
|
||||||
|
<button class="pma-tab active" id="tabSql" onclick="switchTab('sql')"><i class="fas fa-terminal"></i> SQL</button>
|
||||||
|
<button class="pma-tab" id="tabUsers" onclick="switchTab('users')" style="display:none"><i class="fas fa-users"></i> Users</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="pmaWelcome" class="pma-panel active">
|
||||||
|
<div class="empty-state"><i class="fas fa-database empty-icon" style="opacity:0.2;font-size:5rem"></i><h3>Select a database</h3><p>Click a database in the left panel.</p></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="pmaTables" class="pma-panel"></div>
|
||||||
|
<div id="pmaStructure" class="pma-panel"></div>
|
||||||
|
<div id="pmaBrowse" class="pma-panel"></div>
|
||||||
|
<div id="pmaUsers" class="pma-panel"></div>
|
||||||
|
|
||||||
|
<div id="pmaSql" class="pma-panel active">
|
||||||
|
<div class="pma-sql-editor">
|
||||||
|
<textarea id="sqlInput" class="form-input" rows="6" style="font-family:var(--font-mono);font-size:0.85rem;border-radius:var(--radius) var(--radius) 0 0" placeholder="SELECT * FROM ...">SHOW DATABASES</textarea>
|
||||||
|
<div class="pma-sql-toolbar">
|
||||||
|
<button class="btn btn-primary btn-sm" onclick="runSql()"><i class="fas fa-play"></i> Go</button>
|
||||||
|
<button class="btn btn-secondary btn-sm" onclick="document.getElementById('sqlInput').value=''"><i class="fas fa-eraser"></i></button>
|
||||||
|
<span style="flex:1"></span>
|
||||||
|
<span class="text-muted" style="font-size:0.78rem">Ctrl+Enter</span>
|
||||||
|
</div>
|
||||||
|
<div id="sqlResult" class="nginx-config-viewer" style="display:none;border-radius:0 0 var(--radius) var(--radius);border-top:0"><pre><code id="sqlResultText"></code></pre></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let currentDb = '';
|
||||||
|
let currentTable = '';
|
||||||
|
let openedDb = '';
|
||||||
|
|
||||||
|
function getCsrfToken() { return document.querySelector('meta[name="csrf-token"]')?.content || ''; }
|
||||||
|
|
||||||
|
function filterTables(inp) {
|
||||||
|
const q = inp.value.toLowerCase();
|
||||||
|
const body = inp.closest('.pma-accordion-body');
|
||||||
|
if (body) {
|
||||||
|
body.querySelectorAll('.pma-table-item').forEach(el => {
|
||||||
|
el.style.display = !q || (el.dataset.tbl || '').includes(q) ? '' : 'none';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterSidebar(q) {
|
||||||
|
document.querySelectorAll('.pma-accordion-item').forEach(el => {
|
||||||
|
el.style.display = el.textContent.toLowerCase().includes(q.toLowerCase()) ? '' : 'none';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleDb(db) {
|
||||||
|
const item = document.querySelector(`.pma-accordion-item[data-db="${db.replace(/"/g,'\\"')}"]`);
|
||||||
|
if (!item) return;
|
||||||
|
const body = item.querySelector('.pma-accordion-body');
|
||||||
|
const header = item.querySelector('.pma-accordion-header');
|
||||||
|
const chevron = item.querySelector('.pma-chevron');
|
||||||
|
|
||||||
|
if (item.classList.contains('expanded')) {
|
||||||
|
item.classList.remove('expanded');
|
||||||
|
body.style.maxHeight = '0';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll('.pma-accordion-item.expanded').forEach(el => {
|
||||||
|
el.classList.remove('expanded');
|
||||||
|
el.querySelector('.pma-accordion-body').style.maxHeight = '0';
|
||||||
|
});
|
||||||
|
|
||||||
|
item.classList.add('expanded');
|
||||||
|
currentDb = db;
|
||||||
|
|
||||||
|
if (body.querySelector('.pma-table-item')) {
|
||||||
|
body.style.maxHeight = body.scrollHeight + 'px';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.querySelector('.pma-loading-tables').style.display = '';
|
||||||
|
body.style.maxHeight = '60px';
|
||||||
|
|
||||||
|
fetch('/servers/<?= $server['id'] ?>/database', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type':'application/x-www-form-urlencoded','X-CSRF-Token':getCsrfToken()},
|
||||||
|
body: 'action=tables&db=' + encodeURIComponent(db) + '&_csrf_token=' + encodeURIComponent(getCsrfToken()),
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
body.querySelector('.pma-loading-tables').style.display = 'none';
|
||||||
|
let html = '<div style="padding:0.3rem 0.5rem"><input type="text" class="form-input tbl-filter" placeholder="Filter tables..." style="font-size:0.75rem;padding:0.25rem 0.4rem;width:100%;box-sizing:border-box" oninput="filterTables(this)"></div>';
|
||||||
|
(data.tables || []).forEach(t => {
|
||||||
|
html += '<div class="pma-table-item" data-tbl="' + escHtml(t.name).toLowerCase() + '" onclick="selectTable(\'' + db.replace(/'/g,"\\'") + "','" + t.name.replace(/'/g,"\\'") + '\')">'
|
||||||
|
+ '<i class="fas fa-table"></i> ' + escHtml(t.name)
|
||||||
|
+ ' <span class="pma-db-size">' + t.rows + ' rows</span></div>';
|
||||||
|
});
|
||||||
|
if (!html) html = '<div class="pma-table-item" style="color:var(--text-muted);cursor:default">No tables</div>';
|
||||||
|
body.innerHTML = html;
|
||||||
|
body.style.maxHeight = body.scrollHeight + 'px';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectTable(db, table) {
|
||||||
|
currentDb = db; currentTable = table;
|
||||||
|
document.getElementById('tabBrowse').style.display = '';
|
||||||
|
document.getElementById('tabStructure').style.display = '';
|
||||||
|
document.querySelectorAll('.pma-tab').forEach(t=>t.classList.remove('active'));
|
||||||
|
document.getElementById('tabStructure').classList.add('active');
|
||||||
|
showPanel('structure');
|
||||||
|
loadStructure(db, table);
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadStructure(db, table) {
|
||||||
|
const panel = document.getElementById('pmaStructure');
|
||||||
|
panel.innerHTML = '<div style="text-align:center;padding:2rem;color:var(--text-muted)"><i class="fas fa-spinner fa-spin"></i> Loading structure...</div>';
|
||||||
|
fetch('/servers/<?= $server['id'] ?>/database', {
|
||||||
|
method:'POST', headers:{'Content-Type':'application/x-www-form-urlencoded','X-CSRF-Token':getCsrfToken()},
|
||||||
|
body:'action=structure&db='+encodeURIComponent(db)+'&table='+encodeURIComponent(table)+'&_csrf_token='+encodeURIComponent(getCsrfToken()),
|
||||||
|
}).then(r=>r.json()).then(d=>{
|
||||||
|
if (!d.success||!d.columns) return;
|
||||||
|
let html='<div style="display:flex;gap:0.75rem;align-items:center;margin-bottom:1rem">'
|
||||||
|
+'<h2 style="margin:0;font-size:1rem"><i class="fas fa-list"></i> <code>'+escHtml(table)+'</code></h2>'
|
||||||
|
+'<span class="badge badge-info">'+d.columns.length+' columns</span>'
|
||||||
|
+'<span style="flex:1"></span>'
|
||||||
|
+'<button class="btn btn-xs btn-info" onclick="browseTable(\''+db.replace(/'/g,"\\'")+"','"+table.replace(/'/g,"\\'")+'\')"><i class="fas fa-search"></i> Browse</button>'
|
||||||
|
+'</div>';
|
||||||
|
html+='<div class="table-responsive"><table class="table table-sm"><thead><tr><th>#</th><th>Field</th><th>Type</th><th>Collation</th><th>Null</th><th>Key</th><th>Default</th><th>Extra</th></tr></thead><tbody>';
|
||||||
|
d.columns.forEach((c,i)=>{
|
||||||
|
html+='<tr><td>'+(i+1)+'</td><td><strong>'+escHtml(c.field)+'</strong></td><td><code>'+escHtml(c.type)+'</code></td>'
|
||||||
|
+'<td>'+escHtml(c.collation)+'</td><td>'+escHtml(c.null)+'</td><td><strong>'+escHtml(c.key)+'</strong></td>'
|
||||||
|
+'<td>'+(c.default??'NULL')+'</td><td>'+escHtml(c.extra)+'</td></tr>';
|
||||||
|
});
|
||||||
|
html+='</tbody></table></div>';
|
||||||
|
panel.innerHTML=html;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function browseTable(db, table) {
|
||||||
|
currentDb=db; currentTable=table;
|
||||||
|
document.getElementById('tabBrowse').style.display='';
|
||||||
|
document.getElementById('tabStructure').style.display='';
|
||||||
|
document.querySelectorAll('.pma-tab').forEach(t=>t.classList.remove('active'));
|
||||||
|
document.getElementById('tabBrowse').classList.add('active');
|
||||||
|
showPanel('browse');
|
||||||
|
loadBrowse(db,table,1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadBrowse(db,table,page) {
|
||||||
|
const panel=document.getElementById('pmaBrowse');
|
||||||
|
|
||||||
|
const inputs=document.querySelectorAll('.pma-fi');
|
||||||
|
let filters='';
|
||||||
|
if(inputs.length){
|
||||||
|
const p={};
|
||||||
|
inputs.forEach(inp=>{if(inp.value.trim())p[inp.dataset.col]=inp.value.trim()});
|
||||||
|
const s=new URLSearchParams(p).toString();
|
||||||
|
if(s)filters='&'+s;
|
||||||
|
}
|
||||||
|
|
||||||
|
panel.innerHTML='<div style="text-align:center;padding:2rem;color:var(--text-muted)"><i class="fas fa-spinner fa-spin"></i> Loading...</div>';
|
||||||
|
|
||||||
|
let body='action=browse&db='+encodeURIComponent(db)+'&table='+encodeURIComponent(table)+'&page='+page+filters+'&_csrf_token='+encodeURIComponent(getCsrfToken());
|
||||||
|
|
||||||
|
fetch('/servers/<?= $server['id'] ?>/database',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded','X-CSRF-Token':getCsrfToken()},body})
|
||||||
|
.then(r=>r.json()).then(d=>{
|
||||||
|
if(!d.success)return;
|
||||||
|
const tp=Math.ceil(d.total/d.per_page);
|
||||||
|
let html='<div style="display:flex;gap:0.75rem;align-items:center;margin-bottom:0.5rem">'
|
||||||
|
+'<h2 style="margin:0;font-size:1rem"><i class="fas fa-search"></i> <code>'+escHtml(table)+'</code></h2>'
|
||||||
|
+'<span class="badge badge-info">'+d.total+' rows</span></div>';
|
||||||
|
html+='<form onsubmit="event.preventDefault();loadBrowse(\''+db.replace(/'/g,"\\'")+"','"+table.replace(/'/g,"\\'")+'\',1)">';
|
||||||
|
html+='<div class="table-responsive" style="max-height:450px;overflow-y:auto"><table class="table table-sm"><thead><tr>';
|
||||||
|
d.columns.forEach(c=>{html+='<th>'+escHtml(c)+'</th>'});
|
||||||
|
html+='</tr><tr class="pma-filter-row">';
|
||||||
|
d.columns.forEach(c=>{
|
||||||
|
const cv=new URLSearchParams(filters.replace(/^&/,'')).get(c)||'';
|
||||||
|
html+='<td><input type="text" class="form-input pma-fi" data-col="'+escHtml(c)+'" value="'+escHtml(cv)+'" placeholder="'+escHtml(c)+'" style="font-size:0.75rem;padding:0.2rem 0.35rem;width:100%;min-width:50px"></td>';
|
||||||
|
});
|
||||||
|
html+='</tr></thead><tbody>';
|
||||||
|
d.rows.forEach(row=>{
|
||||||
|
html+='<tr>';
|
||||||
|
row.forEach(val=>{html+='<td style="max-width:180px;overflow:hidden;text-overflow:ellipsis"><code>'+escHtml(String(val).substring(0,80))+'</code></td>'});
|
||||||
|
html+='</tr>';
|
||||||
|
});
|
||||||
|
html+='</tbody></table></div>';
|
||||||
|
html+='<div style="display:flex;gap:0.5rem;margin-top:0.5rem;align-items:center">';
|
||||||
|
html+='<button type="submit" class="btn btn-primary btn-sm"><i class="fas fa-search"></i> Filter</button>';
|
||||||
|
html+='<button type="button" class="btn btn-secondary btn-sm" onclick="clearBrowseFilters(\''+db.replace(/'/g,"\\'")+"','"+table.replace(/'/g,"\\'")+'\')"><i class="fas fa-undo"></i> Clear</button>';
|
||||||
|
if(tp>1){
|
||||||
|
html+='<span style="flex:1"></span>';
|
||||||
|
for(let p=1;p<=tp&&p<=10;p++)html+='<button type="button" class="btn btn-xs '+(p===page?'btn-primary':'btn-secondary')+'" onclick="loadBrowse(\''+db.replace(/'/g,"\\'")+"','"+table.replace(/'/g,"\\'")+'\','+p+')">'+p+'</button>';
|
||||||
|
if(tp>10)html+='<span class="text-muted">...</span>';
|
||||||
|
}
|
||||||
|
html+='</div></form>';
|
||||||
|
panel.innerHTML=html;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearBrowseFilters(db,table){
|
||||||
|
document.querySelectorAll('.pma-fi').forEach(inp=>inp.value='');
|
||||||
|
loadBrowse(db,table,1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadUsers() {
|
||||||
|
showPanel('users');
|
||||||
|
document.getElementById('tabUsers').style.display='';
|
||||||
|
const panel=document.getElementById('pmaUsers');
|
||||||
|
panel.innerHTML='<div style="text-align:center;padding:2rem;color:var(--text-muted)"><i class="fas fa-spinner fa-spin"></i> Loading users...</div>';
|
||||||
|
|
||||||
|
fetch('/servers/<?= $server['id'] ?>/database',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded','X-CSRF-Token':getCsrfToken()},body:'action=users&_csrf_token='+encodeURIComponent(getCsrfToken())})
|
||||||
|
.then(r=>r.json()).then(d=>{
|
||||||
|
let html='<div style="display:flex;gap:0.75rem;align-items:center;margin-bottom:1rem">'
|
||||||
|
+'<h2 style="margin:0;font-size:1rem"><i class="fas fa-users"></i> Database Users</h2>'
|
||||||
|
+'<span class="badge badge-info">'+(d.users?.length||0)+' users</span>'
|
||||||
|
+'<span style="flex:1"></span>'
|
||||||
|
+'<button class="btn btn-sm btn-success" onclick="showCreateUser()"><i class="fas fa-plus"></i> New User</button></div>';
|
||||||
|
html+='<div class="table-responsive"><table class="table table-sm"><thead><tr><th>User</th><th>Host</th><th>Databases</th><th>Expired</th><th>Actions</th></tr></thead><tbody>';
|
||||||
|
(d.users||[]).forEach(u=>{
|
||||||
|
const dbList=(d.grants||[]).filter(g=>g.user===u.user&&g.host===u.host).map(g=>g.db).join(', ');
|
||||||
|
html+='<tr><td><strong>'+escHtml(u.user)+'</strong></td><td><code>'+escHtml(u.host)+'</code></td><td>'+(dbList||'<span class="text-muted">-</span>')+'</td><td>'+escHtml(u.expired)+'</td>'
|
||||||
|
+'<td class="actions-cell">'
|
||||||
|
+'<button class="btn btn-xs btn-secondary" onclick="showEditUser(\''+escHtml(u.user)+"','"+escHtml(u.host)+'\')" title="Change password"><i class="fas fa-key"></i></button> '
|
||||||
|
+'<button class="btn btn-xs btn-danger" onclick="deleteUser(\''+escHtml(u.user)+"','"+escHtml(u.host)+'\')" title="Delete"><i class="fas fa-trash"></i></button>'
|
||||||
|
+'</td></tr>';
|
||||||
|
});
|
||||||
|
html+='</tbody></table></div>';
|
||||||
|
panel.innerHTML=html;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function showCreateUser() {
|
||||||
|
const panel=document.getElementById('pmaUsers');
|
||||||
|
let html='<div style="display:flex;gap:0.75rem;align-items:center;margin-bottom:1rem">'
|
||||||
|
+'<h2 style="margin:0;font-size:1rem"><i class="fas fa-user-plus"></i> Create User</h2>'
|
||||||
|
+'<span style="flex:1"></span>'
|
||||||
|
+'<button class="btn btn-sm btn-secondary" onclick="loadUsers()"><i class="fas fa-arrow-left"></i> Back</button></div>'
|
||||||
|
+'<div class="dashboard-grid"><div class="dashboard-card"><div class="card-header"><h2><i class="fas fa-user"></i> Account</h2></div><div class="card-body">'
|
||||||
|
+'<div class="form-row"><div class="form-group"><label>Username</label><input type="text" id="nuUser" class="form-input" placeholder="username"></div>'
|
||||||
|
+'<div class="form-group"><label>Password</label><input type="password" id="nuPass" class="form-input" placeholder="password"></div>'
|
||||||
|
+'<div class="form-group"><label>Host</label><input type="text" id="nuHost" class="form-input" value="localhost" placeholder="localhost"></div></div></div></div>'
|
||||||
|
+'<div class="dashboard-card"><div class="card-header"><h2><i class="fas fa-database"></i> Database Access</h2></div><div class="card-body">'
|
||||||
|
+'<div style="margin-bottom:0.75rem"><label class="checkbox-label"><input type="checkbox" id="nuAllDb" onchange="document.querySelectorAll(\'.nuDbCheck\').forEach(c=>c.checked=this.checked)"> <strong>Select all</strong></label></div>';
|
||||||
|
|
||||||
|
<?php foreach ($databases as $d): ?>
|
||||||
|
var dbname = '<?= htmlspecialchars($d['name']) ?>';
|
||||||
|
if (dbname !== 'information_schema' && dbname !== 'performance_schema' && dbname !== 'mysql' && dbname !== 'sys') {
|
||||||
|
html+='<label class="checkbox-label" style="margin-top:0.25rem"><input type="checkbox" class="nuDbCheck" value="'+dbname+'"> <span>'+dbname+'</span></label>';
|
||||||
|
}
|
||||||
|
<?php endforeach; ?>
|
||||||
|
|
||||||
|
html+='</div></div></div>'
|
||||||
|
+'<div class="dashboard-card"><div class="card-header"><h2><i class="fas fa-lock"></i> Privileges</h2></div><div class="card-body">'
|
||||||
|
+'<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:0.25rem">';
|
||||||
|
['ALL PRIVILEGES','SELECT','INSERT','UPDATE','DELETE','CREATE','DROP','ALTER','INDEX','CREATE VIEW','SHOW VIEW','CREATE ROUTINE','ALTER ROUTINE','EXECUTE','LOCK TABLES','REFERENCES','EVENT','TRIGGER'].forEach(function(p){
|
||||||
|
var checked = p === 'ALL PRIVILEGES' ? ' checked' : '';
|
||||||
|
html+='<label class="checkbox-label" style="font-size:0.82rem"><input type="checkbox" class="nuPriv" value="'+p+'"'+checked+'> <span>'+p+'</span></label>';
|
||||||
|
});
|
||||||
|
html+='</div></div></div>'
|
||||||
|
+'<div style="margin-top:1rem;display:flex;gap:0.5rem">'
|
||||||
|
+'<button class="btn btn-primary" onclick="doCreateUser()"><i class="fas fa-save"></i> Create User</button>'
|
||||||
|
+'<button class="btn btn-secondary" onclick="loadUsers()">Cancel</button></div>';
|
||||||
|
panel.innerHTML=html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function doCreateUser() {
|
||||||
|
const user=document.getElementById('nuUser').value;
|
||||||
|
const pass=document.getElementById('nuPass').value;
|
||||||
|
const host=document.getElementById('nuHost').value||'localhost';
|
||||||
|
if(!user||!pass){showToast('error','Username and password required');return;}
|
||||||
|
|
||||||
|
const privs=[];
|
||||||
|
document.querySelectorAll('.nuPriv:checked').forEach(cb=>privs.push(cb.value));
|
||||||
|
const grantPrivs = privs.includes('ALL PRIVILEGES') ? 'ALL PRIVILEGES' : privs.join(',');
|
||||||
|
|
||||||
|
fetch('/servers/<?= $server['id'] ?>/database',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded','X-CSRF-Token':getCsrfToken()},body: 'action=create_user&new_user='+encodeURIComponent(user)+'&new_pass='+encodeURIComponent(pass)+'&new_host='+encodeURIComponent(host)+'&_csrf_token='+encodeURIComponent(getCsrfToken())})
|
||||||
|
.then(r=>r.json()).then(d=>{
|
||||||
|
showToast(d.success?'success':'error',d.message);
|
||||||
|
if(d.success){
|
||||||
|
const dbs=[];
|
||||||
|
document.querySelectorAll('.nuDbCheck:checked').forEach(cb=>dbs.push(cb.value));
|
||||||
|
if(dbs.length>0){
|
||||||
|
let cnt=0;
|
||||||
|
dbs.forEach(db=>{
|
||||||
|
fetch('/servers/<?= $server['id'] ?>/database',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded','X-CSRF-Token':getCsrfToken()},body:'action=grant&grant_user='+encodeURIComponent(user)+'&grant_db='+encodeURIComponent(db)+'&grant_host='+encodeURIComponent(host)+'&grant_privs='+encodeURIComponent(grantPrivs)+'&_csrf_token='+encodeURIComponent(getCsrfToken())})
|
||||||
|
.then(r2=>r2.json()).then(g=>{
|
||||||
|
cnt++;
|
||||||
|
if(cnt===dbs.length)loadUsers();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}else{loadUsers();}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function showEditUser(user, host) {
|
||||||
|
const panel = document.getElementById('pmaUsers');
|
||||||
|
panel.innerHTML='<div style="display:flex;gap:0.75rem;align-items:center;margin-bottom:1rem">'
|
||||||
|
+'<h2 style="margin:0;font-size:1rem"><i class="fas fa-key"></i> Change Password</h2>'
|
||||||
|
+'<code>'+escHtml(user)+'</code> @ <code>'+escHtml(host)+'</code>'
|
||||||
|
+'<span style="flex:1"></span>'
|
||||||
|
+'<button class="btn btn-sm btn-secondary" onclick="loadUsers()"><i class="fas fa-arrow-left"></i> Back</button></div>'
|
||||||
|
+'<div class="card"><div class="card-body">'
|
||||||
|
+'<div class="form-group"><label>New Password</label><input type="password" id="epPass" class="form-input" placeholder="new password"></div>'
|
||||||
|
+'<div style="margin-top:1rem;display:flex;gap:0.5rem">'
|
||||||
|
+'<button class="btn btn-primary" onclick="doChangePass(\''+escHtml(user)+"','"+escHtml(host)+'\')"><i class="fas fa-save"></i> Save</button>'
|
||||||
|
+'<button class="btn btn-secondary" onclick="loadUsers()">Cancel</button></div></div></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function doChangePass(user, host) {
|
||||||
|
const pass = document.getElementById('epPass').value;
|
||||||
|
if (!pass) { showToast('error','Password required'); return; }
|
||||||
|
fetch('/servers/<?= $server['id'] ?>/database',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded','X-CSRF-Token':getCsrfToken()},
|
||||||
|
body:'action=change_pass&target_user='+encodeURIComponent(user)+'&target_host='+encodeURIComponent(host)+'&new_pass='+encodeURIComponent(pass)+'&_csrf_token='+encodeURIComponent(getCsrfToken())})
|
||||||
|
.then(r=>r.json()).then(d=>{showToast(d.success?'success':'error',d.message);if(d.success)loadUsers()});
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteUser(user, host) {
|
||||||
|
if (!confirm('Delete user \''+user+'\'@\''+host+'\'?\nThis cannot be undone.')) return;
|
||||||
|
fetch('/servers/<?= $server['id'] ?>/database',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded','X-CSRF-Token':getCsrfToken()},
|
||||||
|
body:'action=drop_user&target_user='+encodeURIComponent(user)+'&target_host='+encodeURIComponent(host)+'&_csrf_token='+encodeURIComponent(getCsrfToken())})
|
||||||
|
.then(r=>r.json()).then(d=>{showToast(d.success?'success':'error',d.message);if(d.success)loadUsers()});
|
||||||
|
}
|
||||||
|
|
||||||
|
function runSql() {
|
||||||
|
const sql=document.getElementById('sqlInput').value; if(!sql)return;
|
||||||
|
const rd=document.getElementById('sqlResult'); const rt=document.getElementById('sqlResultText');
|
||||||
|
rd.style.display='none';
|
||||||
|
fetch('/servers/<?= $server['id'] ?>/database',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded','X-CSRF-Token':getCsrfToken()},body:'action=query&query='+encodeURIComponent(sql)+'&_csrf_token='+encodeURIComponent(getCsrfToken())})
|
||||||
|
.then(r=>r.json()).then(d=>{if(d.output){rt.textContent=d.output;rd.style.display='block'}else showToast('error',d.message||'No results')});
|
||||||
|
}
|
||||||
|
|
||||||
|
function showPanel(id) {
|
||||||
|
document.querySelectorAll('.pma-panel').forEach(p=>p.classList.remove('active'));
|
||||||
|
document.getElementById('pma'+id.charAt(0).toUpperCase()+id.slice(1)).classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchTab(tab) {
|
||||||
|
document.querySelectorAll('.pma-tab').forEach(t=>t.classList.remove('active'));
|
||||||
|
const map={browse:'pmaBrowse',structure:'pmaStructure',sql:'pmaSql',users:'pmaUsers'};
|
||||||
|
const el=document.getElementById('tab'+tab.charAt(0).toUpperCase()+tab.slice(1));
|
||||||
|
if(el)el.classList.add('active');
|
||||||
|
showPanel(tab);
|
||||||
|
if(tab==='sql'){document.getElementById('pmaWelcome').classList.remove('active');document.getElementById('pmaSql').classList.add('active');}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('sqlInput').addEventListener('keydown',function(e){if((e.ctrlKey||e.metaKey)&&e.key==='Enter')runSql()});
|
||||||
|
|
||||||
|
function escHtml(s){const d=document.createElement('div');d.textContent=s;return d.innerHTML;}
|
||||||
|
</script>
|
||||||
|
<?php endif; ?>
|
||||||
112
views/servers/edit.php
Executable file
112
views/servers/edit.php
Executable file
@@ -0,0 +1,112 @@
|
|||||||
|
<?php
|
||||||
|
$server = $server ?? [];
|
||||||
|
$groups = $groups ?? [];
|
||||||
|
$hasPassword = !empty($server['ssh_password']);
|
||||||
|
$hasKey = !empty($server['ssh_key']);
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="page-header">
|
||||||
|
<div class="page-header-left">
|
||||||
|
<div class="back-link">
|
||||||
|
<a href="/servers/<?= $server['id'] ?>"><i class="fas fa-arrow-left"></i> Back to Server</a>
|
||||||
|
</div>
|
||||||
|
<h1 class="page-title">Edit Server</h1>
|
||||||
|
<p class="page-subtitle"><?= htmlspecialchars($server['name'] ?? '') ?></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="POST" action="/servers/<?= $server['id'] ?>/edit" class="form">
|
||||||
|
<input type="hidden" name="_csrf_token" value="<?= $this->csrfToken() ?>">
|
||||||
|
|
||||||
|
<div class="form-section">
|
||||||
|
<h3><i class="fas fa-info-circle"></i> General Information</h3>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="name">Server Name *</label>
|
||||||
|
<input type="text" id="name" name="name" class="form-input"
|
||||||
|
value="<?= htmlspecialchars($server['name'] ?? '') ?>" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="group_name">Group / Category</label>
|
||||||
|
<input type="text" id="group_name" name="group_name" class="form-input"
|
||||||
|
value="<?= htmlspecialchars($server['group_name'] ?? '') ?>"
|
||||||
|
list="groupList">
|
||||||
|
<datalist id="groupList">
|
||||||
|
<?php foreach ($groups as $group): ?>
|
||||||
|
<option value="<?= htmlspecialchars($group) ?>">
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</datalist>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="description">Description</label>
|
||||||
|
<textarea id="description" name="description" class="form-input" rows="3"><?= htmlspecialchars($server['description'] ?? '') ?></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-section">
|
||||||
|
<h3><i class="fas fa-plug"></i> Connection Details</h3>
|
||||||
|
<div class="form-row form-row-3">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="ip_address">IP Address / Hostname *</label>
|
||||||
|
<input type="text" id="ip_address" name="ip_address" class="form-input"
|
||||||
|
value="<?= htmlspecialchars($server['ip_address'] ?? '') ?>" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="ssh_port">SSH Port *</label>
|
||||||
|
<input type="number" id="ssh_port" name="ssh_port" class="form-input"
|
||||||
|
value="<?= $server['ssh_port'] ?? 22 ?>" min="1" max="65535" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="ssh_user">SSH User *</label>
|
||||||
|
<input type="text" id="ssh_user" name="ssh_user" class="form-input"
|
||||||
|
value="<?= htmlspecialchars($server['ssh_user'] ?? '') ?>" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-section">
|
||||||
|
<h3><i class="fas fa-key"></i> Authentication (leave blank to keep current)</h3>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="ssh_password">New SSH Password</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="password" id="ssh_password" name="ssh_password" class="form-input"
|
||||||
|
placeholder="Enter new password or leave blank">
|
||||||
|
<button type="button" class="input-toggle-password" tabindex="-1">
|
||||||
|
<i class="fas fa-eye"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<?php if ($hasPassword): ?>
|
||||||
|
<small class="form-hint">Currently using password authentication.</small>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="ssh_key">New SSH Private Key</label>
|
||||||
|
<textarea id="ssh_key" name="ssh_key" class="form-input font-mono" rows="6"
|
||||||
|
placeholder="Paste new private key or leave blank"><?= htmlspecialchars($server['ssh_key'] ?? '') ?></textarea>
|
||||||
|
<?php if ($hasKey): ?>
|
||||||
|
<small class="form-hint">Currently using key-based authentication.</small>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-section">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input type="checkbox" name="is_active" value="1" <?= ($server['is_active'] ?? 1) ? 'checked' : '' ?>>
|
||||||
|
<span>Server active</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="fas fa-save"></i> Update Server
|
||||||
|
</button>
|
||||||
|
<a href="/servers/<?= $server['id'] ?>" class="btn btn-secondary">Cancel</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
169
views/servers/index.php
Executable file
169
views/servers/index.php
Executable file
@@ -0,0 +1,169 @@
|
|||||||
|
<?php
|
||||||
|
$servers = $servers ?? [];
|
||||||
|
$groups = $groups ?? [];
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="page-header">
|
||||||
|
<div class="page-header-left">
|
||||||
|
<h1 class="page-title">Servers</h1>
|
||||||
|
<p class="page-subtitle">Manage your Linux servers</p>
|
||||||
|
</div>
|
||||||
|
<div class="page-header-right">
|
||||||
|
<a href="/servers/create" class="btn btn-primary">
|
||||||
|
<i class="fas fa-plus"></i> Add Server
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-filters">
|
||||||
|
<div class="search-box">
|
||||||
|
<i class="fas fa-search"></i>
|
||||||
|
<input type="text" id="serverSearch" class="form-input"
|
||||||
|
placeholder="Search servers..." value="<?= htmlspecialchars($search ?? '') ?>">
|
||||||
|
</div>
|
||||||
|
<select id="statusFilter" class="form-select">
|
||||||
|
<option value="">All Status</option>
|
||||||
|
<option value="online" <?= ($statusFilter ?? '') === 'online' ? 'selected' : '' ?>>Online</option>
|
||||||
|
<option value="offline" <?= ($statusFilter ?? '') === 'offline' ? 'selected' : '' ?>>Offline</option>
|
||||||
|
<option value="unknown" <?= ($statusFilter ?? '') === 'unknown' ? 'selected' : '' ?>>Unknown</option>
|
||||||
|
</select>
|
||||||
|
<select id="groupFilter" class="form-select">
|
||||||
|
<option value="">All Groups</option>
|
||||||
|
<?php foreach ($groups as $group): ?>
|
||||||
|
<option value="<?= htmlspecialchars($group) ?>" <?= ($groupFilter ?? '') === $group ? 'selected' : '' ?>>
|
||||||
|
<?= htmlspecialchars($group) ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body">
|
||||||
|
<?php if (empty($servers['data'])): ?>
|
||||||
|
<div class="empty-state">
|
||||||
|
<i class="fas fa-server"></i>
|
||||||
|
<p>No servers found.</p>
|
||||||
|
<a href="/servers/create" class="btn btn-primary">Add Your First Server</a>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>IP Address</th>
|
||||||
|
<th>Group</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>CPU</th>
|
||||||
|
<th>RAM</th>
|
||||||
|
<th>Disk</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($servers['data'] as $server): ?>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a href="/servers/<?= $server['id'] ?>" class="server-name-link">
|
||||||
|
<?= htmlspecialchars($server['name']) ?>
|
||||||
|
</a>
|
||||||
|
<?php if (!$server['is_active']): ?>
|
||||||
|
<span class="badge badge-warning">Inactive</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<td><code><?= htmlspecialchars($server['ip_address']) ?>:<?= $server['ssh_port'] ?></code></td>
|
||||||
|
<td><?= htmlspecialchars($server['group_name'] ?? '-') ?></td>
|
||||||
|
<td>
|
||||||
|
<span class="status-badge status-<?= $server['current_status'] ?? 'unknown' ?>">
|
||||||
|
<?= ucfirst($server['current_status'] ?? 'unknown') ?>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="mini-progress">
|
||||||
|
<div class="mini-progress-bar" style="width: <?= $server['cpu_usage'] ?? 0 ?>%"></div>
|
||||||
|
<span><?= $server['cpu_usage'] ?? '-' ?>%</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="mini-progress mini-progress-blue">
|
||||||
|
<div class="mini-progress-bar" style="width: <?= $server['ram_usage'] ?? 0 ?>%"></div>
|
||||||
|
<span><?= $server['ram_usage'] ?? '-' ?>%</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="mini-progress mini-progress-purple">
|
||||||
|
<div class="mini-progress-bar" style="width: <?= $server['disk_usage'] ?? 0 ?>%"></div>
|
||||||
|
<span><?= $server['disk_usage'] ?? '-' ?>%</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="actions-cell">
|
||||||
|
<div class="btn-group">
|
||||||
|
<a href="/servers/<?= $server['id'] ?>" class="btn btn-xs" title="View Details">
|
||||||
|
<i class="fas fa-eye"></i>
|
||||||
|
</a>
|
||||||
|
<a href="/terminal/<?= $server['id'] ?>" class="btn btn-xs" title="Terminal">
|
||||||
|
<i class="fas fa-terminal"></i>
|
||||||
|
</a>
|
||||||
|
<a href="/servers/<?= $server['id'] ?>/edit" class="btn btn-xs" title="Edit">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</a>
|
||||||
|
<button class="btn btn-xs btn-danger" title="Delete"
|
||||||
|
onclick="deleteServer(<?= $server['id'] ?>, '<?= htmlspecialchars(addslashes($server['name'])) ?>')">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($servers['total_pages'] > 1): ?>
|
||||||
|
<div class="pagination">
|
||||||
|
<?php for ($i = 1; $i <= $servers['total_pages']; $i++): ?>
|
||||||
|
<a href="?page=<?= $i ?>&search=<?= urlencode($search ?? '') ?>&status=<?= urlencode($statusFilter ?? '') ?>&group=<?= urlencode($groupFilter ?? '') ?>"
|
||||||
|
class="page-link <?= $i === ($servers['page'] ?? 1) ? 'active' : '' ?>">
|
||||||
|
<?= $i ?>
|
||||||
|
</a>
|
||||||
|
<?php endfor; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function deleteServer(id, name) {
|
||||||
|
if (confirm('Are you sure you want to delete server "' + name + '"?\nThis action cannot be undone.')) {
|
||||||
|
const form = document.createElement('form');
|
||||||
|
form.method = 'POST';
|
||||||
|
form.action = '/servers/' + id + '/delete';
|
||||||
|
const csrf = document.createElement('input');
|
||||||
|
csrf.type = 'hidden';
|
||||||
|
csrf.name = '_csrf_token';
|
||||||
|
csrf.value = document.querySelector('meta[name="csrf-token"]')?.content || '';
|
||||||
|
form.appendChild(csrf);
|
||||||
|
document.body.appendChild(form);
|
||||||
|
form.submit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('serverSearch')?.addEventListener('input', function(e) {
|
||||||
|
applyFilters();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('statusFilter')?.addEventListener('change', applyFilters);
|
||||||
|
document.getElementById('groupFilter')?.addEventListener('change', applyFilters);
|
||||||
|
|
||||||
|
function applyFilters() {
|
||||||
|
const search = document.getElementById('serverSearch')?.value || '';
|
||||||
|
const status = document.getElementById('statusFilter')?.value || '';
|
||||||
|
const group = document.getElementById('groupFilter')?.value || '';
|
||||||
|
window.location.href = '/servers?search=' + encodeURIComponent(search) +
|
||||||
|
'&status=' + encodeURIComponent(status) +
|
||||||
|
'&group=' + encodeURIComponent(group);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
76
views/servers/logs.php
Executable file
76
views/servers/logs.php
Executable file
@@ -0,0 +1,76 @@
|
|||||||
|
<?php
|
||||||
|
$server = $server ?? [];
|
||||||
|
$logOutput = $logOutput ?? '';
|
||||||
|
$logType = $logType ?? 'syslog';
|
||||||
|
$lines = $lines ?? 100;
|
||||||
|
$logFiles = $logFiles ?? [];
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="page-header">
|
||||||
|
<div class="page-header-left">
|
||||||
|
<div class="back-link">
|
||||||
|
<a href="/servers/<?= $server['id'] ?>"><i class="fas fa-arrow-left"></i> Back to Server</a>
|
||||||
|
</div>
|
||||||
|
<h1 class="page-title">System Logs</h1>
|
||||||
|
<p class="page-subtitle"><?= htmlspecialchars($server['name']) ?></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-filters">
|
||||||
|
<select id="logType" class="form-select" onchange="updateLog()">
|
||||||
|
<?php foreach ($logFiles as $file): ?>
|
||||||
|
<option value="<?= $file ?>" <?= $logType === $file ? 'selected' : '' ?>>
|
||||||
|
<?= ucfirst($file) ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
<select id="logLines" class="form-select" onchange="updateLog()">
|
||||||
|
<option value="50" <?= $lines == 50 ? 'selected' : '' ?>>50 lines</option>
|
||||||
|
<option value="100" <?= $lines == 100 ? 'selected' : '' ?>>100 lines</option>
|
||||||
|
<option value="200" <?= $lines == 200 ? 'selected' : '' ?>>200 lines</option>
|
||||||
|
<option value="500" <?= $lines == 500 ? 'selected' : '' ?>>500 lines</option>
|
||||||
|
</select>
|
||||||
|
<button class="btn btn-secondary btn-sm" onclick="updateLog()">
|
||||||
|
<i class="fas fa-sync-alt"></i> Refresh
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<?php if (empty(trim($logOutput))): ?>
|
||||||
|
<div class="empty-state">
|
||||||
|
<i class="fas fa-file-alt empty-icon"></i>
|
||||||
|
<p>No log data available.</p>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="log-viewer">
|
||||||
|
<?php
|
||||||
|
$logLines = explode("\n", trim($logOutput));
|
||||||
|
foreach ($logLines as $line):
|
||||||
|
$lineClass = 'log-line';
|
||||||
|
$lower = mb_strtolower($line);
|
||||||
|
if (str_contains($lower, 'error') || str_contains($lower, 'critical') || str_contains($lower, 'emergency')) {
|
||||||
|
$lineClass .= ' log-line-error';
|
||||||
|
} elseif (str_contains($lower, 'warning') || str_contains($lower, 'warn')) {
|
||||||
|
$lineClass .= ' log-line-warning';
|
||||||
|
} elseif (str_contains($lower, 'notice') || str_contains($lower, 'info')) {
|
||||||
|
$lineClass .= ' log-line-info';
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<div class="<?= $lineClass ?>">
|
||||||
|
<code><?= htmlspecialchars($line) ?></code>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function updateLog() {
|
||||||
|
const type = document.getElementById('logType').value;
|
||||||
|
const lines = document.getElementById('logLines').value;
|
||||||
|
window.location.href = '/servers/<?= $server['id'] ?>/logs?type=' + type + '&lines=' + lines;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
646
views/servers/nginx.php
Normal file
646
views/servers/nginx.php
Normal file
@@ -0,0 +1,646 @@
|
|||||||
|
<?php
|
||||||
|
$server = $server ?? [];
|
||||||
|
$nginx = $nginx ?? [];
|
||||||
|
$installed = $nginx['installed'] ?? false;
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="page-header">
|
||||||
|
<div class="page-header-left">
|
||||||
|
<div class="back-link">
|
||||||
|
<a href="/servers/<?= $server['id'] ?>"><i class="fas fa-arrow-left"></i> Back to Server</a>
|
||||||
|
</div>
|
||||||
|
<h1 class="page-title">
|
||||||
|
<svg viewBox="0 0 24 24" fill="currentColor" width="24" height="24" style="vertical-align:middle;margin-right:0.5rem;opacity:0.8">
|
||||||
|
<path d="M12 2L2 7.2v9.6L12 22l10-5.2V7.2L12 2zm0 1.5l8.5 4.4v8.2L12 20.5l-8.5-4.4V7.9L12 3.5zM7.4 8.5v7l1.5.9V11l3.1 4.3 1.5.9V8.5l-1.5.9v4.4L8.9 9.4l-1.5-.9zm9.2 0l-1.5.9-3.1 4.4V17l1.5-.9V11l3.1-4.3z"/>
|
||||||
|
</svg>
|
||||||
|
Nginx Manager
|
||||||
|
</h1>
|
||||||
|
<p class="page-subtitle"><?= htmlspecialchars($server['name']) ?></p>
|
||||||
|
</div>
|
||||||
|
<div class="page-header-right">
|
||||||
|
<button class="btn btn-secondary" onclick="location.reload()">
|
||||||
|
<i class="fas fa-sync-alt"></i> Refresh
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if (!$installed): ?>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="empty-state">
|
||||||
|
<svg viewBox="0 0 24 24" fill="currentColor" width="64" height="64" style="opacity:0.4;margin-bottom:1rem">
|
||||||
|
<path d="M12 2L2 7.2v9.6L12 22l10-5.2V7.2L12 2zm0 1.5l8.5 4.4v8.2L12 20.5l-8.5-4.4V7.9L12 3.5zM7.4 8.5v7l1.5.9V11l3.1 4.3 1.5.9V8.5l-1.5.9v4.4L8.9 9.4l-1.5-.9zm9.2 0l-1.5.9-3.1 4.4V17l1.5-.9V11l3.1-4.3z"/>
|
||||||
|
</svg>
|
||||||
|
<h3>Nginx is not installed</h3>
|
||||||
|
<p>This server does not have Nginx installed or it is not in the PATH.</p>
|
||||||
|
<p class="text-muted">Install it with: <code>sudo apt install nginx</code> or <code>sudo yum install nginx</code></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<?php
|
||||||
|
$status = $nginx['status'] ?? 'inactive';
|
||||||
|
$enabled = $nginx['enabled'] ?? 'disabled';
|
||||||
|
$configValid = $nginx['config_valid'] ?? false;
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="dashboard-grid">
|
||||||
|
<div class="dashboard-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2><i class="fas fa-info-circle"></i> Service Status</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="stats-grid" style="grid-template-columns:repeat(3,1fr);gap:0.75rem">
|
||||||
|
<div class="stat-card" style="padding:1rem">
|
||||||
|
<div class="stat-icon" style="width:40px;height:40px;font-size:1rem;
|
||||||
|
background:<?= $status === 'active' ? 'var(--success-bg)' : 'var(--danger-bg)' ?>;
|
||||||
|
color:<?= $status === 'active' ? 'var(--success)' : 'var(--danger)' ?>">
|
||||||
|
<i class="fas <?= $status === 'active' ? 'fa-play-circle' : 'fa-stop-circle' ?>"></i>
|
||||||
|
</div>
|
||||||
|
<div class="stat-info">
|
||||||
|
<span class="stat-value" style="font-size:1rem"><?= ucfirst($status) ?></span>
|
||||||
|
<span class="stat-label">Status</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card" style="padding:1rem">
|
||||||
|
<div class="stat-icon" style="width:40px;height:40px;font-size:1rem;
|
||||||
|
background:<?= $enabled === 'enabled' ? 'var(--success-bg)' : 'var(--warning-bg)' ?>;
|
||||||
|
color:<?= $enabled === 'enabled' ? 'var(--success)' : 'var(--warning)' ?>">
|
||||||
|
<i class="fas fa-power-off"></i>
|
||||||
|
</div>
|
||||||
|
<div class="stat-info">
|
||||||
|
<span class="stat-value" style="font-size:1rem"><?= ucfirst($enabled) ?></span>
|
||||||
|
<span class="stat-label">Auto-start</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card" style="padding:1rem">
|
||||||
|
<div class="stat-icon" style="width:40px;height:40px;font-size:1rem;
|
||||||
|
background:<?= $configValid ? 'var(--success-bg)' : 'var(--danger-bg)' ?>;
|
||||||
|
color:<?= $configValid ? 'var(--success)' : 'var(--danger)' ?>">
|
||||||
|
<i class="fas fa-check-circle"></i>
|
||||||
|
</div>
|
||||||
|
<div class="stat-info">
|
||||||
|
<span class="stat-value" style="font-size:1rem"><?= $configValid ? 'Valid' : 'Invalid' ?></span>
|
||||||
|
<span class="stat-label">Config</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="action-group" style="margin-top:1rem;display:flex;gap:0.5rem;flex-wrap:wrap">
|
||||||
|
<button class="btn btn-success btn-sm" onclick="nginxAction('start')" <?= $status === 'active' ? 'disabled' : '' ?>>
|
||||||
|
<i class="fas fa-play"></i> Start
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-danger btn-sm" onclick="nginxAction('stop')" <?= $status !== 'active' ? 'disabled' : '' ?>>
|
||||||
|
<i class="fas fa-stop"></i> Stop
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-warning btn-sm" onclick="nginxAction('restart')">
|
||||||
|
<i class="fas fa-redo"></i> Restart
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-info btn-sm" onclick="nginxAction('reload')">
|
||||||
|
<i class="fas fa-sync-alt"></i> Reload
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-secondary btn-sm" onclick="nginxAction('test')">
|
||||||
|
<i class="fas fa-vial"></i> Test Config
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php if (!empty($nginx['version'])): ?>
|
||||||
|
<div class="dashboard-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2><i class="fas fa-tag"></i> Version</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<code><?= htmlspecialchars($nginx['version']) ?></code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dashboard-grid">
|
||||||
|
<?php if (!empty($nginx['sites_enabled'])): ?>
|
||||||
|
<div class="dashboard-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2><i class="fas fa-check-circle text-success"></i> Enabled Sites</h2>
|
||||||
|
<span class="badge badge-success"><?= count($nginx['sites_enabled']) ?></span>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="list-group">
|
||||||
|
<?php foreach ($nginx['sites_enabled'] as $site): ?>
|
||||||
|
<div class="list-item list-item-action" onclick="editFile('/etc/nginx/sites-enabled/<?= htmlspecialchars($site) ?>')">
|
||||||
|
<i class="fas fa-file-code"></i>
|
||||||
|
<code><?= htmlspecialchars($site) ?></code>
|
||||||
|
<span class="list-item-actions">
|
||||||
|
<button class="btn btn-xs btn-warning" onclick="event.stopPropagation();toggleSite('<?= htmlspecialchars($site) ?>',false)" title="Disable site">
|
||||||
|
<i class="fas fa-pause"></i>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-xs btn-danger" onclick="event.stopPropagation();deleteSite('<?= htmlspecialchars($site) ?>')" title="Delete site">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (!empty($nginx['sites_available'])): ?>
|
||||||
|
<div class="dashboard-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2><i class="fas fa-folder-open"></i> Available Sites</h2>
|
||||||
|
<div style="display:flex;gap:0.5rem;align-items:center">
|
||||||
|
<span class="badge badge-info"><?= count($nginx['sites_available']) ?></span>
|
||||||
|
<button class="btn btn-sm btn-success" onclick="openNewSiteModal()">
|
||||||
|
<i class="fas fa-plus"></i> New Site
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="list-group">
|
||||||
|
<?php foreach ($nginx['sites_available'] as $site): ?>
|
||||||
|
<?php $isEnabled = in_array($site, $nginx['sites_enabled'] ?? []); ?>
|
||||||
|
<div class="list-item list-item-action" onclick="editFile('/etc/nginx/sites-available/<?= htmlspecialchars($site) ?>')">
|
||||||
|
<i class="fas fa-file-code"></i>
|
||||||
|
<code><?= htmlspecialchars($site) ?></code>
|
||||||
|
<span class="list-item-actions">
|
||||||
|
<button class="btn btn-xs btn-success" onclick="event.stopPropagation();toggleSite('<?= htmlspecialchars($site) ?>',true)" title="Enable site">
|
||||||
|
<i class="fas fa-plus"></i>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-xs btn-danger" onclick="event.stopPropagation();deleteSite('<?= htmlspecialchars($site) ?>')" title="Delete site">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2><i class="fas fa-file-alt"></i> Main Configuration</h2>
|
||||||
|
<div>
|
||||||
|
<button class="btn btn-sm btn-primary" onclick="editFile('/etc/nginx/nginx.conf')">
|
||||||
|
<i class="fas fa-edit"></i> Edit
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="nginx-config-viewer">
|
||||||
|
<pre><code><?= htmlspecialchars($nginx['config'] ?? '# No configuration found') ?></code></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if (!empty($nginx['config_test'])): ?>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2><i class="fas fa-clipboard-list"></i> Config Test Result</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="nginx-config-viewer nginx-test-output">
|
||||||
|
<pre><code class="<?= $configValid ? 'text-success' : 'text-danger' ?>"><?= htmlspecialchars($nginx['config_test']) ?></code></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div id="nginxEditorModal" class="modal editor-modal-overlay">
|
||||||
|
<div class="vscode-editor">
|
||||||
|
<div class="vscode-titlebar">
|
||||||
|
<span class="vscode-title-icon"><i class="far fa-file-code"></i></span>
|
||||||
|
<span class="vscode-title-path" id="editorFilePath">file</span>
|
||||||
|
<span class="vscode-title-dirty" id="editorDirtyIndicator" style="display:none">●</span>
|
||||||
|
<div class="vscode-title-actions">
|
||||||
|
<button class="vscode-btn" onclick="closeEditor()" title="Close (Escape)"><i class="fas fa-times"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="vscode-toolbar">
|
||||||
|
<button class="vscode-btn vscode-btn-primary" onclick="saveFile()" title="Save (Ctrl+S)">
|
||||||
|
<i class="fas fa-save"></i> Save
|
||||||
|
</button>
|
||||||
|
<div class="vscode-separator"></div>
|
||||||
|
<button class="vscode-btn" onclick="undoEditor()" title="Undo (Ctrl+Z)">
|
||||||
|
<i class="fas fa-undo"></i>
|
||||||
|
</button>
|
||||||
|
<button class="vscode-btn" onclick="redoEditor()" title="Redo (Ctrl+Y)">
|
||||||
|
<i class="fas fa-redo"></i>
|
||||||
|
</button>
|
||||||
|
<div class="vscode-separator"></div>
|
||||||
|
<button class="vscode-btn" onclick="saveFile()" title="Save & Test Config">
|
||||||
|
<i class="fas fa-vial"></i> Test
|
||||||
|
</button>
|
||||||
|
<span class="vscode-spacer"></span>
|
||||||
|
<span id="editorStatus" class="vscode-status-text">Ready</span>
|
||||||
|
</div>
|
||||||
|
<div class="vscode-body">
|
||||||
|
<div class="vscode-gutter" id="editorGutter"></div>
|
||||||
|
<textarea id="editorContent" class="vscode-textarea"
|
||||||
|
spellcheck="false" autocomplete="off" wrap="off"></textarea>
|
||||||
|
<pre class="vscode-highlight" id="editorHighlight" aria-hidden="true"><code id="editorHighlightCode"></code></pre>
|
||||||
|
</div>
|
||||||
|
<div class="vscode-statusbar">
|
||||||
|
<span class="vscode-status-item" id="editorCursorPos">Ln 1, Col 1</span>
|
||||||
|
<span class="vscode-status-item">UTF-8</span>
|
||||||
|
<span class="vscode-status-item" id="editorLang">Nginx Config</span>
|
||||||
|
<span class="vscode-status-item vscode-status-end" id="editorFileSize"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="nginxNewSiteModal" class="modal">
|
||||||
|
<div class="modal-dialog" style="max-width:550px">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3><i class="fas fa-plus-circle text-success"></i> New Nginx Site</h3>
|
||||||
|
<button class="modal-close" onclick="closeNewSiteModal()">×</button>
|
||||||
|
</div>
|
||||||
|
<form id="newSiteForm" onsubmit="createSite(event)">
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="siteName">Site name (domain)</label>
|
||||||
|
<input type="text" id="siteName" name="site_name" class="form-input"
|
||||||
|
placeholder="e.g. example.com" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="siteRoot">Root directory</label>
|
||||||
|
<input type="text" id="siteRoot" name="site_root" class="form-input"
|
||||||
|
placeholder="/var/www/example.com" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="siteServerName">Server name</label>
|
||||||
|
<input type="text" id="siteServerName" name="server_name" class="form-input"
|
||||||
|
placeholder="example.com www.example.com">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="siteIndex">Index file</label>
|
||||||
|
<input type="text" id="siteIndex" name="index" class="form-input"
|
||||||
|
value="index.html index.htm index.php" placeholder="index.html">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="margin-top:0.5rem">
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input type="checkbox" id="sitePhp" name="enable_php" value="1" checked>
|
||||||
|
<span>Enable PHP-FPM support</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input type="checkbox" id="siteEnable" name="enable_site" value="1" checked>
|
||||||
|
<span>Enable site after creation</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" onclick="closeNewSiteModal()">Cancel</button>
|
||||||
|
<button type="submit" class="btn btn-success" id="createSiteBtn">
|
||||||
|
<i class="fas fa-plus"></i> Create Site
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="nginxResultModal" class="modal">
|
||||||
|
<div class="modal-dialog" style="max-width:600px">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3 id="nginxResultTitle">Result</h3>
|
||||||
|
<button class="modal-close" onclick="closeNginxModal()">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p id="nginxResultMessage"></p>
|
||||||
|
<div id="nginxResultOutput" class="nginx-config-viewer nginx-test-output" style="display:none;margin-top:0.75rem">
|
||||||
|
<pre><code id="nginxResultOutputText"></code></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-secondary" onclick="closeNginxModal()">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let currentEditFile = null;
|
||||||
|
let editorDirty = false;
|
||||||
|
let editorUndoStack = [];
|
||||||
|
let editorRedoStack = [];
|
||||||
|
let editorHistoryIndex = -1;
|
||||||
|
|
||||||
|
function getCsrfToken() {
|
||||||
|
return document.querySelector('meta[name="csrf-token"]')?.content || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateLineNumbers() {
|
||||||
|
const ta = document.getElementById('editorContent');
|
||||||
|
const gutter = document.getElementById('editorGutter');
|
||||||
|
const lines = ta.value.split('\n').length;
|
||||||
|
const numbers = [];
|
||||||
|
for (let i = 1; i <= lines; i++) {
|
||||||
|
numbers.push('<span>' + i + '</span>');
|
||||||
|
}
|
||||||
|
gutter.innerHTML = numbers.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCursorPos() {
|
||||||
|
const ta = document.getElementById('editorContent');
|
||||||
|
const before = ta.value.substring(0, ta.selectionStart);
|
||||||
|
const lines = before.split('\n');
|
||||||
|
const line = lines.length;
|
||||||
|
const col = lines[lines.length - 1].length + 1;
|
||||||
|
document.getElementById('editorCursorPos').textContent = 'Ln ' + line + ', Col ' + col;
|
||||||
|
}
|
||||||
|
|
||||||
|
function highlightNginx(text) {
|
||||||
|
let h = text
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/#(.*)$/gm, '<span class="hl-comment">#$1</span>')
|
||||||
|
.replace(/\b(server|location|listen|server_name|root|index|try_files|include|fastcgi_pass|proxy_pass|return|rewrite|set|if|error_page|ssl_certificate|ssl_certificate_key|ssl_protocols|ssl_ciphers|add_header|access_log|error_log|keepalive|gzip|proxy_set_header|proxy_redirect|client_max_body_size|autoindex)\b/g, '<span class="hl-keyword">$1</span>')
|
||||||
|
.replace(/\b(\d+)\b/g, '<span class="hl-number">$1</span>')
|
||||||
|
.replace(/\b(https?|fastcgi|unix):\/\/[^\s;{]+/g, '<span class="hl-string">$&</span>')
|
||||||
|
.replace(/'(.*?)'/g, '<span class="hl-string">\'$1\'</span>')
|
||||||
|
.replace(/(\$\w+)/g, '<span class="hl-var">$1</span>')
|
||||||
|
.replace(/\b(\w+\.\w+(?:\.\w+)*)\b/g, function(m) {
|
||||||
|
if (m.includes('com') || m.includes('org') || m.includes('net') || m.includes('io') || m.includes('local')) {
|
||||||
|
return '<span class="hl-string">' + m + '</span>';
|
||||||
|
}
|
||||||
|
return m;
|
||||||
|
});
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncHighlight() {
|
||||||
|
const ta = document.getElementById('editorContent');
|
||||||
|
const code = document.getElementById('editorHighlightCode');
|
||||||
|
code.innerHTML = highlightNginx(ta.value) + '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
function pushHistory() {
|
||||||
|
const ta = document.getElementById('editorContent');
|
||||||
|
if (editorHistoryIndex >= 0 && editorUndoStack[editorHistoryIndex] === ta.value) return;
|
||||||
|
editorUndoStack = editorUndoStack.slice(0, editorHistoryIndex + 1);
|
||||||
|
editorUndoStack.push(ta.value);
|
||||||
|
editorRedoStack = [];
|
||||||
|
editorHistoryIndex = editorUndoStack.length - 1;
|
||||||
|
if (editorUndoStack.length > 100) {
|
||||||
|
editorUndoStack.shift();
|
||||||
|
editorHistoryIndex--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function undoEditor() {
|
||||||
|
if (editorHistoryIndex <= 0) return;
|
||||||
|
editorRedoStack.push(editorUndoStack[editorHistoryIndex]);
|
||||||
|
editorHistoryIndex--;
|
||||||
|
const ta = document.getElementById('editorContent');
|
||||||
|
ta.value = editorUndoStack[editorHistoryIndex];
|
||||||
|
ta.dispatchEvent(new Event('input'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function redoEditor() {
|
||||||
|
if (editorRedoStack.length === 0) return;
|
||||||
|
const val = editorRedoStack.pop();
|
||||||
|
editorUndoStack.push(val);
|
||||||
|
editorHistoryIndex++;
|
||||||
|
const ta = document.getElementById('editorContent');
|
||||||
|
ta.value = val;
|
||||||
|
ta.dispatchEvent(new Event('input'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function nginxAction(action) {
|
||||||
|
const btn = event.target.closest('button');
|
||||||
|
const origText = btn.innerHTML;
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Working...';
|
||||||
|
|
||||||
|
fetch('/servers/<?= $server['id'] ?>/nginx', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'X-CSRF-Token': getCsrfToken(),
|
||||||
|
},
|
||||||
|
body: 'action=' + action + '&_csrf_token=' + encodeURIComponent(getCsrfToken()),
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
showToast(data.success ? 'success' : 'error', data.message);
|
||||||
|
if (data.output) {
|
||||||
|
document.getElementById('nginxResultMessage').textContent = data.message;
|
||||||
|
document.getElementById('nginxResultOutputText').textContent = data.output;
|
||||||
|
document.getElementById('nginxResultOutput').style.display = 'block';
|
||||||
|
document.getElementById('nginxResultModal').style.display = 'flex';
|
||||||
|
}
|
||||||
|
if (data.success && ['restart', 'reload', 'start', 'stop'].includes(action)) {
|
||||||
|
setTimeout(() => location.reload(), 1500);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => showToast('error', 'Request failed: ' + err.message))
|
||||||
|
.finally(() => { btn.disabled = false; btn.innerHTML = origText; });
|
||||||
|
}
|
||||||
|
|
||||||
|
function editFile(file) {
|
||||||
|
currentEditFile = file;
|
||||||
|
editorUndoStack = [];
|
||||||
|
editorRedoStack = [];
|
||||||
|
editorHistoryIndex = -1;
|
||||||
|
editorDirty = false;
|
||||||
|
document.getElementById('editorFilePath').textContent = file;
|
||||||
|
document.getElementById('editorContent').value = 'Loading...';
|
||||||
|
document.getElementById('editorStatus').textContent = 'Loading...';
|
||||||
|
document.getElementById('editorDirtyIndicator').style.display = 'none';
|
||||||
|
document.getElementById('editorFileSize').textContent = '';
|
||||||
|
document.getElementById('nginxEditorModal').style.display = 'flex';
|
||||||
|
setTimeout(() => document.getElementById('editorContent').focus(), 100);
|
||||||
|
|
||||||
|
fetch('/servers/<?= $server['id'] ?>/nginx/file?file=' + encodeURIComponent(file))
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
const ta = document.getElementById('editorContent');
|
||||||
|
ta.value = data.content;
|
||||||
|
document.getElementById('editorFileSize').textContent = (data.content.length / 1024).toFixed(1) + ' KB';
|
||||||
|
document.getElementById('editorStatus').textContent = 'Ready';
|
||||||
|
document.getElementById('editorDirtyIndicator').style.display = 'none';
|
||||||
|
editorDirty = false;
|
||||||
|
updateLineNumbers();
|
||||||
|
syncHighlight();
|
||||||
|
pushHistory();
|
||||||
|
} else {
|
||||||
|
showToast('error', data.message);
|
||||||
|
closeEditor();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
showToast('error', 'Failed to load file');
|
||||||
|
closeEditor();
|
||||||
|
});
|
||||||
|
|
||||||
|
const ta = document.getElementById('editorContent');
|
||||||
|
ta.oninput = function() {
|
||||||
|
editorDirty = true;
|
||||||
|
document.getElementById('editorStatus').textContent = 'Unsaved changes';
|
||||||
|
document.getElementById('editorDirtyIndicator').style.display = 'inline';
|
||||||
|
updateLineNumbers();
|
||||||
|
syncHighlight();
|
||||||
|
pushHistory();
|
||||||
|
};
|
||||||
|
ta.onscroll = function() {
|
||||||
|
document.getElementById('editorGutter').scrollTop = this.scrollTop;
|
||||||
|
document.getElementById('editorHighlight').scrollTop = this.scrollTop;
|
||||||
|
};
|
||||||
|
ta.onkeydown = function(e) {
|
||||||
|
if (e.key === 'Tab') {
|
||||||
|
e.preventDefault();
|
||||||
|
const start = this.selectionStart;
|
||||||
|
const end = this.selectionEnd;
|
||||||
|
this.value = this.value.substring(0, start) + ' ' + this.value.substring(end);
|
||||||
|
this.selectionStart = this.selectionEnd = start + 4;
|
||||||
|
this.dispatchEvent(new Event('input'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
ta.onmouseup = ta.onkeyup = function() {
|
||||||
|
updateCursorPos();
|
||||||
|
};
|
||||||
|
ta.addEventListener('paste', function() {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.dispatchEvent(new Event('input'));
|
||||||
|
updateCursorPos();
|
||||||
|
}, 10);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveFile() {
|
||||||
|
const content = document.getElementById('editorContent').value;
|
||||||
|
document.getElementById('editorStatus').textContent = 'Saving...';
|
||||||
|
|
||||||
|
fetch('/servers/<?= $server['id'] ?>/nginx/file', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'X-CSRF-Token': getCsrfToken(),
|
||||||
|
},
|
||||||
|
body: 'file=' + encodeURIComponent(currentEditFile) + '&content=' + encodeURIComponent(content) + '&_csrf_token=' + encodeURIComponent(getCsrfToken()),
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
showToast(data.success ? 'success' : 'error', data.message);
|
||||||
|
if (data.success) {
|
||||||
|
editorDirty = false;
|
||||||
|
document.getElementById('editorStatus').textContent = 'Saved';
|
||||||
|
document.getElementById('editorDirtyIndicator').style.display = 'none';
|
||||||
|
closeEditor();
|
||||||
|
setTimeout(() => location.reload(), 500);
|
||||||
|
} else {
|
||||||
|
document.getElementById('editorStatus').textContent = 'Save failed';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
showToast('error', 'Save request failed');
|
||||||
|
document.getElementById('editorStatus').textContent = 'Error';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteSite(site) {
|
||||||
|
if (!confirm("Permanently delete site '" + site + "'?\nThis will remove both the config file and site directory contents.")) return;
|
||||||
|
if (!confirm("Are you sure? This cannot be undone.")) return;
|
||||||
|
|
||||||
|
fetch('/servers/<?= $server['id'] ?>/nginx/site', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'X-CSRF-Token': getCsrfToken(),
|
||||||
|
},
|
||||||
|
body: '_method=DELETE&site=' + encodeURIComponent(site) + '&_csrf_token=' + encodeURIComponent(getCsrfToken()),
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
showToast(data.success ? 'success' : 'error', data.message);
|
||||||
|
if (data.success) setTimeout(() => location.reload(), 500);
|
||||||
|
})
|
||||||
|
.catch(err => showToast('error', 'Request failed: ' + err.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSite(site, enable) {
|
||||||
|
const action = enable ? 'enable' : 'disable';
|
||||||
|
if (!confirm((enable ? 'Enable' : 'Disable') + " site '" + site + "'?")) return;
|
||||||
|
|
||||||
|
fetch('/servers/<?= $server['id'] ?>/nginx/toggle-site', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'X-CSRF-Token': getCsrfToken(),
|
||||||
|
},
|
||||||
|
body: 'site=' + encodeURIComponent(site) + '&enable=' + (enable ? '1' : '0') + '&_csrf_token=' + encodeURIComponent(getCsrfToken()),
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
showToast(data.success ? 'success' : 'error', data.message);
|
||||||
|
if (data.success) setTimeout(() => location.reload(), 500);
|
||||||
|
})
|
||||||
|
.catch(err => showToast('error', 'Request failed: ' + err.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeEditor() {
|
||||||
|
if (editorDirty && !confirm('Discard unsaved changes?')) return;
|
||||||
|
document.getElementById('nginxEditorModal').style.display = 'none';
|
||||||
|
currentEditFile = null;
|
||||||
|
editorDirty = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeNginxModal() {
|
||||||
|
document.getElementById('nginxResultModal').style.display = 'none';
|
||||||
|
document.getElementById('nginxResultOutput').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
function openNewSiteModal() {
|
||||||
|
document.getElementById('newSiteForm').reset();
|
||||||
|
document.getElementById('siteServerName').value = '';
|
||||||
|
document.getElementById('sitePhp').checked = true;
|
||||||
|
document.getElementById('siteEnable').checked = true;
|
||||||
|
document.getElementById('nginxNewSiteModal').style.display = 'flex';
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeNewSiteModal() {
|
||||||
|
document.getElementById('nginxNewSiteModal').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSite(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const btn = document.getElementById('createSiteBtn');
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Creating...';
|
||||||
|
|
||||||
|
const formData = new URLSearchParams(new FormData(document.getElementById('newSiteForm')));
|
||||||
|
formData.append('_csrf_token', getCsrfToken());
|
||||||
|
|
||||||
|
fetch('/servers/<?= $server['id'] ?>/nginx/site', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'X-CSRF-Token': getCsrfToken(),
|
||||||
|
},
|
||||||
|
body: formData.toString(),
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
showToast(data.success ? 'success' : 'error', data.message);
|
||||||
|
if (data.success) {
|
||||||
|
closeNewSiteModal();
|
||||||
|
setTimeout(() => location.reload(), 800);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => showToast('error', 'Request failed: ' + err.message))
|
||||||
|
.finally(() => { btn.disabled = false; btn.innerHTML = '<i class="fas fa-plus"></i> Create Site'; });
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('keydown', function(e) {
|
||||||
|
if (e.key === 'Escape') { closeEditor(); closeNginxModal(); }
|
||||||
|
if ((e.ctrlKey || e.metaKey) && e.key === 's' && currentEditFile) {
|
||||||
|
e.preventDefault();
|
||||||
|
saveFile();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<?php endif; ?>
|
||||||
166
views/servers/processes.php
Executable file
166
views/servers/processes.php
Executable file
@@ -0,0 +1,166 @@
|
|||||||
|
<?php
|
||||||
|
$server = $server ?? [];
|
||||||
|
$processList = $processList ?? [];
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="page-header">
|
||||||
|
<div class="page-header-left">
|
||||||
|
<div class="back-link">
|
||||||
|
<a href="/servers/<?= $server['id'] ?>"><i class="fas fa-arrow-left"></i> Back to Server</a>
|
||||||
|
</div>
|
||||||
|
<h1 class="page-title">Processes</h1>
|
||||||
|
<p class="page-subtitle">
|
||||||
|
<?= htmlspecialchars($server['name']) ?>
|
||||||
|
· <?= count($processList) ?> processes
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="page-header-right">
|
||||||
|
<button class="btn btn-secondary" onclick="location.reload()">
|
||||||
|
<i class="fas fa-sync-alt"></i> Refresh
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<?php if (empty($processList)): ?>
|
||||||
|
<div class="empty-state">
|
||||||
|
<i class="fas fa-microchip empty-icon"></i>
|
||||||
|
<p>No process data available.</p>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover table-sm" id="processTable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>PID</th>
|
||||||
|
<th>User</th>
|
||||||
|
<th>CPU %</th>
|
||||||
|
<th>MEM %</th>
|
||||||
|
<th>RSS</th>
|
||||||
|
<th>STAT</th>
|
||||||
|
<th>Time</th>
|
||||||
|
<th>Command</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($processList as $p): ?>
|
||||||
|
<tr>
|
||||||
|
<td class="font-mono"><?= $p['pid'] ?></td>
|
||||||
|
<td><?= htmlspecialchars($p['user']) ?></td>
|
||||||
|
<td>
|
||||||
|
<span class="badge <?= $p['cpu'] > 50 ? 'badge-danger' : ($p['cpu'] > 10 ? 'badge-warning' : 'badge-info') ?>">
|
||||||
|
<?= number_format($p['cpu'], 1) ?>%
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td><?= number_format($p['mem'], 1) ?>%</td>
|
||||||
|
<td class="font-mono"><?= format_bytes($p['rss'] * 1024) ?></td>
|
||||||
|
<td><code><?= htmlspecialchars($p['stat']) ?></code></td>
|
||||||
|
<td class="font-mono"><?= htmlspecialchars($p['time']) ?></td>
|
||||||
|
<td class="cell-command" title="<?= htmlspecialchars($p['command']) ?>">
|
||||||
|
<code><?= htmlspecialchars(mb_substr($p['command'], 0, 60)) ?><?= mb_strlen($p['command']) > 60 ? '...' : '' ?></code>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-xs btn-danger" onclick="killProcess(<?= $p['pid'] ?>, this)"
|
||||||
|
title="Kill process <?= $p['pid'] ?>">
|
||||||
|
<i class="fas fa-times"></i> Kill
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="killModal" class="modal">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3><i class="fas fa-exclamation-triangle text-danger"></i> Kill Process</h3>
|
||||||
|
<button class="modal-close" onclick="closeKillModal()">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>Are you sure you want to terminate process <strong id="killPidDisplay"></strong>?</p>
|
||||||
|
<div class="form-group" style="margin-top: 1rem;">
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input type="checkbox" id="killForce">
|
||||||
|
<span>Force kill (SIGKILL -9) if process does not respond</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-secondary" onclick="closeKillModal()">Cancel</button>
|
||||||
|
<button class="btn btn-danger" id="confirmKillBtn" onclick="confirmKill()">
|
||||||
|
<i class="fas fa-times"></i> Terminate
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let pendingPid = null;
|
||||||
|
let pendingBtn = null;
|
||||||
|
|
||||||
|
function getCsrfToken() {
|
||||||
|
return document.querySelector('meta[name="csrf-token"]')?.content || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function killProcess(pid, btn) {
|
||||||
|
pendingPid = pid;
|
||||||
|
pendingBtn = btn;
|
||||||
|
document.getElementById('killPidDisplay').textContent = pid;
|
||||||
|
document.getElementById('killModal').style.display = 'flex';
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeKillModal() {
|
||||||
|
pendingPid = null;
|
||||||
|
pendingBtn = null;
|
||||||
|
document.getElementById('killForce').checked = false;
|
||||||
|
document.getElementById('killModal').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmKill() {
|
||||||
|
if (!pendingPid) return;
|
||||||
|
|
||||||
|
const signal = document.getElementById('killForce').checked ? 9 : 15;
|
||||||
|
const btn = document.getElementById('confirmKillBtn');
|
||||||
|
const originalText = btn.innerHTML;
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Killing...';
|
||||||
|
|
||||||
|
fetch('/servers/<?= $server['id'] ?>/processes/kill', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'X-CSRF-Token': getCsrfToken(),
|
||||||
|
},
|
||||||
|
body: 'pid=' + pendingPid + '&signal=' + signal + '&_csrf_token=' + encodeURIComponent(getCsrfToken()),
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
showToast(data.success ? 'success' : 'error', data.message);
|
||||||
|
if (data.success && pendingBtn) {
|
||||||
|
const row = pendingBtn.closest('tr');
|
||||||
|
row.style.opacity = '0.4';
|
||||||
|
pendingBtn.disabled = true;
|
||||||
|
pendingBtn.innerHTML = '<i class="fas fa-check"></i> Killed';
|
||||||
|
}
|
||||||
|
closeKillModal();
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
showToast('error', 'Request failed: ' + err.message);
|
||||||
|
closeKillModal();
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.innerHTML = originalText;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('keydown', function(e) {
|
||||||
|
if (e.key === 'Escape') closeKillModal();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
371
views/servers/show.php
Executable file
371
views/servers/show.php
Executable file
@@ -0,0 +1,371 @@
|
|||||||
|
<?php
|
||||||
|
$server = $server ?? [];
|
||||||
|
$metrics = $metrics ?? null;
|
||||||
|
$historicalMetrics = $historicalMetrics ?? [];
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="page-header">
|
||||||
|
<div class="page-header-left">
|
||||||
|
<div class="back-link">
|
||||||
|
<a href="/servers"><i class="fas fa-arrow-left"></i> Back to Servers</a>
|
||||||
|
</div>
|
||||||
|
<h1 class="page-title"><?= htmlspecialchars($server['name'] ?? 'Unknown') ?></h1>
|
||||||
|
<p class="page-subtitle">
|
||||||
|
<?= htmlspecialchars($server['ip_address'] ?? '') ?>:<?= $server['ssh_port'] ?? 22 ?>
|
||||||
|
·
|
||||||
|
<span class="status-badge status-<?= $server['current_status'] ?? 'unknown' ?>">
|
||||||
|
<?= ucfirst($server['current_status'] ?? 'unknown') ?>
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="page-header-right">
|
||||||
|
<div class="btn-group">
|
||||||
|
<a href="/terminal/<?= $server['id'] ?>" class="btn btn-dark">
|
||||||
|
<i class="fas fa-terminal"></i> Terminal
|
||||||
|
</a>
|
||||||
|
<a href="/servers/<?= $server['id'] ?>/edit" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-edit"></i> Edit
|
||||||
|
</a>
|
||||||
|
<div class="dropdown">
|
||||||
|
<button class="btn btn-secondary dropdown-toggle">
|
||||||
|
<i class="fas fa-cog"></i> Actions
|
||||||
|
</button>
|
||||||
|
<div class="dropdown-menu">
|
||||||
|
<button onclick="serverAction('reboot')" class="dropdown-item">
|
||||||
|
<i class="fas fa-redo"></i> Reboot
|
||||||
|
</button>
|
||||||
|
<button onclick="serverAction('shutdown')" class="dropdown-item text-danger">
|
||||||
|
<i class="fas fa-power-off"></i> Shutdown
|
||||||
|
</button>
|
||||||
|
<div class="dropdown-divider"></div>
|
||||||
|
<button onclick="serverAction('test-connection')" class="dropdown-item">
|
||||||
|
<i class="fas fa-plug"></i> Test Connection
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($server['description'] ?? ''): ?>
|
||||||
|
<div class="card card-description">
|
||||||
|
<div class="card-body">
|
||||||
|
<p><?= nl2br(htmlspecialchars($server['description'])) ?></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="stats-grid">
|
||||||
|
<div class="stat-card stat-card-info">
|
||||||
|
<div class="stat-icon"><i class="fas fa-microchip"></i></div>
|
||||||
|
<div class="stat-info">
|
||||||
|
<span class="stat-value" id="cpuMetric"><?= $metrics['cpu_usage'] ?? '-' ?>%</span>
|
||||||
|
<span class="stat-label">CPU Usage</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card stat-card-warning">
|
||||||
|
<div class="stat-icon"><i class="fas fa-memory"></i></div>
|
||||||
|
<div class="stat-info">
|
||||||
|
<span class="stat-value" id="ramMetric"><?= $metrics['ram_usage'] ?? '-' ?>%</span>
|
||||||
|
<span class="stat-label">RAM Usage</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card stat-card-purple">
|
||||||
|
<div class="stat-icon"><i class="fas fa-hdd"></i></div>
|
||||||
|
<div class="stat-info">
|
||||||
|
<span class="stat-value" id="diskMetric"><?= $metrics['disk_usage'] ?? '-' ?>%</span>
|
||||||
|
<span class="stat-label">Disk Usage</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card stat-card-primary">
|
||||||
|
<div class="stat-icon"><i class="fas fa-tachometer-alt"></i></div>
|
||||||
|
<div class="stat-info">
|
||||||
|
<span class="stat-value" id="loadMetric"><?= $metrics['load_average'] ?? '-' ?></span>
|
||||||
|
<span class="stat-label">Load Average</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card stat-card-success">
|
||||||
|
<div class="stat-icon"><i class="fas fa-clock"></i></div>
|
||||||
|
<div class="stat-info">
|
||||||
|
<span class="stat-value" id="uptimeMetric"><?= htmlspecialchars($metrics['uptime'] ?? '-') ?></span>
|
||||||
|
<span class="stat-label">Uptime</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dashboard-grid">
|
||||||
|
<div class="dashboard-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2><i class="fas fa-chart-line"></i> Resource Usage (24h)</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<canvas id="metricsChart" height="300"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dashboard-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2><i class="fas fa-info-circle"></i> Connection Details</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<dl class="details-list">
|
||||||
|
<dt>Host</dt>
|
||||||
|
<dd><code><?= htmlspecialchars($server['ip_address'] ?? '') ?></code></dd>
|
||||||
|
<dt>SSH Port</dt>
|
||||||
|
<dd><?= $server['ssh_port'] ?? 22 ?></dd>
|
||||||
|
<dt>SSH User</dt>
|
||||||
|
<dd><?= htmlspecialchars($server['ssh_user'] ?? '') ?></dd>
|
||||||
|
<dt>Auth Method</dt>
|
||||||
|
<dd>
|
||||||
|
<?php if (!empty($server['ssh_key'])): ?>
|
||||||
|
<span class="badge badge-info">Key-based</span>
|
||||||
|
<?php else: ?>
|
||||||
|
<span class="badge badge-warning">Password</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</dd>
|
||||||
|
<dt>Group</dt>
|
||||||
|
<dd><?= htmlspecialchars($server['group_name'] ?? 'None') ?></dd>
|
||||||
|
<dt>Last Check</dt>
|
||||||
|
<dd><?= $server['last_check_at'] ?? 'Never' ?></dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2><i class="fas fa-tools"></i> Management Actions</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="action-grid">
|
||||||
|
<a href="/terminal/<?= $server['id'] ?>" class="action-card">
|
||||||
|
<i class="fas fa-terminal"></i>
|
||||||
|
<span>Web Terminal</span>
|
||||||
|
</a>
|
||||||
|
<a href="/servers/<?= $server['id'] ?>/processes" class="action-card">
|
||||||
|
<i class="fas fa-tasks"></i>
|
||||||
|
<span>Processes</span>
|
||||||
|
</a>
|
||||||
|
<a href="/servers/<?= $server['id'] ?>/system-info" class="action-card">
|
||||||
|
<i class="fas fa-info-circle"></i>
|
||||||
|
<span>System Info</span>
|
||||||
|
</a>
|
||||||
|
<a href="/servers/<?= $server['id'] ?>/logs" class="action-card">
|
||||||
|
<i class="fas fa-file-alt"></i>
|
||||||
|
<span>System Logs</span>
|
||||||
|
</a>
|
||||||
|
<a href="/servers/<?= $server['id'] ?>/nginx" class="action-card">
|
||||||
|
<svg viewBox="0 0 24 24" fill="currentColor" width="24" height="24" style="opacity:0.8">
|
||||||
|
<path d="M12 2L2 7.2v9.6L12 22l10-5.2V7.2L12 2zm0 1.5l8.5 4.4v8.2L12 20.5l-8.5-4.4V7.9L12 3.5zM7.4 8.5v7l1.5.9V11l3.1 4.3 1.5.9V8.5l-1.5.9v4.4L8.9 9.4l-1.5-.9zm9.2 0l-1.5.9-3.1 4.4V17l1.5-.9V11l3.1-4.3z"/>
|
||||||
|
</svg>
|
||||||
|
<span>Nginx</span>
|
||||||
|
</a>
|
||||||
|
<a href="/servers/<?= $server['id'] ?>/ssl" class="action-card">
|
||||||
|
<i class="fas fa-shield-alt"></i>
|
||||||
|
<span>SSL Certificates</span>
|
||||||
|
</a>
|
||||||
|
<a href="/servers/<?= $server['id'] ?>/database" class="action-card">
|
||||||
|
<i class="fas fa-database"></i>
|
||||||
|
<span>Database Manager</span>
|
||||||
|
</a>
|
||||||
|
<button class="action-card" onclick="restartService()">
|
||||||
|
<i class="fas fa-sync-alt"></i>
|
||||||
|
<span>Restart Service</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form id="csrfForm" style="display:none">
|
||||||
|
<input type="hidden" name="_csrf_token" value="<?= $this->csrfToken() ?>">
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
|
||||||
|
<script>
|
||||||
|
const historicalData = <?= json_encode($historicalMetrics) ?>;
|
||||||
|
|
||||||
|
let metricsChart = null;
|
||||||
|
|
||||||
|
function initChart(data) {
|
||||||
|
const container = document.getElementById('metricsChart').parentNode;
|
||||||
|
const oldCanvas = document.getElementById('metricsChart');
|
||||||
|
const newCanvas = document.createElement('canvas');
|
||||||
|
newCanvas.id = 'metricsChart';
|
||||||
|
newCanvas.height = 300;
|
||||||
|
container.replaceChild(newCanvas, oldCanvas);
|
||||||
|
const ctx = newCanvas.getContext('2d');
|
||||||
|
const labels = data.map(m => {
|
||||||
|
const d = new Date(m.created_at);
|
||||||
|
return d.getHours().toString().padStart(2, '0') + ':' + d.getMinutes().toString().padStart(2, '0');
|
||||||
|
});
|
||||||
|
|
||||||
|
metricsChart = new Chart(ctx, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: labels,
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: 'CPU',
|
||||||
|
data: data.map(m => m.cpu_usage),
|
||||||
|
borderColor: '#3b82f6',
|
||||||
|
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
||||||
|
fill: true,
|
||||||
|
tension: 0.3,
|
||||||
|
pointRadius: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'RAM',
|
||||||
|
data: data.map(m => m.ram_usage),
|
||||||
|
borderColor: '#22c55e',
|
||||||
|
backgroundColor: 'rgba(34, 197, 94, 0.1)',
|
||||||
|
fill: true,
|
||||||
|
tension: 0.3,
|
||||||
|
pointRadius: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Disk',
|
||||||
|
data: data.map(m => m.disk_usage),
|
||||||
|
borderColor: '#8b5cf6',
|
||||||
|
backgroundColor: 'rgba(139, 92, 246, 0.1)',
|
||||||
|
fill: true,
|
||||||
|
tension: 0.3,
|
||||||
|
pointRadius: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
interaction: {
|
||||||
|
intersect: false,
|
||||||
|
mode: 'index',
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
labels: {
|
||||||
|
color: '#9ca3af',
|
||||||
|
font: { family: "'Inter', sans-serif" },
|
||||||
|
boxWidth: 12,
|
||||||
|
padding: 16,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
backgroundColor: '#1a1d2b',
|
||||||
|
borderColor: '#2a2d3d',
|
||||||
|
borderWidth: 1,
|
||||||
|
titleColor: '#e1e4ed',
|
||||||
|
bodyColor: '#9ca3af',
|
||||||
|
padding: 10,
|
||||||
|
cornerRadius: 8,
|
||||||
|
callbacks: {
|
||||||
|
label: function(ctx) {
|
||||||
|
return ctx.dataset.label + ': ' + ctx.parsed.y.toFixed(1) + '%';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
ticks: {
|
||||||
|
color: '#6b7280',
|
||||||
|
maxTicksLimit: 12,
|
||||||
|
font: { size: 11 },
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
color: 'rgba(255,255,255,0.04)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
min: 0,
|
||||||
|
max: 100,
|
||||||
|
ticks: {
|
||||||
|
color: '#6b7280',
|
||||||
|
font: { size: 11 },
|
||||||
|
callback: function(v) { return v + '%'; },
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
color: 'rgba(255,255,255,0.04)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (historicalData.length > 0) {
|
||||||
|
initChart(historicalData);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCsrfToken() {
|
||||||
|
return document.querySelector('meta[name="csrf-token"]')?.content || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function serverAction(action) {
|
||||||
|
if (action === 'shutdown' || action === 'reboot') {
|
||||||
|
if (!confirm('Are you sure you want to ' + action + ' this server?')) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch('/servers/<?= $server['id'] ?>/' + action, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'X-CSRF-Token': getCsrfToken(),
|
||||||
|
},
|
||||||
|
body: '_csrf_token=' + encodeURIComponent(getCsrfToken()),
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
showToast(data.success ? 'success' : 'error', data.message);
|
||||||
|
})
|
||||||
|
.catch(err => showToast('error', 'Request failed: ' + err.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
function restartService() {
|
||||||
|
const service = prompt('Enter service name to restart (e.g., nginx, mysql, apache2):');
|
||||||
|
if (!service) return;
|
||||||
|
|
||||||
|
fetch('/servers/<?= $server['id'] ?>/restart-service', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'X-CSRF-Token': getCsrfToken(),
|
||||||
|
},
|
||||||
|
body: 'service=' + encodeURIComponent(service) + '&_csrf_token=' + encodeURIComponent(getCsrfToken()),
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
showToast(data.success ? 'success' : 'error', data.message);
|
||||||
|
})
|
||||||
|
.catch(err => showToast('error', 'Request failed: ' + err.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshMetrics() {
|
||||||
|
fetch('/servers/<?= $server['id'] ?>/metrics')
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success && data.data) {
|
||||||
|
const m = data.data;
|
||||||
|
document.getElementById('cpuMetric').textContent = (m.cpu_usage || '-') + '%';
|
||||||
|
document.getElementById('ramMetric').textContent = (m.ram_usage || '-') + '%';
|
||||||
|
document.getElementById('diskMetric').textContent = (m.disk_usage || '-') + '%';
|
||||||
|
document.getElementById('loadMetric').textContent = m.load_average || '-';
|
||||||
|
document.getElementById('uptimeMetric').textContent = m.uptime || '-';
|
||||||
|
if (metricsChart && metricsChart.data.labels.length > 0) {
|
||||||
|
const now = new Date();
|
||||||
|
const label = now.getHours().toString().padStart(2, '0') + ':' + now.getMinutes().toString().padStart(2, '0');
|
||||||
|
metricsChart.data.labels.push(label);
|
||||||
|
metricsChart.data.datasets[0].data.push(m.cpu_usage);
|
||||||
|
metricsChart.data.datasets[1].data.push(m.ram_usage);
|
||||||
|
metricsChart.data.datasets[2].data.push(m.disk_usage);
|
||||||
|
if (metricsChart.data.labels.length > 48) {
|
||||||
|
metricsChart.data.labels.shift();
|
||||||
|
metricsChart.data.datasets.forEach(ds => ds.data.shift());
|
||||||
|
}
|
||||||
|
metricsChart.update('none');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setInterval(refreshMetrics, 30000);
|
||||||
|
</script>
|
||||||
265
views/servers/ssl.php
Normal file
265
views/servers/ssl.php
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
<?php
|
||||||
|
$server = $server ?? [];
|
||||||
|
$ssl = $ssl ?? [];
|
||||||
|
$certbotInstalled = $ssl['certbot_installed'] ?? false;
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="page-header">
|
||||||
|
<div class="page-header-left">
|
||||||
|
<div class="back-link">
|
||||||
|
<a href="/servers/<?= $server['id'] ?>"><i class="fas fa-arrow-left"></i> Back to Server</a>
|
||||||
|
</div>
|
||||||
|
<h1 class="page-title"><i class="fas fa-shield-alt"></i> SSL Certificates</h1>
|
||||||
|
<p class="page-subtitle"><?= htmlspecialchars($server['name']) ?></p>
|
||||||
|
</div>
|
||||||
|
<div class="page-header-right">
|
||||||
|
<button class="btn btn-secondary" onclick="location.reload()">
|
||||||
|
<i class="fas fa-sync-alt"></i> Refresh
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if (!$certbotInstalled): ?>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="empty-state">
|
||||||
|
<i class="fas fa-shield-alt empty-icon" style="font-size:3rem;opacity:0.4"></i>
|
||||||
|
<h3>Certbot is not installed</h3>
|
||||||
|
<p>Certbot is required to manage Let's Encrypt SSL certificates on this server.</p>
|
||||||
|
<p class="text-muted">Would you like to install it?</p>
|
||||||
|
<div class="action-group" style="margin-top:1.5rem;display:flex;gap:0.75rem;justify-content:center">
|
||||||
|
<button class="btn btn-primary" onclick="sslAction('install')">
|
||||||
|
<i class="fas fa-download"></i> Install Certbot
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-secondary" onclick="history.back()">
|
||||||
|
<i class="fas fa-arrow-left"></i> Go Back
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<?php
|
||||||
|
$certs = $ssl['certificates'] ?? [];
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="dashboard-grid">
|
||||||
|
<div class="dashboard-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2><i class="fas fa-info-circle"></i> Certbot</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="stats-grid" style="grid-template-columns:repeat(2,1fr);gap:0.75rem">
|
||||||
|
<div class="stat-card" style="padding:1rem">
|
||||||
|
<div class="stat-icon" style="width:40px;height:40px;font-size:1rem;background:var(--success-bg);color:var(--success)">
|
||||||
|
<i class="fas fa-check-circle"></i>
|
||||||
|
</div>
|
||||||
|
<div class="stat-info">
|
||||||
|
<span class="stat-value" style="font-size:1rem">Installed</span>
|
||||||
|
<span class="stat-label"><?= htmlspecialchars($ssl['version'] ?? '') ?></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card" style="padding:1rem">
|
||||||
|
<div class="stat-icon" style="width:40px;height:40px;font-size:1rem;background:var(--info-bg);color:var(--info)">
|
||||||
|
<i class="fas fa-certificate"></i>
|
||||||
|
</div>
|
||||||
|
<div class="stat-info">
|
||||||
|
<span class="stat-value" style="font-size:1rem"><?= count($certs) ?></span>
|
||||||
|
<span class="stat-label">Certificates</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="action-group" style="margin-top:1rem;display:flex;gap:0.5rem;flex-wrap:wrap">
|
||||||
|
<button class="btn btn-sm btn-success" onclick="openRequestModal()">
|
||||||
|
<i class="fas fa-plus"></i> New Certificate
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-warning" onclick="sslAction('renew')">
|
||||||
|
<i class="fas fa-sync-alt"></i> Renew All
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if (empty($certs)): ?>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="empty-state">
|
||||||
|
<i class="fas fa-certificate empty-icon" style="opacity:0.3"></i>
|
||||||
|
<p>No SSL certificates found.</p>
|
||||||
|
<button class="btn btn-primary" onclick="openRequestModal()">
|
||||||
|
<i class="fas fa-plus"></i> Request First Certificate
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="dashboard-grid">
|
||||||
|
<?php foreach ($certs as $cert): ?>
|
||||||
|
<?php
|
||||||
|
$expiryTs = strtotime($cert['expiry']);
|
||||||
|
$expiryDays = $expiryTs ? floor(($expiryTs - time()) / 86400) : null;
|
||||||
|
$expiryClass = 'success';
|
||||||
|
if ($expiryDays !== null && $expiryDays < 30) $expiryClass = 'danger';
|
||||||
|
elseif ($expiryDays !== null && $expiryDays < 60) $expiryClass = 'warning';
|
||||||
|
?>
|
||||||
|
<div class="dashboard-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2><i class="fas fa-certificate text-success"></i> <?= htmlspecialchars($cert['name']) ?></h2>
|
||||||
|
<span class="badge badge-<?= $expiryClass ?>"><?= $expiryDays !== null ? $expiryDays . ' days' : 'Unknown' ?></span>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<dl class="details-list">
|
||||||
|
<dt>Domains</dt>
|
||||||
|
<dd>
|
||||||
|
<?php foreach ($cert['domains'] as $domain): ?>
|
||||||
|
<code><?= htmlspecialchars($domain) ?></code><?= $domain !== end($cert['domains']) ? ', ' : '' ?>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</dd>
|
||||||
|
<dt>Expiry</dt>
|
||||||
|
<dd><?= htmlspecialchars($cert['expiry'] ?: 'Unknown') ?></dd>
|
||||||
|
<?php if (!empty($cert['path'])): ?>
|
||||||
|
<dt>Certificate Path</dt>
|
||||||
|
<dd><code><?= htmlspecialchars($cert['path']) ?></code></dd>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (!empty($cert['key_type'])): ?>
|
||||||
|
<dt>Key Type</dt>
|
||||||
|
<dd><code><?= htmlspecialchars($cert['key_type']) ?></code></dd>
|
||||||
|
<?php endif; ?>
|
||||||
|
</dl>
|
||||||
|
<div class="action-group" style="margin-top:0.75rem;display:flex;gap:0.5rem">
|
||||||
|
<button class="btn btn-xs btn-danger" onclick="deleteCert('<?= htmlspecialchars($cert['name']) ?>')">
|
||||||
|
<i class="fas fa-trash"></i> Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div id="sslRequestModal" class="modal">
|
||||||
|
<div class="modal-dialog" style="max-width:500px">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3><i class="fas fa-plus-circle text-success"></i> New SSL Certificate</h3>
|
||||||
|
<button class="modal-close" onclick="closeRequestModal()">×</button>
|
||||||
|
</div>
|
||||||
|
<form id="sslRequestForm" onsubmit="requestCert(event)">
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="sslDomains">Domain(s)</label>
|
||||||
|
<input type="text" id="sslDomains" name="domains" class="form-input"
|
||||||
|
placeholder="example.com www.example.com" required>
|
||||||
|
<small class="form-hint">Separate multiple domains with spaces</small>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="sslEmail">Email (optional)</label>
|
||||||
|
<input type="email" id="sslEmail" name="email" class="form-input"
|
||||||
|
placeholder="admin@example.com">
|
||||||
|
<small class="form-hint">For renewal notices</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" onclick="closeRequestModal()">Cancel</button>
|
||||||
|
<button type="submit" class="btn btn-success" id="requestCertBtn">
|
||||||
|
<i class="fas fa-plus"></i> Request Certificate
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div id="sslResultModal" class="modal">
|
||||||
|
<div class="modal-dialog" style="max-width:600px">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3 id="sslResultTitle">Result</h3>
|
||||||
|
<button class="modal-close" onclick="closeSslResultModal()">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p id="sslResultMessage"></p>
|
||||||
|
<div id="sslResultOutput" class="nginx-config-viewer nginx-test-output" style="display:none;margin-top:0.75rem">
|
||||||
|
<pre><code id="sslResultOutputText"></code></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-secondary" onclick="closeSslResultModal()">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function getCsrfToken() {
|
||||||
|
return document.querySelector('meta[name="csrf-token"]')?.content || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function sslAction(action, extraData) {
|
||||||
|
const data = 'action=' + action + '&_csrf_token=' + encodeURIComponent(getCsrfToken());
|
||||||
|
const body = extraData ? data + '&' + extraData : data;
|
||||||
|
|
||||||
|
fetch('/servers/<?= $server['id'] ?>/ssl', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'X-CSRF-Token': getCsrfToken(),
|
||||||
|
},
|
||||||
|
body: body,
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
showToast(data.success ? 'success' : 'error', data.message);
|
||||||
|
if (data.output) {
|
||||||
|
document.getElementById('sslResultMessage').textContent = data.message || '';
|
||||||
|
document.getElementById('sslResultOutputText').textContent = data.output;
|
||||||
|
document.getElementById('sslResultOutput').style.display = 'block';
|
||||||
|
document.getElementById('sslResultModal').style.display = 'flex';
|
||||||
|
}
|
||||||
|
if (action === 'install' && data.success) {
|
||||||
|
setTimeout(() => location.reload(), 1500);
|
||||||
|
}
|
||||||
|
if (['renew', 'delete'].includes(action) && data.success) {
|
||||||
|
setTimeout(() => location.reload(), 1000);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => showToast('error', 'Request failed: ' + err.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
function openRequestModal() {
|
||||||
|
document.getElementById('sslRequestForm').reset();
|
||||||
|
document.getElementById('sslRequestModal').style.display = 'flex';
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeRequestModal() {
|
||||||
|
document.getElementById('sslRequestModal').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestCert(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const btn = document.getElementById('requestCertBtn');
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Requesting...';
|
||||||
|
|
||||||
|
const domains = document.getElementById('sslDomains').value;
|
||||||
|
const email = document.getElementById('sslEmail').value;
|
||||||
|
|
||||||
|
sslAction('request', 'domains=' + encodeURIComponent(domains) + '&email=' + encodeURIComponent(email));
|
||||||
|
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.innerHTML = '<i class="fas fa-plus"></i> Request Certificate';
|
||||||
|
closeRequestModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteCert(name) {
|
||||||
|
if (!confirm("Delete certificate '" + name + "'?")) return;
|
||||||
|
sslAction('delete', 'cert_name=' + encodeURIComponent(name));
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeSslResultModal() {
|
||||||
|
document.getElementById('sslResultModal').style.display = 'none';
|
||||||
|
document.getElementById('sslResultOutput').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('keydown', function(e) {
|
||||||
|
if (e.key === 'Escape') { closeRequestModal(); closeSslResultModal(); }
|
||||||
|
});
|
||||||
|
</script>
|
||||||
171
views/servers/system_info.php
Executable file
171
views/servers/system_info.php
Executable file
@@ -0,0 +1,171 @@
|
|||||||
|
<?php
|
||||||
|
$server = $server ?? [];
|
||||||
|
$systemInfo = $systemInfo ?? [];
|
||||||
|
|
||||||
|
$memTotal = (int) ($systemInfo['mem_total'] ?? 0);
|
||||||
|
$memUsed = (int) ($systemInfo['mem_used'] ?? 0);
|
||||||
|
$memAvail = (int) ($systemInfo['mem_avail'] ?? 0);
|
||||||
|
$memPercent = $memTotal > 0 ? round($memUsed / $memTotal * 100, 1) : 0;
|
||||||
|
|
||||||
|
$diskTotal = (int) ($systemInfo['disk_total'] ?? 0);
|
||||||
|
$diskUsed = (int) ($systemInfo['disk_used'] ?? 0);
|
||||||
|
$diskAvail = (int) ($systemInfo['disk_avail'] ?? 0);
|
||||||
|
$diskPercent = (int) ($systemInfo['disk_percent'] ?? 0);
|
||||||
|
|
||||||
|
$cpuModel = trim(str_replace('model name : ', '', $systemInfo['cpu_model'] ?? ''));
|
||||||
|
$osName = trim(str_replace('NAME=', '', $systemInfo['os'] ?? ''), '"');
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="page-header">
|
||||||
|
<div class="page-header-left">
|
||||||
|
<div class="back-link">
|
||||||
|
<a href="/servers/<?= $server['id'] ?>"><i class="fas fa-arrow-left"></i> Back to Server</a>
|
||||||
|
</div>
|
||||||
|
<h1 class="page-title">System Information</h1>
|
||||||
|
<p class="page-subtitle"><?= htmlspecialchars($server['name']) ?></p>
|
||||||
|
</div>
|
||||||
|
<div class="page-header-right">
|
||||||
|
<button class="btn btn-secondary" onclick="location.reload()">
|
||||||
|
<i class="fas fa-sync-alt"></i> Refresh
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dashboard-grid">
|
||||||
|
<div class="dashboard-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2><i class="fas fa-server"></i> System</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<dl class="details-list">
|
||||||
|
<dt>Hostname</dt>
|
||||||
|
<dd><code><?= htmlspecialchars($systemInfo['hostname'] ?? 'Unknown') ?></code></dd>
|
||||||
|
<dt>Operating System</dt>
|
||||||
|
<dd><?= htmlspecialchars($osName ?: 'Unknown') ?></dd>
|
||||||
|
<dt>Kernel</dt>
|
||||||
|
<dd><code><?= htmlspecialchars($systemInfo['kernel'] ?? 'Unknown') ?></code></dd>
|
||||||
|
<dt>Uptime</dt>
|
||||||
|
<dd><i class="fas fa-clock text-success"></i> <?= htmlspecialchars($systemInfo['uptime'] ?? 'Unknown') ?></dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dashboard-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2><i class="fas fa-microchip"></i> CPU</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<dl class="details-list">
|
||||||
|
<dt>Model</dt>
|
||||||
|
<dd><code><?= htmlspecialchars($cpuModel ?: 'Unknown') ?></code></dd>
|
||||||
|
<dt>Cores</dt>
|
||||||
|
<dd>
|
||||||
|
<span class="badge badge-primary badge-lg">
|
||||||
|
<i class="fas fa-microchip"></i>
|
||||||
|
<?= htmlspecialchars($systemInfo['cpu_cores'] ?? '?') ?> cores
|
||||||
|
</span>
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dashboard-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2><i class="fas fa-memory"></i> Memory</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<?php if ($memTotal > 0): ?>
|
||||||
|
<div class="metric-block">
|
||||||
|
<div class="metric-info">
|
||||||
|
<span class="metric-label">Usage</span>
|
||||||
|
<span class="metric-value">
|
||||||
|
<?= format_bytes($memUsed) ?> / <?= format_bytes($memTotal) ?>
|
||||||
|
<span class="text-muted">(<?= $memPercent ?>%)</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="progress-bar">
|
||||||
|
<div class="progress-fill <?= $memPercent > 90 ? 'progress-danger' : ($memPercent > 70 ? 'progress-warning' : 'progress-success') ?>"
|
||||||
|
style="width: <?= min($memPercent, 100) ?>%"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="metric-block" style="margin-top: 0.75rem;">
|
||||||
|
<div class="metric-info">
|
||||||
|
<span class="metric-label">Available</span>
|
||||||
|
<span class="metric-value"><?= format_bytes($memAvail) ?></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<p class="text-muted">Memory information not available</p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dashboard-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2><i class="fas fa-hdd"></i> Storage</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<?php if ($diskTotal > 0): ?>
|
||||||
|
<div class="metric-block">
|
||||||
|
<div class="metric-info">
|
||||||
|
<span class="metric-label">Usage</span>
|
||||||
|
<span class="metric-value">
|
||||||
|
<?= format_bytes($diskUsed) ?> / <?= format_bytes($diskTotal) ?>
|
||||||
|
<span class="text-muted">(<?= $diskPercent ?>%)</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="progress-bar">
|
||||||
|
<div class="progress-fill <?= $diskPercent > 90 ? 'progress-danger' : ($diskPercent > 70 ? 'progress-warning' : 'progress-success') ?>"
|
||||||
|
style="width: <?= min($diskPercent, 100) ?>%"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="metric-block" style="margin-top: 0.75rem;">
|
||||||
|
<div class="metric-info">
|
||||||
|
<span class="metric-label">Free</span>
|
||||||
|
<span class="metric-value"><?= format_bytes($diskAvail) ?></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<p class="text-muted">Disk information not available</p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2><i class="fas fa-chart-pie"></i> Resource Summary</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="stats-grid">
|
||||||
|
<div class="stat-card stat-card-info">
|
||||||
|
<div class="stat-icon"><i class="fas fa-microchip"></i></div>
|
||||||
|
<div class="stat-info">
|
||||||
|
<span class="stat-value"><?= htmlspecialchars($systemInfo['cpu_cores'] ?? '0') ?></span>
|
||||||
|
<span class="stat-label">CPU Cores</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card stat-card-warning">
|
||||||
|
<div class="stat-icon"><i class="fas fa-memory"></i></div>
|
||||||
|
<div class="stat-info">
|
||||||
|
<span class="stat-value"><?= format_bytes($memTotal) ?></span>
|
||||||
|
<span class="stat-label">Total RAM</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card stat-card-purple">
|
||||||
|
<div class="stat-icon"><i class="fas fa-hdd"></i></div>
|
||||||
|
<div class="stat-info">
|
||||||
|
<span class="stat-value"><?= format_bytes($diskTotal) ?></span>
|
||||||
|
<span class="stat-label">Total Disk</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card stat-card-success">
|
||||||
|
<div class="stat-icon"><i class="fas fa-clock"></i></div>
|
||||||
|
<div class="stat-info">
|
||||||
|
<span class="stat-value" style="font-size:0.8rem"><?= htmlspecialchars($systemInfo['uptime'] ?? '-') ?></span>
|
||||||
|
<span class="stat-label">Uptime</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
269
views/terminal/index.php
Executable file
269
views/terminal/index.php
Executable file
@@ -0,0 +1,269 @@
|
|||||||
|
<?php
|
||||||
|
$server = $server ?? [];
|
||||||
|
$commandHistory = $commandHistory ?? [];
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="page-header">
|
||||||
|
<div class="page-header-left">
|
||||||
|
<div class="back-link">
|
||||||
|
<a href="/servers/<?= $server['id'] ?>"><i class="fas fa-arrow-left"></i> Back to Server</a>
|
||||||
|
</div>
|
||||||
|
<h1 class="page-title">Web Terminal</h1>
|
||||||
|
<p class="page-subtitle">
|
||||||
|
<?= htmlspecialchars($server['name']) ?> — <?= htmlspecialchars($server['ip_address']) ?>:<?= $server['ssh_port'] ?>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="page-header-right">
|
||||||
|
<button class="btn btn-secondary" onclick="clearTerminal()">
|
||||||
|
<i class="fas fa-eraser"></i> Clear
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-danger btn-sm" onclick="clearHistory()">
|
||||||
|
<i class="fas fa-trash"></i> Clear History
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="terminal-container" id="terminalContainer">
|
||||||
|
<div class="terminal-header">
|
||||||
|
<span class="terminal-title">
|
||||||
|
<i class="fas fa-terminal"></i>
|
||||||
|
<?= htmlspecialchars($server['ssh_user']) ?>@<?= htmlspecialchars($server['ip_address']) ?>
|
||||||
|
</span>
|
||||||
|
<span class="terminal-status" id="connectionStatus">
|
||||||
|
<span class="status-dot offline"></span> Disconnected
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="terminal-body" id="terminalOutput">
|
||||||
|
<div class="terminal-welcome">
|
||||||
|
<pre class="terminal-ascii">
|
||||||
|
____ __ __
|
||||||
|
/ ___| ___ _ ____ _____ _ __ | \/ | __ _ _ __ __ _ __ _ ___ _ __
|
||||||
|
\___ \ / _ \ '__\ \ / / _ \ '__| | |\/| |/ _` | '_ \ / _` |/ _` |/ _ \ '__|
|
||||||
|
___) | __/ | \ V / __/ | | | | | (_| | | | | (_| | (_| | __/ |
|
||||||
|
|____/ \___|_| \_/ \___|_| |_| |_|\__,_|_| |_|\__,_|\__, |\___|_|
|
||||||
|
|___/
|
||||||
|
</pre>
|
||||||
|
<p>Connected to <strong><?= htmlspecialchars($server['name']) ?></strong></p>
|
||||||
|
<p class="text-muted">Type commands below to execute them on the remote server.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="terminal-input-area">
|
||||||
|
<div class="terminal-prompt">
|
||||||
|
<span class="prompt-user"><?= htmlspecialchars($server['ssh_user']) ?></span>
|
||||||
|
<span>@</span>
|
||||||
|
<span class="prompt-host"><?= htmlspecialchars($server['ip_address']) ?></span>
|
||||||
|
<span>:</span>
|
||||||
|
<span class="prompt-path">~</span>
|
||||||
|
<span>$ </span>
|
||||||
|
</div>
|
||||||
|
<input type="text" id="terminalInput" class="terminal-input"
|
||||||
|
placeholder="Type command and press Enter..."
|
||||||
|
autocomplete="off" autofocus>
|
||||||
|
<button class="btn btn-primary" id="executeBtn" onclick="executeCommand()">
|
||||||
|
<i class="fas fa-play"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dashboard-grid" style="margin-top: 1.5rem;">
|
||||||
|
<div class="dashboard-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2><i class="fas fa-history"></i> Command History</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<?php if (empty($commandHistory)): ?>
|
||||||
|
<div class="empty-state small">
|
||||||
|
<p>No command history.</p>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="table-responsive" style="max-height: 300px; overflow-y: auto;">
|
||||||
|
<table class="table table-sm">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Time</th>
|
||||||
|
<th>Command</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>User</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($commandHistory as $entry): ?>
|
||||||
|
<tr>
|
||||||
|
<td><?= date('H:i:s', strtotime($entry['executed_at'] ?? '')) ?></td>
|
||||||
|
<td><code><?= htmlspecialchars($entry['command']) ?></code></td>
|
||||||
|
<td>
|
||||||
|
<span class="badge <?= ($entry['exit_status'] ?? 1) == 0 ? 'badge-success' : 'badge-danger' ?>">
|
||||||
|
<?= ($entry['exit_status'] ?? 1) == 0 ? 'OK' : 'ERR' ?>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td><?= htmlspecialchars($entry['username'] ?? 'System') ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dashboard-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2><i class="fas fa-question-circle"></i> Quick Commands</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="quick-commands">
|
||||||
|
<button class="quick-cmd" onclick="runQuickCmd('uptime')">
|
||||||
|
<i class="fas fa-clock"></i> uptime
|
||||||
|
</button>
|
||||||
|
<button class="quick-cmd" onclick="runQuickCmd('free -h')">
|
||||||
|
<i class="fas fa-memory"></i> free -h
|
||||||
|
</button>
|
||||||
|
<button class="quick-cmd" onclick="runQuickCmd('df -h')">
|
||||||
|
<i class="fas fa-hdd"></i> df -h
|
||||||
|
</button>
|
||||||
|
<button class="quick-cmd" onclick="runQuickCmd('top -bn1 | head -20')">
|
||||||
|
<i class="fas fa-chart-bar"></i> top
|
||||||
|
</button>
|
||||||
|
<button class="quick-cmd" onclick="runQuickCmd('ps aux --sort=-%cpu | head -20')">
|
||||||
|
<i class="fas fa-tasks"></i> ps aux
|
||||||
|
</button>
|
||||||
|
<button class="quick-cmd" onclick="runQuickCmd('who')">
|
||||||
|
<i class="fas fa-users"></i> who
|
||||||
|
</button>
|
||||||
|
<button class="quick-cmd" onclick="runQuickCmd('netstat -tulpn 2>/dev/null || ss -tulpn')">
|
||||||
|
<i class="fas fa-network-wired"></i> netstat
|
||||||
|
</button>
|
||||||
|
<button class="quick-cmd" onclick="runQuickCmd('systemctl list-units --type=service --state=running')">
|
||||||
|
<i class="fas fa-cogs"></i> services
|
||||||
|
</button>
|
||||||
|
<button class="quick-cmd" onclick="runQuickCmd('tail -n 50 /var/log/syslog 2>/dev/null || tail -n 50 /var/log/messages')">
|
||||||
|
<i class="fas fa-file-alt"></i> syslog
|
||||||
|
</button>
|
||||||
|
<button class="quick-cmd" onclick="runQuickCmd('last -n 20')">
|
||||||
|
<i class="fas fa-history"></i> last
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const serverId = <?= $server['id'] ?>;
|
||||||
|
|
||||||
|
function getCsrfToken() {
|
||||||
|
return document.querySelector('meta[name="csrf-token"]')?.content || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const terminalInput = document.getElementById('terminalInput');
|
||||||
|
const terminalOutput = document.getElementById('terminalOutput');
|
||||||
|
const connectionStatus = document.getElementById('connectionStatus');
|
||||||
|
|
||||||
|
terminalInput.addEventListener('keydown', function(e) {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
executeCommand();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function escapeHtml(text) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = text;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
function executeCommand() {
|
||||||
|
const command = terminalInput.value.trim();
|
||||||
|
if (!command) return;
|
||||||
|
|
||||||
|
appendTerminalLine(command, 'command');
|
||||||
|
|
||||||
|
terminalInput.value = '';
|
||||||
|
terminalInput.disabled = true;
|
||||||
|
document.getElementById('executeBtn').disabled = true;
|
||||||
|
|
||||||
|
updateStatus('executing', 'Executing...');
|
||||||
|
|
||||||
|
fetch('/terminal/' + serverId + '/execute', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'X-CSRF-Token': getCsrfToken(),
|
||||||
|
},
|
||||||
|
body: 'command=' + encodeURIComponent(command) + '&_csrf_token=' + encodeURIComponent(getCsrfToken()),
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
terminalInput.disabled = false;
|
||||||
|
document.getElementById('executeBtn').disabled = false;
|
||||||
|
terminalInput.focus();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
if (data.output) {
|
||||||
|
appendTerminalLine(data.output, 'output');
|
||||||
|
}
|
||||||
|
if (data.stderr) {
|
||||||
|
appendTerminalLine(data.stderr, 'error');
|
||||||
|
}
|
||||||
|
updateStatus('connected', 'Connected');
|
||||||
|
} else {
|
||||||
|
appendTerminalLine(data.message || 'Command failed', 'error');
|
||||||
|
updateStatus('error', 'Error');
|
||||||
|
}
|
||||||
|
|
||||||
|
const terminalBody = document.getElementById('terminalOutput');
|
||||||
|
terminalBody.scrollTop = terminalBody.scrollHeight;
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
terminalInput.disabled = false;
|
||||||
|
document.getElementById('executeBtn').disabled = false;
|
||||||
|
appendTerminalLine('Connection error: ' + err.message, 'error');
|
||||||
|
updateStatus('error', 'Error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendTerminalLine(text, type) {
|
||||||
|
const line = document.createElement('div');
|
||||||
|
line.className = 'terminal-line terminal-' + type;
|
||||||
|
if (type === 'command') {
|
||||||
|
line.innerHTML = '<span class="terminal-prompt-inline">$</span> ' + escapeHtml(text);
|
||||||
|
} else {
|
||||||
|
line.innerHTML = '<pre>' + escapeHtml(text) + '</pre>';
|
||||||
|
}
|
||||||
|
terminalOutput.appendChild(line);
|
||||||
|
terminalOutput.scrollTop = terminalOutput.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
function runQuickCmd(cmd) {
|
||||||
|
terminalInput.value = cmd;
|
||||||
|
executeCommand();
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearTerminal() {
|
||||||
|
terminalOutput.innerHTML = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearHistory() {
|
||||||
|
if (!confirm('Clear all command history for this server?')) return;
|
||||||
|
|
||||||
|
fetch('/terminal/' + serverId + '/clear-history', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'X-CSRF-Token': getCsrfToken(),
|
||||||
|
},
|
||||||
|
body: '_csrf_token=' + encodeURIComponent(getCsrfToken()),
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
showToast(data.success ? 'success' : 'error', data.message);
|
||||||
|
if (data.success) location.reload();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateStatus(status, text) {
|
||||||
|
const dot = connectionStatus.querySelector('.status-dot');
|
||||||
|
dot.className = 'status-dot ' + status;
|
||||||
|
connectionStatus.lastChild.textContent = ' ' + text;
|
||||||
|
}
|
||||||
|
|
||||||
|
terminalInput.focus();
|
||||||
|
</script>
|
||||||
Reference in New Issue
Block a user