- Created complete frontend structure with Jinja2 templates - Implemented three main pages: Dashboard, Fleet Roster, and Unit Detail - Added HTMX auto-refresh for real-time updates (10s interval) - Integrated dark/light mode toggle with localStorage persistence - Built responsive card-based UI with sidebar navigation - Created API endpoints for status snapshot, roster, unit details, and photos - Added mock data service for development (emit_status_snapshot) - Implemented tabbed interface on unit detail page (Photos, Map, History) - Integrated Leaflet maps for unit location visualization - Configured static file serving and photo management - Updated requirements.txt with Jinja2 and aiofiles - Reorganized backend structure into routers and services - Added comprehensive FRONTEND_README.md documentation Frontend features: - Auto-refreshing dashboard with fleet summary and alerts - Sortable fleet roster table (prioritizes Missing > Pending > OK) - Unit detail view with status, deployment info, and notes - Photo gallery with thumbnail navigation - Interactive maps showing unit coordinates - Consistent styling with brand colors (orange, navy, burgundy) Ready for integration with real Series3 emitter data.
151 lines
7.5 KiB
HTML
151 lines
7.5 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en" class="dark">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>{% block title %}Seismo Fleet Manager{% endblock %}</title>
|
|
|
|
<!-- Tailwind CSS -->
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
|
|
<!-- HTMX -->
|
|
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
|
|
|
|
<!-- Leaflet for maps -->
|
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
|
|
|
<!-- Custom Tailwind Config -->
|
|
<script>
|
|
tailwind.config = {
|
|
darkMode: 'class',
|
|
theme: {
|
|
extend: {
|
|
colors: {
|
|
seismo: {
|
|
orange: '#f48b1c',
|
|
navy: '#142a66',
|
|
burgundy: '#7d234d',
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style>
|
|
/* Custom scrollbar */
|
|
::-webkit-scrollbar {
|
|
width: 8px;
|
|
height: 8px;
|
|
}
|
|
::-webkit-scrollbar-track {
|
|
background: transparent;
|
|
}
|
|
::-webkit-scrollbar-thumb {
|
|
background: #888;
|
|
border-radius: 4px;
|
|
}
|
|
::-webkit-scrollbar-thumb:hover {
|
|
background: #555;
|
|
}
|
|
|
|
/* Smooth transitions */
|
|
* {
|
|
transition: background-color 0.2s ease, color 0.2s ease;
|
|
}
|
|
</style>
|
|
|
|
{% block extra_head %}{% endblock %}
|
|
</head>
|
|
<body class="bg-gray-100 dark:bg-gray-900 text-gray-900 dark:text-gray-100">
|
|
<div class="flex h-screen overflow-hidden">
|
|
<!-- Sidebar -->
|
|
<aside class="w-64 bg-white dark:bg-slate-800 shadow-lg flex flex-col">
|
|
<!-- Logo -->
|
|
<div class="p-6 border-b border-gray-200 dark:border-gray-700">
|
|
<h1 class="text-2xl font-bold text-seismo-navy dark:text-seismo-orange">
|
|
Seismo<br>
|
|
<span class="text-seismo-orange dark:text-seismo-burgundy">Fleet Manager</span>
|
|
</h1>
|
|
</div>
|
|
|
|
<!-- Navigation -->
|
|
<nav class="flex-1 p-4 space-y-2">
|
|
<a href="/" class="flex items-center px-4 py-3 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 {% if request.url.path == '/' %}bg-gray-100 dark:bg-gray-700{% endif %}">
|
|
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"></path>
|
|
</svg>
|
|
Dashboard
|
|
</a>
|
|
|
|
<a href="/roster" class="flex items-center px-4 py-3 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 {% if request.url.path == '/roster' %}bg-gray-100 dark:bg-gray-700{% endif %}">
|
|
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path>
|
|
</svg>
|
|
Fleet Roster
|
|
</a>
|
|
|
|
<a href="#" class="flex items-center px-4 py-3 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 opacity-50 cursor-not-allowed">
|
|
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"></path>
|
|
</svg>
|
|
Projects
|
|
</a>
|
|
|
|
<a href="#" class="flex items-center px-4 py-3 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 opacity-50 cursor-not-allowed">
|
|
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path>
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
|
</svg>
|
|
Settings
|
|
</a>
|
|
</nav>
|
|
|
|
<!-- Dark mode toggle -->
|
|
<div class="p-4 border-t border-gray-200 dark:border-gray-700">
|
|
<button onclick="toggleDarkMode()" class="w-full flex items-center justify-center px-4 py-3 rounded-lg bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600">
|
|
<svg id="theme-toggle-dark-icon" class="w-5 h-5 hidden dark:block" fill="currentColor" viewBox="0 0 20 20">
|
|
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"></path>
|
|
</svg>
|
|
<svg id="theme-toggle-light-icon" class="w-5 h-5 block dark:hidden" fill="currentColor" viewBox="0 0 20 20">
|
|
<path d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" fill-rule="evenodd" clip-rule="evenodd"></path>
|
|
</svg>
|
|
<span class="ml-3">Toggle theme</span>
|
|
</button>
|
|
</div>
|
|
</aside>
|
|
|
|
<!-- Main content -->
|
|
<main class="flex-1 overflow-y-auto">
|
|
<div class="p-8">
|
|
{% block content %}{% endblock %}
|
|
</div>
|
|
</main>
|
|
</div>
|
|
|
|
<script>
|
|
// Dark mode toggle
|
|
function toggleDarkMode() {
|
|
const html = document.documentElement;
|
|
if (html.classList.contains('dark')) {
|
|
html.classList.remove('dark');
|
|
localStorage.setItem('theme', 'light');
|
|
} else {
|
|
html.classList.add('dark');
|
|
localStorage.setItem('theme', 'dark');
|
|
}
|
|
}
|
|
|
|
// Load saved theme preference
|
|
if (localStorage.getItem('theme') === 'light') {
|
|
document.documentElement.classList.remove('dark');
|
|
} else if (localStorage.getItem('theme') === 'dark') {
|
|
document.documentElement.classList.add('dark');
|
|
}
|
|
</script>
|
|
|
|
{% block extra_scripts %}{% endblock %}
|
|
</body>
|
|
</html>
|