- Implemented a new API router for managing report templates, including endpoints for listing, creating, retrieving, updating, and deleting templates. - Added a new HTML partial for a unified SLM settings modal, allowing users to configure SLM settings with dynamic modem selection and FTP credentials. - Created a report preview page with an editable data table using jspreadsheet, enabling users to modify report details and download the report as an Excel file.
372 lines
14 KiB
HTML
372 lines
14 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Sound Level Meters - Seismo Fleet Manager{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="mb-8">
|
|
<h1 class="text-3xl font-bold text-gray-900 dark:text-white flex items-center">
|
|
<svg class="w-8 h-8 mr-3 text-seismo-orange" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.536 8.464a5 5 0 010 7.072m2.828-9.9a9 9 0 010 12.728M5.586 15H4a1 1 0 01-1-1v-4a1 1 0 011-1h1.586l4.707-4.707C10.923 3.663 12 4.109 12 5v14c0 .891-1.077 1.337-1.707.707L5.586 15z"></path>
|
|
</svg>
|
|
Sound Level Meters
|
|
</h1>
|
|
<p class="text-gray-600 dark:text-gray-400 mt-1">Monitor and control sound level measurement devices</p>
|
|
</div>
|
|
|
|
<!-- Summary Stats -->
|
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8"
|
|
hx-get="/api/slm-dashboard/stats"
|
|
hx-trigger="load, every 10s"
|
|
hx-swap="innerHTML">
|
|
<!-- Stats will be loaded here -->
|
|
<div class="animate-pulse bg-gray-200 dark:bg-gray-700 h-24 rounded-xl"></div>
|
|
<div class="animate-pulse bg-gray-200 dark:bg-gray-700 h-24 rounded-xl"></div>
|
|
<div class="animate-pulse bg-gray-200 dark:bg-gray-700 h-24 rounded-xl"></div>
|
|
<div class="animate-pulse bg-gray-200 dark:bg-gray-700 h-24 rounded-xl"></div>
|
|
</div>
|
|
|
|
<!-- Device List with Quick Actions -->
|
|
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-lg p-6 mb-8">
|
|
<div class="flex items-center justify-between mb-6">
|
|
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">Active Devices</h2>
|
|
<div class="flex items-center gap-3">
|
|
<span class="text-sm text-gray-500 dark:text-gray-400">Auto-refresh: 15s</span>
|
|
<a href="/roster" class="text-sm text-seismo-orange hover:text-seismo-navy">Manage roster</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="slm-devices-list"
|
|
class="space-y-3"
|
|
hx-get="/api/slm-dashboard/units?include_measurement=true"
|
|
hx-trigger="load, every 15s"
|
|
hx-swap="innerHTML">
|
|
<div class="animate-pulse space-y-3">
|
|
<div class="bg-gray-200 dark:bg-gray-700 h-32 rounded-lg"></div>
|
|
<div class="bg-gray-200 dark:bg-gray-700 h-32 rounded-lg"></div>
|
|
<div class="bg-gray-200 dark:bg-gray-700 h-32 rounded-lg"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Live Measurement Chart - shows when a device is selected -->
|
|
<div id="live-chart-panel" class="hidden bg-white dark:bg-slate-800 rounded-xl shadow-lg p-6 mb-8">
|
|
<div class="flex items-center justify-between mb-6">
|
|
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">Live Measurements</h2>
|
|
<button onclick="closeLiveChart()" 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>
|
|
|
|
<!-- Current Metrics -->
|
|
<div class="grid grid-cols-5 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 (Instant)</p>
|
|
<p id="chart-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="chart-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="chart-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="chart-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 class="bg-orange-50 dark:bg-orange-900/20 rounded-lg p-4">
|
|
<p class="text-xs text-gray-600 dark:text-gray-400 mb-1">Lpeak (Peak)</p>
|
|
<p id="chart-lpeak" class="text-2xl font-bold text-orange-600 dark:text-orange-400">--</p>
|
|
<p class="text-xs text-gray-500 dark:text-gray-400">dB</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Chart -->
|
|
<div class="bg-gray-50 dark:bg-gray-900/50 rounded-lg p-4" style="min-height: 400px;">
|
|
<canvas id="dashboardLiveChart"></canvas>
|
|
</div>
|
|
|
|
<!-- Stream Control -->
|
|
<div class="mt-4 flex justify-center gap-3">
|
|
<button id="start-chart-stream" onclick="startDashboardStream()"
|
|
class="px-6 py-2 bg-seismo-orange hover:bg-orange-600 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="M13 10V3L4 14h7v7l9-11h-7z"></path>
|
|
</svg>
|
|
Start Live Stream
|
|
</button>
|
|
<button id="stop-chart-stream" onclick="stopDashboardStream()" style="display: none;"
|
|
class="px-6 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 Live Stream
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Projects Overview -->
|
|
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-lg p-6">
|
|
<div class="flex items-center justify-between mb-4">
|
|
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">Active Projects</h2>
|
|
<a href="/projects" class="text-sm text-seismo-orange hover:text-seismo-navy">View all</a>
|
|
</div>
|
|
|
|
<div id="slm-projects-list"
|
|
class="space-y-3 max-h-[400px] overflow-y-auto"
|
|
hx-get="/api/projects/list?status=active&project_type_id=sound_monitoring&view=compact"
|
|
hx-trigger="load, every 60s"
|
|
hx-swap="innerHTML">
|
|
<div class="animate-pulse space-y-3">
|
|
<div class="bg-gray-200 dark:bg-gray-700 h-20 rounded-lg"></div>
|
|
<div class="bg-gray-200 dark:bg-gray-700 h-20 rounded-lg"></div>
|
|
<div class="bg-gray-200 dark:bg-gray-700 h-20 rounded-lg"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Unified SLM Settings Modal -->
|
|
{% include 'partials/slm_settings_modal.html' %}
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
<script>
|
|
// Global variables
|
|
window.dashboardChart = null;
|
|
window.dashboardWebSocket = null;
|
|
window.selectedUnitId = null;
|
|
window.dashboardChartData = {
|
|
timestamps: [],
|
|
lp: [],
|
|
leq: []
|
|
};
|
|
|
|
// Initialize Chart.js
|
|
function initializeDashboardChart() {
|
|
if (typeof Chart === 'undefined') {
|
|
setTimeout(initializeDashboardChart, 100);
|
|
return;
|
|
}
|
|
|
|
const canvas = document.getElementById('dashboardLiveChart');
|
|
if (!canvas) return;
|
|
|
|
if (window.dashboardChart) {
|
|
window.dashboardChart.destroy();
|
|
}
|
|
|
|
const ctx = canvas.getContext('2d');
|
|
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.dashboardChart = new Chart(ctx, {
|
|
type: 'line',
|
|
data: {
|
|
labels: [],
|
|
datasets: [
|
|
{
|
|
label: 'Lp (Instantaneous)',
|
|
data: [],
|
|
borderColor: 'rgb(59, 130, 246)',
|
|
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
|
tension: 0.3,
|
|
borderWidth: 2,
|
|
pointRadius: 0
|
|
},
|
|
{
|
|
label: 'Leq (Equivalent)',
|
|
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 }
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Show live chart for a specific unit
|
|
function showLiveChart(unitId) {
|
|
window.selectedUnitId = unitId;
|
|
const panel = document.getElementById('live-chart-panel');
|
|
panel.classList.remove('hidden');
|
|
|
|
// Initialize chart if needed
|
|
if (!window.dashboardChart) {
|
|
initializeDashboardChart();
|
|
}
|
|
|
|
// Reset data
|
|
window.dashboardChartData = {
|
|
timestamps: [],
|
|
lp: [],
|
|
leq: []
|
|
};
|
|
|
|
// Scroll to chart
|
|
panel.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
}
|
|
|
|
function closeLiveChart() {
|
|
stopDashboardStream();
|
|
document.getElementById('live-chart-panel').classList.add('hidden');
|
|
window.selectedUnitId = null;
|
|
}
|
|
|
|
// WebSocket streaming
|
|
function startDashboardStream() {
|
|
if (!window.selectedUnitId) return;
|
|
|
|
// Close existing connection
|
|
if (window.dashboardWebSocket) {
|
|
window.dashboardWebSocket.close();
|
|
}
|
|
|
|
// Reset chart data
|
|
window.dashboardChartData = { timestamps: [], lp: [], leq: [] };
|
|
if (window.dashboardChart) {
|
|
window.dashboardChart.data.labels = [];
|
|
window.dashboardChart.data.datasets[0].data = [];
|
|
window.dashboardChart.data.datasets[1].data = [];
|
|
window.dashboardChart.update();
|
|
}
|
|
|
|
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
const wsUrl = `${wsProtocol}//${window.location.host}/api/slmm/${window.selectedUnitId}/live`;
|
|
|
|
window.dashboardWebSocket = new WebSocket(wsUrl);
|
|
|
|
window.dashboardWebSocket.onopen = function() {
|
|
console.log('Dashboard WebSocket connected');
|
|
document.getElementById('start-chart-stream').style.display = 'none';
|
|
document.getElementById('stop-chart-stream').style.display = 'flex';
|
|
};
|
|
|
|
window.dashboardWebSocket.onmessage = function(event) {
|
|
try {
|
|
const data = JSON.parse(event.data);
|
|
updateDashboardMetrics(data);
|
|
updateDashboardChart(data);
|
|
} catch (error) {
|
|
console.error('Error parsing WebSocket message:', error);
|
|
}
|
|
};
|
|
|
|
window.dashboardWebSocket.onerror = function(error) {
|
|
console.error('Dashboard WebSocket error:', error);
|
|
};
|
|
|
|
window.dashboardWebSocket.onclose = function() {
|
|
console.log('Dashboard WebSocket closed');
|
|
document.getElementById('start-chart-stream').style.display = 'flex';
|
|
document.getElementById('stop-chart-stream').style.display = 'none';
|
|
};
|
|
}
|
|
|
|
function stopDashboardStream() {
|
|
if (window.dashboardWebSocket) {
|
|
window.dashboardWebSocket.close();
|
|
window.dashboardWebSocket = null;
|
|
}
|
|
}
|
|
|
|
function updateDashboardMetrics(data) {
|
|
document.getElementById('chart-lp').textContent = data.lp || '--';
|
|
document.getElementById('chart-leq').textContent = data.leq || '--';
|
|
document.getElementById('chart-lmax').textContent = data.lmax || '--';
|
|
document.getElementById('chart-lmin').textContent = data.lmin || '--';
|
|
document.getElementById('chart-lpeak').textContent = data.lpeak || '--';
|
|
}
|
|
|
|
function updateDashboardChart(data) {
|
|
const now = new Date();
|
|
window.dashboardChartData.timestamps.push(now.toLocaleTimeString());
|
|
window.dashboardChartData.lp.push(parseFloat(data.lp || 0));
|
|
window.dashboardChartData.leq.push(parseFloat(data.leq || 0));
|
|
|
|
// Keep only last 60 data points
|
|
if (window.dashboardChartData.timestamps.length > 60) {
|
|
window.dashboardChartData.timestamps.shift();
|
|
window.dashboardChartData.lp.shift();
|
|
window.dashboardChartData.leq.shift();
|
|
}
|
|
|
|
if (window.dashboardChart) {
|
|
window.dashboardChart.data.labels = window.dashboardChartData.timestamps;
|
|
window.dashboardChart.data.datasets[0].data = window.dashboardChartData.lp;
|
|
window.dashboardChart.data.datasets[1].data = window.dashboardChartData.leq;
|
|
window.dashboardChart.update('none');
|
|
}
|
|
}
|
|
|
|
// Configuration modal - use unified SLM settings modal
|
|
function openDeviceConfigModal(unitId) {
|
|
// Call the unified modal function from slm_settings_modal.html
|
|
if (typeof openSLMSettingsModal === 'function') {
|
|
openSLMSettingsModal(unitId);
|
|
} else {
|
|
console.error('openSLMSettingsModal not found');
|
|
}
|
|
}
|
|
|
|
function closeDeviceConfigModal() {
|
|
// Call the unified modal close function
|
|
if (typeof closeSLMSettingsModal === 'function') {
|
|
closeSLMSettingsModal();
|
|
}
|
|
}
|
|
|
|
// Cleanup on page unload
|
|
window.addEventListener('beforeunload', function() {
|
|
stopDashboardStream();
|
|
});
|
|
</script>
|
|
{% endblock %}
|