297 lines
12 KiB
C#
297 lines
12 KiB
C#
using Extrudex.API.DTOs.Printers;
|
|
using Extrudex.Domain.Entities;
|
|
using Extrudex.Domain.Enums;
|
|
using Extrudex.Infrastructure.Data;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace Extrudex.API.Controllers;
|
|
|
|
/// <summary>
|
|
/// Controller for managing 3D printers in the fleet.
|
|
/// Provides CRUD operations, a lightweight status endpoint, and soft-delete.
|
|
/// Real-time printer status is also available via the SignalR PrinterHub.
|
|
/// </summary>
|
|
[ApiController]
|
|
[Route("api/printers")]
|
|
public class PrintersController : ControllerBase
|
|
{
|
|
private readonly ExtrudexDbContext _dbContext;
|
|
private readonly ILogger<PrintersController> _logger;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="PrintersController"/> class.
|
|
/// </summary>
|
|
/// <param name="dbContext">The database context for data access.</param>
|
|
/// <param name="logger">The logger for diagnostic output.</param>
|
|
public PrintersController(ExtrudexDbContext dbContext, ILogger<PrintersController> logger)
|
|
{
|
|
_dbContext = dbContext;
|
|
_logger = logger;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets all printers, optionally filtered by active status.
|
|
/// Results are ordered by printer name alphabetically.
|
|
/// </summary>
|
|
/// <param name="isActive">
|
|
/// Optional filter: <c>true</c> for active printers only,
|
|
/// <c>false</c> for inactive (soft-deleted) printers,
|
|
/// omit for all printers.
|
|
/// </param>
|
|
/// <returns>A list of printers matching the filter criteria.</returns>
|
|
/// <response code="200">Returns the list of printers.</response>
|
|
[HttpGet]
|
|
[ProducesResponseType(typeof(IEnumerable<PrinterResponse>), StatusCodes.Status200OK)]
|
|
public async Task<ActionResult<IEnumerable<PrinterResponse>>> GetPrinters(
|
|
[FromQuery] bool? isActive = null)
|
|
{
|
|
_logger.LogDebug("Getting printers, isActive filter: {IsActive}", isActive);
|
|
|
|
var query = _dbContext.Printers.AsQueryable();
|
|
|
|
if (isActive.HasValue)
|
|
query = query.Where(p => p.IsActive == isActive.Value);
|
|
|
|
var printers = await query
|
|
.OrderBy(p => p.Name)
|
|
.Select(p => MapToPrinterResponse(p))
|
|
.ToListAsync();
|
|
|
|
return Ok(printers);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a specific printer by its unique identifier.
|
|
/// </summary>
|
|
/// <param name="id">The unique identifier of the printer.</param>
|
|
/// <returns>The full printer details including connection configuration.</returns>
|
|
/// <response code="200">Returns the printer details.</response>
|
|
/// <response code="404">The printer was not found.</response>
|
|
[HttpGet("{id:guid}")]
|
|
[ProducesResponseType(typeof(PrinterResponse), StatusCodes.Status200OK)]
|
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
|
public async Task<ActionResult<PrinterResponse>> GetPrinter(Guid id)
|
|
{
|
|
_logger.LogDebug("Getting printer {Id}", id);
|
|
|
|
var printer = await _dbContext.Printers.FindAsync(id);
|
|
if (printer is null)
|
|
{
|
|
_logger.LogWarning("Printer {Id} not found", id);
|
|
return NotFound(new { error = $"Printer with ID '{id}' not found." });
|
|
}
|
|
|
|
return Ok(MapToPrinterResponse(printer));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the real-time connection/health status of a specific printer.
|
|
/// This is a lightweight endpoint suitable for polling or dashboard displays.
|
|
/// For push-based real-time updates, subscribe to the SignalR PrinterHub instead.
|
|
/// </summary>
|
|
/// <param name="id">The unique identifier of the printer.</param>
|
|
/// <returns>The printer's current status, last-seen timestamp, and active flag.</returns>
|
|
/// <response code="200">Returns the printer status.</response>
|
|
/// <response code="404">The printer was not found.</response>
|
|
[HttpGet("{id:guid}/status")]
|
|
[ProducesResponseType(typeof(PrinterStatusResponse), StatusCodes.Status200OK)]
|
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
|
public async Task<ActionResult<PrinterStatusResponse>> GetPrinterStatus(Guid id)
|
|
{
|
|
_logger.LogDebug("Getting status for printer {Id}", id);
|
|
|
|
var printer = await _dbContext.Printers.FindAsync(id);
|
|
if (printer is null)
|
|
{
|
|
_logger.LogWarning("Printer {Id} not found for status check", id);
|
|
return NotFound(new { error = $"Printer with ID '{id}' not found." });
|
|
}
|
|
|
|
return Ok(new PrinterStatusResponse
|
|
{
|
|
Id = printer.Id,
|
|
Name = printer.Name,
|
|
Status = printer.Status.ToString(),
|
|
LastSeenAt = printer.LastSeenAt,
|
|
IsActive = printer.IsActive
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// Registers a new printer in the fleet.
|
|
/// The printer is created with an initial status of Offline.
|
|
/// The port defaults to 8883 for MQTT or 7125 for Moonraker if not specified.
|
|
/// </summary>
|
|
/// <param name="request">The printer registration request with connection details.</param>
|
|
/// <returns>The newly created printer.</returns>
|
|
/// <response code="201">The printer was created successfully.</response>
|
|
/// <response code="400">The request body is invalid or validation failed.</response>
|
|
[HttpPost]
|
|
[ProducesResponseType(typeof(PrinterResponse), StatusCodes.Status201Created)]
|
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
|
public async Task<ActionResult<PrinterResponse>> CreatePrinter(
|
|
[FromBody] CreatePrinterRequest request)
|
|
{
|
|
if (!ModelState.IsValid)
|
|
return BadRequest(ModelState);
|
|
|
|
_logger.LogInformation("Creating printer: {Name} ({Manufacturer} {Model})",
|
|
request.Name, request.Manufacturer, request.Model);
|
|
|
|
// Parse enums from string values
|
|
if (!Enum.TryParse<PrinterType>(request.PrinterType, true, out var printerType))
|
|
{
|
|
return BadRequest(new { error = $"Invalid PrinterType: '{request.PrinterType}'. Must be 'Fdm' or 'Resin'." });
|
|
}
|
|
|
|
if (!Enum.TryParse<ConnectionType>(request.ConnectionType, true, out var connectionType))
|
|
{
|
|
return BadRequest(new { error = $"Invalid ConnectionType: '{request.ConnectionType}'. Must be 'Mqtt' or 'Moonraker'." });
|
|
}
|
|
|
|
// Default port based on connection type
|
|
var port = request.Port > 0
|
|
? request.Port
|
|
: connectionType == ConnectionType.Mqtt ? 8883 : 7125;
|
|
|
|
var entity = new Printer
|
|
{
|
|
Name = request.Name,
|
|
Manufacturer = request.Manufacturer,
|
|
Model = request.Model,
|
|
PrinterType = printerType,
|
|
ConnectionType = connectionType,
|
|
HostnameOrIp = request.HostnameOrIp,
|
|
Port = port,
|
|
MqttUsername = request.MqttUsername,
|
|
MqttPassword = request.MqttPassword,
|
|
MqttUseTls = request.MqttUseTls,
|
|
ApiKey = request.ApiKey,
|
|
IsActive = request.IsActive,
|
|
Status = PrinterStatus.Offline
|
|
};
|
|
|
|
_dbContext.Printers.Add(entity);
|
|
await _dbContext.SaveChangesAsync();
|
|
|
|
var response = MapToPrinterResponse(entity);
|
|
return CreatedAtAction(nameof(GetPrinter), new { id = entity.Id }, response);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates an existing printer's configuration and connection info.
|
|
/// Uses full replacement semantics — all fields are written.
|
|
/// The port defaults to 8883 for MQTT or 7125 for Moonraker if not specified.
|
|
/// </summary>
|
|
/// <param name="id">The unique identifier of the printer to update.</param>
|
|
/// <param name="request">The printer update request with new values.</param>
|
|
/// <returns>The updated printer.</returns>
|
|
/// <response code="200">The printer was updated successfully.</response>
|
|
/// <response code="400">The request body is invalid or validation failed.</response>
|
|
/// <response code="404">The printer was not found.</response>
|
|
[HttpPut("{id:guid}")]
|
|
[ProducesResponseType(typeof(PrinterResponse), StatusCodes.Status200OK)]
|
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
|
public async Task<ActionResult<PrinterResponse>> UpdatePrinter(
|
|
Guid id, [FromBody] UpdatePrinterRequest request)
|
|
{
|
|
if (!ModelState.IsValid)
|
|
return BadRequest(ModelState);
|
|
|
|
_logger.LogInformation("Updating printer {Id}", id);
|
|
|
|
var entity = await _dbContext.Printers.FindAsync(id);
|
|
if (entity is null)
|
|
{
|
|
_logger.LogWarning("Printer {Id} not found for update", id);
|
|
return NotFound(new { error = $"Printer with ID '{id}' not found." });
|
|
}
|
|
|
|
// Parse enums from string values
|
|
if (!Enum.TryParse<PrinterType>(request.PrinterType, true, out var printerType))
|
|
{
|
|
return BadRequest(new { error = $"Invalid PrinterType: '{request.PrinterType}'. Must be 'Fdm' or 'Resin'." });
|
|
}
|
|
|
|
if (!Enum.TryParse<ConnectionType>(request.ConnectionType, true, out var connectionType))
|
|
{
|
|
return BadRequest(new { error = $"Invalid ConnectionType: '{request.ConnectionType}'. Must be 'Mqtt' or 'Moonraker'." });
|
|
}
|
|
|
|
var port = request.Port > 0
|
|
? request.Port
|
|
: connectionType == ConnectionType.Mqtt ? 8883 : 7125;
|
|
|
|
entity.Name = request.Name;
|
|
entity.Manufacturer = request.Manufacturer;
|
|
entity.Model = request.Model;
|
|
entity.PrinterType = printerType;
|
|
entity.ConnectionType = connectionType;
|
|
entity.HostnameOrIp = request.HostnameOrIp;
|
|
entity.Port = port;
|
|
entity.MqttUsername = request.MqttUsername;
|
|
entity.MqttPassword = request.MqttPassword;
|
|
entity.MqttUseTls = request.MqttUseTls;
|
|
entity.ApiKey = request.ApiKey;
|
|
entity.IsActive = request.IsActive;
|
|
|
|
await _dbContext.SaveChangesAsync();
|
|
|
|
return Ok(MapToPrinterResponse(entity));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Soft-deletes a printer by setting <c>IsActive</c> to <c>false</c>.
|
|
/// The printer record is retained for historical and audit purposes.
|
|
/// Soft-deleted printers can be recovered via PUT with <c>IsActive = true</c>.
|
|
/// </summary>
|
|
/// <param name="id">The unique identifier of the printer to soft-delete.</param>
|
|
/// <returns>No content on success.</returns>
|
|
/// <response code="204">The printer was soft-deleted successfully.</response>
|
|
/// <response code="404">The printer was not found.</response>
|
|
[HttpDelete("{id:guid}")]
|
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
|
public async Task<IActionResult> DeletePrinter(Guid id)
|
|
{
|
|
_logger.LogInformation("Soft-deleting printer {Id}", id);
|
|
|
|
var entity = await _dbContext.Printers.FindAsync(id);
|
|
if (entity is null)
|
|
{
|
|
_logger.LogWarning("Printer {Id} not found for soft-delete", id);
|
|
return NotFound(new { error = $"Printer with ID '{id}' not found." });
|
|
}
|
|
|
|
entity.IsActive = false;
|
|
await _dbContext.SaveChangesAsync();
|
|
|
|
return NoContent();
|
|
}
|
|
|
|
// ── Mapping helper ───────────────────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// Maps a <see cref="Printer"/> domain entity to a <see cref="PrinterResponse"/> DTO.
|
|
/// </summary>
|
|
/// <param name="p">The printer entity to map.</param>
|
|
/// <returns>A response DTO suitable for API output.</returns>
|
|
private static PrinterResponse MapToPrinterResponse(Printer p) => new()
|
|
{
|
|
Id = p.Id,
|
|
Status = p.Status.ToString(),
|
|
Name = p.Name,
|
|
Manufacturer = p.Manufacturer,
|
|
Model = p.Model,
|
|
PrinterType = p.PrinterType.ToString(),
|
|
ConnectionType = p.ConnectionType.ToString(),
|
|
HostnameOrIp = p.HostnameOrIp,
|
|
Port = p.Port,
|
|
IsActive = p.IsActive,
|
|
LastSeenAt = p.LastSeenAt,
|
|
CreatedAt = p.CreatedAt,
|
|
UpdatedAt = p.UpdatedAt
|
|
};
|
|
} |