`;
list.appendChild(item);
});
}
async function refreshHostStatuses() {
if (!authToken || connections.length === 0) return;
connections.forEach(conn => { conn.hostStatus = null; });
renderConnectionList();
try {
const response = await fetch(`${apiBaseUrl}/api/connections/status`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`
},
body: JSON.stringify({ ids: connections.map(conn => conn.id) })
});
if (!response.ok) return;
const data = await response.json();
connections.forEach(conn => {
if (Object.prototype.hasOwnProperty.call(data.statuses || {}, conn.id)) {
conn.hostStatus = data.statuses[conn.id];
}
});
renderConnectionList();
} catch (e) {
console.warn('Failed to refresh host statuses:', e);
}
}
function startHostStatusPolling() {
if (hostStatusTimer) clearInterval(hostStatusTimer);
hostStatusTimer = setInterval(refreshHostStatuses, 30000);
}
function selectConnection(id) {
currentConnectionId = id;
const conn = getCurrentConnection();
if (conn) {
const quickConnectFields = {
connName: conn.name,
host: conn.host,
port: conn.port,
user: conn.username,
authType: conn.authType || 'password',
password: conn.password || '',
sshKey: conn.key || '',
lockTimeout: conn.lockTimeout || 15,
disconnectTimeout: conn.disconnectTimeout || 60
};
Object.entries(quickConnectFields).forEach(([fieldId, value]) => {
const field = document.getElementById(fieldId);
if (field) field.value = value;
});
if (document.getElementById('authType')) toggleAuthFields();
document.getElementById('terminalTitle').textContent = `${conn.name} - ${conn.username}@${conn.host}:${conn.port}`;
updateTerminalStatus(conn.connected ? 'connected' : 'disconnected');
// Restore terminal buffer for this connection
term.clear();
conn.terminalBuffer.forEach(line => term.writeln(line));
}
renderConnectionList();
}
function appendToBuffer(connId, data) {
const conn = getConnection(connId);
if (conn) {
const lines = data.split('\n');
lines.forEach(line => conn.terminalBuffer.push(line));
// Keep only last 1000 lines
if (conn.terminalBuffer.length > 1000) {
conn.terminalBuffer = conn.terminalBuffer.slice(-1000);
}
}
}
function saveConnectionSettings() {
const conn = getCurrentConnection();
if (!conn) { alert('No connection selected'); return; }
showEditConnectionModal(conn.id);
}
function saveEditedConnection() {
const conn = getConnection(currentConnectionId);
if (!conn) { alert('No connection selected'); return; }
const lockTimeout = parseInt(document.getElementById('editModalLockTimeout').value) || 15;
const disconnectTimeout = parseInt(document.getElementById('editModalDisconnectTimeout').value) || 60;
if (disconnectTimeout !== 0 && disconnectTimeout <= lockTimeout) {
alert(`Disconnect timeout (${disconnectTimeout} min) must be greater than Lock timeout (${lockTimeout} min).`);
return;
}
conn.name = document.getElementById('editModalName').value || 'New Connection';
conn.host = document.getElementById('editModalHost').value;
conn.port = parseInt(document.getElementById('editModalPort').value) || 22;
conn.username = document.getElementById('editModalUser').value;
conn.authType = document.getElementById('editModalAuthType').value;
conn.password = document.getElementById('editModalPassword').value;
conn.key = document.getElementById('editModalKey').value;
conn.lockTimeout = lockTimeout;
conn.disconnectTimeout = disconnectTimeout;
if (!conn.host || !conn.username) { alert('Please enter host and username'); return; }
if (conn.authType === 'password' && !conn.password) { alert('Please enter password'); return; }
if (conn.authType === 'key' && !conn.key) { alert('Please paste SSH private key'); return; }
renderConnectionList();
selectConnection(conn.id);
document.getElementById('terminalTitle').textContent = `${conn.name} - ${conn.username}@${conn.host}:${conn.port}`;
term.writeln('[Settings saved]');
autoSaveConnections();
saveConnectionToServer(conn);
hideEditConnectionModal();
}
function updateTerminalStatus(status) {
const el = document.getElementById('terminalStatus');
el.className = `terminal-status ${status}`;
el.textContent = status.charAt(0).toUpperCase() + status.slice(1);
}
function connect() {
const conn = getCurrentConnection();
if (conn) {
connectConnection(conn.id);
} else {
// Quick connect - create connection from current form data
const name = document.getElementById('connName').value || 'Quick Connect';
const host = document.getElementById('host').value;
const port = parseInt(document.getElementById('port').value) || 22;
const username = document.getElementById('user').value;
const authType = document.getElementById('authType').value;
const password = document.getElementById('password').value;
const key = document.getElementById('sshKey').value;
if (!host || !username) {
alert('Please enter host and username, or select/create a connection first');
return;
}
if (authType === 'password' && !password) {
alert('Please enter password');
return;
}
if (authType === 'key' && !key) {
alert('Please paste SSH private key');
return;
}
// Create temporary connection
const newConn = {
id: generateId(),
name: name,
host: host,
port: port,
username: username,
authType: authType,
password: password,
key: key,
connected: false,
socket: null,
terminalBuffer: []
};
connections.push(newConn);
renderConnectionList();
autoSaveConnections();
saveConnectionToServer(newConn); // Save to server
selectConnection(newConn.id);
connectConnection(newConn.id);
}
}
async function connectConnection(id) {
const conn = getConnection(id);
if (!conn) { alert('Connection not found'); return; }
// Don't disconnect other connections - allow multiple!
if (id !== currentConnectionId) {
selectConnection(id);
}
if (conn.socket && conn.socket.readyState === WebSocket.OPEN) {
term.writeln('[Already connected]');
return;
}
term.writeln(`Connecting to ${conn.name} (${conn.username}@${conn.host}:${conn.port})...`);
updateTerminalStatus('connecting');
conn.loadAverage = null;
renderConnectionList();
try {
if (!backendWsPort) {
await loadConfig();
}
const wsUrl = getWsUrl();
const wsUrlWithToken = wsUrl + '?token=' + encodeURIComponent(authToken || '');
conn.socket = new WebSocket(wsUrlWithToken);
conn.socket.onopen = () => {
// Generate session ID for recording
const sessionId = generateSessionId();
conn.sessionId = sessionId;
const authData = {
host: conn.host, port: conn.port, username: conn.username,
password: conn.authType === 'password' ? conn.password : undefined,
key: conn.authType === 'key' ? conn.key : undefined,
session_id: sessionId,
connection_id: conn.id,
connection_name: conn.name, // Connection name for display
cols: term.cols,
rows: term.rows
};
conn.socket.send(JSON.stringify(authData));
};
conn.socket.onmessage = (event) => {
try {
const msg = JSON.parse(event.data);
if (msg.type === 'connected') {
conn.connected = true;
if (conn.id === currentConnectionId) {
updateTerminalStatus('connected');
term.writeln('[Connected]');
sendResizeForConnection(conn);
}
renderConnectionList();
// Start inactivity timers for auto-lock
resetInactivityTimers();
return;
}
if (msg.type === 'load_average') {
conn.loadAverage = msg.value || null;
renderConnectionList();
return;
}
if (msg.type === 'error') {
if (conn.id === currentConnectionId) {
term.writeln(`[ERROR] ${msg.message}`);
updateTerminalStatus('disconnected');
}
conn.connected = false;
conn.loadAverage = null;
renderConnectionList();
return;
}
} catch (e) {
// Not JSON, it's terminal data
if (conn.id === currentConnectionId) {
term.write(event.data);
}
appendToBuffer(conn.id, event.data);
}
};
conn.socket.onerror = () => {
if (conn.id === currentConnectionId) {
term.writeln('[ERROR] WebSocket connection failed');
updateTerminalStatus('disconnected');
}
conn.connected = false;
conn.loadAverage = null;
renderConnectionList();
};
conn.socket.onclose = (event) => {
conn.connected = false;
conn.loadAverage = null;
conn.socket = null;
if (conn.id === currentConnectionId) {
term.writeln(`[DISCONNECTED] Code: ${event.code}`);
updateTerminalStatus('disconnected');
}
renderConnectionList();
};
} catch (e) {
if (conn.id === currentConnectionId) {
term.writeln(`[ERROR] Failed to create WebSocket: ${e.message}`);
updateTerminalStatus('disconnected');
}
}
}
function disconnect() {
const conn = getCurrentConnection();
if (conn && conn.socket) {
conn.socket.close();
}
}
function disconnectConnection(id) {
const conn = getConnection(id);
if (conn && conn.socket) {
conn.socket.close();
}
}
async function deleteConnection(id) {
if (!confirm('Delete this connection?')) return;
const conn = getConnection(id);
if (!await deleteConnectionFromServer(id)) {
alert('Failed to delete connection from server. Please try again.');
return;
}
if (conn && conn.socket) conn.socket.close();
connections = connections.filter(c => c.id !== id);
autoSaveConnections();
if (currentConnectionId === id) {
currentConnectionId = null;
term.clear();
document.getElementById('terminalTitle').textContent = 'Terminal';
updateTerminalStatus('disconnected');
['connName','host','port','user','password','sshKey'].forEach(id => document.getElementById(id).value = '');
document.getElementById('port').value = '22';
}
renderConnectionList();
}
function sendResizeForCurrent() {
const conn = getCurrentConnection();
if (conn) sendResizeForConnection(conn);
}
function sendResizeForConnection(conn) {
if (conn && conn.socket && conn.connected) {
conn.socket.send(JSON.stringify({ type: 'resize', cols: term.cols, rows: term.rows }));
}
}
// Initialize
toggleAuthFields();
term.writeln('WebSSH Multi-Connection Client');
term.writeln('Click "+ New Connection" to get started');
// ============================================
// SFTP File Manager Functions
// Global variables for SFTP
let sftpSocket = null;
let sftpCurrentPath = '.';
let sftpSelectedFile = null;
let sftpSelectedFileIsDir = false;
let sftpUploadQueue = [];
let currentSftpConnection = null;
// Load configuration from API
function loadConfig() {
console.log('loadConfig() called, token exists:', !!localStorage.getItem('webssh_token'));
console.log('apiBaseUrl:', apiBaseUrl);
fetch(`${apiBaseUrl}/api/config`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${localStorage.getItem('webssh_token')}`
}
})
.then(response => {
console.log('loadConfig() response status:', response.status);
if (response.ok) {
return response.json();
} else {
console.log('Failed to load config, response:', response.status);
return null;
}
})
.then(config => {
console.log('loadConfig() config:', config);
if (config && config.config) {
// Load max upload size
if (config.config.max_upload_size) {
maxUploadSize = config.config.max_upload_size;
localStorage.setItem('maxUploadSize', maxUploadSize);
console.log(`Loaded max upload size: ${formatBytes(maxUploadSize)} (${maxUploadSize} bytes)`);
}
if (config.config.ws_port) {
backendWsPort = config.config.ws_port;
backendWsHost = config.config.ws_host || window.location.hostname || 'localhost';
console.log(`Loaded backend WebSocket port: ${backendWsPort}`);
}
// Load stored users for lock screen verification
if (config.config.stored_users) {
storedUsers = config.config.stored_users;
console.log('Loaded stored users for lock screen');
}
// Load current user
if (config.config.current_user) {
currentUser = config.config.current_user;
}
} else {
console.log('No config data found');
}
})
.catch(error => {
console.warn('Failed to load config:', error);
});
// Log current maxUploadSize for debugging
console.log('Current maxUploadSize: ' + maxUploadSize + ' bytes (' + (maxUploadSize / 1024 / 1024).toFixed(1) + 'MB)');
}
// Load config when page loads
document.addEventListener('DOMContentLoaded', function() {
// Wait a bit for token to be available
setTimeout(loadConfig, 1000);
});
// Local file system variables
let localCurrentPath = [];
let localSelectedFile = null;
let localCurrentItems = []; // Store current directory items
let localDirectoryHandle = null; // Added missing variable
async function sftpChooseLocalFolder() {
try {
if (window.showDirectoryPicker) {
const dirHandle = await window.showDirectoryPicker();
const structure = await sftpBuildDirectoryStructureFromHandle(dirHandle);
localDirectoryHandle = {
type: 'fs_access',
rootHandle: dirHandle,
structure: structure,
currentPath: []
};
const pathInput = document.getElementById('sftpLocalPathInput');
if (pathInput) {
pathInput.value = structure.name;
}
sftpRenderLocalDirectory(structure, []);
return;
}
} catch (e) {
console.error('Failed to pick directory via showDirectoryPicker:', e);
}
// Fallback: use webkitdirectory input (still requires user selection)
sftpBrowseLocalFolder();
}
async function sftpBuildDirectoryStructureFromHandle(dirHandle) {
const root = { name: dirHandle.name || 'Local', type: 'directory', children: {}, handle: dirHandle };
for await (const entry of dirHandle.values()) {
if (entry.kind === 'directory') {
root.children[entry.name] = await sftpBuildDirectoryStructureFromHandle(entry);
} else {
root.children[entry.name] = {
name: entry.name,
type: 'file',
handle: entry
};
}
}
return root;
}
// Local File Browser Functions - Cross-browser compatible
function sftpBrowseLocalFolder() {
const input = document.createElement('input');
input.type = 'file';
// Fallback for browsers without showDirectoryPicker (e.g. Firefox)
// Try multiple directory-picking attributes for broader compatibility.
input.setAttribute('webkitdirectory', '');
input.setAttribute('directory', '');
input.setAttribute('mozdirectory', '');
input.multiple = true;
input.onchange = (e) => {
if (e.target.files.length > 0) {
const files = Array.from(e.target.files);
if (files[0].webkitRelativePath) {
const structure = buildDirectoryStructure(files);
sftpShowLocalDirectoryStructure(structure, files);
} else {
sftpShowLocalFilesFlat(files);
}
}
};
input.click();
}
async function sftpShowLocalFilesFlat(files) {
const container = document.getElementById('sftpLocalFiles');
if (!container) return;
// Store for later use
localDirectoryHandle = {
type: 'flat',
allFiles: files
};
// Update path
const pathInput = document.getElementById('sftpLocalPathInput');
if (pathInput) {
pathInput.value = 'Local Files';
}
// Column headers
let html = `
FilenameSizeDate
`;
files.forEach(file => {
const icon = getFileIcon(file.name);
const size = formatBytes(file.size || 0);
const date = file.lastModified ? new Date(file.lastModified).toLocaleString() : '';
html += `
${icon}${escapeHtml(file.name)}${size}${date}
`;
});
container.innerHTML = html;
}
function sftpSelectLocalFlatFile(element, name) {
localSelectedFile = name;
document.querySelectorAll('#sftpLocalFiles .sftp-file-item').forEach(el => el.classList.remove('selected'));
element.classList.add('selected');
}
function sftpHandleLocalFolderDragOver(e) {
e.preventDefault();
e.stopPropagation();
e.dataTransfer.dropEffect = 'copy';
document.getElementById('sftpLocalFiles').classList.add('drag-over');
}
async function sftpHandleLocalFolderDrop(e) {
e.preventDefault();
e.stopPropagation();
const items = e.dataTransfer.items;
if (!items || items.length === 0) return;
// Check if dropped item is a folder
const item = items[0];
if (item.kind === 'file') {
const entry = item.webkitGetAsEntry ? item.webkitGetAsEntry() : null;
if (entry && entry.isDirectory) {
// It's a folder - traverse it
traverseDirectory(entry, (files) => {
if (files.length > 0) {
const structure = buildDirectoryStructure(files);
sftpShowLocalDirectoryStructure(structure, files);
}
});
} else {
// It's a file - just show it
const files = Array.from(e.dataTransfer.files);
sftpShowLocalFilesFlat(files);
}
}
}
function traverseDirectory(entry, callback) {
const files = [];
function readEntries(dirReader) {
dirReader.readEntries((entries) => {
if (entries.length === 0) {
callback(files);
return;
}
let count = entries.length;
entries.forEach((entry) => {
if (entry.isFile) {
entry.file((file) => {
// Add relative path info
const relativePath = entry.fullPath.substring(1); // Remove leading /
Object.defineProperty(file, 'webkitRelativePath', {
value: relativePath,
writable: false
});
files.push(file);
count--;
if (count === 0) readEntries(dirReader);
});
} else if (entry.isDirectory) {
readEntries(entry.createReader());
count--;
if (count === 0) readEntries(dirReader);
}
});
});
}
if (entry.isDirectory) {
readEntries(entry.createReader());
} else {
entry.file((file) => {
files.push(file);
callback(files);
});
}
}
function buildDirectoryStructure(files) {
const root = { name: 'Home', type: 'directory', children: {} };
files.forEach(file => {
const pathParts = file.webkitRelativePath.split('/');
let current = root;
pathParts.forEach((part, index) => {
if (index === pathParts.length - 1) {
// File
current.children[part] = {
name: part,
type: 'file',
file: file,
size: file.size,
lastModified: file.lastModified
};
} else {
// Directory
if (!current.children[part]) {
current.children[part] = {
name: part,
type: 'directory',
children: {}
};
}
current = current.children[part];
}
});
});
return root;
}
function sftpShowLocalDirectoryStructure(structure, allFiles) {
const container = document.getElementById('sftpLocalFiles');
if (!container) return;
// Store for later use
localDirectoryHandle = {
type: 'fallback',
structure: structure,
currentPath: [],
allFiles: allFiles
};
// Update path
const pathInput = document.getElementById('sftpLocalPathInput');
if (pathInput) {
pathInput.value = structure.name;
}
sftpRenderLocalDirectory(structure, []);
}
async function sftpRenderLocalDirectory(dir, path) {
const container = document.getElementById('sftpLocalFiles');
if (!container) return;
const children = Object.values(dir.children || {});
const dirs = children.filter(c => c.type === 'directory').sort((a, b) => a.name.localeCompare(b.name));
const files = children.filter(c => c.type === 'file').sort((a, b) => a.name.localeCompare(b.name));
const sorted = [...dirs, ...files];
// Column headers
let html = `
FilenameSizeDate
`;
// Parent directory
if (path.length > 0) {
html += `
📁..
`;
}
// Get file sizes and dates for File System Access API files
for (const item of sorted) {
const icon = item.type === 'directory' ? '📁' : getFileIcon(item.name);
// Get file size - for File System Access API, we need to get the file handle and query its size
let size = item.type === 'directory' ? '' : formatBytes(item.size || 0);
let date = item.lastModified ? new Date(item.lastModified).toLocaleString() : '';
// For File System Access API files, try to get actual file size and date
if (item.type === 'file' && item.handle && !item.size) {
try {
const file = await item.handle.getFile();
item.size = file.size; // Cache the size for future use
size = formatBytes(file.size);
console.log(`Got file size for ${item.name}: ${file.size} bytes`);
} catch (error) {
console.warn(`Could not get file size for ${item.name}:`, error);
}
}
// Also get file modification date if missing
if (item.type === 'file' && item.handle && !item.lastModified) {
try {
const file = await item.handle.getFile();
item.lastModified = file.lastModified;
date = new Date(file.lastModified).toLocaleString();
console.log(`Got file date for ${item.name}: ${file.lastModified}`);
} catch (error) {
console.warn(`Could not get file date for ${item.name}:`, error);
}
}
const pathStr = path.concat(item.name).join('/');
if (item.type === 'directory') {
html += `
${icon}${escapeHtml(item.name)}${size}${date}
`;
} else {
html += `
${icon}${escapeHtml(item.name)}${size}${date}
`;
}
}
container.innerHTML = html;
}
function sftpOpenLocalStructureDir(pathStr) {
if (!localDirectoryHandle || !localDirectoryHandle.structure) return;
console.log('sftpOpenLocalStructureDir called with path:', pathStr);
console.log('Current structure:', localDirectoryHandle.structure);
const parts = pathStr.split('/');
let current = localDirectoryHandle.structure;
for (const part of parts) {
if (part === '') continue; // Skip empty parts from leading/trailing slashes
if (current.children && current.children[part]) {
current = current.children[part];
console.log('Found part:', part, 'current:', current);
} else {
console.log('Part not found:', part, 'in current:', current);
return;
}
}
localDirectoryHandle.currentPath = parts;
// Update path display
const pathInput = document.getElementById('sftpLocalPathInput');
if (pathInput) {
const fullPath = [localDirectoryHandle.structure.name, ...parts].join('/');
pathInput.value = fullPath;
}
console.log('Calling sftpRenderLocalDirectory with:', current, parts);
sftpRenderLocalDirectory(current, parts);
}
function sftpNavigateLocalStructureUp() {
if (!localDirectoryHandle || !localDirectoryHandle.structure) return;
const path = localDirectoryHandle.currentPath;
if (path.length === 0) return;
path.pop();
let current = localDirectoryHandle.structure;
for (const part of path) {
if (current.children && current.children[part]) {
current = current.children[part];
}
}
localDirectoryHandle.currentPath = path;
// Update path display
const pathInput = document.getElementById('sftpLocalPathInput');
if (pathInput) {
const fullPath = [localDirectoryHandle.structure.name, ...path].join('/');
pathInput.value = fullPath;
}
sftpRenderLocalDirectory(current, path);
}
function sftpSelectLocalStructureFile(element, name) {
localSelectedFile = name;
document.querySelectorAll('#sftpLocalFiles .sftp-file-item').forEach(el => el.classList.remove('selected'));
element.classList.add('selected');
}
async function sftpUploadFromLocal() {
// Check if we have a file selected from structure view
const selectedEl = document.querySelector('#sftpLocalFiles .sftp-file-item.selected');
if (selectedEl) {
// Check for path-based selection (directory structure mode)
if (selectedEl.dataset.path) {
const path = selectedEl.dataset.path;
const file = await findFileInStructure(path);
if (file) {
// Check file size before upload
if (file.size > maxUploadSize) {
// Show error in Transfer Queue
const queueId = sftpAddToQueue(file.name, file.size, 'upload');
sftpUpdateQueueProgress(queueId, 0, 'Failed: File too large. Max size: ' + formatBytes(maxUploadSize));
return;
}
sftpUploadFiles([file]);
return;
}
}
// Check for filename-based selection (flat file mode)
if (selectedEl.dataset.filename) {
const filename = selectedEl.dataset.filename;
if (localDirectoryHandle && localDirectoryHandle.allFiles) {
const file = localDirectoryHandle.allFiles.find(f => f.name === filename);
if (file) {
// Check file size before upload
if (file.size > maxUploadSize) {
alert(`File "${file.name}" is too large for upload.\n\nSize: ${formatBytes(file.size)}\nMaximum size: ${formatBytes(maxUploadSize)}\n\nPlease select a smaller file.`);
return;
}
sftpUploadFiles([file]);
return;
}
}
}
}
// No file selected in UI, so open a picker for direct upload
const input = document.createElement('input');
input.type = 'file';
input.multiple = true;
input.onchange = (e) => {
if (e.target.files.length > 0) {
sftpUploadFiles(e.target.files);
}
};
input.click();
}
function findFileInStructure(path) {
if (!localDirectoryHandle) return null;
if (localDirectoryHandle.type === 'fs_access') {
const parts = path.split('/').filter(Boolean);
let current = localDirectoryHandle.structure;
for (const part of parts) {
if (!current.children || !current.children[part]) return null;
current = current.children[part];
}
if (current.type !== 'file' || !current.handle) return null;
return current.handle.getFile();
}
if (!localDirectoryHandle.allFiles) return null;
// Find file by matching path (webkitdirectory mode)
return localDirectoryHandle.allFiles.find(f => f.webkitRelativePath === path);
}
function toggleSftpView() {
const terminalView = document.getElementById('terminalView');
const sftpView = document.getElementById('sftpView');
const sftpToggleBtn = document.getElementById('sftpToggleBtn');
if (window.getComputedStyle(sftpView).display === 'none') {
// Show SFTP, hide terminal
const conn = getCurrentConnection() || connections.find(connection =>
connection.id === document.querySelector('.connection-item.active')?.dataset.connectionId
);
if (!conn) {
alert('Please select a connection first');
return;
}
if (currentConnectionId !== conn.id) {
selectConnection(conn.id);
}
currentSftpConnection = conn;
terminalView.style.display = 'none';
sftpView.style.display = 'flex';
sftpToggleBtn.classList.add('active');
document.getElementById('terminalToggleBtn').classList.remove('active');
// Connect SFTP
sftpConnect(conn);
} else {
// Show terminal, hide SFTP
terminalView.style.display = 'flex';
sftpView.style.display = 'none';
sftpToggleBtn.classList.remove('active');
document.getElementById('terminalToggleBtn').classList.add('active');
}
}
function hideSftpPanel() {
// Hide SFTP view and show terminal
const terminalView = document.getElementById('terminalView');
const sftpView = document.getElementById('sftpView');
const sftpToggleBtn = document.getElementById('sftpToggleBtn');
terminalView.style.display = 'flex';
sftpView.style.display = 'none';
sftpToggleBtn.classList.remove('active');
document.getElementById('terminalToggleBtn').classList.add('active');
}
function showSftpPanel() {
// Legacy function - delegates to toggleSftpView
const sftpView = document.getElementById('sftpView');
if (sftpView.style.display === 'none') {
toggleSftpView();
}
}
function sftpConnect(conn) {
currentSftpConnection = conn;
// Directly list the root directory
sftpList('.');
}
async function sftpList(path) {
if (!currentSftpConnection) return;
try {
const response = await fetch(`${apiBaseUrl}/api/sftp/list`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`
},
body: JSON.stringify({
conn_id: currentSftpConnection.id,
path: path
})
});
if (!response.ok) {
throw new Error('Failed to list directory');
}
const data = await response.json();
sftpRenderFiles(data.files, data.path);
} catch (err) {
console.error('List error:', err);
document.getElementById('sftpRemoteFiles').innerHTML = '
`;
}
// Show recordings for a specific connection
async function showConnectionRecordings(connectionId) {
const container = document.getElementById('recordingsList');
container.innerHTML = '