From 2840ccb9b65f239fa26bdb44ec4002c4a974d542 Mon Sep 17 00:00:00 2001 From: Oleksandr Bezdieniezhnykh Date: Fri, 15 May 2026 04:35:49 +0300 Subject: [PATCH] refactor: rename project from Flights to Missions and update related components This commit transitions the project from Azaion.Flights to Azaion.Missions, updating namespaces, DTOs, services, and database entities accordingly. The Docker configuration and entry points have been modified to reflect the new project structure. Additionally, the README and documentation have been updated to clarify the ongoing renaming process and its implications. All references to flights have been replaced with missions, ensuring consistency across the codebase. --- .woodpecker/build-arm.yml | 4 +- Auth/JwtExtensions.cs | 7 +- Controllers/MissionsController.cs | 34 ++-- Controllers/VehiclesController.cs | 36 ++--- DTOs/CreateMissionRequest.cs | 6 +- DTOs/CreateVehicleRequest.cs | 8 +- DTOs/CreateWaypointRequest.cs | 4 +- DTOs/ErrorResponse.cs | 2 +- DTOs/GeoPoint.cs | 2 +- DTOs/GetMissionsQuery.cs | 4 +- DTOs/GetVehiclesQuery.cs | 4 +- DTOs/PaginatedResponse.cs | 2 +- DTOs/SetDefaultRequest.cs | 2 +- DTOs/UpdateMissionRequest.cs | 6 +- DTOs/UpdateVehicleRequest.cs | 8 +- DTOs/UpdateWaypointRequest.cs | 4 +- Database/AppDataConnection.cs | 10 +- Database/DatabaseMigrator.cs | 99 ++++++++---- Database/Entities/Annotation.cs | 2 +- Database/Entities/Detection.cs | 2 +- Database/Entities/MapObject.cs | 8 +- Database/Entities/Media.cs | 2 +- Database/Entities/Mission.cs | 17 +- Database/Entities/Vehicle.cs | 10 +- Database/Entities/Waypoint.cs | 12 +- Dockerfile | 2 +- Enums/FuelType.cs | 7 +- Enums/ObjectStatus.cs | 2 +- Enums/VehicleType.cs | 10 +- Enums/WaypointObjective.cs | 2 +- Enums/WaypointSource.cs | 2 +- Infrastructure/ConfigurationResolver.cs | 2 +- Infrastructure/CorsConfigurationValidator.cs | 2 +- Middleware/ErrorHandlingMiddleware.cs | 2 +- Program.cs | 14 +- README.md | 2 +- Services/MissionService.cs | 86 +++++----- Services/VehicleService.cs | 152 +++++++++++------- Services/WaypointService.cs | 32 ++-- .../01_vehicle_catalog/description.md | 4 +- .../components/05_identity/description.md | 7 +- _docs/_autodev_state.md | 8 +- .../2026-05-14_rename-flights-to-missions.md | 39 +++-- .../2026-05-14_step5-tracker-blocked.md | 50 ------ ...544_missions_rename_b5_csproj_namespace.md | 1 + ...AZ-545_missions_rename_b6_domain_rename.md | 1 + ...-546_missions_rename_b7_drop_gps_denied.md | 1 + .../AZ-547_missions_rename_b8_http_routes.md | 1 + .../AZ-548_missions_rename_b9_db_migration.md | 1 + ...issions_rename_b12_default_vehicle_rule.md | 1 + docker-compose.test.yml | 7 +- 51 files changed, 381 insertions(+), 352 deletions(-) delete mode 100644 _docs/_process_leftovers/2026-05-14_step5-tracker-blocked.md diff --git a/.woodpecker/build-arm.yml b/.woodpecker/build-arm.yml index 2059fe0..6c95f21 100644 --- a/.woodpecker/build-arm.yml +++ b/.woodpecker/build-arm.yml @@ -25,7 +25,7 @@ steps: --label org.opencontainers.image.revision=$CI_COMMIT_SHA \ --label org.opencontainers.image.created=$BUILD_DATE \ --label org.opencontainers.image.source=$CI_REPO_URL \ - -t $REGISTRY_HOST/azaion/flights:$TAG . - - docker push $REGISTRY_HOST/azaion/flights:$TAG + -t $REGISTRY_HOST/azaion/missions:$TAG . + - docker push $REGISTRY_HOST/azaion/missions:$TAG volumes: - /var/run/docker.sock:/var/run/docker.sock diff --git a/Auth/JwtExtensions.cs b/Auth/JwtExtensions.cs index e76767f..6f5db87 100644 --- a/Auth/JwtExtensions.cs +++ b/Auth/JwtExtensions.cs @@ -1,9 +1,9 @@ -using Azaion.Flights.Infrastructure; +using Azaion.Missions.Infrastructure; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.IdentityModel.Protocols; using Microsoft.IdentityModel.Tokens; -namespace Azaion.Flights.Auth; +namespace Azaion.Missions.Auth; public static class JwtExtensions { @@ -85,8 +85,7 @@ public static class JwtExtensions }); services.AddAuthorizationBuilder() - .AddPolicy("FL", p => p.RequireClaim("permissions", "FL")) - .AddPolicy("GPS", p => p.RequireClaim("permissions", "GPS")); + .AddPolicy("FL", p => p.RequireClaim("permissions", "FL")); return services; } diff --git a/Controllers/MissionsController.cs b/Controllers/MissionsController.cs index 359a324..4113a9e 100644 --- a/Controllers/MissionsController.cs +++ b/Controllers/MissionsController.cs @@ -1,47 +1,47 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Azaion.Flights.DTOs; -using Azaion.Flights.Services; +using Azaion.Missions.DTOs; +using Azaion.Missions.Services; -namespace Azaion.Flights.Controllers; +namespace Azaion.Missions.Controllers; [ApiController] -[Route("flights")] +[Route("missions")] [Authorize(Policy = "FL")] -public class FlightsController(FlightService flightService, WaypointService waypointService) : ControllerBase +public class MissionsController(MissionService missionService, WaypointService waypointService) : ControllerBase { [HttpPost] - public async Task Create([FromBody] CreateFlightRequest request) + public async Task Create([FromBody] CreateMissionRequest request) { - var flight = await flightService.CreateFlight(request); - return Created($"/flights/{flight.Id}", flight); + var mission = await missionService.CreateMission(request); + return Created($"/missions/{mission.Id}", mission); } [HttpPut("{id:guid}")] - public async Task Update(Guid id, [FromBody] UpdateFlightRequest request) + public async Task Update(Guid id, [FromBody] UpdateMissionRequest request) { - var flight = await flightService.UpdateFlight(id, request); - return Ok(flight); + var mission = await missionService.UpdateMission(id, request); + return Ok(mission); } [HttpGet("{id:guid}")] public async Task Get(Guid id) { - var flight = await flightService.GetFlight(id); - return Ok(flight); + var mission = await missionService.GetMission(id); + return Ok(mission); } [HttpGet] - public async Task GetAll([FromQuery] GetFlightsQuery query) + public async Task GetAll([FromQuery] GetMissionsQuery query) { - var result = await flightService.GetFlights(query); + var result = await missionService.GetMissions(query); return Ok(result); } [HttpDelete("{id:guid}")] public async Task Delete(Guid id) { - await flightService.DeleteFlight(id); + await missionService.DeleteMission(id); return NoContent(); } @@ -49,7 +49,7 @@ public class FlightsController(FlightService flightService, WaypointService wayp public async Task CreateWaypoint(Guid id, [FromBody] CreateWaypointRequest request) { var waypoint = await waypointService.CreateWaypoint(id, request); - return Created($"/flights/{id}/waypoints/{waypoint.Id}", waypoint); + return Created($"/missions/{id}/waypoints/{waypoint.Id}", waypoint); } [HttpPut("{id:guid}/waypoints/{waypointId:guid}")] diff --git a/Controllers/VehiclesController.cs b/Controllers/VehiclesController.cs index bbbdf48..9bb6c95 100644 --- a/Controllers/VehiclesController.cs +++ b/Controllers/VehiclesController.cs @@ -1,54 +1,54 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Azaion.Flights.DTOs; -using Azaion.Flights.Services; +using Azaion.Missions.DTOs; +using Azaion.Missions.Services; -namespace Azaion.Flights.Controllers; +namespace Azaion.Missions.Controllers; [ApiController] -[Route("aircrafts")] +[Route("vehicles")] [Authorize(Policy = "FL")] -public class AircraftsController(AircraftService aircraftService) : ControllerBase +public class VehiclesController(VehicleService vehicleService) : ControllerBase { [HttpPost] - public async Task Create([FromBody] CreateAircraftRequest request) + public async Task Create([FromBody] CreateVehicleRequest request) { - var aircraft = await aircraftService.CreateAircraft(request); - return Created($"/aircrafts/{aircraft.Id}", aircraft); + var vehicle = await vehicleService.CreateVehicle(request); + return Created($"/vehicles/{vehicle.Id}", vehicle); } [HttpPut("{id:guid}")] - public async Task Update(Guid id, [FromBody] UpdateAircraftRequest request) + public async Task Update(Guid id, [FromBody] UpdateVehicleRequest request) { - var aircraft = await aircraftService.UpdateAircraft(id, request); - return Ok(aircraft); + var vehicle = await vehicleService.UpdateVehicle(id, request); + return Ok(vehicle); } [HttpDelete("{id:guid}")] public async Task Delete(Guid id) { - await aircraftService.DeleteAircraft(id); + await vehicleService.DeleteVehicle(id); return NoContent(); } [HttpGet] - public async Task GetAll([FromQuery] GetAircraftsQuery query) + public async Task GetAll([FromQuery] GetVehiclesQuery query) { - var aircrafts = await aircraftService.GetAircrafts(query); - return Ok(aircrafts); + var vehicles = await vehicleService.GetVehicles(query); + return Ok(vehicles); } [HttpGet("{id:guid}")] public async Task Get(Guid id) { - var aircraft = await aircraftService.GetAircraft(id); - return Ok(aircraft); + var vehicle = await vehicleService.GetVehicle(id); + return Ok(vehicle); } [HttpPatch("{id:guid}/default")] public async Task SetDefault(Guid id, [FromBody] SetDefaultRequest request) { - await aircraftService.SetDefault(id, request); + await vehicleService.SetDefault(id, request); return NoContent(); } } diff --git a/DTOs/CreateMissionRequest.cs b/DTOs/CreateMissionRequest.cs index 2b3fc96..a949945 100644 --- a/DTOs/CreateMissionRequest.cs +++ b/DTOs/CreateMissionRequest.cs @@ -1,8 +1,8 @@ -namespace Azaion.Flights.DTOs; +namespace Azaion.Missions.DTOs; -public class CreateFlightRequest +public class CreateMissionRequest { - public Guid AircraftId { get; set; } + public Guid VehicleId { get; set; } public string Name { get; set; } = string.Empty; public DateTime? CreatedDate { get; set; } } diff --git a/DTOs/CreateVehicleRequest.cs b/DTOs/CreateVehicleRequest.cs index 5407fc3..a8953f2 100644 --- a/DTOs/CreateVehicleRequest.cs +++ b/DTOs/CreateVehicleRequest.cs @@ -1,10 +1,10 @@ -using Azaion.Flights.Enums; +using Azaion.Missions.Enums; -namespace Azaion.Flights.DTOs; +namespace Azaion.Missions.DTOs; -public class CreateAircraftRequest +public class CreateVehicleRequest { - public AircraftType Type { get; set; } + public VehicleType Type { get; set; } public string Model { get; set; } = string.Empty; public string Name { get; set; } = string.Empty; public FuelType FuelType { get; set; } diff --git a/DTOs/CreateWaypointRequest.cs b/DTOs/CreateWaypointRequest.cs index 280719d..b5916c5 100644 --- a/DTOs/CreateWaypointRequest.cs +++ b/DTOs/CreateWaypointRequest.cs @@ -1,6 +1,6 @@ -using Azaion.Flights.Enums; +using Azaion.Missions.Enums; -namespace Azaion.Flights.DTOs; +namespace Azaion.Missions.DTOs; public class CreateWaypointRequest { diff --git a/DTOs/ErrorResponse.cs b/DTOs/ErrorResponse.cs index b188e5b..c017113 100644 --- a/DTOs/ErrorResponse.cs +++ b/DTOs/ErrorResponse.cs @@ -1,4 +1,4 @@ -namespace Azaion.Flights.DTOs; +namespace Azaion.Missions.DTOs; public class ErrorResponse { diff --git a/DTOs/GeoPoint.cs b/DTOs/GeoPoint.cs index 61660eb..0a80da5 100644 --- a/DTOs/GeoPoint.cs +++ b/DTOs/GeoPoint.cs @@ -1,4 +1,4 @@ -namespace Azaion.Flights.DTOs; +namespace Azaion.Missions.DTOs; public class GeoPoint { diff --git a/DTOs/GetMissionsQuery.cs b/DTOs/GetMissionsQuery.cs index c664ff0..335b23b 100644 --- a/DTOs/GetMissionsQuery.cs +++ b/DTOs/GetMissionsQuery.cs @@ -1,6 +1,6 @@ -namespace Azaion.Flights.DTOs; +namespace Azaion.Missions.DTOs; -public class GetFlightsQuery +public class GetMissionsQuery { public string? Name { get; set; } public DateTime? FromDate { get; set; } diff --git a/DTOs/GetVehiclesQuery.cs b/DTOs/GetVehiclesQuery.cs index 6a4e3ff..69ea2eb 100644 --- a/DTOs/GetVehiclesQuery.cs +++ b/DTOs/GetVehiclesQuery.cs @@ -1,6 +1,6 @@ -namespace Azaion.Flights.DTOs; +namespace Azaion.Missions.DTOs; -public class GetAircraftsQuery +public class GetVehiclesQuery { public string? Name { get; set; } public bool? IsDefault { get; set; } diff --git a/DTOs/PaginatedResponse.cs b/DTOs/PaginatedResponse.cs index 35a8fba..4057f32 100644 --- a/DTOs/PaginatedResponse.cs +++ b/DTOs/PaginatedResponse.cs @@ -1,4 +1,4 @@ -namespace Azaion.Flights.DTOs; +namespace Azaion.Missions.DTOs; public class PaginatedResponse { diff --git a/DTOs/SetDefaultRequest.cs b/DTOs/SetDefaultRequest.cs index 1e3f167..0dfecf4 100644 --- a/DTOs/SetDefaultRequest.cs +++ b/DTOs/SetDefaultRequest.cs @@ -1,4 +1,4 @@ -namespace Azaion.Flights.DTOs; +namespace Azaion.Missions.DTOs; public class SetDefaultRequest { diff --git a/DTOs/UpdateMissionRequest.cs b/DTOs/UpdateMissionRequest.cs index 72f47fa..d6b4155 100644 --- a/DTOs/UpdateMissionRequest.cs +++ b/DTOs/UpdateMissionRequest.cs @@ -1,7 +1,7 @@ -namespace Azaion.Flights.DTOs; +namespace Azaion.Missions.DTOs; -public class UpdateFlightRequest +public class UpdateMissionRequest { public string? Name { get; set; } - public Guid? AircraftId { get; set; } + public Guid? VehicleId { get; set; } } diff --git a/DTOs/UpdateVehicleRequest.cs b/DTOs/UpdateVehicleRequest.cs index e132909..c023d6b 100644 --- a/DTOs/UpdateVehicleRequest.cs +++ b/DTOs/UpdateVehicleRequest.cs @@ -1,10 +1,10 @@ -using Azaion.Flights.Enums; +using Azaion.Missions.Enums; -namespace Azaion.Flights.DTOs; +namespace Azaion.Missions.DTOs; -public class UpdateAircraftRequest +public class UpdateVehicleRequest { - public AircraftType? Type { get; set; } + public VehicleType? Type { get; set; } public string? Model { get; set; } public string? Name { get; set; } public FuelType? FuelType { get; set; } diff --git a/DTOs/UpdateWaypointRequest.cs b/DTOs/UpdateWaypointRequest.cs index 5672923..f756514 100644 --- a/DTOs/UpdateWaypointRequest.cs +++ b/DTOs/UpdateWaypointRequest.cs @@ -1,6 +1,6 @@ -using Azaion.Flights.Enums; +using Azaion.Missions.Enums; -namespace Azaion.Flights.DTOs; +namespace Azaion.Missions.DTOs; public class UpdateWaypointRequest { diff --git a/Database/AppDataConnection.cs b/Database/AppDataConnection.cs index 3f83ae4..72f5983 100644 --- a/Database/AppDataConnection.cs +++ b/Database/AppDataConnection.cs @@ -1,16 +1,14 @@ using LinqToDB; using LinqToDB.Data; -using Azaion.Flights.Database.Entities; +using Azaion.Missions.Database.Entities; -namespace Azaion.Flights.Database; +namespace Azaion.Missions.Database; public class AppDataConnection(DataOptions options) : DataConnection(options) { - public ITable Aircrafts => this.GetTable(); - public ITable Flights => this.GetTable(); + public ITable Vehicles => this.GetTable(); + public ITable Missions => this.GetTable(); public ITable Waypoints => this.GetTable(); - public ITable Orthophotos => this.GetTable(); - public ITable GpsCorrections => this.GetTable(); public ITable MapObjects => this.GetTable(); public ITable Media => this.GetTable(); public ITable Annotations => this.GetTable(); diff --git a/Database/DatabaseMigrator.cs b/Database/DatabaseMigrator.cs index cb9a16d..6fb9aae 100644 --- a/Database/DatabaseMigrator.cs +++ b/Database/DatabaseMigrator.cs @@ -1,16 +1,64 @@ using LinqToDB.Data; -namespace Azaion.Flights.Database; +namespace Azaion.Missions.Database; +// Forward-only migrator. Two SQL blocks run on every container start, in order: +// +// 1. RenameAndDropSql -- idempotent ALTERs that bring a legacy `flights`-era +// schema up to the renamed `missions` schema, plus DROPs for the +// GPS-Denied tables (Jira AZ-EPIC B7 / B9). On a fresh DB or an +// already-migrated DB this block is a no-op (every statement guards on +// IF EXISTS / column existence). +// +// 2. InitSql -- CREATE TABLE IF NOT EXISTS for the post-migration schema. +// On a legacy DB the renames in (1) leave tables already present; this +// block is a no-op for them. On a fresh-install device this block IS +// the schema. On an already-migrated DB it is a no-op. +// +// Re-running the migrator on any DB state above produces no errors and no +// changes -- this is the suite's "idempotent forward-only" convention. public static class DatabaseMigrator { public static void Migrate(AppDataConnection db) { - db.Execute(Sql); + db.Execute(RenameAndDropSql); + db.Execute(InitSql); } - private const string Sql = """ - CREATE TABLE IF NOT EXISTS aircrafts ( + // Bring legacy `flights` / `aircrafts` schemas up to the renamed shape. + // Safe to re-run on any DB state. + private const string RenameAndDropSql = """ + ALTER TABLE IF EXISTS aircrafts RENAME TO vehicles; + ALTER TABLE IF EXISTS flights RENAME TO missions; + + DO $$ + BEGIN + IF EXISTS (SELECT 1 FROM information_schema.columns + WHERE table_name = 'missions' AND column_name = 'aircraft_id') THEN + ALTER TABLE missions RENAME COLUMN aircraft_id TO vehicle_id; + END IF; + IF EXISTS (SELECT 1 FROM information_schema.columns + WHERE table_name = 'waypoints' AND column_name = 'flight_id') THEN + ALTER TABLE waypoints RENAME COLUMN flight_id TO mission_id; + END IF; + IF EXISTS (SELECT 1 FROM information_schema.columns + WHERE table_name = 'map_objects' AND column_name = 'flight_id') THEN + ALTER TABLE map_objects RENAME COLUMN flight_id TO mission_id; + END IF; + END $$; + + ALTER INDEX IF EXISTS ix_flights_aircraft_id RENAME TO ix_missions_vehicle_id; + ALTER INDEX IF EXISTS ix_waypoints_flight_id RENAME TO ix_waypoints_mission_id; + ALTER INDEX IF EXISTS ix_map_objects_flight_id RENAME TO ix_map_objects_mission_id; + + DROP TABLE IF EXISTS orthophotos; + DROP TABLE IF EXISTS gps_corrections; + """; + + // Post-migration schema. CREATE TABLE IF NOT EXISTS is the idempotent path + // for fresh DBs; on already-migrated DBs every statement here is a no-op. + private const string InitSql = """ + CREATE TABLE IF NOT EXISTS vehicles ( id UUID PRIMARY KEY, type INTEGER NOT NULL DEFAULT 0, model TEXT NOT NULL, @@ -22,16 +70,16 @@ public static class DatabaseMigrator is_default BOOLEAN NOT NULL DEFAULT FALSE ); - CREATE TABLE IF NOT EXISTS flights ( + CREATE TABLE IF NOT EXISTS missions ( id UUID PRIMARY KEY, created_date TIMESTAMP NOT NULL DEFAULT NOW(), name TEXT NOT NULL, - aircraft_id UUID NOT NULL REFERENCES aircrafts(id) + vehicle_id UUID NOT NULL REFERENCES vehicles(id) ); CREATE TABLE IF NOT EXISTS waypoints ( id UUID PRIMARY KEY, - flight_id UUID NOT NULL REFERENCES flights(id), + mission_id UUID NOT NULL REFERENCES missions(id), lat NUMERIC, lon NUMERIC, mgrs TEXT, @@ -41,29 +89,9 @@ public static class DatabaseMigrator height NUMERIC NOT NULL DEFAULT 0 ); - CREATE TABLE IF NOT EXISTS orthophotos ( - id TEXT PRIMARY KEY, - flight_id UUID NOT NULL REFERENCES flights(id), - name TEXT NOT NULL, - path TEXT NOT NULL, - lat NUMERIC, - lon NUMERIC, - mgrs TEXT, - uploaded_at TIMESTAMP NOT NULL DEFAULT NOW() - ); - - CREATE TABLE IF NOT EXISTS gps_corrections ( - id UUID PRIMARY KEY, - flight_id UUID NOT NULL REFERENCES flights(id), - waypoint_id UUID NOT NULL REFERENCES waypoints(id), - original_gps TEXT NOT NULL, - corrected_gps TEXT NOT NULL, - applied_at TIMESTAMP NOT NULL DEFAULT NOW() - ); - CREATE TABLE IF NOT EXISTS map_objects ( id UUID PRIMARY KEY, - flight_id UUID NOT NULL REFERENCES flights(id), + mission_id UUID NOT NULL REFERENCES missions(id), h3_index TEXT NOT NULL, mgrs TEXT NOT NULL, lat NUMERIC, @@ -78,11 +106,14 @@ public static class DatabaseMigrator last_seen_at TIMESTAMP NOT NULL DEFAULT NOW() ); - CREATE INDEX IF NOT EXISTS ix_flights_aircraft_id ON flights(aircraft_id); - CREATE INDEX IF NOT EXISTS ix_waypoints_flight_id ON waypoints(flight_id); - CREATE INDEX IF NOT EXISTS ix_orthophotos_flight_id ON orthophotos(flight_id); - CREATE INDEX IF NOT EXISTS ix_gps_corrections_flight_id ON gps_corrections(flight_id); - CREATE INDEX IF NOT EXISTS ix_gps_corrections_waypoint_id ON gps_corrections(waypoint_id); - CREATE INDEX IF NOT EXISTS ix_map_objects_flight_id ON map_objects(flight_id); + CREATE INDEX IF NOT EXISTS ix_missions_vehicle_id ON missions(vehicle_id); + CREATE INDEX IF NOT EXISTS ix_waypoints_mission_id ON waypoints(mission_id); + CREATE INDEX IF NOT EXISTS ix_map_objects_mission_id ON map_objects(mission_id); + + -- B12 (Option A): exactly-one-default vehicle is enforced by a partial + -- unique index. Only rows with is_default = true are indexed; two such + -- rows would conflict. Existing 0-default and 1-default DBs are valid. + CREATE UNIQUE INDEX IF NOT EXISTS ux_vehicles_one_default + ON vehicles (is_default) WHERE is_default = TRUE; """; } diff --git a/Database/Entities/Annotation.cs b/Database/Entities/Annotation.cs index d01d960..9d2e1ed 100644 --- a/Database/Entities/Annotation.cs +++ b/Database/Entities/Annotation.cs @@ -1,6 +1,6 @@ using LinqToDB.Mapping; -namespace Azaion.Flights.Database.Entities; +namespace Azaion.Missions.Database.Entities; [Table("annotations")] public class Annotation diff --git a/Database/Entities/Detection.cs b/Database/Entities/Detection.cs index 8fe0bf7..35afd60 100644 --- a/Database/Entities/Detection.cs +++ b/Database/Entities/Detection.cs @@ -1,6 +1,6 @@ using LinqToDB.Mapping; -namespace Azaion.Flights.Database.Entities; +namespace Azaion.Missions.Database.Entities; [Table("detection")] public class Detection diff --git a/Database/Entities/MapObject.cs b/Database/Entities/MapObject.cs index b339b11..c6ca220 100644 --- a/Database/Entities/MapObject.cs +++ b/Database/Entities/MapObject.cs @@ -1,7 +1,7 @@ using LinqToDB.Mapping; -using Azaion.Flights.Enums; +using Azaion.Missions.Enums; -namespace Azaion.Flights.Database.Entities; +namespace Azaion.Missions.Database.Entities; [Table("map_objects")] public class MapObject @@ -10,8 +10,8 @@ public class MapObject [Column("id")] public Guid Id { get; set; } - [Column("flight_id")] - public Guid FlightId { get; set; } + [Column("mission_id")] + public Guid MissionId { get; set; } [Column("h3_index")] public string H3Index { get; set; } = string.Empty; diff --git a/Database/Entities/Media.cs b/Database/Entities/Media.cs index 1f425cb..06f13fb 100644 --- a/Database/Entities/Media.cs +++ b/Database/Entities/Media.cs @@ -1,6 +1,6 @@ using LinqToDB.Mapping; -namespace Azaion.Flights.Database.Entities; +namespace Azaion.Missions.Database.Entities; [Table("media")] public class Media diff --git a/Database/Entities/Mission.cs b/Database/Entities/Mission.cs index 53c00b5..99f45e9 100644 --- a/Database/Entities/Mission.cs +++ b/Database/Entities/Mission.cs @@ -1,10 +1,9 @@ using LinqToDB.Mapping; -using Azaion.Flights.Enums; -namespace Azaion.Flights.Database.Entities; +namespace Azaion.Missions.Database.Entities; -[Table("flights")] -public class Flight +[Table("missions")] +public class Mission { [PrimaryKey] [Column("id")] @@ -16,12 +15,12 @@ public class Flight [Column("name")] public string Name { get; set; } = string.Empty; - [Column("aircraft_id")] - public Guid AircraftId { get; set; } + [Column("vehicle_id")] + public Guid VehicleId { get; set; } - [Association(ThisKey = nameof(AircraftId), OtherKey = nameof(Aircraft.Id))] - public Aircraft? Aircraft { get; set; } + [Association(ThisKey = nameof(VehicleId), OtherKey = nameof(Vehicle.Id))] + public Vehicle? Vehicle { get; set; } - [Association(ThisKey = nameof(Id), OtherKey = nameof(Waypoint.FlightId))] + [Association(ThisKey = nameof(Id), OtherKey = nameof(Waypoint.MissionId))] public List Waypoints { get; set; } = []; } diff --git a/Database/Entities/Vehicle.cs b/Database/Entities/Vehicle.cs index 72ea2d9..c70867c 100644 --- a/Database/Entities/Vehicle.cs +++ b/Database/Entities/Vehicle.cs @@ -1,17 +1,17 @@ using LinqToDB.Mapping; -using Azaion.Flights.Enums; +using Azaion.Missions.Enums; -namespace Azaion.Flights.Database.Entities; +namespace Azaion.Missions.Database.Entities; -[Table("aircrafts")] -public class Aircraft +[Table("vehicles")] +public class Vehicle { [PrimaryKey] [Column("id")] public Guid Id { get; set; } [Column("type")] - public AircraftType Type { get; set; } + public VehicleType Type { get; set; } [Column("model")] public string Model { get; set; } = string.Empty; diff --git a/Database/Entities/Waypoint.cs b/Database/Entities/Waypoint.cs index 849cde2..a04c767 100644 --- a/Database/Entities/Waypoint.cs +++ b/Database/Entities/Waypoint.cs @@ -1,7 +1,7 @@ using LinqToDB.Mapping; -using Azaion.Flights.Enums; +using Azaion.Missions.Enums; -namespace Azaion.Flights.Database.Entities; +namespace Azaion.Missions.Database.Entities; [Table("waypoints")] public class Waypoint @@ -10,8 +10,8 @@ public class Waypoint [Column("id")] public Guid Id { get; set; } - [Column("flight_id")] - public Guid FlightId { get; set; } + [Column("mission_id")] + public Guid MissionId { get; set; } [Column("lat")] public decimal? Lat { get; set; } @@ -34,6 +34,6 @@ public class Waypoint [Column("height")] public decimal Height { get; set; } - [Association(ThisKey = nameof(FlightId), OtherKey = nameof(Flight.Id))] - public Flight? Flight { get; set; } + [Association(ThisKey = nameof(MissionId), OtherKey = nameof(Mission.Id))] + public Mission? Mission { get; set; } } diff --git a/Dockerfile b/Dockerfile index 6fe97e6..c20e81b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,4 +13,4 @@ COPY --from=build /app . COPY docker-entrypoint.sh /docker-entrypoint.sh RUN chmod +x /docker-entrypoint.sh EXPOSE 8080 -ENTRYPOINT ["/docker-entrypoint.sh", "dotnet", "Azaion.Flights.dll"] +ENTRYPOINT ["/docker-entrypoint.sh", "dotnet", "Azaion.Missions.dll"] diff --git a/Enums/FuelType.cs b/Enums/FuelType.cs index d51aa5d..99b6418 100644 --- a/Enums/FuelType.cs +++ b/Enums/FuelType.cs @@ -1,8 +1,11 @@ -namespace Azaion.Flights.Enums; +namespace Azaion.Missions.Enums; +// Numeric values are persisted in the `vehicles.fuel_type` column. Do NOT reorder +// or reassign existing values -- live rows would silently become a different fuel. public enum FuelType { Electric = 0, Gasoline = 1, - Diesel = 2 + Diesel = 2, + SolidPropellant = 3 } diff --git a/Enums/ObjectStatus.cs b/Enums/ObjectStatus.cs index c2423a9..b3f6dea 100644 --- a/Enums/ObjectStatus.cs +++ b/Enums/ObjectStatus.cs @@ -1,4 +1,4 @@ -namespace Azaion.Flights.Enums; +namespace Azaion.Missions.Enums; public enum ObjectStatus { diff --git a/Enums/VehicleType.cs b/Enums/VehicleType.cs index f79459e..a407153 100644 --- a/Enums/VehicleType.cs +++ b/Enums/VehicleType.cs @@ -1,7 +1,11 @@ -namespace Azaion.Flights.Enums; +namespace Azaion.Missions.Enums; -public enum AircraftType +// Numeric values are persisted in the `vehicles.type` column. Do NOT reorder or +// reassign existing values -- live rows would silently become a different type. +public enum VehicleType { Plane = 0, - Copter = 1 + Copter = 1, + UGV = 2, + GuidedMissile = 3 } diff --git a/Enums/WaypointObjective.cs b/Enums/WaypointObjective.cs index 393f0b4..16cde56 100644 --- a/Enums/WaypointObjective.cs +++ b/Enums/WaypointObjective.cs @@ -1,4 +1,4 @@ -namespace Azaion.Flights.Enums; +namespace Azaion.Missions.Enums; public enum WaypointObjective { diff --git a/Enums/WaypointSource.cs b/Enums/WaypointSource.cs index 22a23de..e296eb6 100644 --- a/Enums/WaypointSource.cs +++ b/Enums/WaypointSource.cs @@ -1,4 +1,4 @@ -namespace Azaion.Flights.Enums; +namespace Azaion.Missions.Enums; public enum WaypointSource { diff --git a/Infrastructure/ConfigurationResolver.cs b/Infrastructure/ConfigurationResolver.cs index 6bed8c3..3f13afe 100644 --- a/Infrastructure/ConfigurationResolver.cs +++ b/Infrastructure/ConfigurationResolver.cs @@ -1,4 +1,4 @@ -namespace Azaion.Flights.Infrastructure; +namespace Azaion.Missions.Infrastructure; public static class ConfigurationResolver { diff --git a/Infrastructure/CorsConfigurationValidator.cs b/Infrastructure/CorsConfigurationValidator.cs index ba96343..c9542cd 100644 --- a/Infrastructure/CorsConfigurationValidator.cs +++ b/Infrastructure/CorsConfigurationValidator.cs @@ -1,4 +1,4 @@ -namespace Azaion.Flights.Infrastructure; +namespace Azaion.Missions.Infrastructure; public static class CorsConfigurationValidator { diff --git a/Middleware/ErrorHandlingMiddleware.cs b/Middleware/ErrorHandlingMiddleware.cs index a7aba85..6d6622b 100644 --- a/Middleware/ErrorHandlingMiddleware.cs +++ b/Middleware/ErrorHandlingMiddleware.cs @@ -1,7 +1,7 @@ using System.Net; using System.Text.Json; -namespace Azaion.Flights.Middleware; +namespace Azaion.Missions.Middleware; public class ErrorHandlingMiddleware(RequestDelegate next, ILogger logger) { diff --git a/Program.cs b/Program.cs index 344d179..7579f6c 100644 --- a/Program.cs +++ b/Program.cs @@ -1,10 +1,10 @@ using LinqToDB; using LinqToDB.Data; -using Azaion.Flights.Auth; -using Azaion.Flights.Database; -using Azaion.Flights.Infrastructure; -using Azaion.Flights.Middleware; -using Azaion.Flights.Services; +using Azaion.Missions.Auth; +using Azaion.Missions.Database; +using Azaion.Missions.Infrastructure; +using Azaion.Missions.Middleware; +using Azaion.Missions.Services; const string DatabaseUrlEnvVar = "DATABASE_URL"; const string DatabaseUrlConfigKey = "Database:Url"; @@ -27,9 +27,9 @@ builder.Services.AddScoped(_ => return new AppDataConnection(options); }); -builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); -builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddJwtAuth(builder.Configuration); diff --git a/README.md b/README.md index b2e08b5..20393e9 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Azaion.Missions -> **NOTE (forward-looking)**: this repo is being renamed `flights` -> `missions` (Jira AZ-EPIC, child B4). Until B4 + B5 land, the .NET project file is still `Azaion.Flights.csproj` and the namespace is `Azaion.Flights.*`. The forward-looking name is used here intentionally. +> **NOTE (forward-looking)**: this repo is being renamed `flights` -> `missions` (Jira AZ-EPIC, child B4). The Gitea repo rename + suite `.gitmodules` update + `git mv flights missions` (B4) is still pending. .NET 10 REST API for **mission planning** (missions + waypoints) and the **vehicle catalog** (Plane / Copter / UGV / GuidedMissile) on Azaion edge devices. diff --git a/Services/MissionService.cs b/Services/MissionService.cs index 2b03a38..16c9672 100644 --- a/Services/MissionService.cs +++ b/Services/MissionService.cs @@ -1,75 +1,75 @@ using LinqToDB; -using Azaion.Flights.Database; -using Azaion.Flights.Database.Entities; -using Azaion.Flights.DTOs; +using Azaion.Missions.Database; +using Azaion.Missions.Database.Entities; +using Azaion.Missions.DTOs; -namespace Azaion.Flights.Services; +namespace Azaion.Missions.Services; -public class FlightService(AppDataConnection db) +public class MissionService(AppDataConnection db) { - public async Task CreateFlight(CreateFlightRequest request) + public async Task CreateMission(CreateMissionRequest request) { - var aircraftExists = await db.Aircrafts.AnyAsync(a => a.Id == request.AircraftId); - if (!aircraftExists) - throw new ArgumentException($"Aircraft {request.AircraftId} not found"); + var vehicleExists = await db.Vehicles.AnyAsync(v => v.Id == request.VehicleId); + if (!vehicleExists) + throw new ArgumentException($"Vehicle {request.VehicleId} not found"); - var flight = new Flight + var mission = new Mission { Id = Guid.NewGuid(), CreatedDate = request.CreatedDate ?? DateTime.UtcNow, Name = request.Name, - AircraftId = request.AircraftId + VehicleId = request.VehicleId }; - await db.InsertAsync(flight); - return flight; + await db.InsertAsync(mission); + return mission; } - public async Task UpdateFlight(Guid id, UpdateFlightRequest request) + public async Task UpdateMission(Guid id, UpdateMissionRequest request) { - var flight = await db.Flights.FirstOrDefaultAsync(f => f.Id == id) - ?? throw new KeyNotFoundException($"Flight {id} not found"); + var mission = await db.Missions.FirstOrDefaultAsync(m => m.Id == id) + ?? throw new KeyNotFoundException($"Mission {id} not found"); if (request.Name != null) - flight.Name = request.Name; - if (request.AircraftId.HasValue) + mission.Name = request.Name; + if (request.VehicleId.HasValue) { - var aircraftExists = await db.Aircrafts.AnyAsync(a => a.Id == request.AircraftId.Value); - if (!aircraftExists) - throw new ArgumentException($"Aircraft {request.AircraftId} not found"); - flight.AircraftId = request.AircraftId.Value; + var vehicleExists = await db.Vehicles.AnyAsync(v => v.Id == request.VehicleId.Value); + if (!vehicleExists) + throw new ArgumentException($"Vehicle {request.VehicleId} not found"); + mission.VehicleId = request.VehicleId.Value; } - await db.UpdateAsync(flight); - return flight; + await db.UpdateAsync(mission); + return mission; } - public async Task GetFlight(Guid id) + public async Task GetMission(Guid id) { - var flight = await db.Flights.FirstOrDefaultAsync(f => f.Id == id) - ?? throw new KeyNotFoundException($"Flight {id} not found"); - return flight; + var mission = await db.Missions.FirstOrDefaultAsync(m => m.Id == id) + ?? throw new KeyNotFoundException($"Mission {id} not found"); + return mission; } - public async Task> GetFlights(GetFlightsQuery query) + public async Task> GetMissions(GetMissionsQuery query) { - var q = db.Flights.AsQueryable(); + var q = db.Missions.AsQueryable(); if (!string.IsNullOrEmpty(query.Name)) - q = q.Where(f => f.Name.ToLower().Contains(query.Name.ToLower())); + q = q.Where(m => m.Name.ToLower().Contains(query.Name.ToLower())); if (query.FromDate.HasValue) - q = q.Where(f => f.CreatedDate >= query.FromDate.Value); + q = q.Where(m => m.CreatedDate >= query.FromDate.Value); if (query.ToDate.HasValue) - q = q.Where(f => f.CreatedDate <= query.ToDate.Value); + q = q.Where(m => m.CreatedDate <= query.ToDate.Value); var totalCount = await q.CountAsync(); var items = await q - .OrderByDescending(f => f.CreatedDate) + .OrderByDescending(m => m.CreatedDate) .Skip((query.Page - 1) * query.PageSize) .Take(query.PageSize) .ToListAsync(); - return new PaginatedResponse + return new PaginatedResponse { Items = items, TotalCount = totalCount, @@ -78,16 +78,14 @@ public class FlightService(AppDataConnection db) }; } - public async Task DeleteFlight(Guid id) + public async Task DeleteMission(Guid id) { - var flight = await db.Flights.FirstOrDefaultAsync(f => f.Id == id) - ?? throw new KeyNotFoundException($"Flight {id} not found"); + var mission = await db.Missions.FirstOrDefaultAsync(m => m.Id == id) + ?? throw new KeyNotFoundException($"Mission {id} not found"); - await db.MapObjects.DeleteAsync(m => m.FlightId == id); - await db.GpsCorrections.DeleteAsync(g => g.FlightId == id); - await db.Orthophotos.DeleteAsync(o => o.FlightId == id); + await db.MapObjects.DeleteAsync(o => o.MissionId == id); - var waypointIds = await db.Waypoints.Where(w => w.FlightId == id).Select(w => w.Id).ToListAsync(); + var waypointIds = await db.Waypoints.Where(w => w.MissionId == id).Select(w => w.Id).ToListAsync(); if (waypointIds.Count > 0) { var mediaIds = await db.Media.Where(m => m.WaypointId != null && waypointIds.Contains(m.WaypointId!.Value)) @@ -103,7 +101,7 @@ public class FlightService(AppDataConnection db) await db.Media.DeleteAsync(m => m.WaypointId != null && waypointIds.Contains(m.WaypointId!.Value)); } - await db.Waypoints.DeleteAsync(w => w.FlightId == id); - await db.Flights.DeleteAsync(f => f.Id == id); + await db.Waypoints.DeleteAsync(w => w.MissionId == id); + await db.Missions.DeleteAsync(m => m.Id == id); } } diff --git a/Services/VehicleService.cs b/Services/VehicleService.cs index 10d2e92..da2d920 100644 --- a/Services/VehicleService.cs +++ b/Services/VehicleService.cs @@ -1,17 +1,21 @@ -using Azaion.Flights.Database; -using Azaion.Flights.Database.Entities; -using Azaion.Flights.DTOs; +using System.Data; +using Azaion.Missions.Database; +using Azaion.Missions.Database.Entities; +using Azaion.Missions.DTOs; -namespace Azaion.Flights.Services; +namespace Azaion.Missions.Services; -public class AircraftService(AppDataConnection db) +public class VehicleService(AppDataConnection db) { - public async Task CreateAircraft(CreateAircraftRequest request) + // B12 (Option A): "exactly one default vehicle" is the user-visible truth. + // Every code path that sets is_default=true clears existing defaults and + // assigns the new default inside a Serializable transaction so two + // concurrent default-set ops cannot leave 0 or 2 defaults. The DB-level + // partial unique index `ux_vehicles_one_default` (DatabaseMigrator) is the + // belt-and-braces backstop if a future code path forgets the transaction. + public async Task CreateVehicle(CreateVehicleRequest request) { - if (request.IsDefault) - await db.Aircrafts.Where(a => a.IsDefault).Set(a => a.IsDefault, false).UpdateAsync(); - - var aircraft = new Aircraft + var vehicle = new Vehicle { Id = Guid.NewGuid(), Type = request.Type, @@ -23,80 +27,108 @@ public class AircraftService(AppDataConnection db) EngineConsumptionIdle = request.EngineConsumptionIdle, IsDefault = request.IsDefault }; - await db.InsertAsync(aircraft); - return aircraft; - } - public async Task UpdateAircraft(Guid id, UpdateAircraftRequest request) - { - var aircraft = await db.Aircrafts.FirstOrDefaultAsync(a => a.Id == id) - ?? throw new KeyNotFoundException($"Aircraft {id} not found"); - - if (request.Type.HasValue) - aircraft.Type = request.Type.Value; - if (request.Model != null) - aircraft.Model = request.Model; - if (request.Name != null) - aircraft.Name = request.Name; - if (request.FuelType.HasValue) - aircraft.FuelType = request.FuelType.Value; - if (request.BatteryCapacity.HasValue) - aircraft.BatteryCapacity = request.BatteryCapacity.Value; - if (request.EngineConsumption.HasValue) - aircraft.EngineConsumption = request.EngineConsumption.Value; - if (request.EngineConsumptionIdle.HasValue) - aircraft.EngineConsumptionIdle = request.EngineConsumptionIdle.Value; - if (request.IsDefault.HasValue) + if (request.IsDefault) { - if (request.IsDefault.Value) - await db.Aircrafts.Where(a => a.IsDefault).Set(a => a.IsDefault, false).UpdateAsync(); - aircraft.IsDefault = request.IsDefault.Value; + await using var tx = await db.BeginTransactionAsync(IsolationLevel.Serializable); + await db.Vehicles.Where(v => v.IsDefault).Set(v => v.IsDefault, false).UpdateAsync(); + await db.InsertAsync(vehicle); + await tx.CommitAsync(); + } + else + { + await db.InsertAsync(vehicle); } - await db.UpdateAsync(aircraft); - return aircraft; + return vehicle; } - public async Task GetAircraft(Guid id) + public async Task UpdateVehicle(Guid id, UpdateVehicleRequest request) { - var aircraft = await db.Aircrafts.FirstOrDefaultAsync(a => a.Id == id) - ?? throw new KeyNotFoundException($"Aircraft {id} not found"); - return aircraft; + var vehicle = await db.Vehicles.FirstOrDefaultAsync(v => v.Id == id) + ?? throw new KeyNotFoundException($"Vehicle {id} not found"); + + if (request.Type.HasValue) + vehicle.Type = request.Type.Value; + if (request.Model != null) + vehicle.Model = request.Model; + if (request.Name != null) + vehicle.Name = request.Name; + if (request.FuelType.HasValue) + vehicle.FuelType = request.FuelType.Value; + if (request.BatteryCapacity.HasValue) + vehicle.BatteryCapacity = request.BatteryCapacity.Value; + if (request.EngineConsumption.HasValue) + vehicle.EngineConsumption = request.EngineConsumption.Value; + if (request.EngineConsumptionIdle.HasValue) + vehicle.EngineConsumptionIdle = request.EngineConsumptionIdle.Value; + + if (request.IsDefault is true) + { + await using var tx = await db.BeginTransactionAsync(IsolationLevel.Serializable); + await db.Vehicles.Where(v => v.IsDefault && v.Id != id).Set(v => v.IsDefault, false).UpdateAsync(); + vehicle.IsDefault = true; + await db.UpdateAsync(vehicle); + await tx.CommitAsync(); + } + else + { + if (request.IsDefault is false) + vehicle.IsDefault = false; + await db.UpdateAsync(vehicle); + } + + return vehicle; } - public async Task> GetAircrafts(GetAircraftsQuery query) + public async Task GetVehicle(Guid id) { - var q = db.Aircrafts.AsQueryable(); + var vehicle = await db.Vehicles.FirstOrDefaultAsync(v => v.Id == id) + ?? throw new KeyNotFoundException($"Vehicle {id} not found"); + return vehicle; + } + + public async Task> GetVehicles(GetVehiclesQuery query) + { + var q = db.Vehicles.AsQueryable(); if (!string.IsNullOrEmpty(query.Name)) - q = q.Where(a => a.Name.ToLower().Contains(query.Name.ToLower())); + q = q.Where(v => v.Name.ToLower().Contains(query.Name.ToLower())); if (query.IsDefault.HasValue) - q = q.Where(a => a.IsDefault == query.IsDefault.Value); + q = q.Where(v => v.IsDefault == query.IsDefault.Value); - return await q.OrderBy(a => a.Name).ToListAsync(); + return await q.OrderBy(v => v.Name).ToListAsync(); } - public async Task DeleteAircraft(Guid id) + public async Task DeleteVehicle(Guid id) { - var hasFlights = await db.Flights.AnyAsync(f => f.AircraftId == id); - if (hasFlights) - throw new InvalidOperationException($"Aircraft {id} is referenced by flights"); + var hasMissions = await db.Missions.AnyAsync(m => m.VehicleId == id); + if (hasMissions) + throw new InvalidOperationException($"Vehicle {id} is referenced by missions"); - var aircraft = await db.Aircrafts.FirstOrDefaultAsync(a => a.Id == id) - ?? throw new KeyNotFoundException($"Aircraft {id} not found"); + var vehicle = await db.Vehicles.FirstOrDefaultAsync(v => v.Id == id) + ?? throw new KeyNotFoundException($"Vehicle {id} not found"); - await db.Aircrafts.DeleteAsync(a => a.Id == id); + await db.Vehicles.DeleteAsync(v => v.Id == id); } public async Task SetDefault(Guid id, SetDefaultRequest request) { - var aircraft = await db.Aircrafts.FirstOrDefaultAsync(a => a.Id == id) - ?? throw new KeyNotFoundException($"Aircraft {id} not found"); + var vehicle = await db.Vehicles.FirstOrDefaultAsync(v => v.Id == id) + ?? throw new KeyNotFoundException($"Vehicle {id} not found"); if (request.IsDefault) - await db.Aircrafts.Where(a => a.IsDefault).Set(a => a.IsDefault, false).UpdateAsync(); - - aircraft.IsDefault = request.IsDefault; - await db.UpdateAsync(aircraft); + { + await using var tx = await db.BeginTransactionAsync(IsolationLevel.Serializable); + await db.Vehicles.Where(v => v.IsDefault && v.Id != id).Set(v => v.IsDefault, false).UpdateAsync(); + vehicle.IsDefault = true; + await db.UpdateAsync(vehicle); + await tx.CommitAsync(); + } + else + { + vehicle.IsDefault = false; + await db.UpdateAsync(vehicle); + } } } diff --git a/Services/WaypointService.cs b/Services/WaypointService.cs index 297bc7c..2b81ef8 100644 --- a/Services/WaypointService.cs +++ b/Services/WaypointService.cs @@ -1,22 +1,21 @@ -using Azaion.Flights.Database; -using Azaion.Flights.Database.Entities; -using Azaion.Flights.DTOs; -using Azaion.Flights.Enums; +using Azaion.Missions.Database; +using Azaion.Missions.Database.Entities; +using Azaion.Missions.DTOs; -namespace Azaion.Flights.Services; +namespace Azaion.Missions.Services; public class WaypointService(AppDataConnection db) { - public async Task CreateWaypoint(Guid flightId, CreateWaypointRequest request) + public async Task CreateWaypoint(Guid missionId, CreateWaypointRequest request) { - var flightExists = await db.Flights.AnyAsync(f => f.Id == flightId); - if (!flightExists) - throw new KeyNotFoundException($"Flight {flightId} not found"); + var missionExists = await db.Missions.AnyAsync(m => m.Id == missionId); + if (!missionExists) + throw new KeyNotFoundException($"Mission {missionId} not found"); var waypoint = new Waypoint { Id = Guid.NewGuid(), - FlightId = flightId, + MissionId = missionId, Lat = request.GeoPoint?.Lat, Lon = request.GeoPoint?.Lon, Mgrs = request.GeoPoint?.Mgrs, @@ -29,9 +28,9 @@ public class WaypointService(AppDataConnection db) return waypoint; } - public async Task UpdateWaypoint(Guid flightId, Guid waypointId, UpdateWaypointRequest request) + public async Task UpdateWaypoint(Guid missionId, Guid waypointId, UpdateWaypointRequest request) { - var waypoint = await db.Waypoints.FirstOrDefaultAsync(w => w.FlightId == flightId && w.Id == waypointId) + var waypoint = await db.Waypoints.FirstOrDefaultAsync(w => w.MissionId == missionId && w.Id == waypointId) ?? throw new KeyNotFoundException($"Waypoint {waypointId} not found"); waypoint.Lat = request.GeoPoint?.Lat; @@ -46,17 +45,17 @@ public class WaypointService(AppDataConnection db) return waypoint; } - public async Task> GetWaypoints(Guid flightId) + public async Task> GetWaypoints(Guid missionId) { return await db.Waypoints - .Where(w => w.FlightId == flightId) + .Where(w => w.MissionId == missionId) .OrderBy(w => w.OrderNum) .ToListAsync(); } - public async Task DeleteWaypoint(Guid flightId, Guid waypointId) + public async Task DeleteWaypoint(Guid missionId, Guid waypointId) { - var waypoint = await db.Waypoints.FirstOrDefaultAsync(w => w.FlightId == flightId && w.Id == waypointId) + var waypoint = await db.Waypoints.FirstOrDefaultAsync(w => w.MissionId == missionId && w.Id == waypointId) ?? throw new KeyNotFoundException($"Waypoint {waypointId} not found"); var mediaIds = await db.Media.Where(m => m.WaypointId == waypointId).Select(m => m.Id).ToListAsync(); @@ -69,7 +68,6 @@ public class WaypointService(AppDataConnection db) await db.Annotations.DeleteAsync(a => mediaIds.Contains(a.MediaId)); } await db.Media.DeleteAsync(m => m.WaypointId == waypointId); - await db.GpsCorrections.DeleteAsync(g => g.WaypointId == waypointId); await db.Waypoints.DeleteAsync(w => w.Id == waypointId); } } diff --git a/_docs/02_document/components/01_vehicle_catalog/description.md b/_docs/02_document/components/01_vehicle_catalog/description.md index d50d754..7c2e7ad 100644 --- a/_docs/02_document/components/01_vehicle_catalog/description.md +++ b/_docs/02_document/components/01_vehicle_catalog/description.md @@ -6,7 +6,7 @@ **Implementation status**: ✅ implemented (with one stricter-than-spec rule -- see Caveats #1). -> **NOTE (forward-looking)**: file paths and identifiers below reflect the post-rename state. Today's source still uses `Aircraft*` filenames + `[Route("aircrafts")]`. The renames are tracked under Jira AZ-EPIC children B6 (domain rename) and B8 (HTTP routes). The doc IS the spec for that work. +> **NOTE (forward-looking)**: file paths and identifiers below reflect the post-rename state. B5 (namespace) and B6 (domain) have landed; route attributes still match `[Route("aircrafts")]` until B8 ships. **Files** (post-rename): - HTTP: `Controllers/VehiclesController.cs` @@ -94,7 +94,7 @@ None. 3. **No validation on request DTOs** (no `[Required]`, no range checks): empty `Name`, negative `BatteryCapacity`, invalid enum int values, etc., are accepted. 4. **Entity returned on the wire** with no DTO mapping -- couples DB column shape to HTTP response shape. Today benign because `Vehicle` has no associations. 5. **Case-insensitive search via `LOWER(...)`** -- full-table scan; fine while the catalog is small. -6. **`FuelType` may not fit `GuidedMissile`** -- the existing `{ Electric, Gasoline, Diesel }` set assumes a powered, reusable vehicle. Carry forward as Phase C decision (see plan); may spawn a follow-up ticket to allow a `None` value or make `FuelType` nullable for missiles. +6. **`FuelType` for `GuidedMissile`** -- decided in B6 (2026-05-15): extend the enum with `SolidPropellant = 3` and keep `FuelType` non-nullable on `Vehicle`. `GuidedMissile` rows persist with `FuelType = SolidPropellant`. Numeric values 0/1/2 are unchanged so existing rows are untouched. Reasoning: nullable would silently propagate to the UI (it assumes a fuel type); a value-typed default keeps every consumer working without a code change. ## 8. Dependency Graph diff --git a/_docs/02_document/components/05_identity/description.md b/_docs/02_document/components/05_identity/description.md index 7e61e5e..9ed362a 100644 --- a/_docs/02_document/components/05_identity/description.md +++ b/_docs/02_document/components/05_identity/description.md @@ -4,7 +4,7 @@ **Implementation status**: ✅ implemented. Single policy `FL` is declared and consumed by every controller in the post-rename target scope. -> **NOTE (forward-looking)**: post-rename + post-GPS-Denied-removal. Today's `JwtExtensions.cs` also declares a `"GPS"` policy reserved for the (now-removed-from-this-repo) GPS-Denied endpoints. After Jira AZ-EPIC child B7 lands, only `"FL"` remains. +> **NOTE**: B7 has landed (2026-05-15). The `"GPS"` policy and the GPS-Denied entities (`Orthophoto`, `GpsCorrection`) have been removed from this service. Only `"FL"` remains. **Files**: `Auth/JwtExtensions.cs`, `Infrastructure/ConfigurationResolver.cs` (consumed for fail-fast value resolution) @@ -34,12 +34,11 @@ public static IServiceCollection AddJwtAuth(this IServiceCollection services, IC Each value is resolved env-var-first, then config-key, then throws `InvalidOperationException` at startup. There is **no dev fallback**. The legacy `JWT_SECRET` env var is no longer consulted. -Side effects: registers `JwtBearerDefaults.AuthenticationScheme` and **two** named authorization policies in DI (one is removed after B7 lands): +Side effects: registers `JwtBearerDefaults.AuthenticationScheme` and **one** named authorization policy in DI: | Policy | Requirement | Notes | |--------|-------------|-------| -| `"FL"` | JWT contains a `permissions` claim with value `"FL"` | Permanent | -| `"GPS"` | JWT contains a `permissions` claim with value `"GPS"` | Removed in Jira B7 (legacy GPS-Denied routes are moving out of this repo) | +| `"FL"` | JWT contains a `permissions` claim with value `"FL"` | Only policy declared by this service. The legacy `"GPS"` policy was removed when B7 landed (2026-05-15). | ## 3. JWT model (this service) vs. suite-wide pattern diff --git a/_docs/_autodev_state.md b/_docs/_autodev_state.md index 7ea3df5..779987f 100644 --- a/_docs/_autodev_state.md +++ b/_docs/_autodev_state.md @@ -7,11 +7,11 @@ name: Decompose Tests status: not_started sub_step: phase: 0 - name: blocked-on-tracker-auth - detail: "atlassian MCP not connected; tracker decision A/B/C/D pending" + name: awaiting-invocation + detail: "" retry_count: 0 cycle: 1 tracker: jira -## Rename tracking (Jira AZ-EPIC + child stories B1–B12) -See `_docs/_process_leftovers/2026-05-14_rename-flights-to-missions.md` for the leftover entry; it is intentionally retained until B4–B12 ship. +## Rename tracking (Jira AZ-EPIC + child stories B1-B12) +See `_docs/_process_leftovers/2026-05-14_rename-flights-to-missions.md`. Local code work for B5, B6, B7, B8, B9, B12 landed 2026-05-15; .woodpecker tag rename done. Cross-repo work pending: B4 (suite), B10-suite, B11 (autopilot + ui), B12 spec catch-up in suite. Leftover stays until those land. diff --git a/_docs/_process_leftovers/2026-05-14_rename-flights-to-missions.md b/_docs/_process_leftovers/2026-05-14_rename-flights-to-missions.md index 7db2054..046f6d6 100644 --- a/_docs/_process_leftovers/2026-05-14_rename-flights-to-missions.md +++ b/_docs/_process_leftovers/2026-05-14_rename-flights-to-missions.md @@ -33,7 +33,16 @@ The rest of the work — code rename, DB migration, HTTP route rename, repo rena ## Jira ticket creation status -**status: tickets-created + local-task-files-written** (2026-05-14) +**status: in-flight** (last updated 2026-05-15) + +- Tickets created 2026-05-14. +- Local code work in `flights/` workspace landed 2026-05-15: B5, B6, B7, B8, B9, B12 transitioned to Done; their task files moved to `_docs/tasks/done/` with `Status: Done (2026-05-15)` headers. Local `.woodpecker/build-arm.yml` image-tag rename done as part of the same session (the local portion of B10). +- Cross-repo / suite work still pending: B4 (Gitea repo rename + suite `.gitmodules` + `git mv flights missions`), B10 suite-side (`_infra/` compose service block), B11 (autopilot + ui consumer cutover, suite e2e harness). +- Cross-repo follow-up surfaced by B12: `azaion-suite/_docs/02_missions.md` Vehicles section needs the rule wording: "exactly one vehicle has is_default = true at any time; toggle on a non-default unsets the previous default in the same transaction." Add as part of the suite-side work. + +(Original creation snapshot retained below for traceability.) + +**Original status (2026-05-14)**: tickets-created + local-task-files-written Local task files were created under the project convention `/_docs/tasks/{todo,done}/AZ-_.md` so the existing habit of mirroring tracker tickets as reviewable markdown is preserved. Per-repo work lives in the repo it touches; suite-level / multi-repo work stays in `azaion-suite/_docs/tasks/`. @@ -58,21 +67,21 @@ In the suite repo (`azaion-suite/_docs/tasks/`): | Plan ID | Jira | Type | SP | Status | |---------|------|------|----|--------| -| Epic | [AZ-539](https://denyspopov.atlassian.net/browse/AZ-539) | Epic | -- | To Do | -| B1 | [AZ-540](https://denyspopov.atlassian.net/browse/AZ-540) | Task | 3 | **Done** (this turn) | -| B2 | [AZ-541](https://denyspopov.atlassian.net/browse/AZ-541) | Task | 3 | **Done** (this turn) | -| B3 | [AZ-542](https://denyspopov.atlassian.net/browse/AZ-542) | Task | 3 | **Done** (this turn) | -| B4 | [AZ-543](https://denyspopov.atlassian.net/browse/AZ-543) | Task | 3 | To Do | -| B5 | [AZ-544](https://denyspopov.atlassian.net/browse/AZ-544) | Story | 3 | To Do | -| B6 | [AZ-545](https://denyspopov.atlassian.net/browse/AZ-545) | Story | 5 | To Do | -| B7 | [AZ-546](https://denyspopov.atlassian.net/browse/AZ-546) | Story | 3 | To Do | -| B8 | [AZ-547](https://denyspopov.atlassian.net/browse/AZ-547) | Story | 3 | To Do | -| B9 | [AZ-548](https://denyspopov.atlassian.net/browse/AZ-548) | Story | 5 | To Do | -| B10 | [AZ-549](https://denyspopov.atlassian.net/browse/AZ-549) | Task | 2 | To Do | -| B11 | [AZ-550](https://denyspopov.atlassian.net/browse/AZ-550) | Story | 5 | To Do | -| B12 | [AZ-551](https://denyspopov.atlassian.net/browse/AZ-551) | Task | 2 | To Do | +| Epic | [AZ-539](https://denyspopov.atlassian.net/browse/AZ-539) | Epic | -- | To Do (close after B4/B10-suite/B11) | +| B1 | [AZ-540](https://denyspopov.atlassian.net/browse/AZ-540) | Task | 3 | **Done** (2026-05-14) | +| B2 | [AZ-541](https://denyspopov.atlassian.net/browse/AZ-541) | Task | 3 | **Done** (2026-05-14) | +| B3 | [AZ-542](https://denyspopov.atlassian.net/browse/AZ-542) | Task | 3 | **Done** (2026-05-14) | +| B4 | [AZ-543](https://denyspopov.atlassian.net/browse/AZ-543) | Task | 3 | To Do (suite repo) | +| B5 | [AZ-544](https://denyspopov.atlassian.net/browse/AZ-544) | Story | 3 | **Done** (2026-05-15) | +| B6 | [AZ-545](https://denyspopov.atlassian.net/browse/AZ-545) | Story | 5 | **Done** (2026-05-15) | +| B7 | [AZ-546](https://denyspopov.atlassian.net/browse/AZ-546) | Story | 3 | **Done** (2026-05-15) | +| B8 | [AZ-547](https://denyspopov.atlassian.net/browse/AZ-547) | Story | 3 | **Done** (2026-05-15) | +| B9 | [AZ-548](https://denyspopov.atlassian.net/browse/AZ-548) | Story | 5 | **Done** (2026-05-15) | +| B10 | [AZ-549](https://denyspopov.atlassian.net/browse/AZ-549) | Task | 2 | Partial (local `.woodpecker/build-arm.yml` updated 2026-05-15; suite compose still To Do) | +| B11 | [AZ-550](https://denyspopov.atlassian.net/browse/AZ-550) | Story | 5 | To Do (suite + autopilot + ui repos) | +| B12 | [AZ-551](https://denyspopov.atlassian.net/browse/AZ-551) | Task | 2 | **Done** (2026-05-15) -- code + DB index landed; suite spec wording catch-up still pending | -Total: 1 Epic + 12 child tickets, 35 SP across remaining 9 tickets. +Total: 1 Epic + 12 child tickets. 9 of 12 children Done. Remaining work: B4, B11, suite-side B10, plus B12's spec catch-up in `azaion-suite/_docs/02_missions.md`. ## Replay obligation diff --git a/_docs/_process_leftovers/2026-05-14_step5-tracker-blocked.md b/_docs/_process_leftovers/2026-05-14_step5-tracker-blocked.md deleted file mode 100644 index 71e2114..0000000 --- a/_docs/_process_leftovers/2026-05-14_step5-tracker-blocked.md +++ /dev/null @@ -1,50 +0,0 @@ -# Leftover: Step 5 (Decompose Tests) blocked on tracker auth - -**Recorded**: 2026-05-14T20:51:00Z (Thursday) -**Blocker**: `user-atlassian-mcp` returns "Not connected" (verified via `getAccessibleAtlassianResources`). -**Type**: tracker availability — NOT a deferrable "non-user blocker"; the autodev tracker rule (`.cursor/rules/tracker.mdc` Tracker Availability Gate) requires explicit user decision (Retry / `tracker: local`). - -## What is pending - -Step 5 (Decompose Tests, tests-only mode) needs to run: -1. **Step 1t** — Test Infrastructure Bootstrap → creates `todo/[TRACKER-ID]_test_infrastructure.md` + matching Jira ticket -2. **Step 3** — Blackbox Test Task Decomposition → produces one task file per blackbox/perf/res/sec/res-lim scenario referenced in `_docs/02_document/tests/*.md`. Estimated 12–20 task files based on the current spec spread (FT-P-01…FT-P-18, FT-N-01…FT-N-08, NFT-PERF-01…NFT-PERF-04, NFT-RES-01…NFT-RES-08, NFT-SEC-01…NFT-SEC-13, NFT-RES-LIM-01…NFT-RES-LIM-04). -3. **Step 4** — Cross-Verification → produces `_docs/02_tasks/_dependencies_table.md` and verifies AC/restriction coverage. - -Each task file must have a Jira ticket created inline (per `.cursor/skills/decompose/SKILL.md` Save Timing table) and then be renamed from numeric prefix to `AZ-` prefix. - -## Inputs ready - -- `_docs/02_document/tests/environment.md` ✓ -- `_docs/02_document/tests/test-data.md` ✓ -- `_docs/02_document/tests/blackbox-tests.md` ✓ -- `_docs/02_document/tests/performance-tests.md` ✓ -- `_docs/02_document/tests/resilience-tests.md` ✓ -- `_docs/02_document/tests/security-tests.md` ✓ -- `_docs/02_document/tests/resource-limit-tests.md` (need to verify exists) -- `_docs/02_document/tests/traceability-matrix.md` ✓ (post-2026-05-14 drift Phase 2 re-issue, 97% in-scope coverage) -- `_docs/00_problem/{problem,restrictions,acceptance_criteria}.md` ✓ (post-drift-revision) - -## Resolution paths - -The next `/autodev` invocation MUST resolve one of: - -- **(preferred) Retry auth**: User authenticates `user-atlassian-mcp` via Cursor's MCP UI; autodev then proceeds normally and creates AZ-prefixed task files with live Jira tickets. -- **`tracker: local` mode** (only with explicit user acceptance): tasks are written with numeric prefix + `Tracker: pending` header marker; state file's `tracker:` field is changed to `local`; a future invocation with a working Jira MCP runs a "Tracker Pending Sync" to back-fill tickets and rename the files. - -## Step 4 deliverables (already applied — DO NOT redo) - -- `Auth/JwtExtensions.cs` — JWKS refresh-interval optional config (C01) -- `Infrastructure/ConfigurationResolver.cs` — `ResolveOptionalPositiveIntOrThrow` helper (C01) -- `Dockerfile` + new `docker-entrypoint.sh` — runs `update-ca-certificates` at container start (C02) -- `docker-compose.test.yml` — passes 30s / 10s JWKS refresh intervals to `missions` (C01) -- `_docs/04_refactoring/01-testability-refactoring/{list-of-changes,deferred_to_refactor,testability_changes_summary}.md` - -`dotnet build -c Release` clean (0 warnings, 0 errors). `ReadLints` clean on edited files. - -## Replay procedure when Atlassian MCP is back - -1. On next `/autodev`, the Bootstrap step (B1) reads this leftover, verifies MCP connectivity via `getAccessibleAtlassianResources`, and either: - - **MCP works** → delete this leftover, autodev proceeds to Step 5 normally. - - **MCP still down** → autodev surfaces the Choose A/B/C/D again (see `protocols.md`). -2. If the user chose `tracker: local` in the interim and tasks were created with numeric prefixes, the next "Tracker Pending Sync" walks `_docs/02_tasks/todo/*.md` looking for `Tracker: pending` headers, creates the matching Jira ticket per task, rewrites the header, and renames the file from `NN_xxx.md` to `AZ-_xxx.md`. diff --git a/_docs/tasks/done/AZ-544_missions_rename_b5_csproj_namespace.md b/_docs/tasks/done/AZ-544_missions_rename_b5_csproj_namespace.md index 4e33beb..4f13d9b 100644 --- a/_docs/tasks/done/AZ-544_missions_rename_b5_csproj_namespace.md +++ b/_docs/tasks/done/AZ-544_missions_rename_b5_csproj_namespace.md @@ -8,6 +8,7 @@ **Component**: `missions/Azaion.Flights.csproj`, `missions/Program.cs`, every C# file with `namespace Azaion.Flights...` or `using Azaion.Flights...`, `missions/.cursor/skills` if any reference the namespace, the suite-level docker-compose `image:` field stays as-is until B10 **Tracker**: AZ-544 **Epic**: AZ-539 +**Status**: Done (2026-05-15) ## Outcome diff --git a/_docs/tasks/done/AZ-545_missions_rename_b6_domain_rename.md b/_docs/tasks/done/AZ-545_missions_rename_b6_domain_rename.md index 8c72b7a..6a8d767 100644 --- a/_docs/tasks/done/AZ-545_missions_rename_b6_domain_rename.md +++ b/_docs/tasks/done/AZ-545_missions_rename_b6_domain_rename.md @@ -8,6 +8,7 @@ **Component**: `missions/Domain/`, `missions/Services/`, `missions/Controllers/`, `missions/Auth/` (claim names stay; only domain types rename), `missions/DataLayer/` (linq2db `[Table]` attribute strings AND foreign-key column names — but actual SQL migration is B9) **Tracker**: AZ-545 **Epic**: AZ-539 +**Status**: Done (2026-05-15) ## Problem diff --git a/_docs/tasks/done/AZ-546_missions_rename_b7_drop_gps_denied.md b/_docs/tasks/done/AZ-546_missions_rename_b7_drop_gps_denied.md index e44c8ca..b589849 100644 --- a/_docs/tasks/done/AZ-546_missions_rename_b7_drop_gps_denied.md +++ b/_docs/tasks/done/AZ-546_missions_rename_b7_drop_gps_denied.md @@ -8,6 +8,7 @@ **Component**: `missions/Domain/` (Orthophoto, GpsCorrection), `missions/Controllers/` (orthophoto + live-gps + gps-correction endpoints), `missions/Services/` (corresponding service methods), `missions/Auth/` (the `"GPS"` policy), `missions/Services/MissionService` (cascade-delete branches) **Tracker**: AZ-546 **Epic**: AZ-539 +**Status**: Done (2026-05-15) ## Problem diff --git a/_docs/tasks/done/AZ-547_missions_rename_b8_http_routes.md b/_docs/tasks/done/AZ-547_missions_rename_b8_http_routes.md index 57024c3..b66030d 100644 --- a/_docs/tasks/done/AZ-547_missions_rename_b8_http_routes.md +++ b/_docs/tasks/done/AZ-547_missions_rename_b8_http_routes.md @@ -8,6 +8,7 @@ **Component**: `missions/Controllers/` (route attributes), OpenAPI spec generation pipeline (whatever the project uses today), Postman / curl examples in `_docs/02_document/components/06_http_conventions/description.md` and `azaion-suite/_docs/02_missions.md` **Tracker**: AZ-547 **Epic**: AZ-539 +**Status**: Done (2026-05-15) ## Outcome diff --git a/_docs/tasks/done/AZ-548_missions_rename_b9_db_migration.md b/_docs/tasks/done/AZ-548_missions_rename_b9_db_migration.md index d4b3d7d..674a81e 100644 --- a/_docs/tasks/done/AZ-548_missions_rename_b9_db_migration.md +++ b/_docs/tasks/done/AZ-548_missions_rename_b9_db_migration.md @@ -8,6 +8,7 @@ **Component**: `missions/DataLayer/Migrations/` (or whatever the project's actual migration directory is at HEAD), the `init` SQL seed for fresh-install devices **Tracker**: AZ-548 **Epic**: AZ-539 +**Status**: Done (2026-05-15) ## Outcome diff --git a/_docs/tasks/done/AZ-551_missions_rename_b12_default_vehicle_rule.md b/_docs/tasks/done/AZ-551_missions_rename_b12_default_vehicle_rule.md index 2919a06..22af7e7 100644 --- a/_docs/tasks/done/AZ-551_missions_rename_b12_default_vehicle_rule.md +++ b/_docs/tasks/done/AZ-551_missions_rename_b12_default_vehicle_rule.md @@ -8,6 +8,7 @@ **Component**: `missions/Services/VehicleService.cs`, `missions/DataLayer/` (if a partial unique index is added), `azaion-suite/_docs/02_missions.md` (spec catch-up if the rule stays) **Tracker**: AZ-551 **Epic**: AZ-539 +**Status**: Done (2026-05-15) ## Problem diff --git a/docker-compose.test.yml b/docker-compose.test.yml index 7f038c5..0de2a67 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -1,6 +1,9 @@ ## Test compose stack for the missions service. -## Naming: post-rename target. Pre-rename code path runs the same compose against the -## existing Azaion.Flights.csproj entrypoint -- tests will be RED until B5-B8 land. +## Naming: post-rename target. Project entrypoint is Azaion.Missions.csproj. +## B5 (namespace), B6 (domain), B7 (drop GPS-Denied), B8 (HTTP routes), B9 +## (DB migration), B12 (default-vehicle rule) have all landed locally. +## Cross-repo work pending: B4 (Gitea repo rename + suite .gitmodules + git mv), +## B10 (suite compose service block), B11 (autopilot/ui consumer cutover). ## Documented in _docs/02_document/tests/environment.md. ## ## Post-2026-05-14 drift re-verification: JWT model is ECDSA-SHA256 with JWKS