- 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.
65 lines
1.9 KiB
Python
65 lines
1.9 KiB
Python
from fastapi import APIRouter, HTTPException
|
|
from fastapi.responses import FileResponse
|
|
from pathlib import Path
|
|
from typing import List
|
|
import os
|
|
|
|
router = APIRouter(prefix="/api", tags=["photos"])
|
|
|
|
PHOTOS_BASE_DIR = Path("data/photos")
|
|
|
|
|
|
@router.get("/unit/{unit_id}/photos")
|
|
def get_unit_photos(unit_id: str):
|
|
"""
|
|
Reads /data/photos/<unit_id>/ and returns list of image filenames.
|
|
Primary photo = most recent file.
|
|
"""
|
|
unit_photo_dir = PHOTOS_BASE_DIR / unit_id
|
|
|
|
if not unit_photo_dir.exists():
|
|
# Return empty list if no photos directory exists
|
|
return {
|
|
"unit_id": unit_id,
|
|
"photos": [],
|
|
"primary_photo": None
|
|
}
|
|
|
|
# Get all image files
|
|
image_extensions = {".jpg", ".jpeg", ".png", ".gif", ".webp"}
|
|
photos = []
|
|
|
|
for file_path in unit_photo_dir.iterdir():
|
|
if file_path.is_file() and file_path.suffix.lower() in image_extensions:
|
|
photos.append({
|
|
"filename": file_path.name,
|
|
"path": f"/api/unit/{unit_id}/photo/{file_path.name}",
|
|
"modified": file_path.stat().st_mtime
|
|
})
|
|
|
|
# Sort by modification time (most recent first)
|
|
photos.sort(key=lambda x: x["modified"], reverse=True)
|
|
|
|
# Primary photo is the most recent
|
|
primary_photo = photos[0]["filename"] if photos else None
|
|
|
|
return {
|
|
"unit_id": unit_id,
|
|
"photos": [p["filename"] for p in photos],
|
|
"primary_photo": primary_photo,
|
|
"photo_urls": [p["path"] for p in photos]
|
|
}
|
|
|
|
|
|
@router.get("/unit/{unit_id}/photo/{filename}")
|
|
def get_photo(unit_id: str, filename: str):
|
|
"""
|
|
Serves a specific photo file.
|
|
"""
|
|
file_path = PHOTOS_BASE_DIR / unit_id / filename
|
|
|
|
if not file_path.exists() or not file_path.is_file():
|
|
raise HTTPException(status_code=404, detail="Photo not found")
|
|
|
|
return FileResponse(file_path)
|