diff --git a/scripts/test-teachers-db.js b/scripts/test-teachers-db.js new file mode 100644 index 0000000..889e706 --- /dev/null +++ b/scripts/test-teachers-db.js @@ -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!'); diff --git a/scripts/test-teachers-parser.js b/scripts/test-teachers-parser.js new file mode 100644 index 0000000..2fa86c0 --- /dev/null +++ b/scripts/test-teachers-parser.js @@ -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 = ` + + + + + + + + +
Абалымова Людмила Павловна
Абрамова Светлана Геннадьевна
Айриянц Илона Артуровна
Алёхин Иван Николаевич
Андреевская Наталья Владимировна
+ + +`; + +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'); +} diff --git a/src/app/parser/schedule.ts b/src/app/parser/schedule.ts index 56d2a6c..144bdcd 100644 --- a/src/app/parser/schedule.ts +++ b/src/app/parser/schedule.ts @@ -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() || '' } // Всё, что идёт после до , это строка с группой и типом занятия 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 + } + } } // Для расписания групп используем отдельный парсер, который опирается на структуру diff --git a/src/app/parser/teachers-list.ts b/src/app/parser/teachers-list.ts index f51928e..b03c922 100644 --- a/src/app/parser/teachers-list.ts +++ b/src/app/parser/teachers-list.ts @@ -13,32 +13,32 @@ 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) { const href = link.getAttribute('href') if (!href) continue - + // Парсим URL вида ?mn=3&obj=XXX или /?mn=3&obj=XXX 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 }) } } - - // Если не нашли ссылки, пытаемся найти в таблице + + // Способ 2: Если не нашли ссылки, пытаемся найти в таблице if (teachers.length === 0) { const tables = Array.from(document.querySelectorAll('table')) for (const table of tables) { @@ -46,28 +46,71 @@ export function parseTeachersList(document: Document): TeacherListItem[] { for (const row of rows) { const link = row.querySelector('a[href*="obj="]') if (!link) continue - + const href = link.getAttribute('href') if (!href || !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() || row.textContent?.trim() if (!name || name.length === 0) continue - + if (!teachers.find(t => t.parseId === parseId)) { teachers.push({ parseId, name }) } } } } - + + // Способ 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)) - + return teachers } diff --git a/src/pages/api/admin/settings.ts b/src/pages/api/admin/settings.ts index 23fffe6..70e9243 100644 --- a/src/pages/api/admin/settings.ts +++ b/src/pages/api/admin/settings.ts @@ -88,11 +88,17 @@ async function handler( ...(validatedDebug !== undefined && { debug: validatedDebug }) } - saveSettings(settings) - // Сбрасываем кеш и загружаем свежие настройки для подтверждения - clearSettingsCache() - const savedSettings = loadSettings(true) - res.status(200).json({ success: true, settings: savedSettings }) + 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 } } diff --git a/src/pages/api/admin/teachers.ts b/src/pages/api/admin/teachers.ts index 26544c7..104adce 100644 --- a/src/pages/api/admin/teachers.ts +++ b/src/pages/api/admin/teachers.ts @@ -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,20 +106,23 @@ 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') setTeachersLastUpdateTime(Date.now()) - + // Сбрасываем кеш и загружаем свежие данные из БД clearTeachersCache() const updatedTeachers = loadTeachers(true) - - res.status(200).json({ - success: true, + console.log(`[Teachers API] Loaded ${Object.keys(updatedTeachers).length} teachers from database`) + + res.status(200).json({ + success: true, teachers: updatedTeachers, parsed: teachersList.length }) diff --git a/src/shared/data/database.ts b/src/shared/data/database.ts index 466ed9f..7189bf4 100644 --- a/src/shared/data/database.ts +++ b/src/shared/data/database.ts @@ -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) + } + } } // Экспортируем функцию для закрытия соединения (полезно для тестов) diff --git a/src/shared/data/teachers-loader.ts b/src/shared/data/teachers-loader.ts index 6e4e205..08f086a 100644 --- a/src/shared/data/teachers-loader.ts +++ b/src/shared/data/teachers-loader.ts @@ -11,7 +11,7 @@ const CACHE_TTL_MS = 1000 * 60 // 1 минута export function loadTeachers(forceRefresh: boolean = false): TeachersData { const now = Date.now() const isCacheValid = cachedTeachers !== null && !forceRefresh && (now - cacheTimestamp) < CACHE_TTL_MS - + if (isCacheValid && cachedTeachers !== null) { return cachedTeachers } @@ -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)