Enhance channel and settings functionalities: introduce viewer settings in ChannelEndpoints, update SiteSettings to include channel number toggling, and refactor related data structures. Implement new endpoints for validating templates and diffing scheduling changes, improving overall user experience and configuration management.
build / backend (push) Successful in 1m54s
build / frontend (push) Successful in 43s
tests / backend-tests (push) Successful in 2m4s

This commit is contained in:
Leonid Pershin
2026-07-26 14:50:32 +03:00
parent 8494eb5e6a
commit f699576582
54 changed files with 5857 additions and 1631 deletions
@@ -0,0 +1,141 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using LiteCqrs;
using Microsoft.EntityFrameworkCore;
using TeleWave.Application.Broadcast;
using TeleWave.Application.Common.Interfaces;
using TeleWave.Application.Common.Models;
using TeleWave.Application.Programming.Templates;
using TeleWave.Domain.Programming;
using TeleWave.Domain.Programming.Planning;
namespace TeleWave.Application.Programming.Planning.Trace;
public sealed class GetEntryTraceQueryHandler(IAppDbContext dbContext)
: IQueryHandler<GetEntryTraceQuery, Result<EntryTraceDto>>
{
/// <summary>Те же настройки, что при записи трейса генератором.</summary>
private static readonly JsonSerializerOptions TraceJsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
Converters = { new JsonStringEnumConverter() },
};
public async Task<Result<EntryTraceDto>> Handle(
GetEntryTraceQuery query,
CancellationToken cancellationToken
)
{
var entry = await dbContext
.ScheduleEntries.AsNoTracking()
.FirstOrDefaultAsync(e => e.Id == query.EntryId, cancellationToken);
if (entry is null)
return Result.Failure<EntryTraceDto>(ChannelErrors.NotFound);
var showName =
entry.ShowId is { } showId
? await dbContext
.Shows.AsNoTracking()
.Where(s => s.Id == showId)
.Select(s => s.Name)
.FirstOrDefaultAsync(cancellationToken)
: null;
var trace = Parse(entry.TraceJson);
if (trace is null)
return Result.Success(
new EntryTraceDto(
entry.Id,
entry.StartsAtUtc,
entry.EndsAtUtc,
showName,
entry.EpisodeIndex,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
0,
false,
null
)
);
// Слот мог быть удалён или изменён после генерации — трейс от этого не портится, просто
// часть подписей окажется пустой.
var slot = trace.SlotId is { } slotId
? await dbContext.Slots.AsNoTracking().FirstOrDefaultAsync(s => s.Id == slotId, cancellationToken)
: null;
var layer = slot is null
? null
: await dbContext
.GridLayers.AsNoTracking()
.FirstOrDefaultAsync(l => l.Id == slot.LayerId, cancellationToken);
var group = slot?.GroupId is { } groupId
? await dbContext
.Groups.AsNoTracking()
.Where(g => g.Id == groupId)
.Select(g => new { g.Name, g.ItemCount })
.FirstOrDefaultAsync(cancellationToken)
: null;
var strategy = SlotStrategy.FromJson(slot?.StrategyJson);
var junctionId = slot?.JunctionAfterId ?? slot?.JunctionBetweenId;
var junctionName = junctionId is { } id
? await dbContext
.JunctionTemplates.AsNoTracking()
.Where(j => j.Id == id)
.Select(j => j.Name)
.FirstOrDefaultAsync(cancellationToken)
: null;
return Result.Success(
new EntryTraceDto(
entry.Id,
entry.StartsAtUtc,
entry.EndsAtUtc,
showName,
entry.EpisodeIndex,
layer?.Name,
layer?.Priority,
slot?.Title,
trace.SlotKind,
slot?.Weekday,
slot?.TargetStart,
slot?.TargetDurationMinutes,
group?.Name,
group?.ItemCount,
trace.Strategy,
strategy?.CooldownDays,
trace.CandidatesAfterCooldown,
trace.DriftMinutes,
trace.Snapped,
junctionName
)
);
}
private static PlanTrace? Parse(string? json)
{
if (string.IsNullOrWhiteSpace(json))
return null;
try
{
return JsonSerializer.Deserialize<PlanTrace>(json, TraceJsonOptions);
}
catch (JsonException)
{
return null;
}
}
}