From 2d4f1467de0f3ce89345dc361e247bd0221bef15 Mon Sep 17 00:00:00 2001 From: James Pattinson Date: Tue, 16 Dec 2025 12:59:43 -0500 Subject: [PATCH] Circuits handling --- backend/alembic/versions/002_local_flights.py | 20 ++- backend/app/api/api.py | 3 +- backend/app/api/endpoints/circuits.py | 108 ++++++++++++++++ backend/app/crud/crud_circuit.py | 55 ++++++++ backend/app/models/circuit.py | 12 ++ backend/app/schemas/circuit.py | 24 ++++ web/admin.html | 117 ++++++++++++++++++ 7 files changed, 337 insertions(+), 2 deletions(-) create mode 100644 backend/app/api/endpoints/circuits.py create mode 100644 backend/app/crud/crud_circuit.py create mode 100644 backend/app/models/circuit.py create mode 100644 backend/app/schemas/circuit.py diff --git a/backend/alembic/versions/002_local_flights.py b/backend/alembic/versions/002_local_flights.py index 1596c26..efee2ef 100644 --- a/backend/alembic/versions/002_local_flights.py +++ b/backend/alembic/versions/002_local_flights.py @@ -108,12 +108,30 @@ def upgrade() -> None: op.create_index('idx_arr_created_dt', 'arrivals', ['created_dt']) op.create_index('idx_arr_eta', 'arrivals', ['eta']) op.create_index('idx_arr_created_by', 'arrivals', ['created_by']) + + # Create circuits table for tracking touch-and-go events during circuit training + op.create_table('circuits', + sa.Column('id', sa.BigInteger(), autoincrement=True, nullable=False), + sa.Column('local_flight_id', sa.BigInteger(), nullable=False), + sa.Column('circuit_timestamp', sa.DateTime(), nullable=False), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.ForeignKeyConstraint(['local_flight_id'], ['local_flights.id'], ondelete='CASCADE'), + mysql_engine='InnoDB', + mysql_charset='utf8mb4', + mysql_collate='utf8mb4_unicode_ci' + ) + + # Create indexes for circuits + op.create_index('idx_circuit_local_flight_id', 'circuits', ['local_flight_id']) + op.create_index('idx_circuit_timestamp', 'circuits', ['circuit_timestamp']) def downgrade() -> None: """ - Drop the local_flights, departures, and arrivals tables. + Drop the circuits, arrivals, departures, and local_flights tables. """ + op.drop_table('circuits') op.drop_table('arrivals') op.drop_table('departures') op.drop_table('local_flights') diff --git a/backend/app/api/api.py b/backend/app/api/api.py index 8021ec3..488cd5c 100644 --- a/backend/app/api/api.py +++ b/backend/app/api/api.py @@ -1,5 +1,5 @@ from fastapi import APIRouter -from app.api.endpoints import auth, pprs, public, aircraft, airport, local_flights, departures, arrivals +from app.api.endpoints import auth, pprs, public, aircraft, airport, local_flights, departures, arrivals, circuits api_router = APIRouter() @@ -8,6 +8,7 @@ api_router.include_router(pprs.router, prefix="/pprs", tags=["pprs"]) api_router.include_router(local_flights.router, prefix="/local-flights", tags=["local_flights"]) api_router.include_router(departures.router, prefix="/departures", tags=["departures"]) api_router.include_router(arrivals.router, prefix="/arrivals", tags=["arrivals"]) +api_router.include_router(circuits.router, prefix="/circuits", tags=["circuits"]) api_router.include_router(public.router, prefix="/public", tags=["public"]) api_router.include_router(aircraft.router, prefix="/aircraft", tags=["aircraft"]) api_router.include_router(airport.router, prefix="/airport", tags=["airport"]) \ No newline at end of file diff --git a/backend/app/api/endpoints/circuits.py b/backend/app/api/endpoints/circuits.py new file mode 100644 index 0000000..697bfae --- /dev/null +++ b/backend/app/api/endpoints/circuits.py @@ -0,0 +1,108 @@ +from typing import List +from fastapi import APIRouter, Depends, HTTPException, status, Request +from sqlalchemy.orm import Session +from app.api.deps import get_db, get_current_read_user, get_current_operator_user +from app.crud.crud_circuit import crud_circuit +from app.schemas.circuit import Circuit, CircuitCreate, CircuitUpdate +from app.models.ppr import User +from app.core.utils import get_client_ip + +router = APIRouter() + + +@router.get("/", response_model=List[Circuit]) +async def get_circuits( + skip: int = 0, + limit: int = 100, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_read_user) +): + """Get circuit records""" + circuits = crud_circuit.get_multi(db, skip=skip, limit=limit) + return circuits + + +@router.get("/flight/{local_flight_id}", response_model=List[Circuit]) +async def get_circuits_by_flight( + local_flight_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_read_user) +): + """Get all circuits for a specific local flight""" + circuits = crud_circuit.get_by_local_flight(db, local_flight_id=local_flight_id) + return circuits + + +@router.post("/", response_model=Circuit) +async def create_circuit( + request: Request, + circuit_in: CircuitCreate, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_operator_user) +): + """Record a new circuit (touch and go) for a local flight""" + circuit = crud_circuit.create(db, obj_in=circuit_in) + + # Send real-time update via WebSocket + if hasattr(request.app.state, 'connection_manager'): + await request.app.state.connection_manager.broadcast({ + "type": "circuit_recorded", + "data": { + "id": circuit.id, + "local_flight_id": circuit.local_flight_id, + "circuit_timestamp": circuit.circuit_timestamp.isoformat() + } + }) + + return circuit + + +@router.get("/{circuit_id}", response_model=Circuit) +async def get_circuit( + circuit_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_read_user) +): + """Get a specific circuit record""" + circuit = crud_circuit.get(db, circuit_id=circuit_id) + if not circuit: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Circuit record not found" + ) + return circuit + + +@router.put("/{circuit_id}", response_model=Circuit) +async def update_circuit( + circuit_id: int, + circuit_in: CircuitUpdate, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_operator_user) +): + """Update a circuit record""" + circuit = crud_circuit.get(db, circuit_id=circuit_id) + if not circuit: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Circuit record not found" + ) + circuit = crud_circuit.update(db, db_obj=circuit, obj_in=circuit_in) + return circuit + + +@router.delete("/{circuit_id}") +async def delete_circuit( + circuit_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_operator_user) +): + """Delete a circuit record""" + circuit = crud_circuit.get(db, circuit_id=circuit_id) + if not circuit: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Circuit record not found" + ) + crud_circuit.delete(db, circuit_id=circuit_id) + return {"detail": "Circuit record deleted"} diff --git a/backend/app/crud/crud_circuit.py b/backend/app/crud/crud_circuit.py new file mode 100644 index 0000000..90bccf0 --- /dev/null +++ b/backend/app/crud/crud_circuit.py @@ -0,0 +1,55 @@ +from typing import List, Optional +from sqlalchemy.orm import Session +from sqlalchemy import desc +from datetime import datetime +from app.models.circuit import Circuit +from app.schemas.circuit import CircuitCreate, CircuitUpdate + + +class CRUDCircuit: + def get(self, db: Session, circuit_id: int) -> Optional[Circuit]: + return db.query(Circuit).filter(Circuit.id == circuit_id).first() + + def get_by_local_flight(self, db: Session, local_flight_id: int) -> List[Circuit]: + """Get all circuits for a specific local flight""" + return db.query(Circuit).filter( + Circuit.local_flight_id == local_flight_id + ).order_by(Circuit.circuit_timestamp).all() + + def get_multi( + self, + db: Session, + skip: int = 0, + limit: int = 100 + ) -> List[Circuit]: + return db.query(Circuit).order_by(desc(Circuit.created_at)).offset(skip).limit(limit).all() + + def create(self, db: Session, obj_in: CircuitCreate) -> Circuit: + db_obj = Circuit( + local_flight_id=obj_in.local_flight_id, + circuit_timestamp=obj_in.circuit_timestamp + ) + db.add(db_obj) + db.commit() + db.refresh(db_obj) + return db_obj + + def update(self, db: Session, db_obj: Circuit, obj_in: CircuitUpdate) -> Circuit: + obj_data = obj_in.dict(exclude_unset=True) + for field, value in obj_data.items(): + setattr(db_obj, field, value) + db.add(db_obj) + db.commit() + db.refresh(db_obj) + return db_obj + + def delete(self, db: Session, circuit_id: int) -> bool: + circuit = self.get(db, circuit_id) + if circuit: + db.delete(circuit) + db.commit() + return True + return False + + +crud_circuit = CRUDCircuit() diff --git a/backend/app/models/circuit.py b/backend/app/models/circuit.py new file mode 100644 index 0000000..c67fa20 --- /dev/null +++ b/backend/app/models/circuit.py @@ -0,0 +1,12 @@ +from sqlalchemy import Column, DateTime, BigInteger, ForeignKey +from sqlalchemy.sql import func +from app.db.session import Base + + +class Circuit(Base): + __tablename__ = "circuits" + + id = Column(BigInteger, primary_key=True, autoincrement=True) + local_flight_id = Column(BigInteger, ForeignKey("local_flights.id", ondelete="CASCADE"), nullable=False, index=True) + circuit_timestamp = Column(DateTime, nullable=False, index=True) + created_at = Column(DateTime, nullable=False, server_default=func.current_timestamp()) diff --git a/backend/app/schemas/circuit.py b/backend/app/schemas/circuit.py new file mode 100644 index 0000000..775059a --- /dev/null +++ b/backend/app/schemas/circuit.py @@ -0,0 +1,24 @@ +from pydantic import BaseModel +from datetime import datetime +from typing import Optional + + +class CircuitBase(BaseModel): + local_flight_id: int + circuit_timestamp: datetime + + +class CircuitCreate(CircuitBase): + pass + + +class CircuitUpdate(BaseModel): + circuit_timestamp: Optional[datetime] = None + + +class Circuit(CircuitBase): + id: int + created_at: datetime + + class Config: + from_attributes = True diff --git a/web/admin.html b/web/admin.html index ebfa212..b01e83d 100644 --- a/web/admin.html +++ b/web/admin.html @@ -797,6 +797,32 @@ + + +