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
+438
View File
@@ -6315,6 +6315,251 @@
}
}
},
"/api/playouts/{id}/blocks": {
"get": {
"tags": [
"Playouts"
],
"summary": "Get the blocks scheduled by a block playout",
"description": "Lists the distinct blocks reachable through a Block playout's templates, ordered by group then name. A playout with no templates (including non-Block playouts) returns an empty list.",
"operationId": "GetPlayoutBlocks",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "integer",
"format": "int32"
}
}
],
"responses": {
"200": {
"description": "OK",
"content": {
"text/plain": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/BlockResponseModel"
}
}
},
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/BlockResponseModel"
}
}
},
"text/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/BlockResponseModel"
}
}
}
}
},
"404": {
"description": "Not Found",
"content": {
"text/plain": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
},
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
},
"text/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
}
}
}
}
}
},
"/api/playouts/{id}/blocks/{blockId}/history": {
"get": {
"tags": [
"Playouts"
],
"summary": "Get a block's playout history",
"description": "Returns the paged scheduling history for a single block within a block playout, oldest first. Each row's Key and Details carry raw JSON; decode a row via GET /api/playouts/history/{id}.",
"operationId": "GetPlayoutBlockHistory",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "integer",
"format": "int32"
}
},
{
"name": "blockId",
"in": "path",
"required": true,
"schema": {
"type": "integer",
"format": "int32"
}
},
{
"name": "pageNum",
"in": "query",
"schema": {
"type": "integer",
"format": "int32",
"default": 0
}
},
{
"name": "pageSize",
"in": "query",
"schema": {
"type": "integer",
"format": "int32",
"default": 100
}
}
],
"responses": {
"200": {
"description": "OK",
"content": {
"text/plain": {
"schema": {
"$ref": "#/components/schemas/PagedPlayoutHistoryResponseModel"
}
},
"application/json": {
"schema": {
"$ref": "#/components/schemas/PagedPlayoutHistoryResponseModel"
}
},
"text/json": {
"schema": {
"$ref": "#/components/schemas/PagedPlayoutHistoryResponseModel"
}
}
}
},
"404": {
"description": "Not Found",
"content": {
"text/plain": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
},
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
},
"text/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
}
}
}
}
}
},
"/api/playouts/history/{id}": {
"get": {
"tags": [
"Playouts"
],
"summary": "Decode a playout history row",
"description": "Decodes a single playout history row (by its id) into its playback order, collection, and media-item details. Returns 422 if the row's stored Key/Details JSON cannot be decoded.",
"operationId": "GetPlayoutHistoryDetails",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "integer",
"format": "int32"
}
}
],
"responses": {
"200": {
"description": "OK",
"content": {
"text/plain": {
"schema": {
"$ref": "#/components/schemas/PlayoutHistoryDetailsResponseModel"
}
},
"application/json": {
"schema": {
"$ref": "#/components/schemas/PlayoutHistoryDetailsResponseModel"
}
},
"text/json": {
"schema": {
"$ref": "#/components/schemas/PlayoutHistoryDetailsResponseModel"
}
}
}
},
"404": {
"description": "Not Found",
"content": {
"text/plain": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
},
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
},
"text/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
}
}
},
"422": {
"description": "Unprocessable Entity",
"content": {
"text/plain": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
},
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
},
"text/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
}
}
}
}
}
},
"/api/playouts/reset-all": {
"post": {
"tags": [
@@ -10164,6 +10409,83 @@
}
}
},
"/api/troubleshoot/validate-schedule": {
"post": {
"tags": [
"Troubleshooting"
],
"summary": "Validate a sequential schedule YAML document",
"description": "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.",
"operationId": "ValidateSequentialSchedule",
"requestBody": {
"content": {
"application/json-patch+json": {
"schema": {
"$ref": "#/components/schemas/ValidateSequentialScheduleRequest"
}
},
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidateSequentialScheduleRequest"
}
},
"text/json": {
"schema": {
"$ref": "#/components/schemas/ValidateSequentialScheduleRequest"
}
},
"application/*+json": {
"schema": {
"$ref": "#/components/schemas/ValidateSequentialScheduleRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "OK",
"content": {
"text/plain": {
"schema": {
"$ref": "#/components/schemas/ValidateSequentialScheduleResponseModel"
}
},
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidateSequentialScheduleResponseModel"
}
},
"text/json": {
"schema": {
"$ref": "#/components/schemas/ValidateSequentialScheduleResponseModel"
}
}
}
},
"400": {
"description": "Bad Request",
"content": {
"text/plain": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
},
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
},
"text/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
}
}
}
}
}
},
"/api/troubleshoot/playback.m3u8": {
"head": {
"tags": [
@@ -15149,6 +15471,25 @@
}
}
},
"PagedPlayoutHistoryResponseModel": {
"required": [
"totalCount",
"page"
],
"type": "object",
"properties": {
"totalCount": {
"type": "integer",
"format": "int32"
},
"page": {
"type": "array",
"items": {
"$ref": "#/components/schemas/PlayoutHistoryResponseModel"
}
}
}
},
"PagedPlayoutItemsResponseModel": {
"required": [
"totalCount",
@@ -15509,6 +15850,63 @@
}
}
},
"PlayoutHistoryDetailsResponseModel": {
"required": [
"playbackOrder",
"collectionType",
"name",
"mediaItemType",
"mediaItemTitle"
],
"type": "object",
"properties": {
"playbackOrder": {
"$ref": "#/components/schemas/PlaybackOrder"
},
"collectionType": {
"$ref": "#/components/schemas/CollectionType"
},
"name": {
"type": "string"
},
"mediaItemType": {
"type": "string"
},
"mediaItemTitle": {
"type": "string"
}
}
},
"PlayoutHistoryResponseModel": {
"required": [
"id",
"when",
"finish",
"key",
"details"
],
"type": "object",
"properties": {
"id": {
"type": "integer",
"format": "int32"
},
"when": {
"type": "string",
"format": "date-time"
},
"finish": {
"type": "string",
"format": "date-time"
},
"key": {
"type": "string"
},
"details": {
"type": "string"
}
}
},
"PlayoutItemResponseModel": {
"required": [
"title",
@@ -18276,6 +18674,46 @@
],
"type": "string"
},
"ValidateSequentialScheduleRequest": {
"required": [
"yaml",
"isImport"
],
"type": "object",
"properties": {
"yaml": {
"type": [
"null",
"string"
]
},
"isImport": {
"type": "boolean"
}
}
},
"ValidateSequentialScheduleResponseModel": {
"required": [
"isValid",
"messages",
"json"
],
"type": "object",
"properties": {
"isValid": {
"type": "boolean"
},
"messages": {
"type": "array",
"items": {
"type": "string"
}
},
"json": {
"type": "string"
}
}
},
"WatermarkFullResponseModel": {
"required": [
"id",