Add grid auto-build functionality with profile management and preview capabilities
ci / build-backend (push) Successful in 2m40s
ci / build-frontend (push) Successful in 1m14s
ci / tests (push) Successful in 2m45s
ci / sonar (push) Successful in 7m4s

Implemented new API endpoints and frontend components for grid auto-building based on predefined profiles. Added functionality to list grid profiles, preview generated grids, and generate grids with specified parameters. Enhanced the TemplateEndpoints and related services to support these features, improving the user experience for channel scheduling. Localization updates were made to accommodate new features in both English and Russian.
This commit is contained in:
Leonid Pershin
2026-07-27 08:08:56 +03:00
parent 6fe6404e61
commit bef029b858
33 changed files with 2623 additions and 44 deletions
+7
View File
@@ -24,6 +24,13 @@ export const qk = {
issues: (id: string) => ['admin', 'channels', id, 'issues'] as const,
diff: (id: string) => ['admin', 'channels', id, 'diff'] as const,
preview: (id: string, days: number) => ['admin', 'channels', id, 'preview', days] as const,
/** План автосборки — свой на каждую комбинацию профиля и режима. */
gridPlan: (id: string, profile: string, mode: string) =>
['admin', 'channels', id, 'grid-plan', profile, mode] as const,
},
gridProfiles: {
all: ['admin', 'grid-profiles'] as const,
},
entries: {
+46
View File
@@ -109,6 +109,8 @@ export type MediaStatsDto = {
queued: number
processing: number
averageProcessingSeconds: number | null
/** Оценка времени до конца очереди, сек; null — очередь пуста либо среднее ещё не набралось. */
estimatedRemainingSeconds: number | null
}
// ── Библиотека (жанры) ─────────────────────────────────────────────────────
@@ -692,6 +694,50 @@ export type CopyTemplateResultDto = {
droppedBumperRefs: number
}
// ── Автосборка сетки ──────────────────────────────────────────────────────
/** Архетип сетки: с какого типа канала списан ритм суток. */
export type GridProfileKind = 'Mixed' | 'Animation' | 'Sitcom' | 'Music' | 'Movies' | 'Kids'
/** Что делать с тем, что в сетке уже есть. */
export type GridGenerationMode = 'FillGaps' | 'Rebuild'
export type GridProfileDto = {
kind: GridProfileKind
name: string
/** На что похоже — «как у 2×2». */
reference: string
description: string
}
/** Строка предпросмотра: будущий слот до того, как он создан. */
export type GridPlanSlotDto = {
layer: 'Main' | 'Weekend'
weekday: number | null
start: string
durationMinutes: number
title: string
daypart: Daypart
slotKind: SlotKind
groupName: string | null
/** Человеческое описание блока: «4 подряд», «до конца слота». */
block: string
}
export type GridPlanDto = {
profile: GridProfileKind
profileName: string
slots: GridPlanSlotDto[]
notes: string[]
/** Больше нуля только при пересборке — столько слотов будет снесено. */
slotsToRemove: number
fallbackGroupName: string | null
freeMinutes: number
coveredMinutes: number
}
export type GenerateGridResultDto = { created: number; removed: number }
/** Проверки сетки по правилам, до генерации (см. 5.1). */
type TemplateIssueKind =
| 'GroupEmpty'
+26
View File
@@ -318,6 +318,11 @@ export const en = {
processingShort: 'Processing',
average: 'Average processing time (recent)',
averageShort: 'Avg time',
eta: 'Estimated time until the queue drains: work left at the average rate, divided by the number of parallel transcodes',
etaShort: 'Left',
etaSoon: 'under a minute',
hoursShort: 'h',
minutesShort: 'min',
},
},
gallery: {
@@ -423,6 +428,27 @@ export const en = {
newLayerName: 'New layer',
addSlotHere: 'Add slot',
newSlot: 'New slot',
/** Grid auto-build by profile: plan preview and creation. */
generate: {
action: 'Build grid',
title: 'Grid auto-build',
hint: 'A profile sets the rhythm of the day: block length, what airs in prime time and what fills the night. Groups are picked from the existing ones by rating, content type and how many units they hold.',
modes: {
FillGaps: 'Fill gaps',
Rebuild: 'Rebuild the week',
},
modeHints: {
FillGaps: 'Existing slots are left alone — only uncovered time is filled.',
Rebuild:
'All template slots are removed; the weekend is built as a separate layer on top.',
},
summary: 'Slots to create: {{slots}} — that is {{percent}}% of the free week.',
willRemove: 'Slots to remove: {{count}}.',
fallback: 'The fallback group will be "{{name}}" — it covers pauses between slots.',
nothing: 'Nothing to create: there is no free time in the grid.',
create: 'Build',
done: 'Created {{created}} slots, removed {{removed}}.',
},
editSlot: 'Slot',
slotTitle: 'Block title',
slotStart: 'Start',
+25
View File
@@ -319,6 +319,11 @@ export const ru = {
processingShort: 'В обработке',
average: 'Среднее время обработки (по недавним)',
averageShort: 'Ср. время',
eta: 'Примерное время до конца очереди: оставшаяся работа по среднему, делённая на число параллельных транскодов',
etaShort: 'Осталось',
etaSoon: 'меньше минуты',
hoursShort: 'ч',
minutesShort: 'мин',
},
},
gallery: {
@@ -424,6 +429,26 @@ export const ru = {
newLayerName: 'Новый слой',
addSlotHere: 'Добавить слот',
newSlot: 'Новый слот',
/** Автосборка сетки по профилю: предпросмотр плана и его создание. */
generate: {
action: 'Собрать сетку',
title: 'Автосборка сетки',
hint: 'Профиль задаёт ритм суток: длину блоков, что стоит в прайме и чем закрыта ночь. Группы под каждую полосу подбираются из существующих — по рейтингу, типу контента и запасу серий.',
modes: {
FillGaps: 'Заполнить дыры',
Rebuild: 'Собрать неделю с нуля',
},
modeHints: {
FillGaps: 'Существующие слоты не трогаются — застраивается только незакрытое время.',
Rebuild: 'Все слоты шаблона сносятся; выходные строятся отдельным слоем поверх будних.',
},
summary: 'Будет создано слотов: {{slots}} — это {{percent}}% свободного времени недели.',
willRemove: 'Будет снесено слотов: {{count}}.',
fallback: 'Аварийной группой станет «{{name}}» — ею закрываются паузы между слотами.',
nothing: 'Создавать нечего: свободного времени в сетке нет.',
create: 'Собрать',
done: 'Создано слотов: {{created}}, снесено: {{removed}}.',
},
editSlot: 'Слот',
slotTitle: 'Название блока',
slotStart: 'Начало',