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:
@@ -18,13 +18,61 @@
|
||||
</a>
|
||||
</nav>
|
||||
{% else %}
|
||||
<a href="/roster" class="text-seismo-orange hover:text-seismo-orange-dark flex items-center">
|
||||
<a href="#" onclick="goBack(event)" class="text-seismo-orange hover:text-seismo-orange-dark flex items-center">
|
||||
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
|
||||
</svg>
|
||||
Back to Roster
|
||||
<span id="back-link-text">Back to Sound Level Meters</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<script>
|
||||
function goBack(event) {
|
||||
event.preventDefault();
|
||||
|
||||
// Check if there's a previous page in history
|
||||
// and it's from the same site (not external)
|
||||
if (window.history.length > 1 && document.referrer) {
|
||||
const referrer = new URL(document.referrer);
|
||||
const current = new URL(window.location.href);
|
||||
|
||||
// If referrer is from the same origin, go back
|
||||
if (referrer.origin === current.origin) {
|
||||
window.history.back();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, go to SLM dashboard
|
||||
window.location.href = '/sound-level-meters';
|
||||
}
|
||||
|
||||
// Update the back link text based on referrer
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const backText = document.getElementById('back-link-text');
|
||||
if (backText && document.referrer) {
|
||||
try {
|
||||
const referrer = new URL(document.referrer);
|
||||
const current = new URL(window.location.href);
|
||||
|
||||
// Only update if from same origin
|
||||
if (referrer.origin === current.origin) {
|
||||
if (referrer.pathname.includes('/sound-level-meters')) {
|
||||
backText.textContent = 'Back to Sound Level Meters';
|
||||
} else if (referrer.pathname.includes('/roster')) {
|
||||
backText.textContent = 'Back to Roster';
|
||||
} else if (referrer.pathname.includes('/projects')) {
|
||||
backText.textContent = 'Back to Projects';
|
||||
} else if (referrer.pathname === '/') {
|
||||
backText.textContent = 'Back to Dashboard';
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Invalid referrer, keep default text
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
|
||||
<div class="mb-8">
|
||||
@@ -52,159 +100,16 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Control Panel -->
|
||||
<div class="mb-8">
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">Control Panel</h2>
|
||||
<div hx-get="/slm/partials/{{ unit_id }}/controls"
|
||||
hx-trigger="load, every 5s"
|
||||
<!-- Command Center -->
|
||||
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-lg p-6">
|
||||
<div id="slm-command-center"
|
||||
hx-get="/api/slm-dashboard/live-view/{{ unit_id }}"
|
||||
hx-trigger="load"
|
||||
hx-swap="innerHTML">
|
||||
<div class="text-center py-8 text-gray-500">Loading controls...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Real-time Data Stream -->
|
||||
<div class="mb-8">
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">Real-time Measurements</h2>
|
||||
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-lg p-6">
|
||||
<div id="slm-stream-container">
|
||||
<div class="text-center py-8">
|
||||
<button onclick="startStream()"
|
||||
id="stream-start-btn"
|
||||
class="px-6 py-3 bg-seismo-orange text-white rounded-lg hover:bg-seismo-orange-dark transition-colors">
|
||||
Start Real-time Stream
|
||||
</button>
|
||||
<p class="text-sm text-gray-500 mt-2">Click to begin streaming live measurement data</p>
|
||||
</div>
|
||||
<div id="stream-data" class="hidden">
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-4">
|
||||
<div class="bg-gray-50 dark:bg-gray-900 rounded-lg p-4">
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400 mb-1">Lp (Instant)</div>
|
||||
<div id="stream-lp" class="text-3xl font-bold text-gray-900 dark:text-white">--</div>
|
||||
<div class="text-xs text-gray-500">dB</div>
|
||||
</div>
|
||||
<div class="bg-gray-50 dark:bg-gray-900 rounded-lg p-4">
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400 mb-1">Leq (Average)</div>
|
||||
<div id="stream-leq" class="text-3xl font-bold text-blue-600 dark:text-blue-400">--</div>
|
||||
<div class="text-xs text-gray-500">dB</div>
|
||||
</div>
|
||||
<div class="bg-gray-50 dark:bg-gray-900 rounded-lg p-4">
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400 mb-1">Lmax</div>
|
||||
<div id="stream-lmax" class="text-3xl font-bold text-red-600 dark:text-red-400">--</div>
|
||||
<div class="text-xs text-gray-500">dB</div>
|
||||
</div>
|
||||
<div class="bg-gray-50 dark:bg-gray-900 rounded-lg p-4">
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400 mb-1">Lmin</div>
|
||||
<div id="stream-lmin" class="text-3xl font-bold text-green-600 dark:text-green-400">--</div>
|
||||
<div class="text-xs text-gray-500">dB</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="text-xs text-gray-500">
|
||||
<span class="inline-block w-2 h-2 bg-green-500 rounded-full mr-2 animate-pulse"></span>
|
||||
Streaming
|
||||
</div>
|
||||
<button onclick="stopStream()"
|
||||
class="px-4 py-2 bg-red-600 text-white text-sm rounded-lg hover:bg-red-700 transition-colors">
|
||||
Stop Stream
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center py-8 text-gray-500">
|
||||
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-seismo-orange mx-auto mb-4"></div>
|
||||
<p>Loading command center...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Device Information -->
|
||||
<div class="mb-8">
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">Device Information</h2>
|
||||
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-lg p-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400">Model</div>
|
||||
<div class="text-lg font-medium text-gray-900 dark:text-white">{{ unit.slm_model or 'NL-43' }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400">Serial Number</div>
|
||||
<div class="text-lg font-medium text-gray-900 dark:text-white">{{ unit.slm_serial_number or 'N/A' }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400">Host</div>
|
||||
<div class="text-lg font-medium text-gray-900 dark:text-white">{{ unit.slm_host or 'Not configured' }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400">TCP Port</div>
|
||||
<div class="text-lg font-medium text-gray-900 dark:text-white">{{ unit.slm_tcp_port or 'N/A' }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400">Frequency Weighting</div>
|
||||
<div class="text-lg font-medium text-gray-900 dark:text-white">{{ unit.slm_frequency_weighting or 'A' }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400">Time Weighting</div>
|
||||
<div class="text-lg font-medium text-gray-900 dark:text-white">{{ unit.slm_time_weighting or 'F (Fast)' }}</div>
|
||||
</div>
|
||||
<div class="md:col-span-2">
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400">Location</div>
|
||||
<div class="text-lg font-medium text-gray-900 dark:text-white">{{ unit.address or unit.location or 'Not specified' }}</div>
|
||||
</div>
|
||||
{% if unit.note %}
|
||||
<div class="md:col-span-2">
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400">Notes</div>
|
||||
<div class="text-gray-900 dark:text-white">{{ unit.note }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let ws = null;
|
||||
|
||||
function startStream() {
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const wsUrl = `${protocol}//${window.location.host}/api/slmm/{{ unit_id }}/stream`;
|
||||
|
||||
ws = new WebSocket(wsUrl);
|
||||
|
||||
ws.onopen = () => {
|
||||
document.getElementById('stream-start-btn').classList.add('hidden');
|
||||
document.getElementById('stream-data').classList.remove('hidden');
|
||||
console.log('WebSocket connected');
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
if (data.error) {
|
||||
console.error('Stream error:', data.error);
|
||||
stopStream();
|
||||
alert('Error: ' + data.error);
|
||||
return;
|
||||
}
|
||||
|
||||
// Update values
|
||||
document.getElementById('stream-lp').textContent = data.lp || '--';
|
||||
document.getElementById('stream-leq').textContent = data.leq || '--';
|
||||
document.getElementById('stream-lmax').textContent = data.lmax || '--';
|
||||
document.getElementById('stream-lmin').textContent = data.lmin || '--';
|
||||
};
|
||||
|
||||
ws.onerror = (error) => {
|
||||
console.error('WebSocket error:', error);
|
||||
stopStream();
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
console.log('WebSocket closed');
|
||||
};
|
||||
}
|
||||
|
||||
function stopStream() {
|
||||
if (ws) {
|
||||
ws.close();
|
||||
ws = null;
|
||||
}
|
||||
document.getElementById('stream-start-btn').classList.remove('hidden');
|
||||
document.getElementById('stream-data').classList.add('hidden');
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user