Initial Commit

This commit is contained in:
Denis Urs Rudolph
2025-12-11 21:38:33 +01:00
commit 3d4dec79a9
19 changed files with 1121 additions and 0 deletions

View File

@@ -0,0 +1,141 @@
using Microsoft.EntityFrameworkCore;
using OtaFleet.Api.Data;
using OtaFleet.Api.Data.Entities;
namespace OtaFleet.Api.Endpoints;
public static class AdminEndpoints
{
public static void MapAdminEndpoints(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/api/admin");
// Vehicles
group.MapGet("/vehicles", async (OtaDbContext db) =>
{
return await db.Vehicles.Include(v => v.Group).ToListAsync();
});
// Groups
group.MapGet("/groups", async (OtaDbContext db) =>
{
return await db.VehicleGroups.Include(g => g.Vehicles).ToListAsync();
});
group.MapPost("/groups", async (OtaDbContext db, CreateGroupRequest request) =>
{
var group = new VehicleGroup
{
Name = request.Name,
Description = request.Description
};
db.VehicleGroups.Add(group);
await db.SaveChangesAsync();
return Results.Created($"/api/admin/groups/{group.Id}", group);
});
group.MapPut("/groups/{id}", async (OtaDbContext db, int id, UpdateGroupRequest request) =>
{
var group = await db.VehicleGroups.FindAsync(id);
if (group == null) return Results.NotFound();
group.Name = request.Name;
group.Description = request.Description;
await db.SaveChangesAsync();
return Results.Ok(group);
});
group.MapPost("/groups/{groupId}/vehicles", async (OtaDbContext db, int groupId, AssignVehicleRequest request) =>
{
var vehicle = await db.Vehicles.FindAsync(request.Vin);
if (vehicle == null) return Results.NotFound("Vehicle not found");
var group = await db.VehicleGroups.FindAsync(groupId);
if (group == null) return Results.NotFound("Group not found");
vehicle.GroupId = groupId;
await db.SaveChangesAsync();
return Results.Ok();
});
// Updates
group.MapPost("/updates", async (OtaDbContext db, HttpRequest request) =>
{
if (!request.HasFormContentType)
return Results.BadRequest("Expected multipart/form-data");
var form = await request.ReadFormAsync();
var file = form.Files["file"];
var version = form["version"];
var description = form["description"];
if (file == null || file.Length == 0)
return Results.BadRequest("No file uploaded");
if (string.IsNullOrEmpty(version))
return Results.BadRequest("Version is required");
// Save file
var uploadsDir = Path.Combine(Directory.GetCurrentDirectory(), "uploads");
Directory.CreateDirectory(uploadsDir);
var filePath = Path.Combine(uploadsDir, $"{version}_{file.FileName}");
using (var stream = new FileStream(filePath, FileMode.Create))
{
await file.CopyToAsync(stream);
}
var update = new FirmwareUpdate
{
Version = version!,
Description = description,
FilePath = filePath,
UploadedAt = DateTime.UtcNow
};
db.FirmwareUpdates.Add(update);
await db.SaveChangesAsync();
return Results.Ok(update);
});
group.MapGet("/updates", async (OtaDbContext db) =>
{
return await db.FirmwareUpdates.OrderByDescending(u => u.UploadedAt).ToListAsync();
});
// Deployments
group.MapPost("/deployments", async (OtaDbContext db, CreateDeploymentRequest request) =>
{
var update = await db.FirmwareUpdates.FindAsync(request.UpdateId);
if (update == null) return Results.NotFound("Update not found");
var deployment = new Deployment
{
UpdateId = request.UpdateId,
TargetVin = request.TargetVin,
TargetGroupId = request.TargetGroupId,
Status = "Pending",
CreatedAt = DateTime.UtcNow
};
db.Deployments.Add(deployment);
await db.SaveChangesAsync();
return Results.Created($"/api/admin/deployments/{deployment.Id}", deployment);
});
group.MapGet("/deployments", async (OtaDbContext db) =>
{
return await db.Deployments
.Include(d => d.Update)
.OrderByDescending(d => d.CreatedAt)
.ToListAsync();
});
}
}
public record CreateGroupRequest(string Name, string? Description);
public record UpdateGroupRequest(string Name, string? Description);
public record AssignVehicleRequest(string Vin);
public record CreateDeploymentRequest(int UpdateId, string? TargetVin, int? TargetGroupId);

View File

@@ -0,0 +1,98 @@
using Microsoft.EntityFrameworkCore;
using OtaFleet.Api.Data;
using OtaFleet.Api.Data.Entities;
namespace OtaFleet.Api.Endpoints;
public static class VehicleEndpoints
{
public static void MapVehicleEndpoints(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/api/vehicles");
group.MapPost("/register", async (OtaDbContext db, RegisterVehicleRequest request) =>
{
var vehicle = await db.Vehicles.FindAsync(request.Vin);
if (vehicle == null)
{
vehicle = new Vehicle
{
Vin = request.Vin,
Status = "Online",
CurrentVersion = request.CurrentVersion,
LastHeartbeat = DateTime.UtcNow
};
db.Vehicles.Add(vehicle);
}
else
{
vehicle.Status = "Online";
vehicle.CurrentVersion = request.CurrentVersion;
vehicle.LastHeartbeat = DateTime.UtcNow;
}
await db.SaveChangesAsync();
return Results.Ok(vehicle);
});
group.MapPost("/heartbeat", async (OtaDbContext db, HeartbeatRequest request) =>
{
var vehicle = await db.Vehicles.FindAsync(request.Vin);
if (vehicle == null) return Results.NotFound();
vehicle.LastHeartbeat = DateTime.UtcNow;
vehicle.Status = request.Status;
await db.SaveChangesAsync();
return Results.Ok();
});
group.MapGet("/updates", async (OtaDbContext db, string vin) =>
{
var vehicle = await db.Vehicles.Include(v => v.Group).FirstOrDefaultAsync(v => v.Vin == vin);
if (vehicle == null) return Results.NotFound();
// Find deployment for this vehicle or its group
var deployment = await db.Deployments
.Include(d => d.Update)
.Where(d => (d.TargetVin == vin || (vehicle.GroupId.HasValue && d.TargetGroupId == vehicle.GroupId)) && d.Status != "Completed") // Simple logic, can be improved
.OrderByDescending(d => d.CreatedAt)
.FirstOrDefaultAsync();
if (deployment == null) return Results.NoContent();
// Check if vehicle already has this version
if (deployment.Update.Version == vehicle.CurrentVersion) return Results.NoContent();
return Results.Ok(new VehicleUpdateResponse
{
UpdateId = deployment.Update.Id,
Version = deployment.Update.Version,
Description = deployment.Update.Description,
DownloadUrl = $"/api/vehicles/updates/{deployment.Update.Id}/download"
});
});
group.MapGet("/updates/{id}/download", async (OtaDbContext db, int id) =>
{
var update = await db.FirmwareUpdates.FindAsync(id);
if (update == null) return Results.NotFound();
if (!File.Exists(update.FilePath)) return Results.NotFound("File not found on server");
var fileName = Path.GetFileName(update.FilePath);
var mimeType = "application/octet-stream";
return Results.File(update.FilePath, mimeType, fileName);
});
}
}
public record RegisterVehicleRequest(string Vin, string CurrentVersion);
public record HeartbeatRequest(string Vin, string Status);
public record VehicleUpdateResponse
{
public int UpdateId { get; set; }
public string Version { get; set; } = string.Empty;
public string? Description { get; set; }
public string DownloadUrl { get; set; } = string.Empty;
}