mirror of
https://github.com/azaion/missions.git
synced 2026-06-22 20:11:08 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2840ccb9b6 | |||
| 4f226e91d5 |
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Azaion.Flights.DTOs;
|
||||
using Azaion.Flights.Services;
|
||||
|
||||
namespace Azaion.Flights.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("aircrafts")]
|
||||
[Authorize(Policy = "FL")]
|
||||
public class AircraftsController(AircraftService aircraftService) : ControllerBase
|
||||
{
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Create([FromBody] CreateAircraftRequest request)
|
||||
{
|
||||
var aircraft = await aircraftService.CreateAircraft(request);
|
||||
return Created($"/aircrafts/{aircraft.Id}", aircraft);
|
||||
}
|
||||
|
||||
[HttpPut("{id:guid}")]
|
||||
public async Task<IActionResult> Update(Guid id, [FromBody] UpdateAircraftRequest request)
|
||||
{
|
||||
var aircraft = await aircraftService.UpdateAircraft(id, request);
|
||||
return Ok(aircraft);
|
||||
}
|
||||
|
||||
[HttpDelete("{id:guid}")]
|
||||
public async Task<IActionResult> Delete(Guid id)
|
||||
{
|
||||
await aircraftService.DeleteAircraft(id);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetAll([FromQuery] GetAircraftsQuery query)
|
||||
{
|
||||
var aircrafts = await aircraftService.GetAircrafts(query);
|
||||
return Ok(aircrafts);
|
||||
}
|
||||
|
||||
[HttpGet("{id:guid}")]
|
||||
public async Task<IActionResult> Get(Guid id)
|
||||
{
|
||||
var aircraft = await aircraftService.GetAircraft(id);
|
||||
return Ok(aircraft);
|
||||
}
|
||||
|
||||
[HttpPatch("{id:guid}/default")]
|
||||
public async Task<IActionResult> SetDefault(Guid id, [FromBody] SetDefaultRequest request)
|
||||
{
|
||||
await aircraftService.SetDefault(id, request);
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
@@ -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<IActionResult> Create([FromBody] CreateFlightRequest request)
|
||||
public async Task<IActionResult> 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<IActionResult> Update(Guid id, [FromBody] UpdateFlightRequest request)
|
||||
public async Task<IActionResult> 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<IActionResult> 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<IActionResult> GetAll([FromQuery] GetFlightsQuery query)
|
||||
public async Task<IActionResult> 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<IActionResult> 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<IActionResult> 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}")]
|
||||
@@ -0,0 +1,54 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Azaion.Missions.DTOs;
|
||||
using Azaion.Missions.Services;
|
||||
|
||||
namespace Azaion.Missions.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("vehicles")]
|
||||
[Authorize(Policy = "FL")]
|
||||
public class VehiclesController(VehicleService vehicleService) : ControllerBase
|
||||
{
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Create([FromBody] CreateVehicleRequest request)
|
||||
{
|
||||
var vehicle = await vehicleService.CreateVehicle(request);
|
||||
return Created($"/vehicles/{vehicle.Id}", vehicle);
|
||||
}
|
||||
|
||||
[HttpPut("{id:guid}")]
|
||||
public async Task<IActionResult> Update(Guid id, [FromBody] UpdateVehicleRequest request)
|
||||
{
|
||||
var vehicle = await vehicleService.UpdateVehicle(id, request);
|
||||
return Ok(vehicle);
|
||||
}
|
||||
|
||||
[HttpDelete("{id:guid}")]
|
||||
public async Task<IActionResult> Delete(Guid id)
|
||||
{
|
||||
await vehicleService.DeleteVehicle(id);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetAll([FromQuery] GetVehiclesQuery query)
|
||||
{
|
||||
var vehicles = await vehicleService.GetVehicles(query);
|
||||
return Ok(vehicles);
|
||||
}
|
||||
|
||||
[HttpGet("{id:guid}")]
|
||||
public async Task<IActionResult> Get(Guid id)
|
||||
{
|
||||
var vehicle = await vehicleService.GetVehicle(id);
|
||||
return Ok(vehicle);
|
||||
}
|
||||
|
||||
[HttpPatch("{id:guid}/default")]
|
||||
public async Task<IActionResult> SetDefault(Guid id, [FromBody] SetDefaultRequest request)
|
||||
{
|
||||
await vehicleService.SetDefault(id, request);
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
namespace Azaion.Flights.DTOs;
|
||||
|
||||
public class CreateFlightRequest
|
||||
{
|
||||
public Guid AircraftId { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public DateTime? CreatedDate { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Azaion.Missions.DTOs;
|
||||
|
||||
public class CreateMissionRequest
|
||||
{
|
||||
public Guid VehicleId { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public DateTime? CreatedDate { get; set; }
|
||||
}
|
||||
@@ -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; }
|
||||
@@ -1,6 +1,6 @@
|
||||
using Azaion.Flights.Enums;
|
||||
using Azaion.Missions.Enums;
|
||||
|
||||
namespace Azaion.Flights.DTOs;
|
||||
namespace Azaion.Missions.DTOs;
|
||||
|
||||
public class CreateWaypointRequest
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Azaion.Flights.DTOs;
|
||||
namespace Azaion.Missions.DTOs;
|
||||
|
||||
public class ErrorResponse
|
||||
{
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace Azaion.Flights.DTOs;
|
||||
namespace Azaion.Missions.DTOs;
|
||||
|
||||
public class GeoPoint
|
||||
{
|
||||
|
||||
@@ -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; }
|
||||
@@ -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; }
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Azaion.Flights.DTOs;
|
||||
namespace Azaion.Missions.DTOs;
|
||||
|
||||
public class PaginatedResponse<T>
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Azaion.Flights.DTOs;
|
||||
namespace Azaion.Missions.DTOs;
|
||||
|
||||
public class SetDefaultRequest
|
||||
{
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
namespace Azaion.Flights.DTOs;
|
||||
|
||||
public class UpdateFlightRequest
|
||||
{
|
||||
public string? Name { get; set; }
|
||||
public Guid? AircraftId { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Azaion.Missions.DTOs;
|
||||
|
||||
public class UpdateMissionRequest
|
||||
{
|
||||
public string? Name { get; set; }
|
||||
public Guid? VehicleId { get; set; }
|
||||
}
|
||||
@@ -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; }
|
||||
@@ -1,6 +1,6 @@
|
||||
using Azaion.Flights.Enums;
|
||||
using Azaion.Missions.Enums;
|
||||
|
||||
namespace Azaion.Flights.DTOs;
|
||||
namespace Azaion.Missions.DTOs;
|
||||
|
||||
public class UpdateWaypointRequest
|
||||
{
|
||||
|
||||
@@ -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<Aircraft> Aircrafts => this.GetTable<Aircraft>();
|
||||
public ITable<Flight> Flights => this.GetTable<Flight>();
|
||||
public ITable<Vehicle> Vehicles => this.GetTable<Vehicle>();
|
||||
public ITable<Mission> Missions => this.GetTable<Mission>();
|
||||
public ITable<Waypoint> Waypoints => this.GetTable<Waypoint>();
|
||||
public ITable<Orthophoto> Orthophotos => this.GetTable<Orthophoto>();
|
||||
public ITable<GpsCorrection> GpsCorrections => this.GetTable<GpsCorrection>();
|
||||
public ITable<MapObject> MapObjects => this.GetTable<MapObject>();
|
||||
public ITable<Media> Media => this.GetTable<Media>();
|
||||
public ITable<Annotation> Annotations => this.GetTable<Annotation>();
|
||||
|
||||
@@ -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;
|
||||
""";
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using LinqToDB.Mapping;
|
||||
|
||||
namespace Azaion.Flights.Database.Entities;
|
||||
namespace Azaion.Missions.Database.Entities;
|
||||
|
||||
[Table("annotations")]
|
||||
public class Annotation
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using LinqToDB.Mapping;
|
||||
|
||||
namespace Azaion.Flights.Database.Entities;
|
||||
namespace Azaion.Missions.Database.Entities;
|
||||
|
||||
[Table("detection")]
|
||||
public class Detection
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
using LinqToDB.Mapping;
|
||||
|
||||
namespace Azaion.Flights.Database.Entities;
|
||||
|
||||
[Table("gps_corrections")]
|
||||
public class GpsCorrection
|
||||
{
|
||||
[PrimaryKey]
|
||||
[Column("id")]
|
||||
public Guid Id { get; set; }
|
||||
|
||||
[Column("flight_id")]
|
||||
public Guid FlightId { get; set; }
|
||||
|
||||
[Column("waypoint_id")]
|
||||
public Guid WaypointId { get; set; }
|
||||
|
||||
[Column("original_gps")]
|
||||
public string OriginalGps { get; set; } = string.Empty;
|
||||
|
||||
[Column("corrected_gps")]
|
||||
public string CorrectedGps { get; set; } = string.Empty;
|
||||
|
||||
[Column("applied_at")]
|
||||
public DateTime AppliedAt { get; set; }
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using LinqToDB.Mapping;
|
||||
|
||||
namespace Azaion.Flights.Database.Entities;
|
||||
namespace Azaion.Missions.Database.Entities;
|
||||
|
||||
[Table("media")]
|
||||
public class Media
|
||||
|
||||
@@ -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<Waypoint> Waypoints { get; set; } = [];
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
using LinqToDB.Mapping;
|
||||
|
||||
namespace Azaion.Flights.Database.Entities;
|
||||
|
||||
[Table("orthophotos")]
|
||||
public class Orthophoto
|
||||
{
|
||||
[PrimaryKey]
|
||||
[Column("id")]
|
||||
public string Id { get; set; } = string.Empty;
|
||||
|
||||
[Column("flight_id")]
|
||||
public Guid FlightId { get; set; }
|
||||
|
||||
[Column("name")]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
[Column("path")]
|
||||
public string Path { get; set; } = string.Empty;
|
||||
|
||||
[Column("lat")]
|
||||
public decimal? Lat { get; set; }
|
||||
|
||||
[Column("lon")]
|
||||
public decimal? Lon { get; set; }
|
||||
|
||||
[Column("mgrs")]
|
||||
public string? Mgrs { get; set; }
|
||||
|
||||
[Column("uploaded_at")]
|
||||
public DateTime UploadedAt { get; set; }
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
+1
-1
@@ -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"]
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
namespace Azaion.Flights.Enums;
|
||||
|
||||
public enum AircraftType
|
||||
{
|
||||
Plane = 0,
|
||||
Copter = 1
|
||||
}
|
||||
+5
-2
@@ -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
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Azaion.Flights.Enums;
|
||||
namespace Azaion.Missions.Enums;
|
||||
|
||||
public enum ObjectStatus
|
||||
{
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace Azaion.Missions.Enums;
|
||||
|
||||
// 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,
|
||||
UGV = 2,
|
||||
GuidedMissile = 3
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Azaion.Flights.Enums;
|
||||
namespace Azaion.Missions.Enums;
|
||||
|
||||
public enum WaypointObjective
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Azaion.Flights.Enums;
|
||||
namespace Azaion.Missions.Enums;
|
||||
|
||||
public enum WaypointSource
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Azaion.Flights.Infrastructure;
|
||||
namespace Azaion.Missions.Infrastructure;
|
||||
|
||||
public static class ConfigurationResolver
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Azaion.Flights.Infrastructure;
|
||||
namespace Azaion.Missions.Infrastructure;
|
||||
|
||||
public static class CorsConfigurationValidator
|
||||
{
|
||||
|
||||
@@ -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<ErrorHandlingMiddleware> logger)
|
||||
{
|
||||
|
||||
+7
-7
@@ -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<FlightService>();
|
||||
builder.Services.AddScoped<MissionService>();
|
||||
builder.Services.AddScoped<WaypointService>();
|
||||
builder.Services.AddScoped<AircraftService>();
|
||||
builder.Services.AddScoped<VehicleService>();
|
||||
|
||||
builder.Services.AddJwtAuth(builder.Configuration);
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
using Azaion.Flights.Database;
|
||||
using Azaion.Flights.Database.Entities;
|
||||
using Azaion.Flights.DTOs;
|
||||
|
||||
namespace Azaion.Flights.Services;
|
||||
|
||||
public class AircraftService(AppDataConnection db)
|
||||
{
|
||||
public async Task<Aircraft> CreateAircraft(CreateAircraftRequest request)
|
||||
{
|
||||
if (request.IsDefault)
|
||||
await db.Aircrafts.Where(a => a.IsDefault).Set(a => a.IsDefault, false).UpdateAsync();
|
||||
|
||||
var aircraft = new Aircraft
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Type = request.Type,
|
||||
Model = request.Model,
|
||||
Name = request.Name,
|
||||
FuelType = request.FuelType,
|
||||
BatteryCapacity = request.BatteryCapacity,
|
||||
EngineConsumption = request.EngineConsumption,
|
||||
EngineConsumptionIdle = request.EngineConsumptionIdle,
|
||||
IsDefault = request.IsDefault
|
||||
};
|
||||
await db.InsertAsync(aircraft);
|
||||
return aircraft;
|
||||
}
|
||||
|
||||
public async Task<Aircraft> 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.Value)
|
||||
await db.Aircrafts.Where(a => a.IsDefault).Set(a => a.IsDefault, false).UpdateAsync();
|
||||
aircraft.IsDefault = request.IsDefault.Value;
|
||||
}
|
||||
|
||||
await db.UpdateAsync(aircraft);
|
||||
return aircraft;
|
||||
}
|
||||
|
||||
public async Task<Aircraft> GetAircraft(Guid id)
|
||||
{
|
||||
var aircraft = await db.Aircrafts.FirstOrDefaultAsync(a => a.Id == id)
|
||||
?? throw new KeyNotFoundException($"Aircraft {id} not found");
|
||||
return aircraft;
|
||||
}
|
||||
|
||||
public async Task<List<Aircraft>> GetAircrafts(GetAircraftsQuery query)
|
||||
{
|
||||
var q = db.Aircrafts.AsQueryable();
|
||||
|
||||
if (!string.IsNullOrEmpty(query.Name))
|
||||
q = q.Where(a => a.Name.ToLower().Contains(query.Name.ToLower()));
|
||||
if (query.IsDefault.HasValue)
|
||||
q = q.Where(a => a.IsDefault == query.IsDefault.Value);
|
||||
|
||||
return await q.OrderBy(a => a.Name).ToListAsync();
|
||||
}
|
||||
|
||||
public async Task DeleteAircraft(Guid id)
|
||||
{
|
||||
var hasFlights = await db.Flights.AnyAsync(f => f.AircraftId == id);
|
||||
if (hasFlights)
|
||||
throw new InvalidOperationException($"Aircraft {id} is referenced by flights");
|
||||
|
||||
var aircraft = await db.Aircrafts.FirstOrDefaultAsync(a => a.Id == id)
|
||||
?? throw new KeyNotFoundException($"Aircraft {id} not found");
|
||||
|
||||
await db.Aircrafts.DeleteAsync(a => a.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");
|
||||
|
||||
if (request.IsDefault)
|
||||
await db.Aircrafts.Where(a => a.IsDefault).Set(a => a.IsDefault, false).UpdateAsync();
|
||||
|
||||
aircraft.IsDefault = request.IsDefault;
|
||||
await db.UpdateAsync(aircraft);
|
||||
}
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
using LinqToDB;
|
||||
using Azaion.Flights.Database;
|
||||
using Azaion.Flights.Database.Entities;
|
||||
using Azaion.Flights.DTOs;
|
||||
|
||||
namespace Azaion.Flights.Services;
|
||||
|
||||
public class FlightService(AppDataConnection db)
|
||||
{
|
||||
public async Task<Flight> CreateFlight(CreateFlightRequest request)
|
||||
{
|
||||
var aircraftExists = await db.Aircrafts.AnyAsync(a => a.Id == request.AircraftId);
|
||||
if (!aircraftExists)
|
||||
throw new ArgumentException($"Aircraft {request.AircraftId} not found");
|
||||
|
||||
var flight = new Flight
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
CreatedDate = request.CreatedDate ?? DateTime.UtcNow,
|
||||
Name = request.Name,
|
||||
AircraftId = request.AircraftId
|
||||
};
|
||||
await db.InsertAsync(flight);
|
||||
return flight;
|
||||
}
|
||||
|
||||
public async Task<Flight> UpdateFlight(Guid id, UpdateFlightRequest request)
|
||||
{
|
||||
var flight = await db.Flights.FirstOrDefaultAsync(f => f.Id == id)
|
||||
?? throw new KeyNotFoundException($"Flight {id} not found");
|
||||
|
||||
if (request.Name != null)
|
||||
flight.Name = request.Name;
|
||||
if (request.AircraftId.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;
|
||||
}
|
||||
|
||||
await db.UpdateAsync(flight);
|
||||
return flight;
|
||||
}
|
||||
|
||||
public async Task<Flight> GetFlight(Guid id)
|
||||
{
|
||||
var flight = await db.Flights.FirstOrDefaultAsync(f => f.Id == id)
|
||||
?? throw new KeyNotFoundException($"Flight {id} not found");
|
||||
return flight;
|
||||
}
|
||||
|
||||
public async Task<PaginatedResponse<Flight>> GetFlights(GetFlightsQuery query)
|
||||
{
|
||||
var q = db.Flights.AsQueryable();
|
||||
|
||||
if (!string.IsNullOrEmpty(query.Name))
|
||||
q = q.Where(f => f.Name.ToLower().Contains(query.Name.ToLower()));
|
||||
if (query.FromDate.HasValue)
|
||||
q = q.Where(f => f.CreatedDate >= query.FromDate.Value);
|
||||
if (query.ToDate.HasValue)
|
||||
q = q.Where(f => f.CreatedDate <= query.ToDate.Value);
|
||||
|
||||
var totalCount = await q.CountAsync();
|
||||
|
||||
var items = await q
|
||||
.OrderByDescending(f => f.CreatedDate)
|
||||
.Skip((query.Page - 1) * query.PageSize)
|
||||
.Take(query.PageSize)
|
||||
.ToListAsync();
|
||||
|
||||
return new PaginatedResponse<Flight>
|
||||
{
|
||||
Items = items,
|
||||
TotalCount = totalCount,
|
||||
Page = query.Page,
|
||||
PageSize = query.PageSize
|
||||
};
|
||||
}
|
||||
|
||||
public async Task DeleteFlight(Guid id)
|
||||
{
|
||||
var flight = await db.Flights.FirstOrDefaultAsync(f => f.Id == id)
|
||||
?? throw new KeyNotFoundException($"Flight {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);
|
||||
|
||||
var waypointIds = await db.Waypoints.Where(w => w.FlightId == 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))
|
||||
.Select(m => m.Id).ToListAsync();
|
||||
if (mediaIds.Count > 0)
|
||||
{
|
||||
var annotationIds = await db.Annotations.Where(a => mediaIds.Contains(a.MediaId))
|
||||
.Select(a => a.Id).ToListAsync();
|
||||
if (annotationIds.Count > 0)
|
||||
await db.Detections.DeleteAsync(d => annotationIds.Contains(d.AnnotationId));
|
||||
await db.Annotations.DeleteAsync(a => mediaIds.Contains(a.MediaId));
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
using LinqToDB;
|
||||
using Azaion.Missions.Database;
|
||||
using Azaion.Missions.Database.Entities;
|
||||
using Azaion.Missions.DTOs;
|
||||
|
||||
namespace Azaion.Missions.Services;
|
||||
|
||||
public class MissionService(AppDataConnection db)
|
||||
{
|
||||
public async Task<Mission> CreateMission(CreateMissionRequest request)
|
||||
{
|
||||
var vehicleExists = await db.Vehicles.AnyAsync(v => v.Id == request.VehicleId);
|
||||
if (!vehicleExists)
|
||||
throw new ArgumentException($"Vehicle {request.VehicleId} not found");
|
||||
|
||||
var mission = new Mission
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
CreatedDate = request.CreatedDate ?? DateTime.UtcNow,
|
||||
Name = request.Name,
|
||||
VehicleId = request.VehicleId
|
||||
};
|
||||
await db.InsertAsync(mission);
|
||||
return mission;
|
||||
}
|
||||
|
||||
public async Task<Mission> UpdateMission(Guid id, UpdateMissionRequest request)
|
||||
{
|
||||
var mission = await db.Missions.FirstOrDefaultAsync(m => m.Id == id)
|
||||
?? throw new KeyNotFoundException($"Mission {id} not found");
|
||||
|
||||
if (request.Name != null)
|
||||
mission.Name = request.Name;
|
||||
if (request.VehicleId.HasValue)
|
||||
{
|
||||
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(mission);
|
||||
return mission;
|
||||
}
|
||||
|
||||
public async Task<Mission> GetMission(Guid id)
|
||||
{
|
||||
var mission = await db.Missions.FirstOrDefaultAsync(m => m.Id == id)
|
||||
?? throw new KeyNotFoundException($"Mission {id} not found");
|
||||
return mission;
|
||||
}
|
||||
|
||||
public async Task<PaginatedResponse<Mission>> GetMissions(GetMissionsQuery query)
|
||||
{
|
||||
var q = db.Missions.AsQueryable();
|
||||
|
||||
if (!string.IsNullOrEmpty(query.Name))
|
||||
q = q.Where(m => m.Name.ToLower().Contains(query.Name.ToLower()));
|
||||
if (query.FromDate.HasValue)
|
||||
q = q.Where(m => m.CreatedDate >= query.FromDate.Value);
|
||||
if (query.ToDate.HasValue)
|
||||
q = q.Where(m => m.CreatedDate <= query.ToDate.Value);
|
||||
|
||||
var totalCount = await q.CountAsync();
|
||||
|
||||
var items = await q
|
||||
.OrderByDescending(m => m.CreatedDate)
|
||||
.Skip((query.Page - 1) * query.PageSize)
|
||||
.Take(query.PageSize)
|
||||
.ToListAsync();
|
||||
|
||||
return new PaginatedResponse<Mission>
|
||||
{
|
||||
Items = items,
|
||||
TotalCount = totalCount,
|
||||
Page = query.Page,
|
||||
PageSize = query.PageSize
|
||||
};
|
||||
}
|
||||
|
||||
public async Task DeleteMission(Guid id)
|
||||
{
|
||||
var mission = await db.Missions.FirstOrDefaultAsync(m => m.Id == id)
|
||||
?? throw new KeyNotFoundException($"Mission {id} not found");
|
||||
|
||||
await db.MapObjects.DeleteAsync(o => o.MissionId == id);
|
||||
|
||||
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))
|
||||
.Select(m => m.Id).ToListAsync();
|
||||
if (mediaIds.Count > 0)
|
||||
{
|
||||
var annotationIds = await db.Annotations.Where(a => mediaIds.Contains(a.MediaId))
|
||||
.Select(a => a.Id).ToListAsync();
|
||||
if (annotationIds.Count > 0)
|
||||
await db.Detections.DeleteAsync(d => annotationIds.Contains(d.AnnotationId));
|
||||
await db.Annotations.DeleteAsync(a => mediaIds.Contains(a.MediaId));
|
||||
}
|
||||
await db.Media.DeleteAsync(m => m.WaypointId != null && waypointIds.Contains(m.WaypointId!.Value));
|
||||
}
|
||||
|
||||
await db.Waypoints.DeleteAsync(w => w.MissionId == id);
|
||||
await db.Missions.DeleteAsync(m => m.Id == id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
using System.Data;
|
||||
using Azaion.Missions.Database;
|
||||
using Azaion.Missions.Database.Entities;
|
||||
using Azaion.Missions.DTOs;
|
||||
|
||||
namespace Azaion.Missions.Services;
|
||||
|
||||
public class VehicleService(AppDataConnection db)
|
||||
{
|
||||
// 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<Vehicle> CreateVehicle(CreateVehicleRequest request)
|
||||
{
|
||||
var vehicle = new Vehicle
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Type = request.Type,
|
||||
Model = request.Model,
|
||||
Name = request.Name,
|
||||
FuelType = request.FuelType,
|
||||
BatteryCapacity = request.BatteryCapacity,
|
||||
EngineConsumption = request.EngineConsumption,
|
||||
EngineConsumptionIdle = request.EngineConsumptionIdle,
|
||||
IsDefault = request.IsDefault
|
||||
};
|
||||
|
||||
if (request.IsDefault)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
return vehicle;
|
||||
}
|
||||
|
||||
public async Task<Vehicle> UpdateVehicle(Guid id, UpdateVehicleRequest request)
|
||||
{
|
||||
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<Vehicle> GetVehicle(Guid id)
|
||||
{
|
||||
var vehicle = await db.Vehicles.FirstOrDefaultAsync(v => v.Id == id)
|
||||
?? throw new KeyNotFoundException($"Vehicle {id} not found");
|
||||
return vehicle;
|
||||
}
|
||||
|
||||
public async Task<List<Vehicle>> GetVehicles(GetVehiclesQuery query)
|
||||
{
|
||||
var q = db.Vehicles.AsQueryable();
|
||||
|
||||
if (!string.IsNullOrEmpty(query.Name))
|
||||
q = q.Where(v => v.Name.ToLower().Contains(query.Name.ToLower()));
|
||||
if (query.IsDefault.HasValue)
|
||||
q = q.Where(v => v.IsDefault == query.IsDefault.Value);
|
||||
|
||||
return await q.OrderBy(v => v.Name).ToListAsync();
|
||||
}
|
||||
|
||||
public async Task DeleteVehicle(Guid id)
|
||||
{
|
||||
var hasMissions = await db.Missions.AnyAsync(m => m.VehicleId == id);
|
||||
if (hasMissions)
|
||||
throw new InvalidOperationException($"Vehicle {id} is referenced by missions");
|
||||
|
||||
var vehicle = await db.Vehicles.FirstOrDefaultAsync(v => v.Id == id)
|
||||
?? throw new KeyNotFoundException($"Vehicle {id} not found");
|
||||
|
||||
await db.Vehicles.DeleteAsync(v => v.Id == id);
|
||||
}
|
||||
|
||||
public async Task SetDefault(Guid id, SetDefaultRequest request)
|
||||
{
|
||||
var vehicle = await db.Vehicles.FirstOrDefaultAsync(v => v.Id == id)
|
||||
?? throw new KeyNotFoundException($"Vehicle {id} not found");
|
||||
|
||||
if (request.IsDefault)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+15
-17
@@ -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<Waypoint> CreateWaypoint(Guid flightId, CreateWaypointRequest request)
|
||||
public async Task<Waypoint> 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<Waypoint> UpdateWaypoint(Guid flightId, Guid waypointId, UpdateWaypointRequest request)
|
||||
public async Task<Waypoint> 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<List<Waypoint>> GetWaypoints(Guid flightId)
|
||||
public async Task<List<Waypoint>> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 `<repo>/_docs/tasks/{todo,done}/AZ-<n>_<slug>.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
|
||||
|
||||
|
||||
@@ -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-<id>` 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-<id>_xxx.md`.
|
||||
+1
@@ -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
|
||||
|
||||
+1
@@ -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
|
||||
|
||||
+1
@@ -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
|
||||
|
||||
+1
@@ -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
|
||||
|
||||
+1
@@ -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
|
||||
|
||||
+1
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user