261 lines
11 KiB
HTML
261 lines
11 KiB
HTML
<!-- Live View Panel for {{ unit.id }} -->
|
|
<div class="h-full flex flex-col">
|
|
<!-- 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>
|
|
{% if modem %}
|
|
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
|
via Modem: {{ modem.id }}{% if modem_ip %} ({{ modem_ip }}){% endif %}
|
|
</p>
|
|
{% elif modem_ip %}
|
|
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
|
Direct: {{ modem_ip }}
|
|
</p>
|
|
{% else %}
|
|
<p class="text-xs text-red-500 dark:text-red-400 mt-1">
|
|
⚠️ No modem assigned or IP configured
|
|
</p>
|
|
{% 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 %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Control Buttons -->
|
|
<div class="flex gap-2 mb-6">
|
|
<button onclick="controlUnit('{{ unit.id }}', 'start')"
|
|
class="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-lg flex items-center">
|
|
<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="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"></path>
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
|
</svg>
|
|
Start
|
|
</button>
|
|
|
|
<button onclick="controlUnit('{{ unit.id }}', 'pause')"
|
|
class="px-4 py-2 bg-yellow-600 hover:bg-yellow-700 text-white rounded-lg flex items-center">
|
|
<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="M10 9v6m4-6v6m7-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
|
</svg>
|
|
Pause
|
|
</button>
|
|
|
|
<button onclick="controlUnit('{{ unit.id }}', 'stop')"
|
|
class="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg flex items-center">
|
|
<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="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 10a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4z"></path>
|
|
</svg>
|
|
Stop
|
|
</button>
|
|
|
|
<button onclick="controlUnit('{{ unit.id }}', 'reset')"
|
|
class="px-4 py-2 bg-gray-600 hover:bg-gray-700 text-white rounded-lg flex items-center">
|
|
<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="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>
|
|
Reset
|
|
</button>
|
|
|
|
<button onclick="initLiveDataStream('{{ unit.id }}')"
|
|
class="px-4 py-2 bg-seismo-orange hover:bg-orange-600 text-white rounded-lg flex items-center ml-auto">
|
|
<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="M13 10V3L4 14h7v7l9-11h-7z"></path>
|
|
</svg>
|
|
Start Live Stream
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Current Metrics -->
|
|
<div class="grid grid-cols-4 gap-4 mb-6">
|
|
<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 (Current)</p>
|
|
<p id="live-lp" class="text-2xl font-bold text-blue-600 dark:text-blue-400">
|
|
{% if current_status and current_status.lp %}{{ current_status.lp }}{% else %}--{% endif %}
|
|
</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="live-leq" class="text-2xl font-bold text-green-600 dark:text-green-400">
|
|
{% if current_status and current_status.leq %}{{ current_status.leq }}{% else %}--{% endif %}
|
|
</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 (Peak)</p>
|
|
<p id="live-lmax" class="text-2xl font-bold text-red-600 dark:text-red-400">
|
|
{% if current_status and current_status.lmax %}{{ current_status.lmax }}{% else %}--{% endif %}
|
|
</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</p>
|
|
<p id="live-lmin" class="text-2xl font-bold text-purple-600 dark:text-purple-400">
|
|
{% if current_status and current_status.lmin %}{{ current_status.lmin }}{% else %}--{% endif %}
|
|
</p>
|
|
<p class="text-xs text-gray-500 dark:text-gray-400">dB</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Live Chart -->
|
|
<div class="flex-1 bg-gray-50 dark:bg-gray-900/50 rounded-lg p-4">
|
|
<canvas id="liveChart"></canvas>
|
|
</div>
|
|
|
|
<!-- Device Info -->
|
|
<div class="mt-4 grid grid-cols-2 gap-4 text-sm">
|
|
<div>
|
|
<span class="text-gray-600 dark:text-gray-400">Battery:</span>
|
|
<span class="font-medium text-gray-900 dark:text-white ml-2">
|
|
{% if current_status and current_status.battery_level %}{{ current_status.battery_level }}%{% else %}--{% endif %}
|
|
</span>
|
|
</div>
|
|
<div>
|
|
<span class="text-gray-600 dark:text-gray-400">Power:</span>
|
|
<span class="font-medium text-gray-900 dark:text-white ml-2">
|
|
{% if current_status and current_status.power_source %}{{ current_status.power_source }}{% else %}--{% endif %}
|
|
</span>
|
|
</div>
|
|
<div>
|
|
<span class="text-gray-600 dark:text-gray-400">Weighting:</span>
|
|
<span class="font-medium text-gray-900 dark:text-white ml-2">
|
|
{% if unit.slm_frequency_weighting %}{{ unit.slm_frequency_weighting }}{% else %}--{% endif %} /
|
|
{% if unit.slm_time_weighting %}{{ unit.slm_time_weighting }}{% else %}--{% endif %}
|
|
</span>
|
|
</div>
|
|
<div>
|
|
<span class="text-gray-600 dark:text-gray-400">SD Remaining:</span>
|
|
<span class="font-medium text-gray-900 dark:text-white ml-2">
|
|
{% if current_status and current_status.sd_remaining_mb %}{{ current_status.sd_remaining_mb }} MB{% else %}--{% endif %}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
<script>
|
|
// Initialize Chart.js for live data visualization
|
|
const ctx = document.getElementById('liveChart').getContext('2d');
|
|
|
|
// Dark mode detection
|
|
const isDarkMode = document.documentElement.classList.contains('dark');
|
|
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, {
|
|
type: 'line',
|
|
data: {
|
|
labels: [],
|
|
datasets: [
|
|
{
|
|
label: 'Lp (Current Level)',
|
|
data: [],
|
|
borderColor: 'rgb(59, 130, 246)',
|
|
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
|
tension: 0.3,
|
|
borderWidth: 2,
|
|
pointRadius: 0
|
|
},
|
|
{
|
|
label: 'Leq (Average)',
|
|
data: [],
|
|
borderColor: 'rgb(34, 197, 94)',
|
|
backgroundColor: 'rgba(34, 197, 94, 0.1)',
|
|
tension: 0.3,
|
|
borderWidth: 2,
|
|
pointRadius: 0
|
|
}
|
|
]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
animation: false,
|
|
interaction: {
|
|
intersect: false,
|
|
mode: 'index'
|
|
},
|
|
scales: {
|
|
x: {
|
|
display: true,
|
|
grid: {
|
|
color: gridColor
|
|
},
|
|
ticks: {
|
|
color: textColor,
|
|
maxTicksLimit: 10
|
|
}
|
|
},
|
|
y: {
|
|
display: true,
|
|
title: {
|
|
display: true,
|
|
text: 'Sound Level (dB)',
|
|
color: textColor
|
|
},
|
|
grid: {
|
|
color: gridColor
|
|
},
|
|
ticks: {
|
|
color: textColor
|
|
},
|
|
min: 30,
|
|
max: 130
|
|
}
|
|
},
|
|
plugins: {
|
|
legend: {
|
|
labels: {
|
|
color: textColor
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Control function
|
|
async function controlUnit(unitId, action) {
|
|
try {
|
|
const response = await fetch(`/api/slm-dashboard/control/${unitId}/${action}`, {
|
|
method: 'POST'
|
|
});
|
|
const result = await response.json();
|
|
|
|
if (result.status === 'ok') {
|
|
// Reload the live view to update status
|
|
setTimeout(() => {
|
|
htmx.ajax('GET', `/api/slm-dashboard/live-view/${unitId}`, {
|
|
target: '#live-view-panel',
|
|
swap: 'innerHTML'
|
|
});
|
|
}, 500);
|
|
} else {
|
|
alert(`Error: ${result.detail || 'Unknown error'}`);
|
|
}
|
|
} catch (error) {
|
|
alert(`Failed to control unit: ${error.message}`);
|
|
}
|
|
}
|
|
</script>
|