Files
terra-view/templates/partials/projects/ftp_browser.html
2026-01-19 21:31:22 +00:00

608 lines
31 KiB
HTML

<!-- FTP File Browser for SLMs v2.0 - Folder Download Support -->
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-lg p-6 mb-6">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">Download Files from SLMs</h2>
{% if units %}
<div class="space-y-6">
{% for unit_item in units %}
<div class="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
<!-- Unit Header -->
<div class="bg-gray-50 dark:bg-gray-900 px-4 py-3 flex items-center justify-between">
<div class="flex items-center gap-3">
<h3 class="font-semibold text-gray-900 dark:text-white">
{{ unit_item.unit.id }}
</h3>
{% if unit_item.location %}
<span class="text-xs text-gray-500 dark:text-gray-400">
@ {{ unit_item.location.name }}
</span>
{% endif %}
<span id="ftp-status-{{ unit_item.unit.id }}" class="px-2 py-1 text-xs font-medium rounded-full bg-gray-200 text-gray-700 dark:bg-gray-700 dark:text-gray-300">
Checking...
</span>
</div>
<div class="flex items-center gap-2">
<button onclick="enableFTP('{{ unit_item.unit.id }}')"
id="enable-ftp-{{ unit_item.unit.id }}"
class="px-3 py-1 text-xs bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors"
disabled>
Enable FTP
</button>
<button onclick="disableFTP('{{ unit_item.unit.id }}')"
id="disable-ftp-{{ unit_item.unit.id }}"
class="px-3 py-1 text-xs bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors"
disabled>
Disable FTP
</button>
<button onclick="loadFTPFiles('{{ unit_item.unit.id }}', '/NL-43')"
id="browse-ftp-{{ unit_item.unit.id }}"
class="px-3 py-1 text-xs bg-seismo-orange text-white rounded-lg hover:bg-seismo-navy transition-colors"
disabled>
Browse Files
</button>
</div>
</div>
<!-- FTP File List -->
<div id="ftp-files-{{ unit_item.unit.id }}" class="hidden" data-location-id="{{ unit_item.location.id if unit_item.location else '' }}">
<div class="p-4">
<div class="flex items-center gap-2 mb-3">
<svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"></path>
</svg>
<span id="current-path-{{ unit_item.unit.id }}" class="text-sm font-mono text-gray-600 dark:text-gray-400">/NL-43</span>
<button onclick="loadFTPFiles('{{ unit_item.unit.id }}', '/NL-43')"
class="ml-auto text-xs px-2 py-1 bg-gray-100 dark:bg-gray-700 rounded hover:bg-gray-200 dark:hover:bg-gray-600">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
</svg>
</button>
</div>
<div id="ftp-file-list-{{ unit_item.unit.id }}" class="space-y-1">
<!-- Files will be loaded here -->
</div>
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="text-center py-8 text-gray-500 dark:text-gray-400">
<svg class="w-12 h-12 mx-auto mb-3 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z"></path>
</svg>
<p>No units assigned to this project</p>
</div>
{% endif %}
</div>
<script>
async function checkFTPStatus(unitId) {
const statusSpan = document.getElementById(`ftp-status-${unitId}`);
const enableBtn = document.getElementById(`enable-ftp-${unitId}`);
const disableBtn = document.getElementById(`disable-ftp-${unitId}`);
const browseBtn = document.getElementById(`browse-ftp-${unitId}`);
try {
const response = await fetch(`/api/slmm/${unitId}/ftp/status`);
const data = await response.json();
if (data.ftp_enabled) {
statusSpan.textContent = 'FTP Enabled';
statusSpan.className = 'px-2 py-1 text-xs font-medium rounded-full bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300';
enableBtn.disabled = true;
disableBtn.disabled = false;
browseBtn.disabled = false;
} else {
statusSpan.textContent = 'FTP Disabled';
statusSpan.className = 'px-2 py-1 text-xs font-medium rounded-full bg-gray-200 text-gray-700 dark:bg-gray-700 dark:text-gray-300';
enableBtn.disabled = false;
disableBtn.disabled = true;
browseBtn.disabled = true;
}
} catch (error) {
statusSpan.textContent = 'Error';
statusSpan.className = 'px-2 py-1 text-xs font-medium rounded-full bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300';
console.error('Error checking FTP status:', error);
}
}
async function enableFTP(unitId) {
const enableBtn = document.getElementById(`enable-ftp-${unitId}`);
enableBtn.disabled = true;
enableBtn.textContent = 'Enabling...';
try {
const response = await fetch(`/api/slmm/${unitId}/ftp/enable`, {
method: 'POST'
});
if (response.ok) {
await checkFTPStatus(unitId);
// Auto-load files after enabling
setTimeout(() => loadFTPFiles(unitId, '/NL-43'), 1000);
} else {
alert('Failed to enable FTP');
}
} catch (error) {
alert('Error enabling FTP: ' + error);
} finally {
enableBtn.textContent = 'Enable FTP';
enableBtn.disabled = false;
}
}
async function disableFTP(unitId) {
if (!confirm('Disable FTP on this unit? This will close the FTP connection.')) return;
const disableBtn = document.getElementById(`disable-ftp-${unitId}`);
disableBtn.disabled = true;
disableBtn.textContent = 'Disabling...';
try {
const response = await fetch(`/api/slmm/${unitId}/ftp/disable`, {
method: 'POST'
});
if (response.ok) {
await checkFTPStatus(unitId);
// Hide file list
document.getElementById(`ftp-files-${unitId}`).classList.add('hidden');
} else {
alert('Failed to disable FTP');
}
} catch (error) {
alert('Error disabling FTP: ' + error);
} finally {
disableBtn.textContent = 'Disable FTP';
disableBtn.disabled = false;
}
}
async function loadFTPFiles(unitId, path) {
const fileListDiv = document.getElementById(`ftp-file-list-${unitId}`);
const filesContainer = document.getElementById(`ftp-files-${unitId}`);
const currentPathSpan = document.getElementById(`current-path-${unitId}`);
// Show loading
fileListDiv.innerHTML = '<div class="text-center py-4 text-gray-500">Loading files...</div>';
filesContainer.classList.remove('hidden');
currentPathSpan.textContent = path;
try {
const response = await fetch(`/api/slmm/${unitId}/ftp/files?path=${encodeURIComponent(path)}`);
const data = await response.json();
if (!data.files || data.files.length === 0) {
fileListDiv.innerHTML = '<div class="text-center py-4 text-gray-500">No files found</div>';
return;
}
// Sort: directories first, then files
const sorted = data.files.sort((a, b) => {
if (a.is_dir && !b.is_dir) return -1;
if (!a.is_dir && b.is_dir) return 1;
return a.name.localeCompare(b.name);
});
function escapeForAttribute(str) {
return String(str).replace(/'/g, "\\'").replace(/"/g, '&quot;');
}
function escapeHtml(str) {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
function getFileIcon(file) {
if (file.is_dir) {
return '<svg class="w-5 h-5 mr-3 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"></path></svg>';
} else if (file.name.toLowerCase().endsWith('.csv')) {
return '<svg class="w-5 h-5 mr-3 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path></svg>';
} else if (file.name.toLowerCase().match(/\.(txt|log)$/)) {
return '<svg class="w-5 h-5 mr-3 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path></svg>';
}
return '<svg class="w-5 h-5 mr-3 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z"></path></svg>';
}
// Render file list with collapsible folders
let html = '<div class="space-y-1">';
for (const file of sorted) {
const icon = getFileIcon(file);
const sizeStr = file.is_dir ? '' : formatFileSize(file.size);
const folderId = 'proj-folder-' + file.path.replace(/[^a-zA-Z0-9]/g, '-');
if (file.is_dir) {
// Collapsible folder row
html += `
<div class="border border-gray-200 dark:border-gray-600 rounded mb-1">
<div class="flex items-center justify-between p-3 hover:bg-gray-50 dark:hover:bg-gray-700 rounded transition-colors cursor-pointer"
onclick="toggleFTPFolderProject('${unitId}', '${escapeForAttribute(file.path)}', '${folderId}', this)">
<div class="flex items-center flex-1">
<svg class="w-4 h-4 mr-2 text-gray-400 transition-transform folder-chevron" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
</svg>
${icon}
<span class="text-gray-900 dark:text-white font-medium">${escapeHtml(file.name)}</span>
<span class="ml-2 text-xs text-gray-400 folder-status"></span>
</div>
<div class="flex items-center gap-2 flex-shrink-0 ml-4">
<span class="text-xs text-gray-500 hidden sm:inline">${file.modified || ''}</span>
<button onclick="event.stopPropagation(); downloadFolderToServer('${unitId}', '${escapeForAttribute(file.path)}', '${escapeForAttribute(file.name)}')"
class="px-3 py-1 text-xs bg-seismo-orange text-white rounded hover:bg-seismo-navy transition-colors flex items-center"
title="Download folder from device to server and add to project database">
<svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path>
</svg>
Download & Save
</button>
</div>
</div>
<div id="${folderId}" class="hidden pl-6 pr-2 pb-2 border-t border-gray-100 dark:border-gray-700">
<div class="text-sm text-gray-500 py-2">Click to load contents...</div>
</div>
</div>
`;
} else {
html += `
<div class="flex items-center justify-between p-3 hover:bg-gray-50 dark:hover:bg-gray-700 rounded transition-colors">
<div class="flex items-center flex-1 min-w-0">
${icon}
<span class="text-gray-900 dark:text-white truncate">${escapeHtml(file.name)}</span>
</div>
<div class="flex items-center gap-3 flex-shrink-0 ml-4">
<span class="text-xs text-gray-500 hidden sm:inline">${sizeStr}</span>
<span class="text-xs text-gray-500 hidden md:inline">${file.modified || ''}</span>
<button onclick="downloadToServer('${unitId}', '${escapeForAttribute(file.path)}', '${escapeForAttribute(file.name)}')"
class="px-3 py-1 text-xs bg-seismo-orange text-white rounded hover:bg-seismo-navy transition-colors flex items-center"
title="Download file from device to server and add to project database">
<svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path>
</svg>
Download & Save
</button>
</div>
</div>
`;
}
}
html += '</div>';
fileListDiv.innerHTML = html;
} catch (error) {
fileListDiv.innerHTML = '<div class="text-center py-4 text-red-500">Error loading files: ' + error + '</div>';
console.error('Error loading FTP files:', error);
}
}
// Toggle folder expand/collapse and load contents for Project FTP browser
async function toggleFTPFolderProject(unitId, folderPath, folderId, headerElement) {
const contentDiv = document.getElementById(folderId);
const chevron = headerElement.querySelector('.folder-chevron');
const statusSpan = headerElement.querySelector('.folder-status');
if (!contentDiv) return;
const isExpanded = !contentDiv.classList.contains('hidden');
if (isExpanded) {
// Collapse
contentDiv.classList.add('hidden');
chevron.style.transform = 'rotate(0deg)';
} else {
// Expand and load contents if not already loaded
contentDiv.classList.remove('hidden');
chevron.style.transform = 'rotate(90deg)';
// Check if already loaded
if (contentDiv.dataset.loaded === 'true') {
return;
}
// Show loading state
contentDiv.innerHTML = '<div class="text-sm text-gray-500 py-2 flex items-center"><svg class="w-4 h-4 mr-2 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path></svg>Loading...</div>';
try {
const response = await fetch(`/api/slmm/${unitId}/ftp/files?path=${encodeURIComponent(folderPath)}`);
const result = await response.json();
if (result.status !== 'ok' && !result.files) {
throw new Error(result.detail || 'Failed to list files');
}
const files = result.files || [];
if (files.length === 0) {
contentDiv.innerHTML = '<div class="text-sm text-gray-500 py-2 italic">Empty folder</div>';
statusSpan.textContent = '(empty)';
contentDiv.dataset.loaded = 'true';
return;
}
// Sort: directories first, then by name
files.sort((a, b) => {
if (a.is_dir && !b.is_dir) return -1;
if (!a.is_dir && b.is_dir) return 1;
return a.name.localeCompare(b.name);
});
// Update status with file count
const dirCount = files.filter(f => f.is_dir).length;
const fileCount = files.length - dirCount;
let statusText = [];
if (dirCount > 0) statusText.push(`${dirCount} folder${dirCount > 1 ? 's' : ''}`);
if (fileCount > 0) statusText.push(`${fileCount} file${fileCount > 1 ? 's' : ''}`);
statusSpan.textContent = `(${statusText.join(', ')})`;
function escapeForAttribute(str) {
return String(str).replace(/'/g, "\\'").replace(/"/g, '&quot;');
}
function escapeHtml(str) {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
function getFileIcon(file) {
if (file.is_dir) {
return '<svg class="w-4 h-4 mr-2 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"></path></svg>';
} else if (file.name.toLowerCase().endsWith('.csv')) {
return '<svg class="w-4 h-4 mr-2 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path></svg>';
} else if (file.name.toLowerCase().match(/\.(txt|log)$/)) {
return '<svg class="w-4 h-4 mr-2 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path></svg>';
}
return '<svg class="w-4 h-4 mr-2 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z"></path></svg>';
}
let html = '<div class="space-y-1 mt-2">';
files.forEach(file => {
const fullPath = file.path || `${folderPath}/${file.name}`;
const icon = getFileIcon(file);
const sizeText = file.size ? formatFileSize(file.size) : '';
const subFolderId = 'proj-folder-' + fullPath.replace(/[^a-zA-Z0-9]/g, '-');
if (file.is_dir) {
// Nested collapsible folder
html += `
<div class="border border-gray-200 dark:border-gray-600 rounded">
<div class="flex items-center justify-between p-2 hover:bg-gray-50 dark:hover:bg-gray-700 rounded transition-colors cursor-pointer text-sm"
onclick="toggleFTPFolderProject('${unitId}', '${escapeForAttribute(fullPath)}', '${subFolderId}', this)">
<div class="flex items-center flex-1">
<svg class="w-3 h-3 mr-2 text-gray-400 transition-transform folder-chevron" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
</svg>
${icon}
<span class="text-gray-900 dark:text-white font-medium">${escapeHtml(file.name)}</span>
<span class="ml-2 text-xs text-gray-400 folder-status"></span>
</div>
<div class="flex items-center gap-2 flex-shrink-0 ml-2">
<button onclick="event.stopPropagation(); downloadFolderToServer('${unitId}', '${escapeForAttribute(fullPath)}', '${escapeForAttribute(file.name)}')"
class="px-2 py-1 bg-seismo-orange hover:bg-seismo-navy text-white text-xs rounded transition-colors flex items-center"
title="Download folder to server">
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path>
</svg>
</button>
</div>
</div>
<div id="${subFolderId}" class="hidden pl-4 pr-2 pb-2 border-t border-gray-100 dark:border-gray-700">
<div class="text-sm text-gray-500 py-2">Click to load contents...</div>
</div>
</div>
`;
} else {
html += `
<div class="flex items-center justify-between p-2 hover:bg-gray-50 dark:hover:bg-gray-700 rounded transition-colors text-sm">
<div class="flex items-center flex-1 min-w-0">
${icon}
<span class="text-gray-900 dark:text-white truncate">${escapeHtml(file.name)}</span>
</div>
<div class="flex items-center gap-2 flex-shrink-0 ml-2">
<span class="text-xs text-gray-500 hidden sm:inline">${sizeText}</span>
<button onclick="downloadToServer('${unitId}', '${escapeForAttribute(fullPath)}', '${escapeForAttribute(file.name)}')"
class="px-2 py-1 bg-seismo-orange hover:bg-seismo-navy text-white text-xs rounded transition-colors"
title="Download to server">
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path>
</svg>
</button>
</div>
</div>
`;
}
});
html += '</div>';
contentDiv.innerHTML = html;
contentDiv.dataset.loaded = 'true';
} catch (error) {
console.error('Failed to load folder contents:', error);
contentDiv.innerHTML = `<div class="text-sm text-red-500 py-2">Error: ${error.message}</div>`;
}
}
}
async function downloadFTPFile(unitId, remotePath, fileName) {
const btn = event.target;
btn.disabled = true;
btn.textContent = 'Downloading...';
try {
const response = await fetch(`/api/slmm/${unitId}/ftp/download`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
remote_path: remotePath
})
});
if (response.ok) {
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = fileName;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
} else {
const errorData = await response.json();
alert('Download failed: ' + (errorData.detail || 'Unknown error'));
}
} catch (error) {
alert('Error downloading file: ' + error);
} finally {
btn.disabled = false;
btn.textContent = 'Download';
}
}
async function downloadFTPFolder(unitId, remotePath, folderName) {
const btn = event.target;
const originalHTML = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '<svg class="w-3 h-3 inline mr-1 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>Downloading...';
try {
const response = await fetch(`/api/slmm/${unitId}/ftp/download-folder`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
remote_path: remotePath
})
});
if (response.ok) {
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${folderName}.zip`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
// Show success message
alert(`✓ Folder "${folderName}" downloaded successfully as ZIP file!`);
} else {
const errorData = await response.json();
alert('Folder download failed: ' + (errorData.detail || 'Unknown error'));
}
} catch (error) {
alert('Error downloading folder: ' + error);
} finally {
btn.disabled = false;
btn.innerHTML = originalHTML;
}
}
async function downloadFolderToServer(unitId, remotePath, folderName) {
const btn = event.target;
const originalHTML = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '<svg class="w-3 h-3 inline mr-1 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>Downloading...';
// Get location_id from the unit's data attribute
const unitContainer = btn.closest('[id^="ftp-files-"]');
const locationId = unitContainer.dataset.locationId;
try {
const response = await fetch(`/api/projects/{{ project_id }}/ftp-download-folder-to-server`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
unit_id: unitId,
remote_path: remotePath,
location_id: locationId
})
});
const data = await response.json();
if (response.ok) {
// Show success message
alert(`✓ Folder "${folderName}" downloaded successfully!\n\n${data.file_count} files extracted\nTotal size: ${formatFileSize(data.total_size)}\n\nFiles are now available in the Project Files section below.`);
// Refresh the unified files list
htmx.trigger('#unified-files', 'refresh');
} else {
alert('Folder download to server failed: ' + (data.detail || 'Unknown error'));
}
} catch (error) {
alert('Error downloading folder to server: ' + error);
} finally {
btn.disabled = false;
btn.innerHTML = originalHTML;
}
}
async function downloadToServer(unitId, remotePath, fileName) {
const btn = event.target;
const originalText = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '<svg class="w-3 h-3 inline mr-1 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path></svg>Downloading...';
// Get location_id from the unit's data attribute
const unitContainer = btn.closest('[id^="ftp-files-"]');
const locationId = unitContainer.dataset.locationId;
try {
const response = await fetch(`/api/projects/{{ project_id }}/ftp-download-to-server`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
unit_id: unitId,
remote_path: remotePath,
location_id: locationId
})
});
const data = await response.json();
if (response.ok) {
// Show success message
alert(`${fileName} downloaded to server successfully!\n\nFile ID: ${data.file_id}\nSize: ${formatFileSize(data.file_size)}`);
// Refresh the unified files list
htmx.trigger('#unified-files', 'refresh');
} else {
alert('Download to server failed: ' + (data.detail || 'Unknown error'));
}
} catch (error) {
alert('Error downloading to server: ' + error);
} finally {
btn.disabled = false;
btn.innerHTML = originalText;
}
}
function formatFileSize(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
if (bytes < 1073741824) return (bytes / 1048576).toFixed(1) + ' MB';
return (bytes / 1073741824).toFixed(2) + ' GB';
}
// Check FTP status for all units on load
// Use setTimeout to ensure DOM elements exist when HTMX loads this partial
setTimeout(function() {
{% for unit_item in units %}
checkFTPStatus('{{ unit_item.unit.id }}');
{% endfor %}
}, 100);
</script>