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('' + i + ''); } 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(/#(.*)$/gm, '#$1') .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, '$1') .replace(/\b(\d+)\b/g, '$1') .replace(/\b(https?|fastcgi|unix):\/\/[^\s;{]+/g, '$&') .replace(/'(.*?)'/g, '\'$1\'') .replace(/(\$\w+)/g, '$1') .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 '' + m + ''; } 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 = ' 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 = ' 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 = ' 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(); } });