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
324 lines
11 KiB
JavaScript
324 lines
11 KiB
JavaScript
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);
|
|
}
|
|
|
|
let pendingAgentAction = null;
|
|
|
|
function showAgentModal(action) {
|
|
pendingAgentAction = action;
|
|
const modal = document.getElementById('agentConfirmModal');
|
|
const title = document.getElementById('agentModalTitle');
|
|
const msg = document.getElementById('agentModalMessage');
|
|
const btn = document.getElementById('agentModalConfirmBtn');
|
|
|
|
if (action === 'install') {
|
|
title.textContent = 'Install Monitoring Agent';
|
|
msg.innerHTML = 'This will install the monitoring agent on <strong>serverName</strong> via SSH.';
|
|
btn.textContent = 'Install';
|
|
btn.className = 'btn btn-primary';
|
|
} else {
|
|
title.textContent = 'Uninstall Monitoring Agent';
|
|
msg.innerHTML = 'This will uninstall the monitoring agent from <strong>serverName</strong> via SSH. The service will be stopped and all agent files removed.';
|
|
btn.textContent = 'Uninstall';
|
|
btn.className = 'btn btn-danger';
|
|
}
|
|
|
|
modal.style.display = 'flex';
|
|
modal.onclick = function(e) {
|
|
if (e.target === modal) closeAgentModal();
|
|
};
|
|
}
|
|
|
|
function closeAgentModal() {
|
|
document.getElementById('agentConfirmModal').style.display = 'none';
|
|
pendingAgentAction = null;
|
|
}
|
|
|
|
function confirmAgentAction() {
|
|
if (!pendingAgentAction) return;
|
|
|
|
const action = pendingAgentAction;
|
|
const btn = document.getElementById('agentModalConfirmBtn');
|
|
btn.disabled = true;
|
|
btn.textContent = action === 'install' ? 'Installing...' : 'Uninstalling...';
|
|
|
|
closeAgentModal();
|
|
|
|
fetch('/servers/serverId/agent/' + 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 => {
|
|
if (data.success) {
|
|
showToast('success', action === 'install' ? 'Agent installed successfully.' : 'Agent uninstalled successfully.');
|
|
setTimeout(() => location.reload(), 1500);
|
|
} else {
|
|
let msg = data.error || (action === 'install' ? 'Installation failed.' : 'Uninstallation failed.');
|
|
if (data.output) msg += ' Output: ' + data.output.replace(/\n/g, ' | ');
|
|
showToast('error', msg);
|
|
}
|
|
})
|
|
.catch(err => {
|
|
showToast('error', 'Request failed: ' + err.message);
|
|
})
|
|
.finally(() => {
|
|
btn.disabled = false;
|
|
btn.textContent = action === 'install' ? 'Install' : 'Uninstall';
|
|
});
|
|
}
|
|
|
|
function copyAgentKey() {
|
|
const key = document.getElementById('agentKeyFull').value;
|
|
navigator.clipboard.writeText(key).then(() => {
|
|
showToast('success', 'Agent key copied to clipboard.');
|
|
}).catch(() => {
|
|
showToast('error', 'Failed to copy agent key.');
|
|
});
|
|
}
|
|
|
|
function regenerateAgentKey() {
|
|
if (!confirm('Regenerate agent key? The current key will stop working immediately.')) return;
|
|
|
|
fetch('/servers/serverId/agent/regenerate-key', {
|
|
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 => {
|
|
if (data.success) {
|
|
showToast('success', 'Agent key regenerated.');
|
|
setTimeout(() => location.reload(), 1500);
|
|
} else {
|
|
showToast('error', data.error || 'Failed to regenerate key.');
|
|
}
|
|
})
|
|
.catch(err => showToast('error', 'Request failed: ' + err.message));
|
|
}
|
|
|
|
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/serverId/' + 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/serverId/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/serverId/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);
|
|
|
|
function showThresholdModal() {
|
|
document.getElementById('thresholdModal').style.display = 'flex';
|
|
document.getElementById('thresholdModal').onclick = function(e) {
|
|
if (e.target === this) closeThresholdModal();
|
|
};
|
|
}
|
|
|
|
function closeThresholdModal() {
|
|
document.getElementById('thresholdModal').style.display = 'none';
|
|
}
|
|
|
|
function saveThresholds() {
|
|
const form = document.getElementById('thresholdForm');
|
|
const data = new URLSearchParams(new FormData(form));
|
|
data.append('_csrf_token', getCsrfToken());
|
|
|
|
document.querySelector('#thresholdModal .btn-primary').disabled = true;
|
|
|
|
fetch('/servers/serverId/thresholds', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
'X-CSRF-Token': getCsrfToken(),
|
|
},
|
|
body: data.toString(),
|
|
})
|
|
.then(r => r.json())
|
|
.then(d => {
|
|
showToast(d.success ? 'success' : 'error', d.message ?? d.error);
|
|
if (d.success) closeThresholdModal();
|
|
})
|
|
.catch(e => showToast('error', 'Request failed: ' + e.message))
|
|
.finally(() => {
|
|
document.querySelector('#thresholdModal .btn-primary').disabled = false;
|
|
});
|
|
}
|