feat: Add Rename Unit functionality and improve navigation in SLM dashboard
- Implemented a modal for renaming units with validation and confirmation prompts. - Added JavaScript functions to handle opening, closing, and submitting the rename unit form. - Enhanced the back navigation in the SLM detail page to check referrer history. - Updated breadcrumb navigation in the legacy dashboard to accommodate NRL locations. - Improved the sound level meters page with a more informative header and device list. - Introduced a live measurement chart with WebSocket support for real-time data streaming. - Added functionality to manage active devices and projects with auto-refresh capabilities.
This commit is contained in:
@@ -1,51 +1,63 @@
|
||||
<!-- SLM Device List -->
|
||||
{% if units %}
|
||||
{% for unit in units %}
|
||||
<a href="/slm/{{ unit.id }}" class="block bg-gray-50 dark:bg-gray-800 rounded-lg p-4 border border-transparent hover:border-seismo-orange transition-colors relative">
|
||||
<button onclick="event.preventDefault(); event.stopPropagation(); openDeviceConfigModal('{{ unit.id }}');"
|
||||
class="absolute top-3 right-3 text-gray-500 hover:text-seismo-orange dark:text-gray-400 dark:hover:text-seismo-orange"
|
||||
title="Configure {{ unit.id }}">
|
||||
<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="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div class="min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-semibold text-gray-900 dark:text-white">{{ unit.id }}</span>
|
||||
{% if unit.slm_model %}
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">• {{ unit.slm_model }}</span>
|
||||
<div class="bg-gray-50 dark:bg-gray-800 rounded-lg p-4 border border-transparent hover:border-seismo-orange transition-colors relative">
|
||||
<div class="absolute top-3 right-3 flex gap-2">
|
||||
<button onclick="event.preventDefault(); event.stopPropagation(); showLiveChart('{{ unit.id }}');"
|
||||
class="text-gray-500 hover:text-seismo-orange dark:text-gray-400 dark:hover:text-seismo-orange"
|
||||
title="View live chart">
|
||||
<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="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button onclick="event.preventDefault(); event.stopPropagation(); openDeviceConfigModal('{{ unit.id }}');"
|
||||
class="text-gray-500 hover:text-seismo-orange dark:text-gray-400 dark:hover:text-seismo-orange"
|
||||
title="Configure {{ unit.id }}">
|
||||
<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="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<a href="/slm/{{ unit.id }}" class="block">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-semibold text-gray-900 dark:text-white">{{ unit.id }}</span>
|
||||
{% if unit.slm_model %}
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">• {{ unit.slm_model }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if unit.address %}
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 truncate mt-1">{{ unit.address }}</p>
|
||||
{% elif unit.location %}
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 truncate mt-1">{{ unit.location }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if unit.address %}
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 truncate mt-1">{{ unit.address }}</p>
|
||||
{% elif unit.location %}
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 truncate mt-1">{{ unit.location }}</p>
|
||||
|
||||
{% if unit.retired %}
|
||||
<span class="shrink-0 px-2 py-1 text-xs font-medium bg-gray-200 text-gray-700 dark:bg-gray-700 dark:text-gray-300 rounded-full">Retired</span>
|
||||
{% elif not unit.deployed %}
|
||||
<span class="shrink-0 px-2 py-1 text-xs font-medium bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-300 rounded-full">Benched</span>
|
||||
{% elif unit.measurement_state == "Start" %}
|
||||
<span class="shrink-0 px-2 py-1 text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300 rounded-full">Measuring</span>
|
||||
{% elif unit.is_recent %}
|
||||
<span class="shrink-0 px-2 py-1 text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300 rounded-full">Active</span>
|
||||
{% else %}
|
||||
<span class="shrink-0 px-2 py-1 text-xs font-medium bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300 rounded-full">Idle</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if unit.retired %}
|
||||
<span class="shrink-0 px-2 py-1 text-xs font-medium bg-gray-200 text-gray-700 dark:bg-gray-700 dark:text-gray-300 rounded-full">Retired</span>
|
||||
{% elif not unit.deployed %}
|
||||
<span class="shrink-0 px-2 py-1 text-xs font-medium bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-300 rounded-full">Benched</span>
|
||||
{% elif unit.measurement_state == "Start" %}
|
||||
<span class="shrink-0 px-2 py-1 text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300 rounded-full">Measuring</span>
|
||||
{% elif unit.is_recent %}
|
||||
<span class="shrink-0 px-2 py-1 text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300 rounded-full">Active</span>
|
||||
{% else %}
|
||||
<span class="shrink-0 px-2 py-1 text-xs font-medium bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300 rounded-full">Idle</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
{% if unit.slm_last_check %}
|
||||
Last check: {{ unit.slm_last_check.strftime('%Y-%m-%d %H:%M') }}
|
||||
{% else %}
|
||||
No recent check-in
|
||||
{% endif %}
|
||||
</div>
|
||||
</a>
|
||||
<div class="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
{% if unit.slm_last_check %}
|
||||
Last check: {{ unit.slm_last_check.strftime('%Y-%m-%d %H:%M') }}
|
||||
{% else %}
|
||||
No recent check-in
|
||||
{% endif %}
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="text-center py-8 text-gray-500 dark:text-gray-400">
|
||||
|
||||
@@ -23,18 +23,40 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Measurement Status Badge -->
|
||||
<div>
|
||||
{% if is_measuring %}
|
||||
<span class="px-4 py-2 bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-400 rounded-lg font-medium flex items-center">
|
||||
<span class="w-2 h-2 bg-green-500 rounded-full mr-2 animate-pulse"></span>
|
||||
Measuring
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="px-4 py-2 bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg font-medium">
|
||||
Stopped
|
||||
</span>
|
||||
{% endif %}
|
||||
<!-- Status and Actions -->
|
||||
<div class="flex items-center gap-3">
|
||||
<!-- Settings Gear -->
|
||||
<button onclick="openSettingsModal('{{ unit.id }}')"
|
||||
class="p-2 text-gray-600 dark:text-gray-400 hover:text-seismo-orange dark:hover:text-seismo-orange rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
|
||||
title="Unit Settings">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- FTP Browser -->
|
||||
<button onclick="openFTPBrowser('{{ unit.id }}')"
|
||||
class="p-2 text-gray-600 dark:text-gray-400 hover:text-seismo-orange dark:hover:text-seismo-orange rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
|
||||
title="Browse Files (FTP)">
|
||||
<svg class="w-6 h-6" 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>
|
||||
</button>
|
||||
|
||||
<!-- Measurement Status Badge -->
|
||||
<div>
|
||||
{% if is_measuring %}
|
||||
<span class="px-4 py-2 bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-400 rounded-lg font-medium flex items-center">
|
||||
<span class="w-2 h-2 bg-green-500 rounded-full mr-2 animate-pulse"></span>
|
||||
Measuring
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="px-4 py-2 bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg font-medium">
|
||||
Stopped
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -564,4 +586,864 @@ window.addEventListener('beforeunload', function() {
|
||||
window.currentWebSocket.close();
|
||||
}
|
||||
});
|
||||
|
||||
// ========================================
|
||||
// Settings Modal
|
||||
// ========================================
|
||||
async function openSettingsModal(unitId) {
|
||||
const modal = document.getElementById('settings-modal');
|
||||
const errorDiv = document.getElementById('settings-error');
|
||||
const successDiv = document.getElementById('settings-success');
|
||||
|
||||
// Clear previous messages
|
||||
errorDiv.classList.add('hidden');
|
||||
successDiv.classList.add('hidden');
|
||||
|
||||
// Store unit ID
|
||||
document.getElementById('settings-unit-id').value = unitId;
|
||||
|
||||
// Load current SLMM config
|
||||
try {
|
||||
const response = await fetch(`/api/slmm/${unitId}/config`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to load configuration');
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
const config = result.data || {};
|
||||
|
||||
// Populate form fields
|
||||
document.getElementById('settings-host').value = config.host || '';
|
||||
document.getElementById('settings-tcp-port').value = config.tcp_port || 2255;
|
||||
document.getElementById('settings-ftp-port').value = config.ftp_port || 21;
|
||||
document.getElementById('settings-ftp-username').value = config.ftp_username || '';
|
||||
document.getElementById('settings-ftp-password').value = config.ftp_password || '';
|
||||
document.getElementById('settings-tcp-enabled').checked = config.tcp_enabled !== false;
|
||||
document.getElementById('settings-ftp-enabled').checked = config.ftp_enabled === true;
|
||||
document.getElementById('settings-web-enabled').checked = config.web_enabled === true;
|
||||
|
||||
modal.classList.remove('hidden');
|
||||
} catch (error) {
|
||||
console.error('Failed to load SLMM config:', error);
|
||||
errorDiv.textContent = 'Failed to load configuration: ' + error.message;
|
||||
errorDiv.classList.remove('hidden');
|
||||
modal.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function closeSettingsModal() {
|
||||
document.getElementById('settings-modal').classList.add('hidden');
|
||||
}
|
||||
|
||||
document.getElementById('settings-form').addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const unitId = document.getElementById('settings-unit-id').value;
|
||||
const errorDiv = document.getElementById('settings-error');
|
||||
const successDiv = document.getElementById('settings-success');
|
||||
|
||||
errorDiv.classList.add('hidden');
|
||||
successDiv.classList.add('hidden');
|
||||
|
||||
// Gather form data
|
||||
const configData = {
|
||||
host: document.getElementById('settings-host').value.trim(),
|
||||
tcp_port: parseInt(document.getElementById('settings-tcp-port').value),
|
||||
ftp_port: parseInt(document.getElementById('settings-ftp-port').value),
|
||||
ftp_username: document.getElementById('settings-ftp-username').value.trim() || null,
|
||||
ftp_password: document.getElementById('settings-ftp-password').value || null,
|
||||
tcp_enabled: document.getElementById('settings-tcp-enabled').checked,
|
||||
ftp_enabled: document.getElementById('settings-ftp-enabled').checked,
|
||||
web_enabled: document.getElementById('settings-web-enabled').checked
|
||||
};
|
||||
|
||||
// Validation
|
||||
if (!configData.host) {
|
||||
errorDiv.textContent = 'Host/IP address is required';
|
||||
errorDiv.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
if (configData.tcp_port < 1 || configData.tcp_port > 65535) {
|
||||
errorDiv.textContent = 'TCP port must be between 1 and 65535';
|
||||
errorDiv.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
if (configData.ftp_port < 1 || configData.ftp_port > 65535) {
|
||||
errorDiv.textContent = 'FTP port must be between 1 and 65535';
|
||||
errorDiv.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/slmm/${unitId}/config`, {
|
||||
method: 'PUT',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(configData)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const data = await response.json().catch(() => ({}));
|
||||
throw new Error(data.detail || 'Failed to update configuration');
|
||||
}
|
||||
|
||||
successDiv.textContent = 'Configuration saved successfully!';
|
||||
successDiv.classList.remove('hidden');
|
||||
|
||||
// Close modal after 1.5 seconds
|
||||
setTimeout(() => {
|
||||
closeSettingsModal();
|
||||
// Optionally reload the page to reflect changes
|
||||
// window.location.reload();
|
||||
}, 1500);
|
||||
} catch (error) {
|
||||
errorDiv.textContent = error.message;
|
||||
errorDiv.classList.remove('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
// ========================================
|
||||
// FTP Browser Modal
|
||||
// ========================================
|
||||
async function openFTPBrowser(unitId) {
|
||||
const modal = document.getElementById('ftp-modal');
|
||||
document.getElementById('ftp-unit-id').value = unitId;
|
||||
modal.classList.remove('hidden');
|
||||
|
||||
// Check FTP status and update UI
|
||||
await updateFTPStatus(unitId);
|
||||
loadFTPFiles(unitId, '/');
|
||||
}
|
||||
|
||||
async function updateFTPStatus(unitId) {
|
||||
const statusBadge = document.getElementById('ftp-status-badge');
|
||||
|
||||
// Show checking state
|
||||
statusBadge.innerHTML = '<div class="animate-spin rounded-full h-3 w-3 border-b-2 border-gray-500 mr-2"></div>Checking...';
|
||||
statusBadge.className = 'flex items-center text-xs px-3 py-1 rounded bg-gray-200 dark:bg-gray-700 text-gray-600 dark:text-gray-400';
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/slmm/${unitId}/ftp/status`);
|
||||
const result = await response.json();
|
||||
|
||||
if (result.status === 'ok' && result.ftp_enabled) {
|
||||
statusBadge.innerHTML = '<span class="w-2 h-2 bg-green-500 rounded-full mr-2"></span>FTP Enabled';
|
||||
statusBadge.className = 'flex items-center text-xs px-3 py-1 rounded bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-400';
|
||||
} else {
|
||||
statusBadge.innerHTML = '<span class="w-2 h-2 bg-amber-500 rounded-full mr-2"></span>FTP Disabled';
|
||||
statusBadge.className = 'flex items-center text-xs px-3 py-1 rounded bg-amber-100 dark:bg-amber-900/30 text-amber-800 dark:text-amber-400';
|
||||
}
|
||||
} catch (error) {
|
||||
statusBadge.innerHTML = '<span class="w-2 h-2 bg-gray-500 rounded-full mr-2"></span>Status Unknown';
|
||||
statusBadge.className = 'flex items-center text-xs px-3 py-1 rounded bg-gray-200 dark:bg-gray-700 text-gray-600 dark:text-gray-400';
|
||||
}
|
||||
}
|
||||
|
||||
function closeFTPBrowser() {
|
||||
document.getElementById('ftp-modal').classList.add('hidden');
|
||||
}
|
||||
|
||||
async function loadFTPFiles(unitId, path) {
|
||||
const container = document.getElementById('ftp-files-list');
|
||||
const pathDisplay = document.getElementById('ftp-current-path');
|
||||
const errorDiv = document.getElementById('ftp-error');
|
||||
|
||||
// Update path display
|
||||
pathDisplay.textContent = path || '/';
|
||||
|
||||
// Show loading state
|
||||
container.innerHTML = '<div class="text-center py-8 text-gray-500"><div class="animate-spin rounded-full h-8 w-8 border-b-2 border-seismo-orange mx-auto mb-2"></div>Loading files...</div>';
|
||||
errorDiv.classList.add('hidden');
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/slmm/${unitId}/ftp/files?path=${encodeURIComponent(path)}`);
|
||||
const result = await response.json();
|
||||
|
||||
if (response.status === 502) {
|
||||
// FTP connection failed - likely not enabled or network issue
|
||||
const detail = result.detail || 'Connection failed';
|
||||
errorDiv.innerHTML = `
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div class="flex-1">
|
||||
<p class="font-medium">FTP Connection Failed</p>
|
||||
<p class="text-sm mt-1">${detail}</p>
|
||||
</div>
|
||||
<button onclick="enableFTP('${unitId}')"
|
||||
class="px-4 py-2 bg-seismo-orange text-white rounded-lg hover:bg-orange-600 transition-colors whitespace-nowrap">
|
||||
Enable FTP
|
||||
</button>
|
||||
</div>
|
||||
<div class="text-xs bg-blue-50 dark:bg-blue-900/20 p-3 rounded">
|
||||
<p class="font-medium text-blue-800 dark:text-blue-400 mb-1">Troubleshooting:</p>
|
||||
<ul class="list-disc list-inside space-y-1 text-blue-700 dark:text-blue-300">
|
||||
<li>Click "Enable FTP" to activate FTP on the device</li>
|
||||
<li>Ensure the device is powered on and connected to the network</li>
|
||||
<li>Check that port 21 (FTP) is not blocked by firewalls</li>
|
||||
<li>Verify the modem/IP address is correct in unit settings</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
errorDiv.classList.remove('hidden');
|
||||
container.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.status !== 'ok') {
|
||||
throw new Error(result.detail || 'Failed to list files');
|
||||
}
|
||||
|
||||
// SLMM returns 'files' not 'data'
|
||||
const files = result.files || result.data || [];
|
||||
|
||||
if (files.length === 0) {
|
||||
container.innerHTML = '<div class="text-center py-8 text-gray-500">No files found</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort: directories first, then by name
|
||||
files.sort((a, b) => {
|
||||
if (a.type === 'directory' && b.type !== 'directory') return -1;
|
||||
if (a.type !== 'directory' && b.type === 'directory') return 1;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
|
||||
let html = '<div class="space-y-1">';
|
||||
|
||||
// Add parent directory link if not at root
|
||||
if (path && path !== '/') {
|
||||
const parentPath = path.split('/').slice(0, -1).join('/') || '/';
|
||||
html += `
|
||||
<div class="flex items-center p-3 hover:bg-gray-50 dark:hover:bg-gray-700 rounded cursor-pointer" onclick="loadFTPFiles('${unitId}', '${parentPath}')">
|
||||
<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="M10 19l-7-7m0 0l7-7m-7 7h18"></path>
|
||||
</svg>
|
||||
<span class="text-gray-600 dark:text-gray-400">..</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Add files and directories
|
||||
files.forEach(file => {
|
||||
const fullPath = file.path || (path === '/' ? `/${file.name}` : `${path}/${file.name}`);
|
||||
const isDir = file.is_dir || file.type === 'directory';
|
||||
|
||||
// Determine file type icon and color
|
||||
let icon, iconColor = 'text-gray-400';
|
||||
if (isDir) {
|
||||
icon = '<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')) {
|
||||
icon = '<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)$/)) {
|
||||
icon = '<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>';
|
||||
} else {
|
||||
icon = '<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>';
|
||||
}
|
||||
|
||||
const sizeText = file.size ? formatFileSize(file.size) : '';
|
||||
const dateText = file.modified || file.modified_time || '';
|
||||
const canPreview = !isDir && (file.name.toLowerCase().match(/\.(csv|txt|log)$/));
|
||||
|
||||
if (isDir) {
|
||||
html += `
|
||||
<div class="flex items-center justify-between p-3 hover:bg-gray-50 dark:hover:bg-gray-700 rounded cursor-pointer transition-colors" onclick="loadFTPFiles('${unitId}', '${escapeForAttribute(fullPath)}')">
|
||||
<div class="flex items-center flex-1">
|
||||
${icon}
|
||||
<span class="text-gray-900 dark:text-white font-medium">${escapeHtml(file.name)}</span>
|
||||
</div>
|
||||
<span class="text-xs text-gray-500">${dateText}</span>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
html += `
|
||||
<div class="flex items-center justify-between p-3 hover:bg-gray-50 dark:hover:bg-gray-700 rounded transition-colors group">
|
||||
<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">${sizeText}</span>
|
||||
<span class="text-xs text-gray-500 hidden md:inline">${dateText}</span>
|
||||
${canPreview ? `
|
||||
<button onclick="previewFile('${unitId}', '${escapeForAttribute(fullPath)}', '${escapeForAttribute(file.name)}')"
|
||||
class="px-3 py-1 bg-blue-600 hover:bg-blue-700 text-white text-sm rounded transition-colors flex items-center"
|
||||
title="Preview file">
|
||||
<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="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
` : ''}
|
||||
<button onclick="downloadFTPFile('${unitId}', '${escapeForAttribute(fullPath)}', '${escapeForAttribute(file.name)}')"
|
||||
class="px-3 py-1 bg-seismo-orange hover:bg-orange-600 text-white text-sm rounded transition-colors flex items-center"
|
||||
title="Download to your computer">
|
||||
<svg class="w-4 h-4 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>
|
||||
<span class="hidden lg:inline">Download</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
});
|
||||
|
||||
function escapeForAttribute(str) {
|
||||
return String(str).replace(/'/g, "\\'").replace(/"/g, '"');
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
container.innerHTML = html;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to load FTP files:', error);
|
||||
errorDiv.textContent = error.message;
|
||||
errorDiv.classList.remove('hidden');
|
||||
container.innerHTML = '';
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadFTPFile(unitId, filePath, fileName) {
|
||||
try {
|
||||
// Show download indicator
|
||||
const downloadBtn = event.target;
|
||||
const originalText = downloadBtn.innerHTML;
|
||||
downloadBtn.innerHTML = '<svg class="w-4 h-4 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>';
|
||||
downloadBtn.disabled = true;
|
||||
|
||||
const response = await fetch(`/api/slmm/${unitId}/ftp/download`, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({ remote_path: filePath })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Download failed');
|
||||
}
|
||||
|
||||
// The response is a file, so we need to create a download link
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = fileName || filePath.split('/').pop();
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
window.URL.revokeObjectURL(url);
|
||||
|
||||
// Reset button
|
||||
downloadBtn.innerHTML = originalText;
|
||||
downloadBtn.disabled = false;
|
||||
|
||||
// Show success message briefly
|
||||
const originalBtnClass = downloadBtn.className;
|
||||
downloadBtn.className = downloadBtn.className.replace('bg-seismo-orange', 'bg-green-600');
|
||||
setTimeout(() => {
|
||||
downloadBtn.className = originalBtnClass;
|
||||
}, 2000);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to download file:', error);
|
||||
alert('Failed to download file: ' + error.message);
|
||||
// Reset button on error
|
||||
if (event.target) {
|
||||
event.target.innerHTML = originalText;
|
||||
event.target.disabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadToServer(unitId, filePath, fileName) {
|
||||
try {
|
||||
const response = await fetch(`/api/slmm/${unitId}/ftp/download`, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({ remote_path: filePath, save_to_server: true })
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.status === 'ok') {
|
||||
alert(`File saved to server at: ${result.local_path}`);
|
||||
} else {
|
||||
throw new Error(result.detail || 'Failed to save to server');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to save file to server:', error);
|
||||
alert('Failed to save file to server: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function previewFile(unitId, filePath, fileName) {
|
||||
const modal = document.getElementById('preview-modal');
|
||||
const previewContent = document.getElementById('preview-content');
|
||||
const previewTitle = document.getElementById('preview-title');
|
||||
|
||||
previewTitle.textContent = fileName;
|
||||
previewContent.innerHTML = '<div class="text-center py-8"><div class="animate-spin rounded-full h-8 w-8 border-b-2 border-seismo-orange mx-auto mb-2"></div>Loading preview...</div>';
|
||||
modal.classList.remove('hidden');
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/slmm/${unitId}/ftp/download`, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({ remote_path: filePath })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to load file');
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
const text = await blob.text();
|
||||
|
||||
// Check file type for syntax highlighting
|
||||
const isCSV = fileName.toLowerCase().endsWith('.csv');
|
||||
const isTXT = fileName.toLowerCase().endsWith('.txt') || fileName.toLowerCase().endsWith('.log');
|
||||
|
||||
if (isCSV) {
|
||||
// Parse and display CSV as table
|
||||
const lines = text.split('\n').filter(l => l.trim());
|
||||
if (lines.length > 0) {
|
||||
const headers = lines[0].split(',');
|
||||
let tableHTML = '<div class="overflow-x-auto"><table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700"><thead class="bg-gray-50 dark:bg-gray-800"><tr>';
|
||||
headers.forEach(h => {
|
||||
tableHTML += `<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">${h.trim()}</th>`;
|
||||
});
|
||||
tableHTML += '</tr></thead><tbody class="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-700">';
|
||||
|
||||
for (let i = 1; i < Math.min(lines.length, 101); i++) {
|
||||
const cells = lines[i].split(',');
|
||||
tableHTML += '<tr>';
|
||||
cells.forEach(c => {
|
||||
tableHTML += `<td class="px-4 py-2 text-sm text-gray-900 dark:text-gray-100">${c.trim()}</td>`;
|
||||
});
|
||||
tableHTML += '</tr>';
|
||||
}
|
||||
|
||||
tableHTML += '</tbody></table></div>';
|
||||
if (lines.length > 101) {
|
||||
tableHTML += `<p class="text-sm text-gray-500 mt-4 text-center">Showing first 100 rows of ${lines.length - 1} total rows</p>`;
|
||||
}
|
||||
previewContent.innerHTML = tableHTML;
|
||||
}
|
||||
} else {
|
||||
// Display as plain text with line numbers
|
||||
const lines = text.split('\n');
|
||||
const maxLines = 1000;
|
||||
const displayLines = lines.slice(0, maxLines);
|
||||
|
||||
let preHTML = '<pre class="text-xs font-mono bg-gray-900 text-gray-100 p-4 rounded-lg overflow-x-auto"><code>';
|
||||
displayLines.forEach((line, i) => {
|
||||
preHTML += `<span class="text-gray-500">${String(i + 1).padStart(4, ' ')}</span> ${escapeHtml(line)}\n`;
|
||||
});
|
||||
preHTML += '</code></pre>';
|
||||
|
||||
if (lines.length > maxLines) {
|
||||
preHTML += `<p class="text-sm text-gray-500 mt-4 text-center">Showing first ${maxLines} lines of ${lines.length} total lines</p>`;
|
||||
}
|
||||
|
||||
previewContent.innerHTML = preHTML;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to preview file:', error);
|
||||
previewContent.innerHTML = `<div class="text-center py-8 text-red-500">Failed to load preview: ${error.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function closePreviewModal() {
|
||||
document.getElementById('preview-modal').classList.add('hidden');
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
async function enableFTP(unitId) {
|
||||
const errorDiv = document.getElementById('ftp-error');
|
||||
const container = document.getElementById('ftp-files-list');
|
||||
|
||||
// Show loading state
|
||||
errorDiv.innerHTML = '<div class="flex items-center"><div class="animate-spin rounded-full h-5 w-5 border-b-2 border-seismo-orange mr-3"></div>Enabling FTP on device...</div>';
|
||||
errorDiv.classList.remove('hidden');
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/slmm/${unitId}/ftp/enable`, {
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.status !== 'ok') {
|
||||
throw new Error(result.detail || 'Failed to enable FTP');
|
||||
}
|
||||
|
||||
// Success - wait a moment then try loading files again
|
||||
errorDiv.innerHTML = '<div class="flex items-center text-green-600 dark:text-green-400"><svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path></svg>FTP enabled successfully. Loading files...</div>';
|
||||
|
||||
setTimeout(() => {
|
||||
loadFTPFiles(unitId, '/');
|
||||
}, 2000);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to enable FTP:', error);
|
||||
errorDiv.innerHTML = `
|
||||
<div>
|
||||
<p class="font-medium text-red-600 dark:text-red-400">Failed to enable FTP</p>
|
||||
<p class="text-sm mt-1">${error.message}</p>
|
||||
<button onclick="loadFTPFiles('${unitId}', '/')"
|
||||
class="mt-2 px-3 py-1 bg-gray-600 text-white text-sm rounded hover:bg-gray-700 transition-colors">
|
||||
Retry Connection
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
errorDiv.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
async function enableFTPFromHeader() {
|
||||
const unitId = document.getElementById('ftp-unit-id').value;
|
||||
const statusBadge = document.getElementById('ftp-status-badge');
|
||||
const errorDiv = document.getElementById('ftp-error');
|
||||
|
||||
// Show enabling state
|
||||
statusBadge.innerHTML = '<div class="animate-spin rounded-full h-3 w-3 border-b-2 border-seismo-orange mr-2"></div>Enabling...';
|
||||
statusBadge.className = 'flex items-center text-xs px-3 py-1 rounded bg-blue-100 dark:bg-blue-900/30 text-blue-800 dark:text-blue-400';
|
||||
errorDiv.classList.add('hidden');
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/slmm/${unitId}/ftp/enable`, {
|
||||
method: 'POST'
|
||||
});
|
||||
const result = await response.json();
|
||||
|
||||
if (result.status !== 'ok') {
|
||||
throw new Error(result.detail || 'Failed to enable FTP');
|
||||
}
|
||||
|
||||
// Update status badge
|
||||
statusBadge.innerHTML = '<span class="w-2 h-2 bg-green-500 rounded-full mr-2"></span>FTP Enabled';
|
||||
statusBadge.className = 'flex items-center text-xs px-3 py-1 rounded bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-400';
|
||||
|
||||
// Show success message and refresh files
|
||||
errorDiv.innerHTML = '<div class="flex items-center text-green-600 dark:text-green-400"><svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path></svg>FTP enabled successfully</div>';
|
||||
errorDiv.classList.remove('hidden');
|
||||
|
||||
setTimeout(() => {
|
||||
errorDiv.classList.add('hidden');
|
||||
loadFTPFiles(unitId, '/');
|
||||
}, 2000);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to enable FTP:', error);
|
||||
statusBadge.innerHTML = '<span class="w-2 h-2 bg-red-500 rounded-full mr-2"></span>Enable Failed';
|
||||
statusBadge.className = 'flex items-center text-xs px-3 py-1 rounded bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-400';
|
||||
|
||||
errorDiv.innerHTML = `<p class="font-medium">Failed to enable FTP:</p><p class="text-sm mt-1">${error.message}</p>`;
|
||||
errorDiv.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
async function disableFTPFromHeader() {
|
||||
const unitId = document.getElementById('ftp-unit-id').value;
|
||||
const statusBadge = document.getElementById('ftp-status-badge');
|
||||
const errorDiv = document.getElementById('ftp-error');
|
||||
|
||||
// Show disabling state
|
||||
statusBadge.innerHTML = '<div class="animate-spin rounded-full h-3 w-3 border-b-2 border-amber-500 mr-2"></div>Disabling...';
|
||||
statusBadge.className = 'flex items-center text-xs px-3 py-1 rounded bg-amber-100 dark:bg-amber-900/30 text-amber-800 dark:text-amber-400';
|
||||
errorDiv.classList.add('hidden');
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/slmm/${unitId}/ftp/disable`, {
|
||||
method: 'POST'
|
||||
});
|
||||
const result = await response.json();
|
||||
|
||||
if (result.status !== 'ok') {
|
||||
throw new Error(result.detail || 'Failed to disable FTP');
|
||||
}
|
||||
|
||||
// Update status badge
|
||||
statusBadge.innerHTML = '<span class="w-2 h-2 bg-amber-500 rounded-full mr-2"></span>FTP Disabled';
|
||||
statusBadge.className = 'flex items-center text-xs px-3 py-1 rounded bg-amber-100 dark:bg-amber-900/30 text-amber-800 dark:text-amber-400';
|
||||
|
||||
// Show success message and clear files
|
||||
errorDiv.innerHTML = '<div class="flex items-center text-amber-600 dark:text-amber-400"><svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path></svg>FTP disabled successfully</div>';
|
||||
errorDiv.classList.remove('hidden');
|
||||
|
||||
document.getElementById('ftp-files-list').innerHTML = '<div class="text-center py-8 text-gray-500">FTP is disabled. Enable it to browse files.</div>';
|
||||
|
||||
setTimeout(() => {
|
||||
errorDiv.classList.add('hidden');
|
||||
}, 3000);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to disable FTP:', error);
|
||||
statusBadge.innerHTML = '<span class="w-2 h-2 bg-red-500 rounded-full mr-2"></span>Disable Failed';
|
||||
statusBadge.className = 'flex items-center text-xs px-3 py-1 rounded bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-400';
|
||||
|
||||
errorDiv.innerHTML = `<p class="font-medium">Failed to disable FTP:</p><p class="text-sm mt-1">${error.message}</p>`;
|
||||
errorDiv.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function refreshFTPFiles() {
|
||||
const unitId = document.getElementById('ftp-unit-id').value;
|
||||
const currentPath = document.getElementById('ftp-current-path').textContent;
|
||||
|
||||
// Update status
|
||||
updateFTPStatus(unitId);
|
||||
|
||||
// Reload files at current path
|
||||
loadFTPFiles(unitId, currentPath);
|
||||
}
|
||||
|
||||
function formatFileSize(bytes) {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
// Close modals on Escape key
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') {
|
||||
closeSettingsModal();
|
||||
closeFTPBrowser();
|
||||
}
|
||||
});
|
||||
|
||||
// Close modals when clicking outside
|
||||
document.getElementById('settings-modal')?.addEventListener('click', function(e) {
|
||||
if (e.target === this) {
|
||||
closeSettingsModal();
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('ftp-modal')?.addEventListener('click', function(e) {
|
||||
if (e.target === this) {
|
||||
closeFTPBrowser();
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('preview-modal')?.addEventListener('click', function(e) {
|
||||
if (e.target === this) {
|
||||
closePreviewModal();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Settings Modal -->
|
||||
<div id="settings-modal" class="hidden fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center overflow-y-auto">
|
||||
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-2xl w-full max-w-2xl m-4 my-8">
|
||||
<div class="p-6 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between">
|
||||
<h3 class="text-xl font-bold text-gray-900 dark:text-white">SLM Configuration</h3>
|
||||
<button onclick="closeSettingsModal()" class="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form id="settings-form" class="p-6 space-y-6">
|
||||
<input type="hidden" id="settings-unit-id">
|
||||
|
||||
<!-- Network Configuration -->
|
||||
<div class="border border-gray-200 dark:border-gray-700 rounded-lg p-4">
|
||||
<h4 class="font-semibold text-gray-900 dark:text-white mb-4">Network Configuration</h4>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="col-span-2">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Host / IP Address</label>
|
||||
<input type="text" id="settings-host"
|
||||
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||||
placeholder="e.g., 192.168.1.100" required>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">TCP Port</label>
|
||||
<input type="number" id="settings-tcp-port"
|
||||
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||||
placeholder="2255" min="1" max="65535" required>
|
||||
<p class="text-xs text-gray-500 mt-1">Default: 2255 for NL-43/NL-53</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">FTP Port</label>
|
||||
<input type="number" id="settings-ftp-port"
|
||||
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||||
placeholder="21" min="1" max="65535" required>
|
||||
<p class="text-xs text-gray-500 mt-1">Standard FTP port (default: 21)</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- FTP Credentials -->
|
||||
<div class="border border-gray-200 dark:border-gray-700 rounded-lg p-4">
|
||||
<h4 class="font-semibold text-gray-900 dark:text-white mb-4">FTP Credentials</h4>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Username</label>
|
||||
<input type="text" id="settings-ftp-username"
|
||||
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||||
placeholder="anonymous">
|
||||
<p class="text-xs text-gray-500 mt-1">Leave blank for anonymous</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Password</label>
|
||||
<input type="password" id="settings-ftp-password"
|
||||
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||||
placeholder="••••••••">
|
||||
<p class="text-xs text-gray-500 mt-1">Leave blank for anonymous</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Protocol Toggles -->
|
||||
<div class="border border-gray-200 dark:border-gray-700 rounded-lg p-4">
|
||||
<h4 class="font-semibold text-gray-900 dark:text-white mb-4">Protocol Settings</h4>
|
||||
|
||||
<div class="space-y-3">
|
||||
<label class="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700">
|
||||
<div>
|
||||
<span class="font-medium text-gray-900 dark:text-white">TCP Communication</span>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Enable TCP control commands</p>
|
||||
</div>
|
||||
<input type="checkbox" id="settings-tcp-enabled"
|
||||
class="w-5 h-5 text-seismo-orange rounded border-gray-300 dark:border-gray-600 focus:ring-seismo-orange">
|
||||
</label>
|
||||
|
||||
<label class="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700">
|
||||
<div>
|
||||
<span class="font-medium text-gray-900 dark:text-white">FTP File Transfer</span>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Enable FTP file browsing and downloads</p>
|
||||
</div>
|
||||
<input type="checkbox" id="settings-ftp-enabled"
|
||||
class="w-5 h-5 text-seismo-orange rounded border-gray-300 dark:border-gray-600 focus:ring-seismo-orange">
|
||||
</label>
|
||||
|
||||
<label class="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700">
|
||||
<div>
|
||||
<span class="font-medium text-gray-900 dark:text-white">Web Interface</span>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Enable web UI access (future feature)</p>
|
||||
</div>
|
||||
<input type="checkbox" id="settings-web-enabled"
|
||||
class="w-5 h-5 text-seismo-orange rounded border-gray-300 dark:border-gray-600 focus:ring-seismo-orange">
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="settings-error" class="hidden text-sm p-3 bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 rounded-lg"></div>
|
||||
<div id="settings-success" class="hidden text-sm p-3 bg-green-50 dark:bg-green-900/20 text-green-600 dark:text-green-400 rounded-lg"></div>
|
||||
|
||||
<div class="flex justify-end gap-3 pt-2 border-t border-gray-200 dark:border-gray-700">
|
||||
<button type="button" onclick="closeSettingsModal()"
|
||||
class="px-6 py-2 border border-gray-300 dark:border-gray-600 rounded-lg text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit"
|
||||
class="px-6 py-2 bg-seismo-orange hover:bg-orange-600 text-white rounded-lg font-medium">
|
||||
Save Configuration
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- FTP Browser Modal -->
|
||||
<div id="ftp-modal" class="hidden fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center">
|
||||
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-2xl w-full max-w-4xl max-h-[90vh] overflow-hidden m-4 flex flex-col">
|
||||
<div class="p-6 border-b border-gray-200 dark:border-gray-700">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-xl font-bold text-gray-900 dark:text-white">FTP File Browser</h3>
|
||||
<button onclick="closeFTPBrowser()" class="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-4">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
Path: <span id="ftp-current-path" class="font-mono">/</span>
|
||||
</p>
|
||||
<div id="ftp-status-badge" class="flex items-center text-xs px-3 py-1 rounded bg-gray-200 dark:bg-gray-700 text-gray-600 dark:text-gray-400">
|
||||
<div class="animate-spin rounded-full h-3 w-3 border-b-2 border-gray-500 mr-2"></div>
|
||||
Checking...
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<button id="ftp-enable-btn" onclick="enableFTPFromHeader()"
|
||||
class="px-3 py-1.5 bg-green-600 hover:bg-green-700 text-white text-sm rounded-lg flex items-center transition-colors">
|
||||
<svg class="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
||||
</svg>
|
||||
Enable FTP
|
||||
</button>
|
||||
|
||||
<button id="ftp-disable-btn" onclick="disableFTPFromHeader()"
|
||||
class="px-3 py-1.5 bg-amber-600 hover:bg-amber-700 text-white text-sm rounded-lg flex items-center transition-colors">
|
||||
<svg class="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
Disable FTP
|
||||
</button>
|
||||
|
||||
<button id="ftp-refresh-btn" onclick="refreshFTPFiles()"
|
||||
class="px-3 py-1.5 bg-seismo-orange hover:bg-orange-600 text-white text-sm rounded-lg flex items-center transition-colors">
|
||||
<svg class="w-4 h-4 mr-1.5" 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>
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-y-auto p-6">
|
||||
<input type="hidden" id="ftp-unit-id">
|
||||
|
||||
<div id="ftp-error" class="hidden mb-4 p-4 bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-400 rounded-lg text-sm"></div>
|
||||
|
||||
<div id="ftp-files-list">
|
||||
<!-- Files will be loaded here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- File Preview Modal -->
|
||||
<div id="preview-modal" class="hidden fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4">
|
||||
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-2xl w-full max-w-6xl max-h-[90vh] overflow-hidden flex flex-col">
|
||||
<div class="p-6 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between flex-shrink-0">
|
||||
<h3 class="text-xl font-bold text-gray-900 dark:text-white flex items-center">
|
||||
<svg class="w-6 h-6 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="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
|
||||
</svg>
|
||||
<span id="preview-title">File Preview</span>
|
||||
</h3>
|
||||
<button onclick="closePreviewModal()" class="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-y-auto p-6 bg-gray-50 dark:bg-gray-900" id="preview-content">
|
||||
<!-- Preview content will be loaded here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user