Commit Graph

18 Commits

Author SHA1 Message Date
6f16736359 Аудитории: фильтры по типу, вместимости и оснащению + режим «свободные на паре»
Бэкенд:
- parse_rooms_info разбирает описание аудитории (формат «тип; оснащение; N мест»)
  в атрибуты: type (фразой как в таблице), capacity (места, а для компьютерных
  классов — рабочие места; null, если данных нет), equipment (ПК, Проектор,
  Интерактивная панель, Телевизор, Маркер, Меловая доска) и available_days
  («402 четверг пятница»); «без проектора» трактуется как отсутствие;
- /rooms-day и /rooms-free отдают атрибуты (room_payload); аудитории с
  ограничением по дням не предлагаются в /rooms-free в остальные дни.

Фронтенд (вкладка «Аудитории»):
- панель фильтров: тип (значения из данных таблицы), «мест не меньше», оснащение
  чекбоксами с выбором условия «все (И)» / «любое (ИЛИ)»;
- счётчик «Показано N из M» и пояснения: сколько аудиторий скрыто из-за
  отсутствия данных о вместимости, сколько доступны только по отдельным дням;
- переключатель «Занятость за день» / «Свободные на паре» (с выбором пары) —
  вместе с фильтрами это «свободная аудитория на 30 человек с проектором в
  3-ю пару»; выбранные фильтры запоминаются в браузере;
- в карточке аудитории видны разобранные атрибуты и пометки из таблицы.

