(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');
})();