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
This commit is contained in:
40
public/assets/js/admin-notifications.js
Normal file
40
public/assets/js/admin-notifications.js
Normal file
@@ -0,0 +1,40 @@
|
||||
function showSendModal() {
|
||||
document.getElementById('notificationModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
document.getElementById('notificationModal').style.display = 'none';
|
||||
}
|
||||
|
||||
function sendNotification() {
|
||||
const form = document.getElementById('notificationForm');
|
||||
const formData = new FormData(form);
|
||||
const btn = document.querySelector('#notificationModal .btn-primary');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Sending...';
|
||||
|
||||
fetch('/admin/notifications/send', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]')?.content || '',
|
||||
},
|
||||
body: new URLSearchParams(formData),
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showToast('success', data.message);
|
||||
setTimeout(() => location.reload(), 1000);
|
||||
} else {
|
||||
showToast('error', data.message);
|
||||
}
|
||||
closeModal();
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="fas fa-paper-plane"></i> Send';
|
||||
})
|
||||
.catch(() => {
|
||||
showToast('error', 'Failed to send notification');
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="fas fa-paper-plane"></i> Send';
|
||||
});
|
||||
}
|
||||
17
public/assets/js/admin-security.js
Normal file
17
public/assets/js/admin-security.js
Normal file
@@ -0,0 +1,17 @@
|
||||
function generateKey() {
|
||||
if (!confirm('WARNING: Generating a new master key will make all existing encrypted credentials UNREADABLE. ' +
|
||||
'You will need to re-enter all SSH credentials. Continue?')) return;
|
||||
|
||||
fetch('/admin/security/generate-key', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]')?.content || '',
|
||||
},
|
||||
body: '_csrf_token=' + encodeURIComponent(document.querySelector('meta[name="csrf-token"]')?.content || ''),
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
showToast(data.success ? 'success' : 'error', data.message);
|
||||
});
|
||||
}
|
||||
76
public/assets/js/admin-users.js
Normal file
76
public/assets/js/admin-users.js
Normal file
@@ -0,0 +1,76 @@
|
||||
function showCreateUserModal() {
|
||||
document.getElementById('modalTitle').textContent = 'Create User';
|
||||
document.getElementById('userId').value = '';
|
||||
document.getElementById('modalUsername').value = '';
|
||||
document.getElementById('modalEmail').value = '';
|
||||
document.getElementById('modalPassword').value = '';
|
||||
document.getElementById('modalPassword').required = true;
|
||||
document.getElementById('passwordHint').style.display = 'block';
|
||||
document.getElementById('modalRole').value = 'admin';
|
||||
document.getElementById('modalActive').checked = true;
|
||||
document.getElementById('saveUserBtn').textContent = 'Create';
|
||||
document.getElementById('userModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
function editUser(id, username, email, role, isActive) {
|
||||
document.getElementById('modalTitle').textContent = 'Edit User';
|
||||
document.getElementById('userId').value = id;
|
||||
document.getElementById('modalUsername').value = username;
|
||||
document.getElementById('modalEmail').value = email;
|
||||
document.getElementById('modalPassword').value = '';
|
||||
document.getElementById('modalPassword').required = false;
|
||||
document.getElementById('passwordHint').style.display = 'none';
|
||||
document.getElementById('modalRole').value = role;
|
||||
document.getElementById('modalActive').checked = !!isActive;
|
||||
document.getElementById('saveUserBtn').textContent = 'Update';
|
||||
document.getElementById('userModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
document.getElementById('userModal').style.display = 'none';
|
||||
}
|
||||
|
||||
function saveUser() {
|
||||
const id = document.getElementById('userId').value;
|
||||
const formData = new FormData(document.getElementById('userForm'));
|
||||
|
||||
const isEdit = !!id;
|
||||
const url = isEdit ? '/admin/users/' + id + '/update' : '/admin/users/create';
|
||||
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]')?.content || '',
|
||||
},
|
||||
body: new URLSearchParams(formData),
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showToast('success', data.message);
|
||||
setTimeout(() => location.reload(), 1000);
|
||||
} else {
|
||||
showToast('error', data.message);
|
||||
}
|
||||
closeModal();
|
||||
})
|
||||
.catch(err => showToast('error', 'Failed to save user'));
|
||||
}
|
||||
|
||||
function deleteUser(id, username) {
|
||||
if (!confirm('Are you sure you want to delete user "' + username + '"?')) return;
|
||||
|
||||
fetch('/admin/users/' + id + '/delete', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]')?.content || '',
|
||||
},
|
||||
body: '_csrf_token=' + encodeURIComponent(document.querySelector('meta[name="csrf-token"]')?.content || ''),
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) location.reload();
|
||||
else showToast('error', data.message);
|
||||
});
|
||||
}
|
||||
0
public/assets/js/app.js
Executable file → Normal file
0
public/assets/js/app.js
Executable file → Normal file
40
public/assets/js/audit-log.js
Normal file
40
public/assets/js/audit-log.js
Normal file
@@ -0,0 +1,40 @@
|
||||
function buildQuery() {
|
||||
const params = [];
|
||||
const fields = [
|
||||
{ key: 'action', id: 'f_action' },
|
||||
{ key: 'username', id: 'f_username' },
|
||||
{ key: 'ip_address', id: 'f_ip_address' },
|
||||
{ key: 'entity_type', id: 'f_entity_type' },
|
||||
{ key: 'details', id: 'f_details' },
|
||||
{ key: 'date_from', id: 'f_date_from' },
|
||||
{ key: 'date_to', id: 'f_date_to' },
|
||||
];
|
||||
fields.forEach(function (f) {
|
||||
const el = document.getElementById(f.id);
|
||||
if (el && el.value) {
|
||||
params.push(encodeURIComponent(f.key) + '=' + encodeURIComponent(el.value));
|
||||
}
|
||||
});
|
||||
return params.length ? '/admin/audit?' + params.join('&') : '/admin/audit';
|
||||
}
|
||||
|
||||
document.addEventListener('change', function (e) {
|
||||
if (e.target.matches('#f_date_from, #f_date_to, #f_action, #f_entity_type')) {
|
||||
window.location.href = buildQuery();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Enter' && e.target.matches('#f_username, #f_details, #f_ip_address')) {
|
||||
window.location.href = buildQuery();
|
||||
}
|
||||
});
|
||||
|
||||
function clearFilters() {
|
||||
window.location.href = '/admin/audit';
|
||||
}
|
||||
|
||||
function clearField(id) {
|
||||
document.getElementById(id).value = '';
|
||||
window.location.href = buildQuery();
|
||||
}
|
||||
136
public/assets/js/dashboard-chart.js
Normal file
136
public/assets/js/dashboard-chart.js
Normal file
@@ -0,0 +1,136 @@
|
||||
let aggregatedChart = null;
|
||||
|
||||
function initAggregatedChart(data) {
|
||||
const container = document.getElementById('aggregatedChart').parentNode;
|
||||
const oldCanvas = document.getElementById('aggregatedChart');
|
||||
const newCanvas = document.createElement('canvas');
|
||||
newCanvas.id = 'aggregatedChart';
|
||||
newCanvas.height = 300;
|
||||
container.replaceChild(newCanvas, oldCanvas);
|
||||
const ctx = newCanvas.getContext('2d');
|
||||
const labels = data.map(m => {
|
||||
const d = new Date(m.time_bucket);
|
||||
return d.getHours().toString().padStart(2, '0') + ':' + d.getMinutes().toString().padStart(2, '0');
|
||||
});
|
||||
|
||||
aggregatedChart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [
|
||||
{
|
||||
label: 'CPU',
|
||||
data: data.map(m => parseFloat(m.avg_cpu)),
|
||||
borderColor: '#3b82f6',
|
||||
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
||||
fill: true,
|
||||
tension: 0.3,
|
||||
pointRadius: 1,
|
||||
},
|
||||
{
|
||||
label: 'RAM',
|
||||
data: data.map(m => parseFloat(m.avg_ram)),
|
||||
borderColor: '#22c55e',
|
||||
backgroundColor: 'rgba(34, 197, 94, 0.1)',
|
||||
fill: true,
|
||||
tension: 0.3,
|
||||
pointRadius: 1,
|
||||
},
|
||||
{
|
||||
label: 'Disk',
|
||||
data: data.map(m => parseFloat(m.avg_disk)),
|
||||
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)',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function refreshAggregatedChart() {
|
||||
fetch('/dashboard/chart-data')
|
||||
.then(r => r.json())
|
||||
.then(response => {
|
||||
if (response.success && response.data) {
|
||||
const data = response.data;
|
||||
const labels = data.map(m => {
|
||||
const d = new Date(m.time_bucket);
|
||||
return d.getHours().toString().padStart(2, '0') + ':' + d.getMinutes().toString().padStart(2, '0');
|
||||
});
|
||||
if (aggregatedChart) {
|
||||
aggregatedChart.data.labels = labels;
|
||||
aggregatedChart.data.datasets[0].data = data.map(m => parseFloat(m.avg_cpu));
|
||||
aggregatedChart.data.datasets[1].data = data.map(m => parseFloat(m.avg_ram));
|
||||
aggregatedChart.data.datasets[2].data = data.map(m => parseFloat(m.avg_disk));
|
||||
aggregatedChart.update('none');
|
||||
} else if (data.length > 0) {
|
||||
initAggregatedChart(data);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (aggregatedData.length > 0) {
|
||||
initAggregatedChart(aggregatedData);
|
||||
}
|
||||
|
||||
setInterval(refreshAggregatedChart, 30000);
|
||||
316
public/assets/js/database-manager.js
Normal file
316
public/assets/js/database-manager.js
Normal file
@@ -0,0 +1,316 @@
|
||||
let currentDb = '';
|
||||
let currentTable = '';
|
||||
let openedDb = '';
|
||||
|
||||
function getCsrfToken() { return document.querySelector('meta[name="csrf-token"]')?.content || ''; }
|
||||
|
||||
function filterTables(inp) {
|
||||
const q = inp.value.toLowerCase();
|
||||
const body = inp.closest('.pma-accordion-body');
|
||||
if (body) {
|
||||
body.querySelectorAll('.pma-table-item').forEach(el => {
|
||||
el.style.display = !q || (el.dataset.tbl || '').includes(q) ? '' : 'none';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function filterSidebar(q) {
|
||||
document.querySelectorAll('.pma-accordion-item').forEach(el => {
|
||||
el.style.display = el.textContent.toLowerCase().includes(q.toLowerCase()) ? '' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
function toggleDb(db) {
|
||||
const item = document.querySelector(`.pma-accordion-item[data-db="${db.replace(/"/g,'\\"')}"]`);
|
||||
if (!item) return;
|
||||
const body = item.querySelector('.pma-accordion-body');
|
||||
const header = item.querySelector('.pma-accordion-header');
|
||||
const chevron = item.querySelector('.pma-chevron');
|
||||
|
||||
if (item.classList.contains('expanded')) {
|
||||
item.classList.remove('expanded');
|
||||
body.style.maxHeight = '0';
|
||||
return;
|
||||
}
|
||||
|
||||
document.querySelectorAll('.pma-accordion-item.expanded').forEach(el => {
|
||||
el.classList.remove('expanded');
|
||||
el.querySelector('.pma-accordion-body').style.maxHeight = '0';
|
||||
});
|
||||
|
||||
item.classList.add('expanded');
|
||||
currentDb = db;
|
||||
|
||||
if (body.querySelector('.pma-table-item')) {
|
||||
body.style.maxHeight = body.scrollHeight + 'px';
|
||||
return;
|
||||
}
|
||||
|
||||
body.querySelector('.pma-loading-tables').style.display = '';
|
||||
body.style.maxHeight = '60px';
|
||||
|
||||
fetch('/servers/' + serverId + '/database', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type':'application/x-www-form-urlencoded','X-CSRF-Token':getCsrfToken()},
|
||||
body: 'action=tables&db=' + encodeURIComponent(db) + '&_csrf_token=' + encodeURIComponent(getCsrfToken()),
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
body.querySelector('.pma-loading-tables').style.display = 'none';
|
||||
let html = '<div style="padding:0.3rem 0.5rem"><input type="text" class="form-input tbl-filter" placeholder="Filter tables..." style="font-size:0.75rem;padding:0.25rem 0.4rem;width:100%;box-sizing:border-box" oninput="filterTables(this)"></div>';
|
||||
(data.tables || []).forEach(t => {
|
||||
html += '<div class="pma-table-item" data-tbl="' + escHtml(t.name).toLowerCase() + '" onclick="selectTable(\'' + db.replace(/'/g,"\\'") + "','" + t.name.replace(/'/g,"\\'") + '\')">'
|
||||
+ '<i class="fas fa-table"></i> ' + escHtml(t.name)
|
||||
+ ' <span class="pma-db-size">' + t.rows + ' rows</span></div>';
|
||||
});
|
||||
if (!html) html = '<div class="pma-table-item" style="color:var(--text-muted);cursor:default">No tables</div>';
|
||||
body.innerHTML = html;
|
||||
body.style.maxHeight = body.scrollHeight + 'px';
|
||||
});
|
||||
}
|
||||
|
||||
function selectTable(db, table) {
|
||||
currentDb = db; currentTable = table;
|
||||
document.getElementById('tabBrowse').style.display = '';
|
||||
document.getElementById('tabStructure').style.display = '';
|
||||
document.querySelectorAll('.pma-tab').forEach(t=>t.classList.remove('active'));
|
||||
document.getElementById('tabStructure').classList.add('active');
|
||||
showPanel('structure');
|
||||
loadStructure(db, table);
|
||||
}
|
||||
|
||||
function loadStructure(db, table) {
|
||||
const panel = document.getElementById('pmaStructure');
|
||||
panel.innerHTML = '<div style="text-align:center;padding:2rem;color:var(--text-muted)"><i class="fas fa-spinner fa-spin"></i> Loading structure...</div>';
|
||||
fetch('/servers/' + serverId + '/database', {
|
||||
method:'POST', headers:{'Content-Type':'application/x-www-form-urlencoded','X-CSRF-Token':getCsrfToken()},
|
||||
body:'action=structure&db='+encodeURIComponent(db)+'&table='+encodeURIComponent(table)+'&_csrf_token='+encodeURIComponent(getCsrfToken()),
|
||||
}).then(r=>r.json()).then(d=>{
|
||||
if (!d.success||!d.columns) return;
|
||||
let html='<div style="display:flex;gap:0.75rem;align-items:center;margin-bottom:1rem">'
|
||||
+'<h2 style="margin:0;font-size:1rem"><i class="fas fa-list"></i> <code>'+escHtml(table)+'</code></h2>'
|
||||
+'<span class="badge badge-info">'+d.columns.length+' columns</span>'
|
||||
+'<span style="flex:1"></span>'
|
||||
+'<button class="btn btn-xs btn-info" onclick="browseTable(\''+db.replace(/'/g,"\\'")+"','"+table.replace(/'/g,"\\'")+'\')"><i class="fas fa-search"></i> Browse</button>'
|
||||
+'</div>';
|
||||
html+='<div class="table-responsive"><table class="table table-sm"><thead><tr><th>#</th><th>Field</th><th>Type</th><th>Collation</th><th>Null</th><th>Key</th><th>Default</th><th>Extra</th></tr></thead><tbody>';
|
||||
d.columns.forEach((c,i)=>{
|
||||
html+='<tr><td>'+(i+1)+'</td><td><strong>'+escHtml(c.field)+'</strong></td><td><code>'+escHtml(c.type)+'</code></td>'
|
||||
+'<td>'+escHtml(c.collation)+'</td><td>'+escHtml(c.null)+'</td><td><strong>'+escHtml(c.key)+'</strong></td>'
|
||||
+'<td>'+(c.default??'NULL')+'</td><td>'+escHtml(c.extra)+'</td></tr>';
|
||||
});
|
||||
html+='</tbody></table></div>';
|
||||
panel.innerHTML=html;
|
||||
});
|
||||
}
|
||||
|
||||
function browseTable(db, table) {
|
||||
currentDb=db; currentTable=table;
|
||||
document.getElementById('tabBrowse').style.display='';
|
||||
document.getElementById('tabStructure').style.display='';
|
||||
document.querySelectorAll('.pma-tab').forEach(t=>t.classList.remove('active'));
|
||||
document.getElementById('tabBrowse').classList.add('active');
|
||||
showPanel('browse');
|
||||
loadBrowse(db,table,1);
|
||||
}
|
||||
|
||||
function loadBrowse(db,table,page) {
|
||||
const panel=document.getElementById('pmaBrowse');
|
||||
|
||||
const inputs=document.querySelectorAll('.pma-fi');
|
||||
let filters='';
|
||||
if(inputs.length){
|
||||
const p={};
|
||||
inputs.forEach(inp=>{if(inp.value.trim())p[inp.dataset.col]=inp.value.trim()});
|
||||
const s=new URLSearchParams(p).toString();
|
||||
if(s)filters='&'+s;
|
||||
}
|
||||
|
||||
panel.innerHTML='<div style="text-align:center;padding:2rem;color:var(--text-muted)"><i class="fas fa-spinner fa-spin"></i> Loading...</div>';
|
||||
|
||||
let body='action=browse&db='+encodeURIComponent(db)+'&table='+encodeURIComponent(table)+'&page='+page+filters+'&_csrf_token='+encodeURIComponent(getCsrfToken());
|
||||
|
||||
fetch('/servers/' + serverId + '/database',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded','X-CSRF-Token':getCsrfToken()},body})
|
||||
.then(r=>r.json()).then(d=>{
|
||||
if(!d.success)return;
|
||||
const tp=Math.ceil(d.total/d.per_page);
|
||||
let html='<div style="display:flex;gap:0.75rem;align-items:center;margin-bottom:0.5rem">'
|
||||
+'<h2 style="margin:0;font-size:1rem"><i class="fas fa-search"></i> <code>'+escHtml(table)+'</code></h2>'
|
||||
+'<span class="badge badge-info">'+d.total+' rows</span></div>';
|
||||
html+='<form onsubmit="event.preventDefault();loadBrowse(\''+db.replace(/'/g,"\\'")+"','"+table.replace(/'/g,"\\'")+'\',1)">';
|
||||
html+='<div class="table-responsive" style="max-height:450px;overflow-y:auto"><table class="table table-sm"><thead><tr>';
|
||||
d.columns.forEach(c=>{html+='<th>'+escHtml(c)+'</th>'});
|
||||
html+='</tr><tr class="pma-filter-row">';
|
||||
d.columns.forEach(c=>{
|
||||
const cv=new URLSearchParams(filters.replace(/^&/,'')).get(c)||'';
|
||||
html+='<td><input type="text" class="form-input pma-fi" data-col="'+escHtml(c)+'" value="'+escHtml(cv)+'" placeholder="'+escHtml(c)+'" style="font-size:0.75rem;padding:0.2rem 0.35rem;width:100%;min-width:50px"></td>';
|
||||
});
|
||||
html+='</tr></thead><tbody>';
|
||||
d.rows.forEach(row=>{
|
||||
html+='<tr>';
|
||||
row.forEach(val=>{html+='<td style="max-width:180px;overflow:hidden;text-overflow:ellipsis"><code>'+escHtml(String(val).substring(0,80))+'</code></td>'});
|
||||
html+='</tr>';
|
||||
});
|
||||
html+='</tbody></table></div>';
|
||||
html+='<div style="display:flex;gap:0.5rem;margin-top:0.5rem;align-items:center">';
|
||||
html+='<button type="submit" class="btn btn-primary btn-sm"><i class="fas fa-search"></i> Filter</button>';
|
||||
html+='<button type="button" class="btn btn-secondary btn-sm" onclick="clearBrowseFilters(\''+db.replace(/'/g,"\\'")+"','"+table.replace(/'/g,"\\'")+'\')"><i class="fas fa-undo"></i> Clear</button>';
|
||||
if(tp>1){
|
||||
html+='<span style="flex:1"></span>';
|
||||
for(let p=1;p<=tp&&p<=10;p++)html+='<button type="button" class="btn btn-xs '+(p===page?'btn-primary':'btn-secondary')+'" onclick="loadBrowse(\''+db.replace(/'/g,"\\'")+"','"+table.replace(/'/g,"\\'")+'\','+p+')">'+p+'</button>';
|
||||
if(tp>10)html+='<span class="text-muted">...</span>';
|
||||
}
|
||||
html+='</div></form>';
|
||||
panel.innerHTML=html;
|
||||
});
|
||||
}
|
||||
|
||||
function clearBrowseFilters(db,table){
|
||||
document.querySelectorAll('.pma-fi').forEach(inp=>inp.value='');
|
||||
loadBrowse(db,table,1);
|
||||
}
|
||||
|
||||
function loadUsers() {
|
||||
showPanel('users');
|
||||
document.getElementById('tabUsers').style.display='';
|
||||
const panel=document.getElementById('pmaUsers');
|
||||
panel.innerHTML='<div style="text-align:center;padding:2rem;color:var(--text-muted)"><i class="fas fa-spinner fa-spin"></i> Loading users...</div>';
|
||||
|
||||
fetch('/servers/' + serverId + '/database',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded','X-CSRF-Token':getCsrfToken()},body:'action=users&_csrf_token='+encodeURIComponent(getCsrfToken())})
|
||||
.then(r=>r.json()).then(d=>{
|
||||
let html='<div style="display:flex;gap:0.75rem;align-items:center;margin-bottom:1rem">'
|
||||
+'<h2 style="margin:0;font-size:1rem"><i class="fas fa-users"></i> Database Users</h2>'
|
||||
+'<span class="badge badge-info">'+(d.users?.length||0)+' users</span>'
|
||||
+'<span style="flex:1"></span>'
|
||||
+'<button class="btn btn-sm btn-success" onclick="showCreateUser()"><i class="fas fa-plus"></i> New User</button></div>';
|
||||
html+='<div class="table-responsive"><table class="table table-sm"><thead><tr><th>User</th><th>Host</th><th>Databases</th><th>Expired</th><th>Actions</th></tr></thead><tbody>';
|
||||
(d.users||[]).forEach(u=>{
|
||||
const dbList=(d.grants||[]).filter(g=>g.user===u.user&&g.host===u.host).map(g=>g.db).join(', ');
|
||||
html+='<tr><td><strong>'+escHtml(u.user)+'</strong></td><td><code>'+escHtml(u.host)+'</code></td><td>'+(dbList||'<span class="text-muted">-</span>')+'</td><td>'+escHtml(u.expired)+'</td>'
|
||||
+'<td class="actions-cell">'
|
||||
+'<button class="btn btn-xs btn-secondary" onclick="showEditUser(\''+escHtml(u.user)+"','"+escHtml(u.host)+'\')" title="Change password"><i class="fas fa-key"></i></button> '
|
||||
+'<button class="btn btn-xs btn-danger" onclick="deleteUser(\''+escHtml(u.user)+"','"+escHtml(u.host)+'\')" title="Delete"><i class="fas fa-trash"></i></button>'
|
||||
+'</td></tr>';
|
||||
});
|
||||
html+='</tbody></table></div>';
|
||||
panel.innerHTML=html;
|
||||
});
|
||||
}
|
||||
|
||||
function showCreateUser() {
|
||||
const panel=document.getElementById('pmaUsers');
|
||||
let html='<div style="display:flex;gap:0.75rem;align-items:center;margin-bottom:1rem">'
|
||||
+'<h2 style="margin:0;font-size:1rem"><i class="fas fa-user-plus"></i> Create User</h2>'
|
||||
+'<span style="flex:1"></span>'
|
||||
+'<button class="btn btn-sm btn-secondary" onclick="loadUsers()"><i class="fas fa-arrow-left"></i> Back</button></div>'
|
||||
+'<div class="dashboard-grid"><div class="dashboard-card"><div class="card-header"><h2><i class="fas fa-user"></i> Account</h2></div><div class="card-body">'
|
||||
+'<div class="form-row"><div class="form-group"><label>Username</label><input type="text" id="nuUser" class="form-input" placeholder="username"></div>'
|
||||
+'<div class="form-group"><label>Password</label><input type="password" id="nuPass" class="form-input" placeholder="password"></div>'
|
||||
+'<div class="form-group"><label>Host</label><input type="text" id="nuHost" class="form-input" value="localhost" placeholder="localhost"></div></div></div></div>'
|
||||
+'<div class="dashboard-card"><div class="card-header"><h2><i class="fas fa-database"></i> Database Access</h2></div><div class="card-body">'
|
||||
+'<div style="margin-bottom:0.75rem"><label class="checkbox-label"><input type="checkbox" id="nuAllDb" onchange="document.querySelectorAll(\'.nuDbCheck\').forEach(c=>c.checked=this.checked)"> <strong>Select all</strong></label></div>';
|
||||
|
||||
databases.forEach(function(dbname) {
|
||||
if (dbname !== 'information_schema' && dbname !== 'performance_schema' && dbname !== 'mysql' && dbname !== 'sys') {
|
||||
html+='<label class="checkbox-label" style="margin-top:0.25rem"><input type="checkbox" class="nuDbCheck" value="'+dbname+'"> <span>'+dbname+'</span></label>';
|
||||
}
|
||||
});
|
||||
|
||||
html+='</div></div></div>'
|
||||
+'<div class="dashboard-card"><div class="card-header"><h2><i class="fas fa-lock"></i> Privileges</h2></div><div class="card-body">'
|
||||
+'<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:0.25rem">';
|
||||
['ALL PRIVILEGES','SELECT','INSERT','UPDATE','DELETE','CREATE','DROP','ALTER','INDEX','CREATE VIEW','SHOW VIEW','CREATE ROUTINE','ALTER ROUTINE','EXECUTE','LOCK TABLES','REFERENCES','EVENT','TRIGGER'].forEach(function(p){
|
||||
var checked = p === 'ALL PRIVILEGES' ? ' checked' : '';
|
||||
html+='<label class="checkbox-label" style="font-size:0.82rem"><input type="checkbox" class="nuPriv" value="'+p+'"'+checked+'> <span>'+p+'</span></label>';
|
||||
});
|
||||
html+='</div></div></div>'
|
||||
+'<div style="margin-top:1rem;display:flex;gap:0.5rem">'
|
||||
+'<button class="btn btn-primary" onclick="doCreateUser()"><i class="fas fa-save"></i> Create User</button>'
|
||||
+'<button class="btn btn-secondary" onclick="loadUsers()">Cancel</button></div>';
|
||||
panel.innerHTML=html;
|
||||
}
|
||||
|
||||
function doCreateUser() {
|
||||
const user=document.getElementById('nuUser').value;
|
||||
const pass=document.getElementById('nuPass').value;
|
||||
const host=document.getElementById('nuHost').value||'localhost';
|
||||
if(!user||!pass){showToast('error','Username and password required');return;}
|
||||
|
||||
const privs=[];
|
||||
document.querySelectorAll('.nuPriv:checked').forEach(cb=>privs.push(cb.value));
|
||||
const grantPrivs = privs.includes('ALL PRIVILEGES') ? 'ALL PRIVILEGES' : privs.join(',');
|
||||
|
||||
fetch('/servers/' + serverId + '/database',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded','X-CSRF-Token':getCsrfToken()},body: 'action=create_user&new_user='+encodeURIComponent(user)+'&new_pass='+encodeURIComponent(pass)+'&new_host='+encodeURIComponent(host)+'&_csrf_token='+encodeURIComponent(getCsrfToken())})
|
||||
.then(r=>r.json()).then(d=>{
|
||||
showToast(d.success?'success':'error',d.message);
|
||||
if(d.success){
|
||||
const dbs=[];
|
||||
document.querySelectorAll('.nuDbCheck:checked').forEach(cb=>dbs.push(cb.value));
|
||||
if(dbs.length>0){
|
||||
let cnt=0;
|
||||
dbs.forEach(db=>{
|
||||
fetch('/servers/' + serverId + '/database',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded','X-CSRF-Token':getCsrfToken()},body:'action=grant&grant_user='+encodeURIComponent(user)+'&grant_db='+encodeURIComponent(db)+'&grant_host='+encodeURIComponent(host)+'&grant_privs='+encodeURIComponent(grantPrivs)+'&_csrf_token='+encodeURIComponent(getCsrfToken())})
|
||||
.then(r2=>r2.json()).then(g=>{
|
||||
cnt++;
|
||||
if(cnt===dbs.length)loadUsers();
|
||||
});
|
||||
});
|
||||
}else{loadUsers();}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function showEditUser(user, host) {
|
||||
const panel = document.getElementById('pmaUsers');
|
||||
panel.innerHTML='<div style="display:flex;gap:0.75rem;align-items:center;margin-bottom:1rem">'
|
||||
+'<h2 style="margin:0;font-size:1rem"><i class="fas fa-key"></i> Change Password</h2>'
|
||||
+'<code>'+escHtml(user)+'</code> @ <code>'+escHtml(host)+'</code>'
|
||||
+'<span style="flex:1"></span>'
|
||||
+'<button class="btn btn-sm btn-secondary" onclick="loadUsers()"><i class="fas fa-arrow-left"></i> Back</button></div>'
|
||||
+'<div class="card"><div class="card-body">'
|
||||
+'<div class="form-group"><label>New Password</label><input type="password" id="epPass" class="form-input" placeholder="new password"></div>'
|
||||
+'<div style="margin-top:1rem;display:flex;gap:0.5rem">'
|
||||
+'<button class="btn btn-primary" onclick="doChangePass(\''+escHtml(user)+"','"+escHtml(host)+'\')"><i class="fas fa-save"></i> Save</button>'
|
||||
+'<button class="btn btn-secondary" onclick="loadUsers()">Cancel</button></div></div></div>';
|
||||
}
|
||||
|
||||
function doChangePass(user, host) {
|
||||
const pass = document.getElementById('epPass').value;
|
||||
if (!pass) { showToast('error','Password required'); return; }
|
||||
fetch('/servers/' + serverId + '/database',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded','X-CSRF-Token':getCsrfToken()},
|
||||
body:'action=change_pass&target_user='+encodeURIComponent(user)+'&target_host='+encodeURIComponent(host)+'&new_pass='+encodeURIComponent(pass)+'&_csrf_token='+encodeURIComponent(getCsrfToken())})
|
||||
.then(r=>r.json()).then(d=>{showToast(d.success?'success':'error',d.message);if(d.success)loadUsers()});
|
||||
}
|
||||
|
||||
function deleteUser(user, host) {
|
||||
if (!confirm('Delete user \''+user+'\'@\''+host+'\'?\nThis cannot be undone.')) return;
|
||||
fetch('/servers/' + serverId + '/database',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded','X-CSRF-Token':getCsrfToken()},
|
||||
body:'action=drop_user&target_user='+encodeURIComponent(user)+'&target_host='+encodeURIComponent(host)+'&_csrf_token='+encodeURIComponent(getCsrfToken())})
|
||||
.then(r=>r.json()).then(d=>{showToast(d.success?'success':'error',d.message);if(d.success)loadUsers()});
|
||||
}
|
||||
|
||||
function runSql() {
|
||||
const sql=document.getElementById('sqlInput').value; if(!sql)return;
|
||||
const rd=document.getElementById('sqlResult'); const rt=document.getElementById('sqlResultText');
|
||||
rd.style.display='none';
|
||||
fetch('/servers/' + serverId + '/database',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded','X-CSRF-Token':getCsrfToken()},body:'action=query&query='+encodeURIComponent(sql)+'&_csrf_token='+encodeURIComponent(getCsrfToken())})
|
||||
.then(r=>r.json()).then(d=>{if(d.output){rt.textContent=d.output;rd.style.display='block'}else showToast('error',d.message||'No results')});
|
||||
}
|
||||
|
||||
function showPanel(id) {
|
||||
document.querySelectorAll('.pma-panel').forEach(p=>p.classList.remove('active'));
|
||||
document.getElementById('pma'+id.charAt(0).toUpperCase()+id.slice(1)).classList.add('active');
|
||||
}
|
||||
|
||||
function switchTab(tab) {
|
||||
document.querySelectorAll('.pma-tab').forEach(t=>t.classList.remove('active'));
|
||||
const map={browse:'pmaBrowse',structure:'pmaStructure',sql:'pmaSql',users:'pmaUsers'};
|
||||
const el=document.getElementById('tab'+tab.charAt(0).toUpperCase()+tab.slice(1));
|
||||
if(el)el.classList.add('active');
|
||||
showPanel(tab);
|
||||
if(tab==='sql'){document.getElementById('pmaWelcome').classList.remove('active');document.getElementById('pmaSql').classList.add('active');}
|
||||
}
|
||||
|
||||
document.getElementById('sqlInput').addEventListener('keydown',function(e){if((e.ctrlKey||e.metaKey)&&e.key==='Enter')runSql()});
|
||||
|
||||
function escHtml(s){const d=document.createElement('div');d.textContent=s;return d.innerHTML;}
|
||||
202
public/assets/js/main.js
Normal file
202
public/assets/js/main.js
Normal file
@@ -0,0 +1,202 @@
|
||||
let serverListLoaded = false;
|
||||
|
||||
function getApiToken() {
|
||||
return document.querySelector('meta[name="api-token"]')?.getAttribute('content') || '';
|
||||
}
|
||||
|
||||
function toggleNotifications() {
|
||||
const dd = document.getElementById('notifDropdown');
|
||||
if (dd.classList.contains('open')) {
|
||||
closeNotifications();
|
||||
} else {
|
||||
openNotifications();
|
||||
}
|
||||
}
|
||||
|
||||
function openNotifications() {
|
||||
const dd = document.getElementById('notifDropdown');
|
||||
dd.classList.add('open');
|
||||
fetchNotifications();
|
||||
document.addEventListener('click', notifOutsideClick);
|
||||
}
|
||||
|
||||
function closeNotifications() {
|
||||
const dd = document.getElementById('notifDropdown');
|
||||
dd.classList.remove('open');
|
||||
document.removeEventListener('click', notifOutsideClick);
|
||||
}
|
||||
|
||||
function notifOutsideClick(e) {
|
||||
const wrapper = document.querySelector('.notification-btn-wrapper');
|
||||
if (!wrapper.contains(e.target)) {
|
||||
closeNotifications();
|
||||
}
|
||||
}
|
||||
|
||||
function fetchNotifications() {
|
||||
const list = document.getElementById('notifList');
|
||||
list.innerHTML = '<div style="padding:40px 20px;text-align:center;color:var(--text-muted)"><i class="fas fa-spinner fa-spin" style="font-size:1.5em;margin-bottom:10px;display:block"></i><span>Loading notifications...</span></div>';
|
||||
|
||||
const token = getApiToken();
|
||||
const headers = { 'Accept': 'application/json' };
|
||||
if (token) headers['Authorization'] = 'Bearer ' + token;
|
||||
|
||||
fetch('/api/notifications?per_page=10', { headers })
|
||||
.then(r => r.json())
|
||||
.then(d => {
|
||||
if (!d.success) {
|
||||
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);
|
||||
})
|
||||
.catch(() => {
|
||||
list.innerHTML = '<div style="padding:40px 20px;text-align:center;color:var(--danger)">Failed to load notifications.</div>';
|
||||
});
|
||||
}
|
||||
|
||||
function renderNotifications(items, unreadCount) {
|
||||
const list = document.getElementById('notifList');
|
||||
|
||||
if (!items.length) {
|
||||
list.innerHTML = '<div style="padding:40px 20px;text-align:center;color:var(--text-muted)">'
|
||||
+ '<i class="fas fa-check-circle" style="font-size:1.8em;margin-bottom:10px;display:block;color:var(--success)"></i>'
|
||||
+ '<span>No notifications</span></div>';
|
||||
document.getElementById('markAllReadBtn').style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '';
|
||||
items.forEach(function(n) {
|
||||
const typeColors = { error: 'danger', warning: 'warning', info: 'info' };
|
||||
const typeColor = typeColors[n.type] || 'info';
|
||||
const icons = { error: 'fa-times-circle', warning: 'fa-exclamation-triangle', info: 'fa-info-circle' };
|
||||
const icon = icons[n.type] || 'fa-info-circle';
|
||||
|
||||
html += '<div class="notif-item ' + (n.is_read ? 'notif-read' : 'notif-unread') + '"'
|
||||
+ ' onclick="markRead(' + n.id + ', this)"'
|
||||
+ ' data-id="' + n.id + '">'
|
||||
+ '<div class="notif-icon notif-' + typeColor + '"><i class="fas ' + icon + '"></i></div>'
|
||||
+ '<div class="notif-content">'
|
||||
+ '<div class="notif-title">' + escapeHtml(n.title) + '</div>'
|
||||
+ '<div class="notif-message">' + escapeHtml(n.message) + '</div>'
|
||||
+ '<div class="notif-time">' + (n.time_ago || '') + '</div>'
|
||||
+ '</div>'
|
||||
+ (n.is_read ? '' : '<div class="notif-dot"></div>')
|
||||
+ '</div>';
|
||||
});
|
||||
list.innerHTML = html;
|
||||
|
||||
document.getElementById('markAllReadBtn').style.display = unreadCount > 0 ? 'inline-flex' : 'none';
|
||||
}
|
||||
|
||||
function markRead(id, el) {
|
||||
if (el.classList.contains('notif-read')) return;
|
||||
|
||||
const token = getApiToken();
|
||||
const headers = { 'Accept': 'application/json' };
|
||||
if (token) headers['Authorization'] = 'Bearer ' + token;
|
||||
|
||||
fetch('/api/notifications/' + id + '/read', { method: 'POST', headers })
|
||||
.then(r => r.json())
|
||||
.then(d => {
|
||||
if (d.success) {
|
||||
el.classList.remove('notif-unread');
|
||||
el.classList.add('notif-read');
|
||||
const dot = el.querySelector('.notif-dot');
|
||||
if (dot) dot.remove();
|
||||
updateUnreadCount();
|
||||
}
|
||||
})
|
||||
.catch(function(){});
|
||||
}
|
||||
|
||||
function markAllRead() {
|
||||
const token = getApiToken();
|
||||
const headers = { 'Accept': 'application/json' };
|
||||
if (token) headers['Authorization'] = 'Bearer ' + token;
|
||||
|
||||
fetch('/api/notifications/read-all', { method: 'POST', headers })
|
||||
.then(r => r.json())
|
||||
.then(d => {
|
||||
if (d.success) {
|
||||
document.querySelectorAll('.notif-item').forEach(function(el) {
|
||||
el.classList.remove('notif-unread');
|
||||
el.classList.add('notif-read');
|
||||
const dot = el.querySelector('.notif-dot');
|
||||
if (dot) dot.remove();
|
||||
});
|
||||
document.getElementById('markAllReadBtn').style.display = 'none';
|
||||
updateUnreadCount();
|
||||
}
|
||||
})
|
||||
.catch(function(){});
|
||||
}
|
||||
|
||||
function updateUnreadCount() {
|
||||
const token = getApiToken();
|
||||
const headers = { 'Accept': 'application/json' };
|
||||
if (token) headers['Authorization'] = 'Bearer ' + token;
|
||||
|
||||
fetch('/api/notifications/unread-count', { headers })
|
||||
.then(r => r.json())
|
||||
.then(d => {
|
||||
const count = d.unread_count || 0;
|
||||
const badge = document.getElementById('notificationBadge');
|
||||
if (count > 0) {
|
||||
badge.textContent = count > 99 ? '99+' : count;
|
||||
badge.style.display = 'inline-flex';
|
||||
} else {
|
||||
badge.style.display = 'none';
|
||||
}
|
||||
})
|
||||
.catch(function(){});
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
const d = document.createElement('div');
|
||||
d.textContent = str;
|
||||
return d.innerHTML;
|
||||
}
|
||||
|
||||
setInterval(updateUnreadCount, 60000);
|
||||
updateUnreadCount();
|
||||
|
||||
function toggleServerList(e) {
|
||||
e.preventDefault();
|
||||
const item = e.currentTarget.closest('.nav-item-accordion');
|
||||
const sub = document.getElementById('serverList');
|
||||
const chevron = item.querySelector('.nav-chevron');
|
||||
if (item.classList.contains('expanded')) {
|
||||
item.classList.remove('expanded');
|
||||
sub.style.maxHeight = '0';
|
||||
return;
|
||||
}
|
||||
item.classList.add('expanded');
|
||||
if (!serverListLoaded) {
|
||||
serverListLoaded = true;
|
||||
fetch('/sidebar/servers')
|
||||
.then(r => r.json())
|
||||
.then(d => {
|
||||
if (d.success && d.data) {
|
||||
let html = '<a href="/servers" class="nav-sub-item"><i class="fas fa-list"></i> All Servers</a>';
|
||||
d.data.forEach(s => {
|
||||
const status = s.current_status === 'online' ? 'success' : s.current_status === 'offline' ? 'danger' : 'muted';
|
||||
html += '<a href="/servers/' + s.id + '" class="nav-sub-item">'
|
||||
+ '<span class="status-dot-sm status-' + status + '"></span> '
|
||||
+ escapeHtml(s.name) + '</a>';
|
||||
});
|
||||
sub.innerHTML = html;
|
||||
} else {
|
||||
sub.innerHTML = '<div class="nav-sub-item" style="color:var(--text-muted)">No servers</div>';
|
||||
}
|
||||
sub.style.maxHeight = sub.scrollHeight + 'px';
|
||||
})
|
||||
.catch(() => {
|
||||
sub.innerHTML = '<div class="nav-sub-item" style="color:var(--danger)">Failed to load</div>';
|
||||
sub.style.maxHeight = sub.scrollHeight + 'px';
|
||||
});
|
||||
} else {
|
||||
sub.style.maxHeight = sub.scrollHeight + 'px';
|
||||
}
|
||||
}
|
||||
321
public/assets/js/nginx-manager.js
Normal file
321
public/assets/js/nginx-manager.js
Normal file
@@ -0,0 +1,321 @@
|
||||
let currentEditFile = null;
|
||||
let editorDirty = false;
|
||||
let editorUndoStack = [];
|
||||
let editorRedoStack = [];
|
||||
let editorHistoryIndex = -1;
|
||||
|
||||
function getCsrfToken() {
|
||||
return document.querySelector('meta[name="csrf-token"]')?.content || '';
|
||||
}
|
||||
|
||||
function updateLineNumbers() {
|
||||
const ta = document.getElementById('editorContent');
|
||||
const gutter = document.getElementById('editorGutter');
|
||||
const lines = ta.value.split('\n').length;
|
||||
const numbers = [];
|
||||
for (let i = 1; i <= lines; i++) {
|
||||
numbers.push('<span>' + i + '</span>');
|
||||
}
|
||||
gutter.innerHTML = numbers.join('');
|
||||
}
|
||||
|
||||
function updateCursorPos() {
|
||||
const ta = document.getElementById('editorContent');
|
||||
const before = ta.value.substring(0, ta.selectionStart);
|
||||
const lines = before.split('\n');
|
||||
const line = lines.length;
|
||||
const col = lines[lines.length - 1].length + 1;
|
||||
document.getElementById('editorCursorPos').textContent = 'Ln ' + line + ', Col ' + col;
|
||||
}
|
||||
|
||||
function highlightNginx(text) {
|
||||
let h = text
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/#(.*)$/gm, '<span class="hl-comment">#$1</span>')
|
||||
.replace(/\b(server|location|listen|server_name|root|index|try_files|include|fastcgi_pass|proxy_pass|return|rewrite|set|if|error_page|ssl_certificate|ssl_certificate_key|ssl_protocols|ssl_ciphers|add_header|access_log|error_log|keepalive|gzip|proxy_set_header|proxy_redirect|client_max_body_size|autoindex)\b/g, '<span class="hl-keyword">$1</span>')
|
||||
.replace(/\b(\d+)\b/g, '<span class="hl-number">$1</span>')
|
||||
.replace(/\b(https?|fastcgi|unix):\/\/[^\s;{]+/g, '<span class="hl-string">$&</span>')
|
||||
.replace(/'(.*?)'/g, '<span class="hl-string">\'$1\'</span>')
|
||||
.replace(/(\$\w+)/g, '<span class="hl-var">$1</span>')
|
||||
.replace(/\b(\w+\.\w+(?:\.\w+)*)\b/g, function(m) {
|
||||
if (m.includes('com') || m.includes('org') || m.includes('net') || m.includes('io') || m.includes('local')) {
|
||||
return '<span class="hl-string">' + m + '</span>';
|
||||
}
|
||||
return m;
|
||||
});
|
||||
return h;
|
||||
}
|
||||
|
||||
function syncHighlight() {
|
||||
const ta = document.getElementById('editorContent');
|
||||
const code = document.getElementById('editorHighlightCode');
|
||||
code.innerHTML = highlightNginx(ta.value) + '\n';
|
||||
}
|
||||
|
||||
function pushHistory() {
|
||||
const ta = document.getElementById('editorContent');
|
||||
if (editorHistoryIndex >= 0 && editorUndoStack[editorHistoryIndex] === ta.value) return;
|
||||
editorUndoStack = editorUndoStack.slice(0, editorHistoryIndex + 1);
|
||||
editorUndoStack.push(ta.value);
|
||||
editorRedoStack = [];
|
||||
editorHistoryIndex = editorUndoStack.length - 1;
|
||||
if (editorUndoStack.length > 100) {
|
||||
editorUndoStack.shift();
|
||||
editorHistoryIndex--;
|
||||
}
|
||||
}
|
||||
|
||||
function undoEditor() {
|
||||
if (editorHistoryIndex <= 0) return;
|
||||
editorRedoStack.push(editorUndoStack[editorHistoryIndex]);
|
||||
editorHistoryIndex--;
|
||||
const ta = document.getElementById('editorContent');
|
||||
ta.value = editorUndoStack[editorHistoryIndex];
|
||||
ta.dispatchEvent(new Event('input'));
|
||||
}
|
||||
|
||||
function redoEditor() {
|
||||
if (editorRedoStack.length === 0) return;
|
||||
const val = editorRedoStack.pop();
|
||||
editorUndoStack.push(val);
|
||||
editorHistoryIndex++;
|
||||
const ta = document.getElementById('editorContent');
|
||||
ta.value = val;
|
||||
ta.dispatchEvent(new Event('input'));
|
||||
}
|
||||
|
||||
function nginxAction(action) {
|
||||
const btn = event.target.closest('button');
|
||||
const origText = btn.innerHTML;
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Working...';
|
||||
|
||||
fetch('/servers/' + serverId + '/nginx', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'X-CSRF-Token': getCsrfToken(),
|
||||
},
|
||||
body: 'action=' + action + '&_csrf_token=' + encodeURIComponent(getCsrfToken()),
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
showToast(data.success ? 'success' : 'error', data.message);
|
||||
if (data.output) {
|
||||
document.getElementById('nginxResultMessage').textContent = data.message;
|
||||
document.getElementById('nginxResultOutputText').textContent = data.output;
|
||||
document.getElementById('nginxResultOutput').style.display = 'block';
|
||||
document.getElementById('nginxResultModal').style.display = 'flex';
|
||||
}
|
||||
if (data.success && ['restart', 'reload', 'start', 'stop'].includes(action)) {
|
||||
setTimeout(() => location.reload(), 1500);
|
||||
}
|
||||
})
|
||||
.catch(err => showToast('error', 'Request failed: ' + err.message))
|
||||
.finally(() => { btn.disabled = false; btn.innerHTML = origText; });
|
||||
}
|
||||
|
||||
function editFile(file) {
|
||||
currentEditFile = file;
|
||||
editorUndoStack = [];
|
||||
editorRedoStack = [];
|
||||
editorHistoryIndex = -1;
|
||||
editorDirty = false;
|
||||
document.getElementById('editorFilePath').textContent = file;
|
||||
document.getElementById('editorContent').value = 'Loading...';
|
||||
document.getElementById('editorStatus').textContent = 'Loading...';
|
||||
document.getElementById('editorDirtyIndicator').style.display = 'none';
|
||||
document.getElementById('editorFileSize').textContent = '';
|
||||
document.getElementById('nginxEditorModal').style.display = 'flex';
|
||||
setTimeout(() => document.getElementById('editorContent').focus(), 100);
|
||||
|
||||
fetch('/servers/' + serverId + '/nginx/file?file=' + encodeURIComponent(file))
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
const ta = document.getElementById('editorContent');
|
||||
ta.value = data.content;
|
||||
document.getElementById('editorFileSize').textContent = (data.content.length / 1024).toFixed(1) + ' KB';
|
||||
document.getElementById('editorStatus').textContent = 'Ready';
|
||||
document.getElementById('editorDirtyIndicator').style.display = 'none';
|
||||
editorDirty = false;
|
||||
updateLineNumbers();
|
||||
syncHighlight();
|
||||
pushHistory();
|
||||
} else {
|
||||
showToast('error', data.message);
|
||||
closeEditor();
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
showToast('error', 'Failed to load file');
|
||||
closeEditor();
|
||||
});
|
||||
|
||||
const ta = document.getElementById('editorContent');
|
||||
ta.oninput = function() {
|
||||
editorDirty = true;
|
||||
document.getElementById('editorStatus').textContent = 'Unsaved changes';
|
||||
document.getElementById('editorDirtyIndicator').style.display = 'inline';
|
||||
updateLineNumbers();
|
||||
syncHighlight();
|
||||
pushHistory();
|
||||
};
|
||||
ta.onscroll = function() {
|
||||
document.getElementById('editorGutter').scrollTop = this.scrollTop;
|
||||
document.getElementById('editorHighlight').scrollTop = this.scrollTop;
|
||||
};
|
||||
ta.onkeydown = function(e) {
|
||||
if (e.key === 'Tab') {
|
||||
e.preventDefault();
|
||||
const start = this.selectionStart;
|
||||
const end = this.selectionEnd;
|
||||
this.value = this.value.substring(0, start) + ' ' + this.value.substring(end);
|
||||
this.selectionStart = this.selectionEnd = start + 4;
|
||||
this.dispatchEvent(new Event('input'));
|
||||
}
|
||||
};
|
||||
ta.onmouseup = ta.onkeyup = function() {
|
||||
updateCursorPos();
|
||||
};
|
||||
ta.addEventListener('paste', function() {
|
||||
setTimeout(() => {
|
||||
this.dispatchEvent(new Event('input'));
|
||||
updateCursorPos();
|
||||
}, 10);
|
||||
});
|
||||
}
|
||||
|
||||
function saveFile() {
|
||||
const content = document.getElementById('editorContent').value;
|
||||
document.getElementById('editorStatus').textContent = 'Saving...';
|
||||
|
||||
fetch('/servers/' + serverId + '/nginx/file', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'X-CSRF-Token': getCsrfToken(),
|
||||
},
|
||||
body: 'file=' + encodeURIComponent(currentEditFile) + '&content=' + encodeURIComponent(content) + '&_csrf_token=' + encodeURIComponent(getCsrfToken()),
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
showToast(data.success ? 'success' : 'error', data.message);
|
||||
if (data.success) {
|
||||
editorDirty = false;
|
||||
document.getElementById('editorStatus').textContent = 'Saved';
|
||||
document.getElementById('editorDirtyIndicator').style.display = 'none';
|
||||
closeEditor();
|
||||
setTimeout(() => location.reload(), 500);
|
||||
} else {
|
||||
document.getElementById('editorStatus').textContent = 'Save failed';
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
showToast('error', 'Save request failed');
|
||||
document.getElementById('editorStatus').textContent = 'Error';
|
||||
});
|
||||
}
|
||||
|
||||
function deleteSite(site) {
|
||||
if (!confirm("Permanently delete site '" + site + "'?\nThis will remove both the config file and site directory contents.")) return;
|
||||
if (!confirm("Are you sure? This cannot be undone.")) return;
|
||||
|
||||
fetch('/servers/' + serverId + '/nginx/site', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'X-CSRF-Token': getCsrfToken(),
|
||||
},
|
||||
body: '_method=DELETE&site=' + encodeURIComponent(site) + '&_csrf_token=' + encodeURIComponent(getCsrfToken()),
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
showToast(data.success ? 'success' : 'error', data.message);
|
||||
if (data.success) setTimeout(() => location.reload(), 500);
|
||||
})
|
||||
.catch(err => showToast('error', 'Request failed: ' + err.message));
|
||||
}
|
||||
|
||||
function toggleSite(site, enable) {
|
||||
const action = enable ? 'enable' : 'disable';
|
||||
if (!confirm((enable ? 'Enable' : 'Disable') + " site '" + site + "'?")) return;
|
||||
|
||||
fetch('/servers/' + serverId + '/nginx/toggle-site', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'X-CSRF-Token': getCsrfToken(),
|
||||
},
|
||||
body: 'site=' + encodeURIComponent(site) + '&enable=' + (enable ? '1' : '0') + '&_csrf_token=' + encodeURIComponent(getCsrfToken()),
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
showToast(data.success ? 'success' : 'error', data.message);
|
||||
if (data.success) setTimeout(() => location.reload(), 500);
|
||||
})
|
||||
.catch(err => showToast('error', 'Request failed: ' + err.message));
|
||||
}
|
||||
|
||||
function closeEditor() {
|
||||
if (editorDirty && !confirm('Discard unsaved changes?')) return;
|
||||
document.getElementById('nginxEditorModal').style.display = 'none';
|
||||
currentEditFile = null;
|
||||
editorDirty = false;
|
||||
}
|
||||
|
||||
function closeNginxModal() {
|
||||
document.getElementById('nginxResultModal').style.display = 'none';
|
||||
document.getElementById('nginxResultOutput').style.display = 'none';
|
||||
}
|
||||
|
||||
function openNewSiteModal() {
|
||||
document.getElementById('newSiteForm').reset();
|
||||
document.getElementById('siteServerName').value = '';
|
||||
document.getElementById('sitePhp').checked = true;
|
||||
document.getElementById('siteEnable').checked = true;
|
||||
document.getElementById('nginxNewSiteModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
function closeNewSiteModal() {
|
||||
document.getElementById('nginxNewSiteModal').style.display = 'none';
|
||||
}
|
||||
|
||||
function createSite(e) {
|
||||
e.preventDefault();
|
||||
const btn = document.getElementById('createSiteBtn');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Creating...';
|
||||
|
||||
const formData = new URLSearchParams(new FormData(document.getElementById('newSiteForm')));
|
||||
formData.append('_csrf_token', getCsrfToken());
|
||||
|
||||
fetch('/servers/' + serverId + '/nginx/site', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'X-CSRF-Token': getCsrfToken(),
|
||||
},
|
||||
body: formData.toString(),
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
showToast(data.success ? 'success' : 'error', data.message);
|
||||
if (data.success) {
|
||||
closeNewSiteModal();
|
||||
setTimeout(() => location.reload(), 800);
|
||||
}
|
||||
})
|
||||
.catch(err => showToast('error', 'Request failed: ' + err.message))
|
||||
.finally(() => { btn.disabled = false; btn.innerHTML = '<i class="fas fa-plus"></i> Create Site'; });
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') { closeEditor(); closeNginxModal(); }
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 's' && currentEditFile) {
|
||||
e.preventDefault();
|
||||
saveFile();
|
||||
}
|
||||
});
|
||||
20
public/assets/js/notifications.js
Normal file
20
public/assets/js/notifications.js
Normal file
@@ -0,0 +1,20 @@
|
||||
function markAllRead() {
|
||||
const token = document.querySelector('meta[name="api-token"]')?.getAttribute('content') || '';
|
||||
const headers = { 'Accept': 'application/json' };
|
||||
if (token) headers['Authorization'] = 'Bearer ' + token;
|
||||
|
||||
fetch('/api/notifications/read-all', { method: 'POST', headers })
|
||||
.then(r => r.json())
|
||||
.then(d => {
|
||||
if (d.success) {
|
||||
document.querySelectorAll('.notif-item').forEach(function(el) {
|
||||
el.classList.remove('notif-unread');
|
||||
el.classList.add('notif-read');
|
||||
const dot = el.querySelector('.notif-dot');
|
||||
if (dot) dot.remove();
|
||||
});
|
||||
location.reload();
|
||||
}
|
||||
})
|
||||
.catch(function(){});
|
||||
}
|
||||
30
public/assets/js/server-list.js
Normal file
30
public/assets/js/server-list.js
Normal file
@@ -0,0 +1,30 @@
|
||||
function deleteServer(id, name) {
|
||||
if (confirm('Are you sure you want to delete server "' + name + '"?\nThis action cannot be undone.')) {
|
||||
const form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
form.action = '/servers/' + id + '/delete';
|
||||
const csrf = document.createElement('input');
|
||||
csrf.type = 'hidden';
|
||||
csrf.name = '_csrf_token';
|
||||
csrf.value = document.querySelector('meta[name="csrf-token"]')?.content || '';
|
||||
form.appendChild(csrf);
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('serverSearch')?.addEventListener('input', function(e) {
|
||||
applyFilters();
|
||||
});
|
||||
|
||||
document.getElementById('statusFilter')?.addEventListener('change', applyFilters);
|
||||
document.getElementById('groupFilter')?.addEventListener('change', applyFilters);
|
||||
|
||||
function applyFilters() {
|
||||
const search = document.getElementById('serverSearch')?.value || '';
|
||||
const status = document.getElementById('statusFilter')?.value || '';
|
||||
const group = document.getElementById('groupFilter')?.value || '';
|
||||
window.location.href = '/servers?search=' + encodeURIComponent(search) +
|
||||
'&status=' + encodeURIComponent(status) +
|
||||
'&group=' + encodeURIComponent(group);
|
||||
}
|
||||
40
public/assets/js/server-permissions.js
Normal file
40
public/assets/js/server-permissions.js
Normal file
@@ -0,0 +1,40 @@
|
||||
function createPermissionRow(type, value, description) {
|
||||
const idx = permIndex++;
|
||||
const container = document.getElementById('permissions-container');
|
||||
const div = document.createElement('div');
|
||||
div.className = 'permission-row';
|
||||
div.dataset.index = idx;
|
||||
div.innerHTML = `
|
||||
<select name="permissions[${idx}][type]" class="form-input permission-type">
|
||||
<option value="sudo" ${type === 'sudo' ? 'selected' : ''}>Sudo (passwordless)</option>
|
||||
<option value="command" ${type === 'command' ? 'selected' : ''}>Command</option>
|
||||
<option value="file_read" ${type === 'file_read' ? 'selected' : ''}>File Read</option>
|
||||
<option value="file_write" ${type === 'file_write' ? 'selected' : ''}>File Write</option>
|
||||
<option value="custom" ${type === 'custom' ? 'selected' : ''}>Custom Command</option>
|
||||
</select>
|
||||
<input type="text" name="permissions[${idx}][value]" class="form-input permission-value"
|
||||
placeholder="e.g., systemctl" value="${escapeHtml(value || '')}">
|
||||
<input type="text" name="permissions[${idx}][description]" class="form-input permission-desc"
|
||||
placeholder="Description (optional)" value="${escapeHtml(description || '')}">
|
||||
<button type="button" class="btn btn-danger btn-sm" onclick="removePermissionRow(this)"><i class="fas fa-times"></i></button>
|
||||
`;
|
||||
container.appendChild(div);
|
||||
}
|
||||
|
||||
function removePermissionRow(btn) {
|
||||
btn.closest('.permission-row').remove();
|
||||
}
|
||||
|
||||
function addPresetPermission(type, value, description) {
|
||||
createPermissionRow(type, value, description);
|
||||
}
|
||||
|
||||
document.getElementById('addPermissionBtn').addEventListener('click', function() {
|
||||
createPermissionRow('command', '', '');
|
||||
});
|
||||
|
||||
function escapeHtml(str) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = str;
|
||||
return div.innerHTML;
|
||||
}
|
||||
62
public/assets/js/server-processes.js
Normal file
62
public/assets/js/server-processes.js
Normal file
@@ -0,0 +1,62 @@
|
||||
let pendingPid = null;
|
||||
let pendingBtn = null;
|
||||
|
||||
function getCsrfToken() {
|
||||
return document.querySelector('meta[name="csrf-token"]')?.content || '';
|
||||
}
|
||||
|
||||
function killProcess(pid, btn) {
|
||||
pendingPid = pid;
|
||||
pendingBtn = btn;
|
||||
document.getElementById('killPidDisplay').textContent = pid;
|
||||
document.getElementById('killModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
function closeKillModal() {
|
||||
pendingPid = null;
|
||||
pendingBtn = null;
|
||||
document.getElementById('killForce').checked = false;
|
||||
document.getElementById('killModal').style.display = 'none';
|
||||
}
|
||||
|
||||
function confirmKill() {
|
||||
if (!pendingPid) return;
|
||||
|
||||
const signal = document.getElementById('killForce').checked ? 9 : 15;
|
||||
const btn = document.getElementById('confirmKillBtn');
|
||||
const originalText = btn.innerHTML;
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Killing...';
|
||||
|
||||
fetch('/servers/' + serverId + '/processes/kill', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'X-CSRF-Token': getCsrfToken(),
|
||||
},
|
||||
body: 'pid=' + pendingPid + '&signal=' + signal + '&_csrf_token=' + encodeURIComponent(getCsrfToken()),
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
showToast(data.success ? 'success' : 'error', data.message);
|
||||
if (data.success && pendingBtn) {
|
||||
const row = pendingBtn.closest('tr');
|
||||
row.style.opacity = '0.4';
|
||||
pendingBtn.disabled = true;
|
||||
pendingBtn.innerHTML = '<i class="fas fa-check"></i> Killed';
|
||||
}
|
||||
closeKillModal();
|
||||
})
|
||||
.catch(err => {
|
||||
showToast('error', 'Request failed: ' + err.message);
|
||||
closeKillModal();
|
||||
})
|
||||
.finally(() => {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = originalText;
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') closeKillModal();
|
||||
});
|
||||
86
public/assets/js/server-services.js
Normal file
86
public/assets/js/server-services.js
Normal file
@@ -0,0 +1,86 @@
|
||||
function applyFilters() {
|
||||
const nameQ = document.getElementById('filterName').value.toLowerCase();
|
||||
const statusQ = document.getElementById('filterStatus').value;
|
||||
const filtered = allServices.filter(s => {
|
||||
if (nameQ && !s.name.toLowerCase().includes(nameQ)) return false;
|
||||
if (statusQ && s.status !== statusQ) return false;
|
||||
return true;
|
||||
});
|
||||
renderPage(filtered, 1);
|
||||
}
|
||||
|
||||
function renderPage(list, page) {
|
||||
const totalPages = Math.ceil(list.length / perPage) || 1;
|
||||
const start = (page - 1) * perPage;
|
||||
const slice = list.slice(start, start + perPage);
|
||||
const tbody = document.getElementById('servicesBody');
|
||||
let html = '';
|
||||
slice.forEach(s => {
|
||||
const isActive = s.status === 'active';
|
||||
const isInactive = s.status === 'inactive';
|
||||
html += '<tr class="service-row">';
|
||||
html += '<td><code>' + escHtml(s.name) + '</code></td>';
|
||||
html += '<td><span class="badge ' + (isActive ? 'badge-success' : isInactive ? 'badge-secondary' : 'badge-warning') + '">' + s.status + '</span></td>';
|
||||
html += '<td class="actions-cell" style="white-space:nowrap">';
|
||||
if (!isActive) html += '<button class="btn btn-xs btn-success" onclick="serviceAction(\'start\',\'' + escHtml(s.name) + '\')" title="Start"><i class="fas fa-play"></i></button> ';
|
||||
if (!isInactive) html += '<button class="btn btn-xs btn-danger" onclick="serviceAction(\'stop\',\'' + escHtml(s.name) + '\')" title="Stop"><i class="fas fa-stop"></i></button> ';
|
||||
html += '<button class="btn btn-xs btn-warning" onclick="serviceAction(\'restart\',\'' + escHtml(s.name) + '\')" title="Restart"><i class="fas fa-redo"></i></button> ';
|
||||
html += '<button class="btn btn-xs btn-info" onclick="serviceAction(\'reload\',\'' + escHtml(s.name) + '\')" title="Reload"><i class="fas fa-sync-alt"></i></button> ';
|
||||
if (!s.enabled) html += '<button class="btn btn-xs btn-secondary" onclick="serviceAction(\'enable\',\'' + escHtml(s.name) + '\')" title="Enable"><i class="fas fa-power-off"></i></button> ';
|
||||
if (s.enabled) html += '<button class="btn btn-xs btn-secondary" onclick="serviceAction(\'disable\',\'' + escHtml(s.name) + '\')" title="Disable"><i class="fas fa-ban"></i></button> ';
|
||||
html += '</td></tr>';
|
||||
});
|
||||
if (!html) html = '<tr><td colspan="3" style="text-align:center;color:var(--text-muted);padding:2rem">No services match the filter.</td></tr>';
|
||||
tbody.innerHTML = html;
|
||||
renderPagination(list, page);
|
||||
}
|
||||
|
||||
function renderPagination(list, page) {
|
||||
const totalPages = Math.ceil(list.length / perPage) || 1;
|
||||
const container = document.getElementById('servicesPagination');
|
||||
let html = '';
|
||||
for (let p = 1; p <= totalPages && p <= 15; p++) {
|
||||
html += '<button class="btn btn-xs ' + (p === page ? 'btn-primary' : 'btn-secondary') + '" onclick="renderPage(list,' + p + ')" data-list=\'' + JSON.stringify(list).replace(/'/g,"'") + '\'>' + p + '</button>';
|
||||
}
|
||||
if (totalPages > 15) html += '<span class="text-muted" style="font-size:0.8rem">...</span>';
|
||||
container.innerHTML = html + ' <span class="text-muted" style="font-size:0.8rem">' + list.length + ' services</span>';
|
||||
container.querySelectorAll('button').forEach(btn => {
|
||||
const p = parseInt(btn.textContent);
|
||||
if (!isNaN(p)) {
|
||||
btn.onclick = function() { renderPage(list, p); };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
applyFilters();
|
||||
|
||||
function getCsrfToken() { return document.querySelector('meta[name="csrf-token"]')?.content || ''; }
|
||||
|
||||
function serviceAction(action, service) {
|
||||
fetch('/servers/' + serverId + '/services', {
|
||||
method:'POST',
|
||||
headers:{'Content-Type':'application/x-www-form-urlencoded','X-CSRF-Token':getCsrfToken()},
|
||||
body:'action='+action+'&service='+encodeURIComponent(service)+'&_csrf_token='+encodeURIComponent(getCsrfToken()),
|
||||
})
|
||||
.then(r=>r.json()).then(d=>{
|
||||
showToast(d.success?'success':'error', d.message);
|
||||
if (d.output) {
|
||||
document.getElementById('srTitle').textContent = d.success ? 'Success' : 'Error';
|
||||
document.getElementById('srMessage').textContent = d.message;
|
||||
document.getElementById('srOutputText').textContent = d.output;
|
||||
document.getElementById('srOutput').style.display = 'block';
|
||||
document.getElementById('serviceResultModal').style.display = 'flex';
|
||||
}
|
||||
setTimeout(() => location.reload(), 1500);
|
||||
})
|
||||
.catch(err => showToast('error', 'Request failed'));
|
||||
}
|
||||
|
||||
function closeSrModal() {
|
||||
document.getElementById('serviceResultModal').style.display = 'none';
|
||||
document.getElementById('srOutput').style.display = 'none';
|
||||
}
|
||||
|
||||
function escHtml(s) { const d=document.createElement('div'); d.textContent=s; return d.innerHTML; }
|
||||
|
||||
document.addEventListener('keydown', function(e) { if (e.key === 'Escape') closeSrModal(); });
|
||||
323
public/assets/js/server-show.js
Normal file
323
public/assets/js/server-show.js
Normal file
@@ -0,0 +1,323 @@
|
||||
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;
|
||||
});
|
||||
}
|
||||
73
public/assets/js/server-ssl.js
Normal file
73
public/assets/js/server-ssl.js
Normal file
@@ -0,0 +1,73 @@
|
||||
function getCsrfToken() {
|
||||
return document.querySelector('meta[name="csrf-token"]')?.content || '';
|
||||
}
|
||||
|
||||
function sslAction(action, extraData) {
|
||||
const data = 'action=' + action + '&_csrf_token=' + encodeURIComponent(getCsrfToken());
|
||||
const body = extraData ? data + '&' + extraData : data;
|
||||
|
||||
fetch('/servers/' + serverId + '/ssl', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'X-CSRF-Token': getCsrfToken(),
|
||||
},
|
||||
body: body,
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
showToast(data.success ? 'success' : 'error', data.message);
|
||||
if (data.output) {
|
||||
document.getElementById('sslResultMessage').textContent = data.message || '';
|
||||
document.getElementById('sslResultOutputText').textContent = data.output;
|
||||
document.getElementById('sslResultOutput').style.display = 'block';
|
||||
document.getElementById('sslResultModal').style.display = 'flex';
|
||||
}
|
||||
if (action === 'install' && data.success) {
|
||||
setTimeout(() => location.reload(), 1500);
|
||||
}
|
||||
if (['renew', 'delete'].includes(action) && data.success) {
|
||||
setTimeout(() => location.reload(), 1000);
|
||||
}
|
||||
})
|
||||
.catch(err => showToast('error', 'Request failed: ' + err.message));
|
||||
}
|
||||
|
||||
function openRequestModal() {
|
||||
document.getElementById('sslRequestForm').reset();
|
||||
document.getElementById('sslRequestModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
function closeRequestModal() {
|
||||
document.getElementById('sslRequestModal').style.display = 'none';
|
||||
}
|
||||
|
||||
function requestCert(e) {
|
||||
e.preventDefault();
|
||||
const btn = document.getElementById('requestCertBtn');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Requesting...';
|
||||
|
||||
const domains = document.getElementById('sslDomains').value;
|
||||
const email = document.getElementById('sslEmail').value;
|
||||
|
||||
sslAction('request', 'domains=' + encodeURIComponent(domains) + '&email=' + encodeURIComponent(email));
|
||||
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="fas fa-plus"></i> Request Certificate';
|
||||
closeRequestModal();
|
||||
}
|
||||
|
||||
function deleteCert(name) {
|
||||
if (!confirm("Delete certificate '" + name + "'?")) return;
|
||||
sslAction('delete', 'cert_name=' + encodeURIComponent(name));
|
||||
}
|
||||
|
||||
function closeSslResultModal() {
|
||||
document.getElementById('sslResultModal').style.display = 'none';
|
||||
document.getElementById('sslResultOutput').style.display = 'none';
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') { closeRequestModal(); closeSslResultModal(); }
|
||||
});
|
||||
42
public/assets/js/system-users.js
Normal file
42
public/assets/js/system-users.js
Normal file
@@ -0,0 +1,42 @@
|
||||
function getCsrfToken() { return document.querySelector('meta[name="csrf-token"]')?.content || ''; }
|
||||
|
||||
function showAddUser() { document.getElementById('addUserModal').style.display = 'flex'; }
|
||||
function showAddGroup() { document.getElementById('addGroupModal').style.display = 'flex'; }
|
||||
function closeModal(id) { document.getElementById(id).style.display = 'none'; }
|
||||
|
||||
function doAddUser() {
|
||||
const data = 'action=add&username=' + encodeURIComponent(document.getElementById('suUser').value)
|
||||
+ '&password=' + encodeURIComponent(document.getElementById('suPass').value)
|
||||
+ '&groups=' + encodeURIComponent(document.getElementById('suGroups').value)
|
||||
+ '&create_home=' + (document.getElementById('suHome').checked ? '1' : '0')
|
||||
+ '&_csrf_token=' + encodeURIComponent(getCsrfToken());
|
||||
fetch('/servers/' + serverId + '/users', {method:'POST', headers:{'Content-Type':'application/x-www-form-urlencoded','X-CSRF-Token':getCsrfToken()}, body:data})
|
||||
.then(r=>r.json()).then(d=>{showToast(d.success?'success':'error',d.message);if(d.success)setTimeout(()=>location.reload(),800)});
|
||||
closeModal('addUserModal');
|
||||
}
|
||||
|
||||
function doAddGroup() {
|
||||
const data = 'action=addgroup&group=' + encodeURIComponent(document.getElementById('sgGroup').value) + '&_csrf_token=' + encodeURIComponent(getCsrfToken());
|
||||
fetch('/servers/' + serverId + '/users', {method:'POST', headers:{'Content-Type':'application/x-www-form-urlencoded','X-CSRF-Token':getCsrfToken()}, body:data})
|
||||
.then(r=>r.json()).then(d=>{showToast(d.success?'success':'error',d.message);if(d.success)setTimeout(()=>location.reload(),800)});
|
||||
closeModal('addGroupModal');
|
||||
}
|
||||
|
||||
function lockUser(u) {
|
||||
if(!confirm('Lock user \''+u+'\'?'))return;
|
||||
fetch('/servers/' + serverId + '/users', {method:'POST', headers:{'Content-Type':'application/x-www-form-urlencoded','X-CSRF-Token':getCsrfToken()}, body:'action=lock&username='+encodeURIComponent(u)+'&_csrf_token='+encodeURIComponent(getCsrfToken())})
|
||||
.then(r=>r.json()).then(d=>{showToast(d.success?'success':'error',d.message);if(d.success)setTimeout(()=>location.reload(),800)});
|
||||
}
|
||||
|
||||
function unlockUser(u) {
|
||||
fetch('/servers/' + serverId + '/users', {method:'POST', headers:{'Content-Type':'application/x-www-form-urlencoded','X-CSRF-Token':getCsrfToken()}, body:'action=unlock&username='+encodeURIComponent(u)+'&_csrf_token='+encodeURIComponent(getCsrfToken())})
|
||||
.then(r=>r.json()).then(d=>{showToast(d.success?'success':'error',d.message);if(d.success)setTimeout(()=>location.reload(),800)});
|
||||
}
|
||||
|
||||
function deleteUser(u) {
|
||||
if(!confirm('Delete user \''+u+'\'?\nThis cannot be undone.'))return;
|
||||
fetch('/servers/' + serverId + '/users', {method:'POST', headers:{'Content-Type':'application/x-www-form-urlencoded','X-CSRF-Token':getCsrfToken()}, body:'action=delete&username='+encodeURIComponent(u)+'&_csrf_token='+encodeURIComponent(getCsrfToken())})
|
||||
.then(r=>r.json()).then(d=>{showToast(d.success?'success':'error',d.message);if(d.success)setTimeout(()=>location.reload(),800)});
|
||||
}
|
||||
|
||||
document.addEventListener('keydown',function(e){if(e.key==='Escape'){closeModal('addUserModal');closeModal('addGroupModal')}});
|
||||
59
public/assets/js/team-index.js
Normal file
59
public/assets/js/team-index.js
Normal file
@@ -0,0 +1,59 @@
|
||||
const allCards = document.querySelectorAll('.team-card');
|
||||
|
||||
function filterTeams(query) {
|
||||
const q = query.toLowerCase().trim();
|
||||
const grid = document.getElementById('teamsGrid');
|
||||
const empty = document.getElementById('emptyState');
|
||||
const clearBtn = document.getElementById('searchClear');
|
||||
let visible = 0;
|
||||
|
||||
clearBtn.style.display = q ? 'block' : 'none';
|
||||
|
||||
if (grid) {
|
||||
allCards.forEach(function(card) {
|
||||
const name = card.dataset.name || '';
|
||||
const desc = card.dataset.desc || '';
|
||||
const match = !q || name.includes(q) || desc.includes(q);
|
||||
card.style.display = match ? '' : 'none';
|
||||
if (match) visible++;
|
||||
});
|
||||
|
||||
if (visible === 0) {
|
||||
if (!empty) {
|
||||
const container = document.getElementById('teamsContainer');
|
||||
const div = document.createElement('div');
|
||||
div.className = 'empty-state';
|
||||
div.id = 'emptyState';
|
||||
div.innerHTML = '<i class="fas fa-users-cog"></i><p>No teams match your search.</p>';
|
||||
container.appendChild(div);
|
||||
} else {
|
||||
empty.querySelector('p').textContent = 'No teams match your search.';
|
||||
empty.style.display = '';
|
||||
}
|
||||
} else {
|
||||
if (empty) empty.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
const url = q ? '?search=' + encodeURIComponent(query) : window.location.pathname;
|
||||
window.history.replaceState({}, '', url);
|
||||
}
|
||||
|
||||
document.getElementById('teamSearch')?.addEventListener('input', function() {
|
||||
filterTeams(this.value);
|
||||
});
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const searchInput = document.getElementById('teamSearch');
|
||||
if (searchInput && searchInput.value) {
|
||||
filterTeams(searchInput.value);
|
||||
}
|
||||
});
|
||||
|
||||
function deleteTeam(id, name) {
|
||||
if (confirm('Delete team "' + name + '"?\nThis will remove access for all members.')) {
|
||||
const form = document.getElementById('deleteForm');
|
||||
form.action = '/teams/' + id + '/delete';
|
||||
form.submit();
|
||||
}
|
||||
}
|
||||
187
public/assets/js/team-show.js
Normal file
187
public/assets/js/team-show.js
Normal file
@@ -0,0 +1,187 @@
|
||||
function getCsrfToken() {
|
||||
return document.querySelector('meta[name="csrf-token"]')?.content || '';
|
||||
}
|
||||
|
||||
function editTeam() {
|
||||
const name = prompt('Team name:', teamName);
|
||||
if (!name) return;
|
||||
const desc = prompt('Description:', teamDescription);
|
||||
|
||||
const form = new FormData();
|
||||
form.append('name', name);
|
||||
form.append('description', desc || '');
|
||||
form.append('_csrf_token', getCsrfToken());
|
||||
|
||||
fetch('/teams/' + teamId + '/update', {
|
||||
method: 'POST',
|
||||
headers: { 'X-CSRF-Token': getCsrfToken() },
|
||||
body: form,
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(res => {
|
||||
if (res.success) { showToast('success', res.message); location.reload(); }
|
||||
else { showToast('error', res.message); }
|
||||
});
|
||||
}
|
||||
|
||||
function deleteTeam() {
|
||||
if (!confirm('Delete this team? Members will lose access to assigned servers.')) return;
|
||||
document.getElementById('postForm').action = '/teams/' + teamId + '/delete';
|
||||
document.getElementById('postForm').submit();
|
||||
}
|
||||
|
||||
let searchTimeout = null;
|
||||
let selectedUserId = null;
|
||||
|
||||
document.getElementById('addUserInput').addEventListener('input', function() {
|
||||
clearTimeout(searchTimeout);
|
||||
const q = this.value.trim();
|
||||
selectedUserId = null;
|
||||
|
||||
if (q.length < 2) {
|
||||
document.getElementById('userDropdown').classList.remove('active');
|
||||
return;
|
||||
}
|
||||
|
||||
searchTimeout = setTimeout(function() {
|
||||
fetch('/teams/search-users?q=' + encodeURIComponent(q), {
|
||||
headers: { 'X-CSRF-Token': getCsrfToken() }
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
const dropdown = document.getElementById('userDropdown');
|
||||
dropdown.innerHTML = '';
|
||||
if (data.users && data.users.length > 0) {
|
||||
data.users.forEach(function(u) {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'autocomplete-item';
|
||||
item.innerHTML = '<strong>' + escapeHtml(u.username) + '</strong> <small>' + escapeHtml(u.email) + '</small>';
|
||||
item.dataset.id = u.id;
|
||||
item.dataset.username = u.username;
|
||||
item.addEventListener('click', function() {
|
||||
selectUser(u.id, u.username);
|
||||
});
|
||||
dropdown.appendChild(item);
|
||||
});
|
||||
dropdown.classList.add('active');
|
||||
} else {
|
||||
dropdown.innerHTML = '<div class="autocomplete-empty">No users found</div>';
|
||||
dropdown.classList.add('active');
|
||||
}
|
||||
})
|
||||
.catch(function() {
|
||||
document.getElementById('userDropdown').classList.remove('active');
|
||||
});
|
||||
}, 300);
|
||||
});
|
||||
|
||||
document.addEventListener('click', function(e) {
|
||||
if (!e.target.closest('.autocomplete')) {
|
||||
document.getElementById('userDropdown').classList.remove('active');
|
||||
}
|
||||
});
|
||||
|
||||
function selectUser(id, username) {
|
||||
selectedUserId = id;
|
||||
document.getElementById('addUserInput').value = username;
|
||||
document.getElementById('userDropdown').classList.remove('active');
|
||||
|
||||
const form = new FormData();
|
||||
form.append('user_id', id);
|
||||
form.append('_csrf_token', getCsrfToken());
|
||||
|
||||
fetch('/teams/' + teamId + '/add-user', {
|
||||
method: 'POST',
|
||||
headers: { 'X-CSRF-Token': getCsrfToken() },
|
||||
body: form,
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(res => {
|
||||
if (res.success) {
|
||||
showToast('success', username + ' added to team');
|
||||
location.reload();
|
||||
} else {
|
||||
showToast('error', res.message);
|
||||
document.getElementById('addUserInput').value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function removeMember(userId, username) {
|
||||
if (!confirm('Remove "' + username + '" from this team?')) return;
|
||||
|
||||
const form = new FormData();
|
||||
form.append('user_id', userId);
|
||||
form.append('_csrf_token', getCsrfToken());
|
||||
|
||||
fetch('/teams/' + teamId + '/remove-user', {
|
||||
method: 'POST',
|
||||
headers: { 'X-CSRF-Token': getCsrfToken() },
|
||||
body: form,
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(res => {
|
||||
if (res.success) { showToast('success', res.message); location.reload(); }
|
||||
else { showToast('error', res.message); }
|
||||
});
|
||||
}
|
||||
|
||||
function addServer(e) {
|
||||
e.preventDefault();
|
||||
const serverId = document.getElementById('addServerId').value;
|
||||
const role = document.getElementById('addServerRole').value;
|
||||
if (!serverId) return;
|
||||
|
||||
const form = new FormData();
|
||||
form.append('server_id', serverId);
|
||||
form.append('role', role);
|
||||
form.append('_csrf_token', getCsrfToken());
|
||||
|
||||
fetch('/teams/' + teamId + '/add-server', {
|
||||
method: 'POST',
|
||||
headers: { 'X-CSRF-Token': getCsrfToken() },
|
||||
body: form,
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(res => {
|
||||
if (res.success) { showToast('success', res.message); location.reload(); }
|
||||
else { showToast('error', res.message); }
|
||||
});
|
||||
}
|
||||
|
||||
function removeServer(serverId, name) {
|
||||
if (!confirm('Remove "' + name + '" from this team?')) return;
|
||||
|
||||
const form = new FormData();
|
||||
form.append('server_id', serverId);
|
||||
form.append('_csrf_token', getCsrfToken());
|
||||
|
||||
fetch('/teams/' + teamId + '/remove-server', {
|
||||
method: 'POST',
|
||||
headers: { 'X-CSRF-Token': getCsrfToken() },
|
||||
body: form,
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(res => {
|
||||
if (res.success) { showToast('success', res.message); location.reload(); }
|
||||
else { showToast('error', res.message); }
|
||||
});
|
||||
}
|
||||
|
||||
function updateServerRole(serverId, role) {
|
||||
const form = new FormData();
|
||||
form.append('server_id', serverId);
|
||||
form.append('role', role);
|
||||
form.append('_csrf_token', getCsrfToken());
|
||||
|
||||
fetch('/teams/' + teamId + '/update-server-role', {
|
||||
method: 'POST',
|
||||
headers: { 'X-CSRF-Token': getCsrfToken() },
|
||||
body: form,
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(res => {
|
||||
if (res.success) { showToast('success', 'Role updated to ' + role); }
|
||||
else { showToast('error', res.message); }
|
||||
});
|
||||
}
|
||||
110
public/assets/js/terminal.js
Normal file
110
public/assets/js/terminal.js
Normal file
@@ -0,0 +1,110 @@
|
||||
function getCsrfToken() {
|
||||
return document.querySelector('meta[name="csrf-token"]')?.content || '';
|
||||
}
|
||||
|
||||
const terminalInput = document.getElementById('terminalInput');
|
||||
const terminalOutput = document.getElementById('terminalOutput');
|
||||
const connectionStatus = document.getElementById('connectionStatus');
|
||||
|
||||
terminalInput.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
executeCommand();
|
||||
}
|
||||
});
|
||||
|
||||
function executeCommand() {
|
||||
const command = terminalInput.value.trim();
|
||||
if (!command) return;
|
||||
|
||||
appendTerminalLine(command, 'command');
|
||||
|
||||
terminalInput.value = '';
|
||||
terminalInput.disabled = true;
|
||||
document.getElementById('executeBtn').disabled = true;
|
||||
|
||||
updateStatus('executing', 'Executing...');
|
||||
|
||||
fetch('/terminal/' + serverId + '/execute', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'X-CSRF-Token': getCsrfToken(),
|
||||
},
|
||||
body: 'command=' + encodeURIComponent(command) + '&_csrf_token=' + encodeURIComponent(getCsrfToken()),
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
terminalInput.disabled = false;
|
||||
document.getElementById('executeBtn').disabled = false;
|
||||
terminalInput.focus();
|
||||
|
||||
if (data.success) {
|
||||
if (data.output) {
|
||||
appendTerminalLine(data.output, 'output');
|
||||
}
|
||||
if (data.stderr) {
|
||||
appendTerminalLine(data.stderr, 'error');
|
||||
}
|
||||
updateStatus('connected', 'Connected');
|
||||
} else {
|
||||
appendTerminalLine(data.message || 'Command failed', 'error');
|
||||
updateStatus('error', 'Error');
|
||||
}
|
||||
|
||||
const terminalBody = document.getElementById('terminalOutput');
|
||||
terminalBody.scrollTop = terminalBody.scrollHeight;
|
||||
})
|
||||
.catch(err => {
|
||||
terminalInput.disabled = false;
|
||||
document.getElementById('executeBtn').disabled = false;
|
||||
appendTerminalLine('Connection error: ' + err.message, 'error');
|
||||
updateStatus('error', 'Error');
|
||||
});
|
||||
}
|
||||
|
||||
function appendTerminalLine(text, type) {
|
||||
const line = document.createElement('div');
|
||||
line.className = 'terminal-line terminal-' + type;
|
||||
if (type === 'command') {
|
||||
line.innerHTML = '<span class="terminal-prompt-inline">$</span> ' + escapeHtml(text);
|
||||
} else {
|
||||
line.innerHTML = '<pre>' + escapeHtml(text) + '</pre>';
|
||||
}
|
||||
terminalOutput.appendChild(line);
|
||||
terminalOutput.scrollTop = terminalOutput.scrollHeight;
|
||||
}
|
||||
|
||||
function runQuickCmd(cmd) {
|
||||
terminalInput.value = cmd;
|
||||
executeCommand();
|
||||
}
|
||||
|
||||
function clearTerminal() {
|
||||
terminalOutput.innerHTML = '';
|
||||
}
|
||||
|
||||
function clearHistory() {
|
||||
if (!confirm('Clear all command history for this server?')) return;
|
||||
|
||||
fetch('/terminal/' + serverId + '/clear-history', {
|
||||
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);
|
||||
if (data.success) location.reload();
|
||||
});
|
||||
}
|
||||
|
||||
function updateStatus(status, text) {
|
||||
const dot = connectionStatus.querySelector('.status-dot');
|
||||
dot.className = 'status-dot ' + status;
|
||||
connectionStatus.lastChild.textContent = ' ' + text;
|
||||
}
|
||||
|
||||
terminalInput.focus();
|
||||
Reference in New Issue
Block a user