refactor: optimize project structure, migrate to SQLite, and add new features

fix:
  - Fix TypeScript type errors in api-wrapper.ts (ApiResponse type)
  - Fix backward compatibility in database.ts getSettings() for missing fields
  - Fix default value for weekNavigationEnabled (changed from true to false)
  - Fix API routes error handling with unified wrapper
  - Fix duplicate toggle switch code in admin.tsx (6 instances)
  - Fix inconsistent authentication check in API routes (unified with withAuth)
  - Fix error message text in loading-context.tsx (improved user experience)

add:
  - Add database.ts: SQLite database layer with better-sqlite3 for persistent storage
    * Groups management (CRUD operations)
    * Settings management with caching
    * Admin password hashing with bcrypt
    * Automatic database initialization and migration
  - Add api-wrapper.ts utility for unified API route handling
    * withAuth wrapper for protected routes
    * withMethods wrapper for public routes
    * Consistent error handling and method validation
  - Add validation.ts utility with centralized validation functions
    * validateCourse - course validation (1-5)
    * validateGroupId - group ID format validation
    * validatePassword - password strength validation
  - Add showAddGroupButton setting to control visibility of 'Add Group' button on homepage
  - Add toggle switch component in admin.tsx for reusable UI (replaces 6 duplicate instances)
  - Add CourseSelect component in admin.tsx for reusable course selection
  - Add DialogFooterButtons component in admin.tsx for reusable dialog footer
  - Add unified loadData function in admin.tsx to reduce code duplication
  - Add change-password.ts API endpoint for admin password management
  - Add logs.ts API endpoint for viewing error logs in admin panel
  - Add logErrorToFile function in logger.ts for persistent error logging
  - Add comprehensive error logging in schedule.ts (parsing, fetch, timeout, network errors)
  - Add comprehensive project structure documentation in README.md
  - Add architecture and code organization section in README.md
  - Add database information section in README.md
  - Add SQLite and bcrypt to tech stack documentation
  - Add better-sqlite3 and bcrypt dependencies to package.json
  - Add .gitignore rules for error.log and database files (data/, *.db, *.db-shm, *.db-wal)

refactor:
  - Refactor admin.tsx: extract reusable components (toggle, select, dialog footer)
  - Refactor API routes to use withAuth wrapper for consistent authentication
  - Refactor API routes to use validation utilities instead of inline validation
  - Refactor groups.ts and groups.json: move to old/data/ directory (deprecated, now using SQLite)
  - Refactor settings-loader.ts: migrate from JSON to SQLite database
  - Refactor groups-loader.ts: migrate from JSON to SQLite database
  - Refactor database.ts: improve backward compatibility for settings migration
  - Refactor admin.tsx: unify data loading functions (loadGroupsList, loadSettingsList)
  - Refactor index.tsx: add showAddGroupButton prop and conditional rendering
  - Refactor API routes: consistent error handling and method validation
  - Refactor README.md: update tech stack, project structure, and admin panel documentation
  - Refactor auth.ts: improve session management and cookie handling
  - Refactor schedule.ts: improve error handling with detailed logging and error types
  - Refactor logger.ts: add file-based error logging functionality
  - Refactor loading-context.tsx: improve error message clarity

remove:
  - Remove hello.ts test API endpoint
  - Remove groups.ts and groups.json (moved to old/data/, replaced by SQLite)

update:
  - Update .gitignore to exclude old data files, database files, and error logs
  - Update package.json: add better-sqlite3, bcrypt and their type definitions
  - Update README.md with new features, architecture, and database information
  - Update all API routes to use new wrapper system
  - Update admin panel with new settings and improved UI
  - Update sitemap.xml with cache usage comment
This commit is contained in:
kilyabin
2025-12-03 21:44:07 +04:00
parent 0907581cc0
commit e46a2419c3
27 changed files with 1937 additions and 627 deletions

View File

