old leftovers from pre merge
This commit is contained in:
@@ -23,18 +23,31 @@
|
||||
{% 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 %}
|
||||
<!-- Measurement Status Badge & Timer -->
|
||||
<div class="flex flex-col items-end gap-2">
|
||||
<div class="flex items-center gap-3">
|
||||
<!-- Timer Display -->
|
||||
<div id="measurement-timer" class="px-4 py-2 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
|
||||
<div class="flex items-center gap-2">
|
||||
<svg class="w-4 h-4 text-blue-600 dark:text-blue-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
<span id="timer-display" class="text-xl font-mono font-bold text-blue-600 dark:text-blue-400">00:00:00</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status Badge -->
|
||||
{% 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>
|
||||
|
||||
@@ -209,6 +222,171 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Device Settings -->
|
||||
<div class="mt-6">
|
||||
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-3">Device Settings</h3>
|
||||
|
||||
<!-- Clock Sync -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg p-4 border border-gray-200 dark:border-gray-700">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div class="flex items-center">
|
||||
<svg class="w-5 h-5 text-gray-500 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">Device Clock</span>
|
||||
</div>
|
||||
<button onclick="syncClockToNow()"
|
||||
class="px-3 py-1.5 text-xs bg-blue-600 hover:bg-blue-700 text-white rounded flex items-center">
|
||||
<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 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"/>
|
||||
</svg>
|
||||
Sync to Current Time
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400 mb-3">
|
||||
Set the device clock manually or sync to your computer's current time. Device must be stopped to change clock.
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<input type="datetime-local"
|
||||
id="clock-input"
|
||||
class="flex-1 px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-white">
|
||||
<button onclick="setDeviceClock()"
|
||||
class="px-4 py-2 text-sm bg-green-600 hover:bg-green-700 text-white rounded">
|
||||
Set Clock
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="clock-status" class="mt-2 text-xs hidden"></div>
|
||||
</div>
|
||||
|
||||
<!-- Store Name (Index Number) -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg p-4 border border-gray-200 dark:border-gray-700 mt-3">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div class="flex items-center">
|
||||
<svg class="w-5 h-5 text-gray-500 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M7 3a1 1 0 000 2h6a1 1 0 100-2H7zM4 7a1 1 0 011-1h10a1 1 0 110 2H5a1 1 0 01-1-1zM2 11a2 2 0 012-2h12a2 2 0 012 2v4a2 2 0 01-2 2H4a2 2 0 01-2-2v-4z"/>
|
||||
</svg>
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">Store Name (Index)</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span id="store-name-display" class="text-sm font-mono text-gray-600 dark:text-gray-400">----</span>
|
||||
<span id="overwrite-indicator" class="text-xs px-2 py-1 rounded hidden"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400 mb-3">
|
||||
Set the file index number (0000-9999) for the next measurement storage. <strong>Requirements:</strong> Device must be <strong>stopped</strong> and <strong>FTP must be disabled</strong> to change this setting.
|
||||
</div>
|
||||
|
||||
<div id="overwrite-warning" class="hidden mb-3 p-3 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-700 rounded">
|
||||
<div class="flex items-start">
|
||||
<svg class="w-5 h-5 text-yellow-600 dark:text-yellow-500 mr-2 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
<div class="flex-1">
|
||||
<p class="text-xs font-semibold text-yellow-800 dark:text-yellow-200">Data Already Exists</p>
|
||||
<p class="text-xs text-yellow-700 dark:text-yellow-300 mt-1">Starting a measurement will overwrite existing data at this index. Consider using auto-increment to find an empty slot.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<input type="number"
|
||||
id="store-name-input"
|
||||
min="0"
|
||||
max="9999"
|
||||
placeholder="0000"
|
||||
class="flex-1 px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-white">
|
||||
<button onclick="setStoreName()"
|
||||
class="px-4 py-2 text-sm bg-green-600 hover:bg-green-700 text-white rounded">
|
||||
Set Index
|
||||
</button>
|
||||
<button onclick="autoIncrementStoreName()"
|
||||
class="px-4 py-2 text-sm bg-blue-600 hover:bg-blue-700 text-white rounded"
|
||||
title="Find next available index">
|
||||
Auto +
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="store-name-status" class="mt-2 text-xs hidden"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Measurement Data Files -->
|
||||
<div class="mt-6">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300">Measurement Data Files</h3>
|
||||
<div class="flex gap-2">
|
||||
<button id="enable-ftp-btn" onclick="enableFTP()" class="px-3 py-1 text-xs bg-green-600 hover:bg-green-700 text-white rounded hidden">
|
||||
Enable FTP
|
||||
</button>
|
||||
<button id="disable-ftp-btn" onclick="disableFTP()" class="px-3 py-1 text-xs bg-red-600 hover:bg-red-700 text-white rounded hidden">
|
||||
Disable FTP
|
||||
</button>
|
||||
<button onclick="refreshFileList()"
|
||||
class="px-3 py-1 text-xs bg-blue-600 hover:bg-blue-700 text-white rounded flex items-center">
|
||||
<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 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"/>
|
||||
</svg>
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- FTP Status Indicator -->
|
||||
<div id="ftp-status" class="mb-3 p-3 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg hidden">
|
||||
<div class="flex items-center text-sm text-yellow-800 dark:text-yellow-400">
|
||||
<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
<span id="ftp-status-text">FTP server is disabled. Click "Enable FTP" to browse files.</span>
|
||||
</div>
|
||||
<button onclick="enableFTP()"
|
||||
class="mt-2 px-3 py-1 text-xs bg-yellow-600 hover:bg-yellow-700 text-white rounded">
|
||||
Enable FTP
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- File Browser -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700">
|
||||
<!-- Current Path -->
|
||||
<div class="px-4 py-2 bg-gray-50 dark:bg-gray-900/50 border-b border-gray-200 dark:border-gray-700 flex items-center">
|
||||
<svg class="w-4 h-4 text-gray-500 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M2 6a2 2 0 012-2h5l2 2h5a2 2 0 012 2v6a2 2 0 01-2 2H4a2 2 0 01-2-2V6z"/>
|
||||
</svg>
|
||||
<span id="current-path" class="text-sm text-gray-700 dark:text-gray-300 font-mono">/</span>
|
||||
</div>
|
||||
|
||||
<!-- File List -->
|
||||
<div id="file-list" class="divide-y divide-gray-200 dark:divide-gray-700 max-h-96 overflow-y-auto">
|
||||
<div class="p-8 text-center text-gray-500 dark:text-gray-400">
|
||||
<svg class="w-12 h-12 mx-auto mb-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"/>
|
||||
</svg>
|
||||
<p>Click "Refresh" to load files from the device</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div id="file-list-loading" class="p-8 text-center hidden">
|
||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-3"></div>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">Loading files...</p>
|
||||
</div>
|
||||
|
||||
<!-- Error State -->
|
||||
<div id="file-list-error" class="p-4 bg-red-50 dark:bg-red-900/20 border-t border-red-200 dark:border-red-800 hidden">
|
||||
<div class="flex items-center text-sm text-red-800 dark:text-red-400">
|
||||
<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
<span id="file-list-error-text">Failed to load files</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
@@ -236,9 +414,14 @@ function initializeChart() {
|
||||
if (window.liveChart && typeof window.liveChart.destroy === 'function') {
|
||||
console.log('Destroying existing chart');
|
||||
window.liveChart.destroy();
|
||||
window.liveChart = null;
|
||||
}
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) {
|
||||
console.error('Failed to get 2D context from canvas');
|
||||
return;
|
||||
}
|
||||
console.log('Creating new chart...');
|
||||
|
||||
// Dark mode detection
|
||||
@@ -246,7 +429,8 @@ function initializeChart() {
|
||||
const gridColor = isDarkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
|
||||
const textColor = isDarkMode ? 'rgba(255, 255, 255, 0.7)' : 'rgba(0, 0, 0, 0.7)';
|
||||
|
||||
window.liveChart = new Chart(ctx, {
|
||||
try {
|
||||
window.liveChart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: [],
|
||||
@@ -317,12 +501,33 @@ function initializeChart() {
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Chart created successfully:', window.liveChart);
|
||||
console.log('Chart created successfully:', window.liveChart);
|
||||
} catch (error) {
|
||||
console.error('Failed to create chart:', error);
|
||||
// Display error message in the chart container
|
||||
const container = canvas.parentElement;
|
||||
if (container) {
|
||||
container.innerHTML = `
|
||||
<div class="flex items-center justify-center h-full text-sm text-red-600 dark:text-red-400">
|
||||
<div class="text-center">
|
||||
<svg class="w-8 h-8 mx-auto mb-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"/>
|
||||
</svg>
|
||||
<p>Chart failed to load</p>
|
||||
<p class="text-xs mt-1">Try refreshing the page</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize chart when DOM is ready
|
||||
console.log('Executing initializeChart...');
|
||||
initializeChart();
|
||||
// Use a small delay to ensure DOM is fully ready
|
||||
setTimeout(() => {
|
||||
initializeChart();
|
||||
}, 50);
|
||||
|
||||
// WebSocket management (use global scope to avoid redeclaration)
|
||||
if (typeof window.currentWebSocket === 'undefined') {
|
||||
@@ -455,13 +660,19 @@ async function controlUnit(unitId, action) {
|
||||
const result = await response.json();
|
||||
|
||||
if (result.status === 'ok') {
|
||||
// Reload the live view to update status
|
||||
// Give device more time to change state before checking
|
||||
// Start/Stop commands need time to propagate
|
||||
setTimeout(() => {
|
||||
checkMeasurementState();
|
||||
}, 1500);
|
||||
|
||||
// Reload the live view to update all status displays
|
||||
setTimeout(() => {
|
||||
htmx.ajax('GET', `/api/slm-dashboard/live-view/${unitId}`, {
|
||||
target: '#live-view-panel',
|
||||
swap: 'innerHTML'
|
||||
});
|
||||
}, 500);
|
||||
}, 2000);
|
||||
} else {
|
||||
alert(`Error: ${result.detail || 'Unknown error'}`);
|
||||
}
|
||||
@@ -472,7 +683,9 @@ async function controlUnit(unitId, action) {
|
||||
|
||||
|
||||
// Auto-refresh status every 30 seconds
|
||||
let refreshInterval;
|
||||
if (typeof window.refreshInterval === 'undefined') {
|
||||
window.refreshInterval = null;
|
||||
}
|
||||
const REFRESH_INTERVAL_MS = 30000; // 30 seconds
|
||||
const unit_id = '{{ unit.id }}';
|
||||
|
||||
@@ -556,6 +769,182 @@ function stopAutoRefresh() {
|
||||
}
|
||||
|
||||
// Start auto-refresh when page loads
|
||||
|
||||
|
||||
// Backend-Synced Measurement Timer
|
||||
// Uses measurement_start_time from backend (tracked via state transitions)
|
||||
// This works even for manually-started measurements!
|
||||
if (typeof window.timerInterval === 'undefined') {
|
||||
window.timerInterval = null;
|
||||
window.measurementStartTime = null; // ISO string from backend
|
||||
}
|
||||
|
||||
// Format elapsed time as HH:MM:SS
|
||||
function formatElapsedTime(milliseconds) {
|
||||
const totalSeconds = Math.floor(milliseconds / 1000);
|
||||
const hours = Math.floor(totalSeconds / 3600);
|
||||
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
||||
const seconds = totalSeconds % 60;
|
||||
|
||||
return hours.toString().padStart(2, '0') + ':' + minutes.toString().padStart(2, '0') + ':' + seconds.toString().padStart(2, '0');
|
||||
}
|
||||
|
||||
// Update timer display based on backend measurement_start_time
|
||||
function updateTimerDisplay() {
|
||||
const timerElement = document.getElementById('timer-display');
|
||||
if (!timerElement) {
|
||||
console.error('Timer display element not found!');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!window.measurementStartTime) {
|
||||
timerElement.textContent = '00:00:00';
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate elapsed time from backend-provided start time (UTC)
|
||||
// Ensure the timestamp is treated as UTC by adding 'Z' if not present
|
||||
let timestamp = window.measurementStartTime;
|
||||
if (!timestamp.endsWith('Z') && !timestamp.includes('+')) {
|
||||
timestamp = timestamp + 'Z';
|
||||
}
|
||||
const startTime = new Date(timestamp);
|
||||
const elapsed = Date.now() - startTime.getTime();
|
||||
const formattedTime = formatElapsedTime(elapsed);
|
||||
timerElement.textContent = formattedTime;
|
||||
}
|
||||
|
||||
// Sync timer with backend data (with FTP fallback)
|
||||
async function syncTimerWithBackend(measurementState, measurementStartTime) {
|
||||
// Device returns "Start" when measuring, "Stop" when stopped
|
||||
const isMeasuring = measurementState === 'Start';
|
||||
|
||||
if (isMeasuring && measurementStartTime) {
|
||||
// Measurement is running - check both backend and FTP timestamps
|
||||
// Use whichever is earlier (older = actual measurement start)
|
||||
|
||||
// First check FTP for potentially older timestamp
|
||||
try {
|
||||
const response = await fetch(`/api/slmm/${unit_id}/ftp/latest-measurement-time`);
|
||||
const result = await response.json();
|
||||
|
||||
if (result.status === 'ok' && result.latest_timestamp) {
|
||||
const backendTime = new Date(measurementStartTime + 'Z');
|
||||
const ftpTime = new Date(result.latest_timestamp + 'Z');
|
||||
|
||||
// Use the earlier timestamp (represents actual measurement start)
|
||||
if (ftpTime < backendTime) {
|
||||
window.measurementStartTime = result.latest_timestamp;
|
||||
window.timerSource = 'ftp';
|
||||
console.log('Timer synced with FTP folder (earlier):', result.latest_folder, '@', result.latest_timestamp);
|
||||
} else {
|
||||
window.measurementStartTime = measurementStartTime;
|
||||
window.timerSource = 'backend';
|
||||
console.log('Timer synced with backend state (earlier):', measurementStartTime);
|
||||
}
|
||||
} else {
|
||||
// No FTP timestamp, use backend
|
||||
window.measurementStartTime = measurementStartTime;
|
||||
window.timerSource = 'backend';
|
||||
console.log('Timer synced with backend state:', measurementStartTime);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to check FTP timestamp:', error);
|
||||
// Fallback to backend on error
|
||||
window.measurementStartTime = measurementStartTime;
|
||||
window.timerSource = 'backend';
|
||||
console.log('Timer synced with backend state (FTP check failed):', measurementStartTime);
|
||||
}
|
||||
|
||||
// Start interval if not already running
|
||||
if (!window.timerInterval) {
|
||||
window.timerInterval = setInterval(updateTimerDisplay, 1000);
|
||||
console.log('Timer display started');
|
||||
}
|
||||
|
||||
// Update display immediately
|
||||
updateTimerDisplay();
|
||||
} else if (isMeasuring && !measurementStartTime) {
|
||||
// Device is measuring but backend doesn't have start time yet
|
||||
// Try FTP fallback to get measurement start from latest folder timestamp
|
||||
if (!window.measurementStartTime || window.timerSource !== 'ftp') {
|
||||
console.log('Device measuring but no backend start time - checking FTP fallback...');
|
||||
try {
|
||||
const response = await fetch(`/api/slmm/${unit_id}/ftp/latest-measurement-time`);
|
||||
const result = await response.json();
|
||||
|
||||
if (result.status === 'ok' && result.latest_timestamp) {
|
||||
window.measurementStartTime = result.latest_timestamp;
|
||||
window.timerSource = 'ftp';
|
||||
console.log('Timer synced with FTP folder:', result.latest_folder, '@', result.latest_timestamp);
|
||||
|
||||
// Start timer interval if not already running
|
||||
if (!window.timerInterval) {
|
||||
window.timerInterval = setInterval(updateTimerDisplay, 1000);
|
||||
console.log('Timer display started (FTP source)');
|
||||
}
|
||||
|
||||
updateTimerDisplay();
|
||||
} else {
|
||||
console.log('No FTP timestamp available');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to get FTP timestamp:', error);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Not measuring - stop timer
|
||||
if (window.timerInterval) {
|
||||
clearInterval(window.timerInterval);
|
||||
window.timerInterval = null;
|
||||
console.log('Timer display stopped');
|
||||
}
|
||||
window.measurementStartTime = null;
|
||||
window.timerSource = null;
|
||||
document.getElementById('timer-display').textContent = '00:00:00';
|
||||
}
|
||||
}
|
||||
|
||||
// Check measurement state and sync timer
|
||||
async function checkMeasurementState() {
|
||||
try {
|
||||
const response = await fetch(`/api/slmm/${unit_id}/live`);
|
||||
const result = await response.json();
|
||||
|
||||
if (result.status === 'ok' && result.data) {
|
||||
await syncTimerWithBackend(
|
||||
result.data.measurement_state,
|
||||
result.data.measurement_start_time
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to check measurement state:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize timer immediately (this partial is loaded via HTMX, so DOMContentLoaded may have already fired)
|
||||
if (typeof window.timerInitialized === 'undefined') {
|
||||
window.timerInitialized = true;
|
||||
|
||||
// Execute initialization immediately
|
||||
(async function() {
|
||||
console.log('Timer initialization: Starting immediately');
|
||||
|
||||
// Check measurement state immediately
|
||||
await checkMeasurementState();
|
||||
|
||||
// Load store name (index number)
|
||||
await loadStoreName();
|
||||
|
||||
// Check FTP status and update buttons
|
||||
await checkFTPStatus();
|
||||
|
||||
// Check measurement state periodically (every 5 seconds)
|
||||
setInterval(checkMeasurementState, 5000);
|
||||
|
||||
console.log('Timer initialization: Complete, polling every 5 seconds');
|
||||
})();
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', startAutoRefresh);
|
||||
|
||||
// Cleanup on page unload
|
||||
@@ -564,4 +953,510 @@ window.addEventListener('beforeunload', function() {
|
||||
window.currentWebSocket.close();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// File Browser Functions
|
||||
if (typeof window.currentPath === 'undefined') {
|
||||
window.currentPath = '/';
|
||||
}
|
||||
|
||||
// Format file size
|
||||
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];
|
||||
}
|
||||
|
||||
// Format date
|
||||
function formatDate(dateStr) {
|
||||
try {
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleString();
|
||||
} catch (e) {
|
||||
return dateStr;
|
||||
}
|
||||
}
|
||||
|
||||
// Enable FTP on device
|
||||
async function enableFTP() {
|
||||
try {
|
||||
const response = await fetch(`/api/slmm/${unit_id}/ftp/enable`, {
|
||||
method: 'POST'
|
||||
});
|
||||
const result = await response.json();
|
||||
|
||||
if (result.status === 'ok') {
|
||||
document.getElementById('ftp-status').classList.add('hidden');
|
||||
updateFTPButtons(true);
|
||||
refreshFileList();
|
||||
} else {
|
||||
alert('Failed to enable FTP: ' + (result.detail || 'Unknown error'));
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Failed to enable FTP: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function disableFTP() {
|
||||
try {
|
||||
const response = await fetch(`/api/slmm/${unit_id}/ftp/disable`, {
|
||||
method: 'POST'
|
||||
});
|
||||
const result = await response.json();
|
||||
|
||||
if (result.status === 'ok') {
|
||||
updateFTPButtons(false);
|
||||
alert('FTP disabled. TCP commands are now available.');
|
||||
} else {
|
||||
alert('Failed to disable FTP: ' + (result.detail || 'Unknown error'));
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Failed to disable FTP: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
function updateFTPButtons(ftpEnabled) {
|
||||
const enableBtn = document.getElementById('enable-ftp-btn');
|
||||
const disableBtn = document.getElementById('disable-ftp-btn');
|
||||
|
||||
console.log('updateFTPButtons called with ftpEnabled:', ftpEnabled);
|
||||
console.log('Enable button element:', enableBtn);
|
||||
console.log('Disable button element:', disableBtn);
|
||||
|
||||
if (enableBtn && disableBtn) {
|
||||
if (ftpEnabled) {
|
||||
enableBtn.classList.add('hidden');
|
||||
disableBtn.classList.remove('hidden');
|
||||
console.log('FTP is enabled - showing Disable FTP button');
|
||||
} else {
|
||||
enableBtn.classList.remove('hidden');
|
||||
disableBtn.classList.add('hidden');
|
||||
console.log('FTP is disabled - showing Enable FTP button');
|
||||
}
|
||||
} else {
|
||||
console.error('FTP button elements not found in DOM');
|
||||
}
|
||||
}
|
||||
|
||||
async function checkFTPStatus() {
|
||||
console.log('checkFTPStatus: Checking FTP status for unit_id:', unit_id);
|
||||
try {
|
||||
const response = await fetch(`/api/slmm/${unit_id}/ftp/status`);
|
||||
const result = await response.json();
|
||||
|
||||
console.log('checkFTPStatus: Response:', result);
|
||||
|
||||
if (result.status === 'ok') {
|
||||
console.log('checkFTPStatus: FTP enabled status:', result.ftp_enabled);
|
||||
updateFTPButtons(result.ftp_enabled);
|
||||
} else {
|
||||
console.error('checkFTPStatus: Unexpected response status:', result.status);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('checkFTPStatus: Failed to check FTP status:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Clock Management Functions
|
||||
async function syncClockToNow() {
|
||||
const now = new Date();
|
||||
// Format as YYYY-MM-DDTHH:mm for datetime-local input
|
||||
const localDateTime = new Date(now.getTime() - (now.getTimezoneOffset() * 60000)).toISOString().slice(0, 16);
|
||||
|
||||
// Update the input field
|
||||
document.getElementById('clock-input').value = localDateTime;
|
||||
|
||||
// Auto-submit
|
||||
await setDeviceClock();
|
||||
}
|
||||
|
||||
async function setDeviceClock() {
|
||||
const clockInput = document.getElementById('clock-input');
|
||||
const statusDiv = document.getElementById('clock-status');
|
||||
|
||||
if (!clockInput.value) {
|
||||
showClockStatus('Please select a date and time', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert datetime-local to NL43 format: YYYY/MM/DD,HH:MM:SS
|
||||
const dateTime = new Date(clockInput.value);
|
||||
const nl43Format = dateTime.getFullYear() + '/' +
|
||||
String(dateTime.getMonth() + 1).padStart(2, '0') + '/' +
|
||||
String(dateTime.getDate()).padStart(2, '0') + ',' +
|
||||
String(dateTime.getHours()).padStart(2, '0') + ':' +
|
||||
String(dateTime.getMinutes()).padStart(2, '0') + ':' +
|
||||
String(dateTime.getSeconds()).padStart(2, '0');
|
||||
|
||||
showClockStatus('Setting device clock...', 'info');
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/slmm/${unit_id}/clock`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ datetime: nl43Format })
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok && result.status === 'ok') {
|
||||
showClockStatus('✓ Clock set successfully to ' + dateTime.toLocaleString(), 'success');
|
||||
} else {
|
||||
showClockStatus('✗ Failed: ' + (result.detail || 'Unknown error'), 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showClockStatus('✗ Failed: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function showClockStatus(message, type) {
|
||||
const statusDiv = document.getElementById('clock-status');
|
||||
statusDiv.textContent = message;
|
||||
statusDiv.classList.remove('hidden', 'text-green-600', 'text-red-600', 'text-blue-600');
|
||||
|
||||
if (type === 'success') {
|
||||
statusDiv.classList.add('text-green-600', 'dark:text-green-400');
|
||||
} else if (type === 'error') {
|
||||
statusDiv.classList.add('text-red-600', 'dark:text-red-400');
|
||||
} else {
|
||||
statusDiv.classList.add('text-blue-600', 'dark:text-blue-400');
|
||||
}
|
||||
}
|
||||
|
||||
// Store Name (Index) Management Functions
|
||||
async function loadStoreName() {
|
||||
try {
|
||||
const response = await fetch(`/api/slmm/${unit_id}/index-number`);
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok && result.status === 'ok') {
|
||||
const indexNum = result.index_number;
|
||||
document.getElementById('store-name-display').textContent = indexNum;
|
||||
document.getElementById('store-name-input').value = parseInt(indexNum);
|
||||
|
||||
// Check overwrite status after loading
|
||||
await checkOverwriteStatus();
|
||||
} else {
|
||||
console.error('Failed to load store name:', result);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading store name:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function setStoreName() {
|
||||
const input = document.getElementById('store-name-input');
|
||||
const statusDiv = document.getElementById('store-name-status');
|
||||
|
||||
const index = parseInt(input.value);
|
||||
|
||||
if (isNaN(index) || index < 0 || index > 9999) {
|
||||
showStoreNameStatus('Please enter a number between 0 and 9999', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
showStoreNameStatus('Setting store name...', 'info');
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/slmm/${unit_id}/index-number`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ index: index })
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok && result.status === 'ok') {
|
||||
showStoreNameStatus('✓ Store name set successfully to ' + index.toString().padStart(4, '0'), 'success');
|
||||
// Update display
|
||||
document.getElementById('store-name-display').textContent = index.toString().padStart(4, '0');
|
||||
// Check overwrite status for new index
|
||||
await checkOverwriteStatus();
|
||||
} else {
|
||||
showStoreNameStatus('✗ Failed: ' + (result.detail || 'Unknown error'), 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showStoreNameStatus('✗ Failed: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function showStoreNameStatus(message, type) {
|
||||
const statusDiv = document.getElementById('store-name-status');
|
||||
statusDiv.textContent = message;
|
||||
statusDiv.classList.remove('hidden', 'text-green-600', 'text-red-600', 'text-blue-600');
|
||||
|
||||
if (type === 'success') {
|
||||
statusDiv.classList.add('text-green-600', 'dark:text-green-400');
|
||||
} else if (type === 'error') {
|
||||
statusDiv.classList.add('text-red-600', 'dark:text-red-400');
|
||||
} else {
|
||||
statusDiv.classList.add('text-blue-600', 'dark:text-blue-400');
|
||||
}
|
||||
}
|
||||
|
||||
// Check Overwrite Status
|
||||
async function checkOverwriteStatus() {
|
||||
try {
|
||||
const response = await fetch(`/api/slmm/${unit_id}/overwrite-check`);
|
||||
const result = await response.json();
|
||||
|
||||
const warningDiv = document.getElementById('overwrite-warning');
|
||||
const indicator = document.getElementById('overwrite-indicator');
|
||||
|
||||
if (response.ok && result.status === 'ok') {
|
||||
if (result.will_overwrite) {
|
||||
// Show warning
|
||||
warningDiv.classList.remove('hidden');
|
||||
indicator.classList.remove('hidden', 'bg-green-100', 'text-green-800', 'dark:bg-green-900', 'dark:text-green-200');
|
||||
indicator.classList.add('bg-yellow-100', 'text-yellow-800', 'dark:bg-yellow-900', 'dark:text-yellow-200');
|
||||
indicator.textContent = 'Data Exists';
|
||||
} else {
|
||||
// Hide warning
|
||||
warningDiv.classList.add('hidden');
|
||||
indicator.classList.remove('hidden', 'bg-yellow-100', 'text-yellow-800', 'dark:bg-yellow-900', 'dark:text-yellow-200');
|
||||
indicator.classList.add('bg-green-100', 'text-green-800', 'dark:bg-green-900', 'dark:text-green-200');
|
||||
indicator.textContent = 'Safe';
|
||||
}
|
||||
} else {
|
||||
// Hide on error
|
||||
warningDiv.classList.add('hidden');
|
||||
indicator.classList.add('hidden');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking overwrite status:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-increment to find next available index
|
||||
async function autoIncrementStoreName() {
|
||||
showStoreNameStatus('Finding next available index...', 'info');
|
||||
|
||||
try {
|
||||
const currentIndexResponse = await fetch(`/api/slmm/${unit_id}/index-number`);
|
||||
const currentIndexResult = await currentIndexResponse.json();
|
||||
|
||||
if (!currentIndexResponse.ok || currentIndexResult.status !== 'ok') {
|
||||
showStoreNameStatus('✗ Failed to get current index', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
let currentIndex = parseInt(currentIndexResult.index_number);
|
||||
let foundEmpty = false;
|
||||
let checkedCount = 0;
|
||||
const maxChecks = 100; // Safety limit
|
||||
|
||||
// Start from next index
|
||||
currentIndex++;
|
||||
|
||||
while (!foundEmpty && checkedCount < maxChecks && currentIndex <= 9999) {
|
||||
// Set the index
|
||||
const setResponse = await fetch(`/api/slmm/${unit_id}/index-number`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ index: currentIndex })
|
||||
});
|
||||
|
||||
if (!setResponse.ok) {
|
||||
currentIndex++;
|
||||
checkedCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if data exists at this index
|
||||
const checkResponse = await fetch(`/api/slmm/${unit_id}/overwrite-check`);
|
||||
const checkResult = await checkResponse.json();
|
||||
|
||||
if (checkResponse.ok && checkResult.status === 'ok') {
|
||||
if (!checkResult.will_overwrite) {
|
||||
// Found empty slot!
|
||||
foundEmpty = true;
|
||||
document.getElementById('store-name-display').textContent = currentIndex.toString().padStart(4, '0');
|
||||
document.getElementById('store-name-input').value = currentIndex;
|
||||
showStoreNameStatus(`✓ Found empty slot at index ${currentIndex.toString().padStart(4, '0')}`, 'success');
|
||||
await checkOverwriteStatus();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
currentIndex++;
|
||||
checkedCount++;
|
||||
}
|
||||
|
||||
if (!foundEmpty) {
|
||||
if (currentIndex > 9999) {
|
||||
showStoreNameStatus('✗ No empty slots found (reached max 9999)', 'error');
|
||||
} else {
|
||||
showStoreNameStatus(`✗ No empty slot found in next ${maxChecks} indices`, 'error');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
showStoreNameStatus('✗ Error: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Navigate to directory
|
||||
function navigateToDir(path) {
|
||||
window.currentPath = path;
|
||||
document.getElementById('current-path').textContent = path;
|
||||
loadFiles(path);
|
||||
}
|
||||
|
||||
// Go up one directory
|
||||
function navigateUp() {
|
||||
const parts = window.currentPath.split('/').filter(p => p);
|
||||
parts.pop();
|
||||
const newPath = '/' + parts.join('/');
|
||||
navigateToDir(newPath || '/');
|
||||
}
|
||||
|
||||
// Download file
|
||||
async function downloadFile(path, filename) {
|
||||
try {
|
||||
const response = await fetch(`/api/slmm/${unit_id}/ftp/download`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ remote_path: path })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Download failed');
|
||||
}
|
||||
|
||||
// Create blob and download
|
||||
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);
|
||||
} catch (error) {
|
||||
alert('Failed to download file: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Load files from path
|
||||
async function loadFiles(path) {
|
||||
const fileList = document.getElementById('file-list');
|
||||
const loading = document.getElementById('file-list-loading');
|
||||
const error = document.getElementById('file-list-error');
|
||||
const ftpStatus = document.getElementById('ftp-status');
|
||||
|
||||
// Show loading state
|
||||
fileList.classList.add('hidden');
|
||||
loading.classList.remove('hidden');
|
||||
error.classList.add('hidden');
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/slmm/${unit_id}/ftp/files?path=${encodeURIComponent(path)}`);
|
||||
const result = await response.json();
|
||||
|
||||
if (result.status === 'ok') {
|
||||
ftpStatus.classList.add('hidden');
|
||||
displayFiles(result.files || []);
|
||||
} else {
|
||||
throw new Error(result.detail || 'Failed to load files');
|
||||
}
|
||||
} catch (err) {
|
||||
loading.classList.add('hidden');
|
||||
|
||||
// Check if it's an FTP disabled error
|
||||
if (err.message.includes('FTP') || err.message.includes('502')) {
|
||||
ftpStatus.classList.remove('hidden');
|
||||
fileList.classList.remove('hidden');
|
||||
fileList.innerHTML = '<div class="p-8 text-center text-gray-500 dark:text-gray-400">FTP is disabled on the device</div>';
|
||||
} else {
|
||||
error.classList.remove('hidden');
|
||||
document.getElementById('file-list-error-text').textContent = err.message;
|
||||
fileList.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Display files in the list
|
||||
function displayFiles(files) {
|
||||
const fileList = document.getElementById('file-list');
|
||||
const loading = document.getElementById('file-list-loading');
|
||||
|
||||
loading.classList.add('hidden');
|
||||
fileList.classList.remove('hidden');
|
||||
|
||||
if (files.length === 0) {
|
||||
fileList.innerHTML = '<div class="p-8 text-center text-gray-500 dark:text-gray-400">No files found</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort: directories first, then files
|
||||
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);
|
||||
});
|
||||
|
||||
let html = '';
|
||||
|
||||
// Add "up" button if not at root
|
||||
if (window.currentPath !== '/') {
|
||||
html += `
|
||||
<div class="px-4 py-3 hover:bg-gray-50 dark:hover:bg-gray-700 cursor-pointer flex items-center"
|
||||
onclick="navigateUp()">
|
||||
<svg class="w-5 h-5 text-gray-500 mr-3" 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"/>
|
||||
</svg>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300 font-medium">..</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
files.forEach(file => {
|
||||
const icon = file.is_dir ?
|
||||
'<svg class="w-5 h-5 text-blue-500" fill="currentColor" viewBox="0 0 20 20"><path d="M2 6a2 2 0 012-2h5l2 2h5a2 2 0 012 2v6a2 2 0 01-2 2H4a2 2 0 01-2-2V6z"/></svg>' :
|
||||
'<svg class="w-5 h-5 text-gray-500" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4z" clip-rule="evenodd"/></svg>';
|
||||
|
||||
const clickHandler = file.is_dir ?
|
||||
`navigateToDir('${file.path}')` :
|
||||
`downloadFile('${file.path}', '${file.name}')`;
|
||||
|
||||
html += `
|
||||
<div class="px-4 py-3 hover:bg-gray-50 dark:hover:bg-gray-700 cursor-pointer flex items-center justify-between"
|
||||
onclick="${clickHandler}">
|
||||
<div class="flex items-center flex-1 min-w-0">
|
||||
${icon}
|
||||
<div class="ml-3 flex-1 min-w-0">
|
||||
<p class="text-sm text-gray-900 dark:text-white truncate">${file.name}</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
${file.is_dir ? 'Folder' : formatFileSize(file.size)}
|
||||
${file.modified_timestamp ? ' • ' + formatDate(file.modified_timestamp) : ''}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
${!file.is_dir ? `
|
||||
<button onclick="event.stopPropagation(); downloadFile('${file.path}', '${file.name}')"
|
||||
class="ml-2 p-2 text-blue-600 hover:bg-blue-50 dark:hover:bg-blue-900/20 rounded">
|
||||
<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 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/>
|
||||
</svg>
|
||||
</button>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
fileList.innerHTML = html;
|
||||
}
|
||||
|
||||
// Refresh file list
|
||||
function refreshFileList() {
|
||||
loadFiles(window.currentPath);
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user