SLM dashboard rework, diagnostics and command pages added
This commit is contained in:
@@ -28,14 +28,28 @@
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">Active Units</h2>
|
||||
|
||||
<!-- Search/Filter -->
|
||||
<div class="mb-4">
|
||||
<input type="text"
|
||||
<div class="mb-4 space-y-2">
|
||||
<!-- Project Filter -->
|
||||
<select id="project-filter"
|
||||
name="project"
|
||||
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||||
hx-get="/api/slm-dashboard/units"
|
||||
hx-trigger="change"
|
||||
hx-target="#slm-list"
|
||||
hx-include="#search-input, #project-filter">
|
||||
<option value="">All Projects</option>
|
||||
<!-- Will be populated dynamically -->
|
||||
</select>
|
||||
|
||||
<!-- Search Input -->
|
||||
<input id="search-input"
|
||||
type="text"
|
||||
placeholder="Search units..."
|
||||
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||||
hx-get="/api/slm-dashboard/units"
|
||||
hx-trigger="keyup changed delay:300ms"
|
||||
hx-target="#slm-list"
|
||||
hx-include="this"
|
||||
hx-include="#search-input, #project-filter"
|
||||
name="search">
|
||||
</div>
|
||||
|
||||
@@ -93,9 +107,94 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Command Center Modal -->
|
||||
<div id="command-center-modal" class="hidden fixed inset-0 bg-black bg-opacity-50 z-50">
|
||||
<div class="flex items-center justify-center min-h-screen p-4">
|
||||
<div class="bg-white dark:bg-gray-900 rounded-lg shadow-xl w-full max-w-7xl max-h-[90vh] overflow-y-auto">
|
||||
<div class="sticky top-0 bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-700 px-6 py-4 flex items-center justify-between z-10">
|
||||
<h2 class="text-xl font-bold text-gray-900 dark:text-white">Command Center</h2>
|
||||
<button onclick="closeCommandCenter()" 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"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div id="command-center-content" class="p-6">
|
||||
<!-- Command center will load here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Function to select a unit and load live view
|
||||
// Global Connection Manager - ensures only one SLM connection at a time
|
||||
window.SLMConnectionManager = {
|
||||
activeIntervals: [],
|
||||
activeWebSocket: null,
|
||||
currentUnitId: null,
|
||||
|
||||
// Clear all existing connections
|
||||
clearAll: function() {
|
||||
console.log('SLMConnectionManager: Clearing all connections');
|
||||
|
||||
// Clear all intervals
|
||||
this.activeIntervals.forEach(interval => {
|
||||
clearInterval(interval);
|
||||
});
|
||||
this.activeIntervals = [];
|
||||
|
||||
// Close WebSocket if exists
|
||||
if (this.activeWebSocket) {
|
||||
this.activeWebSocket.close();
|
||||
this.activeWebSocket = null;
|
||||
}
|
||||
|
||||
// Clear any global intervals that might exist
|
||||
if (window.refreshInterval) {
|
||||
clearInterval(window.refreshInterval);
|
||||
window.refreshInterval = null;
|
||||
}
|
||||
if (window.timerInterval) {
|
||||
clearInterval(window.timerInterval);
|
||||
window.timerInterval = null;
|
||||
}
|
||||
if (window.diagRefreshInterval) {
|
||||
clearInterval(window.diagRefreshInterval);
|
||||
window.diagRefreshInterval = null;
|
||||
}
|
||||
|
||||
console.log('SLMConnectionManager: All connections cleared');
|
||||
},
|
||||
|
||||
// Register a new interval
|
||||
registerInterval: function(intervalId) {
|
||||
this.activeIntervals.push(intervalId);
|
||||
},
|
||||
|
||||
// Register WebSocket
|
||||
registerWebSocket: function(ws) {
|
||||
if (this.activeWebSocket) {
|
||||
this.activeWebSocket.close();
|
||||
}
|
||||
this.activeWebSocket = ws;
|
||||
},
|
||||
|
||||
// Set current unit
|
||||
setCurrentUnit: function(unitId) {
|
||||
if (this.currentUnitId !== unitId) {
|
||||
this.clearAll();
|
||||
this.currentUnitId = unitId;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Function to select a unit and load DIAGNOSTICS CARD (not full command center)
|
||||
function selectUnit(unitId) {
|
||||
console.log(`Selecting unit: ${unitId}`);
|
||||
|
||||
// Clear all existing connections
|
||||
window.SLMConnectionManager.clearAll();
|
||||
|
||||
// Remove active state from all items
|
||||
document.querySelectorAll('.slm-unit-item').forEach(item => {
|
||||
item.classList.remove('bg-seismo-orange', 'text-white');
|
||||
@@ -106,18 +205,58 @@ function selectUnit(unitId) {
|
||||
event.currentTarget.classList.remove('bg-gray-100', 'dark:bg-gray-700');
|
||||
event.currentTarget.classList.add('bg-seismo-orange', 'text-white');
|
||||
|
||||
// Load live view for this unit
|
||||
htmx.ajax('GET', `/api/slm-dashboard/live-view/${unitId}`, {
|
||||
// Load DIAGNOSTICS CARD (not full live view)
|
||||
htmx.ajax('GET', `/api/slm-dashboard/diagnostics/${unitId}`, {
|
||||
target: '#live-view-panel',
|
||||
swap: 'innerHTML'
|
||||
});
|
||||
}
|
||||
|
||||
// Open command center in modal
|
||||
function openCommandCenter(unitId) {
|
||||
console.log(`Opening command center for: ${unitId}`);
|
||||
|
||||
// Clear diagnostics refresh before opening modal
|
||||
window.SLMConnectionManager.clearAll();
|
||||
|
||||
const modal = document.getElementById('command-center-modal');
|
||||
modal.classList.remove('hidden');
|
||||
|
||||
// Load full command center
|
||||
htmx.ajax('GET', `/api/slm-dashboard/live-view/${unitId}`, {
|
||||
target: '#command-center-content',
|
||||
swap: 'innerHTML'
|
||||
});
|
||||
}
|
||||
|
||||
// Close command center modal
|
||||
function closeCommandCenter() {
|
||||
console.log('Closing command center');
|
||||
|
||||
// Clear all command center connections
|
||||
window.SLMConnectionManager.clearAll();
|
||||
|
||||
document.getElementById('command-center-modal').classList.add('hidden');
|
||||
|
||||
// Reload the diagnostics card for the currently selected unit
|
||||
const activeUnit = document.querySelector('.slm-unit-item.bg-seismo-orange');
|
||||
if (activeUnit) {
|
||||
const unitIdMatch = activeUnit.getAttribute('onclick').match(/selectUnit\('(.+?)'\)/);
|
||||
if (unitIdMatch) {
|
||||
const unitId = unitIdMatch[1];
|
||||
htmx.ajax('GET', `/api/slm-dashboard/diagnostics/${unitId}`, {
|
||||
target: '#live-view-panel',
|
||||
swap: 'innerHTML'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Configuration modal functions
|
||||
function openConfigModal(unitId) {
|
||||
const modal = document.getElementById('config-modal');
|
||||
modal.classList.remove('hidden');
|
||||
|
||||
|
||||
// Load configuration form via HTMX
|
||||
htmx.ajax('GET', `/api/slm-dashboard/config/${unitId}`, {
|
||||
target: '#config-modal-content',
|
||||
@@ -129,121 +268,48 @@ function closeConfigModal() {
|
||||
document.getElementById('config-modal').classList.add('hidden');
|
||||
}
|
||||
|
||||
// Close modal on escape key
|
||||
// Close modals on escape key
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') {
|
||||
closeConfigModal();
|
||||
closeCommandCenter();
|
||||
}
|
||||
});
|
||||
|
||||
// Close modal when clicking outside
|
||||
// Close modals when clicking outside
|
||||
document.getElementById('config-modal')?.addEventListener('click', function(e) {
|
||||
if (e.target === this) {
|
||||
closeConfigModal();
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize WebSocket for selected unit
|
||||
let currentWebSocket = null;
|
||||
|
||||
function initLiveDataStream(unitId) {
|
||||
// Close existing connection if any
|
||||
if (currentWebSocket) {
|
||||
currentWebSocket.close();
|
||||
document.getElementById('command-center-modal')?.addEventListener('click', function(e) {
|
||||
if (e.target === this) {
|
||||
closeCommandCenter();
|
||||
}
|
||||
});
|
||||
|
||||
// WebSocket URL for SLMM backend via proxy
|
||||
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const wsUrl = `${wsProtocol}//${window.location.host}/api/slmm/${unitId}/live`;
|
||||
|
||||
currentWebSocket = new WebSocket(wsUrl);
|
||||
|
||||
currentWebSocket.onopen = function() {
|
||||
console.log('WebSocket connected');
|
||||
// Toggle button visibility
|
||||
const startBtn = document.getElementById('start-stream-btn');
|
||||
const stopBtn = document.getElementById('stop-stream-btn');
|
||||
if (startBtn) startBtn.style.display = 'none';
|
||||
if (stopBtn) stopBtn.style.display = 'flex';
|
||||
};
|
||||
|
||||
currentWebSocket.onmessage = function(event) {
|
||||
const data = JSON.parse(event.data);
|
||||
updateLiveChart(data);
|
||||
updateLiveMetrics(data);
|
||||
};
|
||||
|
||||
currentWebSocket.onerror = function(error) {
|
||||
console.error('WebSocket error:', error);
|
||||
};
|
||||
|
||||
currentWebSocket.onclose = function() {
|
||||
console.log('WebSocket closed');
|
||||
// Toggle button visibility
|
||||
const startBtn = document.getElementById('start-stream-btn');
|
||||
const stopBtn = document.getElementById('stop-stream-btn');
|
||||
if (startBtn) startBtn.style.display = 'flex';
|
||||
if (stopBtn) stopBtn.style.display = 'none';
|
||||
};
|
||||
}
|
||||
|
||||
function stopLiveDataStream() {
|
||||
if (currentWebSocket) {
|
||||
currentWebSocket.close();
|
||||
currentWebSocket = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Update live chart with new data point
|
||||
let chartData = {
|
||||
timestamps: [],
|
||||
lp: [],
|
||||
leq: []
|
||||
};
|
||||
|
||||
function updateLiveChart(data) {
|
||||
const now = new Date();
|
||||
chartData.timestamps.push(now.toLocaleTimeString());
|
||||
chartData.lp.push(parseFloat(data.lp || 0));
|
||||
chartData.leq.push(parseFloat(data.leq || 0));
|
||||
|
||||
// Keep only last 60 data points (1 minute at 1 sample/sec)
|
||||
if (chartData.timestamps.length > 60) {
|
||||
chartData.timestamps.shift();
|
||||
chartData.lp.shift();
|
||||
chartData.leq.shift();
|
||||
}
|
||||
|
||||
// Update chart (using Chart.js if available)
|
||||
if (window.liveChart) {
|
||||
window.liveChart.data.labels = chartData.timestamps;
|
||||
window.liveChart.data.datasets[0].data = chartData.lp;
|
||||
window.liveChart.data.datasets[1].data = chartData.leq;
|
||||
window.liveChart.update('none'); // Update without animation for smooth real-time
|
||||
}
|
||||
}
|
||||
|
||||
function updateLiveMetrics(data) {
|
||||
// Update metric displays
|
||||
if (document.getElementById('live-lp')) {
|
||||
document.getElementById('live-lp').textContent = data.lp || '--';
|
||||
}
|
||||
if (document.getElementById('live-leq')) {
|
||||
document.getElementById('live-leq').textContent = data.leq || '--';
|
||||
}
|
||||
if (document.getElementById('live-lmax')) {
|
||||
document.getElementById('live-lmax').textContent = data.lmax || '--';
|
||||
}
|
||||
if (document.getElementById('live-lmin')) {
|
||||
document.getElementById('live-lmin').textContent = data.lmin || '--';
|
||||
}
|
||||
}
|
||||
// Load projects for filter dropdown on page load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
fetch('/api/slm-dashboard/projects')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const projectFilter = document.getElementById('project-filter');
|
||||
if (projectFilter && data.projects) {
|
||||
data.projects.forEach(project => {
|
||||
const option = document.createElement('option');
|
||||
option.value = project;
|
||||
option.textContent = project;
|
||||
projectFilter.appendChild(option);
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(error => console.error('Failed to load projects:', error));
|
||||
});
|
||||
|
||||
// Cleanup on page unload
|
||||
window.addEventListener('beforeunload', function() {
|
||||
if (currentWebSocket) {
|
||||
currentWebSocket.close();
|
||||
}
|
||||
window.SLMConnectionManager.clearAll();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user