Merge pull request 'fix: CSS sync, sidebar restore, asset versioning, route/auth fixes, admin dashboard' (#92) from fix/policy-container-width into master
Reviewed-on: #92 Reviewed-by: rafaga21 <rafaelminaya20@hotmail.com>
This commit was merged in pull request #92.
This commit is contained in:
@@ -301,10 +301,7 @@ code, pre {
|
||||
.sidebar.collapsed .nav-chevron,
|
||||
.sidebar.collapsed .notification-badge,
|
||||
.sidebar.collapsed .nav-divider,
|
||||
.sidebar.collapsed .nav-section,
|
||||
.sidebar.collapsed .user-details,
|
||||
.sidebar.collapsed .user-menu,
|
||||
.sidebar.collapsed .sidebar-footer {
|
||||
.sidebar.collapsed .nav-section {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -317,10 +314,6 @@ code, pre {
|
||||
padding: 0.65rem;
|
||||
}
|
||||
|
||||
.sidebar.collapsed .user-info {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Main Content
|
||||
========================================================================== */
|
||||
@@ -374,6 +367,39 @@ code, pre {
|
||||
font-size: 0.88rem;
|
||||
color: var(--text-secondary);
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
background: none;
|
||||
border: none;
|
||||
font-family: inherit;
|
||||
padding: 0.4rem 0.6rem;
|
||||
border-radius: var(--radius-sm);
|
||||
transition: background var(--transition);
|
||||
}
|
||||
|
||||
.topbar-user:hover {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.topbar-user-dropdown {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dropdown-menu-right {
|
||||
right: 0;
|
||||
left: auto;
|
||||
}
|
||||
|
||||
.dropdown-header {
|
||||
padding: 0.55rem 1rem;
|
||||
font-size: 0.88rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.dropdown-header small {
|
||||
font-size: 0.72rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
@@ -1056,7 +1082,8 @@ textarea.form-input {
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.dropdown.active .dropdown-menu {
|
||||
.dropdown.active .dropdown-menu,
|
||||
.dropdown-menu.open {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@@ -2302,10 +2329,7 @@ textarea.form-input {
|
||||
.sidebar.mobile-open.collapsed .nav-chevron,
|
||||
.sidebar.mobile-open.collapsed .notification-badge,
|
||||
.sidebar.mobile-open.collapsed .nav-divider,
|
||||
.sidebar.mobile-open.collapsed .nav-section,
|
||||
.sidebar.mobile-open.collapsed .user-details,
|
||||
.sidebar.mobile-open.collapsed .user-menu,
|
||||
.sidebar.mobile-open.collapsed .sidebar-footer {
|
||||
.sidebar.mobile-open.collapsed .nav-section {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@@ -2318,10 +2342,6 @@ textarea.form-input {
|
||||
padding: 0.65rem 1.25rem;
|
||||
}
|
||||
|
||||
.sidebar.mobile-open.collapsed .user-info {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.main-area {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
@@ -3280,4 +3300,21 @@ input[type="range"]::-moz-range-track {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.notif-view-all {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 10px 16px;
|
||||
font-size: 0.82rem;
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
border-top: 1px solid var(--border-color);
|
||||
transition: background var(--transition);
|
||||
}
|
||||
|
||||
.notif-view-all:hover {
|
||||
background: var(--bg-hover);
|
||||
color: var(--accent-hover);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@ function openNotifications() {
|
||||
const dd = document.getElementById('notifDropdown');
|
||||
dd.classList.add('open');
|
||||
fetchNotifications();
|
||||
const userMenu = document.getElementById('userMenu');
|
||||
if (userMenu) userMenu.classList.remove('open');
|
||||
document.addEventListener('click', notifOutsideClick);
|
||||
}
|
||||
|
||||
@@ -31,6 +33,11 @@ function notifOutsideClick(e) {
|
||||
if (!wrapper.contains(e.target)) {
|
||||
closeNotifications();
|
||||
}
|
||||
const userWrapper = document.querySelector('.topbar-user-dropdown');
|
||||
if (userWrapper && !userWrapper.contains(e.target)) {
|
||||
const userMenu = document.getElementById('userMenu');
|
||||
if (userMenu) userMenu.classList.remove('open');
|
||||
}
|
||||
}
|
||||
|
||||
function fetchNotifications() {
|
||||
@@ -48,14 +55,14 @@ function fetchNotifications() {
|
||||
list.innerHTML = '<div style="padding:40px 20px;text-align:center;color:var(--text-muted)">Could not load notifications.</div>';
|
||||
return;
|
||||
}
|
||||
renderNotifications(d.data || [], d.unread_count || 0);
|
||||
renderNotifications(d.data || [], d.unread_count || 0, d.pagination?.total || 0);
|
||||
})
|
||||
.catch(() => {
|
||||
list.innerHTML = '<div style="padding:40px 20px;text-align:center;color:var(--danger)">Failed to load notifications.</div>';
|
||||
});
|
||||
}
|
||||
|
||||
function renderNotifications(items, unreadCount) {
|
||||
function renderNotifications(items, unreadCount, total) {
|
||||
const list = document.getElementById('notifList');
|
||||
|
||||
if (!items.length) {
|
||||
@@ -85,6 +92,13 @@ function renderNotifications(items, unreadCount) {
|
||||
+ (n.is_read ? '' : '<div class="notif-dot"></div>')
|
||||
+ '</div>';
|
||||
});
|
||||
|
||||
if (total > items.length) {
|
||||
html += '<a href="/notifications" class="notif-view-all">'
|
||||
+ 'View all notifications (' + total + ' total)'
|
||||
+ '</a>';
|
||||
}
|
||||
|
||||
list.innerHTML = html;
|
||||
|
||||
document.getElementById('markAllReadBtn').style.display = unreadCount > 0 ? 'inline-flex' : 'none';
|
||||
@@ -200,3 +214,32 @@ function toggleServerList(e) {
|
||||
sub.style.maxHeight = sub.scrollHeight + 'px';
|
||||
}
|
||||
}
|
||||
|
||||
function toggleUserMenu(e) {
|
||||
e.stopPropagation();
|
||||
const menu = document.getElementById('userMenu');
|
||||
if (!menu) return;
|
||||
const isOpen = menu.classList.contains('open');
|
||||
document.querySelectorAll('.dropdown-menu.open').forEach(function(m) {
|
||||
if (m !== menu) m.classList.remove('open');
|
||||
});
|
||||
if (!isOpen) {
|
||||
menu.classList.add('open');
|
||||
closeNotifications();
|
||||
setTimeout(function() {
|
||||
document.addEventListener('click', closeUserMenu);
|
||||
}, 0);
|
||||
} else {
|
||||
menu.classList.remove('open');
|
||||
}
|
||||
}
|
||||
|
||||
function closeUserMenu(e) {
|
||||
const menu = document.getElementById('userMenu');
|
||||
if (!menu) return;
|
||||
const wrapper = menu.closest('.topbar-user-dropdown');
|
||||
if (wrapper && !wrapper.contains(e.target)) {
|
||||
menu.classList.remove('open');
|
||||
document.removeEventListener('click', closeUserMenu);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,10 +96,7 @@ $router->post('/terminal/:id/execute', [TerminalController::class, 'execute'], [
|
||||
$router->get('/terminal/:id/history', [TerminalController::class, 'history'], [AuthMiddleware::class]);
|
||||
$router->post('/terminal/:id/clear-history', [TerminalController::class, 'clearHistory'], [AuthMiddleware::class, CSRFMiddleware::class]);
|
||||
|
||||
$router->get('/admin', function () {
|
||||
$view = \ServerManager\Core\App::getInstance()->getView();
|
||||
$view->redirect('/admin/audit');
|
||||
}, [AuthMiddleware::class, RoleMiddleware::class]);
|
||||
$router->get('/admin', [AdminController::class, 'index'], [AuthMiddleware::class, RoleMiddleware::class]);
|
||||
|
||||
$router->get('/admin/activity', function () {
|
||||
$view = \ServerManager\Core\App::getInstance()->getView();
|
||||
|
||||
@@ -37,6 +37,13 @@ class AdminController
|
||||
}
|
||||
}
|
||||
|
||||
public function index(): void
|
||||
{
|
||||
$this->view->display('admin.index', [
|
||||
'title' => 'Administration - ServerManager',
|
||||
]);
|
||||
}
|
||||
|
||||
public function users(): void
|
||||
{
|
||||
$this->requireSuperAdmin();
|
||||
|
||||
35
views/admin/index.php
Normal file
35
views/admin/index.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Administration</h1>
|
||||
<p class="page-subtitle">Manage system settings and configurations</p>
|
||||
</div>
|
||||
|
||||
<div class="action-grid">
|
||||
<a href="/teams" class="action-card">
|
||||
<i class="fas fa-users-cog"></i>
|
||||
<span>Teams</span>
|
||||
</a>
|
||||
<a href="/admin/users" class="action-card">
|
||||
<i class="fas fa-users"></i>
|
||||
<span>Users</span>
|
||||
</a>
|
||||
<a href="/admin/audit" class="action-card">
|
||||
<i class="fas fa-history"></i>
|
||||
<span>Audit Logs</span>
|
||||
</a>
|
||||
<a href="/admin/security" class="action-card">
|
||||
<i class="fas fa-shield-alt"></i>
|
||||
<span>Security</span>
|
||||
</a>
|
||||
<a href="/admin/app-version" class="action-card">
|
||||
<i class="fas fa-mobile-alt"></i>
|
||||
<span>App Version</span>
|
||||
</a>
|
||||
<a href="/admin/notifications" class="action-card">
|
||||
<i class="fas fa-bell"></i>
|
||||
<span>Notifications</span>
|
||||
</a>
|
||||
<a href="/admin/policies" class="action-card">
|
||||
<i class="fas fa-file-contract"></i>
|
||||
<span>Policies</span>
|
||||
</a>
|
||||
</div>
|
||||
@@ -44,12 +44,6 @@ if (!isset($user) || $user === null) {
|
||||
<span>Dashboard</span>
|
||||
</a>
|
||||
|
||||
<!-- Notifications -->
|
||||
<a href="/notifications" class="nav-item <?= str_starts_with($currentRoute, '/notifications') ? 'active' : '' ?>">
|
||||
<i class="fas fa-bell"></i>
|
||||
<span>Notifications</span>
|
||||
</a>
|
||||
|
||||
<!-- Servers -->
|
||||
<div class="nav-item-accordion <?= str_starts_with($currentRoute, '/servers') || str_starts_with($currentRoute, '/terminal') ? 'expanded' : '' ?>">
|
||||
<a href="#" class="nav-item nav-item-toggle <?= str_starts_with($currentRoute, '/servers') ? 'active' : '' ?>" onclick="toggleServerList(event)">
|
||||
@@ -67,39 +61,9 @@ if (!isset($user) || $user === null) {
|
||||
<hr class="nav-divider">
|
||||
<div class="nav-section">Administration</div>
|
||||
|
||||
<a href="/teams" class="nav-item <?= str_starts_with($currentRoute, '/teams') ? 'active' : '' ?>">
|
||||
<i class="fas fa-users-cog"></i>
|
||||
<span>Teams</span>
|
||||
</a>
|
||||
|
||||
<a href="/admin/users" class="nav-item <?= str_starts_with($currentRoute, '/admin/users') ? 'active' : '' ?>">
|
||||
<i class="fas fa-users"></i>
|
||||
<span>Users</span>
|
||||
</a>
|
||||
|
||||
<a href="/admin/audit" class="nav-item <?= str_starts_with($currentRoute, '/admin/audit') ? 'active' : '' ?>">
|
||||
<i class="fas fa-history"></i>
|
||||
<span>Audit Logs</span>
|
||||
</a>
|
||||
|
||||
<a href="/admin/security" class="nav-item <?= str_starts_with($currentRoute, '/admin/security') ? 'active' : '' ?>">
|
||||
<i class="fas fa-shield-alt"></i>
|
||||
<span>Security</span>
|
||||
</a>
|
||||
|
||||
<a href="/admin/app-version" class="nav-item <?= str_starts_with($currentRoute, '/admin/app-version') ? 'active' : '' ?>">
|
||||
<i class="fas fa-mobile-alt"></i>
|
||||
<span>App Version</span>
|
||||
</a>
|
||||
|
||||
<a href="/admin/notifications" class="nav-item <?= str_starts_with($currentRoute, '/admin/notifications') ? 'active' : '' ?>">
|
||||
<i class="fas fa-bell"></i>
|
||||
<span>Notifications</span>
|
||||
</a>
|
||||
|
||||
<a href="/admin/policies" class="nav-item <?= str_starts_with($currentRoute, '/admin/policies') ? 'active' : '' ?>">
|
||||
<i class="fas fa-file-contract"></i>
|
||||
<span>Policies</span>
|
||||
<a href="/admin" class="nav-item <?= str_starts_with($currentRoute, '/admin') ? 'active' : '' ?>">
|
||||
<i class="fas fa-cog"></i>
|
||||
<span>Administration</span>
|
||||
</a>
|
||||
<?php elseif (is_admin()): ?>
|
||||
<hr class="nav-divider">
|
||||
@@ -120,22 +84,7 @@ if (!isset($user) || $user === null) {
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<div class="sidebar-footer">
|
||||
<div class="user-info">
|
||||
<div class="user-avatar"><?= strtoupper(substr($user['username'] ?? '?', 0, 2)) ?></div>
|
||||
<div class="user-details">
|
||||
<span class="user-name"><?= htmlspecialchars($user['username'] ?? 'Guest') ?></span>
|
||||
<span class="user-role"><?= htmlspecialchars(ucfirst(str_replace('_', ' ', $user['role'] ?? 'guest'))) ?></span>
|
||||
</div>
|
||||
<div class="user-menu">
|
||||
<a href="/profile" title="Profile"><i class="fas fa-user-cog"></i></a>
|
||||
<form method="POST" action="/logout" class="logout-form" style="display:inline">
|
||||
<input type="hidden" name="_csrf_token" value="<?= $this->csrfToken() ?>">
|
||||
<button type="submit" title="Logout" style="background:none;border:none;color:inherit;cursor:pointer;padding:0;font:inherit;line-height:1"><i class="fas fa-sign-out-alt"></i></button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</aside>
|
||||
|
||||
<!-- Main Content -->
|
||||
@@ -171,10 +120,28 @@ if (!isset($user) || $user === null) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="topbar-user">
|
||||
<i class="fas fa-user-circle"></i>
|
||||
<?= htmlspecialchars($user['username'] ?? 'Guest') ?>
|
||||
</span>
|
||||
<div class="dropdown topbar-user-dropdown">
|
||||
<button class="topbar-user" onclick="toggleUserMenu(event)">
|
||||
<i class="fas fa-user-circle"></i>
|
||||
<?= htmlspecialchars($user['username'] ?? 'Guest') ?>
|
||||
<i class="fas fa-chevron-down" style="font-size:0.7rem;margin-left:4px"></i>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right" id="userMenu">
|
||||
<div class="dropdown-header">
|
||||
<strong><?= htmlspecialchars($user['username'] ?? 'Guest') ?></strong>
|
||||
<small><?= htmlspecialchars(ucfirst(str_replace('_', ' ', $user['role'] ?? 'guest'))) ?></small>
|
||||
</div>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a href="/profile" class="dropdown-item"><i class="fas fa-user-cog"></i> My Account</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<form method="POST" action="/logout" style="padding:0;margin:0">
|
||||
<input type="hidden" name="_csrf_token" value="<?= $this->csrfToken() ?>">
|
||||
<button type="submit" class="dropdown-item" style="width:100%;text-align:left;border:none;background:none;cursor:pointer;font:inherit">
|
||||
<i class="fas fa-sign-out-alt"></i> Log out
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user