Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;

#nullable disable

namespace SatOps.Data.migrations
{
/// <inheritdoc />
public partial class AllowNullableGroundStationIdInFlightPlans : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_flight_plans_ground_stations_GroundStationId",
table: "flight_plans");

migrationBuilder.AlterColumn<int>(
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);
}

/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_flight_plans_ground_stations_GroundStationId",
table: "flight_plans");

migrationBuilder.AlterColumn<int>(
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);
}
}
}
19 changes: 9 additions & 10 deletions Data/Migrations/SatOpsDbContextModelSnapshot.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.Property<string>("FailureReason")
.HasColumnType("text");

b.Property<int>("GroundStationId")
b.Property<int?>("GroundStationId")
.HasColumnType("integer");

b.Property<string>("Name")
Expand Down Expand Up @@ -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)
});
});

Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion Data/SatOpsDbContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
55 changes: 45 additions & 10 deletions Modules/FlightPlan/Controller.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,13 @@ public async Task<ActionResult<FlightPlanDto>> 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
});
}
}

Expand All @@ -77,18 +83,26 @@ public async Task<ActionResult<FlightPlanDto>> 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());
}
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
});
}
}

Expand All @@ -105,16 +119,25 @@ public async Task<ActionResult> 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 });
Expand All @@ -135,7 +158,13 @@ public async Task<ActionResult> 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 });
Expand All @@ -161,7 +190,13 @@ public async Task<ActionResult<List<string>>> 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
});
}
}

Expand Down
2 changes: 1 addition & 1 deletion Modules/FlightPlan/Dto.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }

Expand Down
4 changes: 2 additions & 2 deletions Modules/FlightPlan/Entity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,15 @@ 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; }
public int? ApprovedById { get; set; }

// --- 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!;
Expand Down
19 changes: 13 additions & 6 deletions Modules/FlightPlan/SchedulerService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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,
Expand All @@ -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))
Expand Down
7 changes: 6 additions & 1 deletion Modules/FlightPlan/Service.cs
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,11 @@ public async Task<FlightPlan> 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.");

Expand All @@ -169,7 +174,7 @@ public async Task<FlightPlan> CreateAsync(CreateFlightPlanDto createDto)
var availableOverpasses = await overpassService.CalculateOverpassesAsync(new OverpassWindowsCalculationRequestDto
{
SatelliteId = flightPlan.SatelliteId,
GroundStationId = flightPlan.GroundStationId,
GroundStationId = flightPlan.GroundStationId.Value,
StartTime = expandedStartTime,
EndTime = expandedEndTime,
});
Expand Down
Loading
Loading