Files
server-manager/public/assets/js/app.js
Agent 88f3c1f964 refactor: extract inline CSS/JS from views into separate asset files (20+ files)
Phase A — HTML removed from PHP files:
- RateLimitMiddleware.php: heredoc moved to views/errors/429.php
- public/index.php: inline HTML replaced with View::error()
- AuthMiddleware.php: <script> redirect replaced with <meta refresh>

Phase B — Inline CSS/JS extracted from views:
- CSS: landing.css (459 lines), legal.css (shared by terms+privacy)
- JS: 17 new files extracted from ~20 view files
  - main.js (layout sidebar + notifications)
  - dashboard-chart.js, server-show.js, server-services.js
  - nginx-manager.js, database-manager.js
  - terminal.js, team-show.js, team-index.js
  - server-list.js, server-permissions.js (shared by edit+create)
  - server-ssl.js, server-processes.js, system-users.js
  - admin-users.js, audit-log.js, admin-notifications.js, admin-security.js
  - notifications.js

Total: -3264 lines from views, +288 lines in new assets
2026-06-14 08:04:01 -04:00

300 lines
11 KiB
JavaScript

/**
* ServerManager - Main Application JavaScript
* ES6+ Modules and utilities
*/
(function () {
'use strict';
/* ==========================================================================
Sidebar Toggle
========================================================================== */
const sidebar = document.getElementById('sidebar');
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) {
sidebarToggle.addEventListener('click', function () {
const isCollapsed = sidebar.classList.toggle('collapsed');
try { localStorage.setItem('sidebar_collapsed', isCollapsed ? '1' : '0'); } catch (e) {}
});
try {
if (window.innerWidth > 768 && localStorage.getItem('sidebar_collapsed') === '1') {
sidebar.classList.add('collapsed');
}
} 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
========================================================================== */
const themeToggle = document.getElementById('themeToggle');
if (themeToggle) {
const savedTheme = localStorage.getItem('theme') || 'dark';
document.documentElement.setAttribute('data-theme', savedTheme);
updateThemeIcon(savedTheme);
themeToggle.addEventListener('click', function () {
const current = document.documentElement.getAttribute('data-theme');
const next = current === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', next);
localStorage.setItem('theme', next);
updateThemeIcon(next);
});
}
function updateThemeIcon(theme) {
if (!themeToggle) return;
const icon = themeToggle.querySelector('i');
if (icon) {
icon.className = theme === 'dark' ? 'fas fa-sun' : 'fas fa-moon';
}
}
/* ==========================================================================
Toast Notifications
========================================================================== */
window.showToast = function (type, message) {
const container = document.getElementById('toastContainer');
if (!container) return;
const toast = document.createElement('div');
toast.className = 'toast toast-' + type;
toast.innerHTML = `
<div class="toast-body">
<i class="fas fa-${type === 'success' ? 'check-circle' :
type === 'error' ? 'times-circle' :
type === 'warning' ? 'exclamation-triangle' : 'info-circle'}"></i>
${escapeHtml(message)}
</div>
<button class="toast-close">&times;</button>
`;
container.appendChild(toast);
const closeBtn = toast.querySelector('.toast-close');
closeBtn.addEventListener('click', function () {
toast.remove();
});
setTimeout(() => {
if (toast.parentNode) {
toast.style.opacity = '0';
toast.style.transition = 'opacity 0.3s ease';
setTimeout(() => toast.remove(), 300);
}
}, 5000);
};
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
/* ==========================================================================
Auto-dismiss Toasts
========================================================================== */
document.addEventListener('DOMContentLoaded', function () {
document.querySelectorAll('.toast[data-autohide="true"]').forEach(function (toast) {
setTimeout(() => {
toast.style.opacity = '0';
toast.style.transition = 'opacity 0.3s ease';
setTimeout(() => toast.remove(), 300);
}, 5000);
});
});
/* ==========================================================================
Password Toggle
========================================================================== */
document.addEventListener('click', function (e) {
const toggle = e.target.closest('.input-toggle-password');
if (!toggle) return;
const input = toggle.parentElement.querySelector('input');
const icon = toggle.querySelector('i');
if (input) {
const isPassword = input.type === 'password';
input.type = isPassword ? 'text' : 'password';
if (icon) {
icon.className = isPassword ? 'fas fa-eye-slash' : 'fas fa-eye';
}
}
});
/* ==========================================================================
Dropdowns
========================================================================== */
document.addEventListener('click', function (e) {
const toggle = e.target.closest('.dropdown-toggle');
if (toggle) {
const dropdown = toggle.closest('.dropdown');
const wasActive = dropdown.classList.contains('active');
document.querySelectorAll('.dropdown.active').forEach(d => {
d.classList.remove('active');
});
if (!wasActive) {
dropdown.classList.add('active');
}
e.preventDefault();
return;
}
if (!e.target.closest('.dropdown')) {
document.querySelectorAll('.dropdown.active').forEach(d => {
d.classList.remove('active');
});
}
});
/* ==========================================================================
Dashboard Auto-Refresh
========================================================================== */
const refreshBtn = document.getElementById('refreshBtn');
let autoRefreshInterval = null;
const AUTO_REFRESH_MS = 30000;
if (refreshBtn) {
refreshBtn.addEventListener('click', function () {
if (autoRefreshInterval) {
clearInterval(autoRefreshInterval);
autoRefreshInterval = null;
refreshBtn.classList.remove('spinning');
showToast('info', 'Auto-refresh disabled');
} else {
autoRefreshInterval = setInterval(refreshDashboardStats, AUTO_REFRESH_MS);
refreshBtn.classList.add('spinning');
showToast('info', 'Auto-refresh enabled (30s)');
refreshDashboardStats();
}
});
}
function refreshDashboardStats() {
fetch('/dashboard/stats')
.then(r => r.json())
.then(data => {
if (data.success && data.data) {
const stats = data.data;
const el = (id) => document.getElementById(id);
if (el('statTotalServers')) el('statTotalServers').textContent = stats.total_servers;
if (el('statOnlineServers')) el('statOnlineServers').textContent = stats.online_servers;
if (el('statOfflineServers')) el('statOfflineServers').textContent = stats.offline_servers;
if (el('statAvgCpu')) el('statAvgCpu').textContent = stats.avg_cpu + '%';
if (el('statAvgRam')) el('statAvgRam').textContent = stats.avg_ram + '%';
if (el('statAvgDisk')) el('statAvgDisk').textContent = stats.avg_disk + '%';
}
})
.catch(() => {});
}
/* ==========================================================================
API Request Helper
========================================================================== */
window.api = {
get: function (url) {
return fetch(url, {
headers: {
'Accept': 'application/json',
'Authorization': 'Bearer ' + (localStorage.getItem('api_token') || ''),
},
}).then(r => r.json());
},
post: function (url, data) {
return fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': 'Bearer ' + (localStorage.getItem('api_token') || ''),
},
body: JSON.stringify(data),
}).then(r => r.json());
},
put: function (url, data) {
return fetch(url, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': 'Bearer ' + (localStorage.getItem('api_token') || ''),
},
body: JSON.stringify(data),
}).then(r => r.json());
},
delete: function (url) {
return fetch(url, {
method: 'DELETE',
headers: {
'Accept': 'application/json',
'Authorization': 'Bearer ' + (localStorage.getItem('api_token') || ''),
},
}).then(r => r.json());
},
};
/* ==========================================================================
Console Info
========================================================================== */
console.log(
'%c ServerManager %c v1.0.0 ',
'background:#6366f1;color:white;padding:4px 8px;border-radius:4px 0 0 4px;font-weight:bold;',
'background:#1a1d2b;color:#9ca3af;padding:4px 8px;border-radius:0 4px 4px 0;'
);
console.log('%cLinux Server Management Platform', 'color:#6b7280;font-style:italic;');
})();