fix: several fixes

This commit is contained in:
kilyabin
2026-03-05 14:59:44 +04:00
parent c43bed396e
commit 67a7374b1a
8 changed files with 389 additions and 41 deletions

View File

@@ -0,0 +1,98 @@
#!/usr/bin/env node
/**
* Скрипт для проверки базы данных преподавателей
* Запуск: node scripts/test-teachers-db.js
*/
const path = require('path');
const fs = require('fs');
// Определяем директорию базы данных
function getDatabaseDir() {
if (process.env.DATABASE_DIR) {
return process.env.DATABASE_DIR;
}
const cwd = process.cwd();
console.log(`Current working directory: ${cwd}`);
if (cwd.includes('.next/standalone')) {
const standaloneMatch = cwd.match(/^(.+?)\/\.next\/standalone/);
if (standaloneMatch && standaloneMatch[1]) {
return standaloneMatch[1];
}
return path.resolve(cwd, '..', '..');
}
if (fs.existsSync('/opt/kspguti-schedule')) {
return '/opt/kspguti-schedule';
}
return cwd;
}
const DATABASE_DIR = getDatabaseDir();
const DB_PATH = path.join(DATABASE_DIR, 'db', 'schedule-app.db');
console.log(`Database directory: ${DATABASE_DIR}`);
console.log(`Database path: ${DB_PATH}`);
console.log(`Database exists: ${fs.existsSync(DB_PATH)}`);
if (!fs.existsSync(DB_PATH)) {
console.error('Database file does not exist!');
process.exit(1);
}
// Проверяем права доступа
try {
fs.accessSync(DB_PATH, fs.constants.R_OK | fs.constants.W_OK);
console.log('Database file is readable and writable');
} catch (err) {
console.error('Database file permissions error:', err.message);
process.exit(1);
}
// Подключаемся к базе данных
const Database = require('better-sqlite3');
const db = new Database(DB_PATH);
// Проверяем таблицу teachers
console.log('\n=== Teachers Table ===');
const teachersCount = db.prepare('SELECT COUNT(*) as count FROM teachers').get();
console.log(`Total teachers in database: ${teachersCount.count}`);
if (teachersCount.count > 0) {
const teachers = db.prepare('SELECT id, parseId, name FROM teachers LIMIT 10').all();
console.log('First 10 teachers:');
teachers.forEach((t, i) => {
console.log(` ${i + 1}. [${t.id}] ${t.name} (parseId: ${t.parseId})`);
});
} else {
console.log('Teachers table is EMPTY!');
}
// Проверяем таблицу groups
console.log('\n=== Groups Table ===');
const groupsCount = db.prepare('SELECT COUNT(*) as count FROM groups').get();
console.log(`Total groups in database: ${groupsCount.count}`);
if (groupsCount.count > 0) {
const groups = db.prepare('SELECT id, parseId, name, course FROM groups LIMIT 10').all();
console.log('First 10 groups:');
groups.forEach((g, i) => {
console.log(` ${i + 1}. [${g.id}] ${g.name} (parseId: ${g.parseId}, course: ${g.course})`);
});
}
// Проверяем таблицу settings
console.log('\n=== Settings Table ===');
const settings = db.prepare('SELECT value FROM settings WHERE key = ?').get('app');
if (settings) {
console.log('App settings:', settings.value);
} else {
console.log('No app settings found');
}
db.close();
console.log('\nDone!');

View File