@@ -1,8 +1,8 @@
import fs from 'fs'
import path from 'path'
import { getSettings as getSettingsFromDB, updateSettings as updateSettingsInDB } from './database'
export type AppSettings = {
weekNavigationEnabled: boolean
showAddGroupButton: boolean
debug?: {
forceCache?: boolean
forceEmpty?: boolean
@@ -13,190 +13,64 @@ export type AppSettings = {
}
let cachedSettings: AppSettings | null = null
let cachedSettingsPath: string | null = null
let cachedSettingsMtime: number | null = null
const defaultSettings: AppSettings = {
weekNavigationEnabled: true,
debug: {
forceCache: false,
forceEmpty: false,
forceError: false,
forceTimeout: false,
showCacheInfo: false
}
}
let cacheTimestamp: number = 0
const CACHE_TTL_MS = 1000 * 60 // 1 минута
/**
* Загружает настройки из JSON файла
* Проверяет время модификации файла для инвалидации кеша
* Загружает настройки из базы данных
* Использует кеш с TTL для оптимизации, но всегда загружает свежие данные при необходимости
*/
export function loadSettings(): AppSettings {
// В production Next.js может использовать другую структуру директорий
// Пробуем несколько путей
const possiblePaths = [
path.join(process.cwd(), 'src/shared/data/settings.json'),
path.join(process.cwd(), '.next/standalone/src/shared/data/settings.json'),
path.join(process.cwd(), 'settings.json'),
]
export function loadSettings(forceRefresh: boolean = false): AppSettings {
const now = Date.now()
const isCacheValid = cachedSettings !== null && !forceRefresh && (now - cacheTimestamp) < CACHE_TTL_MS
// Ищем существующий файл
let foundPath: string | null = null
for (const filePath of possiblePaths) {
if (fs.existsSync(filePath)) {
foundPath = filePath
break
}
if (isCacheValid && cachedSettings !== null) {
return cachedSettings
}
// Если файл найден, проверяем, изменился ли он
if (foundPath) {
try {
const stats = fs.statSync(foundPath)
const mtime = stats.mtimeMs
// Если файл изменился или путь изменился, сбрасываем кеш
if (cachedSettings && (cachedSettingsPath !== foundPath || cachedSettingsMtime !== mtime)) {
cachedSettings = null
cachedSettingsPath = null
cachedSettingsMtime = null
}
// Если кеш валиден, возвращаем его
if (cachedSettings && cachedSettingsPath === foundPath && cachedSettingsMtime === mtime) {
return cachedSettings
}
// Загружаем файл заново
const fileContents = fs.readFileSync(foundPath, 'utf8')
const settings = JSON.parse(fileContents) as AppSettings
// Убеждаемся, что все обязательные поля присутствуют
const mergedSettings: AppSettings = {
...defaultSettings,
...settings,
debug: {
...defaultSettings.debug,
...settings.debug
}
}
cachedSettings = mergedSettings
cachedSettingsPath = foundPath
cachedSettingsMtime = mtime
return mergedSettings
} catch (error) {
console.error('Error reading settings.json:', error)
// Продолжаем дальше, чтобы создать файл с настройками по умолчанию
}
}
// Если файл не найден, создаем его с настройками по умолчанию
const mainPath = path.join(process.cwd(), 'src/shared/data/settings.json')
try {
// Создаем директорию, если её нет
const dir = path.dirname(mainPath)
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true })
}
fs.writeFileSync(mainPath, JSON.stringify(defaultSettings, null, 2), 'utf8')
const stats = fs.statSync(mainPath)
cachedSettings = defaultSettings
cachedSettingsPath = mainPath
cachedSettingsMtime = stats.mtimeMs
return defaultSettings
cachedSettings = getSettingsFromDB()
cacheTimestamp = now
return cachedSettings
} catch (error) {
console.error('Error creating settings.json:', error)
console.error('Error loading settings from database:', error)
// Возвращаем настройки по умолчанию
const defaultSettings: AppSettings = {
weekNavigationEnabled: false,
showAddGroupButton: true,
debug: {
forceCache: false,
forceEmpty: false,
forceError: false,
forceTimeout: false,
showCacheInfo: false
}
}
return defaultSettings
}
}
/**
* Сохраняет настройки в JSON файл
* Сохраняет настройки в базу данных
*/
export function saveSettings(settings: AppSettings): void {
// Сначала пытаемся найти существующий файл
const possiblePaths = [
path.join(process.cwd(), 'src/shared/data/settings.json'),
path.join(process.cwd(), '.next/standalone/src/shared/data/settings.json'),
path.join(process.cwd(), 'settings.json'),
]
// Объединяем с настройками по умолчанию для сохранения всех полей
const mergedSettings: AppSettings = {
...defaultSettings,
...settings,
debug: {
...defaultSettings.debug,
...settings.debug
}
}
// Ищем существующий файл
let targetPath: string | null = null
for (const filePath of possiblePaths) {
if (fs.existsSync(filePath)) {
targetPath = filePath
break
}
}
// Если файл не найден, используем основной путь
if (!targetPath) {
targetPath = path.join(process.cwd(), 'src/shared/data/settings.json')
}
try {
// Создаем директорию, если её нет
const dir = path.dirname(targetPath)
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true })
}
// Сохраняем файл
fs.writeFileSync(targetPath, JSON.stringify(mergedSettings, null, 2), 'utf8')
// Обновляем кеш с новыми метаданными
try {
const stats = fs.statSync(targetPath)
cachedSettings = mergedSettings
cachedSettingsPath = targetPath
cachedSettingsMtime = stats.mtimeMs
} catch (error) {
// Если не удалось получить stats, просто обновляем кеш
cachedSettings = mergedSettings
cachedSettingsPath = targetPath
cachedSettingsMtime = null
}
// Также сохраняем в другие возможные пути для совместимости (если они существуют)
for (const filePath of possiblePaths) {
if (filePath !== targetPath && fs.existsSync(path.dirname(filePath))) {
try {
fs.writeFileSync(filePath, JSON.stringify(mergedSettings, null, 2), 'utf8')
} catch (error) {
// Игнорируем ошибки при сохранении в дополнительные пути
}
}
}
updateSettingsInDB(settings)
// Сбрасываем кеш и timestamp
cachedSettings = null
cacheTimestamp = 0
} catch (error) {
console.error('Error saving settings.json:', error)
console.error('Error saving settings to database:', error)
throw new Error('Failed to save settings')
}
}
/**
* Сбрасывает кеш настроек (полезно после обновления файла)
* Сбрасывает кеш настроек (полезно после обновления)
*/
export function clearSettingsCache(): void {
cachedSettings = null
cachedSettingsPath = null
cachedSettingsMtime = null
cacheTimestamp = 0
}