Merge pull request 'fix: sync CSS with refactored layout, restore sidebar, add asset versioning, fix routes/auth' (#90) from fix/policy-container-width into master

Reviewed-on: #90
Reviewed-by: rafaga21 <rafaelminaya20@hotmail.com>
This commit was merged in pull request #90.
This commit is contained in:
2026-06-14 08:55:35 -04:00
31 changed files with 230 additions and 245 deletions

View File

@@ -9,6 +9,7 @@ return [
'debug' => filter_var($_ENV['APP_DEBUG'] ?? false, FILTER_VALIDATE_BOOLEAN), 'debug' => filter_var($_ENV['APP_DEBUG'] ?? false, FILTER_VALIDATE_BOOLEAN),
'url' => $_ENV['APP_URL'] ?? 'http://localhost', 'url' => $_ENV['APP_URL'] ?? 'http://localhost',
'timezone' => $_ENV['APP_TIMEZONE'] ?? 'America/Santo_Domingo', 'timezone' => $_ENV['APP_TIMEZONE'] ?? 'America/Santo_Domingo',
'asset_version' => $_ENV['ASSET_VERSION'] ?? '0.0.0',
], ],
'database' => [ 'database' => [

View File

@@ -121,7 +121,7 @@ code, pre {
} }
/* App Container */ /* App Container */
.app-container { .app-layout {
display: flex; display: flex;
min-height: 100vh; min-height: 100vh;
} }
@@ -159,7 +159,7 @@ code, pre {
min-height: var(--topbar-height); min-height: var(--topbar-height);
} }
.sidebar-logo { .sidebar-brand {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.75rem; gap: 0.75rem;
@@ -169,32 +169,13 @@ code, pre {
white-space: nowrap; white-space: nowrap;
} }
.sidebar-logo i { .sidebar-brand i {
font-size: 1.5rem; font-size: 1.5rem;
color: var(--accent); color: var(--accent);
} }
.sidebar-toggle { .nav-item i,
background: none; .nav-sub-item i {
border: none;
color: var(--text-secondary);
cursor: pointer;
font-size: 1.1rem;
padding: 0.5rem;
border-radius: var(--radius-sm);
transition: color var(--transition);
flex-shrink: 0;
}
.sidebar-toggle:hover {
color: var(--text-primary);
}
.sidebar-toggle i,
.mobile-toggle i,
.nav-link i,
.nav-sub-item i,
.user-menu a i {
pointer-events: none; pointer-events: none;
} }
@@ -204,61 +185,6 @@ code, pre {
padding: 0.75rem 0; padding: 0.75rem 0;
} }
.nav-list {
list-style: none;
}
.nav-item {
margin: 2px 0;
}
.nav-link {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.65rem 1.25rem;
color: var(--text-secondary);
border-radius: 0;
transition: all var(--transition);
font-size: 0.92rem;
white-space: nowrap;
border-left: 3px solid transparent;
}
.nav-link i {
width: 20px;
text-align: center;
flex-shrink: 0;
}
.nav-link:hover {
color: var(--text-primary);
background: var(--bg-hover);
}
.nav-link.active {
color: var(--accent);
background: var(--accent-light);
border-left-color: var(--accent);
font-weight: 500;
}
.nav-divider {
height: 1px;
background: var(--border-color);
margin: 0.5rem 1rem;
}
.nav-section {
padding: 0.5rem 1.25rem;
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text-muted);
font-weight: 600;
white-space: nowrap;
}
.sidebar-footer { .sidebar-footer {
border-top: 1px solid var(--border-color); border-top: 1px solid var(--border-color);
padding: 0.75rem 1rem; padding: 0.75rem 1rem;
@@ -321,21 +247,72 @@ code, pre {
color: var(--text-primary); color: var(--text-primary);
} }
.nav-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.65rem 1.25rem;
color: var(--text-secondary);
border-radius: 0;
transition: all var(--transition);
font-size: 0.92rem;
white-space: nowrap;
border-left: 3px solid transparent;
}
.nav-item i {
width: 20px;
text-align: center;
flex-shrink: 0;
}
.nav-item:hover {
color: var(--text-primary);
background: var(--bg-hover);
}
.nav-item.active {
color: var(--accent);
background: var(--accent-light);
border-left-color: var(--accent);
font-weight: 500;
}
.nav-divider {
height: 1px;
background: var(--border-color);
margin: 0.5rem 1rem;
}
.nav-section {
padding: 0.5rem 1.25rem;
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text-muted);
font-weight: 600;
white-space: nowrap;
}
/* Collapsed state */ /* Collapsed state */
.sidebar.collapsed .logo-text, .sidebar.collapsed .nav-item span,
.sidebar.collapsed .nav-link span, .sidebar.collapsed .nav-chevron,
.sidebar.collapsed .notification-badge,
.sidebar.collapsed .nav-divider,
.sidebar.collapsed .nav-section, .sidebar.collapsed .nav-section,
.sidebar.collapsed .user-details, .sidebar.collapsed .user-details,
.sidebar.collapsed .user-menu, .sidebar.collapsed .user-menu,
.sidebar.collapsed .nav-divider { .sidebar.collapsed .sidebar-footer {
display: none; display: none;
} }
.sidebar.collapsed .sidebar-logo { .sidebar.collapsed .sidebar-brand {
justify-content: center; justify-content: center;
} }
.sidebar.collapsed .nav-link { .sidebar.collapsed .nav-item {
justify-content: center; justify-content: center;
padding: 0.65rem; padding: 0.65rem;
} }
@@ -348,7 +325,7 @@ code, pre {
Main Content Main Content
========================================================================== */ ========================================================================== */
.main-content { .main-area {
flex: 1; flex: 1;
margin-left: var(--sidebar-width); margin-left: var(--sidebar-width);
transition: margin-left var(--transition); transition: margin-left var(--transition);
@@ -357,7 +334,7 @@ code, pre {
flex-direction: column; flex-direction: column;
} }
.sidebar.collapsed ~ .main-content { .sidebar.collapsed ~ .main-area {
margin-left: var(--sidebar-collapsed); margin-left: var(--sidebar-collapsed);
} }
@@ -365,7 +342,7 @@ code, pre {
Top Bar Top Bar
========================================================================== */ ========================================================================== */
.top-bar { .topbar {
height: var(--topbar-height); height: var(--topbar-height);
background: var(--bg-secondary); background: var(--bg-secondary);
border-bottom: 1px solid var(--border-color); border-bottom: 1px solid var(--border-color);
@@ -378,48 +355,25 @@ code, pre {
z-index: 50; z-index: 50;
} }
.top-bar-left { .topbar-left {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 1rem; gap: 1rem;
} }
.mobile-toggle { .topbar-right {
display: none;
background: none;
border: none;
color: var(--text-secondary);
font-size: 1.3rem;
cursor: pointer;
padding: 0.5rem 0.75rem;
border-radius: var(--radius-sm);
transition: color var(--transition);
}
.mobile-toggle:hover {
color: var(--text-primary);
background: var(--bg-hover);
}
.mobile-overlay {
display: none;
}
.breadcrumb {
font-size: 0.92rem;
color: var(--text-secondary);
font-weight: 500;
}
.top-bar-right {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.5rem;
} }
.top-bar-actions { .topbar-user {
display: flex; display: flex;
gap: 0.25rem; align-items: center;
gap: 0.5rem;
font-size: 0.88rem;
color: var(--text-secondary);
white-space: nowrap;
} }
.btn-icon { .btn-icon {
@@ -442,11 +396,15 @@ code, pre {
Content Area Content Area
========================================================================== */ ========================================================================== */
.content-area { .page-content {
flex: 1; flex: 1;
padding: 1.5rem; padding: 1.5rem;
} }
.container {
max-width: 100%;
}
/* ========================================================================== /* ==========================================================================
Page Header Page Header
========================================================================== */ ========================================================================== */
@@ -2340,20 +2298,22 @@ textarea.form-input {
width: 280px; width: 280px;
} }
.sidebar.mobile-open.collapsed .logo-text, .sidebar.mobile-open.collapsed .nav-item span,
.sidebar.mobile-open.collapsed .nav-link span, .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 .nav-section,
.sidebar.mobile-open.collapsed .user-details, .sidebar.mobile-open.collapsed .user-details,
.sidebar.mobile-open.collapsed .user-menu, .sidebar.mobile-open.collapsed .user-menu,
.sidebar.mobile-open.collapsed .nav-divider { .sidebar.mobile-open.collapsed .sidebar-footer {
display: flex; display: flex;
} }
.sidebar.mobile-open.collapsed .sidebar-logo { .sidebar.mobile-open.collapsed .sidebar-brand {
justify-content: flex-start; justify-content: flex-start;
} }
.sidebar.mobile-open.collapsed .nav-link { .sidebar.mobile-open.collapsed .nav-item {
justify-content: flex-start; justify-content: flex-start;
padding: 0.65rem 1.25rem; padding: 0.65rem 1.25rem;
} }
@@ -2362,31 +2322,15 @@ textarea.form-input {
justify-content: flex-start; justify-content: flex-start;
} }
.main-content { .main-area {
margin-left: 0 !important; margin-left: 0 !important;
} }
.mobile-toggle { .page-content {
display: block;
}
.mobile-overlay {
display: none;
position: fixed;
inset: 0;
background: rgba(0,0,0,0.5);
z-index: 99;
}
.mobile-overlay.active {
display: block;
}
.content-area {
padding: 1rem; padding: 1rem;
} }
.top-bar { .topbar {
padding: 0 1rem; padding: 0 1rem;
} }
@@ -2414,7 +2358,7 @@ textarea.form-input {
display: none; display: none;
} }
.nav-submenu { .nav-sublist {
background: rgba(0,0,0,0.1); background: rgba(0,0,0,0.1);
} }
} }
@@ -2481,11 +2425,11 @@ textarea.form-input {
========================================================================== */ ========================================================================== */
@media print { @media print {
.sidebar, .top-bar { .sidebar, .topbar {
display: none; display: none;
} }
.main-content { .main-area {
margin-left: 0; margin-left: 0;
} }
} }
@@ -2780,7 +2724,7 @@ textarea.form-input {
} }
/* Sidebar accordion */ /* Sidebar accordion */
.nav-item-accordion .nav-link { .nav-item-accordion .nav-item-toggle {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.5rem;
@@ -2797,20 +2741,13 @@ textarea.form-input {
transform: rotate(90deg); transform: rotate(90deg);
} }
.nav-submenu { .nav-sublist {
max-height: 0; max-height: 0;
overflow: hidden; overflow: hidden;
transition: max-height 0.25s ease; transition: max-height 0.25s ease;
background: rgba(0,0,0,0.15); background: rgba(0,0,0,0.15);
} }
.nav-submenu-loading {
padding: 0.5rem 1rem;
font-size: 0.78rem;
color: var(--text-muted);
text-align: center;
}
.nav-sub-item { .nav-sub-item {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -3330,7 +3267,7 @@ input[type="range"]::-moz-range-track {
display: block; display: block;
} }
.notif-dropdown-header { .notif-header {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
@@ -3338,11 +3275,9 @@ input[type="range"]::-moz-range-track {
border-bottom: 1px solid var(--border-color); border-bottom: 1px solid var(--border-color);
} }
.notif-dropdown-body { .notif-list {
max-height: 360px; max-height: 360px;
overflow-y: auto; overflow-y: auto;
} }
.notif-dropdown-footer {
border-top: 1px solid var(--border-color);
}

View File

@@ -12,14 +12,6 @@
const sidebar = document.getElementById('sidebar'); const sidebar = document.getElementById('sidebar');
const sidebarToggle = document.getElementById('sidebarToggle'); const sidebarToggle = document.getElementById('sidebarToggle');
const mobileToggle = document.getElementById('mobileToggle');
const mobileOverlay = document.getElementById('mobileOverlay');
function closeMobileSidebar() {
if (!sidebar) return;
sidebar.classList.remove('mobile-open');
if (mobileOverlay) mobileOverlay.classList.remove('active');
}
if (sidebarToggle && sidebar) { if (sidebarToggle && sidebar) {
sidebarToggle.addEventListener('click', function () { sidebarToggle.addEventListener('click', function () {
@@ -34,35 +26,6 @@
} catch (e) {} } catch (e) {}
} }
if (mobileToggle && sidebar) {
mobileToggle.addEventListener('click', function () {
const opening = !sidebar.classList.contains('mobile-open');
if (opening) {
sidebar.classList.remove('collapsed');
sidebar.classList.add('mobile-open');
if (mobileOverlay) mobileOverlay.classList.add('active');
} else {
closeMobileSidebar();
}
});
}
if (sidebar) {
document.addEventListener('click', function (e) {
if (window.innerWidth <= 768 &&
sidebar.classList.contains('mobile-open') &&
!sidebar.contains(e.target) &&
e.target !== mobileToggle &&
e.target !== mobileOverlay) {
closeMobileSidebar();
}
});
}
if (mobileOverlay) {
mobileOverlay.addEventListener('click', closeMobileSidebar);
}
/* ========================================================================== /* ==========================================================================
Theme Toggle Theme Toggle
========================================================================== */ ========================================================================== */

View File

@@ -31,6 +31,7 @@ $router->get('/login', [AuthController::class, 'loginForm']);
$router->post('/login', [AuthController::class, 'login'], [CSRFMiddleware::class, RateLimitMiddleware::class]); $router->post('/login', [AuthController::class, 'login'], [CSRFMiddleware::class, RateLimitMiddleware::class]);
$router->get('/register', [AuthController::class, 'registerForm']); $router->get('/register', [AuthController::class, 'registerForm']);
$router->post('/register', [AuthController::class, 'register'], [CSRFMiddleware::class]); $router->post('/register', [AuthController::class, 'register'], [CSRFMiddleware::class]);
$router->get('/logout', [AuthController::class, 'logout']);
$router->post('/logout', [AuthController::class, 'logout'], [CSRFMiddleware::class]); $router->post('/logout', [AuthController::class, 'logout'], [CSRFMiddleware::class]);
$router->get('/profile', [AuthController::class, 'profile'], [AuthMiddleware::class]); $router->get('/profile', [AuthController::class, 'profile'], [AuthMiddleware::class]);
@@ -72,6 +73,11 @@ $router->get('/servers/:id/metrics', [ServerController::class, 'metrics'], [Auth
$router->post('/servers/:id/thresholds', [ServerController::class, 'saveThresholds'], [AuthMiddleware::class, CSRFMiddleware::class]); $router->post('/servers/:id/thresholds', [ServerController::class, 'saveThresholds'], [AuthMiddleware::class, CSRFMiddleware::class]);
$router->get('/sidebar/servers', [ServerController::class, 'sidebarServers'], [AuthMiddleware::class]); $router->get('/sidebar/servers', [ServerController::class, 'sidebarServers'], [AuthMiddleware::class]);
$router->get('/account', function () {
$view = \ServerManager\Core\App::getInstance()->getView();
$view->redirect('/profile');
});
$router->get('/teams', [TeamController::class, 'index'], [AuthMiddleware::class, RoleMiddleware::class]); $router->get('/teams', [TeamController::class, 'index'], [AuthMiddleware::class, RoleMiddleware::class]);
$router->get('/teams/search-users', [TeamController::class, 'searchUsers'], [AuthMiddleware::class, RoleMiddleware::class]); $router->get('/teams/search-users', [TeamController::class, 'searchUsers'], [AuthMiddleware::class, RoleMiddleware::class]);
$router->get('/teams/create', [TeamController::class, 'create'], [AuthMiddleware::class, RoleMiddleware::class]); $router->get('/teams/create', [TeamController::class, 'create'], [AuthMiddleware::class, RoleMiddleware::class]);
@@ -90,6 +96,16 @@ $router->post('/terminal/:id/execute', [TerminalController::class, 'execute'], [
$router->get('/terminal/:id/history', [TerminalController::class, 'history'], [AuthMiddleware::class]); $router->get('/terminal/:id/history', [TerminalController::class, 'history'], [AuthMiddleware::class]);
$router->post('/terminal/:id/clear-history', [TerminalController::class, 'clearHistory'], [AuthMiddleware::class, CSRFMiddleware::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/activity', function () {
$view = \ServerManager\Core\App::getInstance()->getView();
$view->redirect('/admin/audit');
}, [AuthMiddleware::class, RoleMiddleware::class]);
$router->group(['prefix' => 'admin', 'middleware' => [AuthMiddleware::class, RoleMiddleware::class]], function (ServerManager\Core\Router $router) { $router->group(['prefix' => 'admin', 'middleware' => [AuthMiddleware::class, RoleMiddleware::class]], function (ServerManager\Core\Router $router) {
$router->get('/users', [AdminController::class, 'users']); $router->get('/users', [AdminController::class, 'users']);
$router->post('/users/create', [AdminController::class, 'createUser'], [CSRFMiddleware::class]); $router->post('/users/create', [AdminController::class, 'createUser'], [CSRFMiddleware::class]);

View File

@@ -400,6 +400,7 @@ class ApiController
'error' => 'Command execution failed.', 'error' => 'Command execution failed.',
], 500); ], 500);
} }
}
public function users(): void public function users(): void
{ {

View File

@@ -67,6 +67,7 @@ class AuthController
Session::set('username', $user['username']); Session::set('username', $user['username']);
Session::set('user_role', $user['role']); Session::set('user_role', $user['role']);
Session::set('user_email', $user['email']); Session::set('user_email', $user['email']);
Session::set('api_token', $user['api_token']);
Session::set('login_time', time()); Session::set('login_time', time());
$lifetime = App::getConfig()['session']['lifetime'] ?? 1440; $lifetime = App::getConfig()['session']['lifetime'] ?? 1440;

View File

@@ -47,6 +47,14 @@ if (!function_exists('asset')) {
} }
} }
if (!function_exists('asset_version')) {
function asset_version(): string
{
$config = \ServerManager\Core\App::getConfig();
return $config['app']['asset_version'] ?? '0.0.0';
}
}
if (!function_exists('url')) { if (!function_exists('url')) {
function url(string $path = ''): string function url(string $path = ''): string
{ {
@@ -90,11 +98,23 @@ if (!function_exists('auth')) {
return null; return null;
} }
$apiToken = \ServerManager\Core\Session::get('api_token');
if (empty($apiToken)) {
$userModel = new \ServerManager\Models\User();
$user = $userModel->findById((int) \ServerManager\Core\Session::get('user_id'));
if ($user && !empty($user['api_token'])) {
$apiToken = $user['api_token'];
\ServerManager\Core\Session::set('api_token', $apiToken);
}
}
return [ return [
'id' => \ServerManager\Core\Session::get('user_id'), 'id' => \ServerManager\Core\Session::get('user_id'),
'username' => \ServerManager\Core\Session::get('username'), 'username' => \ServerManager\Core\Session::get('username'),
'role' => \ServerManager\Core\Session::get('user_role'), 'role' => \ServerManager\Core\Session::get('user_role'),
'email' => \ServerManager\Core\Session::get('user_email'), 'email' => \ServerManager\Core\Session::get('user_email'),
'api_token' => $apiToken,
]; ];
} }
} }

View File

@@ -187,4 +187,4 @@ function selected(string $value, string $compare): string {
</div> </div>
</div> </div>
<script src="/assets/js/audit-log.js"></script> <script src="/assets/js/audit-log.js?v=<?= asset_version() ?>"></script>

View File

@@ -139,4 +139,4 @@ $data = $notifications['data'] ?? [];
</div> </div>
</div> </div>
<script src="/assets/js/admin-notifications.js"></script> <script src="/assets/js/admin-notifications.js?v=<?= asset_version() ?>"></script>

View File

@@ -102,4 +102,4 @@
</div> </div>
</div> </div>
<script src="/assets/js/admin-security.js"></script> <script src="/assets/js/admin-security.js?v=<?= asset_version() ?>"></script>

View File

@@ -121,4 +121,4 @@
</div> </div>
</div> </div>
<script src="/assets/js/admin-users.js"></script> <script src="/assets/js/admin-users.js?v=<?= asset_version() ?>"></script>

View File

@@ -165,5 +165,5 @@ $aggregatedMetrics = $aggregatedMetrics ?? [];
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
<script>const aggregatedData = <?= json_encode($aggregatedMetrics, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES) ?>;</script> <script>const aggregatedData = <?= json_encode($aggregatedMetrics, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES) ?>;</script>
<script src="/assets/js/dashboard-chart.js"></script> <script src="/assets/js/dashboard-chart.js?v=<?= asset_version() ?>"></script>
</div> </div>

View File

@@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= htmlspecialchars($title ?? 'ServerManager') ?></title> <title><?= htmlspecialchars($title ?? 'ServerManager') ?></title>
<link rel="icon" type="image/svg+xml" href="/assets/img/server.svg"> <link rel="icon" type="image/svg+xml" href="/assets/img/server.svg">
<link rel="stylesheet" href="/assets/css/style.css"> <link rel="stylesheet" href="/assets/css/style.css?v=<?= asset_version() ?>">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
</head> </head>
<body class="auth-page"> <body class="auth-page">
@@ -13,6 +13,6 @@
<?= $content ?? '' ?> <?= $content ?? '' ?>
</div> </div>
<div id="toastContainer" class="toast-container"></div> <div id="toastContainer" class="toast-container"></div>
<script src="/assets/js/app.js"></script> <script src="/assets/js/app.js?v=<?= asset_version() ?>"></script>
</body> </body>
</html> </html>

View File

@@ -1,5 +1,4 @@
<?php <?php
$user = $user ?? null;
$notifications = $notifications ?? []; $notifications = $notifications ?? [];
$unreadCount = $unreadCount ?? 0; $unreadCount = $unreadCount ?? 0;
$teamId = $teamId ?? null; $teamId = $teamId ?? null;
@@ -7,6 +6,10 @@ $title = $title ?? 'ServerManager';
$content = $content ?? ''; $content = $content ?? '';
$currentRoute = $_SERVER['REQUEST_URI'] ?? '/'; $currentRoute = $_SERVER['REQUEST_URI'] ?? '/';
if (!isset($user) || $user === null) {
$user = auth();
}
?> ?>
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
@@ -16,7 +19,7 @@ $currentRoute = $_SERVER['REQUEST_URI'] ?? '/';
<title><?= htmlspecialchars($title) ?> — ServerManager</title> <title><?= htmlspecialchars($title) ?> — ServerManager</title>
<meta name="api-token" content="<?= htmlspecialchars($user['api_token'] ?? '') ?>"> <meta name="api-token" content="<?= htmlspecialchars($user['api_token'] ?? '') ?>">
<meta name="csrf-token" content="<?= $this->csrfToken() ?>"> <meta name="csrf-token" content="<?= $this->csrfToken() ?>">
<link rel="stylesheet" href="/assets/css/style.css"> <link rel="stylesheet" href="/assets/css/style.css?v=<?= asset_version() ?>">
<link rel="icon" type="image/svg+xml" href="/assets/img/server.svg"> <link rel="icon" type="image/svg+xml" href="/assets/img/server.svg">
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
@@ -41,6 +44,12 @@ $currentRoute = $_SERVER['REQUEST_URI'] ?? '/';
<span>Dashboard</span> <span>Dashboard</span>
</a> </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 --> <!-- Servers -->
<div class="nav-item-accordion <?= str_starts_with($currentRoute, '/servers') || str_starts_with($currentRoute, '/terminal') ? 'expanded' : '' ?>"> <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)"> <a href="#" class="nav-item nav-item-toggle <?= str_starts_with($currentRoute, '/servers') ? 'active' : '' ?>" onclick="toggleServerList(event)">
@@ -53,41 +62,80 @@ $currentRoute = $_SERVER['REQUEST_URI'] ?? '/';
</div> </div>
</div> </div>
<!-- Teams --> <!-- Administration -->
<?php if (is_super_admin()): ?>
<hr class="nav-divider">
<div class="nav-section">Administration</div>
<a href="/teams" class="nav-item <?= str_starts_with($currentRoute, '/teams') ? 'active' : '' ?>"> <a href="/teams" class="nav-item <?= str_starts_with($currentRoute, '/teams') ? 'active' : '' ?>">
<i class="fas fa-users"></i> <i class="fas fa-users-cog"></i>
<span>Teams</span> <span>Teams</span>
</a> </a>
<!-- Activity Log (admin+) --> <a href="/admin/users" class="nav-item <?= str_starts_with($currentRoute, '/admin/users') ? 'active' : '' ?>">
<?php if (is_admin() || is_super_admin()): ?> <i class="fas fa-users"></i>
<a href="/admin/activity" class="nav-item <?= str_starts_with($currentRoute, '/admin/activity') ? 'active' : '' ?>"> <span>Users</span>
</a>
<a href="/admin/audit" class="nav-item <?= str_starts_with($currentRoute, '/admin/audit') ? 'active' : '' ?>">
<i class="fas fa-history"></i> <i class="fas fa-history"></i>
<span>Activity Log</span> <span>Audit Logs</span>
</a> </a>
<?php endif; ?>
<!-- Admin panel (admin+) --> <a href="/admin/security" class="nav-item <?= str_starts_with($currentRoute, '/admin/security') ? 'active' : '' ?>">
<?php if (is_admin() || is_super_admin()): ?> <i class="fas fa-shield-alt"></i>
<a href="/admin" class="nav-item <?= str_starts_with($currentRoute, '/admin') ? 'active' : '' ?>"> <span>Security</span>
<i class="fas fa-cog"></i>
<span>Administration</span>
</a> </a>
<?php endif; ?>
<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>
<?php elseif (is_admin()): ?>
<hr class="nav-divider"> <hr class="nav-divider">
<div class="nav-section">Administration</div>
<!-- Account --> <a href="/teams" class="nav-item <?= str_starts_with($currentRoute, '/teams') ? 'active' : '' ?>">
<a href="/account" class="nav-item <?= str_starts_with($currentRoute, '/account') ? 'active' : '' ?>"> <i class="fas fa-users-cog"></i>
<i class="fas fa-user"></i> <span>Teams</span>
<span>My Account</span>
</a> </a>
<?php endif; ?>
<a href="/logout" class="nav-item nav-item-danger"> <!-- Mobile -->
<i class="fas fa-sign-out-alt"></i> <hr class="nav-divider">
<span>Log out</span> <div class="nav-section">Mobile</div>
<a href="/sysadmin.apk" class="nav-item" download>
<i class="fas fa-download"></i>
<span>Download APK</span>
</a> </a>
</nav> </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> </aside>
<!-- Main Content --> <!-- Main Content -->
@@ -153,12 +201,12 @@ $currentRoute = $_SERVER['REQUEST_URI'] ?? '/';
<div id="toastContainer" class="toast-container"></div> <div id="toastContainer" class="toast-container"></div>
<script src="/assets/js/app.js"></script> <script src="/assets/js/app.js?v=<?= asset_version() ?>"></script>
<?php if (isset($scripts)): ?> <?php if (isset($scripts)): ?>
<?php foreach ($scripts as $script): ?> <?php foreach ($scripts as $script): ?>
<script src="<?= htmlspecialchars($script) ?>"></script> <script src="<?= htmlspecialchars($script) ?>"></script>
<?php endforeach; ?> <?php endforeach; ?>
<?php endif; ?> <?php endif; ?>
<script src="/assets/js/main.js"></script> <script src="/assets/js/main.js?v=<?= asset_version() ?>"></script>
</body> </body>
</html> </html>

View File

@@ -67,4 +67,4 @@ $data = $notifications ?? [];
</div> </div>
<?php endif; ?> <?php endif; ?>
<script src="/assets/js/notifications.js"></script> <script src="/assets/js/notifications.js?v=<?= asset_version() ?>"></script>

View File

@@ -1,4 +1,4 @@
<?php ?><link rel="stylesheet" href="/assets/css/landing.css"> <?php ?><link rel="stylesheet" href="/assets/css/landing.css?v=<?= asset_version() ?>">
<div class="landing-page"> <div class="landing-page">
<section class="landing-hero"> <section class="landing-hero">

View File

@@ -1,4 +1,4 @@
<?php ?><link rel="stylesheet" href="/assets/css/legal.css"> <?php ?><link rel="stylesheet" href="/assets/css/legal.css?v=<?= asset_version() ?>">
<div class="legal-page"> <div class="legal-page">
<div class="legal-container"> <div class="legal-container">
<div class="legal-card" style="width:85%;margin:3rem auto;padding:3rem 3.5rem;background:var(--card-bg);border-radius:12px;box-shadow:0 2px 16px rgba(0,0,0,0.18)"> <div class="legal-card" style="width:85%;margin:3rem auto;padding:3rem 3.5rem;background:var(--card-bg);border-radius:12px;box-shadow:0 2px 16px rgba(0,0,0,0.18)">

View File

@@ -1,4 +1,4 @@
<?php ?><link rel="stylesheet" href="/assets/css/legal.css"> <?php ?><link rel="stylesheet" href="/assets/css/legal.css?v=<?= asset_version() ?>">
<div class="legal-page"> <div class="legal-page">
<div class="legal-container"> <div class="legal-container">
<div class="legal-card" style="width:85%;margin:3rem auto;padding:3rem 3.5rem;background:var(--card-bg);border-radius:12px;box-shadow:0 2px 16px rgba(0,0,0,0.18)"> <div class="legal-card" style="width:85%;margin:3rem auto;padding:3rem 3.5rem;background:var(--card-bg);border-radius:12px;box-shadow:0 2px 16px rgba(0,0,0,0.18)">

View File

@@ -171,4 +171,4 @@ document.querySelectorAll('.auth-method-tabs .tab').forEach(tab => {
let permIndex = <?= !empty($savedPerms) && is_array($savedPerms) ? count($savedPerms) : 0 ?>; let permIndex = <?= !empty($savedPerms) && is_array($savedPerms) ? count($savedPerms) : 0 ?>;
</script> </script>
<script src="/assets/js/server-permissions.js"></script> <script src="/assets/js/server-permissions.js?v=<?= asset_version() ?>"></script>

View File

@@ -95,5 +95,5 @@ $status = $db['status'] ?? 'inactive';
const serverId = <?= json_encode($server['id']) ?>; const serverId = <?= json_encode($server['id']) ?>;
const databases = <?= json_encode(array_column($databases, 'name')) ?>; const databases = <?= json_encode(array_column($databases, 'name')) ?>;
</script> </script>
<script src="/assets/js/database-manager.js"></script> <script src="/assets/js/database-manager.js?v=<?= asset_version() ?>"></script>
<?php endif; ?> <?php endif; ?>

View File

@@ -156,4 +156,4 @@ $hasKey = !empty($server['ssh_key']);
<script> <script>
let permIndex = <?= count($permissions) ?>; let permIndex = <?= count($permissions) ?>;
</script> </script>
<script src="/assets/js/server-permissions.js"></script> <script src="/assets/js/server-permissions.js?v=<?= asset_version() ?>"></script>

View File

@@ -141,4 +141,4 @@ $groups = $groups ?? [];
</div> </div>
</div> </div>
<script src="/assets/js/server-list.js"></script> <script src="/assets/js/server-list.js?v=<?= asset_version() ?>"></script>

View File

@@ -323,5 +323,5 @@ $configValid = $nginx['config_valid'] ?? false;
<script> <script>
const serverId = <?= json_encode($server['id']) ?>; const serverId = <?= json_encode($server['id']) ?>;
</script> </script>
<script src="/assets/js/nginx-manager.js"></script> <script src="/assets/js/nginx-manager.js?v=<?= asset_version() ?>"></script>
<?php endif; ?> <?php endif; ?>

View File

@@ -103,4 +103,4 @@ $processList = $processList ?? [];
<script> <script>
const serverId = <?= $server['id'] ?>; const serverId = <?= $server['id'] ?>;
</script> </script>
<script src="/assets/js/server-processes.js"></script> <script src="/assets/js/server-processes.js?v=<?= asset_version() ?>"></script>

View File

@@ -51,5 +51,5 @@ const allServices = <?= json_encode($services, JSON_HEX_TAG | JSON_UNESCAPED_SLA
const perPage = <?= $perPage ?>; const perPage = <?= $perPage ?>;
const serverId = <?= $server['id'] ?>; const serverId = <?= $server['id'] ?>;
</script> </script>
<script src="/assets/js/server-services.js"></script> <script src="/assets/js/server-services.js?v=<?= asset_version() ?>"></script>

View File

@@ -375,4 +375,4 @@ $canManage = $server_role === 'owner' || $server_role === 'manager';
const serverId = <?= $server['id'] ?>; const serverId = <?= $server['id'] ?>;
const serverName = '<?= addslashes($server['name'] ?? '') ?>'; const serverName = '<?= addslashes($server['name'] ?? '') ?>';
</script> </script>
<script src="/assets/js/server-show.js"></script> <script src="/assets/js/server-show.js?v=<?= asset_version() ?>"></script>

View File

@@ -191,4 +191,4 @@ $certs = $ssl['certificates'] ?? [];
<script> <script>
const serverId = <?= $server['id'] ?>; const serverId = <?= $server['id'] ?>;
</script> </script>
<script src="/assets/js/server-ssl.js"></script> <script src="/assets/js/server-ssl.js?v=<?= asset_version() ?>"></script>

View File

@@ -123,4 +123,4 @@ $lastLogins = $sysusers['last_logins'] ?? '';
<script> <script>
const serverId = <?= $server['id'] ?>; const serverId = <?= $server['id'] ?>;
</script> </script>
<script src="/assets/js/system-users.js"></script> <script src="/assets/js/system-users.js?v=<?= asset_version() ?>"></script>

View File

@@ -71,4 +71,4 @@
<input type="hidden" name="_csrf_token" value="<?= $this->csrfToken() ?>"> <input type="hidden" name="_csrf_token" value="<?= $this->csrfToken() ?>">
</form> </form>
<script src="/assets/js/team-index.js"></script> <script src="/assets/js/team-index.js?v=<?= asset_version() ?>"></script>

View File

@@ -150,4 +150,4 @@ const teamId = <?= $team['id'] ?>;
const teamName = '<?= htmlspecialchars(addslashes($team['name'] ?? '')) ?>'; const teamName = '<?= htmlspecialchars(addslashes($team['name'] ?? '')) ?>';
const teamDescription = '<?= htmlspecialchars(addslashes($team['description'] ?? '')) ?>'; const teamDescription = '<?= htmlspecialchars(addslashes($team['description'] ?? '')) ?>';
</script> </script>
<script src="/assets/js/team-show.js"></script> <script src="/assets/js/team-show.js?v=<?= asset_version() ?>"></script>

View File

@@ -150,4 +150,4 @@ $commandHistory = $commandHistory ?? [];
<script> <script>
const serverId = <?= $server['id'] ?>; const serverId = <?= $server['id'] ?>;
</script> </script>
<script src="/assets/js/terminal.js"></script> <script src="/assets/js/terminal.js?v=<?= asset_version() ?>"></script>