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:
2026-06-14 09:43:43 -04:00
6 changed files with 168 additions and 82 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -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">
<div class="dropdown topbar-user-dropdown">
<button class="topbar-user" onclick="toggleUserMenu(event)">
<i class="fas fa-user-circle"></i>
<?= htmlspecialchars($user['username'] ?? 'Guest') ?>
</span>
<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>