Slice A of the async-op API contract normalization.
MaintenanceController:
- EmptyTrash error path: was 500 text/plain (error.ToString()); now maps the
BaseError Left through ApiResults.ToErrorResult() -> 404 (NotFoundError) / 422
ProblemDetails. Success stays 200 OkResult. Added ProducesResponseType 200 + 422.
- CleanArtwork: fire-and-forget enqueue of DeleteOrphanedArtwork was a silent 200;
now returns 202 Accepted (AcceptedResult) since it queues background work.
Added ProducesResponseType 202. (Controller does not derive from ControllerBase,
so results are built directly as before.)
TroubleshootController.TroubleshootPlayback (GET|HEAD /api/troubleshoot/playback.m3u8):
- Two bare body-less NotFound() call sites conflated "not found" with "prepare/
playback failure". Both now return a ProblemDetails body:
* prepare-failure (result.IsLeft): mapped through error.ToErrorResult() -> 404 for
NotFoundError (unknown media item/channel) else 422 for a validation BaseError.
* terminal fall-through (prepare ok but no playable output): kept 404 with a
distinguishing ApiResults.NotFoundProblem(...) detail.
- Added ProducesResponseType 404 + 422 (409 already present).
Consumer check: the SPA (PlaybackTroubleshootingScreen) feeds the playback.m3u8 URL
straight to hls.js via HlsPlayer, which never inspects the HTTP status code — playback
state is surfaced via the separate /api/troubleshoot/playback/status poll. So the
404->422 split for the validation subcase is safe; no player code branches on the
status code.
Tests: MaintenanceControllerTests (200/422/202 + enqueue assertion),
TroubleshootControllerTests (prepare 404 NotFoundError, 422 validation). All green;
Api error-metadata/contract/security scans still pass.
Note: OpenAPI artifacts (v1.json / v1.d.ts) intentionally NOT regenerated here — the
orchestrator regenerates once after all #235 slices merge.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
50 lines
1.6 KiB
C#
50 lines
1.6 KiB
C#
using System.Threading.Channels;
|
|
using ErsatzTV.Application;
|
|
using ErsatzTV.Application.Maintenance;
|
|
using ErsatzTV.Core;
|
|
using ErsatzTV.Extensions;
|
|
using MediatR;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
|
|
namespace ErsatzTV.Controllers.Api;
|
|
|
|
[ApiController]
|
|
[EndpointGroupName("general")]
|
|
public class MaintenanceController(IMediator mediator, ChannelWriter<IBackgroundServiceRequest> workerChannel)
|
|
{
|
|
[HttpGet("/api/maintenance/gc")]
|
|
[Tags("Maintenance")]
|
|
[EndpointSummary("Garbage collect")]
|
|
public async Task<IActionResult> GarbageCollection([FromQuery] bool force = false)
|
|
{
|
|
await mediator.Send(new ReleaseMemory(force));
|
|
return new OkResult();
|
|
}
|
|
|
|
[HttpPost("/api/maintenance/empty_trash")]
|
|
[Tags("Maintenance")]
|
|
[EndpointSummary("Empty trash")]
|
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
|
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status422UnprocessableEntity)]
|
|
public async Task<IActionResult> EmptyTrash()
|
|
{
|
|
Either<BaseError, Unit> result = await mediator.Send(new EmptyTrash());
|
|
foreach (BaseError error in result.LeftToSeq())
|
|
{
|
|
return error.ToErrorResult();
|
|
}
|
|
|
|
return new OkResult();
|
|
}
|
|
|
|
[HttpPost("/api/maintenance/clean_artwork")]
|
|
[Tags("Maintenance")]
|
|
[EndpointSummary("Clean artwork cache")]
|
|
[ProducesResponseType(StatusCodes.Status202Accepted)]
|
|
public async Task<IActionResult> CleanArtwork(CancellationToken cancellationToken)
|
|
{
|
|
await workerChannel.WriteAsync(new DeleteOrphanedArtwork(), cancellationToken);
|
|
return new AcceptedResult();
|
|
}
|
|
}
|