Files
Extrudex/backend/API/Controllers/PrintersController.cs
cubecraft-agents[bot] 230c3b295d initial commit
2026-04-25 18:51:05 +00:00

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
};
}