feat: better error messaging and trying to fix teacher schedule
This commit is contained in:
@@ -67,4 +67,4 @@ README.md
|
|||||||
db/
|
db/
|
||||||
*.db
|
*.db
|
||||||
*.db-shm
|
*.db-shm
|
||||||
*.db-wal
|
*.db-wal
|
||||||
@@ -115,6 +115,23 @@ export async function getSchedule(groupID: number, groupName: string, wk?: numbe
|
|||||||
networkErrorCode: networkError.code,
|
networkErrorCode: networkError.code,
|
||||||
networkErrorMessage: networkError.message
|
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 {
|
} else {
|
||||||
// Логируем другие ошибки тоже
|
// Логируем другие ошибки тоже
|
||||||
logErrorToFile(errorObj, {
|
logErrorToFile(errorObj, {
|
||||||
@@ -125,6 +142,18 @@ export async function getSchedule(groupID: number, groupName: string, wk?: numbe
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else {
|
} 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
|
// Логируем ошибки без cause
|
||||||
logErrorToFile(errorObj, {
|
logErrorToFile(errorObj, {
|
||||||
type: 'unknown_error',
|
type: 'unknown_error',
|
||||||
@@ -234,6 +263,23 @@ export async function getTeacherSchedule(teacherID: number, teacherName: string,
|
|||||||
networkErrorCode: networkError.code,
|
networkErrorCode: networkError.code,
|
||||||
networkErrorMessage: networkError.message
|
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 {
|
} else {
|
||||||
// Логируем другие ошибки тоже
|
// Логируем другие ошибки тоже
|
||||||
logErrorToFile(errorObj, {
|
logErrorToFile(errorObj, {
|
||||||
@@ -244,6 +290,18 @@ export async function getTeacherSchedule(teacherID: number, teacherName: string,
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else {
|
} 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
|
// Логируем ошибки без cause
|
||||||
logErrorToFile(errorObj, {
|
logErrorToFile(errorObj, {
|
||||||
type: 'unknown_error',
|
type: 'unknown_error',
|
||||||
|
|||||||
@@ -624,19 +624,37 @@ const parseLesson = (row: Element, isTeacherSchedule: boolean = false): Lesson |
|
|||||||
}
|
}
|
||||||
} else if (isTeacherSchedule) {
|
} else if (isTeacherSchedule) {
|
||||||
// Для преподавателей место может быть в другом формате в тексте ячейки
|
// Для преподавателей место может быть в другом формате в тексте ячейки
|
||||||
// Формат: "ПредметГруппа(Аудитория)Адрес"
|
// Формат: "ПредметГруппа(Аудитория)Адрес" или в отдельной ячейке
|
||||||
const fullText = disciplineCell.textContent?.trim() || ''
|
// Сначала проверяем наличие отдельной ячейки с местом (как для групп)
|
||||||
if (fullText) {
|
const placeCellIndex = cells.length >= 6 ? 5 : (cells.length >= 5 ? 4 : -1)
|
||||||
// Ищем паттерн: группа в скобках и адрес после
|
if (placeCellIndex >= 0 && cells[placeCellIndex]) {
|
||||||
// Например: "(ИКС-8)Московское шоссе, 120" или "(ССА-15к)Моск"
|
const placeCell = cells[placeCellIndex]
|
||||||
const placeMatch = fullText.match(/\(([^)]+)\)([^(]+?)(?:\d+|$)/)
|
const placeText = placeCell.textContent?.trim() || ''
|
||||||
|
// Ищем адрес и кабинет в формате "адрес\nКабинет: номер"
|
||||||
|
const placeMatch = placeText.match(/([^\n]+)\n.*?Кабинет:\s*([^\s\n]+)/i)
|
||||||
if (placeMatch) {
|
if (placeMatch) {
|
||||||
const classroom = placeMatch[1].trim()
|
lesson.place = {
|
||||||
const address = placeMatch[2].trim()
|
address: placeMatch[1].trim(),
|
||||||
if (classroom && address) {
|
classroom: placeMatch[2].trim()
|
||||||
lesson.place = {
|
}
|
||||||
address,
|
}
|
||||||
classroom
|
}
|
||||||
|
|
||||||
|
// Если не нашли в отдельной ячейке, ищем в тексте ячейки с предметом
|
||||||
|
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: Если не нашли, ищем таблицу по имени в первой строке (может быть заголовок)
|
// Способ 2: Если не нашли, ищем таблицу по имени в первой строке (может быть заголовок)
|
||||||
if (!table) {
|
if (!table) {
|
||||||
table = tables.find(table => {
|
table = tables.find(table => {
|
||||||
const firstRow = table.querySelector(':scope > tbody > tr:first-child')
|
const firstRow = table.querySelector(':scope > tbody > tr:first-child') || table.querySelector(':scope > tr:first-child')
|
||||||
return firstRow?.textContent?.trim() === groupName
|
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 {
|
} else {
|
||||||
|
|||||||
@@ -96,10 +96,8 @@ export default function HomePage(props: NextSerialized<PageProps>) {
|
|||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<CardDescription className="text-base">
|
<CardDescription className="text-base whitespace-pre-wrap">
|
||||||
{error.isTimeout
|
{error?.message || 'Произошла ошибка при загрузке расписания'}
|
||||||
? 'Превышено время ожидания ответа от сервера. Пожалуйста, попробуйте обновить страницу через несколько минут.'
|
|
||||||
: 'Произошла ошибка при загрузке расписания. Пожалуйста, попробуйте обновить страницу позже.'}
|
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -270,11 +268,28 @@ export async function getServerSideProps(context: GetServerSidePropsContext<{ gr
|
|||||||
} else {
|
} else {
|
||||||
// Если кэша нет, возвращаем страницу с ошибкой вместо throw
|
// Если кэша нет, возвращаем страницу с ошибкой вместо throw
|
||||||
const isTimeout = e instanceof ScheduleTimeoutError
|
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(`Schedule fetch failed for group ${group}, no cache available:`, e)
|
||||||
|
console.error(`Error message: ${errorMessage}`)
|
||||||
|
|
||||||
const cacheAvailableFor = Array.from(cachedSchedules.entries())
|
const cacheAvailableFor = Array.from(cachedSchedules.entries())
|
||||||
.filter(([, v]) => v.lastFetched.getTime() + maxCacheDurationInMS > Date.now())
|
.filter(([, v]) => v.lastFetched.getTime() + maxCacheDurationInMS > Date.now())
|
||||||
|
|||||||
@@ -234,17 +234,19 @@ export default function HomePage(props: HomePageProps) {
|
|||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Кнопка перехода к расписанию преподавателей */}
|
||||||
|
<div
|
||||||
|
className="stagger-card mt-6"
|
||||||
|
style={{ animationDelay: `${0.15 + courseOffsets.totalGroups * 0.04 + 0.05}s` } as React.CSSProperties}
|
||||||
|
>
|
||||||
|
<Link href="/teachers" className="block">
|
||||||
|
<Button variant="default" className="w-full h-auto py-4 text-base font-semibold">
|
||||||
|
Расписание преподавателей
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col sm:flex-row items-center justify-center gap-3 mt-8">
|
<div className="flex flex-col sm:flex-row items-center justify-center gap-3 mt-8">
|
||||||
<div
|
|
||||||
className="stagger-card"
|
|
||||||
style={{ animationDelay: `${0.15 + courseOffsets.totalGroups * 0.04 + 0.05}s` } as React.CSSProperties}
|
|
||||||
>
|
|
||||||
<Link href="/teachers">
|
|
||||||
<Button variant="secondary" className="gap-2">
|
|
||||||
Преподаватели
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
{showAddGroupButton && (
|
{showAddGroupButton && (
|
||||||
<div
|
<div
|
||||||
className="stagger-card"
|
className="stagger-card"
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ import { getDayOfWeek } from '@/shared/utils'
|
|||||||
import Head from 'next/head'
|
import Head from 'next/head'
|
||||||
import { WeekInfo } from '@/app/parser/schedule'
|
import { WeekInfo } from '@/app/parser/schedule'
|
||||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/shadcn/ui/card'
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/shadcn/ui/card'
|
||||||
|
import { Button } from '@/shadcn/ui/button'
|
||||||
import { AlertCircle } from 'lucide-react'
|
import { AlertCircle } from 'lucide-react'
|
||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
type PageProps = {
|
type PageProps = {
|
||||||
schedule?: Day[]
|
schedule?: Day[]
|
||||||
@@ -99,9 +101,16 @@ export default function TeacherPage(props: NextSerialized<PageProps>) {
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
<CardDescription className="text-base">
|
<CardDescription className="text-base">
|
||||||
{error.isTimeout
|
{error.isTimeout
|
||||||
? 'Превышено время ожидания ответа от сервера. Пожалуйста, попробуйте обновить страницу через несколько минут.'
|
? 'Превышено время ожидания ответа от сервера при загрузке расписания преподавателя. Пожалуйста, попробуйте обновить страницу через несколько минут.'
|
||||||
: 'Произошла ошибка при загрузке расписания. Пожалуйста, попробуйте обновить страницу позже.'}
|
: `Не удалось загрузить расписание преподавателя ${teacher.name}. Возможно, расписание временно недоступно или произошла ошибка на сервере. Пожалуйста, попробуйте обновить страницу позже или вернитесь к списку преподавателей.`}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
|
<div className="mt-4">
|
||||||
|
<Link href="/teachers">
|
||||||
|
<Button variant="outline" className="w-full sm:w-auto">
|
||||||
|
Вернуться к списку преподавателей
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
@@ -284,35 +293,47 @@ export async function getServerSideProps(context: GetServerSidePropsContext<{ te
|
|||||||
} else {
|
} else {
|
||||||
console.warn(`Schedule fetch error for teacher ${teacherInfo.name}, using fallback cache from ${cachedSchedule.lastFetched.toISOString()} (${cacheAge} minutes old)`)
|
console.warn(`Schedule fetch error for teacher ${teacherInfo.name}, using fallback cache from ${cachedSchedule.lastFetched.toISOString()} (${cacheAge} minutes old)`)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Если кэша нет, возвращаем страницу с ошибкой вместо throw
|
// Если кэша нет, возвращаем страницу с ошибкой вместо throw
|
||||||
const isTimeout = e instanceof ScheduleTimeoutError
|
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') ||
|
||||||
console.error(`Schedule fetch failed for teacher ${teacherInfo.name}, no cache available:`, e)
|
errorMessageObj.message?.includes('certificate') ||
|
||||||
|
(errorMessageObj.cause instanceof Error && (
|
||||||
const cacheAvailableFor = Array.from(cachedTeacherSchedules.entries())
|
(errorMessageObj.cause as any).code === 'DEPTH_ZERO_SELF_SIGNED_CERT' ||
|
||||||
.filter(([, v]) => v.lastFetched.getTime() + maxCacheDurationInMS > Date.now())
|
errorMessageObj.cause.message?.includes('self-signed certificate')
|
||||||
.map(([k]) => k.split('_')[1]) // Берем parseId из ключа кэша
|
))
|
||||||
|
|
||||||
return {
|
const errorMessage = isTimeout
|
||||||
props: nextSerialized({
|
? 'Превышено время ожидания ответа от сервера'
|
||||||
teacher: {
|
: isSSLError
|
||||||
id: teacherInfo.id,
|
? 'В колледже что-то сломалось (проблема с сертификатом безопасности). Здесь я бессилен, проблема не на моей стороне.'
|
||||||
name: teacherInfo.name
|
: errorMessageObj.message || 'Произошла ошибка при загрузке расписания'
|
||||||
},
|
|
||||||
cacheAvailableFor,
|
console.error(`Schedule fetch failed for teacher ${teacherInfo.name}, no cache available:`, e)
|
||||||
groups,
|
|
||||||
settings,
|
const cacheAvailableFor = Array.from(cachedTeacherSchedules.entries())
|
||||||
error: {
|
.filter(([, v]) => v.lastFetched.getTime() + maxCacheDurationInMS > Date.now())
|
||||||
message: errorMessage,
|
.map(([k]) => k.split('_')[1]) // Берем parseId из ключа кэша
|
||||||
isTimeout
|
|
||||||
}
|
return {
|
||||||
}) as NextSerialized<PageProps>
|
props: nextSerialized({
|
||||||
|
teacher: {
|
||||||
|
id: teacherInfo.id,
|
||||||
|
name: teacherInfo.name
|
||||||
|
},
|
||||||
|
cacheAvailableFor,
|
||||||
|
groups,
|
||||||
|
settings,
|
||||||
|
error: {
|
||||||
|
message: errorMessage,
|
||||||
|
isTimeout
|
||||||
|
}
|
||||||
|
}) as NextSerialized<PageProps>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user