@@ -0,0 +1,76 @@
#!/usr/bin/env node
/**
* Тест парсера преподавателей
* Запуск: node scripts/test-teachers-parser.js
*/
const { JSDOM } = require('jsdom');
const path = require('path');
const fs = require('fs');
// Импортируем парсер
const { parseTeachersList } = require('./src/app/parser/teachers-list');
// HTML с сервера (сохраните в файл или передайте через аргумент)
const testHtml = `
<html>
<body>
<table border="0" cellpadding="1" cellspacing="1" width="100%" bgcolor="ffffff">
<tr><td bgcolor='eeeeee' align=center><a href='?mn=3&obj=3'><b>Абалымова Людмила Павловна</b></a></td></tr>
<tr><td bgcolor='dddddd' align=center><a href='?mn=3&obj=4'><b>Абрамова Светлана Геннадьевна</b></a></td></tr>
<tr><td bgcolor='eeeeee' align=center><a href='?mn=3&obj=253'><b>Айриянц Илона Артуровна</b></a></td></tr>
<tr><td bgcolor='dddddd' align=center><a href='?mn=3&obj=2'><b>Алёхин Иван Николаевич</b></a></td></tr>
<tr><td bgcolor='eeeeee' align=center><a href='?mn=3&obj=65'><b>Андреевская Наталья Владимировна</b></a></td></tr>
</table>
</body>
</html>
`;
console.log('=== Testing Teachers Parser ===\n');
// Создаём JSDOM
const dom = new JSDOM(testHtml, { url: 'https://lk.ks.psuti.ru/?mn=3' });
const document = dom.window.document;
// Проверяем, находит ли селектор ссылки
const links = Array.from(document.querySelectorAll('a[href*="?mn=3&obj="], a[href*="mn=3&obj="]'));
console.log(`Links found by selector: ${links.length}`);
links.forEach((link, i) => {
console.log(` ${i + 1}. href="${link.getAttribute('href')}", text="${link.textContent?.trim()}"`);
});
// Запускаем парсер
const teachers = parseTeachersList(document);
console.log(`\nTeachers parsed: ${teachers.length}`);
teachers.forEach((t, i) => {
console.log(` ${i + 1}. [${t.parseId}] ${t.name}`);
});
dom.window.close();
// Теперь тестируем на реальном HTML с сервера
console.log('\n\n=== Testing with Real HTML from Server ===\n');
const realHtmlPath = path.join(__dirname, 'teachers-test.html');
if (fs.existsSync(realHtmlPath)) {
const realHtml = fs.readFileSync(realHtmlPath, 'utf8');
const realDom = new JSDOM(realHtml, { url: 'https://lk.ks.psuti.ru/?mn=3' });
const realDocument = realDom.window.document;
const realTeachers = parseTeachersList(realDocument);
console.log(`Real teachers parsed: ${realTeachers.length}`);
realTeachers.slice(0, 10).forEach((t, i) => {
console.log(` ${i + 1}. [${t.parseId}] ${t.name}`);
});
if (realTeachers.length > 10) {
console.log(` ... and ${realTeachers.length - 10} more`);
}
realDom.window.close();
} else {
console.log(`Test file not found: ${realHtmlPath}`);
console.log('To test with real HTML, save the curl output to scripts/teachers-test.html');
console.log('Example: curl -L "https://lk.ks.psuti.ru/?mn=3" > scripts/teachers-test.html');
}

View File

