From 47b8bc7dade2282bd6db9606363fc1bf900376d2 Mon Sep 17 00:00:00 2001 From: kilyabin <65072190+kilyabin@users.noreply.github.com> Date: Fri, 30 Jan 2026 00:54:35 +0400 Subject: [PATCH] feat: better error messaging and trying to fix teacher schedule --- .dockerignore | 2 +- src/app/agregator/schedule.ts | 58 +++++++++++++++++++++++ src/app/parser/schedule.ts | 59 ++++++++++++++++++------ src/pages/[group].tsx | 29 +++++++++--- src/pages/index.tsx | 22 +++++---- src/pages/teacher/[teacher].tsx | 81 +++++++++++++++++++++------------ 6 files changed, 189 insertions(+), 62 deletions(-) diff --git a/.dockerignore b/.dockerignore index b95d525..c6d1fd7 100644 --- a/.dockerignore +++ b/.dockerignore @@ -67,4 +67,4 @@ README.md db/ *.db *.db-shm -*.db-wal +*.db-wal \ No newline at end of file diff --git a/src/app/agregator/schedule.ts b/src/app/agregator/schedule.ts index 961f98a..ab847ca 100644 --- a/src/app/agregator/schedule.ts +++ b/src/app/agregator/schedule.ts @@ -115,6 +115,23 @@ export async function getSchedule(groupID: number, groupName: string, wk?: numbe networkErrorCode: networkError.code, networkErrorMessage: networkError.message }) + } else if (networkError.code === 'DEPTH_ZERO_SELF_SIGNED_CERT' || networkError.message?.includes('self-signed certificate') || networkError.message?.includes('certificate')) { + // Ошибка SSL сертификата + console.error(`SSL certificate error while fetching ${PROXY_URL}:`, { + code: networkError.code, + message: networkError.message, + url + }) + const sslError = new Error(`В колледже что-то сломалось (проблема с сертификатом безопасности). Здесь я бессилен, проблема не на моей стороне.`) + logErrorToFile(sslError, { + type: 'ssl_certificate_error', + groupName, + url, + groupID, + networkErrorCode: networkError.code, + networkErrorMessage: networkError.message + }) + throw sslError } else { // Логируем другие ошибки тоже logErrorToFile(errorObj, { @@ -125,6 +142,18 @@ export async function getSchedule(groupID: number, groupName: string, wk?: numbe }) } } else { + // Проверяем сообщение об ошибке на наличие упоминания сертификата + if (errorObj.message?.includes('self-signed certificate') || errorObj.message?.includes('certificate')) { + const sslError = new Error(`В колледже что-то сломалось (проблема с сертификатом безопасности). Здесь я бессилен, проблема не на моей стороне.`) + logErrorToFile(sslError, { + type: 'ssl_certificate_error', + groupName, + url, + groupID, + errorMessage: errorObj.message + }) + throw sslError + } // Логируем ошибки без cause logErrorToFile(errorObj, { type: 'unknown_error', @@ -234,6 +263,23 @@ export async function getTeacherSchedule(teacherID: number, teacherName: string, networkErrorCode: networkError.code, networkErrorMessage: networkError.message }) + } else if (networkError.code === 'DEPTH_ZERO_SELF_SIGNED_CERT' || networkError.message?.includes('self-signed certificate') || networkError.message?.includes('certificate')) { + // Ошибка SSL сертификата + console.error(`SSL certificate error while fetching ${PROXY_URL}:`, { + code: networkError.code, + message: networkError.message, + url + }) + const sslError = new Error(`В колледже что-то сломалось (проблема с сертификатом безопасности). Здесь я бессилен, проблема не на моей стороне.`) + logErrorToFile(sslError, { + type: 'ssl_certificate_error', + teacherName, + url, + teacherID, + networkErrorCode: networkError.code, + networkErrorMessage: networkError.message + }) + throw sslError } else { // Логируем другие ошибки тоже logErrorToFile(errorObj, { @@ -244,6 +290,18 @@ export async function getTeacherSchedule(teacherID: number, teacherName: string, }) } } else { + // Проверяем сообщение об ошибке на наличие упоминания сертификата + if (errorObj.message?.includes('self-signed certificate') || errorObj.message?.includes('certificate')) { + const sslError = new Error(`В колледже что-то сломалось (проблема с сертификатом безопасности). Здесь я бессилен, проблема не на моей стороне.`) + logErrorToFile(sslError, { + type: 'ssl_certificate_error', + teacherName, + url, + teacherID, + errorMessage: errorObj.message + }) + throw sslError + } // Логируем ошибки без cause logErrorToFile(errorObj, { type: 'unknown_error', diff --git a/src/app/parser/schedule.ts b/src/app/parser/schedule.ts index 5fbe0f5..b65092a 100644 --- a/src/app/parser/schedule.ts +++ b/src/app/parser/schedule.ts @@ -624,19 +624,37 @@ const parseLesson = (row: Element, isTeacherSchedule: boolean = false): Lesson | } } else if (isTeacherSchedule) { // Для преподавателей место может быть в другом формате в тексте ячейки - // Формат: "ПредметГруппа(Аудитория)Адрес" - const fullText = disciplineCell.textContent?.trim() || '' - if (fullText) { - // Ищем паттерн: группа в скобках и адрес после - // Например: "(ИКС-8)Московское шоссе, 120" или "(ССА-15к)Моск" - const placeMatch = fullText.match(/\(([^)]+)\)([^(]+?)(?:\d+|$)/) + // Формат: "ПредметГруппа(Аудитория)Адрес" или в отдельной ячейке + // Сначала проверяем наличие отдельной ячейки с местом (как для групп) + const placeCellIndex = cells.length >= 6 ? 5 : (cells.length >= 5 ? 4 : -1) + if (placeCellIndex >= 0 && cells[placeCellIndex]) { + const placeCell = cells[placeCellIndex] + const placeText = placeCell.textContent?.trim() || '' + // Ищем адрес и кабинет в формате "адрес\nКабинет: номер" + const placeMatch = placeText.match(/([^\n]+)\n.*?Кабинет:\s*([^\s\n]+)/i) if (placeMatch) { - const classroom = placeMatch[1].trim() - const address = placeMatch[2].trim() - if (classroom && address) { - lesson.place = { - address, - classroom + lesson.place = { + address: placeMatch[1].trim(), + classroom: placeMatch[2].trim() + } + } + } + + // Если не нашли в отдельной ячейке, ищем в тексте ячейки с предметом + if (!lesson.place) { + const fullText = disciplineCell.textContent?.trim() || '' + if (fullText) { + // Ищем паттерн: группа в скобках и адрес после + // Например: "(ИКС-8)Московское шоссе, 120" или "(ССА-15к)Моск" + const placeMatch = fullText.match(/\(([^)]+)\)([^(]+?)(?:\d+|$)/) + if (placeMatch) { + const classroom = placeMatch[1].trim() + const address = placeMatch[2].trim() + if (classroom && address && address.length > 3) { + lesson.place = { + address, + classroom + } } } } @@ -698,8 +716,21 @@ export function parsePage(document: Document, groupName: string, url?: string, s // Способ 2: Если не нашли, ищем таблицу по имени в первой строке (может быть заголовок) if (!table) { table = tables.find(table => { - const firstRow = table.querySelector(':scope > tbody > tr:first-child') - return firstRow?.textContent?.trim() === groupName + const firstRow = table.querySelector(':scope > tbody > tr:first-child') || table.querySelector(':scope > tr:first-child') + const firstRowText = firstRow?.textContent?.trim() || '' + // Проверяем точное совпадение + return firstRowText === groupName + }) + } + + // Способ 2.5: Ищем таблицу, которая содержит имя где-то в первых строках (только если имя длинное) + if (!table && groupName.length > 10) { + table = tables.find(table => { + const rows = Array.from(table.querySelectorAll('tr')).slice(0, 3) + return rows.some(row => { + const rowText = row.textContent?.trim() || '' + return rowText.includes(groupName) + }) }) } } else { diff --git a/src/pages/[group].tsx b/src/pages/[group].tsx index db0d1bd..1e17297 100644 --- a/src/pages/[group].tsx +++ b/src/pages/[group].tsx @@ -96,10 +96,8 @@ export default function HomePage(props: NextSerialized) { - - {error.isTimeout - ? 'Превышено время ожидания ответа от сервера. Пожалуйста, попробуйте обновить страницу через несколько минут.' - : 'Произошла ошибка при загрузке расписания. Пожалуйста, попробуйте обновить страницу позже.'} + + {error?.message || 'Произошла ошибка при загрузке расписания'} @@ -270,11 +268,28 @@ export async function getServerSideProps(context: GetServerSidePropsContext<{ gr } else { // Если кэша нет, возвращаем страницу с ошибкой вместо throw const isTimeout = e instanceof ScheduleTimeoutError - const errorMessage = isTimeout - ? 'Превышено время ожидания ответа от сервера' - : 'Произошла ошибка при загрузке расписания' + const errorMessageObj = e instanceof Error ? e : new Error(String(e)) + const isSSLError = errorMessageObj.message?.includes('колледже что-то сломалось') || + errorMessageObj.message?.includes('SSL сертификата') || + errorMessageObj.message?.includes('self-signed certificate') || + errorMessageObj.message?.includes('certificate') || + (errorMessageObj.cause instanceof Error && ( + (errorMessageObj.cause as any).code === 'DEPTH_ZERO_SELF_SIGNED_CERT' || + errorMessageObj.cause.message?.includes('self-signed certificate') + )) + + // Если ошибка уже содержит нужное сообщение, используем его напрямую + let errorMessage: string + if (isTimeout) { + errorMessage = 'Превышено время ожидания ответа от сервера' + } else if (isSSLError || errorMessageObj.message?.includes('колледже что-то сломалось')) { + errorMessage = 'В колледже что-то сломалось (проблема с сертификатом безопасности). Здесь я бессилен, проблема не на моей стороне.' + } else { + errorMessage = errorMessageObj.message || 'Произошла ошибка при загрузке расписания' + } console.error(`Schedule fetch failed for group ${group}, no cache available:`, e) + console.error(`Error message: ${errorMessage}`) const cacheAvailableFor = Array.from(cachedSchedules.entries()) .filter(([, v]) => v.lastFetched.getTime() + maxCacheDurationInMS > Date.now()) diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 5ba36ea..a22263b 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -234,17 +234,19 @@ export default function HomePage(props: HomePageProps) { )} + {/* Кнопка перехода к расписанию преподавателей */} +
+ + + +
+
-
- - - -
{showAddGroupButton && (
) { {error.isTimeout - ? 'Превышено время ожидания ответа от сервера. Пожалуйста, попробуйте обновить страницу через несколько минут.' - : 'Произошла ошибка при загрузке расписания. Пожалуйста, попробуйте обновить страницу позже.'} + ? 'Превышено время ожидания ответа от сервера при загрузке расписания преподавателя. Пожалуйста, попробуйте обновить страницу через несколько минут.' + : `Не удалось загрузить расписание преподавателя ${teacher.name}. Возможно, расписание временно недоступно или произошла ошибка на сервере. Пожалуйста, попробуйте обновить страницу позже или вернитесь к списку преподавателей.`} +
+ + + +
@@ -284,35 +293,47 @@ export async function getServerSideProps(context: GetServerSidePropsContext<{ te } else { console.warn(`Schedule fetch error for teacher ${teacherInfo.name}, using fallback cache from ${cachedSchedule.lastFetched.toISOString()} (${cacheAge} minutes old)`) } - } else { - // Если кэша нет, возвращаем страницу с ошибкой вместо throw - const isTimeout = e instanceof ScheduleTimeoutError - const errorMessage = isTimeout - ? 'Превышено время ожидания ответа от сервера' - : 'Произошла ошибка при загрузке расписания' - - console.error(`Schedule fetch failed for teacher ${teacherInfo.name}, no cache available:`, e) - - const cacheAvailableFor = Array.from(cachedTeacherSchedules.entries()) - .filter(([, v]) => v.lastFetched.getTime() + maxCacheDurationInMS > Date.now()) - .map(([k]) => k.split('_')[1]) // Берем parseId из ключа кэша - - return { - props: nextSerialized({ - teacher: { - id: teacherInfo.id, - name: teacherInfo.name - }, - cacheAvailableFor, - groups, - settings, - error: { - message: errorMessage, - isTimeout - } - }) as NextSerialized + } else { + // Если кэша нет, возвращаем страницу с ошибкой вместо throw + const isTimeout = e instanceof ScheduleTimeoutError + const errorMessageObj = e instanceof Error ? e : new Error(String(e)) + const isSSLError = errorMessageObj.message?.includes('колледже что-то сломалось') || + errorMessageObj.message?.includes('SSL сертификата') || + errorMessageObj.message?.includes('self-signed certificate') || + errorMessageObj.message?.includes('certificate') || + (errorMessageObj.cause instanceof Error && ( + (errorMessageObj.cause as any).code === 'DEPTH_ZERO_SELF_SIGNED_CERT' || + errorMessageObj.cause.message?.includes('self-signed certificate') + )) + + const errorMessage = isTimeout + ? 'Превышено время ожидания ответа от сервера' + : isSSLError + ? 'В колледже что-то сломалось (проблема с сертификатом безопасности). Здесь я бессилен, проблема не на моей стороне.' + : errorMessageObj.message || 'Произошла ошибка при загрузке расписания' + + console.error(`Schedule fetch failed for teacher ${teacherInfo.name}, no cache available:`, e) + + const cacheAvailableFor = Array.from(cachedTeacherSchedules.entries()) + .filter(([, v]) => v.lastFetched.getTime() + maxCacheDurationInMS > Date.now()) + .map(([k]) => k.split('_')[1]) // Берем parseId из ключа кэша + + return { + props: nextSerialized({ + teacher: { + id: teacherInfo.id, + name: teacherInfo.name + }, + cacheAvailableFor, + groups, + settings, + error: { + message: errorMessage, + isTimeout + } + }) as NextSerialized + } } - } } }