SLM dashboard rework, diagnostics and command pages added

This commit is contained in:
serversdwn
2026-01-12 04:42:08 +00:00
parent ee025f1f34
commit e1b965c24c
4 changed files with 513 additions and 155 deletions

View File

@@ -0,0 +1,206 @@
<!-- Compact Diagnostics Card for {{ unit.id }} -->
<div class="h-full flex flex-col p-6">
<!-- Header -->
<div class="flex items-center justify-between mb-6">
<div>
<h2 class="text-2xl font-bold text-gray-900 dark:text-white">{{ unit.id }}</h2>
<p class="text-sm text-gray-600 dark:text-gray-400">
{% if unit.slm_model %}{{ unit.slm_model }}{% endif %}
{% if unit.slm_serial_number %} • S/N: {{ unit.slm_serial_number }}{% endif %}
</p>
</div>
<!-- Status Badge -->
<span id="diag-status-badge" class="px-4 py-2 bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg font-medium">
Loading...
</span>
</div>
<!-- Connection Status -->
<div class="bg-white dark:bg-gray-800 rounded-lg p-4 border border-gray-200 dark:border-gray-700 mb-4">
<div class="flex items-center justify-between">
<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="M5.05 3.636a1 1 0 010 1.414 7 7 0 000 9.9 1 1 0 11-1.414 1.414 9 9 0 010-12.728 1 1 0 011.414 0zm9.9 0a1 1 0 011.414 0 9 9 0 010 12.728 1 1 0 11-1.414-1.414 7 7 0 000-9.9 1 1 0 010-1.414zM7.879 6.464a1 1 0 010 1.414 3 3 0 000 4.243 1 1 0 11-1.415 1.414 5 5 0 010-7.07 1 1 0 011.415 0zm4.242 0a1 1 0 011.415 0 5 5 0 010 7.072 1 1 0 01-1.415-1.415 3 3 0 000-4.242 1 1 0 010-1.415zM10 9a1 1 0 011 1v.01a1 1 0 11-2 0V10a1 1 0 011-1z" clip-rule="evenodd"/>
</svg>
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">Connection</span>
</div>
<div>
{% if modem %}
<span class="text-sm text-gray-600 dark:text-gray-400">via {{ modem.id }}</span>
{% elif modem_ip %}
<span class="text-sm text-gray-600 dark:text-gray-400">Direct: {{ modem_ip }}</span>
{% else %}
<span class="text-sm text-red-600 dark:text-red-400">Not configured</span>
{% endif %}
<span id="connection-status" class="ml-2 w-2 h-2 bg-gray-400 rounded-full inline-block"></span>
</div>
</div>
</div>
<!-- Current Sound Levels -->
<div class="grid grid-cols-2 gap-3 mb-4">
<div class="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-4">
<p class="text-xs text-gray-600 dark:text-gray-400 mb-1">Lp (Instant)</p>
<p id="diag-lp" class="text-2xl font-bold text-blue-600 dark:text-blue-400">--</p>
<p class="text-xs text-gray-500 dark:text-gray-400">dB</p>
</div>
<div class="bg-green-50 dark:bg-green-900/20 rounded-lg p-4">
<p class="text-xs text-gray-600 dark:text-gray-400 mb-1">Leq (Average)</p>
<p id="diag-leq" class="text-2xl font-bold text-green-600 dark:text-green-400">--</p>
<p class="text-xs text-gray-500 dark:text-gray-400">dB</p>
</div>
<div class="bg-red-50 dark:bg-red-900/20 rounded-lg p-4">
<p class="text-xs text-gray-600 dark:text-gray-400 mb-1">Lmax (Max)</p>
<p id="diag-lmax" class="text-2xl font-bold text-red-600 dark:text-red-400">--</p>
<p class="text-xs text-gray-500 dark:text-gray-400">dB</p>
</div>
<div class="bg-purple-50 dark:bg-purple-900/20 rounded-lg p-4">
<p class="text-xs text-gray-600 dark:text-gray-400 mb-1">Lmin (Min)</p>
<p id="diag-lmin" class="text-2xl font-bold text-purple-600 dark:text-purple-400">--</p>
<p class="text-xs text-gray-500 dark:text-gray-400">dB</p>
</div>
</div>
<!-- Battery and Power -->
<div class="grid grid-cols-2 gap-3 mb-4">
<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-2">
<span class="text-xs text-gray-600 dark:text-gray-400">Battery</span>
<svg class="w-4 h-4 text-gray-500" fill="currentColor" viewBox="0 0 20 20">
<path d="M6 2a2 2 0 00-2 2v12a2 2 0 002 2h8a2 2 0 002-2V4a2 2 0 00-2-2H6zm0 2h8v12H6V4zm7 2a1 1 0 011 1v6a1 1 0 11-2 0V7a1 1 0 011-1z"/>
</svg>
</div>
<div id="diag-battery-level" class="text-xl font-bold text-gray-900 dark:text-white">--</div>
<div class="mt-2 bg-gray-200 dark:bg-gray-700 rounded-full h-2">
<div id="diag-battery-bar" class="bg-gray-400 h-2 rounded-full transition-all" style="width: 0%"></div>
</div>
</div>
<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-2">
<span class="text-xs text-gray-600 dark:text-gray-400">Power</span>
<svg class="w-4 h-4 text-gray-500" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M11.3 1.046A1 1 0 0112 2v5h4a1 1 0 01.82 1.573l-7 10A1 1 0 018 18v-5H4a1 1 0 01-.82-1.573l7-10a1 1 0 011.12-.38z" clip-rule="evenodd"/>
</svg>
</div>
<div id="diag-power-source" class="text-lg font-semibold text-gray-900 dark:text-white">--</div>
</div>
</div>
<!-- Last Check-in -->
<div class="bg-white dark:bg-gray-800 rounded-lg p-4 border border-gray-200 dark:border-gray-700 mb-6">
<div class="flex items-center justify-between">
<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">Last Check-in</span>
</div>
<div>
{% if unit.slm_last_check %}
<span class="text-sm text-gray-600 dark:text-gray-400">{{ unit.slm_last_check.strftime('%Y-%m-%d %H:%M:%S') }}</span>
{% else %}
<span class="text-sm text-gray-500 dark:text-gray-500">Never</span>
{% endif %}
</div>
</div>
</div>
<!-- Open Command Center Button -->
<div class="mt-auto">
<button onclick="openCommandCenter('{{ unit.id }}')"
class="w-full px-6 py-3 bg-seismo-orange hover:bg-orange-600 text-white rounded-lg font-medium flex items-center justify-center transition-colors">
<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="M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zM9 10l12-3"/>
</svg>
Open Command Center
</button>
</div>
</div>
<script>
(function() {
const diagUnitId = '{{ unit.id }}';
// Clear any existing connections before starting new ones
window.SLMConnectionManager.setCurrentUnit(diagUnitId);
function updateDiagnosticsData() {
fetch(`/api/slmm/${diagUnitId}/live`)
.then(response => response.json())
.then(result => {
if (result.status === 'ok' && result.data) {
const data = result.data;
// Update status badge
const statusBadge = document.getElementById('diag-status-badge');
if (statusBadge) {
const isMeasuring = data.measurement_state === 'Start';
if (isMeasuring) {
statusBadge.className = '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';
statusBadge.innerHTML = '<span class="w-2 h-2 bg-green-500 rounded-full mr-2 animate-pulse"></span>Measuring';
} else {
statusBadge.className = 'px-4 py-2 bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg font-medium';
statusBadge.textContent = 'Stopped';
}
}
// Update sound levels
['lp', 'leq', 'lmax', 'lmin'].forEach(metric => {
const el = document.getElementById(`diag-${metric}`);
if (el) el.textContent = data[metric] || '--';
});
// Update battery
const batteryEl = document.getElementById('diag-battery-level');
const batteryBar = document.getElementById('diag-battery-bar');
if (batteryEl && data.battery_level) {
const level = parseInt(data.battery_level);
batteryEl.textContent = `${level}%`;
if (batteryBar) {
batteryBar.style.width = `${level}%`;
if (level > 50) {
batteryBar.className = 'bg-green-500 h-2 rounded-full transition-all';
} else if (level > 20) {
batteryBar.className = 'bg-yellow-500 h-2 rounded-full transition-all';
} else {
batteryBar.className = 'bg-red-500 h-2 rounded-full transition-all';
}
}
}
// Update power source
const powerEl = document.getElementById('diag-power-source');
if (powerEl) powerEl.textContent = data.power_source || '--';
// Update connection status
const connStatus = document.getElementById('connection-status');
if (connStatus) {
connStatus.className = 'ml-2 w-2 h-2 bg-green-500 rounded-full inline-block';
}
}
})
.catch(error => {
console.error('Failed to refresh diagnostics:', error);
const connStatus = document.getElementById('connection-status');
if (connStatus) {
connStatus.className = 'ml-2 w-2 h-2 bg-red-500 rounded-full inline-block';
}
});
}
// Initial update
updateDiagnosticsData();
// Set up refresh interval and register it
const interval = setInterval(updateDiagnosticsData, 10000);
window.SLMConnectionManager.registerInterval(interval);
console.log(`Diagnostics card for ${diagUnitId} initialized`);
})();
</script>

