From 247405c361f6a808a222563ab2215fec35d23d90 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 22 Nov 2025 00:16:26 +0000 Subject: [PATCH 1/2] Add MVP frontend scaffold with FastAPI + HTMX + TailwindCSS - 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. --- FRONTEND_README.md | 303 +++++++++++++++++++++++++++ database.py => backend/database.py | 0 backend/main.py | 108 ++++++++++ models.py => backend/models.py | 2 +- backend/routers/photos.py | 64 ++++++ backend/routers/roster.py | 46 ++++ backend/routers/units.py | 44 ++++ routes.py => backend/routes.py | 4 +- backend/services/snapshot.py | 96 +++++++++ backend/static/style.css | 12 ++ requirements.txt | 2 + templates/base.html | 150 +++++++++++++ templates/dashboard.html | 178 ++++++++++++++++ templates/partials/roster_table.html | 87 ++++++++ templates/roster.html | 29 +++ templates/unit_detail.html | 268 +++++++++++++++++++++++ 16 files changed, 1390 insertions(+), 3 deletions(-) create mode 100644 FRONTEND_README.md rename database.py => backend/database.py (100%) create mode 100644 backend/main.py rename models.py => backend/models.py (94%) create mode 100644 backend/routers/photos.py create mode 100644 backend/routers/roster.py create mode 100644 backend/routers/units.py rename routes.py => backend/routes.py (96%) create mode 100644 backend/services/snapshot.py create mode 100644 backend/static/style.css create mode 100644 templates/base.html create mode 100644 templates/dashboard.html create mode 100644 templates/partials/roster_table.html create mode 100644 templates/roster.html create mode 100644 templates/unit_detail.html diff --git a/FRONTEND_README.md b/FRONTEND_README.md new file mode 100644 index 0000000..b9e2fdb --- /dev/null +++ b/FRONTEND_README.md @@ -0,0 +1,303 @@ +# Seismo Fleet Manager - Frontend Documentation + +## Overview + +This is the MVP frontend scaffold for **Seismo Fleet Manager**, built with: +- **FastAPI** (backend framework) +- **HTMX** (dynamic updates without JavaScript frameworks) +- **TailwindCSS** (utility-first styling) +- **Jinja2** (server-side templating) +- **Leaflet** (interactive maps) + +No React, Vue, or other frontend frameworks are used. + +## Project Structure + +``` +seismo-fleet-manager/ +├── backend/ +│ ├── main.py # FastAPI app entry point +│ ├── routers/ +│ │ ├── roster.py # Fleet roster endpoints +│ │ ├── units.py # Individual unit endpoints +│ │ └── photos.py # Photo management endpoints +│ ├── services/ +│ │ └── snapshot.py # Mock status snapshot (replace with real logic) +│ ├── static/ +│ │ └── style.css # Custom CSS +│ ├── database.py # SQLAlchemy database setup +│ ├── models.py # Database models +│ └── routes.py # Legacy API routes +├── templates/ +│ ├── base.html # Base layout with sidebar & dark mode +│ ├── dashboard.html # Main dashboard page +│ ├── roster.html # Fleet roster page +│ ├── unit_detail.html # Unit detail page +│ └── partials/ +│ └── roster_table.html # HTMX partial for roster table +├── data/ +│ └── photos/ # Photo storage (organized by unit_id) +└── requirements.txt +``` + +## Running the Application + +### Install Dependencies + +```bash +pip install -r requirements.txt +``` + +### Run the Server + +```bash +uvicorn backend.main:app --host 0.0.0.0 --port 8001 --reload +``` + +The application will be available at: +- **Web UI**: http://localhost:8001/ +- **API Docs**: http://localhost:8001/docs +- **Health Check**: http://localhost:8001/health + +## Features + +### 1. Dashboard (`/`) + +The main dashboard provides an at-a-glance view of the fleet: + +- **Fleet Summary Card**: Total units, deployed units, status breakdown +- **Recent Alerts Card**: Shows units with Missing or Pending status +- **Recent Photos Card**: Placeholder for photo gallery +- **Fleet Status Preview**: Quick view of first 5 units + +**Auto-refresh**: Dashboard updates every 10 seconds via HTMX + +### 2. Fleet Roster (`/roster`) + +A comprehensive table view of all seismograph units: + +**Columns**: +- Status indicator (colored dot: green=OK, yellow=Pending, red=Missing) +- Deployment indicator (blue dot if deployed) +- Unit ID +- Last seen timestamp +- Age since last contact +- Notes +- Actions (View detail button) + +**Features**: +- Auto-refresh every 10 seconds +- Sorted by priority (Missing > Pending > OK) +- Click any row to view unit details + +### 3. Unit Detail Page (`/unit/{unit_id}`) + +Split-screen layout with detailed information: + +**Left Column**: +- Status card with real-time updates +- Deployment status +- Last contact time and file +- Notes section +- Editable metadata (mock form) + +**Right Column - Tabbed Interface**: +- **Photos Tab**: Primary photo with thumbnail gallery +- **Map Tab**: Interactive Leaflet map showing unit location +- **History Tab**: Placeholder for event history + +**Auto-refresh**: Unit data updates every 10 seconds + +### 4. Dark/Light Mode + +Toggle button in sidebar switches between themes: +- Uses Tailwind's `dark:` classes +- Preference saved to localStorage +- Smooth transitions on theme change + +## API Endpoints + +### Status & Fleet Data + +```http +GET /api/status-snapshot +``` +Returns complete fleet status snapshot with statistics. + +```http +GET /api/roster +``` +Returns sorted list of all units for roster table. + +```http +GET /api/unit/{unit_id} +``` +Returns detailed information for a single unit including coordinates. + +### Photo Management + +```http +GET /api/unit/{unit_id}/photos +``` +Returns list of photos for a unit, sorted by recency. + +```http +GET /api/unit/{unit_id}/photo/{filename} +``` +Serves a specific photo file. + +### Legacy Endpoints + +```http +POST /emitters/report +``` +Endpoint for emitters to report status (from original backend). + +```http +GET /fleet/status +``` +Returns database-backed fleet status (from original backend). + +## Mock Data + +### Location: `backend/services/snapshot.py` + +The `emit_status_snapshot()` function currently returns mock data with 8 units: + +- **BE1234**: OK, deployed (San Francisco) +- **BE5678**: Pending, deployed (Los Angeles) +- **BE9012**: Missing, deployed (New York) +- **BE3456**: OK, benched (Chicago) +- **BE7890**: OK, deployed (Houston) +- **BE2468**: Pending, deployed +- **BE1357**: OK, benched +- **BE8642**: Missing, deployed + +**To replace with real data**: Update the `emit_status_snapshot()` function to call your Series3 emitter logic. + +## Styling + +### Color Palette + +The application uses your brand colors: + +```css +orange: #f48b1c +navy: #142a66 +burgundy: #7d234d +``` + +These are configured in the Tailwind config as `seismo-orange`, `seismo-navy`, `seismo-burgundy`. + +### Cards + +All cards use the consistent styling: +```html +
+``` + +### Status Indicators + +- Green dot: OK status +- Yellow dot: Pending status +- Red dot: Missing status +- Blue dot: Deployed +- Gray dot: Benched + +## HTMX Usage + +HTMX enables dynamic updates without writing JavaScript: + +### Auto-refresh Example (Dashboard) + +```html +
+``` + +This fetches the snapshot on page load and every 10 seconds, then calls a JavaScript function to update the DOM. + +### Partial Template Loading (Roster) + +```html +
+``` + +This replaces the entire inner HTML with the server-rendered roster table every 10 seconds. + +## Adding Photos + +To add photos for a unit: + +1. Create a directory: `data/photos/{unit_id}/` +2. Add image files (jpg, jpeg, png, gif, webp) +3. Photos will automatically appear on the unit detail page +4. Most recent file becomes the primary photo + +Example: +```bash +mkdir -p data/photos/BE1234 +cp my-photo.jpg data/photos/BE1234/deployment-site.jpg +``` + +## Customization + +### Adding New Pages + +1. Create a template in `templates/` +2. Add a route in `backend/main.py`: + +```python +@app.get("/my-page", response_class=HTMLResponse) +async def my_page(request: Request): + return templates.TemplateResponse("my_page.html", {"request": request}) +``` + +3. Add a navigation link in `templates/base.html` + +### Adding New API Endpoints + +1. Create a router file in `backend/routers/` +2. Include the router in `backend/main.py`: + +```python +from backend.routers import my_router +app.include_router(my_router.router) +``` + +## Docker Deployment + +The project includes Docker configuration: + +```bash +docker-compose up +``` + +This will start the application on port 8001 (configured to avoid conflicts with port 8000). + +## Next Steps + +1. **Replace Mock Data**: Update `backend/services/snapshot.py` with real Series3 emitter logic +2. **Database Integration**: The existing SQLAlchemy models can store historical data +3. **Photo Upload**: Add a form to upload photos from the UI +4. **Projects Management**: Implement the "Projects" page +5. **Settings**: Add user preferences and configuration +6. **Event History**: Populate the History tab with real event data +7. **Authentication**: Add user login/authentication if needed +8. **Notifications**: Add real-time alerts for critical status changes + +## Development Tips + +- The `--reload` flag auto-reloads the server when code changes +- Use browser DevTools to debug HTMX requests (look for `HX-Request` headers) +- Check `/docs` for interactive API documentation (Swagger UI) +- Dark mode state persists in browser localStorage +- All timestamps are currently mock data - replace with real values + +## License + +See main README.md for license information. diff --git a/database.py b/backend/database.py similarity index 100% rename from database.py rename to backend/database.py diff --git a/backend/main.py b/backend/main.py new file mode 100644 index 0000000..9e75d26 --- /dev/null +++ b/backend/main.py @@ -0,0 +1,108 @@ +from fastapi import FastAPI, Request +from fastapi.middleware.cors import CORSMiddleware +from fastapi.staticfiles import StaticFiles +from fastapi.templating import Jinja2Templates +from fastapi.responses import HTMLResponse + +from backend.database import engine, Base +from backend.routers import roster, units, photos +from backend.services.snapshot import emit_status_snapshot + +# Create database tables +Base.metadata.create_all(bind=engine) + +# Initialize FastAPI app +app = FastAPI( + title="Seismo Fleet Manager", + description="Backend API for managing seismograph fleet status", + version="0.1.0" +) + +# Configure CORS +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# Mount static files +app.mount("/static", StaticFiles(directory="backend/static"), name="static") + +# Setup Jinja2 templates +templates = Jinja2Templates(directory="templates") + +# Include API routers +app.include_router(roster.router) +app.include_router(units.router) +app.include_router(photos.router) + + +# Legacy routes from the original backend +from backend import routes as legacy_routes +app.include_router(legacy_routes.router) + + +# HTML page routes +@app.get("/", response_class=HTMLResponse) +async def dashboard(request: Request): + """Dashboard home page""" + return templates.TemplateResponse("dashboard.html", {"request": request}) + + +@app.get("/roster", response_class=HTMLResponse) +async def roster_page(request: Request): + """Fleet roster page""" + return templates.TemplateResponse("roster.html", {"request": request}) + + +@app.get("/unit/{unit_id}", response_class=HTMLResponse) +async def unit_detail_page(request: Request, unit_id: str): + """Unit detail page""" + return templates.TemplateResponse("unit_detail.html", { + "request": request, + "unit_id": unit_id + }) + + +@app.get("/partials/roster-table", response_class=HTMLResponse) +async def roster_table_partial(request: Request): + """Partial template for roster table (HTMX)""" + from datetime import datetime + snapshot = emit_status_snapshot() + + units_list = [] + for unit_id, unit_data in snapshot["units"].items(): + units_list.append({ + "id": unit_id, + "status": unit_data["status"], + "age": unit_data["age"], + "last_seen": unit_data["last"], + "deployed": unit_data["deployed"], + "note": unit_data.get("note", ""), + }) + + # Sort by status priority (Missing > Pending > OK) then by ID + status_priority = {"Missing": 0, "Pending": 1, "OK": 2} + units_list.sort(key=lambda x: (status_priority.get(x["status"], 3), x["id"])) + + return templates.TemplateResponse("partials/roster_table.html", { + "request": request, + "units": units_list, + "timestamp": datetime.now().strftime("%H:%M:%S") + }) + + +@app.get("/health") +def health_check(): + """Health check endpoint""" + return { + "message": "Seismo Fleet Manager v0.1", + "status": "running" + } + + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8001) diff --git a/models.py b/backend/models.py similarity index 94% rename from models.py rename to backend/models.py index 9ae42c1..64490fc 100644 --- a/models.py +++ b/backend/models.py @@ -1,6 +1,6 @@ from sqlalchemy import Column, String, DateTime from datetime import datetime -from database import Base +from backend.database import Base class Emitter(Base): diff --git a/backend/routers/photos.py b/backend/routers/photos.py new file mode 100644 index 0000000..0d083bc --- /dev/null +++ b/backend/routers/photos.py @@ -0,0 +1,64 @@ +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// 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) diff --git a/backend/routers/roster.py b/backend/routers/roster.py new file mode 100644 index 0000000..d2792e1 --- /dev/null +++ b/backend/routers/roster.py @@ -0,0 +1,46 @@ +from fastapi import APIRouter, Depends +from sqlalchemy.orm import Session +from datetime import datetime, timedelta +from typing import Dict, Any +import random + +from backend.database import get_db +from backend.services.snapshot import emit_status_snapshot + +router = APIRouter(prefix="/api", tags=["roster"]) + + +@router.get("/status-snapshot") +def get_status_snapshot(db: Session = Depends(get_db)): + """ + Calls emit_status_snapshot() to get current fleet status. + This will be replaced with real Series3 emitter logic later. + """ + return emit_status_snapshot() + + +@router.get("/roster") +def get_roster(db: Session = Depends(get_db)): + """ + Returns list of units with their metadata and status. + Uses mock data for now. + """ + snapshot = emit_status_snapshot() + units_list = [] + + for unit_id, unit_data in snapshot["units"].items(): + units_list.append({ + "id": unit_id, + "status": unit_data["status"], + "age": unit_data["age"], + "last_seen": unit_data["last"], + "deployed": unit_data["deployed"], + "note": unit_data.get("note", ""), + "last_file": unit_data.get("fname", "") + }) + + # Sort by status priority (Missing > Pending > OK) then by ID + status_priority = {"Missing": 0, "Pending": 1, "OK": 2} + units_list.sort(key=lambda x: (status_priority.get(x["status"], 3), x["id"])) + + return {"units": units_list} diff --git a/backend/routers/units.py b/backend/routers/units.py new file mode 100644 index 0000000..654fa2c --- /dev/null +++ b/backend/routers/units.py @@ -0,0 +1,44 @@ +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session +from datetime import datetime +from typing import Dict, Any + +from backend.database import get_db +from backend.services.snapshot import emit_status_snapshot + +router = APIRouter(prefix="/api", tags=["units"]) + + +@router.get("/unit/{unit_id}") +def get_unit_detail(unit_id: str, db: Session = Depends(get_db)): + """ + Returns detailed data for a single unit. + """ + snapshot = emit_status_snapshot() + + if unit_id not in snapshot["units"]: + raise HTTPException(status_code=404, detail=f"Unit {unit_id} not found") + + unit_data = snapshot["units"][unit_id] + + # Mock coordinates for now (will be replaced with real data) + mock_coords = { + "BE1234": {"lat": 37.7749, "lon": -122.4194, "location": "San Francisco, CA"}, + "BE5678": {"lat": 34.0522, "lon": -118.2437, "location": "Los Angeles, CA"}, + "BE9012": {"lat": 40.7128, "lon": -74.0060, "location": "New York, NY"}, + "BE3456": {"lat": 41.8781, "lon": -87.6298, "location": "Chicago, IL"}, + "BE7890": {"lat": 29.7604, "lon": -95.3698, "location": "Houston, TX"}, + } + + coords = mock_coords.get(unit_id, {"lat": 39.8283, "lon": -98.5795, "location": "Unknown"}) + + return { + "id": unit_id, + "status": unit_data["status"], + "age": unit_data["age"], + "last_seen": unit_data["last"], + "last_file": unit_data.get("fname", ""), + "deployed": unit_data["deployed"], + "note": unit_data.get("note", ""), + "coordinates": coords + } diff --git a/routes.py b/backend/routes.py similarity index 96% rename from routes.py rename to backend/routes.py index 4cdb4fa..e7e34da 100644 --- a/routes.py +++ b/backend/routes.py @@ -4,8 +4,8 @@ from pydantic import BaseModel from datetime import datetime from typing import Optional, List -from database import get_db -from models import Emitter +from backend.database import get_db +from backend.models import Emitter router = APIRouter() diff --git a/backend/services/snapshot.py b/backend/services/snapshot.py new file mode 100644 index 0000000..8d7e38d --- /dev/null +++ b/backend/services/snapshot.py @@ -0,0 +1,96 @@ +""" +Mock implementation of emit_status_snapshot(). +This will be replaced with real Series3 emitter logic by the user. +""" + +from datetime import datetime, timedelta +import random + + +def emit_status_snapshot(): + """ + Mock function that returns fleet status snapshot. + In production, this will call the real Series3 emitter logic. + + Returns a dictionary with unit statuses, ages, deployment status, etc. + """ + + # Mock data for demonstration + mock_units = { + "BE1234": { + "status": "OK", + "age": "1h 12m", + "last": "2025-11-22 12:32:10", + "fname": "evt_1234.mlg", + "deployed": True, + "note": "Bridge monitoring project - Golden Gate" + }, + "BE5678": { + "status": "Pending", + "age": "2h 45m", + "last": "2025-11-22 11:05:33", + "fname": "evt_5678.mlg", + "deployed": True, + "note": "Dam structural analysis" + }, + "BE9012": { + "status": "Missing", + "age": "5d 3h", + "last": "2025-11-17 09:15:00", + "fname": "evt_9012.mlg", + "deployed": True, + "note": "Tunnel excavation site" + }, + "BE3456": { + "status": "OK", + "age": "30m", + "last": "2025-11-22 13:20:45", + "fname": "evt_3456.mlg", + "deployed": False, + "note": "Benched for maintenance" + }, + "BE7890": { + "status": "OK", + "age": "15m", + "last": "2025-11-22 13:35:22", + "fname": "evt_7890.mlg", + "deployed": True, + "note": "Pipeline monitoring" + }, + "BE2468": { + "status": "Pending", + "age": "4h 20m", + "last": "2025-11-22 09:30:15", + "fname": "evt_2468.mlg", + "deployed": True, + "note": "Building foundation survey" + }, + "BE1357": { + "status": "OK", + "age": "45m", + "last": "2025-11-22 13:05:00", + "fname": "evt_1357.mlg", + "deployed": False, + "note": "Awaiting deployment" + }, + "BE8642": { + "status": "Missing", + "age": "2d 12h", + "last": "2025-11-20 01:30:00", + "fname": "evt_8642.mlg", + "deployed": True, + "note": "Offshore platform - comms issue suspected" + } + } + + return { + "units": mock_units, + "timestamp": datetime.now().isoformat(), + "total_units": len(mock_units), + "deployed_units": sum(1 for u in mock_units.values() if u["deployed"]), + "status_summary": { + "OK": sum(1 for u in mock_units.values() if u["status"] == "OK"), + "Pending": sum(1 for u in mock_units.values() if u["status"] == "Pending"), + "Missing": sum(1 for u in mock_units.values() if u["status"] == "Missing") + } + } diff --git a/backend/static/style.css b/backend/static/style.css new file mode 100644 index 0000000..e05260a --- /dev/null +++ b/backend/static/style.css @@ -0,0 +1,12 @@ +/* Custom styles for Seismo Fleet Manager */ + +/* Additional custom styles can go here */ + +.card-hover { + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +.card-hover:hover { + transform: translateY(-2px); + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1); +} diff --git a/requirements.txt b/requirements.txt index 4ab590c..f526daf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,5 @@ uvicorn[standard]==0.24.0 sqlalchemy==2.0.23 pydantic==2.5.0 python-multipart==0.0.6 +jinja2==3.1.2 +aiofiles==23.2.1 diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..155f6ac --- /dev/null +++ b/templates/base.html @@ -0,0 +1,150 @@ + + + + + + {% block title %}Seismo Fleet Manager{% endblock %} + + + + + + + + + + + + + + + + + {% block extra_head %}{% endblock %} + + +
+ + + + +
+
+ {% block content %}{% endblock %} +
+
+
+ + + + {% block extra_scripts %}{% endblock %} + + diff --git a/templates/dashboard.html b/templates/dashboard.html new file mode 100644 index 0000000..3430a4f --- /dev/null +++ b/templates/dashboard.html @@ -0,0 +1,178 @@ +{% extends "base.html" %} + +{% block title %}Dashboard - Seismo Fleet Manager{% endblock %} + +{% block content %} +
+

Dashboard

+

Fleet overview and recent activity

+
+ + +
+
+ +
+
+

Fleet Summary

+ + + +
+
+
+ Total Units + -- +
+
+ Deployed + -- +
+
+
+
+ + OK +
+ -- +
+
+
+ + Pending +
+ -- +
+
+
+ + Missing +
+ -- +
+
+
+
+ + +
+
+

Recent Alerts

+ + + +
+
+

Loading alerts...

+
+
+ + +
+
+

Recent Photos

+ + + +
+
+ + + +

No recent photos

+
+
+
+ + +
+
+

Fleet Status

+ + View All + + + + +
+
+

Loading fleet data...

+
+
+
+ + +{% endblock %} diff --git a/templates/partials/roster_table.html b/templates/partials/roster_table.html new file mode 100644 index 0000000..1b33fb3 --- /dev/null +++ b/templates/partials/roster_table.html @@ -0,0 +1,87 @@ +
+ + + + + + + + + + + + + {% for unit in units %} + + + + + + + + + {% endfor %} + +
+ Status + + Unit ID + + Last Seen + + Age + + Note + + Actions +
+
+ {% if unit.status == 'OK' %} + + {% elif unit.status == 'Pending' %} + + {% else %} + + {% endif %} + + {% if unit.deployed %} + + {% else %} + + {% endif %} +
+
+
{{ unit.id }}
+
+
{{ unit.last_seen }}
+
+
+ {{ unit.age }} +
+
+
+ {{ unit.note if unit.note else '-' }} +
+
+ + View + + + + +
+ + +
+ Last updated: {{ timestamp }} +
+
+ + diff --git a/templates/roster.html b/templates/roster.html new file mode 100644 index 0000000..c26f2f5 --- /dev/null +++ b/templates/roster.html @@ -0,0 +1,29 @@ +{% extends "base.html" %} + +{% block title %}Fleet Roster - Seismo Fleet Manager{% endblock %} + +{% block content %} +
+

Fleet Roster

+

Real-time status of all seismograph units

+
+ + +
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +{% endblock %} diff --git a/templates/unit_detail.html b/templates/unit_detail.html new file mode 100644 index 0000000..193c0f0 --- /dev/null +++ b/templates/unit_detail.html @@ -0,0 +1,268 @@ +{% extends "base.html" %} + +{% block title %}Unit {{ unit_id }} - Seismo Fleet Manager{% endblock %} + +{% block content %} +
+ + + + + Back to Fleet Roster + +

Unit {{ unit_id }}

+
+ + +
+
+ +
+ +
+

Unit Status

+
+
+ Status +
+ + Loading... +
+
+
+ Deployed + -- +
+
+ Age + -- +
+
+ Last Seen + -- +
+
+ Last File + -- +
+
+
+ + +
+

Notes

+
+ Loading... +
+
+ + +
+

Edit Metadata

+
+
+ + +
+ +
+
+
+ + +
+ +
+
+ +
+ + +
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + +
+
+
+
+
+ + +{% endblock %} From 02a99ea47d9edb73fcf46a9473d5c403db13a7eb Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 24 Nov 2025 23:49:21 +0000 Subject: [PATCH 2/2] Fix Docker configuration for new backend structure - Update Dockerfile to use backend.main:app instead of main:app - Change exposed port from 8000 to 8001 - Fix docker-compose.yml port mapping to 8001:8001 - Update healthcheck to use correct port and /health endpoint - Remove old main.py from root directory Docker now correctly runs the new frontend + backend structure. --- Dockerfile | 6 +++--- docker-compose.yml | 6 +++--- main.py | 41 ----------------------------------------- 3 files changed, 6 insertions(+), 47 deletions(-) delete mode 100644 main.py diff --git a/Dockerfile b/Dockerfile index 3533224..d567991 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,7 +13,7 @@ RUN pip install --no-cache-dir -r requirements.txt COPY . . # Expose port -EXPOSE 8000 +EXPOSE 8001 -# Run the application -CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] +# Run the application using the new backend structure +CMD ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "8001"] diff --git a/docker-compose.yml b/docker-compose.yml index 6214c26..410d866 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,15 +5,15 @@ services: build: . container_name: seismo-fleet-manager ports: - - "8001:8000" + - "8001:8001" volumes: - # Persist SQLite database + # Persist SQLite database and photos - ./data:/app/data environment: - PYTHONUNBUFFERED=1 restart: unless-stopped healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8000/"] + test: ["CMD", "curl", "-f", "http://localhost:8001/health"] interval: 30s timeout: 10s retries: 3 diff --git a/main.py b/main.py deleted file mode 100644 index 9458fd8..0000000 --- a/main.py +++ /dev/null @@ -1,41 +0,0 @@ -from fastapi import FastAPI -from fastapi.middleware.cors import CORSMiddleware - -from database import engine, Base -from routes import router - -# Create database tables -Base.metadata.create_all(bind=engine) - -# Initialize FastAPI app -app = FastAPI( - title="Seismo Fleet Manager", - description="Backend API for managing seismograph fleet status", - version="0.1.0" -) - -# Configure CORS (adjust origins as needed for your deployment) -app.add_middleware( - CORSMiddleware, - allow_origins=["*"], # In production, specify exact origins - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - -# Include API routes -app.include_router(router) - - -@app.get("/") -def root(): - """Root endpoint - health check""" - return { - "message": "Seismo Fleet Manager API v0.1", - "status": "running" - } - - -if __name__ == "__main__": - import uvicorn - uvicorn.run(app, host="0.0.0.0", port=8000)