@@ -421,7 +421,8 @@ function parseTeacherSchedule(
currentWeekNumber = weekNumber
}
// Ищем родительскую таблицу с парами (cellpadding="1")
// Ищем родительскую таблицу с парами
// Сначала пробуем найти по cellpadding="1"
let parent: Element | null = anchor as Element
for (let i = 0; i < 10 && parent; i++) {
parent = parent.parentElement
@@ -430,6 +431,24 @@ function parseTeacherSchedule(
}
}
// Если не нашли по cellpadding, ищем просто ближайшую таблицу
if (!parent || parent.tagName !== 'TABLE') {
parent = anchor.closest('table')
}
// Если все еще не нашли, ищем таблицу рядом с якорем
if (!parent || parent.tagName !== 'TABLE') {
// Ищем следующую таблицу после якоря
let nextSibling: Node | null = anchor as Node
while (nextSibling) {
nextSibling = nextSibling.nextSibling
if (nextSibling && nextSibling.nodeType === 1 && (nextSibling as Element).tagName === 'TABLE') {
parent = nextSibling as Element
break
}
}
}
const lessons: Lesson[] = []
if (parent && parent.tagName === 'TABLE') {
@@ -448,7 +467,8 @@ function parseTeacherSchedule(
const endTime = (endTimeRaw || '').trim()
const subjCell = cells[2]
const roomText = cells[3].textContent?.trim() || ''
// Проверяем наличие ячейки перед доступом к textContent
const roomText = cells[3]?.textContent?.trim() || ''
// Извлекаем предмет, аудиторию и тип занятия по логике pythonпарсера
let subject = ''
@@ -457,19 +477,20 @@ function parseTeacherSchedule(
let lessonType = ''
let location = ''
const bold = subjCell.querySelector('b')
// Проверяем наличие subjCell перед поиском элементов
const bold = subjCell?.querySelector('b')
if (bold) {
subject = bold.textContent?.trim() || ''
}
const fontGreen = subjCell.querySelector('font.t_green_10')
const fontGreen = subjCell?.querySelector('font.t_green_10')
if (fontGreen) {
location = fontGreen.textContent?.trim() || ''
}
// Всё, что идёт после <b> до <font>, это строка с группой и типом занятия
let raw = ''
if (bold) {
if (bold && subjCell) {
let node: ChildNode | null = bold.nextSibling
while (node) {
const nodeType = (node as any).nodeType
@@ -1048,7 +1069,24 @@ export function parsePage(
// Для расписания преподавателей используем отдельный, более надежный парсер,
// основанный на уже отлаженной pythonверсии.
if (isTeacherSchedule) {
return parseTeacherSchedule(document, url, shouldParseWeekNavigation)
try {
const result = parseTeacherSchedule(document, url, shouldParseWeekNavigation)
// Если парсер не нашел дней, пробуем fallback на parseGroupSchedule
if (result.days.length === 0) {
logDebug('parsePage: parseTeacherSchedule returned no days, trying fallback')
return parseGroupSchedule(document, groupName, url, shouldParseWeekNavigation)
}
return result
} catch (error) {
// При ошибке парсинга преподавателя, пробуем fallback
logDebug('parsePage: parseTeacherSchedule failed, trying fallback', { error })
try {
return parseGroupSchedule(document, groupName, url, shouldParseWeekNavigation)
} catch (fallbackError) {
// Если и fallback не сработал, выбрасываем оригинальную ошибку
throw error
}
}
}
// Для расписания групп используем отдельный парсер, который опирается на структуру

View File

@@ -14,7 +14,7 @@ export type TeacherListItem = {
export function parseTeachersList(document: Document): TeacherListItem[] {
const teachers: TeacherListItem[] = []
// Ищем все ссылки, которые содержат ?mn=3&obj=
// Способ 1: Ищем все ссылки, которые содержат ?mn=3&obj= или mn=3&obj=
const links = Array.from(document.querySelectorAll('a[href*="?mn=3&obj="], a[href*="mn=3&obj="]'))
for (const link of links) {
@@ -38,7 +38,7 @@ export function parseTeachersList(document: Document): TeacherListItem[] {
}
}
// Если не нашли ссылки, пытаемся найти в таблице
// Способ 2: Если не нашли ссылки, пытаемся найти в таблице
if (teachers.length === 0) {
const tables = Array.from(document.querySelectorAll('table'))
for (const table of tables) {
@@ -66,6 +66,49 @@ export function parseTeachersList(document: Document): TeacherListItem[] {
}
}
// Способ 3: Ищем все ссылки с obj= в URL (более общий поиск)
if (teachers.length === 0) {
const allLinks = Array.from(document.querySelectorAll('a[href*="obj="]'))
for (const link of allLinks) {
const href = link.getAttribute('href')
if (!href) continue
// Проверяем, что это mn=3 (преподаватели)
if (!href.includes('mn=3')) continue
const objMatch = href.match(/[?&]obj=(\d+)/)
if (!objMatch) continue
const parseId = Number(objMatch[1])
if (isNaN(parseId) || parseId <= 0) continue
const name = link.textContent?.trim()
if (!name || name.length === 0) continue
if (!teachers.find(t => t.parseId === parseId)) {
teachers.push({ parseId, name })
}
}
}
// Способ 4: Ищем в формах и input элементах
if (teachers.length === 0) {
const forms = Array.from(document.querySelectorAll('form[action*="mn=3"]'))
for (const form of forms) {
const action = form.getAttribute('action') || ''
const objMatch = action.match(/[?&]obj=(\d+)/)
if (objMatch) {
const parseId = Number(objMatch[1])
if (!isNaN(parseId) && parseId > 0) {
const name = form.textContent?.trim() || `Преподаватель ${parseId}`
if (!teachers.find(t => t.parseId === parseId)) {
teachers.push({ parseId, name })
}
}
}
}
}
// Сортируем по имени
teachers.sort((a, b) => a.name.localeCompare(b.name))

View File

@@ -88,11 +88,17 @@ async function handler(
...(validatedDebug !== undefined && { debug: validatedDebug })
}
try {
saveSettings(settings)
// Сбрасываем кеш и загружаем свежие настройки для подтверждения
clearSettingsCache()
const savedSettings = loadSettings(true)
res.status(200).json({ success: true, settings: savedSettings })
} catch (error) {
console.error('Error saving settings:', error)
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
res.status(500).json({ error: `Failed to save settings: ${errorMessage}` })
}
return
}
}

View File

@@ -27,31 +27,71 @@ async function handler(
// Парсинг и обновление списка преподавателей
try {
const url = `${PROXY_URL}/?mn=3`
console.log(`[Teachers API] Fetching teachers list from: ${url}`)
console.log(`[Teachers API] PROXY_URL: ${PROXY_URL}`)
// Добавляем таймаут 10 секунд для fetch запроса
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), 10000)
const page = await fetch(url, { signal: controller.signal })
const page = await fetch(url, {
signal: controller.signal,
redirect: 'follow',
headers: {
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
}
})
clearTimeout(timeoutId)
console.log(`[Teachers API] Response status: ${page.status}`)
console.log(`[Teachers API] Response URL: ${page.url}`)
console.log(`[Teachers API] Response redirected: ${page.redirected}`)
const content = await page.text()
const contentType = page.headers.get('content-type')
console.log(`[Teachers API] Content length: ${content.length}, Content-Type: ${contentType}`)
if (page.status !== 200 || !contentType || contentTypeParser.parse(contentType).type !== 'text/html') {
console.error(`[Teachers API] Invalid response: status ${page.status}, contentType: ${contentType}`)
res.status(500).json({ error: `Failed to fetch teachers list: status ${page.status}` })
return
}
// Проверяем, не редирект ли на страницу авторизации
if (content.includes('login') || content.includes('auth') || content.includes('Вход') || content.includes('Авторизация')) {
console.error('[Teachers API] Response appears to be a login page, not teachers list')
}
const dom = new JSDOM(content, { url })
const document = dom.window.document
// Логируем заголовок страницы для отладки
const pageTitle = document.title
console.log(`[Teachers API] Page title: ${pageTitle}`)
// Логируем немного HTML для отладки
const htmlPreview = content.substring(0, 500).replace(/\n/g, ' ')
console.log(`[Teachers API] HTML preview: ${htmlPreview}...`)
const teachersList = parseTeachersList(document)
console.log(`[Teachers API] Parsed ${teachersList.length} teachers`)
// Закрываем JSDOM для освобождения памяти
dom.window.close()
if (teachersList.length === 0) {
console.error('[Teachers API] No teachers found in HTML')
// Логируем больше информации для отладки
const hasMn3 = content.includes('mn=3')
const hasObj = content.includes('obj=')
const hasTeachersTable = content.includes('Преподаватель') || content.includes('преподавател')
console.log(`[Teachers API] HTML contains 'mn=3': ${hasMn3}, contains 'obj=': ${hasObj}, contains 'преподавател': ${hasTeachersTable}`)
// Проверяем, не ошибка ли это
if (content.includes('Ошибка') || content.includes('Error') || content.includes('404') || content.includes('500')) {
console.error('[Teachers API] Response contains error indicators')
}
res.status(500).json({ error: 'No teachers found on the page' })
return
}
@@ -66,9 +106,11 @@ async function handler(
name: teacher.name
}
}
console.log(`[Teachers API] Created TeachersData with ${Object.keys(teachersData).length} entries`)
// Сохраняем в БД
saveTeachers(teachersData)
console.log('[Teachers API] Saved teachers to database')
// Сохраняем timestamp последнего обновления
const { setTeachersLastUpdateTime } = await import('@/shared/data/database')
@@ -77,6 +119,7 @@ async function handler(
// Сбрасываем кеш и загружаем свежие данные из БД
clearTeachersCache()
const updatedTeachers = loadTeachers(true)
console.log(`[Teachers API] Loaded ${Object.keys(updatedTeachers).length} teachers from database`)
res.status(200).json({
success: true,

View File

@@ -251,6 +251,7 @@ export function getAllTeachers(): TeachersData {
}
}
console.log(`[Database] getAllTeachers: found ${Object.keys(teachers).length} teachers`)
return teachers
}
@@ -592,6 +593,48 @@ function migrateFromJSON(): void {
console.error('Error hashing default password:', err)
}
}
// Мигрируем преподавателей из teachers.ts, если БД пустая
const teachersCount = database.prepare('SELECT COUNT(*) as count FROM teachers').get() as { count: number }
if (teachersCount.count === 0) {
try {
// Пытаемся импортировать преподавателей из teachers.ts
const possiblePaths = [
path.join(process.cwd(), 'src/shared/data/teachers.ts'),
path.join(process.cwd(), '.next/standalone/src/shared/data/teachers.ts'),
path.join(process.cwd(), 'teachers.ts')
]
for (const filePath of possiblePaths) {
if (fs.existsSync(filePath)) {
console.log(`Migrating teachers from ${filePath}...`)
// Читаем файл и извлекаем JSON массив
const fileContents = fs.readFileSync(filePath, 'utf8')
const jsonMatch = fileContents.match(/export const teachers = (\[[\s\S]*?\])/)
if (jsonMatch && jsonMatch[1]) {
const teachersArray = JSON.parse(jsonMatch[1]) as Array<{ name: string }>
const insertStmt = database.prepare('INSERT INTO teachers (id, parseId, name) VALUES (?, ?, ?)')
const transaction = database.transaction((teachers: Array<{ name: string }>) => {
teachers.forEach((teacher, index) => {
if (teacher.name) {
// Используем индекс как parseId, так как в teachers.ts нет parseId
const id = String(index + 1)
insertStmt.run(id, index + 1, teacher.name)
}
})
})
transaction(teachersArray)
console.log(`Teachers migrated from teachers.ts: ${teachersArray.length} teachers`)
break
}
}
}
} catch (error) {
console.error('Error migrating teachers from teachers.ts:', error)
}
}
}
// Экспортируем функцию для закрытия соединения (полезно для тестов)

View File

@@ -19,6 +19,7 @@ export function loadTeachers(forceRefresh: boolean = false): TeachersData {
try {
cachedTeachers = getAllTeachersFromDB()
cacheTimestamp = now
console.log(`[TeachersLoader] Loaded ${Object.keys(cachedTeachers).length} teachers from database`)
return cachedTeachers
} catch (error) {
console.error('Error loading teachers from database:', error)