View File

@@ -686,8 +686,13 @@ async function controlUnit(unitId, action) {
if (typeof window.refreshInterval === 'undefined') {
window.refreshInterval = null;
}
const REFRESH_INTERVAL_MS = 30000; // 30 seconds
const unit_id = '{{ unit.id }}';
if (typeof window.REFRESH_INTERVAL_MS === 'undefined') {
window.REFRESH_INTERVAL_MS = 30000; // 30 seconds
}
if (typeof window.unit_id === 'undefined' || window.unit_id !== '{{ unit.id }}') {
// Keep HTMX reloads from reusing the old unit id
window.unit_id = '{{ unit.id }}';
}
function updateDeviceStatus() {
fetch(`/api/slmm/${unit_id}/live`)
@@ -755,7 +760,7 @@ function startAutoRefresh() {
updateDeviceStatus();
// Set up interval
refreshInterval = setInterval(updateDeviceStatus, REFRESH_INTERVAL_MS);
refreshInterval = setInterval(updateDeviceStatus, window.REFRESH_INTERVAL_MS);
console.log('Auto-refresh started (30s interval)');
}
@@ -778,6 +783,12 @@ if (typeof window.timerInterval === 'undefined') {
window.timerInterval = null;
window.measurementStartTime = null; // ISO string from backend
}
if (typeof window.timerSource === 'undefined') {
window.timerSource = null;
}
if (typeof window.lastFtpErrorTime === 'undefined') {
window.lastFtpErrorTime = null;
}
// Format elapsed time as HH:MM:SS
function formatElapsedTime(milliseconds) {
@@ -818,42 +829,52 @@ function updateTimerDisplay() {
async function syncTimerWithBackend(measurementState, measurementStartTime) {
// Device returns "Start" when measuring, "Stop" when stopped
const isMeasuring = measurementState === 'Start';
const now = Date.now();
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
const shouldSkipFtp = window.lastFtpErrorTime && (now - window.lastFtpErrorTime < 10000);
if (shouldSkipFtp) {
window.measurementStartTime = measurementStartTime;
window.timerSource = 'backend';
console.log('Timer synced with backend state (FTP check failed):', measurementStartTime);
console.log('Timer using backend state (skipping FTP due to recent error):', measurementStartTime);
} else {
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);
}
window.lastFtpErrorTime = null;
} 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);
window.lastFtpErrorTime = now;
// 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
@@ -869,27 +890,34 @@ async function syncTimerWithBackend(measurementState, measurementStartTime) {
// 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();
const skipFtp = window.lastFtpErrorTime && (Date.now() - window.lastFtpErrorTime < 10000);
if (!skipFtp) {
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);
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)');
// Start timer interval if not already running
if (!window.timerInterval) {
window.timerInterval = setInterval(updateTimerDisplay, 1000);
console.log('Timer display started (FTP source)');
}
updateTimerDisplay();
window.lastFtpErrorTime = null;
} else {
console.log('No FTP timestamp available');
}
updateTimerDisplay();
} else {
console.log('No FTP timestamp available');
} catch (error) {
console.error('Failed to get FTP timestamp:', error);
window.lastFtpErrorTime = Date.now();
}
} catch (error) {
console.error('Failed to get FTP timestamp:', error);
} else {
console.log('Skipping FTP fallback due to recent error');
}
}
} else {