node:22-noble does not exist on Docker Hub (no noble variant of the
official node image; the -noble suffix was carried over from the MS
dotnet image tags). Broke the main image-build job (run 482) after
PR #120 landed, since PR runs skip the build job.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ChannelResponseModel and BulkChannelRequests DTOs lacked file-scoped
#nullable enable, so the OpenAPI generator emitted every string
property as a ["null","string"] union (project Nullable default is
disable), polluting SPA typegen with needless `| null` unions.
ChannelStateResponseModel already complied with this convention
(precedent: PlayoutResponseModel). All properties in both files are
always populated (never null in practice), so no property needed to
become explicitly optional/nullable - just adding #nullable enable
was sufficient to clean up the generated schema.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Add GetChannelGuideDataHandlerTests coverage for the mirror-channel path:
asserts programme Start/Stop shift by PlayoutOffset and that the source
channel's seeded PlayoutItem entities are not mutated, pinning the
WithPlayoutOffset copy fix.
- ChannelController.GetGuide Swagger description said end defaults to "now
plus XmltvDaysToBuild"; it actually defaults to start plus that window
(matters when start is supplied). Reworded and regenerated v1.json.
Add GET /api/guide returning a per-channel EPG grid (ChannelGuideResponseModel).
start defaults to now, end defaults to now + XmltvDaysToBuild.
Extract the guide-group/filler-merge projection (ChannelGuideProjector) and
programme-metadata resolution (ChannelGuideMetadata) plus the metadata eager-load
(IncludeGuideMetadata) out of RefreshChannelDataHandler so the XMLTV cache builder
and the JSON guide share one source of truth and cannot drift. XMLTV output is
unchanged (ChannelGuideGoldenTests pass without regeneration).
The JSON handler mirrors XMLTV semantics: ShowInEpg-only channels, decimal channel
ordering, ExternalJson playouts skipped, mirror playout offset applied to copies
(never mutating the loaded entities). SubTitle/Category are nullable.
refs #102
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adversarial review finding on #111: MultipleMode.Count supports
expressions the estimator cannot evaluate; document that they
produce a null (unknown) estimate.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
POST /api/artwork/uploads (multipart/form-data) accepting logo and
watermark images. Validates content type (png/jpeg/gif/webp) and size
(SystemEnvironment.MaximumUploadMb, default 10MB) mirroring the Blazor
upload path; stores via IImageCache.SaveArtworkToCache; returns
{ path, contentType } consumable by channel create/update.
Includes handler + controller tests, OpenAPI 422 contract-test entry,
and regenerated v1.json.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- GET /api/playouts — paged list over GetPagedPlayouts (query/pageNum/pageSize), list DTO with buildStatus (#100, #107)
- GET /api/playouts/{id}/items — paged future items + UNSCHEDULED gaps over GetFuturePlayoutItemsById; 404 ProblemDetails for unknown playout (matches /api/schedules/{id}/items precedent) (#101)
- buildStatus {lastBuild, success, message} on GET /api/playouts/{id} (BuildStatus now included by GetPlayoutByIdHandler) (#107)
- GET /api/playouts/warnings/count — failed-build count for the warnings badge (#107)
- POST /api/playouts/reset-all — 202 Accepted, wraps ResetAllPlayouts (#110)
- POST /api/channels/{channelNumber}/playout/reset — optional ?mode= override; default branches by ScheduleKind (Classic→Refresh, others→Reset) to match Blazor semantics (#110)
- Option<FillerKind>→FillerKind? projection uses MatchUnsafe (Match throws on null-returning branch; idiom per Health/Mapper.cs)
- Tests: controller unit tests (routes, projections, 404s, reset-mode defaults), OpenAPI ProblemDetails contract entry for /api/playouts/{id}/items get 404
- Regenerated wwwroot/openapi/v1.json
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review finding: NotApplicable is filtered by the handler before mapping,
so a silent ->"info" arm would only mask a missing filter if the mapper
is ever reused unfiltered.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fix the WIP health-check API slice to match established patterns:
- HealthController: add Name="GetHealthChecks" route name and move
[EndpointGroupName("general")] to method level, matching
FillerPresetController/FFmpegProfileController exactly (the
precedent for parameterless 200-only GET actions).
- HealthCheckResponseModel: enable #nullable for the file and mark
Link as string? since the mapper can emit null when
HealthCheckResult.Link is None.
- Mapper: fix a real bug - LanguageExt's Option.Match throws
ResultIsNullException.ResultIsNull if either branch returns null
(by design, to catch accidental nulls). The WIP's
`Link.Match(l => l.Link, () => null)` crashed on every health
check without a Link. Switch to MatchUnsafe, the LanguageExt-
sanctioned way to intentionally produce a nullable result from
Option<T>.
- HealthControllerTests: add the idiomatic-route-assertion test
(route template + Name) and empty-list case, matching
FillerPresetControllerTests.
Verified: GetAllHealthCheckResultsForApiHandler already matches the
existing GetAllHealthCheckResultsHandler's cancellation handling
(both swallow TaskCanceledException/OperationCanceledException), so
no change was needed there. Confirmed no OpenApi contract test
enumerates all endpoints for error-response metadata (it's an
explicit TestCase allowlist), so the new GET needed no new entries.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The WIP GET /api/filler-presets, /api/watermarks, and
/api/graphics-elements endpoints omitted the Name= route attribute
used by the other API GET actions (e.g. FFmpegProfileController). Add
Name = "GetFillerPresets" / "GetWatermarks" / "GetGraphicsElements" for
consistency.
Also add handler tests (FillerPresetHandlerTests, WatermarkHandlerTests,
GraphicsElementHandlerTests) covering the GetAll*ForApi handlers using
the InMemoryTvContext harness: multi-row projection to (Id, Name) DTOs,
the empty-DB case, and the graphics-element ordering rule (named
elements sort before elements whose projected Name equals FileName).
Adversarial review fixes for the #97 endpoint:
- nowPlaying now resolves like the XMLTV guide: when the wall-clock item
is pre/mid-roll filler, surface the guide group's program item (title +
full program bounds) instead of the filler; a guide entry with no
program item (e.g. fallback loop) reports null. Previously an ad break
would retitle the channel and reset progress every 30 seconds.
- covering-item lookup now runs one cheap projected query per distinct
playout offset instead of hydrating the full metadata include tree for
a window widened by the largest offset across ALL channels (up to 24h);
metadata is fetched in a second query scoped to the covering guide
groups, time-bounded to stay safe against GuideGroup recycling (mod
10000). Regression caught while testing: covering items are keyed by
(source channel, lookup time) so a source channel no longer inherits
its offset mirror's lookup result.
- GetChannelStatesForApi now carries Now explicitly (controller passes
DateTime.UtcNow), making boundary semantics testable: new tests pin
now == Start (playing) and now == Finish (not playing), plus mid-roll,
filler-only, and mirror-source-row cases.
- ChannelNowPlayingResponseModel title is non-nullable in the schema
(#nullable enable) — GetDisplayTitle always returns a string.
- GetState uses the typed-return controller convention; Playouts.Mapper
reverted to internal (handler lives in the same assembly).
- Spec doc notes the OnDemand drift caveat and filler semantics.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
An off-air channel has no current playout item, so the handler returns
null for NowPlaying; the DTO now declares that (file-scoped #nullable —
ErsatzTV.Core has NRT disabled project-wide). Regenerated OpenAPI and
web types: nowPlaying keeps its nullable oneOf schema (matching the
schema contract test) and channelNumber loses a spurious nullable
annotation left over from an earlier draft — the field is required.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Living handoff document driving the sequential backend-gap work
(#97, #100-#111, merge pass, then UX screens). Each session works
one queue item and rewrites this file for the next.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>