25 KiB
Расписание ПГУ
Первый рабочий PWA-клиент для расписаний Полоцкого государственного университета. При первом запуске предлагает найти свою группу; исходный сценарий проверки — ФЭФ, 23-БА из общего документа 23-ЛГ_23-БА.docx за 2026–2027 учебный год.
Запуск
Нужны Node.js 22.12+ (проверено на 24.14.1), npm и curl.
npm ci
npm run build
npm start
Откройте http://127.0.0.1:3001. Этот режим включает service worker, установку PWA и офлайн-кэш. Для разработки с обновлением интерфейса:
npm run dev
Vite работает на http://127.0.0.1:5173 и проксирует /api на порт 3001 (другой порт — через API_PORT). В режиме разработки service worker не регистрируется. PORT и HOST настраивают сервер; при размещении нужен HTTPS. Для доступа с телефона используйте HTTPS-хостинг либо локальный HTTPS-туннель.
Возможности
- Живой каталог публичного Nextcloud университета: факультет, форма, курс и поиск по названию группы/файла. Проверенный каталог содержит 113 документов Word.
- Расписание по дням или на всю неделю, поиск предмета, преподавателя и аудитории.
- Экран «Группы»: поиск группы по номеру в любом написании («23-БА», «23 ба», «23ба») по всем документам каталога, без учёта фильтров; одно нажатие открывает нужный документ и группу. Ниже — текущая группа с подгруппой и соседними группами документа, недавние группы (открываются и без сети) и каталог чипами: факультет → курс → форма обучения. Первый запуск начинается с этого экрана, а не с чьей-то группы (
/api/groups). - Общие ячейки попадают в каждую соответствующую группу, чужие занятия отсекаются.
- Отдельные карточки для альтернативных занятий и условий по неделям. Сохраняются исходный текст, цвет ячейки, подгруппы, даты и ограничения.
- Учебные недели по таблице университета: переключатель недель с датами, показ только занятий выбранной недели, статус занятия («ещё 4 занятия · до 27 нояб.», «начнётся 20 нояб.») и сетка недель в деталях. Режим «Все строки Word» показывает документ как есть.
- Номера пар и текущая пара («Идёт · ещё 25 мин») по расписанию звонков.
- «Что изменилось»: когда университет перепубликует документ, приложение сравнивает новую версию с сохранённой и показывает для вашей группы изменённые поля (аудитория, время, преподаватель, недели), добавленные и удалённые занятия. Последний список изменений открывается из меню.
- Моя подгруппа и скрытые занятия: в «Группах» можно выбрать подгруппу — занятия другой подгруппы пропадут; любое занятие можно скрыть из его карточки и вернуть кнопкой «Скрыто · показать». Настройки хранятся только в этом браузере.
- Поиск по университету (вкладка «Поиск»): преподаватель или аудитория — занятия на выбранный день по всем расписаниям каталога; свободные аудитории на дату и пару. Сервер разбирает все документы каталога в фоне (первый раз около 20–30 секунд, дальше из кэша) и считает занятость с учётом недель, цвета строк, семестра и разовых переносов аудиторий вида «432 (07.10-ауд.272)». Дистанционные занятия («200 (ДО)») аудиторию не занимают. Каждый ответ сообщает, сколько документов учтено; «свободна» означает «свободна по расписаниям из Word», мероприятия и закрытые аудитории расписание не знает. Работает только онлайн.
- Сессия: экзамены, зачёты и консультации группы из всех документов каталога (распознаются по «(экз.)», «(зач.)», «(конс.)» и словам «Экзамен», «Зачёт», «Консультация»). На главном экране — карточка с обратным отсчётом до ближайшего экзамена или зачёта, полный список — в листе «Сессия». Список сохраняется для просмотра без сети. В текущем каталоге документов экзаменационной сессии ещё нет; формат проверен на образцах, а не на реальных файлах университета.
- Ссылка и QR-код на расписание группы (
/?doc=<id>&group=<группа>) из меню «Поделиться расписанием». Ссылка открывает документ из каталога и нужную группу; подгруппа и скрытые занятия не передаются. Устаревшая ссылка (документ убран из каталога) открывает поиск группы. Для импортированных файлов ссылка недоступна. - Фильтр белых/зелёных строк. Объединённое на обе строки занятие показывается в обоих вариантах без дублирования.
- Импорт
.docxи.docдо 10 МБ с предварительным просмотром перед сохранением. - Сохранение последних 12 документов и выбранной группы в этом браузере. После первой успешной загрузки собранной PWA интерфейс, шрифты и расписание доступны без сервера/сети.
- Манифест PWA, PNG-иконки, установка на главный экран и ссылка на исходный Word.
Источник и обновления
Каталог университета читается через публичный DAV. Сервер обращается только к этой общей папке. Другие ссылки не принимаются.
Учебный год выбирается сам. Сервер читает корень каталога, находит папки вида 2026-2027 и с 1 сентября (Europe/Minsk) берёт папку нового года. Если её ещё нет, используется самая новая из прошлых, а /api/health предупреждает об этом. Таблица белых и зелёных недель и расписание звонков ищутся по смыслу имени («бел» + «зел» + «недел», «звонк»), сначала в папке года, затем в корне. Можно выложить несколько таблиц недель: они объединяются, при совпадении дат побеждает более новый файл. В список расписаний эти справочные файлы не попадают.
Семестры. Таблица делится на семестры там, где нумерация недель начинается заново или между неделями больше трёх недель. Расписание связывается со своим семестром по тексту Word («осенний семестр 2026/2027»), расписание сессии — по своим датам, документ без указания семестра — по семестру, в котором сегодняшняя дата. Если семестр не найден (например, весенней таблицы ещё нет), даты не рассчитываются и показываются все строки Word.
Обновления у студента. При запуске и при возвращении в приложение, если прошло больше часа, клиент сверяет сохранённое расписание с каталогом. Тот же файл с новой датой тихо перечитывается. Если для группы появился более новый документ (следующий семестр или год), показывается предложение его открыть; отказ запоминается. Если документ исчез из каталога, приложение предлагает найти группу заново.
Каталог и папка года перечитываются раз в 15 минут, таблица недель и звонки — раз в 6 часов, распознанный документ — при изменении даты файла или версии парсера. «Обновить» принудительно перечитывает всё. Если источник недоступен, используются сохранённые копии с явной пометкой. Фоновая синхронизация при закрытом приложении не реализована.
GET /api/health без обращения к сети показывает выбранный учебный год, возраст каталога, найденные файлы недель и звонков, семестры и предупреждения: папка нового года не найдена, сегодняшняя дата вне таблицы недель, источник не обновляется.
npm run sync
Серверный кэш находится в .data/ (исключён из Git). Загрузки пользователей обрабатываются в памяти; временные файлы конвертации .doc удаляются. Данные из университетского каталога могут сохраняться в серверном кэше. Для офлайн-просмотра расписания хранятся в localStorage; очистка данных браузера удалит сохранённые документы. Аналитики и учётных записей нет.
Границы парсера
Основной парсер читает XML и сетку таблиц .docx, включая горизонтальные и вертикальные объединения ячеек. Поддерживаются проверенные дневные шаблоны ФЭФ, ФИТ, ФКНЭ, ИСФ и ГФ, а также заочные установочные сессии с отдельными колонками предмета, аудитории и преподавателя. Это не универсальный парсер любых документов Word: без заголовков групп, дней и времени импорт возвращает понятную ошибку.
Даты занятий рассчитываются по университетской таблице «Расписание белых и зеленых недель» из корня каталога: неделя 1 начинается 01.09.2026 (вторник), нечётные недели белые, чётные зелёные. Пометка в белой или зелёной строке относится только к неделям этого цвета: 1-13н в белой строке — недели 1, 3, …, 13, а в объединённой строке — каждая неделя. с 3н. означает «до конца таблицы». Текстовые даты вроде «15 октября» привязываются к своему дню. Нераспознанные пометки (2,6-12-16н) не скрываются, а помечаются для сверки с Word; исходная пометка всегда видна. Разбор пометок общий для сервера и браузера (shared/weeks.js), поэтому старые сохранённые расписания тоже получают даты. Вне периода таблицы приложение показывает все строки Word. Номера подгрупп соответствуют столбцам внутри группы; именованных подгрупп в этих шаблонах нет.
Поля предмета, преподавателя и аудитории извлекаются по типовым обозначениям; сложные многострочные записи могут содержать несколько преподавателей/аудиторий. Поэтому полный текст всегда доступен в деталях. Если в ячейке явно указано особое время, например 16.50 для кураторского часа, оно имеет приоритет; окончание не выдумывается.
Для старых .doc на сервере нужен LibreOffice Writer (soffice в PATH или LIBREOFFICE_BIN). Конвертация может изменить сложное форматирование, результат нужно сверить. Реальные документы выбранного источника — .docx; поддержка .doc проверяется отдельно на образце, преобразованном LibreOffice.
PDF, Excel, изображения и сканы пока не импортируются. Они не выдаются за распознанное расписание.
Проверки
npm test
npm run build
Тесты используют реальные Word-документы источника в test/fixtures/ с URL и метаданными рядом. Проверяются изоляция 23-БА/23-ЛГ, общие события, объединения ячеек, варианты недель, отдельное время кураторского часа, даты сессии и отклонение повреждённых файлов, а также таблица недель и звонков, разбор пометок с учётом цвета строки и согласие всех пометок 23-БА с таблицей.
Docker
docker compose up -d --build
Приложение откроется на http://localhost:3001. Кэш каталога, таблицы недель и распознанных документов хранится в томе psu-data и переживает пересборку. Первый запуск разбирает все документы каталога (около полуминуты), поэтому проверка здоровья (/api/health) даёт контейнеру минуту на старт. Порт и адрес меняются переменными APP_PORT и APP_BIND.
Для установки PWA на телефон нужен HTTPS. Профиль https поднимает Caddy, который сам получает сертификат Let's Encrypt (домен должен указывать на сервер, порты 80 и 443 открыты):
DOMAIN=schedule.example.by docker compose --profile https up -d --build
Если HTTPS уже даёт ваш обратный прокси, профиль не нужен; приложение можно закрыть от внешней сети через APP_BIND=127.0.0.1. Контейнер включает curl и LibreOffice для .doc. Файл docker-compose.yml проверен docker compose config; сборка образа в этой проверке не запускалась.
Android-приложение
В папке android/ — нативный клиент на Kotlin и Jetpack Compose с компонентами Material 3 Expressive (MaterialExpressiveTheme, ModalWideNavigationRail и WideNavigationRail, соединённые ToggleButton, LoadingIndicator). Цвета совпадают с веб-версией. Разделы открываются из бокового меню (на планшете и в альбомной ориентации — постоянная боковая панель); карточки лежат на подложке с оттенком, системные панели следуют теме приложения.
- Обзор: отчёт за неделю (сколько пар прошло), следующие занятия сегодня с обратным отсчётом и ближайший учебный день.
- Расписание группы по дням с листанием или недельной сеткой по часам; неделя и её цвет, номера пар, текущая пара, детали занятия с исходной записью Word. Если семестр не найден в таблице недель — строки Word по дням недели.
- Предметы: пары по дням текущей недели; у каждого предмета — сокращение как в недельной сетке, следующее занятие с аудиторией, преподаватели и сколько пар на этой неделе. У каждого предмета свой постоянный цвет во всех разделах.
- Настройки отображения: цвета предметов, преподаватели, аудитории, номера пар, метки занятий и пометки из Word можно скрыть; полные сведения остаются в карточке занятия.
- Группы (боковое меню): поиск по номеру во всём каталоге, недавние, каталог по факультету и курсу, выбор подгруппы.
- Поиск по университету: преподаватель или аудитория на день, свободные аудитории на пару.
- Виджет «Сегодня» (Glance): текущая и следующая пара, обновляется каждые 15 минут без сети.
- Напоминания перед парой за 5–30 минут — локальные будильники из сохранённого расписания, без push-сервера. С разрешением «Будильники и напоминания» приходят точно в срок, без него — в окне ±5 минут.
- Офлайн: последнее расписание хранится на устройстве; каждые 6 часов WorkManager обновляет его, виджет и напоминания.
Даты считает сервер: GET /api/timetable?doc=<id>&group=<группа>&from=ГГГГ-ММ-ДД&days=N отдаёт дни с неделями, номерами пар и пометкой uncertain для нераспознанных пометок, плюс строки Word (weekly) и подгруппы. Поэтому правила белых и зелёных недель в Kotlin не дублируются.
Сборка (JDK 17, Android SDK 37):
cd android
./gradlew assembleDebug
Отладочная сборка ходит на http://10.0.2.2:3010/ — это сервер из docker compose на компьютере, видимый из эмулятора (APP_PORT=3010). Другой адрес: -PPSU_API_URL=…. Релизная сборка принимает только HTTPS-адрес. Идентификатор приложения ritm.schedule — рабочий, перед публикацией его стоит заменить своим.
Структура
src/App.tsx— интерфейс, каталог и импорт с предпросмотром.src/storage.ts— сохранённые расписания, предпочтения и календарь недель.src/calendar.ts,src/icons.tsx— подписи дат и пар, иконки Material Symbols.shared/weeks.js— разбор пометок недель и расчёт дат, общий для сервера и браузера.shared/lessons.js— устойчивый ключ занятия, сравнение версий расписания, фильтр подгруппы и скрытых занятий.shared/updates.js— правила проверки обновлений сохранённого расписания.server/directory.js— индекс всех документов: преподаватели и аудитории из текста ячеек, занятость на дату, свободные аудитории, экзамены группы (/api/directory/search,/api/directory/day,/api/directory/free,/api/exams).server/schedules.js— разбор документа каталога с дисковым кэшем.src/Search.tsx— экран «Поиск».server/source.js— DAV-каталог, фиксированный источник и кэш; таблица недель и звонков из корня каталога (/api/calendar, кэш 6 часов).server/calendar.js— разбор таблицы недель и расписания звонков.server/parser.js— таблицы.docx, условия и преобразование.doc.server/index.js— API и выдача собранной PWA.scripts/serviceworker.js— генерация полного офлайн-кэша при сборке.
Это независимый клиент; университет остаётся источником расписания. Шрифт Roboto Flex поставляется локально под SIL Open Font License (public/fonts/OFL-RobotoFlex.txt).
Тема выбирается в меню: «Авто» (как в системе), «Светлая» или «Тёмная». Выбор хранится в браузере и применяется до первой отрисовки страницы, вместе с цветом строки состояния. QR-коды строятся в браузере библиотекой qrcode-generator (MIT). Иконки — Material Symbols Rounded (@material-symbols/svg-400, Apache 2.0), встроены в сборку и работают без сети. Интерфейс использует веб-адаптацию Material 3 Expressive: токены цвета и типографики, тональные поверхности, выразительные формы, навигационную панель и rail. Главный экран содержит расписание; импорт, установка и сведения о Word доступны в меню.