Implemented a new endpoint for exporting debug data related to channel scheduling, allowing users to download an archive containing channel settings, slot states, and trace information. Updated the frontend to include a button for triggering the export, along with necessary API adjustments for handling the download. Enhanced localization strings to support the new debug export feature in both English and Russian. Updated .gitignore to include debug export files while ensuring the directory structure is maintained for development.
270 lines
11 KiB
TypeScript
270 lines
11 KiB
TypeScript
import { apiDownload, apiRequest } from '@/shared/api/client'
|
|
import type {
|
|
ApplyResultDto,
|
|
ChannelDto,
|
|
ChannelSummaryDto,
|
|
CopyTemplateResultDto,
|
|
CreatedIdResponse,
|
|
EntryTraceDto,
|
|
GenerateGridResultDto,
|
|
GridConfig,
|
|
GridGenerationMode,
|
|
GridImportResultDto,
|
|
GridPromptDto,
|
|
GridPlanDto,
|
|
GridProfileDto,
|
|
GridProfileKind,
|
|
LayerApplicability,
|
|
PlanningRules,
|
|
ResetTemplateResultDto,
|
|
RestoreTemplateResultDto,
|
|
ScheduleDiffDto,
|
|
ScheduleEntryDto,
|
|
SchedulePreviewDto,
|
|
ScheduleTemplateDto,
|
|
SlotDto,
|
|
TemplateIssueDto,
|
|
ViewerSettings,
|
|
} from '@/shared/api/types'
|
|
|
|
export function listChannels() {
|
|
return apiRequest<ChannelSummaryDto[]>('/admin/channels')
|
|
}
|
|
|
|
export function getChannel(id: string) {
|
|
return apiRequest<ChannelDto>(`/admin/channels/${id}`)
|
|
}
|
|
|
|
export function createChannel(body: { name: string; slug: string }) {
|
|
return apiRequest<CreatedIdResponse>('/admin/channels', { method: 'POST', body })
|
|
}
|
|
|
|
type ChannelSettingsBody = {
|
|
name: string
|
|
isEnabled: boolean
|
|
fillerAssetId: string | null
|
|
}
|
|
|
|
export function updateChannelSettings(id: string, body: ChannelSettingsBody) {
|
|
return apiRequest<void>(`/admin/channels/${id}/settings`, { method: 'PUT', body })
|
|
}
|
|
|
|
/** Оверлеи и аналоговый фильтр канала — как он выглядит у зрителя. */
|
|
export function updateViewerSettings(id: string, body: ViewerSettings) {
|
|
return apiRequest<void>(`/admin/channels/${id}/viewer`, { method: 'PUT', body })
|
|
}
|
|
|
|
/** Номер канала и его время: смещение от UTC и начало вещательных суток. */
|
|
export function updateChannelTime(
|
|
id: string,
|
|
body: { number: number | null; utcOffsetMinutes: number; dayStartTime: string },
|
|
) {
|
|
return apiRequest<void>(`/admin/channels/${id}/time`, { method: 'PUT', body })
|
|
}
|
|
|
|
// ── Сетка канала ──────────────────────────────────────────────────────────
|
|
|
|
export function getChannelTemplate(channelId: string) {
|
|
return apiRequest<ScheduleTemplateDto>(`/admin/channels/${channelId}/template`)
|
|
}
|
|
|
|
/** Заводит каналу сетку, если её нет: у каналов из старой ротации шаблона может не быть. */
|
|
export function createChannelTemplate(channelId: string) {
|
|
return apiRequest<CreatedIdResponse>(`/admin/channels/${channelId}/template`, { method: 'POST' })
|
|
}
|
|
|
|
/** Применяет правила к эфиру: пересобирает будущий хвост. Правка слотов эфир не двигает. */
|
|
export function applyChannelTemplate(channelId: string) {
|
|
return apiRequest<ApplyResultDto>(`/admin/channels/${channelId}/template/apply`, {
|
|
method: 'POST',
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Откат правок сетки к состоянию, по которому идёт эфир: восстанавливает слои, слоты и правила
|
|
* из снимка последнего применения. Ленту не трогает — она и так по этому состоянию.
|
|
*/
|
|
export function restoreChannelTemplate(channelId: string) {
|
|
return apiRequest<RestoreTemplateResultDto>(`/admin/channels/${channelId}/template/restore`, {
|
|
method: 'POST',
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Полный сброс: снимает сетку и удаляет будущий эфир, собранный по ней. Прошлое и идущая сейчас
|
|
* запись остаются — их нельзя вырезать из-под зрителя.
|
|
*/
|
|
export function resetChannelTemplate(channelId: string) {
|
|
return apiRequest<ResetTemplateResultDto>(`/admin/channels/${channelId}/template/reset`, {
|
|
method: 'POST',
|
|
})
|
|
}
|
|
|
|
/** Проверки сетки по правилам — считаются по шаблону, без прогона генератора. */
|
|
export function getTemplateIssues(channelId: string) {
|
|
return apiRequest<TemplateIssueDto[]>(`/admin/channels/${channelId}/template/issues`)
|
|
}
|
|
|
|
/** Что изменится в эфире, если применить сейчас. Прогон сухой — лента не трогается. */
|
|
export function getApplyDiff(channelId: string) {
|
|
return apiRequest<ScheduleDiffDto>(`/admin/channels/${channelId}/template/diff`)
|
|
}
|
|
|
|
/** Каталог профилей автосборки: имя, на что похоже и для какой библиотеки годится. */
|
|
export function listGridProfiles() {
|
|
return apiRequest<GridProfileDto[]>('/admin/grid-profiles')
|
|
}
|
|
|
|
/** План автосборки: что будет создано и что снесено. Считается тем же кодом, что и сама сборка. */
|
|
export function previewGrid(channelId: string, profile: GridProfileKind, mode: GridGenerationMode) {
|
|
const query = new URLSearchParams({ profile, mode })
|
|
return apiRequest<GridPlanDto>(
|
|
`/admin/channels/${channelId}/template/grid-plan?${query.toString()}`,
|
|
)
|
|
}
|
|
|
|
/** Собирает сетку по профилю. План пересчитывается на сервере — с клиента едут только опции. */
|
|
export function generateGrid(
|
|
channelId: string,
|
|
profile: GridProfileKind,
|
|
mode: GridGenerationMode,
|
|
) {
|
|
return apiRequest<GenerateGridResultDto>(`/admin/channels/${channelId}/template/generate`, {
|
|
method: 'POST',
|
|
body: { profile, mode },
|
|
})
|
|
}
|
|
|
|
/** Выгрузка сетки одним файлом: перенос, бэкап перед экспериментом и образец для ИИ. */
|
|
export function exportGrid(channelId: string) {
|
|
return apiRequest<GridConfig>(`/admin/channels/${channelId}/template/export`)
|
|
}
|
|
|
|
/**
|
|
* Загрузка сетки из файла. `replace` — снести существующие слоты и построить заново; иначе слои
|
|
* и слоты добавляются к тому, что уже есть.
|
|
*/
|
|
export function importGrid(channelId: string, config: GridConfig, replace: boolean) {
|
|
return apiRequest<GridImportResultDto>(`/admin/channels/${channelId}/template/import`, {
|
|
method: 'POST',
|
|
body: { config, replace },
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Собирает текст запроса к ИИ по референс-каналам и пожеланиям. Никуда не отправляется — админ
|
|
* копирует его в свою модель и приносит ответ назад кнопкой импорта.
|
|
*/
|
|
export function buildGridPrompt(channelId: string, references: string[], notes: string) {
|
|
return apiRequest<GridPromptDto>(`/admin/channels/${channelId}/template/ai-prompt`, {
|
|
method: 'POST',
|
|
body: { references, notes: notes || null },
|
|
})
|
|
}
|
|
|
|
/** Копия сетки на другой канал: слои, слоты, стыки и правила. Группы общие и не копируются. */
|
|
export function copyTemplateTo(channelId: string, targetChannelId: string) {
|
|
return apiRequest<CopyTemplateResultDto>(
|
|
`/admin/channels/${channelId}/template/copy-to/${targetChannelId}`,
|
|
{ method: 'POST' },
|
|
)
|
|
}
|
|
|
|
/** Цепочка происхождения записи, записанная в момент генерации. */
|
|
export function getEntryTrace(entryId: string) {
|
|
return apiRequest<EntryTraceDto>(`/admin/channels/entries/${entryId}/trace`)
|
|
}
|
|
|
|
/** Сухой прогон по текущим правилам: ничего не пишет и не двигает курсоры слотов. */
|
|
export function previewTemplate(channelId: string, days: number) {
|
|
const query = new URLSearchParams({ days: String(days) })
|
|
return apiRequest<SchedulePreviewDto>(
|
|
`/admin/channels/${channelId}/template/preview?${query.toString()}`,
|
|
)
|
|
}
|
|
|
|
export function updateTemplate(
|
|
templateId: string,
|
|
body: {
|
|
name: string
|
|
fallbackGroupId: string | null
|
|
defaultJunctionId: string | null
|
|
rules: PlanningRules | null
|
|
},
|
|
) {
|
|
return apiRequest<void>(`/admin/templates/${templateId}`, { method: 'PUT', body })
|
|
}
|
|
|
|
export function createLayer(templateId: string, body: { name: string; priority: number }) {
|
|
return apiRequest<CreatedIdResponse>(`/admin/templates/${templateId}/layers`, {
|
|
method: 'POST',
|
|
body,
|
|
})
|
|
}
|
|
|
|
export function updateLayer(
|
|
layerId: string,
|
|
body: {
|
|
name: string
|
|
priority: number
|
|
applicability: LayerApplicability | null
|
|
isEnabled: boolean
|
|
},
|
|
) {
|
|
return apiRequest<void>(`/admin/layers/${layerId}`, { method: 'PUT', body })
|
|
}
|
|
|
|
export function deleteLayer(layerId: string) {
|
|
return apiRequest<void>(`/admin/layers/${layerId}`, { method: 'DELETE' })
|
|
}
|
|
|
|
/** Тело слота: то же для создания и правки (см. SlotInput на сервере). */
|
|
export type SlotBody = Omit<SlotDto, 'id' | 'layerId' | 'groupName'>
|
|
|
|
/** Слот из ответа сервера → тело запроса: отбрасываем то, что сервер проставляет сам. */
|
|
export function toSlotBody(slot: SlotDto): SlotBody {
|
|
const { id: _id, layerId: _layerId, groupName: _groupName, ...body } = slot
|
|
return body
|
|
}
|
|
|
|
export function createSlot(layerId: string, body: SlotBody) {
|
|
return apiRequest<CreatedIdResponse>(`/admin/layers/${layerId}/slots`, { method: 'POST', body })
|
|
}
|
|
|
|
export function updateSlot(slotId: string, body: SlotBody) {
|
|
return apiRequest<void>(`/admin/slots/${slotId}`, { method: 'PUT', body })
|
|
}
|
|
|
|
export function deleteSlot(slotId: string) {
|
|
return apiRequest<void>(`/admin/slots/${slotId}`, { method: 'DELETE' })
|
|
}
|
|
|
|
// ── Стыки канала ──────────────────────────────────────────────────────────
|
|
|
|
export function getSchedule(id: string, from: Date, to: Date) {
|
|
const query = new URLSearchParams({ from: from.toISOString(), to: to.toISOString() })
|
|
return apiRequest<ScheduleEntryDto[]>(`/admin/channels/${id}/schedule?${query.toString()}`)
|
|
}
|
|
|
|
/**
|
|
* Отладочный дамп канала: вход планировщика, состояние слотов, лента и сухой прогон одним архивом.
|
|
* Тянем через fetch, а не ссылкой: эндпоинт закрыт Bearer'ом, и `<a href>` заголовок не отправит.
|
|
*/
|
|
export async function exportChannelDebug(channelId: string) {
|
|
const { blob, fileName } = await apiDownload(
|
|
`/admin/channels/${channelId}/debug-export`,
|
|
'telewave-debug.zip',
|
|
{ method: 'POST' },
|
|
)
|
|
const url = URL.createObjectURL(blob)
|
|
try {
|
|
const link = document.createElement('a')
|
|
link.href = url
|
|
link.download = fileName
|
|
link.click()
|
|
} finally {
|
|
URL.revokeObjectURL(url)
|
|
}
|
|
return fileName
|
|
}
|