Merge pull request #1 from serversdwn/claude/seismo-backend-server-01FsCdpT2WT4B342V3KtWx38
Build backend server for Seismo Fleet Manager v0.1
This commit is contained in:
19
.dockerignore
Normal file
19
.dockerignore
Normal file
@@ -0,0 +1,19 @@
|
||||
__pycache__
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
.Python
|
||||
*.so
|
||||
*.egg
|
||||
*.egg-info
|
||||
dist
|
||||
build
|
||||
.git
|
||||
.gitignore
|
||||
*.db
|
||||
*.db-journal
|
||||
.env
|
||||
.venv
|
||||
venv/
|
||||
ENV/
|
||||
data/
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -205,3 +205,9 @@ cython_debug/
|
||||
marimo/_static/
|
||||
marimo/_lsp/
|
||||
__marimo__/
|
||||
|
||||
# Seismo Fleet Manager
|
||||
# SQLite database files
|
||||
*.db
|
||||
*.db-journal
|
||||
data/
|
||||
|
||||
19
Dockerfile
Normal file
19
Dockerfile
Normal file
@@ -0,0 +1,19 @@
|
||||
FROM python:3.11-slim
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy requirements first for better caching
|
||||
COPY requirements.txt .
|
||||
|
||||
# Install dependencies
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Copy application code
|
||||
COPY . .
|
||||
|
||||
# Expose port
|
||||
EXPOSE 8000
|
||||
|
||||
# Run the application
|
||||
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
251
README.md
251
README.md
@@ -1,2 +1,249 @@
|
||||
# seismo-fleet-manager
|
||||
Web app and backend for tracking deployed units.
|
||||
# Seismo Fleet Manager - Backend v0.1
|
||||
|
||||
Backend API for managing seismograph fleet status. Track multiple seismographs calling in data from remote deployments, monitor their status, and manage your fleet through a unified database.
|
||||
|
||||
## Features
|
||||
|
||||
- **Fleet Monitoring**: Track all seismograph units in one place
|
||||
- **Status Management**: Automatically mark units as OK, Pending (>12h), or Missing (>24h)
|
||||
- **Data Ingestion**: Accept reports from emitter scripts via REST API
|
||||
- **SQLite Storage**: Lightweight, file-based database for easy deployment
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **FastAPI**: Modern, fast web framework
|
||||
- **SQLAlchemy**: SQL toolkit and ORM
|
||||
- **SQLite**: Lightweight database
|
||||
- **uvicorn**: ASGI server
|
||||
- **Docker**: Containerization for easy deployment
|
||||
|
||||
## Quick Start with Docker Compose (Recommended)
|
||||
|
||||
### Prerequisites
|
||||
- Docker and Docker Compose installed
|
||||
|
||||
### Running the Application
|
||||
|
||||
1. **Start the service:**
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
2. **Check logs:**
|
||||
```bash
|
||||
docker compose logs -f
|
||||
```
|
||||
|
||||
3. **Stop the service:**
|
||||
```bash
|
||||
docker compose down
|
||||
```
|
||||
|
||||
The API will be available at `http://localhost:8000`
|
||||
|
||||
### Data Persistence
|
||||
|
||||
The SQLite database is stored in the `./data` directory, which is mounted as a volume. Your data will persist even if you restart or rebuild the container.
|
||||
|
||||
## Local Development (Without Docker)
|
||||
|
||||
### Prerequisites
|
||||
- Python 3.11+
|
||||
- pip
|
||||
|
||||
### Setup
|
||||
|
||||
1. **Install dependencies:**
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
2. **Run the server:**
|
||||
```bash
|
||||
python main.py
|
||||
```
|
||||
|
||||
Or with auto-reload:
|
||||
```bash
|
||||
uvicorn main:app --reload
|
||||
```
|
||||
|
||||
The API will be available at `http://localhost:8000`
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Root
|
||||
- **GET** `/` - Health check
|
||||
|
||||
### Emitter Report
|
||||
- **POST** `/emitters/report`
|
||||
- Submit status report from a seismograph unit
|
||||
- **Request Body:**
|
||||
```json
|
||||
{
|
||||
"unit": "SEISMO-001",
|
||||
"unit_type": "series3",
|
||||
"timestamp": "2025-11-20T10:30:00",
|
||||
"file": "event_20251120_103000.dat",
|
||||
"status": "OK"
|
||||
}
|
||||
```
|
||||
- **Response:**
|
||||
```json
|
||||
{
|
||||
"message": "Emitter report received",
|
||||
"unit": "SEISMO-001",
|
||||
"status": "OK"
|
||||
}
|
||||
```
|
||||
|
||||
### Fleet Status
|
||||
- **GET** `/fleet/status`
|
||||
- Retrieve status of all seismograph units
|
||||
- **Response:**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "SEISMO-001",
|
||||
"unit_type": "series3",
|
||||
"last_seen": "2025-11-20T10:30:00",
|
||||
"last_file": "event_20251120_103000.dat",
|
||||
"status": "OK",
|
||||
"notes": null
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## API Documentation
|
||||
|
||||
Once running, interactive API documentation is available at:
|
||||
- **Swagger UI**: http://localhost:8000/docs
|
||||
- **ReDoc**: http://localhost:8000/redoc
|
||||
|
||||
## Testing the API
|
||||
|
||||
### Using curl
|
||||
|
||||
**Submit a report:**
|
||||
```bash
|
||||
curl -X POST http://localhost:8000/emitters/report \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"unit": "SEISMO-001",
|
||||
"unit_type": "series3",
|
||||
"timestamp": "2025-11-20T10:30:00",
|
||||
"file": "event_20251120_103000.dat",
|
||||
"status": "OK"
|
||||
}'
|
||||
```
|
||||
|
||||
**Get fleet status:**
|
||||
```bash
|
||||
curl http://localhost:8000/fleet/status
|
||||
```
|
||||
|
||||
### Using Python
|
||||
|
||||
```python
|
||||
import requests
|
||||
from datetime import datetime
|
||||
|
||||
# Submit report
|
||||
response = requests.post(
|
||||
"http://localhost:8000/emitters/report",
|
||||
json={
|
||||
"unit": "SEISMO-001",
|
||||
"unit_type": "series3",
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
"file": "event_20251120_103000.dat",
|
||||
"status": "OK"
|
||||
}
|
||||
)
|
||||
print(response.json())
|
||||
|
||||
# Get fleet status
|
||||
response = requests.get("http://localhost:8000/fleet/status")
|
||||
print(response.json())
|
||||
```
|
||||
|
||||
## Data Model
|
||||
|
||||
### Emitters Table
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| id | string | Unit identifier (primary key) |
|
||||
| unit_type | string | Type of seismograph (e.g., "series3") |
|
||||
| last_seen | datetime | Last report timestamp |
|
||||
| last_file | string | Last file processed |
|
||||
| status | string | Current status: OK, Pending, Missing |
|
||||
| notes | string | Optional notes (nullable) |
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
seismo-fleet-manager/
|
||||
├── main.py # FastAPI app entry point
|
||||
├── database.py # SQLAlchemy database configuration
|
||||
├── models.py # Database models
|
||||
├── routes.py # API endpoints
|
||||
├── requirements.txt # Python dependencies
|
||||
├── Dockerfile # Docker container definition
|
||||
├── docker-compose.yml # Docker Compose configuration
|
||||
├── .dockerignore # Docker ignore rules
|
||||
└── data/ # SQLite database directory (created at runtime)
|
||||
```
|
||||
|
||||
## Docker Commands
|
||||
|
||||
**Build the image:**
|
||||
```bash
|
||||
docker compose build
|
||||
```
|
||||
|
||||
**Start in foreground:**
|
||||
```bash
|
||||
docker compose up
|
||||
```
|
||||
|
||||
**Start in background:**
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
**View logs:**
|
||||
```bash
|
||||
docker compose logs -f seismo-backend
|
||||
```
|
||||
|
||||
**Restart service:**
|
||||
```bash
|
||||
docker compose restart
|
||||
```
|
||||
|
||||
**Stop and remove containers:**
|
||||
```bash
|
||||
docker compose down
|
||||
```
|
||||
|
||||
**Remove containers and volumes:**
|
||||
```bash
|
||||
docker compose down -v
|
||||
```
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- Automated status updates based on last_seen timestamps
|
||||
- Web-based dashboard for fleet monitoring
|
||||
- Email/SMS alerts for missing units
|
||||
- Historical data tracking and reporting
|
||||
- Multi-user authentication
|
||||
- PostgreSQL support for larger deployments
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
## Version
|
||||
|
||||
0.1.0 - Initial Release
|
||||
|
||||
26
database.py
Normal file
26
database.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
import os
|
||||
|
||||
# Ensure data directory exists
|
||||
os.makedirs("data", exist_ok=True)
|
||||
|
||||
SQLALCHEMY_DATABASE_URL = "sqlite:///./data/seismo_fleet.db"
|
||||
|
||||
engine = create_engine(
|
||||
SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
|
||||
)
|
||||
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
|
||||
def get_db():
|
||||
"""Dependency for database sessions"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
23
docker-compose.yml
Normal file
23
docker-compose.yml
Normal file
@@ -0,0 +1,23 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
seismo-backend:
|
||||
build: .
|
||||
container_name: seismo-fleet-manager
|
||||
ports:
|
||||
- "8001:8000"
|
||||
volumes:
|
||||
# Persist SQLite database
|
||||
- ./data:/app/data
|
||||
environment:
|
||||
- PYTHONUNBUFFERED=1
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8000/"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
|
||||
volumes:
|
||||
data:
|
||||
41
main.py
Normal file
41
main.py
Normal file
@@ -0,0 +1,41 @@
|
||||
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)
|
||||
18
models.py
Normal file
18
models.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from sqlalchemy import Column, String, DateTime
|
||||
from datetime import datetime
|
||||
from database import Base
|
||||
|
||||
|
||||
class Emitter(Base):
|
||||
"""Emitter model representing a seismograph unit in the fleet"""
|
||||
__tablename__ = "emitters"
|
||||
|
||||
id = Column(String, primary_key=True, index=True)
|
||||
unit_type = Column(String, nullable=False)
|
||||
last_seen = Column(DateTime, default=datetime.utcnow)
|
||||
last_file = Column(String, nullable=False)
|
||||
status = Column(String, nullable=False) # OK, Pending, Missing
|
||||
notes = Column(String, nullable=True)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Emitter(id={self.id}, type={self.unit_type}, status={self.status})>"
|
||||
5
requirements.txt
Normal file
5
requirements.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
fastapi==0.104.1
|
||||
uvicorn[standard]==0.24.0
|
||||
sqlalchemy==2.0.23
|
||||
pydantic==2.5.0
|
||||
python-multipart==0.0.6
|
||||
82
routes.py
Normal file
82
routes.py
Normal file
@@ -0,0 +1,82 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from pydantic import BaseModel
|
||||
from datetime import datetime
|
||||
from typing import Optional, List
|
||||
|
||||
from database import get_db
|
||||
from models import Emitter
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
# Pydantic schemas for request/response validation
|
||||
class EmitterReport(BaseModel):
|
||||
unit: str
|
||||
unit_type: str
|
||||
timestamp: str
|
||||
file: str
|
||||
status: str
|
||||
|
||||
|
||||
class EmitterResponse(BaseModel):
|
||||
id: str
|
||||
unit_type: str
|
||||
last_seen: datetime
|
||||
last_file: str
|
||||
status: str
|
||||
notes: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
@router.post("/emitters/report", status_code=200)
|
||||
def report_emitter(report: EmitterReport, db: Session = Depends(get_db)):
|
||||
"""
|
||||
Endpoint for emitters to report their status.
|
||||
Creates a new emitter if it doesn't exist, or updates an existing one.
|
||||
"""
|
||||
try:
|
||||
# Parse the timestamp
|
||||
timestamp = datetime.fromisoformat(report.timestamp.replace('Z', '+00:00'))
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="Invalid timestamp format")
|
||||
|
||||
# Check if emitter already exists
|
||||
emitter = db.query(Emitter).filter(Emitter.id == report.unit).first()
|
||||
|
||||
if emitter:
|
||||
# Update existing emitter
|
||||
emitter.unit_type = report.unit_type
|
||||
emitter.last_seen = timestamp
|
||||
emitter.last_file = report.file
|
||||
emitter.status = report.status
|
||||
else:
|
||||
# Create new emitter
|
||||
emitter = Emitter(
|
||||
id=report.unit,
|
||||
unit_type=report.unit_type,
|
||||
last_seen=timestamp,
|
||||
last_file=report.file,
|
||||
status=report.status
|
||||
)
|
||||
db.add(emitter)
|
||||
|
||||
db.commit()
|
||||
db.refresh(emitter)
|
||||
|
||||
return {
|
||||
"message": "Emitter report received",
|
||||
"unit": emitter.id,
|
||||
"status": emitter.status
|
||||
}
|
||||
|
||||
|
||||
@router.get("/fleet/status", response_model=List[EmitterResponse])
|
||||
def get_fleet_status(db: Session = Depends(get_db)):
|
||||
"""
|
||||
Returns a list of all emitters and their current status.
|
||||
"""
|
||||
emitters = db.query(Emitter).all()
|
||||
return emitters
|
||||
Reference in New Issue
Block a user