Revert last 3 commits
This commit is contained in:
@@ -1,8 +1,5 @@
|
|||||||
# Production environment variables for KSPGUTI Schedule
|
# Production environment variables for KSPGUTI Schedule
|
||||||
|
|
||||||
# Database directory (where schedule-app.db will be stored)
|
|
||||||
DATABASE_DIR=/opt/kspguti-schedule
|
|
||||||
|
|
||||||
# Site URL - used for canonical links and sitemap (optional, defaults to https://schedule.itlxrd.space)
|
# Site URL - used for canonical links and sitemap (optional, defaults to https://schedule.itlxrd.space)
|
||||||
NEXT_PUBLIC_SITE_URL=https://schedule.itlxrd.space
|
NEXT_PUBLIC_SITE_URL=https://schedule.itlxrd.space
|
||||||
|
|
||||||
|
|||||||
@@ -1,98 +0,0 @@
|
|||||||
#!/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!');
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
#!/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');
|
|
||||||
}
|
|
||||||
@@ -421,8 +421,7 @@ function parseTeacherSchedule(
|
|||||||
currentWeekNumber = weekNumber
|
currentWeekNumber = weekNumber
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ищем родительскую таблицу с парами
|
// Ищем родительскую таблицу с парами (cellpadding="1")
|
||||||
// Сначала пробуем найти по cellpadding="1"
|
|
||||||
let parent: Element | null = anchor as Element
|
let parent: Element | null = anchor as Element
|
||||||
for (let i = 0; i < 10 && parent; i++) {
|
for (let i = 0; i < 10 && parent; i++) {
|
||||||
parent = parent.parentElement
|
parent = parent.parentElement
|
||||||
@@ -431,24 +430,6 @@ 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[] = []
|
const lessons: Lesson[] = []
|
||||||
|
|
||||||
if (parent && parent.tagName === 'TABLE') {
|
if (parent && parent.tagName === 'TABLE') {
|
||||||
@@ -467,8 +448,7 @@ function parseTeacherSchedule(
|
|||||||
const endTime = (endTimeRaw || '').trim()
|
const endTime = (endTimeRaw || '').trim()
|
||||||
|
|
||||||
const subjCell = cells[2]
|
const subjCell = cells[2]
|
||||||
// Проверяем наличие ячейки перед доступом к textContent
|
const roomText = cells[3].textContent?.trim() || ''
|
||||||
const roomText = cells[3]?.textContent?.trim() || ''
|
|
||||||
|
|
||||||
// Извлекаем предмет, аудиторию и тип занятия по логике python‑парсера
|
// Извлекаем предмет, аудиторию и тип занятия по логике python‑парсера
|
||||||
let subject = ''
|
let subject = ''
|
||||||
@@ -477,20 +457,19 @@ function parseTeacherSchedule(
|
|||||||
let lessonType = ''
|
let lessonType = ''
|
||||||
let location = ''
|
let location = ''
|
||||||
|
|
||||||
// Проверяем наличие subjCell перед поиском элементов
|
const bold = subjCell.querySelector('b')
|
||||||
const bold = subjCell?.querySelector('b')
|
|
||||||
if (bold) {
|
if (bold) {
|
||||||
subject = bold.textContent?.trim() || ''
|
subject = bold.textContent?.trim() || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
const fontGreen = subjCell?.querySelector('font.t_green_10')
|
const fontGreen = subjCell.querySelector('font.t_green_10')
|
||||||
if (fontGreen) {
|
if (fontGreen) {
|
||||||
location = fontGreen.textContent?.trim() || ''
|
location = fontGreen.textContent?.trim() || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
// Всё, что идёт после <b> до <font>, это строка с группой и типом занятия
|
// Всё, что идёт после <b> до <font>, это строка с группой и типом занятия
|
||||||
let raw = ''
|
let raw = ''
|
||||||
if (bold && subjCell) {
|
if (bold) {
|
||||||
let node: ChildNode | null = bold.nextSibling
|
let node: ChildNode | null = bold.nextSibling
|
||||||
while (node) {
|
while (node) {
|
||||||
const nodeType = (node as any).nodeType
|
const nodeType = (node as any).nodeType
|
||||||
@@ -1069,24 +1048,7 @@ export function parsePage(
|
|||||||
// Для расписания преподавателей используем отдельный, более надежный парсер,
|
// Для расписания преподавателей используем отдельный, более надежный парсер,
|
||||||
// основанный на уже отлаженной python‑версии.
|
// основанный на уже отлаженной python‑версии.
|
||||||
if (isTeacherSchedule) {
|
if (isTeacherSchedule) {
|
||||||
try {
|
return parseTeacherSchedule(document, url, shouldParseWeekNavigation)
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Для расписания групп используем отдельный парсер, который опирается на структуру
|
// Для расписания групп используем отдельный парсер, который опирается на структуру
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export type TeacherListItem = {
|
|||||||
export function parseTeachersList(document: Document): TeacherListItem[] {
|
export function parseTeachersList(document: Document): TeacherListItem[] {
|
||||||
const teachers: TeacherListItem[] = []
|
const teachers: TeacherListItem[] = []
|
||||||
|
|
||||||
// Способ 1: Ищем все ссылки, которые содержат ?mn=3&obj= или mn=3&obj=
|
// Ищем все ссылки, которые содержат ?mn=3&obj=
|
||||||
const links = Array.from(document.querySelectorAll('a[href*="?mn=3&obj="], a[href*="mn=3&obj="]'))
|
const links = Array.from(document.querySelectorAll('a[href*="?mn=3&obj="], a[href*="mn=3&obj="]'))
|
||||||
|
|
||||||
for (const link of links) {
|
for (const link of links) {
|
||||||
@@ -38,7 +38,7 @@ export function parseTeachersList(document: Document): TeacherListItem[] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Способ 2: Если не нашли ссылки, пытаемся найти в таблице
|
// Если не нашли ссылки, пытаемся найти в таблице
|
||||||
if (teachers.length === 0) {
|
if (teachers.length === 0) {
|
||||||
const tables = Array.from(document.querySelectorAll('table'))
|
const tables = Array.from(document.querySelectorAll('table'))
|
||||||
for (const table of tables) {
|
for (const table of tables) {
|
||||||
@@ -66,49 +66,6 @@ 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))
|
teachers.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
|
|
||||||
|
|||||||
@@ -88,17 +88,11 @@ async function handler(
|
|||||||
...(validatedDebug !== undefined && { debug: validatedDebug })
|
...(validatedDebug !== undefined && { debug: validatedDebug })
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
saveSettings(settings)
|
||||||
saveSettings(settings)
|
// Сбрасываем кеш и загружаем свежие настройки для подтверждения
|
||||||
// Сбрасываем кеш и загружаем свежие настройки для подтверждения
|
clearSettingsCache()
|
||||||
clearSettingsCache()
|
const savedSettings = loadSettings(true)
|
||||||
const savedSettings = loadSettings(true)
|
res.status(200).json({ success: true, settings: savedSettings })
|
||||||
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
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,71 +27,31 @@ async function handler(
|
|||||||
// Парсинг и обновление списка преподавателей
|
// Парсинг и обновление списка преподавателей
|
||||||
try {
|
try {
|
||||||
const url = `${PROXY_URL}/?mn=3`
|
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 запроса
|
// Добавляем таймаут 10 секунд для fetch запроса
|
||||||
const controller = new AbortController()
|
const controller = new AbortController()
|
||||||
const timeoutId = setTimeout(() => controller.abort(), 10000)
|
const timeoutId = setTimeout(() => controller.abort(), 10000)
|
||||||
|
|
||||||
const page = await fetch(url, {
|
const page = await fetch(url, { signal: controller.signal })
|
||||||
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)
|
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 content = await page.text()
|
||||||
const contentType = page.headers.get('content-type')
|
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') {
|
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}` })
|
res.status(500).json({ error: `Failed to fetch teachers list: status ${page.status}` })
|
||||||
return
|
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 dom = new JSDOM(content, { url })
|
||||||
const document = dom.window.document
|
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)
|
const teachersList = parseTeachersList(document)
|
||||||
console.log(`[Teachers API] Parsed ${teachersList.length} teachers`)
|
|
||||||
|
|
||||||
// Закрываем JSDOM для освобождения памяти
|
// Закрываем JSDOM для освобождения памяти
|
||||||
dom.window.close()
|
dom.window.close()
|
||||||
|
|
||||||
if (teachersList.length === 0) {
|
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' })
|
res.status(500).json({ error: 'No teachers found on the page' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -106,11 +66,9 @@ async function handler(
|
|||||||
name: teacher.name
|
name: teacher.name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log(`[Teachers API] Created TeachersData with ${Object.keys(teachersData).length} entries`)
|
|
||||||
|
|
||||||
// Сохраняем в БД
|
// Сохраняем в БД
|
||||||
saveTeachers(teachersData)
|
saveTeachers(teachersData)
|
||||||
console.log('[Teachers API] Saved teachers to database')
|
|
||||||
|
|
||||||
// Сохраняем timestamp последнего обновления
|
// Сохраняем timestamp последнего обновления
|
||||||
const { setTeachersLastUpdateTime } = await import('@/shared/data/database')
|
const { setTeachersLastUpdateTime } = await import('@/shared/data/database')
|
||||||
@@ -119,7 +77,6 @@ async function handler(
|
|||||||
// Сбрасываем кеш и загружаем свежие данные из БД
|
// Сбрасываем кеш и загружаем свежие данные из БД
|
||||||
clearTeachersCache()
|
clearTeachersCache()
|
||||||
const updatedTeachers = loadTeachers(true)
|
const updatedTeachers = loadTeachers(true)
|
||||||
console.log(`[Teachers API] Loaded ${Object.keys(updatedTeachers).length} teachers from database`)
|
|
||||||
|
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -9,13 +9,11 @@ import type { AppSettings } from './settings-loader'
|
|||||||
function getDatabaseDir(): string {
|
function getDatabaseDir(): string {
|
||||||
// Если указан путь через переменную окружения, используем его
|
// Если указан путь через переменную окружения, используем его
|
||||||
if (process.env.DATABASE_DIR) {
|
if (process.env.DATABASE_DIR) {
|
||||||
console.log(`[Database] Using DATABASE_DIR from env: ${process.env.DATABASE_DIR}`)
|
|
||||||
return process.env.DATABASE_DIR
|
return process.env.DATABASE_DIR
|
||||||
}
|
}
|
||||||
|
|
||||||
// В production режиме (standalone) используем стандартный путь
|
// В production режиме (standalone) используем стандартный путь
|
||||||
const cwd = process.cwd()
|
const cwd = process.cwd()
|
||||||
console.log(`[Database] process.cwd(): ${cwd}`)
|
|
||||||
|
|
||||||
// Если мы в .next/standalone, поднимаемся на 2 уровня вверх к корню проекта
|
// Если мы в .next/standalone, поднимаемся на 2 уровня вверх к корню проекта
|
||||||
if (cwd.includes('.next/standalone')) {
|
if (cwd.includes('.next/standalone')) {
|
||||||
@@ -23,23 +21,18 @@ function getDatabaseDir(): string {
|
|||||||
// Нужно подняться до /opt/kspguti-schedule
|
// Нужно подняться до /opt/kspguti-schedule
|
||||||
const standaloneMatch = cwd.match(/^(.+?)\/\.next\/standalone/)
|
const standaloneMatch = cwd.match(/^(.+?)\/\.next\/standalone/)
|
||||||
if (standaloneMatch && standaloneMatch[1]) {
|
if (standaloneMatch && standaloneMatch[1]) {
|
||||||
console.log(`[Database] Detected standalone mode, using: ${standaloneMatch[1]}`)
|
|
||||||
return standaloneMatch[1]
|
return standaloneMatch[1]
|
||||||
}
|
}
|
||||||
// Альтернативный способ: подняться на 2 уровня вверх
|
// Альтернативный способ: подняться на 2 уровня вверх
|
||||||
const parentDir = path.resolve(cwd, '..', '..')
|
return path.resolve(cwd, '..', '..')
|
||||||
console.log(`[Database] Fallback to parent directory: ${parentDir}`)
|
|
||||||
return parentDir
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Проверяем стандартный путь для production
|
// Проверяем стандартный путь для production
|
||||||
if (fs.existsSync('/opt/kspguti-schedule')) {
|
if (fs.existsSync('/opt/kspguti-schedule')) {
|
||||||
console.log('[Database] Using /opt/kspguti-schedule')
|
|
||||||
return '/opt/kspguti-schedule'
|
return '/opt/kspguti-schedule'
|
||||||
}
|
}
|
||||||
|
|
||||||
// В development используем текущую директорию
|
// В development используем текущую директорию
|
||||||
console.log(`[Database] Using cwd: ${cwd}`)
|
|
||||||
return cwd
|
return cwd
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,8 +44,11 @@ const DEFAULT_PASSWORD = 'ksadmin'
|
|||||||
// Путь к старой базе данных (для миграции)
|
// Путь к старой базе данных (для миграции)
|
||||||
const OLD_DB_PATH = path.join(DATABASE_DIR, 'data', 'schedule-app.db')
|
const OLD_DB_PATH = path.join(DATABASE_DIR, 'data', 'schedule-app.db')
|
||||||
|
|
||||||
console.log(`[Database] DB_PATH: ${DB_PATH}`)
|
// Создаем директорию db, если её нет
|
||||||
console.log(`[Database] dbDir: ${path.dirname(DB_PATH)}`)
|
const dbDir = path.dirname(DB_PATH)
|
||||||
|
if (!fs.existsSync(dbDir)) {
|
||||||
|
fs.mkdirSync(dbDir, { recursive: true })
|
||||||
|
}
|
||||||
|
|
||||||
// Миграция базы данных из data/ в db/ (если старая база существует)
|
// Миграция базы данных из data/ в db/ (если старая база существует)
|
||||||
function migrateDatabaseLocation(): void {
|
function migrateDatabaseLocation(): void {
|
||||||
@@ -90,106 +86,31 @@ function migrateDatabaseLocation(): void {
|
|||||||
|
|
||||||
// Инициализация базы данных
|
// Инициализация базы данных
|
||||||
let db: Database.Database | null = null
|
let db: Database.Database | null = null
|
||||||
let dbInitAttempted = false
|
|
||||||
let dbInitError: Error | null = null
|
|
||||||
|
|
||||||
function getDatabase(): Database.Database {
|
function getDatabase(): Database.Database {
|
||||||
// Если уже есть ошибка инициализации, выбрасываем её сразу
|
|
||||||
if (dbInitError) {
|
|
||||||
throw dbInitError
|
|
||||||
}
|
|
||||||
|
|
||||||
if (db) {
|
if (db) {
|
||||||
return db
|
return db
|
||||||
}
|
}
|
||||||
|
|
||||||
// Защита от повторной инициализации при ошибке
|
|
||||||
if (dbInitAttempted) {
|
|
||||||
if (db) return db
|
|
||||||
throw new Error('Database initialization failed previously')
|
|
||||||
}
|
|
||||||
|
|
||||||
dbInitAttempted = true
|
|
||||||
|
|
||||||
console.log('[Database] Initializing database connection...')
|
|
||||||
console.log(`[Database] DB_PATH: ${DB_PATH}`)
|
|
||||||
console.log(`[Database] DB_PATH exists: ${fs.existsSync(DB_PATH)}`)
|
|
||||||
console.log(`[Database] process.cwd(): ${process.cwd()}`)
|
|
||||||
console.log(`[Database] DATABASE_DIR: ${DATABASE_DIR}`)
|
|
||||||
|
|
||||||
// Создаем директорию db, если её нет
|
|
||||||
const dbDir = path.dirname(DB_PATH)
|
|
||||||
console.log(`[Database] dbDir: ${dbDir}`)
|
|
||||||
console.log(`[Database] dbDir exists: ${fs.existsSync(dbDir)}`)
|
|
||||||
|
|
||||||
if (!fs.existsSync(dbDir)) {
|
|
||||||
console.log(`[Database] Creating directory: ${dbDir}`)
|
|
||||||
try {
|
|
||||||
fs.mkdirSync(dbDir, { recursive: true, mode: 0o755 })
|
|
||||||
console.log(`[Database] Directory created successfully`)
|
|
||||||
} catch (error) {
|
|
||||||
const errMsg = `Failed to create database directory ${dbDir}: ${error}`
|
|
||||||
console.error(`[Database] ${errMsg}`)
|
|
||||||
dbInitError = new Error(errMsg)
|
|
||||||
throw dbInitError
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Проверяем, можем ли записывать в директорию
|
|
||||||
try {
|
|
||||||
const testFile = path.join(dbDir, '.write-test-' + Date.now())
|
|
||||||
fs.writeFileSync(testFile, 'test', { mode: 0o644 })
|
|
||||||
fs.unlinkSync(testFile)
|
|
||||||
console.log('[Database] Directory is writable')
|
|
||||||
} catch (error) {
|
|
||||||
const errMsg = `Directory ${dbDir} is not writable: ${error}`
|
|
||||||
console.error(`[Database] ${errMsg}`)
|
|
||||||
dbInitError = new Error(errMsg)
|
|
||||||
throw new Error(errMsg)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Выполняем миграцию расположения базы данных перед открытием
|
// Выполняем миграцию расположения базы данных перед открытием
|
||||||
migrateDatabaseLocation()
|
migrateDatabaseLocation()
|
||||||
|
|
||||||
try {
|
db = new Database(DB_PATH)
|
||||||
console.log('[Database] Opening database...')
|
|
||||||
db = new Database(DB_PATH)
|
|
||||||
console.log('[Database] Database opened successfully')
|
|
||||||
|
|
||||||
// Проверяем, можем ли записывать
|
// Применяем современные настройки SQLite
|
||||||
try {
|
db.pragma('journal_mode = WAL') // Write-Ahead Logging для лучшей производительности
|
||||||
db.exec('SELECT 1')
|
db.pragma('synchronous = NORMAL') // Баланс между производительностью и надежностью
|
||||||
console.log('[Database] Database is writable')
|
db.pragma('foreign_keys = ON') // Включение проверки внешних ключей
|
||||||
} catch (error) {
|
db.pragma('busy_timeout = 5000') // Таймаут для ожидания блокировок (5 секунд)
|
||||||
const errMsg = `Database is not writable: ${(error as Error).message}`
|
db.pragma('temp_store = MEMORY') // Хранение временных данных в памяти
|
||||||
console.error('[Database] ' + errMsg)
|
db.pragma('mmap_size = 268435456') // Memory-mapped I/O (256MB)
|
||||||
dbInitError = new Error(errMsg)
|
db.pragma('cache_size = -64000') // Размер кеша в страницах (64MB)
|
||||||
throw new Error(errMsg)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Применяем современные настройки SQLite
|
// Создаем таблицы, если их нет
|
||||||
db.pragma('journal_mode = WAL') // Write-Ahead Logging для лучшей производительности
|
initializeTables()
|
||||||
db.pragma('synchronous = NORMAL') // Баланс между производительностью и надежностью
|
|
||||||
db.pragma('foreign_keys = ON') // Включение проверки внешних ключей
|
|
||||||
db.pragma('busy_timeout = 5000') // Таймаут для ожидания блокировок (5 секунд)
|
|
||||||
db.pragma('temp_store = MEMORY') // Хранение временных данных в памяти
|
|
||||||
db.pragma('mmap_size = 268435456') // Memory-mapped I/O (256MB)
|
|
||||||
db.pragma('cache_size = -64000') // Размер кеша в страницах (64MB)
|
|
||||||
|
|
||||||
console.log('[Database] SQLite pragmas applied')
|
// Выполняем миграцию данных из JSON, если БД пустая
|
||||||
|
migrateFromJSON()
|
||||||
// Создаем таблицы, если их нет
|
|
||||||
initializeTables()
|
|
||||||
|
|
||||||
// Выполняем миграцию данных из JSON, если БД пустая
|
|
||||||
migrateFromJSON()
|
|
||||||
|
|
||||||
console.log('[Database] Database initialization complete')
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[Database] Failed to initialize database:', error)
|
|
||||||
dbInitError = error as Error
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
|
|
||||||
return db
|
return db
|
||||||
}
|
}
|
||||||
@@ -330,7 +251,6 @@ export function getAllTeachers(): TeachersData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[Database] getAllTeachers: found ${Object.keys(teachers).length} teachers`)
|
|
||||||
return teachers
|
return teachers
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -672,48 +592,6 @@ function migrateFromJSON(): void {
|
|||||||
console.error('Error hashing default password:', err)
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Экспортируем функцию для закрытия соединения (полезно для тестов)
|
// Экспортируем функцию для закрытия соединения (полезно для тестов)
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ export function loadTeachers(forceRefresh: boolean = false): TeachersData {
|
|||||||
try {
|
try {
|
||||||
cachedTeachers = getAllTeachersFromDB()
|
cachedTeachers = getAllTeachersFromDB()
|
||||||
cacheTimestamp = now
|
cacheTimestamp = now
|
||||||
console.log(`[TeachersLoader] Loaded ${Object.keys(cachedTeachers).length} teachers from database`)
|
|
||||||
return cachedTeachers
|
return cachedTeachers
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading teachers from database:', error)
|
console.error('Error loading teachers from database:', error)
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ Group=www-data
|
|||||||
WorkingDirectory=/opt/kspguti-schedule/.next/standalone
|
WorkingDirectory=/opt/kspguti-schedule/.next/standalone
|
||||||
Environment=NODE_ENV=production
|
Environment=NODE_ENV=production
|
||||||
Environment=NEXT_TELEMETRY_DISABLED=1
|
Environment=NEXT_TELEMETRY_DISABLED=1
|
||||||
Environment=DATABASE_DIR=/opt/kspguti-schedule
|
|
||||||
Environment=PORT=3000
|
Environment=PORT=3000
|
||||||
Environment=HOSTNAME=0.0.0.0
|
Environment=HOSTNAME=0.0.0.0
|
||||||
# Uncomment and set your environment variables:
|
# Uncomment and set your environment variables:
|
||||||
|
|||||||
Reference in New Issue
Block a user