Тесты: разбор всех форматов описаний (включая отрицания и меловую доску),
API атрибутов и учёта доступных дней. Всего 159.
2026-10-10 12:25:16 +03:00
3d5bfe3023 README: убрана пустая строка в блоке структуры после удаления install.sh 2026-10-10 11:51:33 +03:00
b49ef93619 Убран install.sh: установка через deploy.sh
Для личного использования достаточно deploy.sh (проверяет окружение, собирает
образы и поднимает контейнеры). Раздел «Установка на сервер» переписан:
Docker через deploy.sh + вариант без Docker (systemd) и nginx, без интерактивного
установщика и его флагов.
2026-10-10 11:51:14 +03:00
5b3c9dc00a Админка в master: секретный путь, ссылка, интервал, force, блокировка входа, история
Админка нужна на «голом» сервере, поэтому возвращаю её в основную ветку. Она
выключена, пока не заданы ADMIN_LOGIN/ADMIN_PASSWORD: маршруты /api/admin/* не
регистрируются, страницы нет — обычная установка ничего не замечает.

Панель (открывается по секретному пути ADMIN_PATH, кнопки входа на сайте нет,
вход по логину/паролю):
- состояние: группы, когда обновлялось, дата файла на Диске, изменился ли
  файл при последнем чтении (sha256), ошибки, путь файла настроек;
- новая ссылка на таблицу: проверяется скачиванием, при ошибке откат;
- интервал обновления (TTL) в минутах;
- «Обновить сейчас»: принудительно перечитывает таблицу (качает файл всегда,
  интервал — только если на Диске изменилась дата);
- история входов: последняя попытка (откуда, когда, успешна ли) и последний
  успешный вход — и в панели, и в логе службы.

Защита входа: ADMIN_MAX_ATTEMPTS неудач (по умолчанию 3), затем блокировка на
ADMIN_LOCKOUT_MINUTES минут (по умолчанию 15) — всё из .env.

Реализация:
- backend/admin.py: cookie-сессия на HMAC (ключ — пароль, 12 ч, HttpOnly,
  SameSite, Secure за https), сравнение за постоянное время, реальный IP из
  X-Forwarded-For, логирование каждой попытки;
- schedule/settings.py: ссылка, интервал и история входов в JSON; путь по
  умолчанию data/settings.json (в docker — том schedule_data), при
  недоступности файла настройки живут в памяти и панель предупреждает;
- cache.py: apply_link (с проверкой и откатом), force_reload, state();
- frontend: страница админки по ADMIN_PATH (вне static, noindex) и прокси
  /api/admin/* с телом и Set-Cookie;
- install.sh пишет переменные админки в .env, Dockerfile копирует admin.py.

Тесты: 156. Проверено локально (страница, 3 попытки -> блокировка, история) и
в собранном docker-образе (том настроек, запись истории).
2026-10-10 11:12:29 +03:00
962681630f Добавлен установщик 2026-10-07 17:28:55 +03:00
cc194b8eee Исправлен деплой скрипт 2026-10-07 16:20:42 +03:00
d897601d88 Скорректирован сценарий запуска, исправление бага с предметом 2026-10-07 15:17:37 +03:00
34b975da69 Перенос в master всего, кроме админки: ТОП-группы, порядок групп, запуск без Docker
Админка осталась отдельной ветвью admin-panel; в master её нет (ни кода, ни
страниц, ни переменных окружения, ни тома настроек).

Что перенесено:
- parsing: имена групп вида «ИВТ-ТОП-11БО» распознаются, порядок групп = порядок
  столбцов таблицы (похожие заголовки с пробелами группой не считаются);
- main: GET /groups отдаёт группы в порядке столбцов; интервал обновления из
  SCHEDULE_INTERVAL_MINUTES (со старым SCHEDULE_CACHE_TTL как fallback);
- cache: по интервалу сверяем modified — если файл не менялся, не скачиваем;
  при загрузке считаем sha256 и пишем в лог и /status, изменился ли файл;
- source: запросы к Яндекс.Диску с no-cache и меткой времени;
- frontend: ответы API с Cache-Control: no-store, общий .env из корня проекта
  (и backend, и frontend), страницы/прокси админки нет;
- Dockerfile/compose/.env.example: без ADMIN_* и без тома настроек; в api-образ
  попадают все модули backend (проверяется тестом test_deploy);
- Makefile: make help/venv/run/run-api/run-front/test + docker-цели — «make run»
  поднимает всё локально без Docker;
- README: быстрый старт, полный список зависимостей, запуск локально и в Docker;
- deploy/: пример nginx и инструкция «второй стенд рядом с основным».

Тесты: 114 (удалены админские), все зелёные.
2026-10-06 15:15:44 +03:00
0e62d5ca8d Фронт: точки переноса в длинных ссылках (<wbr>), не полагаемся на CSS
Ссылка на онлайн-курс в поле аудитории — одно длинное слово. CSS-фикс
(overflow-wrap/word-break) работает в современных движках, но на телефоне
пользователя переполнение оставалось: старый движок или закэшированный CSS.

Теперь фронтенд сам расставляет <wbr>: после служебных символов ссылок
(/, ?, &, =, ., _, -) и не реже, чем каждые 24 символа; экранирование
сохраняется. Перенос работает в любом браузере.

Проверено в headless Chromium (390px и 320px): переполнений нет как с
включёнными CSS-переносами, так и с принудительно отключёнными
(эмуляция старого движка).
2026-09-22 15:32:43 +03:00
4685301773 Фронт: длинные ссылки переносятся, а не растягивают карточку
На телефонах ссылка вида https://next.uniyar.ac.ru/course/view.php?id=407
в поле аудитории растягивала карточку занятия и страницу по горизонтали.

- .period и .room-row: grid-колонка контента minmax(0, 1fr), иначе
  трек не может сжаться ниже min-content длинного слова
- .lesson и .room-card: min-width: 0 + word-break: break-word +
  overflow-wrap: anywhere (перенос по буквам, работает и в старых iOS)
- у main min-width: 0 уже был, так что переполнение ушло
2026-09-21 23:03:51 +03:00
3247ebcd64 Фронт: без бейджа «Обе недели», недели на страницах преподов/аудиторий, иконка приложения
- пары с одинаковыми ч/з показываются одним списком без пометки недели
  (группы, преподаватели, аудитории); раньше на страницах преподавателей
  и аудиторий такое занятие дублировалось в обеих колонках
- бейдж «Обе недели» убран как информационный шум (отсутствие пометки
  и означает «любая неделя»)
- favicon: квадрат с градиентом боковой панели (#1b2dd8 → #167cf2) и совой
- PWA: manifest.json, sw.js, apple-touch-icon и иконки 192/512 —
  при добавлении на главный экран смартфона появляется логотип
- tools/make_icons.py — генерация иконок из логотипа (rsvg-convert)
- удалён неиспользуемый roomPeriodCell и мёртвый favicon.png
2026-09-21 21:56:34 +03:00
419e65e960 Парсер: замечания отдельной строкой — уровня всей ячейки
В ячейке с несколькими занятиями (курсы по выбору) красная заметка,
занимающая строку(и) целиком, относится ко всем занятиям ячейки
(пара отменяется целиком) и раздаётся каждому занятию. Заметка,
написанная в одной строке с предметом/аудиторией, остаётся
привязанной к своему занятию. Многострочные standalone-заметки
сохраняют переводы строк.

Кейс: ПИЭ-13МО, понедельник, 5-я пара. Diff по 33 группам: затронуто
только поле note в multi-lesson ячейках; порядок «inline->ячейка».
2026-09-14 01:37:53 +03:00
e61ff1d683 xlsx: чистить «осиротевшие» значения в мержах (призрачные занятия)
В интерфейсе в объединённом блоке видно только верхне-левую ячейку;
значения, оставшиеся в остальных ячейках мержа (занесли текст, а сверху
наложили пустой мерж), в таблице не видны. Раньше парсер их отдавал:
реальный кейс — ПИЭ-41БО, вторник, 1 пара, знаменатель (мерж AH14:AI15).
По листу таких ячеек ровно одна; по 33 группам diff только этот слот.
2026-09-09 18:00:13 +03:00
5ac3db50d3 README: запуск (prod/dev), архитектура, устройство парсера, API, деплой 2026-09-09 15:11:46 +03:00
9c9875579c Тесты и инфраструктура
- tests/: 104 теста — lessons (все форматы ячеек), xlsx (мержи/скрытые
  столбцы/красный текст/даты), parsing (группы/недели/индексы), API (TestClient,
  TTL/modified/stale-while-error, 404/400/502)
- xlsx_builder.py: генерация xlsx в памяти для тестов без openpyxl
- docker-compose: healthz-хелскчеки (не блокируют старт), SCHEDULE_LINK
- Makefile: target test
- requirements: закреплённые версии
- .gitignore: __pycache__/, .venv/ и т.д.
2026-09-09 11:33:58 +03:00
2febf9b4ff Фронтенд: стабильность прокси и рендера
- main.py: таймаут прокси 150с (холодная загрузка бэкенда до ~2мин), /healthz, чистый список маршрутов
- app.js: таймаут fetch (AbortController), защита от гонок при переключении вкладок (requestSeq)
- style.css: стили chипа ошибки кэша (multi-line заметки уже поддерживались)
2026-09-09 11:33:43 +03:00
86c2a3097d Бэкенд: пакет schedule/ по зонам ответственности, обобщённый разбор ячеек
- xlsx.py: чтение книги (мержи, скрытые столбцы, красный текст), фикс дат до 1900
- lessons.py: общая схема ролей (предмет/преподаватель/аудитория/заметка)
  вместо ветвлений по числу строк; маркеры к.в., многострочные заметки,
  ФИО с/без отчества, организации, локации, метки групп -> заметки
- source.py: загрузка с Яндекс.Диска с понятными ошибками
- parsing.py: листы -> расписание, недели семестра, индексы преподов/аудиторий
- cache.py: lock, TTL, проверка modified, stale-while-error с retry cooldown
- main.py: create_app, /healthz, логирование; force-обновление убрано (вернётся с авторизацией)
- переменная окружения SCHEDULE_LINK
2026-09-09 11:33:19 +03:00
dcd9b854a0 Базовая функциональность 2026-09-09 08:25:53 +03:00