feat(api): block-playout history + sequential-schedule validation endpoints

Adds the REST surface for the #145 troubleshooting leftovers (#158 items 4-5):

- GET /api/playouts/{id}/blocks - blocks a block playout schedules
- GET /api/playouts/{id}/blocks/{blockId}/history - paged block history
- GET /api/playouts/history/{id} - decode a history row by id
- POST /api/troubleshoot/validate-schedule - validate sequential YAML

New MediatR queries GetPlayoutHistoryDetails (Either, 404 unknown row /
422 malformed JSON) and ValidateSequentialSchedule (wraps
ISequentialScheduleValidator, never throws). DecodePlayoutHistoryHandler
and the new by-id handler now share PlayoutHistoryDecoder. Regenerated
openapi/v1.json.

Refs #145 #158

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-07 22:10:37 +02:00
co-authored by Claude Fable 5
parent e58da21a56
commit f0a423c2de
15 changed files with 777 additions and 81 deletions
@@ -1,3 +1,4 @@
using System.ComponentModel.DataAnnotations;
using System.IO.Abstractions;
using System.Text.Json;
using System.Text.Json.Serialization;
@@ -6,6 +7,7 @@ using ErsatzTV.Application;
using ErsatzTV.Application.MediaItems;
using ErsatzTV.Application.Troubleshooting;
using ErsatzTV.Application.Troubleshooting.Queries;
using ErsatzTV.Controllers.Api.Requests;
using ErsatzTV.Core;
using ErsatzTV.Core.Api.Troubleshooting;
using ErsatzTV.Core.Domain;
@@ -71,6 +73,36 @@ public class TroubleshootController(
info.VideoToolboxCapabilities);
}
[HttpPost("api/troubleshoot/validate-schedule", Name = "ValidateSequentialSchedule")]
[Tags("Troubleshooting")]
[EndpointSummary("Validate a sequential schedule YAML document")]
[EndpointDescription(
"Validates a sequential-schedule YAML string against the full (or import) schema. Returns whether it is " +
"valid, any validation messages, and the JSON conversion of the YAML. Parse/validator errors are reported " +
"as messages (IsValid=false), never as a 500.")]
[EndpointGroupName("general")]
[ProducesResponseType(typeof(ValidateSequentialScheduleResponseModel), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
public async Task<IActionResult> ValidateSchedule(
[Required] [FromBody] ValidateSequentialScheduleRequest request,
CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(request.Yaml))
{
return new BadRequestObjectResult(
new ProblemDetails
{
Status = StatusCodes.Status400BadRequest,
Title = "Validation failed",
Detail = "[Yaml] must not be empty"
});
}
ValidateSequentialScheduleViewModel result = await mediator.Send(request.ToQuery(), cancellationToken);
return new OkObjectResult(
new ValidateSequentialScheduleResponseModel(result.IsValid, result.Messages, result.Json));
}
[HttpHead("api/troubleshoot/playback.m3u8")]
[HttpGet("api/troubleshoot/playback.m3u8")]
[Tags("Troubleshooting")]