diff --git a/backend/src/TeleWave.Api/Program.cs b/backend/src/TeleWave.Api/Program.cs
index 7b7de2c..cbdc289 100644
--- a/backend/src/TeleWave.Api/Program.cs
+++ b/backend/src/TeleWave.Api/Program.cs
@@ -142,7 +142,7 @@ app.UseDefaultFiles();
app.UseStaticFiles();
app.MapFallbackToFile("index.html");
-app.Run();
+await app.RunAsync();
/// Делает неявный класс Program доступным для WebApplicationFactory<Program> в интеграционных тестах.
public partial class Program;
diff --git a/backend/src/TeleWave.Api/wwwroot/index.html b/backend/src/TeleWave.Api/wwwroot/index.html
index 2326906..470e271 100644
--- a/backend/src/TeleWave.Api/wwwroot/index.html
+++ b/backend/src/TeleWave.Api/wwwroot/index.html
@@ -1,5 +1,5 @@
-
+
TeleWave
Frontend build not present yet — run the Vite build to populate wwwroot.
diff --git a/frontend/src/features/admin/channels/components/BumperTemplateEditor.tsx b/frontend/src/features/admin/channels/components/BumperTemplateEditor.tsx
index 96a4524..cbe1d96 100644
--- a/frontend/src/features/admin/channels/components/BumperTemplateEditor.tsx
+++ b/frontend/src/features/admin/channels/components/BumperTemplateEditor.tsx
@@ -80,11 +80,16 @@ export function BumperTemplateEditor({
return (
-
setOpen((o) => !o)}
- >
-
+ {/* Сворачивание висит на кнопке, а не на всей строке: кликабельный div недоступен с клавиатуры.
+ Кнопка удаления при этом вынесена наружу — вложенная кнопка внутри кнопки недопустима,
+ и заодно ей больше не нужен stopPropagation. */}
+
diff --git a/frontend/src/features/admin/channels/components/TemplatePreview.tsx b/frontend/src/features/admin/channels/components/TemplatePreview.tsx
index 922f399..7c187fa 100644
--- a/frontend/src/features/admin/channels/components/TemplatePreview.tsx
+++ b/frontend/src/features/admin/channels/components/TemplatePreview.tsx
@@ -263,7 +263,7 @@ function RepeatHeatmap({ preview }: { preview: SchedulePreviewDto }) {
counts.set(item.title, row)
}
- const sortedDays = [...dayKeys].sort()
+ const sortedDays = [...dayKeys].sort((a, b) => a.localeCompare(b))
const sortedRows = [...counts.entries()]
.map(([title, byDay]) => ({
title,
diff --git a/frontend/src/features/admin/media/episode-regex.ts b/frontend/src/features/admin/media/episode-regex.ts
index e94b3ec..7361e93 100644
--- a/frontend/src/features/admin/media/episode-regex.ts
+++ b/frontend/src/features/admin/media/episode-regex.ts
@@ -18,6 +18,20 @@ export function findNumbers(fileName: string): { index: number; start: number; t
const escapeRegex = (value: string) => value.replace(/[.*+?^${}()|[\]\\/-]/g, '\\$&')
+const isLetter = (char: string) => /\p{L}/u.test(char)
+const isLetterOrDigit = (char: string) => /[\p{L}\p{N}]/u.test(char)
+
+/**
+ * Хвост строки из символов, удовлетворяющих условию. Обходим с конца вручную, а не шаблоном вида
+ * `X*$`: такой шаблон движок примеряет с каждой позиции строки и получает квадратичное время
+ * (см. предупреждение анализатора о backtracking), тогда как здесь один линейный проход.
+ */
+function trailingRun(value: string, matches: (char: string) => boolean): string {
+ let start = value.length
+ while (start > 0 && matches(value[start - 1])) start--
+ return value.slice(start)
+}
+
/**
* Строит regex по указанному пользователем числу в имени файла. Якорем берётся слово перед числом
* («Серия 01» → `Серия\s*(\d{1,3})`): позиция числа в разных файлах гуляет, а слово рядом — нет.
@@ -36,11 +50,11 @@ export function buildEpisodeRegex(fileName: string, occurrenceIndex: number): st
// Разделители между якорем и числом описываем классом, а не буквально: в соседних файлах
// там встречается то пробел, то точка, то подчёркивание.
- const gap = /[^\p{L}\p{N}]*$/u.exec(before)?.[0] ?? ''
+ const gap = trailingRun(before, (char) => !isLetterOrDigit(char))
const anchorSource = before.slice(0, before.length - gap.length)
// Якорь — только буквы: захвати он цифры, «S01E07» дало бы правило `S01E(\d)`, прибитое
// к первому сезону, и на «S02E05» оно бы уже не сработало.
- const anchor = /\p{L}+$/u.exec(anchorSource)?.[0]
+ const anchor = trailingRun(anchorSource, isLetter)
if (anchor) return `${escapeRegex(anchor)}${gap ? '[\\s._-]*' : ''}${digits}`
diff --git a/frontend/src/features/streaming/AirPage.tsx b/frontend/src/features/streaming/AirPage.tsx
index 87a0df2..7c48c05 100644
--- a/frontend/src/features/streaming/AirPage.tsx
+++ b/frontend/src/features/streaming/AirPage.tsx
@@ -160,6 +160,7 @@ export function AirPage() {
{channels.map((channel) => (