📜
connectionmanager.js
Back
📝 Javascript ⚡ Executable Ctrl+S: Save • Ctrl+R: Run • Ctrl+F: Find
(function () { window.AppItems = window.AppItems || []; const section = { title: "Connections", html: ` <div class="conn-container"> <div class="conn-header"> <h2 style="margin:0;">🔐 SFTP Connections</h2> <button class="conn-btn conn-btn-primary" onclick="openConnectionModal()">+ New Connection</button> </div> <div class="connections-grid" id="connectionsGrid"> <!-- Connections will be rendered here --> </div> </div> <!-- Add/Edit Connection Modal --> <div class="conn-modal" id="connectionModal" aria-hidden="true"> <div class="conn-modal__backdrop" onclick="closeConnectionModal()"></div> <div class="conn-modal__dialog"> <div class="conn-modal__header"> <h3 id="modalTitle">New Connection</h3> <button class="conn-close-btn" onclick="closeConnectionModal()">×</button> </div> <form id="connectionForm"> <input type="hidden" id="connectionId"> <div class="conn-form-group"> <label class="conn-label">Connection Name</label> <input type="text" class="conn-input" id="connName" placeholder="My Server" required> </div> <div class="conn-form-group"> <label class="conn-label">Host</label> <input type="text" class="conn-input" id="connHost" placeholder="files.devbrewing.com" required> </div> <div class="conn-form-group"> <label class="conn-label">Port</label> <input type="number" class="conn-input" id="connPort" value="22" required> </div> <div class="conn-form-group"> <label class="conn-label">Username</label> <input type="text" class="conn-input" id="connUser" required> </div> <div class="conn-form-group"> <label class="conn-label">Password</label> <div class="conn-pw-wrap"> <input type="password" class="conn-input" id="connPass" required> <button type="button" class="conn-pw-toggle" onclick="toggleConnPassword()">👁️</button> </div> </div> <div class="conn-form-actions"> <button type="submit" class="conn-btn conn-btn-primary">Save Connection</button> <button type="button" class="conn-btn conn-btn-secondary" onclick="closeConnectionModal()">Cancel</button> </div> <div id="formMessage"></div> </form> </div> </div> <!-- Toast Container --> <div class="conn-toast-container" id="connToastContainer"></div> <style> .conn-container { display: flex; flex-direction: column; height: 100%; padding: 1rem; } .conn-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem; } .connections-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 1rem; } /* Connection Card */ .conn-card { position: relative; background: rgba(30, 41, 59, 0.7); border: 2px solid #2a3648; border-radius: 12px; padding: 1rem; cursor: pointer; transition: all 0.2s ease; } .conn-card:hover { border-color: #3b82f6; transform: translateY(-2px); box-shadow: 0 8px 16px rgba(59, 130, 246, 0.2); } .conn-card.active { border-color: #10b981; background: linear-gradient(135deg, rgba(16, 185, 129, 0.15), rgba(16, 185, 129, 0.05)); } .conn-card.connecting { border-color: #f59e0b; } /* Status Bar */ .conn-status-bar { position: absolute; top: 0; left: 0; right: 0; height: 4px; border-radius: 12px 12px 0 0; background: #2a3648; } .conn-card.active .conn-status-bar { background: linear-gradient(90deg, #10b981, #34d399); } .conn-card.connecting .conn-status-bar { background: linear-gradient(90deg, #f59e0b, #fbbf24); animation: connPulse 1.5s ease-in-out infinite; } @keyframes connPulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } } /* Card Content */ .conn-card-header { display: flex; justify-content: space-between; align-items: start; margin-top: 8px; margin-bottom: 12px; } .conn-card-title { font-size: 18px; font-weight: 700; color: #e6edf3; } .conn-card-actions { display: flex; gap: 4px; } .conn-icon-btn { background: transparent; border: none; color: #9aa4b2; cursor: pointer; padding: 4px 8px; border-radius: 6px; font-size: 16px; transition: all 0.15s; } .conn-icon-btn:hover { background: #263244; color: #e6edf3; } .conn-card-info { font-size: 14px; color: #9aa4b2; line-height: 1.6; } .conn-info-row { display: flex; gap: 8px; margin-bottom: 4px; } .conn-info-label { font-weight: 600; min-width: 60px; } .conn-status-badge { display: inline-block; padding: 4px 10px; border-radius: 12px; font-size: 12px; font-weight: 600; margin-top: 8px; } .conn-status-badge.active { background: rgba(16, 185, 129, 0.2); color: #10b981; } .conn-status-badge.inactive { background: rgba(148, 163, 184, 0.2); color: #94a3b8; } /* Add Connection Card */ .conn-add-card { background: transparent; border: 2px dashed #2a3648; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px; min-height: 200px; color: #9aa4b2; font-weight: 600; } .conn-add-card:hover { border-color: #3b82f6; color: #3b82f6; } .conn-add-icon { font-size: 32px; line-height: 1; } /* Buttons */ .conn-btn { padding: 10px 20px; border: none; border-radius: 8px; font-weight: 600; cursor: pointer; transition: all 0.15s; font-size: 14px; } .conn-btn-primary { background: linear-gradient(135deg, #3b82f6, #9333ea); color: white; } .conn-btn-primary:hover { opacity: 0.9; } .conn-btn-secondary { background: #2a3648; color: #e6edf3; } /* Modal */ .conn-modal { display: none; position: fixed; inset: 0; z-index: 99999; align-items: center; justify-content: center; } .conn-modal[aria-hidden="false"] { display: flex; } .conn-modal__backdrop { position: absolute; inset: 0; background: rgba(0, 0, 0, 0.7); backdrop-filter: blur(4px); } .conn-modal__dialog { position: relative; background: #1a2332; border: 1px solid #2a3648; border-radius: 16px; padding: 24px; max-width: 480px; width: 90%; max-height: 90vh; overflow-y: auto; z-index: 1; } .conn-modal__header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; } .conn-modal__header h3 { margin: 0; font-size: 20px; font-weight: 700; } .conn-close-btn { background: transparent; border: none; color: #9aa4b2; font-size: 28px; cursor: pointer; padding: 0; width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; border-radius: 6px; line-height: 1; } .conn-close-btn:hover { background: #263244; color: #e6edf3; } /* Form */ .conn-form-group { margin-bottom: 16px; } .conn-label { display: block; margin-bottom: 6px; font-weight: 600; font-size: 14px; } .conn-input { width: 100%; padding: 10px 12px; background: #0f1725; border: 1px solid #2a3648; border-radius: 8px; color: #e6edf3; font-size: 14px; } .conn-input:focus { outline: none; border-color: #3b82f6; } .conn-pw-wrap { position: relative; display: flex; align-items: center; } .conn-pw-toggle { position: absolute; right: 8px; background: transparent; border: none; color: #9aa4b2; cursor: pointer; padding: 6px; border-radius: 6px; } .conn-pw-toggle:hover { background: #263244; } .conn-form-actions { display: flex; gap: 8px; margin-top: 24px; } /* Toast */ .conn-toast-container { position: fixed; right: 16px; bottom: 16px; z-index: 999999; display: flex; flex-direction: column; gap: 8px; } .conn-toast { background: rgba(15, 23, 42, 0.98); border: 1px solid rgba(71, 85, 105, 0.5); color: #e2e8f0; padding: 10px 14px; border-radius: 10px; box-shadow: 0 8px 20px rgba(0, 0, 0, 0.35); font-size: 0.9rem; opacity: 0; transform: translateY(8px); animation: connToastIn 200ms ease forwards; } .conn-toast.success { border-color: rgba(16, 185, 129, 0.6); } .conn-toast.error { border-color: rgba(239, 68, 68, 0.6); } @keyframes connToastIn { to { opacity: 1; transform: translateY(0); } } @keyframes connToastOut { to { opacity: 0; transform: translateY(8px); } } </style> ` }; window.AppItems.push(section); // Storage key const STORAGE_KEY = 'sftp_connections'; // Global functions for modal window.openConnectionModal = function(connectionId = null) { const modal = document.getElementById('connectionModal'); const form = document.getElementById('connectionForm'); const title = document.getElementById('modalTitle'); form.reset(); document.getElementById('formMessage').innerHTML = ''; if (connectionId) { const connections = getConnections(); const conn = connections.find(c => c.id === connectionId); if (conn) { title.textContent = 'Edit Connection'; document.getElementById('connectionId').value = conn.id; document.getElementById('connName').value = conn.name; document.getElementById('connHost').value = conn.host; document.getElementById('connPort').value = conn.port; document.getElementById('connUser').value = conn.username; document.getElementById('connPass').value = conn.password; } } else { title.textContent = 'New Connection'; document.getElementById('connectionId').value = ''; } modal.setAttribute('aria-hidden', 'false'); }; window.closeConnectionModal = function() { document.getElementById('connectionModal').setAttribute('aria-hidden', 'true'); }; window.toggleConnPassword = function() { const input = document.getElementById('connPass'); const btn = event.target; input.type = input.type === 'password' ? 'text' : 'password'; btn.textContent = input.type === 'password' ? '👁️' : '🙈'; }; // Get connections from localStorage function getConnections() { const data = localStorage.getItem(STORAGE_KEY); return data ? JSON.parse(data) : []; } // Save connections to localStorage function saveConnections(connections) { localStorage.setItem(STORAGE_KEY, JSON.stringify(connections)); } // Show toast notification function showToast(message, type = 'success', timeout = 2500) { const container = document.getElementById('connToastContainer'); if (!container) return; const toast = document.createElement('div'); toast.className = `conn-toast ${type}`; toast.textContent = message; container.appendChild(toast); setTimeout(() => { toast.style.animation = 'connToastOut 160ms ease forwards'; setTimeout(() => container.removeChild(toast), 170); }, timeout); } // Render connections grid function renderConnections() { const grid = document.getElementById('connectionsGrid'); if (!grid) return; const connections = getConnections(); grid.innerHTML = ''; // Render each connection connections.forEach(conn => { const card = document.createElement('div'); card.className = `conn-card ${conn.active ? 'active' : ''}`; card.innerHTML = ` <div class="conn-status-bar"></div> <div class="conn-card-header"> <div class="conn-card-title">${escapeHtml(conn.name)}</div> <div class="conn-card-actions"> <button class="conn-icon-btn" onclick="editConnection('${conn.id}')" title="Edit">✏️</button> <button class="conn-icon-btn" onclick="deleteConnection('${conn.id}')" title="Delete">🗑️</button> </div> </div> <div class="conn-card-info"> <div class="conn-info-row"> <span class="conn-info-label">Host:</span> <span>${escapeHtml(conn.host)}</span> </div> <div class="conn-info-row"> <span class="conn-info-label">User:</span> <span>${escapeHtml(conn.username)}</span> </div> <div class="conn-info-row"> <span class="conn-info-label">Port:</span> <span>${conn.port}</span> </div> </div> <span class="conn-status-badge ${conn.active ? 'active' : 'inactive'}"> ${conn.active ? '🟢 Connected' : '⚫ Disconnected'} </span> `; // Click to toggle connection card.addEventListener('click', (e) => { if (!e.target.closest('.conn-icon-btn')) { if (conn.active) { // Already connected - disconnect disconnectConnection(conn.id); } else { // Not connected - connect connectToServer(conn.id, card); } } }); grid.appendChild(card); }); // Add "New Connection" card const addCard = document.createElement('div'); addCard.className = 'conn-card conn-add-card'; addCard.innerHTML = ` <div class="conn-add-icon">+</div> <div>Add Connection</div> `; addCard.addEventListener('click', () => openConnectionModal()); grid.appendChild(addCard); } // Edit connection window.editConnection = function(id) { openConnectionModal(id); }; // Delete connection window.deleteConnection = async function(id) { if (!confirm('Are you sure you want to delete this connection?')) return; const connections = getConnections(); const deleted = connections.find(c => c.id === id); // If this was the active connection, disconnect from server if (deleted && deleted.active) { await disconnectFromServer(); } const filtered = connections.filter(c => c.id !== id); saveConnections(filtered); renderConnections(); showToast('Connection deleted', 'success'); }; // Disconnect specific connection window.disconnectConnection = async function(id) { try { // Call server to disconnect await fetch('SFTPconnector.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'disconnect' }) }); // Update local state const connections = getConnections(); const conn = connections.find(c => c.id === id); if (conn) { conn.active = false; saveConnections(connections); renderConnections(); showToast(`Disconnected from ${conn.name}`, 'success'); } } catch (err) { showToast('Disconnect failed: ' + err.message, 'error'); } }; // Connect to server async function connectToServer(id, card) { const connections = getConnections(); const conn = connections.find(c => c.id === id); if (!conn) return; // Mark as connecting card.classList.add('connecting'); try { // First disconnect any active connection const activeConn = connections.find(c => c.active); if (activeConn && activeConn.id !== id) { await disconnectFromServer(); } // Connect to new server const res = await fetch('SFTPconnector.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'connect', host: conn.host, port: conn.port, username: conn.username, password: conn.password }) }); // Check if response is JSON const contentType = res.headers.get('content-type'); if (!contentType || !contentType.includes('application/json')) { const text = await res.text(); console.error('Non-JSON response:', text); throw new Error('Server returned invalid response. Check console for details.'); } const data = await res.json(); console.log('Connection response:', data); if (data.success) { // Update active status connections.forEach(c => c.active = false); conn.active = true; saveConnections(connections); renderConnections(); showToast(`✅ Connected to ${conn.name}`, 'success'); } else { throw new Error(data.message); } } catch (err) { showToast(`❌ Connection failed: ${err.message}`, 'error'); card.classList.remove('connecting'); } } // Disconnect from server async function disconnectFromServer() { try { await fetch('SFTPconnector.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'disconnect' }) }); const connections = getConnections(); connections.forEach(c => c.active = false); saveConnections(connections); } catch (err) { console.error('Disconnect error:', err); } } // Escape HTML function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // Check server status on overlay open document.addEventListener('click', async (e) => { const btn = e.target.closest('.chip'); if (btn && btn.textContent.includes('Connections')) { // Wait for overlay to render setTimeout(async () => { renderConnections(); // Check server status and sync try { const res = await fetch('SFTPconnector.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'status' }) }); const data = await res.json(); const connections = getConnections(); const hasActiveLocal = connections.some(c => c.active); // If server says disconnected but local says connected, sync it if (!data.data?.connected && hasActiveLocal) { connections.forEach(c => c.active = false); saveConnections(connections); renderConnections(); } } catch (err) { console.error('Status check failed:', err); } }, 100); } }); // Form submission document.addEventListener('submit', (e) => { if (e.target.id === 'connectionForm') { e.preventDefault(); const connections = getConnections(); const id = document.getElementById('connectionId').value || Date.now().toString(); const name = document.getElementById('connName').value; const host = document.getElementById('connHost').value; const port = parseInt(document.getElementById('connPort').value); const username = document.getElementById('connUser').value; const password = document.getElementById('connPass').value; const existingIndex = connections.findIndex(c => c.id === id); const connection = { id, name, host, port, username, password, active: existingIndex >= 0 ? connections[existingIndex].active : false }; if (existingIndex >= 0) { connections[existingIndex] = connection; showToast('Connection updated', 'success'); } else { connections.push(connection); showToast('Connection saved', 'success'); } saveConnections(connections); renderConnections(); closeConnectionModal(); } }); // Close modal on escape document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { const modal = document.getElementById('connectionModal'); if (modal && modal.getAttribute('aria-hidden') === 'false') { closeConnectionModal(); } } }); console.log('[connectionmanager.js] Loaded - manages multiple SFTP connections'); })();