diff --git a/backend/alembic/versions/010_drone_request_agl_altitude.py b/backend/alembic/versions/010_drone_request_agl_altitude.py
new file mode 100644
index 0000000..d3bc996
--- /dev/null
+++ b/backend/alembic/versions/010_drone_request_agl_altitude.py
@@ -0,0 +1,34 @@
+"""Rename drone request altitude to AGL
+
+Revision ID: 010_drone_request_agl_altitude
+Revises: 009_drone_requests
+Create Date: 2026-06-29 00:00:00.000000
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+revision = '010_drone_request_agl_altitude'
+down_revision = '009_drone_requests'
+branch_labels = None
+depends_on = None
+
+
+def upgrade() -> None:
+ op.alter_column(
+ 'drone_requests',
+ 'maximum_elevation_ft_amsl',
+ new_column_name='maximum_elevation_ft_agl',
+ existing_type=sa.Integer(),
+ existing_nullable=False,
+ )
+
+
+def downgrade() -> None:
+ op.alter_column(
+ 'drone_requests',
+ 'maximum_elevation_ft_agl',
+ new_column_name='maximum_elevation_ft_amsl',
+ existing_type=sa.Integer(),
+ existing_nullable=False,
+ )
diff --git a/backend/app/api/endpoints/drone_requests.py b/backend/app/api/endpoints/drone_requests.py
index 376acc6..7718331 100644
--- a/backend/app/api/endpoints/drone_requests.py
+++ b/backend/app/api/endpoints/drone_requests.py
@@ -52,7 +52,7 @@ async def _send_drone_email(drone_request, subject: str, message: str):
"takeoff_time": drone_request.estimated_takeoff_at.strftime("%Y-%m-%d %H:%M"),
"completion_time": drone_request.estimated_completion_at.strftime("%Y-%m-%d %H:%M"),
"location": drone_request.location_description or f"{drone_request.location_latitude}, {drone_request.location_longitude}",
- "maximum_elevation_ft_amsl": drone_request.maximum_elevation_ft_amsl,
+ "maximum_elevation_ft_agl": drone_request.maximum_elevation_ft_agl,
"edit_url": f"{settings.base_url}/drone-request.html?token={drone_request.public_token}" if drone_request.public_token else None,
},
)
@@ -70,7 +70,7 @@ async def _send_drone_submitted_email(drone_request):
"takeoff_time": drone_request.estimated_takeoff_at.strftime("%Y-%m-%d %H:%M"),
"completion_time": drone_request.estimated_completion_at.strftime("%Y-%m-%d %H:%M"),
"location": drone_request.location_description or f"{drone_request.location_latitude}, {drone_request.location_longitude}",
- "maximum_elevation_ft_amsl": drone_request.maximum_elevation_ft_amsl,
+ "maximum_elevation_ft_agl": drone_request.maximum_elevation_ft_agl,
"edit_url": f"{settings.base_url}/drone-request.html?token={drone_request.public_token}" if drone_request.public_token else None,
},
)
@@ -93,7 +93,7 @@ async def _send_drone_tower_notification(drone_request):
"takeoff_time": drone_request.estimated_takeoff_at.strftime("%Y-%m-%d %H:%M"),
"completion_time": drone_request.estimated_completion_at.strftime("%Y-%m-%d %H:%M"),
"location": drone_request.location_description or f"{drone_request.location_latitude}, {drone_request.location_longitude}",
- "maximum_elevation_ft_amsl": drone_request.maximum_elevation_ft_amsl,
+ "maximum_elevation_ft_agl": drone_request.maximum_elevation_ft_agl,
"inside_frz": "Yes" if drone_request.location_inside_frz else "No",
"notes": drone_request.applicant_notes,
"requests_url": f"{settings.base_url}/drone-requests",
@@ -115,7 +115,7 @@ async def _send_drone_approved_email(drone_request, message: Optional[str] = Non
"takeoff_time": drone_request.estimated_takeoff_at.strftime("%Y-%m-%d %H:%M"),
"completion_time": drone_request.estimated_completion_at.strftime("%Y-%m-%d %H:%M"),
"location": drone_request.location_description or f"{drone_request.location_latitude}, {drone_request.location_longitude}",
- "maximum_elevation_ft_amsl": drone_request.maximum_elevation_ft_amsl,
+ "maximum_elevation_ft_agl": drone_request.maximum_elevation_ft_agl,
"edit_url": f"{settings.base_url}/drone-request.html?token={drone_request.public_token}" if drone_request.public_token else None,
},
)
diff --git a/backend/app/models/drone_request.py b/backend/app/models/drone_request.py
index fbfbb78..6bb593a 100644
--- a/backend/app/models/drone_request.py
+++ b/backend/app/models/drone_request.py
@@ -35,7 +35,7 @@ class DroneRequest(Base):
estimated_completion_time = Column(String(8), nullable=True)
estimated_takeoff_at = Column(DateTime, nullable=False, index=True)
estimated_completion_at = Column(DateTime, nullable=False, index=True)
- maximum_elevation_ft_amsl = Column(Integer, nullable=False)
+ maximum_elevation_ft_agl = Column(Integer, nullable=False)
location_description = Column(Text, nullable=True)
location_latitude = Column(Float, nullable=False)
diff --git a/backend/app/schemas/drone_request.py b/backend/app/schemas/drone_request.py
index f96322a..d1e6eb2 100644
--- a/backend/app/schemas/drone_request.py
+++ b/backend/app/schemas/drone_request.py
@@ -2,7 +2,7 @@ from datetime import date, datetime
from enum import Enum
from typing import Any, Optional
-from pydantic import BaseModel, EmailStr, Field, validator
+from pydantic import AliasChoices, BaseModel, EmailStr, Field, validator
class DroneRequestStatus(str, Enum):
@@ -20,7 +20,11 @@ class DroneRequestBase(BaseModel):
flight_date: Optional[date] = None
estimated_takeoff_time: Optional[str] = Field(None, max_length=8)
estimated_completion_time: Optional[str] = Field(None, max_length=8)
- maximum_elevation_ft_amsl: int = Field(..., ge=0)
+ maximum_elevation_ft_agl: int = Field(
+ ...,
+ ge=0,
+ validation_alias=AliasChoices("maximum_elevation_ft_agl", "maximum_elevation_ft_amsl"),
+ )
location_description: Optional[str] = None
location_latitude: float = Field(..., ge=-90, le=90)
location_longitude: float = Field(..., ge=-180, le=180)
@@ -68,7 +72,11 @@ class DroneRequestUpdate(BaseModel):
estimated_completion_time: Optional[str] = Field(None, max_length=8)
estimated_takeoff_at: Optional[datetime] = None
estimated_completion_at: Optional[datetime] = None
- maximum_elevation_ft_amsl: Optional[int] = Field(None, ge=0)
+ maximum_elevation_ft_agl: Optional[int] = Field(
+ None,
+ ge=0,
+ validation_alias=AliasChoices("maximum_elevation_ft_agl", "maximum_elevation_ft_amsl"),
+ )
location_description: Optional[str] = None
location_latitude: Optional[float] = Field(None, ge=-90, le=90)
location_longitude: Optional[float] = Field(None, ge=-180, le=180)
diff --git a/backend/app/templates/drone_request_approved.html b/backend/app/templates/drone_request_approved.html
index 989a19d..3fbfbd9 100644
--- a/backend/app/templates/drone_request_approved.html
+++ b/backend/app/templates/drone_request_approved.html
@@ -53,7 +53,7 @@
| Max elevation |
- {{ maximum_elevation_ft_amsl }} ft AMSL |
+ {{ maximum_elevation_ft_agl }} ft AGL |
diff --git a/backend/app/templates/drone_request_submitted.html b/backend/app/templates/drone_request_submitted.html
index 5860708..f67ad2e 100644
--- a/backend/app/templates/drone_request_submitted.html
+++ b/backend/app/templates/drone_request_submitted.html
@@ -47,7 +47,7 @@
| Max elevation |
- {{ maximum_elevation_ft_amsl }} ft AMSL |
+ {{ maximum_elevation_ft_agl }} ft AGL |
diff --git a/backend/app/templates/drone_request_tower_notification.html b/backend/app/templates/drone_request_tower_notification.html
index 1b4790f..a9b92fb 100644
--- a/backend/app/templates/drone_request_tower_notification.html
+++ b/backend/app/templates/drone_request_tower_notification.html
@@ -31,7 +31,7 @@
| Completion | {{ completion_time }} |
| Location | {{ location }} |
| Inside FRZ | {{ inside_frz }} |
- | Max elevation | {{ maximum_elevation_ft_amsl }} ft AMSL |
+ | Max elevation | {{ maximum_elevation_ft_agl }} ft AGL |
| Applicant notes | {{ notes or '-' }} |
diff --git a/backend/app/templates/drone_request_update.html b/backend/app/templates/drone_request_update.html
index 7243104..3ac882c 100644
--- a/backend/app/templates/drone_request_update.html
+++ b/backend/app/templates/drone_request_update.html
@@ -47,7 +47,7 @@
| Max elevation |
- {{ maximum_elevation_ft_amsl }} ft AMSL |
+ {{ maximum_elevation_ft_agl }} ft AGL |
diff --git a/backend/tests/test_drone_requests_api.py b/backend/tests/test_drone_requests_api.py
index 37c7a82..ff24e40 100644
--- a/backend/tests/test_drone_requests_api.py
+++ b/backend/tests/test_drone_requests_api.py
@@ -15,7 +15,7 @@ def drone_payload(**overrides):
"estimated_completion_time": "10:30",
"estimated_takeoff_at": "2026-06-20T10:00:00",
"estimated_completion_at": "2026-06-20T10:30:00",
- "maximum_elevation_ft_amsl": 250,
+ "maximum_elevation_ft_agl": 250,
"location_description": "North apron",
"location_latitude": 51.623389,
"location_longitude": -4.069231,
@@ -81,6 +81,23 @@ def test_public_drone_request_create_edit_cancel_and_journal(client, db, monkeyp
assert client.delete("/api/v1/drone-requests/public/cancel/missing-token").status_code == 404
+def test_drone_request_accepts_legacy_amsl_altitude_key(client, db, monkeypatch):
+ async def fake_send_email(**kwargs):
+ return True
+
+ monkeypatch.setattr("app.api.endpoints.drone_requests.email_service.send_email", fake_send_email)
+
+ payload = drone_payload()
+ payload["maximum_elevation_ft_amsl"] = payload.pop("maximum_elevation_ft_agl")
+
+ create_response = client.post("/api/v1/drone-requests/public", json=payload)
+
+ assert create_response.status_code == 200
+ assert create_response.json()["maximum_elevation_ft_agl"] == 250
+ db_request = db.query(DroneRequest).filter(DroneRequest.id == create_response.json()["id"]).one()
+ assert db_request.maximum_elevation_ft_agl == 250
+
+
def test_authenticated_drone_request_list_update_status_comment_and_journal(auth_client, db, monkeypatch):
sent_emails = []
@@ -100,7 +117,7 @@ def test_authenticated_drone_request_list_update_status_comment_and_journal(auth
get_response = auth_client.get(f"/api/v1/drone-requests/{created['id']}")
update_response = auth_client.patch(
f"/api/v1/drone-requests/{created['id']}",
- json={"operator_comments": "Needs tower review", "maximum_elevation_ft_amsl": 200},
+ json={"operator_comments": "Needs tower review", "maximum_elevation_ft_agl": 200},
)
status_response = auth_client.patch(
f"/api/v1/drone-requests/{created['id']}/status",
@@ -116,7 +133,7 @@ def test_authenticated_drone_request_list_update_status_comment_and_journal(auth
assert [request["id"] for request in list_response.json()] == [created["id"]]
assert get_response.status_code == 200
assert update_response.status_code == 200
- assert update_response.json()["maximum_elevation_ft_amsl"] == 200
+ assert update_response.json()["maximum_elevation_ft_agl"] == 200
assert status_response.status_code == 200
assert status_response.json()["status"] == "APPROVED"
assert status_response.json()["operator_comments"] == "Approved below 200ft"
@@ -133,7 +150,7 @@ def test_authenticated_drone_request_list_update_status_comment_and_journal(auth
def test_drone_request_not_found_and_validation_paths(auth_client, client):
invalid_response = client.post(
"/api/v1/drone-requests/public",
- json=drone_payload(operator_name=" ", location_latitude=100, maximum_elevation_ft_amsl=-1),
+ json=drone_payload(operator_name=" ", location_latitude=100, maximum_elevation_ft_agl=-1),
)
assert invalid_response.status_code == 422
diff --git a/tests/e2e/test_drone_requests.py b/tests/e2e/test_drone_requests.py
index 0ebf43d..e8085f4 100644
--- a/tests/e2e/test_drone_requests.py
+++ b/tests/e2e/test_drone_requests.py
@@ -21,7 +21,7 @@ def drone_payload(operator_name):
"estimated_completion_time": "10:30",
"estimated_takeoff_at": "2026-06-21T10:00:00",
"estimated_completion_at": "2026-06-21T10:30:00",
- "maximum_elevation_ft_amsl": 200,
+ "maximum_elevation_ft_agl": 200,
"location_description": "E2E north apron survey",
"location_latitude": 51.623389,
"location_longitude": -4.069231,
diff --git a/web/drone-request.html b/web/drone-request.html
index 65d5f2a..e06f158 100644
--- a/web/drone-request.html
+++ b/web/drone-request.html
@@ -149,8 +149,8 @@
-
-
+
+
@@ -228,7 +228,7 @@
setValue('phone', request.phone);
setValue('estimated_takeoff_at', toLocalInputValue(request.estimated_takeoff_at));
setValue('estimated_completion_at', toLocalInputValue(request.estimated_completion_at));
- setValue('maximum_elevation_ft_amsl', request.maximum_elevation_ft_amsl);
+ setValue('maximum_elevation_ft_agl', request.maximum_elevation_ft_agl);
setValue('location_inside_frz', request.location_inside_frz ? 'Yes' : 'No');
setValue('location_latitude', request.location_latitude);
setValue('location_longitude', request.location_longitude);
@@ -260,7 +260,7 @@
phone: value('phone') || null,
estimated_takeoff_at: fromLocalInputValue(value('estimated_takeoff_at')),
estimated_completion_at: fromLocalInputValue(value('estimated_completion_at')),
- maximum_elevation_ft_amsl: Number(value('maximum_elevation_ft_amsl')),
+ maximum_elevation_ft_agl: Number(value('maximum_elevation_ft_agl')),
location_latitude: Number(value('location_latitude')),
location_longitude: Number(value('location_longitude')),
location_description: value('location_description') || null,
diff --git a/web/drone-requests.html b/web/drone-requests.html
index f69c92f..e6b0c84 100644
--- a/web/drone-requests.html
+++ b/web/drone-requests.html
@@ -795,7 +795,7 @@
${field('Flyer ID', selectedRequest.flyer_id)}
${field('Takeoff', formatDateTime(selectedRequest.estimated_takeoff_at))}
${field('Completion', formatDateTime(selectedRequest.estimated_completion_at))}
- ${field('Max Elevation', `${selectedRequest.maximum_elevation_ft_amsl} ft AMSL`)}
+ ${field('Max Elevation', `${selectedRequest.maximum_elevation_ft_agl} ft AGL`)}
${field('Inside FRZ', selectedRequest.location_inside_frz === null ? '-' : (selectedRequest.location_inside_frz ? 'Yes' : 'No'))}
${field('Latitude', selectedRequest.location_latitude)}
${field('Longitude', selectedRequest.location_longitude)}
@@ -862,7 +862,7 @@
addLayer(L.marker(point).addTo(map).bindPopup(`
${escapeHtml(selectedRequest.reference_number)}
${escapeHtml(selectedRequest.operator_name)}
- ${selectedRequest.maximum_elevation_ft_amsl} ft AMSL
+ ${selectedRequest.maximum_elevation_ft_agl} ft AGL
`));
}