mirror of
https://github.com/azaion/missions.git
synced 2026-06-23 08:31:07 +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.revision=$CI_COMMIT_SHA \
|
||||||
--label org.opencontainers.image.created=$BUILD_DATE \
|
--label org.opencontainers.image.created=$BUILD_DATE \
|
||||||
--label org.opencontainers.image.source=$CI_REPO_URL \
|
--label org.opencontainers.image.source=$CI_REPO_URL \
|
||||||
-t $REGISTRY_HOST/azaion/flights:$TAG .
|
-t $REGISTRY_HOST/azaion/missions:$TAG .
|
||||||
- docker push $REGISTRY_HOST/azaion/flights:$TAG
|
- docker push $REGISTRY_HOST/azaion/missions:$TAG
|
||||||
volumes:
|
volumes:
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
- /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.AspNetCore.Authentication.JwtBearer;
|
||||||
using Microsoft.IdentityModel.Protocols;
|
using Microsoft.IdentityModel.Protocols;
|
||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
|
||||||
namespace Azaion.Flights.Auth;
|
namespace Azaion.Missions.Auth;
|
||||||
|
|
||||||
public static class JwtExtensions
|
public static class JwtExtensions
|
||||||
{
|
{
|
||||||
@@ -85,8 +85,7 @@ public static class JwtExtensions
|
|||||||
});
|
});
|
||||||
|
|
||||||
services.AddAuthorizationBuilder()
|
services.AddAuthorizationBuilder()
|
||||||
.AddPolicy("FL", p => p.RequireClaim("permissions", "FL"))
|
.AddPolicy("FL", p => p.RequireClaim("permissions", "FL"));
|
||||||
.AddPolicy("GPS", p => p.RequireClaim("permissions", "GPS"));
|
|
||||||
|
|
||||||
return services;
|
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.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Azaion.Flights.DTOs;
|
using Azaion.Missions.DTOs;
|
||||||
using Azaion.Flights.Services;
|
using Azaion.Missions.Services;
|
||||||
|
|
||||||
namespace Azaion.Flights.Controllers;
|
namespace Azaion.Missions.Controllers;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("flights")]
|
[Route("missions")]
|
||||||
[Authorize(Policy = "FL")]
|
[Authorize(Policy = "FL")]
|
||||||
public class FlightsController(FlightService flightService, WaypointService waypointService) : ControllerBase
|
public class MissionsController(MissionService missionService, WaypointService waypointService) : ControllerBase
|
||||||
{
|
{
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
public async Task<IActionResult> Create([FromBody] CreateFlightRequest request)
|
public async Task<IActionResult> Create([FromBody] CreateMissionRequest request)
|
||||||
{
|
{
|
||||||
var flight = await flightService.CreateFlight(request);
|
var mission = await missionService.CreateMission(request);
|
||||||
return Created($"/flights/{flight.Id}", flight);
|
return Created($"/missions/{mission.Id}", mission);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPut("{id:guid}")]
|
[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);
|
var mission = await missionService.UpdateMission(id, request);
|
||||||
return Ok(flight);
|
return Ok(mission);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{id:guid}")]
|
[HttpGet("{id:guid}")]
|
||||||
public async Task<IActionResult> Get(Guid id)
|
public async Task<IActionResult> Get(Guid id)
|
||||||
{
|
{
|
||||||
var flight = await flightService.GetFlight(id);
|
var mission = await missionService.GetMission(id);
|
||||||
return Ok(flight);
|
return Ok(mission);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[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);
|
return Ok(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpDelete("{id:guid}")]
|
[HttpDelete("{id:guid}")]
|
||||||
public async Task<IActionResult> Delete(Guid id)
|
public async Task<IActionResult> Delete(Guid id)
|
||||||
{
|
{
|
||||||
await flightService.DeleteFlight(id);
|
await missionService.DeleteMission(id);
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@ public class FlightsController(FlightService flightService, WaypointService wayp
|
|||||||
public async Task<IActionResult> CreateWaypoint(Guid id, [FromBody] CreateWaypointRequest request)
|
public async Task<IActionResult> CreateWaypoint(Guid id, [FromBody] CreateWaypointRequest request)
|
||||||
{
|
{
|
||||||
var waypoint = await waypointService.CreateWaypoint(id, 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}")]
|
[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 Model { get; set; } = string.Empty;
|
||||||
public string Name { get; set; } = string.Empty;
|
public string Name { get; set; } = string.Empty;
|
||||||
public FuelType FuelType { 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 CreateWaypointRequest
|
public class CreateWaypointRequest
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Azaion.Flights.DTOs;
|
namespace Azaion.Missions.DTOs;
|
||||||
|
|
||||||
public class ErrorResponse
|
public class ErrorResponse
|
||||||
{
|
{
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace Azaion.Flights.DTOs;
|
namespace Azaion.Missions.DTOs;
|
||||||
|
|
||||||
public class GeoPoint
|
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 string? Name { get; set; }
|
||||||
public DateTime? FromDate { 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 string? Name { get; set; }
|
||||||
public bool? IsDefault { get; set; }
|
public bool? IsDefault { get; set; }
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Azaion.Flights.DTOs;
|
namespace Azaion.Missions.DTOs;
|
||||||
|
|
||||||
public class PaginatedResponse<T>
|
public class PaginatedResponse<T>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Azaion.Flights.DTOs;
|
namespace Azaion.Missions.DTOs;
|
||||||
|
|
||||||
public class SetDefaultRequest
|
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? Model { get; set; }
|
||||||
public string? Name { get; set; }
|
public string? Name { get; set; }
|
||||||
public FuelType? FuelType { 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
|
public class UpdateWaypointRequest
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
using LinqToDB;
|
using LinqToDB;
|
||||||
using LinqToDB.Data;
|
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 class AppDataConnection(DataOptions options) : DataConnection(options)
|
||||||
{
|
{
|
||||||
public ITable<Aircraft> Aircrafts => this.GetTable<Aircraft>();
|
public ITable<Vehicle> Vehicles => this.GetTable<Vehicle>();
|
||||||
public ITable<Flight> Flights => this.GetTable<Flight>();
|
public ITable<Mission> Missions => this.GetTable<Mission>();
|
||||||
public ITable<Waypoint> Waypoints => this.GetTable<Waypoint>();
|
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<MapObject> MapObjects => this.GetTable<MapObject>();
|
||||||
public ITable<Media> Media => this.GetTable<Media>();
|
public ITable<Media> Media => this.GetTable<Media>();
|
||||||
public ITable<Annotation> Annotations => this.GetTable<Annotation>();
|
public ITable<Annotation> Annotations => this.GetTable<Annotation>();
|
||||||
|
|||||||
@@ -1,16 +1,64 @@
|
|||||||
using LinqToDB.Data;
|
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 class DatabaseMigrator
|
||||||
{
|
{
|
||||||
public static void Migrate(AppDataConnection db)
|
public static void Migrate(AppDataConnection db)
|
||||||
{
|
{
|
||||||
db.Execute(Sql);
|
db.Execute(RenameAndDropSql);
|
||||||
|
db.Execute(InitSql);
|
||||||
}
|
}
|
||||||
|
|
||||||
private const string Sql = """
|
// Bring legacy `flights` / `aircrafts` schemas up to the renamed shape.
|
||||||
CREATE TABLE IF NOT EXISTS aircrafts (
|
// 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,
|
id UUID PRIMARY KEY,
|
||||||
type INTEGER NOT NULL DEFAULT 0,
|
type INTEGER NOT NULL DEFAULT 0,
|
||||||
model TEXT NOT NULL,
|
model TEXT NOT NULL,
|
||||||
@@ -22,16 +70,16 @@ public static class DatabaseMigrator
|
|||||||
is_default BOOLEAN NOT NULL DEFAULT FALSE
|
is_default BOOLEAN NOT NULL DEFAULT FALSE
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS flights (
|
CREATE TABLE IF NOT EXISTS missions (
|
||||||
id UUID PRIMARY KEY,
|
id UUID PRIMARY KEY,
|
||||||
created_date TIMESTAMP NOT NULL DEFAULT NOW(),
|
created_date TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
name TEXT NOT NULL,
|
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 (
|
CREATE TABLE IF NOT EXISTS waypoints (
|
||||||
id UUID PRIMARY KEY,
|
id UUID PRIMARY KEY,
|
||||||
flight_id UUID NOT NULL REFERENCES flights(id),
|
mission_id UUID NOT NULL REFERENCES missions(id),
|
||||||
lat NUMERIC,
|
lat NUMERIC,
|
||||||
lon NUMERIC,
|
lon NUMERIC,
|
||||||
mgrs TEXT,
|
mgrs TEXT,
|
||||||
@@ -41,29 +89,9 @@ public static class DatabaseMigrator
|
|||||||
height NUMERIC NOT NULL DEFAULT 0
|
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 (
|
CREATE TABLE IF NOT EXISTS map_objects (
|
||||||
id UUID PRIMARY KEY,
|
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,
|
h3_index TEXT NOT NULL,
|
||||||
mgrs TEXT NOT NULL,
|
mgrs TEXT NOT NULL,
|
||||||
lat NUMERIC,
|
lat NUMERIC,
|
||||||
@@ -78,11 +106,14 @@ public static class DatabaseMigrator
|
|||||||
last_seen_at TIMESTAMP NOT NULL DEFAULT NOW()
|
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_missions_vehicle_id ON missions(vehicle_id);
|
||||||
CREATE INDEX IF NOT EXISTS ix_waypoints_flight_id ON waypoints(flight_id);
|
CREATE INDEX IF NOT EXISTS ix_waypoints_mission_id ON waypoints(mission_id);
|
||||||
CREATE INDEX IF NOT EXISTS ix_orthophotos_flight_id ON orthophotos(flight_id);
|
CREATE INDEX IF NOT EXISTS ix_map_objects_mission_id ON map_objects(mission_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);
|
-- B12 (Option A): exactly-one-default vehicle is enforced by a partial
|
||||||
CREATE INDEX IF NOT EXISTS ix_map_objects_flight_id ON map_objects(flight_id);
|
-- 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;
|
using LinqToDB.Mapping;
|
||||||
|
|
||||||
namespace Azaion.Flights.Database.Entities;
|
namespace Azaion.Missions.Database.Entities;
|
||||||
|
|
||||||
[Table("annotations")]
|
[Table("annotations")]
|
||||||
public class Annotation
|
public class Annotation
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using LinqToDB.Mapping;
|
using LinqToDB.Mapping;
|
||||||
|
|
||||||
namespace Azaion.Flights.Database.Entities;
|
namespace Azaion.Missions.Database.Entities;
|
||||||
|
|
||||||
[Table("detection")]
|
[Table("detection")]
|
||||||
public class 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 LinqToDB.Mapping;
|
||||||
using Azaion.Flights.Enums;
|
using Azaion.Missions.Enums;
|
||||||
|
|
||||||
namespace Azaion.Flights.Database.Entities;
|
namespace Azaion.Missions.Database.Entities;
|
||||||
|
|
||||||
[Table("map_objects")]
|
[Table("map_objects")]
|
||||||
public class MapObject
|
public class MapObject
|
||||||
@@ -10,8 +10,8 @@ public class MapObject
|
|||||||
[Column("id")]
|
[Column("id")]
|
||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
|
|
||||||
[Column("flight_id")]
|
[Column("mission_id")]
|
||||||
public Guid FlightId { get; set; }
|
public Guid MissionId { get; set; }
|
||||||
|
|
||||||
[Column("h3_index")]
|
[Column("h3_index")]
|
||||||
public string H3Index { get; set; } = string.Empty;
|
public string H3Index { get; set; } = string.Empty;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using LinqToDB.Mapping;
|
using LinqToDB.Mapping;
|
||||||
|
|
||||||
namespace Azaion.Flights.Database.Entities;
|
namespace Azaion.Missions.Database.Entities;
|
||||||
|
|
||||||
[Table("media")]
|
[Table("media")]
|
||||||
public class Media
|
public class Media
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
using LinqToDB.Mapping;
|
using LinqToDB.Mapping;
|
||||||
using Azaion.Flights.Enums;
|
|
||||||
|
|
||||||
namespace Azaion.Flights.Database.Entities;
|
namespace Azaion.Missions.Database.Entities;
|
||||||
|
|
||||||
[Table("flights")]
|
[Table("missions")]
|
||||||
public class Flight
|
public class Mission
|
||||||
{
|
{
|
||||||
[PrimaryKey]
|
[PrimaryKey]
|
||||||
[Column("id")]
|
[Column("id")]
|
||||||
@@ -16,12 +15,12 @@ public class Flight
|
|||||||
[Column("name")]
|
[Column("name")]
|
||||||
public string Name { get; set; } = string.Empty;
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
[Column("aircraft_id")]
|
[Column("vehicle_id")]
|
||||||
public Guid AircraftId { get; set; }
|
public Guid VehicleId { get; set; }
|
||||||
|
|
||||||
[Association(ThisKey = nameof(AircraftId), OtherKey = nameof(Aircraft.Id))]
|
[Association(ThisKey = nameof(VehicleId), OtherKey = nameof(Vehicle.Id))]
|
||||||
public Aircraft? Aircraft { get; set; }
|
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; } = [];
|
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 LinqToDB.Mapping;
|
||||||
using Azaion.Flights.Enums;
|
using Azaion.Missions.Enums;
|
||||||
|
|
||||||
namespace Azaion.Flights.Database.Entities;
|
namespace Azaion.Missions.Database.Entities;
|
||||||
|
|
||||||
[Table("aircrafts")]
|
[Table("vehicles")]
|
||||||
public class Aircraft
|
public class Vehicle
|
||||||
{
|
{
|
||||||
[PrimaryKey]
|
[PrimaryKey]
|
||||||
[Column("id")]
|
[Column("id")]
|
||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
|
|
||||||
[Column("type")]
|
[Column("type")]
|
||||||
public AircraftType Type { get; set; }
|
public VehicleType Type { get; set; }
|
||||||
|
|
||||||
[Column("model")]
|
[Column("model")]
|
||||||
public string Model { get; set; } = string.Empty;
|
public string Model { get; set; } = string.Empty;
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
using LinqToDB.Mapping;
|
using LinqToDB.Mapping;
|
||||||
using Azaion.Flights.Enums;
|
using Azaion.Missions.Enums;
|
||||||
|
|
||||||
namespace Azaion.Flights.Database.Entities;
|
namespace Azaion.Missions.Database.Entities;
|
||||||
|
|
||||||
[Table("waypoints")]
|
[Table("waypoints")]
|
||||||
public class Waypoint
|
public class Waypoint
|
||||||
@@ -10,8 +10,8 @@ public class Waypoint
|
|||||||
[Column("id")]
|
[Column("id")]
|
||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
|
|
||||||
[Column("flight_id")]
|
[Column("mission_id")]
|
||||||
public Guid FlightId { get; set; }
|
public Guid MissionId { get; set; }
|
||||||
|
|
||||||
[Column("lat")]
|
[Column("lat")]
|
||||||
public decimal? Lat { get; set; }
|
public decimal? Lat { get; set; }
|
||||||
@@ -34,6 +34,6 @@ public class Waypoint
|
|||||||
[Column("height")]
|
[Column("height")]
|
||||||
public decimal Height { get; set; }
|
public decimal Height { get; set; }
|
||||||
|
|
||||||
[Association(ThisKey = nameof(FlightId), OtherKey = nameof(Flight.Id))]
|
[Association(ThisKey = nameof(MissionId), OtherKey = nameof(Mission.Id))]
|
||||||
public Flight? Flight { get; set; }
|
public Mission? Mission { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -13,4 +13,4 @@ COPY --from=build /app .
|
|||||||
COPY docker-entrypoint.sh /docker-entrypoint.sh
|
COPY docker-entrypoint.sh /docker-entrypoint.sh
|
||||||
RUN chmod +x /docker-entrypoint.sh
|
RUN chmod +x /docker-entrypoint.sh
|
||||||
EXPOSE 8080
|
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
|
public enum FuelType
|
||||||
{
|
{
|
||||||
Electric = 0,
|
Electric = 0,
|
||||||
Gasoline = 1,
|
Gasoline = 1,
|
||||||
Diesel = 2
|
Diesel = 2,
|
||||||
|
SolidPropellant = 3
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Azaion.Flights.Enums;
|
namespace Azaion.Missions.Enums;
|
||||||
|
|
||||||
public enum ObjectStatus
|
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
|
public enum WaypointObjective
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Azaion.Flights.Enums;
|
namespace Azaion.Missions.Enums;
|
||||||
|
|
||||||
public enum WaypointSource
|
public enum WaypointSource
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Azaion.Flights.Infrastructure;
|
namespace Azaion.Missions.Infrastructure;
|
||||||
|
|
||||||
public static class ConfigurationResolver
|
public static class ConfigurationResolver
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Azaion.Flights.Infrastructure;
|
namespace Azaion.Missions.Infrastructure;
|
||||||
|
|
||||||
public static class CorsConfigurationValidator
|
public static class CorsConfigurationValidator
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace Azaion.Flights.Middleware;
|
namespace Azaion.Missions.Middleware;
|
||||||
|
|
||||||
public class ErrorHandlingMiddleware(RequestDelegate next, ILogger<ErrorHandlingMiddleware> logger)
|
public class ErrorHandlingMiddleware(RequestDelegate next, ILogger<ErrorHandlingMiddleware> logger)
|
||||||
{
|
{
|
||||||
|
|||||||
+7
-7
@@ -1,10 +1,10 @@
|
|||||||
using LinqToDB;
|
using LinqToDB;
|
||||||
using LinqToDB.Data;
|
using LinqToDB.Data;
|
||||||
using Azaion.Flights.Auth;
|
using Azaion.Missions.Auth;
|
||||||
using Azaion.Flights.Database;
|
using Azaion.Missions.Database;
|
||||||
using Azaion.Flights.Infrastructure;
|
using Azaion.Missions.Infrastructure;
|
||||||
using Azaion.Flights.Middleware;
|
using Azaion.Missions.Middleware;
|
||||||
using Azaion.Flights.Services;
|
using Azaion.Missions.Services;
|
||||||
|
|
||||||
const string DatabaseUrlEnvVar = "DATABASE_URL";
|
const string DatabaseUrlEnvVar = "DATABASE_URL";
|
||||||
const string DatabaseUrlConfigKey = "Database:Url";
|
const string DatabaseUrlConfigKey = "Database:Url";
|
||||||
@@ -27,9 +27,9 @@ builder.Services.AddScoped(_ =>
|
|||||||
return new AppDataConnection(options);
|
return new AppDataConnection(options);
|
||||||
});
|
});
|
||||||
|
|
||||||
builder.Services.AddScoped<FlightService>();
|
builder.Services.AddScoped<MissionService>();
|
||||||
builder.Services.AddScoped<WaypointService>();
|
builder.Services.AddScoped<WaypointService>();
|
||||||
builder.Services.AddScoped<AircraftService>();
|
builder.Services.AddScoped<VehicleService>();
|
||||||
|
|
||||||
builder.Services.AddJwtAuth(builder.Configuration);
|
builder.Services.AddJwtAuth(builder.Configuration);
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Azaion.Missions
|
# 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.
|
.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.Missions.Database;
|
||||||
using Azaion.Flights.Database.Entities;
|
using Azaion.Missions.Database.Entities;
|
||||||
using Azaion.Flights.DTOs;
|
using Azaion.Missions.DTOs;
|
||||||
using Azaion.Flights.Enums;
|
|
||||||
|
|
||||||
namespace Azaion.Flights.Services;
|
namespace Azaion.Missions.Services;
|
||||||
|
|
||||||
public class WaypointService(AppDataConnection db)
|
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);
|
var missionExists = await db.Missions.AnyAsync(m => m.Id == missionId);
|
||||||
if (!flightExists)
|
if (!missionExists)
|
||||||
throw new KeyNotFoundException($"Flight {flightId} not found");
|
throw new KeyNotFoundException($"Mission {missionId} not found");
|
||||||
|
|
||||||
var waypoint = new Waypoint
|
var waypoint = new Waypoint
|
||||||
{
|
{
|
||||||
Id = Guid.NewGuid(),
|
Id = Guid.NewGuid(),
|
||||||
FlightId = flightId,
|
MissionId = missionId,
|
||||||
Lat = request.GeoPoint?.Lat,
|
Lat = request.GeoPoint?.Lat,
|
||||||
Lon = request.GeoPoint?.Lon,
|
Lon = request.GeoPoint?.Lon,
|
||||||
Mgrs = request.GeoPoint?.Mgrs,
|
Mgrs = request.GeoPoint?.Mgrs,
|
||||||
@@ -29,9 +28,9 @@ public class WaypointService(AppDataConnection db)
|
|||||||
return waypoint;
|
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");
|
?? throw new KeyNotFoundException($"Waypoint {waypointId} not found");
|
||||||
|
|
||||||
waypoint.Lat = request.GeoPoint?.Lat;
|
waypoint.Lat = request.GeoPoint?.Lat;
|
||||||
@@ -46,17 +45,17 @@ public class WaypointService(AppDataConnection db)
|
|||||||
return waypoint;
|
return waypoint;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<Waypoint>> GetWaypoints(Guid flightId)
|
public async Task<List<Waypoint>> GetWaypoints(Guid missionId)
|
||||||
{
|
{
|
||||||
return await db.Waypoints
|
return await db.Waypoints
|
||||||
.Where(w => w.FlightId == flightId)
|
.Where(w => w.MissionId == missionId)
|
||||||
.OrderBy(w => w.OrderNum)
|
.OrderBy(w => w.OrderNum)
|
||||||
.ToListAsync();
|
.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");
|
?? throw new KeyNotFoundException($"Waypoint {waypointId} not found");
|
||||||
|
|
||||||
var mediaIds = await db.Media.Where(m => m.WaypointId == waypointId).Select(m => m.Id).ToListAsync();
|
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.Annotations.DeleteAsync(a => mediaIds.Contains(a.MediaId));
|
||||||
}
|
}
|
||||||
await db.Media.DeleteAsync(m => m.WaypointId == waypointId);
|
await db.Media.DeleteAsync(m => m.WaypointId == waypointId);
|
||||||
await db.GpsCorrections.DeleteAsync(g => g.WaypointId == waypointId);
|
|
||||||
await db.Waypoints.DeleteAsync(w => w.Id == 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).
|
**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):
|
**Files** (post-rename):
|
||||||
- HTTP: `Controllers/VehiclesController.cs`
|
- 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.
|
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.
|
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.
|
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
|
## 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.
|
**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)
|
**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.
|
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 |
|
| Policy | Requirement | Notes |
|
||||||
|--------|-------------|-------|
|
|--------|-------------|-------|
|
||||||
| `"FL"` | JWT contains a `permissions` claim with value `"FL"` | Permanent |
|
| `"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). |
|
||||||
| `"GPS"` | JWT contains a `permissions` claim with value `"GPS"` | Removed in Jira B7 (legacy GPS-Denied routes are moving out of this repo) |
|
|
||||||
|
|
||||||
## 3. JWT model (this service) vs. suite-wide pattern
|
## 3. JWT model (this service) vs. suite-wide pattern
|
||||||
|
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ name: Decompose Tests
|
|||||||
status: not_started
|
status: not_started
|
||||||
sub_step:
|
sub_step:
|
||||||
phase: 0
|
phase: 0
|
||||||
name: blocked-on-tracker-auth
|
name: awaiting-invocation
|
||||||
detail: "atlassian MCP not connected; tracker decision A/B/C/D pending"
|
detail: ""
|
||||||
retry_count: 0
|
retry_count: 0
|
||||||
cycle: 1
|
cycle: 1
|
||||||
tracker: jira
|
tracker: jira
|
||||||
|
|
||||||
## Rename tracking (Jira AZ-EPIC + child stories B1–B12)
|
## 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.
|
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
|
## 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/`.
|
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 |
|
| Plan ID | Jira | Type | SP | Status |
|
||||||
|---------|------|------|----|--------|
|
|---------|------|------|----|--------|
|
||||||
| Epic | [AZ-539](https://denyspopov.atlassian.net/browse/AZ-539) | Epic | -- | 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** (this turn) |
|
| 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** (this turn) |
|
| 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** (this turn) |
|
| 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 |
|
| 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 | To Do |
|
| 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 | To Do |
|
| 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 | To Do |
|
| 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 | To Do |
|
| 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 | To Do |
|
| 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 | To Do |
|
| 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 |
|
| 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 | To Do |
|
| 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
|
## 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
|
**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
|
**Tracker**: AZ-544
|
||||||
**Epic**: AZ-539
|
**Epic**: AZ-539
|
||||||
|
**Status**: Done (2026-05-15)
|
||||||
|
|
||||||
## Outcome
|
## 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)
|
**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
|
**Tracker**: AZ-545
|
||||||
**Epic**: AZ-539
|
**Epic**: AZ-539
|
||||||
|
**Status**: Done (2026-05-15)
|
||||||
|
|
||||||
## Problem
|
## 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)
|
**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
|
**Tracker**: AZ-546
|
||||||
**Epic**: AZ-539
|
**Epic**: AZ-539
|
||||||
|
**Status**: Done (2026-05-15)
|
||||||
|
|
||||||
## Problem
|
## 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`
|
**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
|
**Tracker**: AZ-547
|
||||||
**Epic**: AZ-539
|
**Epic**: AZ-539
|
||||||
|
**Status**: Done (2026-05-15)
|
||||||
|
|
||||||
## Outcome
|
## 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
|
**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
|
**Tracker**: AZ-548
|
||||||
**Epic**: AZ-539
|
**Epic**: AZ-539
|
||||||
|
**Status**: Done (2026-05-15)
|
||||||
|
|
||||||
## Outcome
|
## 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)
|
**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
|
**Tracker**: AZ-551
|
||||||
**Epic**: AZ-539
|
**Epic**: AZ-539
|
||||||
|
**Status**: Done (2026-05-15)
|
||||||
|
|
||||||
## Problem
|
## Problem
|
||||||
|
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
## Test compose stack for the missions service.
|
## Test compose stack for the missions service.
|
||||||
## Naming: post-rename target. Pre-rename code path runs the same compose against the
|
## Naming: post-rename target. Project entrypoint is Azaion.Missions.csproj.
|
||||||
## existing Azaion.Flights.csproj entrypoint -- tests will be RED until B5-B8 land.
|
## 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.
|
## Documented in _docs/02_document/tests/environment.md.
|
||||||
##
|
##
|
||||||
## Post-2026-05-14 drift re-verification: JWT model is ECDSA-SHA256 with JWKS
|
## Post-2026-05-14 drift re-verification: JWT model is ECDSA-SHA256 with JWKS
|
||||||
|
|||||||
Reference in New Issue
Block a user