v0.4.0 - merge from claude/dev-015sto5mf2MpPCE57TbNKtaF #1
23
CHANGELOG.md
23
CHANGELOG.md
@@ -5,6 +5,28 @@ All notable changes to Seismo Fleet Manager will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [0.3.3] - 2025-12-12
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- **Mobile Navigation**: Moved hamburger menu button from floating top-right to bottom navigation bar
|
||||||
|
- Bottom nav now shows: Menu (hamburger), Dashboard, Roster, Settings
|
||||||
|
- Removed "Add Unit" from bottom nav (still accessible via sidebar menu)
|
||||||
|
- Hamburger no longer floats over content on mobile
|
||||||
|
- **Status Dot Visibility**: Increased status dot size from 12px to 16px (w-3/h-3 → w-4/h-4) in dashboard fleet overview for better at-a-glance visibility
|
||||||
|
- Affects both Active and Benched tabs in dashboard
|
||||||
|
- Makes status colors (green/yellow/red) easier to spot during quick scroll
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **Location Navigation**: Moved tap-to-navigate functionality from roster card view to unit detail modal only
|
||||||
|
- Roster cards now show simple location text with pin emoji
|
||||||
|
- Navigation links (opening Maps app) only appear in the modal when tapping a unit
|
||||||
|
- Reduces visual clutter and accidental navigation triggers
|
||||||
|
|
||||||
|
### Technical Details
|
||||||
|
- Bottom navigation remains at 4 buttons, first button now triggers sidebar menu
|
||||||
|
- Removed standalone hamburger button element and associated CSS
|
||||||
|
- Modal already had navigation links, no changes needed there
|
||||||
|
|
||||||
## [0.3.2] - 2025-12-12
|
## [0.3.2] - 2025-12-12
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@@ -209,6 +231,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- Photo management per unit
|
- Photo management per unit
|
||||||
- Automated status categorization (OK/Pending/Missing)
|
- Automated status categorization (OK/Pending/Missing)
|
||||||
|
|
||||||
|
[0.3.3]: https://github.com/serversdwn/seismo-fleet-manager/compare/v0.3.2...v0.3.3
|
||||||
[0.3.2]: https://github.com/serversdwn/seismo-fleet-manager/compare/v0.3.1...v0.3.2
|
[0.3.2]: https://github.com/serversdwn/seismo-fleet-manager/compare/v0.3.1...v0.3.2
|
||||||
[0.3.1]: https://github.com/serversdwn/seismo-fleet-manager/compare/v0.3.0...v0.3.1
|
[0.3.1]: https://github.com/serversdwn/seismo-fleet-manager/compare/v0.3.0...v0.3.1
|
||||||
[0.3.0]: https://github.com/serversdwn/seismo-fleet-manager/compare/v0.2.1...v0.3.0
|
[0.3.0]: https://github.com/serversdwn/seismo-fleet-manager/compare/v0.2.1...v0.3.0
|
||||||
|
|||||||
13
README.md
13
README.md
@@ -1,4 +1,4 @@
|
|||||||
# Seismo Fleet Manager v0.3.2
|
# Seismo Fleet Manager v0.3.3
|
||||||
Backend API and HTMX-powered web interface for managing a mixed fleet of seismographs and field modems. Track deployments, monitor health in real time, merge roster intent with incoming telemetry, and control your fleet through a unified database and dashboard.
|
Backend API and HTMX-powered web interface for managing a mixed fleet of seismographs and field modems. Track deployments, monitor health in real time, merge roster intent with incoming telemetry, and control your fleet through a unified database and dashboard.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
@@ -437,6 +437,11 @@ docker compose down -v
|
|||||||
|
|
||||||
## Release Highlights
|
## Release Highlights
|
||||||
|
|
||||||
|
### v0.3.3 — 2025-12-12
|
||||||
|
- **Improved Mobile Navigation**: Hamburger menu moved to bottom nav bar (no more floating button covering content)
|
||||||
|
- **Better Status Visibility**: Larger status dots (16px) in dashboard fleet overview for easier at-a-glance status checks
|
||||||
|
- **Cleaner Roster Cards**: Location navigation links moved to detail modal only, reducing clutter in card view
|
||||||
|
|
||||||
### v0.3.2 — 2025-12-12
|
### v0.3.2 — 2025-12-12
|
||||||
- **Progressive Web App (PWA)**: Complete mobile optimization with offline support, installable as standalone app
|
- **Progressive Web App (PWA)**: Complete mobile optimization with offline support, installable as standalone app
|
||||||
- **Mobile-First UI**: Hamburger menu, bottom navigation bar, card-based roster view optimized for touch
|
- **Mobile-First UI**: Hamburger menu, bottom navigation bar, card-based roster view optimized for touch
|
||||||
@@ -494,9 +499,11 @@ MIT
|
|||||||
|
|
||||||
## Version
|
## Version
|
||||||
|
|
||||||
**Current: 0.3.2** — Progressive Web App with mobile optimization (2025-12-12)
|
**Current: 0.3.3** — Mobile navigation improvements and better status visibility (2025-12-12)
|
||||||
|
|
||||||
Previous: 0.3.1 — Dashboard alerts and status fixes (2025-12-12)
|
Previous: 0.3.2 — Progressive Web App with mobile optimization (2025-12-12)
|
||||||
|
|
||||||
|
0.3.1 — Dashboard alerts and status fixes (2025-12-12)
|
||||||
|
|
||||||
0.3.0 — Series 4 support, settings redesign, user preferences (2025-12-09)
|
0.3.0 — Series 4 support, settings redesign, user preferences (2025-12-09)
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ Base.metadata.create_all(bind=engine)
|
|||||||
ENVIRONMENT = os.getenv("ENVIRONMENT", "production")
|
ENVIRONMENT = os.getenv("ENVIRONMENT", "production")
|
||||||
|
|
||||||
# Initialize FastAPI app
|
# Initialize FastAPI app
|
||||||
VERSION = "0.3.2"
|
VERSION = "0.3.3"
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="Seismo Fleet Manager",
|
title="Seismo Fleet Manager",
|
||||||
description="Backend API for managing seismograph fleet status",
|
description="Backend API for managing seismograph fleet status",
|
||||||
|
|||||||
@@ -455,6 +455,54 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===== MAP OVERLAP FIX ===== */
|
||||||
|
/* Prevent map and controls from overlapping UI elements on mobile */
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
/* Constrain leaflet container to prevent overflow */
|
||||||
|
.leaflet-container {
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Override Leaflet's default high z-index values */
|
||||||
|
/* Bottom nav is z-20, sidebar is z-40, so map must be below */
|
||||||
|
.leaflet-pane,
|
||||||
|
.leaflet-tile-pane,
|
||||||
|
.leaflet-overlay-pane,
|
||||||
|
.leaflet-shadow-pane,
|
||||||
|
.leaflet-marker-pane,
|
||||||
|
.leaflet-tooltip-pane,
|
||||||
|
.leaflet-popup-pane {
|
||||||
|
z-index: 1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Map controls should also be below navigation elements */
|
||||||
|
.leaflet-control-container,
|
||||||
|
.leaflet-top,
|
||||||
|
.leaflet-bottom,
|
||||||
|
.leaflet-left,
|
||||||
|
.leaflet-right {
|
||||||
|
z-index: 1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-control {
|
||||||
|
z-index: 1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* When sidebar is open, hide all Leaflet controls (zoom, attribution, etc) */
|
||||||
|
body.menu-open .leaflet-control-container {
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure map tiles are non-interactive when sidebar is open */
|
||||||
|
body.menu-open #fleet-map,
|
||||||
|
body.menu-open #unit-map {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* ===== PENDING SYNC BADGE ===== */
|
/* ===== PENDING SYNC BADGE ===== */
|
||||||
.pending-sync-badge {
|
.pending-sync-badge {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
|||||||
@@ -20,12 +20,14 @@ function toggleMenu() {
|
|||||||
backdrop.classList.remove('show');
|
backdrop.classList.remove('show');
|
||||||
hamburgerBtn?.classList.remove('menu-open');
|
hamburgerBtn?.classList.remove('menu-open');
|
||||||
document.body.style.overflow = '';
|
document.body.style.overflow = '';
|
||||||
|
document.body.classList.remove('menu-open');
|
||||||
} else {
|
} else {
|
||||||
// Open menu
|
// Open menu
|
||||||
sidebar.classList.add('open');
|
sidebar.classList.add('open');
|
||||||
backdrop.classList.add('show');
|
backdrop.classList.add('show');
|
||||||
hamburgerBtn?.classList.add('menu-open');
|
hamburgerBtn?.classList.add('menu-open');
|
||||||
document.body.style.overflow = 'hidden';
|
document.body.style.overflow = 'hidden';
|
||||||
|
document.body.classList.add('menu-open');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -41,6 +43,7 @@ function closeMenuFromBackdrop() {
|
|||||||
backdrop.classList.remove('show');
|
backdrop.classList.remove('show');
|
||||||
hamburgerBtn?.classList.remove('menu-open');
|
hamburgerBtn?.classList.remove('menu-open');
|
||||||
document.body.style.overflow = '';
|
document.body.style.overflow = '';
|
||||||
|
document.body.classList.remove('menu-open');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,6 +59,7 @@ function handleResize() {
|
|||||||
backdrop.classList.remove('show');
|
backdrop.classList.remove('show');
|
||||||
hamburgerBtn?.classList.remove('menu-open');
|
hamburgerBtn?.classList.remove('menu-open');
|
||||||
document.body.style.overflow = '';
|
document.body.style.overflow = '';
|
||||||
|
document.body.classList.remove('menu-open');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,14 +69,6 @@
|
|||||||
{% block extra_head %}{% endblock %}
|
{% block extra_head %}{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-gray-100 dark:bg-gray-900 text-gray-900 dark:text-gray-100">
|
<body class="bg-gray-100 dark:bg-gray-900 text-gray-900 dark:text-gray-100">
|
||||||
<!-- Hamburger Button (Mobile Only) -->
|
|
||||||
<button id="hamburgerBtn" class="hamburger-btn md:hidden" onclick="toggleMenu()" aria-label="Menu">
|
|
||||||
<div class="hamburger-icon">
|
|
||||||
<div class="hamburger-line"></div>
|
|
||||||
<div class="hamburger-line"></div>
|
|
||||||
<div class="hamburger-line"></div>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- Offline Indicator -->
|
<!-- Offline Indicator -->
|
||||||
<div id="offlineIndicator" class="offline-indicator">
|
<div id="offlineIndicator" class="offline-indicator">
|
||||||
@@ -172,6 +164,12 @@
|
|||||||
<!-- Bottom Navigation (Mobile Only) -->
|
<!-- Bottom Navigation (Mobile Only) -->
|
||||||
<nav class="bottom-nav">
|
<nav class="bottom-nav">
|
||||||
<div class="grid grid-cols-4 h-16">
|
<div class="grid grid-cols-4 h-16">
|
||||||
|
<button id="hamburgerBtn" class="bottom-nav-btn" onclick="toggleMenu()" aria-label="Menu">
|
||||||
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
|
||||||
|
</svg>
|
||||||
|
<span>Menu</span>
|
||||||
|
</button>
|
||||||
<button class="bottom-nav-btn" data-href="/" onclick="window.location.href='/'">
|
<button class="bottom-nav-btn" data-href="/" onclick="window.location.href='/'">
|
||||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg 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>
|
<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>
|
||||||
@@ -184,12 +182,6 @@
|
|||||||
</svg>
|
</svg>
|
||||||
<span>Roster</span>
|
<span>Roster</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="bottom-nav-btn" data-href="/roster?action=add" onclick="window.location.href='/roster?action=add'">
|
|
||||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
|
|
||||||
</svg>
|
|
||||||
<span>Add Unit</span>
|
|
||||||
</button>
|
|
||||||
<button class="bottom-nav-btn" data-href="/settings" onclick="window.location.href='/settings'">
|
<button class="bottom-nav-btn" data-href="/settings" onclick="window.location.href='/settings'">
|
||||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg 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="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>
|
||||||
@@ -368,10 +360,10 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Offline Database -->
|
<!-- Offline Database -->
|
||||||
<script src="/static/offline-db.js?v=0.3.2"></script>
|
<script src="/static/offline-db.js?v=0.3.3"></script>
|
||||||
|
|
||||||
<!-- Mobile JavaScript -->
|
<!-- Mobile JavaScript -->
|
||||||
<script src="/static/mobile.js?v=0.3.2"></script>
|
<script src="/static/mobile.js?v=0.3.3"></script>
|
||||||
|
|
||||||
{% block extra_scripts %}{% endblock %}
|
{% block extra_scripts %}{% endblock %}
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -30,16 +30,21 @@
|
|||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
||||||
|
|
||||||
<!-- Fleet Summary Card -->
|
<!-- Fleet Summary Card -->
|
||||||
<div class="rounded-xl shadow-lg bg-white dark:bg-slate-800 p-6">
|
<div class="rounded-xl shadow-lg bg-white dark:bg-slate-800 p-6" id="fleet-summary-card">
|
||||||
<div class="flex items-center justify-between mb-4">
|
<div class="flex items-center justify-between mb-4 cursor-pointer md:cursor-default" onclick="toggleCard('fleet-summary')">
|
||||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">Fleet Summary</h2>
|
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">Fleet Summary</h2>
|
||||||
<svg class="w-6 h-6 text-seismo-orange" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<div class="flex items-center gap-2">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
<svg class="w-6 h-6 text-seismo-orange" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z">
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
</path>
|
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z">
|
||||||
</svg>
|
</path>
|
||||||
|
</svg>
|
||||||
|
<svg class="w-5 h-5 text-gray-500 transition-transform md:hidden chevron" id="fleet-summary-chevron" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-3">
|
<div class="space-y-3 card-content" id="fleet-summary-content">
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<span class="text-gray-600 dark:text-gray-400">Total Units</span>
|
<span class="text-gray-600 dark:text-gray-400">Total Units</span>
|
||||||
<span id="total-units" class="text-3xl md:text-2xl font-bold text-gray-900 dark:text-white">--</span>
|
<span id="total-units" class="text-3xl md:text-2xl font-bold text-gray-900 dark:text-white">--</span>
|
||||||
@@ -92,31 +97,41 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Recent Alerts Card -->
|
<!-- Recent Alerts Card -->
|
||||||
<div class="rounded-xl shadow-lg bg-white dark:bg-slate-800 p-6">
|
<div class="rounded-xl shadow-lg bg-white dark:bg-slate-800 p-6" id="recent-alerts-card">
|
||||||
<div class="flex items-center justify-between mb-4">
|
<div class="flex items-center justify-between mb-4 cursor-pointer md:cursor-default" onclick="toggleCard('recent-alerts')">
|
||||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">Recent Alerts</h2>
|
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">Recent Alerts</h2>
|
||||||
<svg class="w-6 h-6 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<div class="flex items-center gap-2">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
<svg class="w-6 h-6 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z">
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
</path>
|
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z">
|
||||||
</svg>
|
</path>
|
||||||
|
</svg>
|
||||||
|
<svg class="w-5 h-5 text-gray-500 transition-transform md:hidden chevron" id="recent-alerts-chevron" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="alerts-list" class="space-y-3">
|
<div id="alerts-list" class="space-y-3 card-content" id-content="recent-alerts-content">
|
||||||
<p class="text-sm text-gray-500 dark:text-gray-400">Loading alerts...</p>
|
<p class="text-sm text-gray-500 dark:text-gray-400">Loading alerts...</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Recent Photos Card -->
|
<!-- Recent Photos Card -->
|
||||||
<div class="rounded-xl shadow-lg bg-white dark:bg-slate-800 p-6">
|
<div class="rounded-xl shadow-lg bg-white dark:bg-slate-800 p-6" id="recent-photos-card">
|
||||||
<div class="flex items-center justify-between mb-4">
|
<div class="flex items-center justify-between mb-4 cursor-pointer md:cursor-default" onclick="toggleCard('recent-photos')">
|
||||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">Recent Photos</h2>
|
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">Recent Photos</h2>
|
||||||
<svg class="w-6 h-6 text-seismo-burgundy" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<div class="flex items-center gap-2">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
<svg class="w-6 h-6 text-seismo-burgundy" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z">
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
</path>
|
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z">
|
||||||
</svg>
|
</path>
|
||||||
|
</svg>
|
||||||
|
<svg class="w-5 h-5 text-gray-500 transition-transform md:hidden chevron" id="recent-photos-chevron" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-center text-gray-500 dark:text-gray-400">
|
<div class="text-center text-gray-500 dark:text-gray-400 card-content" id="recent-photos-content">
|
||||||
<svg class="w-16 h-16 mx-auto mb-2 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-16 h-16 mx-auto mb-2 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z">
|
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z">
|
||||||
@@ -129,53 +144,67 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Fleet Map -->
|
<!-- Fleet Map -->
|
||||||
<div class="rounded-xl shadow-lg bg-white dark:bg-slate-800 p-6 mb-8">
|
<div class="rounded-xl shadow-lg bg-white dark:bg-slate-800 p-6 mb-8" id="fleet-map-card">
|
||||||
<div class="flex items-center justify-between mb-4">
|
<div class="flex items-center justify-between mb-4 cursor-pointer md:cursor-default" onclick="toggleCard('fleet-map')">
|
||||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">Fleet Map</h2>
|
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">Fleet Map</h2>
|
||||||
<span class="text-sm text-gray-500 dark:text-gray-400">Deployed units</span>
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-sm text-gray-500 dark:text-gray-400">Deployed units</span>
|
||||||
|
<svg class="w-5 h-5 text-gray-500 transition-transform md:hidden chevron" id="fleet-map-chevron" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-content" id="fleet-map-content">
|
||||||
|
<div id="fleet-map" class="w-full h-64 md:h-96 rounded-lg"></div>
|
||||||
</div>
|
</div>
|
||||||
<div id="fleet-map" class="w-full h-64 md:h-96 rounded-lg"></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Fleet Status Section with Tabs -->
|
<!-- Fleet Status Section with Tabs -->
|
||||||
<div class="rounded-xl shadow-lg bg-white dark:bg-slate-800 p-6">
|
<div class="rounded-xl shadow-lg bg-white dark:bg-slate-800 p-6" id="fleet-status-card">
|
||||||
|
|
||||||
<div class="flex items-center justify-between mb-4">
|
<div class="flex items-center justify-between mb-4 cursor-pointer md:cursor-default" onclick="toggleCard('fleet-status')">
|
||||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">Fleet Status</h2>
|
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">Fleet Status</h2>
|
||||||
|
|
||||||
<a href="/roster" class="text-seismo-orange hover:text-seismo-burgundy font-medium flex items-center">
|
<div class="flex items-center gap-2">
|
||||||
Full Roster
|
<a href="/roster" class="text-seismo-orange hover:text-seismo-burgundy font-medium flex items-center" onclick="event.stopPropagation()">
|
||||||
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
Full Roster
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
<svg class="w-5 h-5 text-gray-500 transition-transform md:hidden chevron" id="fleet-status-chevron" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tab Bar -->
|
<div class="card-content" id="fleet-status-content">
|
||||||
<div class="flex border-b border-gray-200 dark:border-gray-700 mb-4">
|
<!-- Tab Bar -->
|
||||||
<button
|
<div class="flex border-b border-gray-200 dark:border-gray-700 mb-4">
|
||||||
class="px-4 py-2 text-sm font-medium tab-button active-tab"
|
<button
|
||||||
hx-get="/dashboard/active"
|
class="px-4 py-2 text-sm font-medium tab-button active-tab"
|
||||||
hx-target="#fleet-table"
|
hx-get="/dashboard/active"
|
||||||
hx-swap="innerHTML">
|
hx-target="#fleet-table"
|
||||||
Active
|
hx-swap="innerHTML">
|
||||||
</button>
|
Active
|
||||||
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="px-4 py-2 text-sm font-medium tab-button"
|
class="px-4 py-2 text-sm font-medium tab-button"
|
||||||
hx-get="/dashboard/benched"
|
hx-get="/dashboard/benched"
|
||||||
hx-target="#fleet-table"
|
hx-target="#fleet-table"
|
||||||
hx-swap="innerHTML">
|
hx-swap="innerHTML">
|
||||||
Benched
|
Benched
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tab Content Target -->
|
<!-- Tab Content Target -->
|
||||||
<div id="fleet-table" class="space-y-2"
|
<div id="fleet-table" class="space-y-2"
|
||||||
hx-get="/dashboard/active"
|
hx-get="/dashboard/active"
|
||||||
hx-trigger="load"
|
hx-trigger="load"
|
||||||
hx-swap="innerHTML">
|
hx-swap="innerHTML">
|
||||||
<p class="text-gray-500 dark:text-gray-400">Loading fleet data...</p>
|
<p class="text-gray-500 dark:text-gray-400">Loading fleet data...</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -195,10 +224,82 @@
|
|||||||
color: #b84a12 !important; /* seismo orange */
|
color: #b84a12 !important; /* seismo orange */
|
||||||
border-bottom: 2px solid #b84a12 !important;
|
border-bottom: 2px solid #b84a12 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Collapsible cards (mobile only) */
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.card-content.collapsed {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.chevron.collapsed {
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
// Toggle card collapse/expand (mobile only)
|
||||||
|
function toggleCard(cardName) {
|
||||||
|
// Only work on mobile
|
||||||
|
if (window.innerWidth >= 768) return;
|
||||||
|
|
||||||
|
const content = document.getElementById(`${cardName}-content`);
|
||||||
|
const chevron = document.getElementById(`${cardName}-chevron`);
|
||||||
|
|
||||||
|
if (!content || !chevron) return;
|
||||||
|
|
||||||
|
// Toggle collapsed state
|
||||||
|
const isCollapsed = content.classList.contains('collapsed');
|
||||||
|
|
||||||
|
if (isCollapsed) {
|
||||||
|
content.classList.remove('collapsed');
|
||||||
|
chevron.classList.remove('collapsed');
|
||||||
|
|
||||||
|
// If expanding the fleet map, invalidate size after animation
|
||||||
|
if (cardName === 'fleet-map' && window.fleetMap) {
|
||||||
|
setTimeout(() => {
|
||||||
|
window.fleetMap.invalidateSize();
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
content.classList.add('collapsed');
|
||||||
|
chevron.classList.add('collapsed');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save state to localStorage
|
||||||
|
const cardStates = JSON.parse(localStorage.getItem('dashboardCardStates') || '{}');
|
||||||
|
cardStates[cardName] = !isCollapsed;
|
||||||
|
localStorage.setItem('dashboardCardStates', JSON.stringify(cardStates));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore card states from localStorage on page load
|
||||||
|
function restoreCardStates() {
|
||||||
|
const cardStates = JSON.parse(localStorage.getItem('dashboardCardStates') || '{}');
|
||||||
|
const cardNames = ['fleet-summary', 'recent-alerts', 'recent-photos', 'fleet-map', 'fleet-status'];
|
||||||
|
|
||||||
|
cardNames.forEach(cardName => {
|
||||||
|
const content = document.getElementById(`${cardName}-content`);
|
||||||
|
const chevron = document.getElementById(`${cardName}-chevron`);
|
||||||
|
|
||||||
|
if (!content || !chevron) return;
|
||||||
|
|
||||||
|
// Default to expanded (true) if no saved state
|
||||||
|
const isCollapsed = cardStates[cardName] === false;
|
||||||
|
|
||||||
|
if (isCollapsed) {
|
||||||
|
content.classList.add('collapsed');
|
||||||
|
chevron.classList.add('collapsed');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore states when DOM is ready
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', restoreCardStates);
|
||||||
|
} else {
|
||||||
|
restoreCardStates();
|
||||||
|
}
|
||||||
|
|
||||||
function updateDashboard(event) {
|
function updateDashboard(event) {
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(event.detail.xhr.response);
|
const data = JSON.parse(event.detail.xhr.response);
|
||||||
@@ -276,15 +377,24 @@ let fleetMap = null;
|
|||||||
let fleetMarkers = [];
|
let fleetMarkers = [];
|
||||||
let fleetMapInitialized = false;
|
let fleetMapInitialized = false;
|
||||||
|
|
||||||
|
// Make fleetMap accessible globally for toggleCard function
|
||||||
|
window.fleetMap = null;
|
||||||
|
|
||||||
function initFleetMap() {
|
function initFleetMap() {
|
||||||
// Initialize the map centered on the US (can adjust based on your deployment area)
|
// Initialize the map centered on the US (can adjust based on your deployment area)
|
||||||
fleetMap = L.map('fleet-map').setView([39.8283, -98.5795], 4);
|
fleetMap = L.map('fleet-map').setView([39.8283, -98.5795], 4);
|
||||||
|
window.fleetMap = fleetMap;
|
||||||
|
|
||||||
// Add OpenStreetMap tiles
|
// Add OpenStreetMap tiles
|
||||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||||
attribution: '© OpenStreetMap contributors',
|
attribution: '© OpenStreetMap contributors',
|
||||||
maxZoom: 18
|
maxZoom: 18
|
||||||
}).addTo(fleetMap);
|
}).addTo(fleetMap);
|
||||||
|
|
||||||
|
// Force map to recalculate size after a brief delay to ensure container is fully rendered
|
||||||
|
setTimeout(() => {
|
||||||
|
fleetMap.invalidateSize();
|
||||||
|
}, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateFleetMap(data) {
|
function updateFleetMap(data) {
|
||||||
@@ -336,9 +446,11 @@ function updateFleetMap(data) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fit map to show all markers only on first load
|
// Fit map to show all markers
|
||||||
if (bounds.length > 0 && !fleetMapInitialized) {
|
if (bounds.length > 0) {
|
||||||
fleetMap.fitBounds(bounds, { padding: [50, 50] });
|
// Use different padding for mobile vs desktop
|
||||||
|
const padding = window.innerWidth < 768 ? [20, 20] : [50, 50];
|
||||||
|
fleetMap.fitBounds(bounds, { padding: padding });
|
||||||
fleetMapInitialized = true;
|
fleetMapInitialized = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,11 +6,11 @@
|
|||||||
<!-- Status Indicator -->
|
<!-- Status Indicator -->
|
||||||
<div class="flex-shrink-0">
|
<div class="flex-shrink-0">
|
||||||
{% if unit.status == 'OK' %}
|
{% if unit.status == 'OK' %}
|
||||||
<span class="w-3 h-3 rounded-full bg-green-500" title="OK"></span>
|
<span class="w-4 h-4 rounded-full bg-green-500" title="OK"></span>
|
||||||
{% elif unit.status == 'Pending' %}
|
{% elif unit.status == 'Pending' %}
|
||||||
<span class="w-3 h-3 rounded-full bg-yellow-500" title="Pending"></span>
|
<span class="w-4 h-4 rounded-full bg-yellow-500" title="Pending"></span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="w-3 h-3 rounded-full bg-red-500" title="Missing"></span>
|
<span class="w-4 h-4 rounded-full bg-red-500" title="Missing"></span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<div class="flex items-center space-x-3 flex-1">
|
<div class="flex items-center space-x-3 flex-1">
|
||||||
<!-- Status Indicator (grayed out for benched) -->
|
<!-- Status Indicator (grayed out for benched) -->
|
||||||
<div class="flex-shrink-0">
|
<div class="flex-shrink-0">
|
||||||
<span class="w-3 h-3 rounded-full bg-gray-400" title="Benched"></span>
|
<span class="w-4 h-4 rounded-full bg-gray-400" title="Benched"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Unit Info -->
|
<!-- Unit Info -->
|
||||||
|
|||||||
@@ -229,32 +229,14 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Location (Tap to Navigate) -->
|
<!-- Location -->
|
||||||
{% if unit.address %}
|
{% if unit.address %}
|
||||||
<div class="text-sm mb-1">
|
<div class="text-sm text-gray-600 dark:text-gray-400 mb-1">
|
||||||
<a href="https://www.google.com/maps/search/?api=1&query={{ unit.address | urlencode }}"
|
📍 {{ unit.address }}
|
||||||
target="_blank"
|
|
||||||
onclick="event.stopPropagation()"
|
|
||||||
class="flex items-center gap-1 text-seismo-orange hover:text-orange-600 dark:text-seismo-orange dark:hover:text-orange-400">
|
|
||||||
<svg class="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"></path>
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
|
||||||
</svg>
|
|
||||||
<span class="underline">{{ unit.address }}</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
{% elif unit.coordinates %}
|
{% elif unit.coordinates %}
|
||||||
<div class="text-sm mb-1">
|
<div class="text-sm text-gray-600 dark:text-gray-400 mb-1">
|
||||||
<a href="https://www.google.com/maps/search/?api=1&query={{ unit.coordinates | urlencode }}"
|
📍 {{ unit.coordinates }}
|
||||||
target="_blank"
|
|
||||||
onclick="event.stopPropagation()"
|
|
||||||
class="flex items-center gap-1 text-seismo-orange hover:text-orange-600 dark:text-seismo-orange dark:hover:text-orange-400">
|
|
||||||
<svg class="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"></path>
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
|
||||||
</svg>
|
|
||||||
<span class="font-mono underline">{{ unit.coordinates }}</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user