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