diff --git a/Data/Migrations/20251209141933_AllowNullableGroundStationIdInFlightPlans.Designer.cs b/Data/Migrations/20251209141933_AllowNullableGroundStationIdInFlightPlans.Designer.cs new file mode 100644 index 0000000..4fcbb3c --- /dev/null +++ b/Data/Migrations/20251209141933_AllowNullableGroundStationIdInFlightPlans.Designer.cs @@ -0,0 +1,561 @@ +// +using System; +using System.Text.Json; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using SatOps.Data; + +#nullable disable + +namespace SatOps.Data.migrations +{ + [DbContext(typeof(SatOpsDbContext))] + [Migration("20251209141933_AllowNullableGroundStationIdInFlightPlans")] + partial class AllowNullableGroundStationIdInFlightPlans + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("SatOps.Modules.FlightPlan.FlightPlan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApprovalDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ApprovedById") + .HasColumnType("integer"); + + b.Property("Commands") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("timezone('utc', now())"); + + b.Property("CreatedById") + .HasColumnType("integer"); + + b.Property("FailureReason") + .HasColumnType("text"); + + b.Property("GroundStationId") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PreviousPlanId") + .HasColumnType("integer"); + + b.Property("SatelliteId") + .HasColumnType("integer"); + + b.Property("ScheduledAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("timezone('utc', now())"); + + b.HasKey("Id"); + + b.HasIndex("ApprovedById"); + + b.HasIndex("CreatedById"); + + b.HasIndex("GroundStationId"); + + b.HasIndex("PreviousPlanId"); + + b.HasIndex("SatelliteId"); + + b.HasIndex("Status"); + + b.ToTable("flight_plans", (string)null); + }); + + modelBuilder.Entity("SatOps.Modules.GroundStationLink.ImageData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CaptureTime") + .HasColumnType("timestamp with time zone"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("FlightPlanId") + .HasColumnType("integer"); + + b.Property("GroundStationId") + .HasColumnType("integer"); + + b.Property("ImageHeight") + .HasColumnType("integer"); + + b.Property("ImageWidth") + .HasColumnType("integer"); + + b.Property("Latitude") + .HasPrecision(9, 6) + .HasColumnType("double precision"); + + b.Property("Longitude") + .HasPrecision(9, 6) + .HasColumnType("double precision"); + + b.Property("Metadata") + .HasColumnType("jsonb"); + + b.Property("ReceivedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("timezone('utc', now())"); + + b.Property("S3ObjectPath") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("SatelliteId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CaptureTime"); + + b.HasIndex("FlightPlanId"); + + b.HasIndex("GroundStationId"); + + b.HasIndex("SatelliteId"); + + b.HasIndex("Latitude", "Longitude"); + + b.ToTable("image_data", (string)null); + }); + + modelBuilder.Entity("SatOps.Modules.Groundstation.GroundStation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApiKeyHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("timezone('utc', now())"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId") + .IsUnique(); + + b.ToTable("ground_stations", (string)null); + + b.HasData( + new + { + Id = 1, + ApiKeyHash = "", + ApplicationId = new Guid("c4bd6c61-8e11-43cf-868a-fdd0a4be81b2"), + CreatedAt = new DateTime(2025, 12, 9, 14, 19, 32, 968, DateTimeKind.Utc).AddTicks(9380), + Name = "Aarhus", + UpdatedAt = new DateTime(2025, 12, 9, 14, 19, 32, 968, DateTimeKind.Utc).AddTicks(9380) + }); + }); + + modelBuilder.Entity("SatOps.Modules.Overpass.Entity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DurationSeconds") + .HasColumnType("integer"); + + b.Property("EndAzimuth") + .HasColumnType("double precision"); + + b.Property("EndTime") + .HasColumnType("timestamp with time zone"); + + b.Property("FlightPlanId") + .HasColumnType("integer"); + + b.Property("GroundStationId") + .HasColumnType("integer"); + + b.Property("MaxElevation") + .HasColumnType("double precision"); + + b.Property("MaxElevationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("SatelliteId") + .HasColumnType("integer"); + + b.Property("StartAzimuth") + .HasColumnType("double precision"); + + b.Property("StartTime") + .HasColumnType("timestamp with time zone"); + + b.Property("TleLine1") + .HasColumnType("text"); + + b.Property("TleLine2") + .HasColumnType("text"); + + b.Property("TleUpdateTime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("FlightPlanId") + .IsUnique(); + + b.HasIndex("GroundStationId"); + + b.HasIndex("SatelliteId", "GroundStationId", "StartTime"); + + b.ToTable("overpasses", (string)null); + }); + + modelBuilder.Entity("SatOps.Modules.Satellite.Satellite", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("timezone('utc', now())"); + + b.Property("LastUpdate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("timezone('utc', now())"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NoradId") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TleLine1") + .IsRequired() + .HasColumnType("text"); + + b.Property("TleLine2") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("NoradId") + .IsUnique(); + + b.HasIndex("Status"); + + b.ToTable("satellites", (string)null); + + b.HasData( + new + { + Id = 1, + CreatedAt = new DateTime(2025, 12, 9, 14, 19, 32, 968, DateTimeKind.Utc).AddTicks(9710), + LastUpdate = new DateTime(2025, 12, 9, 14, 19, 32, 968, DateTimeKind.Utc).AddTicks(9710), + Name = "International Space Station (ISS)", + NoradId = 25544, + Status = 0, + TleLine1 = "1 25544U 98067A 23256.90616898 .00020137 00000-0 35438-3 0 9992", + TleLine2 = "2 25544 51.6416 339.0970 0003835 48.3825 73.2709 15.50030022414673" + }, + new + { + Id = 2, + CreatedAt = new DateTime(2025, 12, 9, 14, 19, 32, 968, DateTimeKind.Utc).AddTicks(9710), + LastUpdate = new DateTime(2025, 12, 9, 14, 19, 32, 968, DateTimeKind.Utc).AddTicks(9710), + Name = "SENTINEL-2C", + NoradId = 60989, + Status = 0, + TleLine1 = "1 60989U 24157A 25270.79510520 .00000303 00000-0 13232-3 0 9996", + TleLine2 = "2 60989 98.5675 344.4033 0001006 86.9003 273.2295 14.30815465 55465" + }); + }); + + modelBuilder.Entity("SatOps.Modules.User.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Auth0UserId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("timezone('utc', now())"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Role") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("timezone('utc', now())"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("Role"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("SatOps.Modules.FlightPlan.FlightPlan", b => + { + b.HasOne("SatOps.Modules.User.User", "ApprovedBy") + .WithMany("ApprovedFlightPlans") + .HasForeignKey("ApprovedById") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SatOps.Modules.User.User", "CreatedBy") + .WithMany("CreatedFlightPlans") + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SatOps.Modules.Groundstation.GroundStation", "GroundStation") + .WithMany("FlightPlans") + .HasForeignKey("GroundStationId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SatOps.Modules.FlightPlan.FlightPlan", "PreviousPlan") + .WithMany() + .HasForeignKey("PreviousPlanId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SatOps.Modules.Satellite.Satellite", "Satellite") + .WithMany("FlightPlans") + .HasForeignKey("SatelliteId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApprovedBy"); + + b.Navigation("CreatedBy"); + + b.Navigation("GroundStation"); + + b.Navigation("PreviousPlan"); + + b.Navigation("Satellite"); + }); + + modelBuilder.Entity("SatOps.Modules.GroundStationLink.ImageData", b => + { + b.HasOne("SatOps.Modules.FlightPlan.FlightPlan", "FlightPlan") + .WithMany() + .HasForeignKey("FlightPlanId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SatOps.Modules.Groundstation.GroundStation", "GroundStation") + .WithMany("Images") + .HasForeignKey("GroundStationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SatOps.Modules.Satellite.Satellite", "Satellite") + .WithMany("Images") + .HasForeignKey("SatelliteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("FlightPlan"); + + b.Navigation("GroundStation"); + + b.Navigation("Satellite"); + }); + + modelBuilder.Entity("SatOps.Modules.Groundstation.GroundStation", b => + { + b.OwnsOne("SatOps.Modules.Groundstation.Location", "Location", b1 => + { + b1.Property("GroundStationId") + .HasColumnType("integer"); + + b1.Property("Altitude") + .ValueGeneratedOnAdd() + .HasColumnType("double precision") + .HasDefaultValue(0.0) + .HasColumnName("altitude"); + + b1.Property("Latitude") + .HasColumnType("double precision") + .HasColumnName("latitude"); + + b1.Property("Longitude") + .HasColumnType("double precision") + .HasColumnName("longitude"); + + b1.HasKey("GroundStationId"); + + b1.ToTable("ground_stations"); + + b1.WithOwner() + .HasForeignKey("GroundStationId"); + + b1.HasData( + new + { + GroundStationId = 1, + Altitude = 62.0, + Latitude = 56.171972897990663, + Longitude = 10.191659216036516 + }); + }); + + b.Navigation("Location") + .IsRequired(); + }); + + modelBuilder.Entity("SatOps.Modules.Overpass.Entity", b => + { + b.HasOne("SatOps.Modules.FlightPlan.FlightPlan", "FlightPlan") + .WithOne("Overpass") + .HasForeignKey("SatOps.Modules.Overpass.Entity", "FlightPlanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SatOps.Modules.Groundstation.GroundStation", "GroundStation") + .WithMany("Overpasses") + .HasForeignKey("GroundStationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SatOps.Modules.Satellite.Satellite", "Satellite") + .WithMany("Overpasses") + .HasForeignKey("SatelliteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("FlightPlan"); + + b.Navigation("GroundStation"); + + b.Navigation("Satellite"); + }); + + modelBuilder.Entity("SatOps.Modules.FlightPlan.FlightPlan", b => + { + b.Navigation("Overpass"); + }); + + modelBuilder.Entity("SatOps.Modules.Groundstation.GroundStation", b => + { + b.Navigation("FlightPlans"); + + b.Navigation("Images"); + + b.Navigation("Overpasses"); + }); + + modelBuilder.Entity("SatOps.Modules.Satellite.Satellite", b => + { + b.Navigation("FlightPlans"); + + b.Navigation("Images"); + + b.Navigation("Overpasses"); + }); + + modelBuilder.Entity("SatOps.Modules.User.User", b => + { + b.Navigation("ApprovedFlightPlans"); + + b.Navigation("CreatedFlightPlans"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Data/Migrations/20251209141933_AllowNullableGroundStationIdInFlightPlans.cs b/Data/Migrations/20251209141933_AllowNullableGroundStationIdInFlightPlans.cs new file mode 100644 index 0000000..6708891 --- /dev/null +++ b/Data/Migrations/20251209141933_AllowNullableGroundStationIdInFlightPlans.cs @@ -0,0 +1,103 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SatOps.Data.migrations +{ + /// + public partial class AllowNullableGroundStationIdInFlightPlans : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_flight_plans_ground_stations_GroundStationId", + table: "flight_plans"); + + migrationBuilder.AlterColumn( + name: "GroundStationId", + table: "flight_plans", + type: "integer", + nullable: true, + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.UpdateData( + table: "ground_stations", + keyColumn: "Id", + keyValue: 1, + columns: new[] { "ApplicationId", "CreatedAt", "UpdatedAt" }, + values: new object[] { new Guid("c4bd6c61-8e11-43cf-868a-fdd0a4be81b2"), new DateTime(2025, 12, 9, 14, 19, 32, 968, DateTimeKind.Utc).AddTicks(9380), new DateTime(2025, 12, 9, 14, 19, 32, 968, DateTimeKind.Utc).AddTicks(9380) }); + + migrationBuilder.UpdateData( + table: "satellites", + keyColumn: "Id", + keyValue: 1, + columns: new[] { "CreatedAt", "LastUpdate" }, + values: new object[] { new DateTime(2025, 12, 9, 14, 19, 32, 968, DateTimeKind.Utc).AddTicks(9710), new DateTime(2025, 12, 9, 14, 19, 32, 968, DateTimeKind.Utc).AddTicks(9710) }); + + migrationBuilder.UpdateData( + table: "satellites", + keyColumn: "Id", + keyValue: 2, + columns: new[] { "CreatedAt", "LastUpdate" }, + values: new object[] { new DateTime(2025, 12, 9, 14, 19, 32, 968, DateTimeKind.Utc).AddTicks(9710), new DateTime(2025, 12, 9, 14, 19, 32, 968, DateTimeKind.Utc).AddTicks(9710) }); + + migrationBuilder.AddForeignKey( + name: "FK_flight_plans_ground_stations_GroundStationId", + table: "flight_plans", + column: "GroundStationId", + principalTable: "ground_stations", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_flight_plans_ground_stations_GroundStationId", + table: "flight_plans"); + + migrationBuilder.AlterColumn( + name: "GroundStationId", + table: "flight_plans", + type: "integer", + nullable: false, + defaultValue: 0, + oldClrType: typeof(int), + oldType: "integer", + oldNullable: true); + + migrationBuilder.UpdateData( + table: "ground_stations", + keyColumn: "Id", + keyValue: 1, + columns: new[] { "ApplicationId", "CreatedAt", "UpdatedAt" }, + values: new object[] { new Guid("eecf3fa7-de5b-41e0-83ac-ce6d14cd9b86"), new DateTime(2025, 11, 12, 20, 16, 41, 276, DateTimeKind.Utc).AddTicks(5207), new DateTime(2025, 11, 12, 20, 16, 41, 276, DateTimeKind.Utc).AddTicks(5208) }); + + migrationBuilder.UpdateData( + table: "satellites", + keyColumn: "Id", + keyValue: 1, + columns: new[] { "CreatedAt", "LastUpdate" }, + values: new object[] { new DateTime(2025, 11, 12, 20, 16, 41, 276, DateTimeKind.Utc).AddTicks(5387), new DateTime(2025, 11, 12, 20, 16, 41, 276, DateTimeKind.Utc).AddTicks(5388) }); + + migrationBuilder.UpdateData( + table: "satellites", + keyColumn: "Id", + keyValue: 2, + columns: new[] { "CreatedAt", "LastUpdate" }, + values: new object[] { new DateTime(2025, 11, 12, 20, 16, 41, 276, DateTimeKind.Utc).AddTicks(5391), new DateTime(2025, 11, 12, 20, 16, 41, 276, DateTimeKind.Utc).AddTicks(5391) }); + + migrationBuilder.AddForeignKey( + name: "FK_flight_plans_ground_stations_GroundStationId", + table: "flight_plans", + column: "GroundStationId", + principalTable: "ground_stations", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + } + } +} diff --git a/Data/Migrations/SatOpsDbContextModelSnapshot.cs b/Data/Migrations/SatOpsDbContextModelSnapshot.cs index b141fd7..6fd0124 100644 --- a/Data/Migrations/SatOpsDbContextModelSnapshot.cs +++ b/Data/Migrations/SatOpsDbContextModelSnapshot.cs @@ -52,7 +52,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("FailureReason") .HasColumnType("text"); - b.Property("GroundStationId") + b.Property("GroundStationId") .HasColumnType("integer"); b.Property("Name") @@ -207,10 +207,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) { Id = 1, ApiKeyHash = "", - ApplicationId = new Guid("eecf3fa7-de5b-41e0-83ac-ce6d14cd9b86"), - CreatedAt = new DateTime(2025, 11, 12, 20, 16, 41, 276, DateTimeKind.Utc).AddTicks(5207), + ApplicationId = new Guid("c4bd6c61-8e11-43cf-868a-fdd0a4be81b2"), + CreatedAt = new DateTime(2025, 12, 9, 14, 19, 32, 968, DateTimeKind.Utc).AddTicks(9380), Name = "Aarhus", - UpdatedAt = new DateTime(2025, 11, 12, 20, 16, 41, 276, DateTimeKind.Utc).AddTicks(5208) + UpdatedAt = new DateTime(2025, 12, 9, 14, 19, 32, 968, DateTimeKind.Utc).AddTicks(9380) }); }); @@ -322,8 +322,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) new { Id = 1, - CreatedAt = new DateTime(2025, 11, 12, 20, 16, 41, 276, DateTimeKind.Utc).AddTicks(5387), - LastUpdate = new DateTime(2025, 11, 12, 20, 16, 41, 276, DateTimeKind.Utc).AddTicks(5388), + CreatedAt = new DateTime(2025, 12, 9, 14, 19, 32, 968, DateTimeKind.Utc).AddTicks(9710), + LastUpdate = new DateTime(2025, 12, 9, 14, 19, 32, 968, DateTimeKind.Utc).AddTicks(9710), Name = "International Space Station (ISS)", NoradId = 25544, Status = 0, @@ -333,8 +333,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) new { Id = 2, - CreatedAt = new DateTime(2025, 11, 12, 20, 16, 41, 276, DateTimeKind.Utc).AddTicks(5391), - LastUpdate = new DateTime(2025, 11, 12, 20, 16, 41, 276, DateTimeKind.Utc).AddTicks(5391), + CreatedAt = new DateTime(2025, 12, 9, 14, 19, 32, 968, DateTimeKind.Utc).AddTicks(9710), + LastUpdate = new DateTime(2025, 12, 9, 14, 19, 32, 968, DateTimeKind.Utc).AddTicks(9710), Name = "SENTINEL-2C", NoradId = 60989, Status = 0, @@ -404,8 +404,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasOne("SatOps.Modules.Groundstation.GroundStation", "GroundStation") .WithMany("FlightPlans") .HasForeignKey("GroundStationId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); + .OnDelete(DeleteBehavior.SetNull); b.HasOne("SatOps.Modules.FlightPlan.FlightPlan", "PreviousPlan") .WithMany() diff --git a/Data/SatOpsDbContext.cs b/Data/SatOpsDbContext.cs index 5601dba..4a78152 100644 --- a/Data/SatOpsDbContext.cs +++ b/Data/SatOpsDbContext.cs @@ -108,7 +108,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) entity.HasOne(fp => fp.GroundStation) .WithMany(gs => gs.FlightPlans) .HasForeignKey(fp => fp.GroundStationId) - .OnDelete(DeleteBehavior.Restrict); + .OnDelete(DeleteBehavior.SetNull); entity.HasOne(fp => fp.Satellite) .WithMany(s => s.FlightPlans) diff --git a/Modules/FlightPlan/Controller.cs b/Modules/FlightPlan/Controller.cs index a97fafd..4ae8326 100644 --- a/Modules/FlightPlan/Controller.cs +++ b/Modules/FlightPlan/Controller.cs @@ -56,7 +56,13 @@ public async Task> Create([FromBody] CreateFlightPla catch (ArgumentException ex) { logger.LogWarning(ex, "Invalid flight plan JSON: {Message}", ex.Message); - return BadRequest(new { detail = ex.Message }); + return BadRequest(new ProblemDetails + { + Title = "Invalid Flight Plan", + Detail = ex.Message, + Status = StatusCodes.Status400BadRequest, + Instance = HttpContext.Request.Path + }); } } @@ -77,10 +83,12 @@ public async Task> Update( var newVersion = await service.CreateNewVersionAsync(id, input); if (newVersion == null) { - return BadRequest(new + return BadRequest(new ProblemDetails { - detail = "Could not update the flight plan. " + - "It may not be in an updateable state." + Title = "Update Failed", + Detail = "Could not update the flight plan. It may not be in an updateable state.", + Status = StatusCodes.Status400BadRequest, + Instance = HttpContext.Request.Path }); } return Ok(newVersion.ToDto()); @@ -88,7 +96,13 @@ public async Task> Update( catch (ArgumentException ex) { logger.LogWarning(ex, "Invalid flight plan update: {Message}", ex.Message); - return BadRequest(new { detail = ex.Message }); + return BadRequest(new ProblemDetails + { + Title = "Invalid Flight Plan Update", + Detail = ex.Message, + Status = StatusCodes.Status400BadRequest, + Instance = HttpContext.Request.Path + }); } } @@ -105,16 +119,25 @@ public async Task Approve(int id, [FromBody] ApproveFlightPlanDto { if (input.Status != "APPROVED" && input.Status != "REJECTED") { - return BadRequest(new + return BadRequest(new ProblemDetails { - detail = "Invalid status. Must be APPROVED or REJECTED" + Title = "Invalid Status", + Detail = "Invalid status. Must be APPROVED or REJECTED", + Status = StatusCodes.Status400BadRequest, + Instance = HttpContext.Request.Path }); } var (success, message) = await service.ApproveOrRejectAsync(id, input.Status); if (!success) { - return Conflict(new { detail = message }); + return Conflict(new ProblemDetails + { + Title = "Approval Conflict", + Detail = message, + Status = StatusCodes.Status409Conflict, + Instance = HttpContext.Request.Path + }); } return Ok(new { success = true, message }); @@ -135,7 +158,13 @@ public async Task AssignOverpass( var (success, message) = await service.AssignOverpassAsync(id, input); if (!success) { - return Conflict(new { detail = message }); + return Conflict(new ProblemDetails + { + Title = "Assignment Conflict", + Detail = message, + Status = StatusCodes.Status409Conflict, + Instance = HttpContext.Request.Path + }); } return Ok(new { success = true, message }); @@ -161,7 +190,13 @@ public async Task>> CompileFlightPlan(int id) } catch (InvalidOperationException ex) { - return BadRequest(new { detail = ex.Message }); + return BadRequest(new ProblemDetails + { + Title = "Compilation Failed", + Detail = ex.Message, + Status = StatusCodes.Status400BadRequest, + Instance = HttpContext.Request.Path + }); } } diff --git a/Modules/FlightPlan/Dto.cs b/Modules/FlightPlan/Dto.cs index 5069631..5fddbdb 100644 --- a/Modules/FlightPlan/Dto.cs +++ b/Modules/FlightPlan/Dto.cs @@ -6,7 +6,7 @@ public class FlightPlanDto { public int Id { get; set; } public int? PreviousPlanId { get; set; } - public int GsId { get; set; } + public int? GsId { get; set; } public int SatId { get; set; } public int? OverpassId { get; set; } diff --git a/Modules/FlightPlan/Entity.cs b/Modules/FlightPlan/Entity.cs index 74f284f..8267f4d 100644 --- a/Modules/FlightPlan/Entity.cs +++ b/Modules/FlightPlan/Entity.cs @@ -21,7 +21,7 @@ public class FlightPlan public string Name { get; set; } = string.Empty; // --- Foreign Key Properties --- - public int GroundStationId { get; set; } + public int? GroundStationId { get; set; } public int SatelliteId { get; set; } public int? PreviousPlanId { get; set; } public int CreatedById { get; set; } @@ -29,7 +29,7 @@ public class FlightPlan // --- Navigation Properties --- [ForeignKey(nameof(GroundStationId))] - public virtual Groundstation.GroundStation GroundStation { get; set; } = null!; + public virtual Groundstation.GroundStation? GroundStation { get; set; } [ForeignKey(nameof(SatelliteId))] public virtual Satellite.Satellite Satellite { get; set; } = null!; diff --git a/Modules/FlightPlan/SchedulerService.cs b/Modules/FlightPlan/SchedulerService.cs index 71cbbc2..b966cdc 100644 --- a/Modules/FlightPlan/SchedulerService.cs +++ b/Modules/FlightPlan/SchedulerService.cs @@ -25,11 +25,18 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) foreach (var plan in plansToSend) { - if (gatewayService.IsGroundStationConnected(plan.GroundStationId)) + // Skip plans that don't have a ground station assigned + if (!plan.GroundStationId.HasValue) + { + logger.LogWarning("Flight plan {PlanId} has no assigned ground station. Skipping.", plan.Id); + continue; + } + + if (gatewayService.IsGroundStationConnected(plan.GroundStationId.Value)) { try { - logger.LogInformation("Found plan {PlanId} for GS {GSId}, scheduled for {ScheduledTime}. Preparing for transmission.", plan.Id, plan.GroundStationId, plan.ScheduledAt); + logger.LogInformation("Found plan {PlanId} for GS {GSId}, scheduled for {ScheduledTime}. Preparing for transmission.", plan.Id, plan.GroundStationId.Value, plan.ScheduledAt); var satellite = await satelliteService.GetAsync(plan.SatelliteId); if (satellite == null) @@ -45,7 +52,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) var cshScript = await flightPlanService.CompileFlightPlanToCshAsync(plan.Id); await gatewayService.SendScheduledCommand( - plan.GroundStationId, + plan.GroundStationId.Value, plan.SatelliteId, satellite.Name, plan.Id, @@ -55,18 +62,18 @@ await gatewayService.SendScheduledCommand( // Update status to prevent re-sending. await flightPlanService.UpdateFlightPlanStatusAsync(plan.Id, FlightPlanStatus.Transmitted); - logger.LogInformation("Successfully transmitted flight plan {PlanId} to Ground Station {GSId}.", plan.Id, plan.GroundStationId); + logger.LogInformation("Successfully transmitted flight plan {PlanId} to Ground Station {GSId}.", plan.Id, plan.GroundStationId.Value); } catch (Exception ex) { var reason = $"An exception occurred during transmission: {ex.Message}"; - logger.LogError(ex, "Failed to transmit flight plan {PlanId} to GS {GSId}", plan.Id, plan.GroundStationId); + logger.LogError(ex, "Failed to transmit flight plan {PlanId} to GS {GSId}", plan.Id, plan.GroundStationId.Value); await flightPlanService.UpdateFlightPlanStatusAsync(plan.Id, FlightPlanStatus.Failed, reason); } } else { - var reason = $"Ground Station {plan.GroundStationId} is not connected."; + var reason = $"Ground Station {plan.GroundStationId.Value} is not connected."; logger.LogWarning("Cannot transmit flight plan {PlanId}: {Reason}", plan.Id, reason); if (plan.ScheduledAt.HasValue && (plan.ScheduledAt.Value - DateTime.UtcNow) < TimeSpan.FromMinutes(1)) diff --git a/Modules/FlightPlan/Service.cs b/Modules/FlightPlan/Service.cs index 64621c2..b8d0bb8 100644 --- a/Modules/FlightPlan/Service.cs +++ b/Modules/FlightPlan/Service.cs @@ -159,6 +159,11 @@ public async Task CreateAsync(CreateFlightPlanDto createDto) return (false, $"Flight plan must be in APPROVED status to assign an overpass. Current: {flightPlan.Status}"); } + if (!flightPlan.GroundStationId.HasValue) + { + return (false, "Flight plan has no assigned ground station."); + } + var satellite = await satelliteService.GetAsync(flightPlan.SatelliteId); if (satellite == null) return (false, "Associated satellite not found."); @@ -169,7 +174,7 @@ public async Task CreateAsync(CreateFlightPlanDto createDto) var availableOverpasses = await overpassService.CalculateOverpassesAsync(new OverpassWindowsCalculationRequestDto { SatelliteId = flightPlan.SatelliteId, - GroundStationId = flightPlan.GroundStationId, + GroundStationId = flightPlan.GroundStationId.Value, StartTime = expandedStartTime, EndTime = expandedEndTime, }); diff --git a/Modules/Overpass/Controller.cs b/Modules/Overpass/Controller.cs index 1a136fb..4ec9de1 100644 --- a/Modules/Overpass/Controller.cs +++ b/Modules/Overpass/Controller.cs @@ -61,15 +61,33 @@ public async Task>> GetOverpassWindows( } catch (ArgumentException ex) { - return BadRequest(ex.Message); + return BadRequest(new ProblemDetails + { + Title = "Invalid Request", + Detail = ex.Message, + Status = StatusCodes.Status400BadRequest, + Instance = HttpContext.Request.Path + }); } catch (InvalidOperationException ex) { - return StatusCode(500, ex.Message); + return StatusCode(StatusCodes.Status500InternalServerError, new ProblemDetails + { + Title = "Internal Server Error", + Detail = ex.Message, + Status = StatusCodes.Status500InternalServerError, + Instance = HttpContext.Request.Path + }); } catch (Exception ex) { - return StatusCode(500, $"An unexpected error occurred: {ex.Message}"); + return StatusCode(StatusCodes.Status500InternalServerError, new ProblemDetails + { + Title = "Unexpected Error", + Detail = $"An unexpected error occurred: {ex.Message}", + Status = StatusCodes.Status500InternalServerError, + Instance = HttpContext.Request.Path + }); } } } diff --git a/diagrams/commandClassDiagram.mermaid b/diagrams/commandClassDiagram.mermaid index ececadb..6cafbd8 100644 --- a/diagrams/commandClassDiagram.mermaid +++ b/diagrams/commandClassDiagram.mermaid @@ -4,24 +4,33 @@ classDiagram +string CommandType* +DateTime? ExecutionTime +bool RequiresExecutionTimeCalculation - +Validate() IEnumerable~ValidationResult~ + +Validate() IEnumerable~ValidationResult~* +CompileToCsh()* Task~List~string~~ } class TriggerCaptureCommand { +CommandType = "TRIGGER_CAPTURE" +RequiresExecutionTimeCalculation = true + -int CameraControllerNode = 5422 +CaptureLocation? CaptureLocation +CameraSettings? CameraSettings + +Validate() IEnumerable~ValidationResult~ +CompileToCsh() Task~List~string~~ } class TriggerPipelineCommand { +CommandType = "TRIGGER_PIPELINE" + -int DippNode = 5423 +int? Mode +CompileToCsh() Task~List~string~~ } + class ConfigureSOMCommand { + +CommandType = "CONFIGURE_SOM" + -int AppSysNode = 5421 + +CompileToCsh() Task~List~string~~ + } + class CaptureLocation { +double Latitude +double Longitude @@ -40,13 +49,14 @@ classDiagram class CameraType { <> - VMB - IR - Test + VMB = 0 + IR = 1 + Test = 2 } Command <|-- TriggerCaptureCommand : inherits Command <|-- TriggerPipelineCommand : inherits + Command <|-- ConfigureSOMCommand : inherits TriggerCaptureCommand --> CaptureLocation : uses TriggerCaptureCommand --> CameraSettings : uses - CameraSettings --> CameraType : uses + CameraSettings --> CameraType : uses \ No newline at end of file diff --git a/diagrams/overpassCalculation.mermaid b/diagrams/overpassCalculation.mermaid new file mode 100644 index 0000000..8c1c6ed --- /dev/null +++ b/diagrams/overpassCalculation.mermaid @@ -0,0 +1,65 @@ +sequenceDiagram + participant Client + participant CalculateOverpasses + participant Repository + participant SatelliteService + participant GroundStationService + participant SGPdotNET + participant MergeAndEnrich + + Client->>CalculateOverpasses: CalculateOverpassesAsync(request) + + Note over CalculateOverpasses: Check for stored overpasses + CalculateOverpasses->>Repository: FindStoredOverpassesInTimeRange() + Repository-->>CalculateOverpasses: storedOverpasses + + Note over CalculateOverpasses: Fetch satellite data + CalculateOverpasses->>SatelliteService: GetAsync(satelliteId) + SatelliteService-->>CalculateOverpasses: satellite (with TLE data) + + Note over CalculateOverpasses: Fetch ground station data + CalculateOverpasses->>GroundStationService: GetAsync(groundStationId) + GroundStationService-->>CalculateOverpasses: groundStation (with location) + + Note over CalculateOverpasses: Initialize SGP4 propagator + CalculateOverpasses->>SGPdotNET: new Tle(name, line1, line2) + SGPdotNET-->>CalculateOverpasses: tle + CalculateOverpasses->>SGPdotNET: new Satellite(tle) + SGPdotNET-->>CalculateOverpasses: sat + CalculateOverpasses->>SGPdotNET: new GroundStation(location) + SGPdotNET-->>CalculateOverpasses: groundStation + + Note over CalculateOverpasses: Time-stepping loop + loop For each time step (1 minute) + CalculateOverpasses->>SGPdotNET: groundStation.Observe(sat, currentTime) + SGPdotNET-->>CalculateOverpasses: observation (elevation, azimuth) + + alt Elevation >= MinimumElevation && not in overpass + Note over CalculateOverpasses: Start new overpass
Record start time, azimuth
Track max elevation + else Elevation >= MinimumElevation && in overpass + Note over CalculateOverpasses: Continue overpass
Update max elevation if higher + else Elevation < MinimumElevation && in overpass + Note over CalculateOverpasses: End overpass
Calculate duration
Check minimum duration filter + alt Duration meets minimum + Note over CalculateOverpasses: Add to overpassWindows list + end + alt MaxResults reached + Note over CalculateOverpasses: Break loop + end + end + end + + Note over CalculateOverpasses: Merge calculated with stored data + CalculateOverpasses->>MergeAndEnrich: MergeAndEnrichOverpasses(calculated, stored) + + loop For each calculated overpass + MergeAndEnrich->>MergeAndEnrich: Find matching stored overpass
(tolerance: 10 minutes) + alt Match found + MergeAndEnrich->>Repository: GetAssociatedFlightPlanAsync(overpassId) + Repository-->>MergeAndEnrich: flightPlan + Note over MergeAndEnrich: Enrich with flight plan details + end + end + + MergeAndEnrich-->>CalculateOverpasses: mergedOverpasses (sorted by start time) + CalculateOverpasses-->>Client: List diff --git a/tests/SatOps.Tests/FlightPlan/FlightPlanServiceTests.cs b/tests/SatOps.Tests/FlightPlan/FlightPlanServiceTests.cs index 1d6a80e..a7ef651 100644 --- a/tests/SatOps.Tests/FlightPlan/FlightPlanServiceTests.cs +++ b/tests/SatOps.Tests/FlightPlan/FlightPlanServiceTests.cs @@ -248,6 +248,7 @@ public async Task AssignOverpassAsync_WhenConflictingPlanExists_ReturnsConflictE Id = planId, Status = FlightPlanStatus.Approved, SatelliteId = satId, + GroundStationId = 1, Name = "New Plan" };