chore: modular monolith folder split (no behavior change)

This commit is contained in:
serversdwn
2026-01-08 20:54:30 +00:00
parent 893cb96e8d
commit 991aaca34b
90 changed files with 16129 additions and 28 deletions

View File

@@ -0,0 +1,249 @@
{% 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">Sound Level Meters</h1>
<p class="text-gray-600 dark:text-gray-400 mt-1">Monitor and manage 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>
<!-- Main Content Grid -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- SLM List -->
<div class="lg:col-span-1">
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-lg p-6">
<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"
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"
name="search">
</div>
<!-- SLM List -->
<div id="slm-list"
class="space-y-2 max-h-[600px] overflow-y-auto"
hx-get="/api/slm-dashboard/units"
hx-trigger="load, every 10s"
hx-swap="innerHTML">
<!-- Loading skeleton -->
<div class="animate-pulse space-y-2">
<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>
</div>
<!-- Live View Panel -->
<div class="lg:col-span-2">
<div id="live-view-panel" class="bg-white dark:bg-slate-800 rounded-xl shadow-lg p-6">
<!-- Initial state - no unit selected -->
<div class="flex flex-col items-center justify-center h-[600px] text-gray-400 dark:text-gray-500">
<svg class="w-24 h-24 mb-4" 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>
<p class="text-lg font-medium">No unit selected</p>
<p class="text-sm mt-2">Select a sound level meter from the list to view live data</p>
</div>
</div>
</div>
</div>
<!-- Configuration Modal -->
<div id="config-modal" class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div class="bg-white dark:bg-slate-800 rounded-xl p-6 max-w-2xl w-full mx-4 max-h-[90vh] overflow-y-auto">
<div class="flex items-center justify-between mb-6">
<h3 class="text-2xl font-bold text-gray-900 dark:text-white">Configure SLM</h3>
<button onclick="closeConfigModal()" 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>
<div id="config-modal-content">
<!-- Content loaded via HTMX -->
<div class="animate-pulse space-y-4">
<div class="h-4 bg-gray-200 dark:bg-gray-700 rounded w-3/4"></div>
<div class="h-4 bg-gray-200 dark:bg-gray-700 rounded"></div>
<div class="h-4 bg-gray-200 dark:bg-gray-700 rounded w-5/6"></div>
</div>
</div>
</div>
</div>
<script>
// Function to select a unit and load live view
function selectUnit(unitId) {
// Remove active state from all items
document.querySelectorAll('.slm-unit-item').forEach(item => {
item.classList.remove('bg-seismo-orange', 'text-white');
item.classList.add('bg-gray-100', 'dark:bg-gray-700');
});
// Add active state to clicked item
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}`, {
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',
swap: 'innerHTML'
});
}
function closeConfigModal() {
document.getElementById('config-modal').classList.add('hidden');
}
// Close modal on escape key
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
closeConfigModal();
}
});
// Close modal 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();
}
// 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 || '--';
}
}
// Cleanup on page unload
window.addEventListener('beforeunload', function() {
if (currentWebSocket) {
currentWebSocket.close();
}
});
</script>
{% endblock %}