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:
@@ -4,8 +4,13 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">Sound Level Meters</h1>
|
||||
<p class="text-gray-600 dark:text-gray-400 mt-1">Monitor and manage sound level measurement devices</p>
|
||||
<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 -->
|
||||
@@ -20,45 +25,114 @@
|
||||
<div class="animate-pulse bg-gray-200 dark:bg-gray-700 h-24 rounded-xl"></div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content Grid -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<!-- Projects Card -->
|
||||
<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">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-[600px] 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>
|
||||
<!-- 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>
|
||||
|
||||
<!-- Devices Card -->
|
||||
<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">Devices</h2>
|
||||
<a href="/roster" class="text-sm text-seismo-orange hover:text-seismo-navy">Manage roster</a>
|
||||
<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 id="slm-devices-list"
|
||||
class="space-y-3 max-h-[600px] overflow-y-auto"
|
||||
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-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 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>
|
||||
@@ -85,7 +159,213 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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
|
||||
function openDeviceConfigModal(unitId) {
|
||||
const modal = document.getElementById('slm-config-modal');
|
||||
modal.classList.remove('hidden');
|
||||
@@ -111,5 +391,10 @@ document.getElementById('slm-config-modal')?.addEventListener('click', function(
|
||||
closeDeviceConfigModal();
|
||||
}
|
||||
});
|
||||
|
||||
// Cleanup on page unload
|
||||
window.addEventListener('beforeunload', function() {
|
||||
stopDashboardStream();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user