Тип, оснащение и пометки приводятся к одному виду: слова пишутся со строчной,
а аббревиатуры («ПК», «МО», «СТ») остаются полностью заглавными; в подписях
фильтров то же самое. В карточках теперь «комп.класс · 12 мест · ПК, маркер»,
«МО и СТ; без проектора». Тесты: 162.
Раньше описание выводилось целиком, и у аудиторий вида 405 («пк») получался
дубль: «ПК» в атрибутах и «пк» строкой ниже.
Теперь описание разбирается по частям: вместимость вырезается, а куски,
которые целиком состоят из типа/оснащения/дней/номера, не показываются.
Остаются только реальные дополнения: «без проектора», «МО и СТ», «амфитиатром».
Слова не режутся по частям (раньше появлялись осколки вида «а», «-», «мелова»);
короткие теги («ПК») учитываются, регистр сохраняется.
- убрана подсказка «С ограничением по дням: N» из панели фильтров;
- в карточке убран префикс «в таблице:» — осталось «занятия только по
четвергам и пятницам», выделенное жёлтым (в тёмной теме — светлее);
- класс .room-note.bad больше не использовался, удалён.
Раньше строку с кнопкой «Сбросить фильтры» и счётчиком перекрывала подсказка
(«скрыто …»), и счётчик висел в середине блока. Теперь порядок строк:
переключатели режима → фильтры → оснащение → подсказка → строка со сбросом и
счётчиком (выровнены по базовой линии, строка вплотную к нижнему отступу).
- описание из таблицы не показываем, если оно сводится к номеру и дням
(«402 четверг пятница») — это дублировало пометку и путало;
- пометка про дни формулируется по-русски: «в таблице: занятия только
по четвергам и пятницам»;
- убрана вторая строка «в этот день занятий в ней нет» (следовала из первой);
- подсказка в фильтрах вместо «аудитория отмечена как доступные…» — просто
«с ограничением по дням: N», предложения начинаются с большой буквы.
- на сайте логотип лежит внутри ссылки и как строчный элемент добавлял 7px:
шапка была 70px против 63px в админке. Теперь логотип блочный, а ссылка —
inline-flex: обе шапки 63px (и min-height layout сходится точно);
- выход и «Админка» при наведении только меняют цвет (тёмно-красный и
синий соответственно), без подъёма, тени и transform — как остальные
кнопки проекта.
Интерфейс:
- после входа на главной появляются «Админка» и «Выйти» — в шапке, перед
переключателем темы и после него соответственно (порядок: Админка, тема,
Выйти);
- «Выйти» теперь красная и заметно реагирует на наведение (темнеет,
приподнимается, тень) — и на сайте, и в админке (одна кнопка в шапке,
из карточки убрана);
- в админке убраны «осталось попыток», путь к файлу настроек и карточка
«Данные расписания» (дублировала шапку); блокировка показывается отдельной
плашкой только когда действительно активна.
Безопасность пути:
- путь к панели не отдаётся ни в HTML, ни в JSON (из /api/admin/state убран),
ни в JS: на главной для вошедшего только флаг, а кнопка «Админка» ведёт на
служебный /api/admin/goto, который отвечает 302 на секретный путь;
- без сессии /api/admin/goto и /api/admin/state отвечают 401, неизвестные пути
дают 404 — перебором путь не находится;
- страница панели: noindex/nofollow/noarchive, Referrer-Policy: no-referrer,
X-Frame-Options: DENY, Cache-Control: no-store;
- robots.txt без пути (только Disallow: /api/), внешние ссылки в панели —
rel=noopener noreferrer.
Тесты: 161 (состояние без пути, /goto: 401 без входа и 302 с ним).
- шапка как на сайте (логотип факультета, «на сайт», переключатель светлой/тёмной
темы — тема общая с сайтом и не мигает при загрузке);
- крупная шапка состояния: «Работает / Ожу данные / Ошибка загрузки», сколько
обновлено назад, число групп и текущий интервал; рядом главная кнопка
«Обновить сейчас»;
- вместо россыпи чипов — карточки: «Ссылка на расписание» (с кнопкой
«открыть таблицу»), «Обновление» (интервал + факты: изменился ли файл, дата,
отпечаток), «Входы в админку» (последняя попытка, последний успешный вход,
остаток попыток или блокировка), «Данные расписания»;
- предупреждения (ошибка загрузки, невозможность сохранить настройки) вынесены
отдельной плашкой под шапкой, а не спрятаны среди полей;
- верстка адаптивная: сетка карточек, на телефоне одна колонка и подписи
под значениями; формы с подписями и подсказками, кнопки с ясной иерархией;
- уведомления и вся логика прежние; тесты 160.
В карточке остаётся формулировка из таблицы («комп.класс Linux»), а в фильтре
близкие варианты сведены к одной группе: «комп.класс» (комп.класс, комп.класс
Linux, компьютерный класс), «кабинет английского» (английский, кабинет
английского), «аудитория кафедры», «лекционная», «лекционно-практическая»,
«аудитория для практических». Добавлен пункт «без типа» — 4 аудитории, где в
таблице тип не указан.
Бэкенд отдаёт в /rooms-day и /rooms-free дополнительное поле type_group
(room_type_group), фронт фильтрует по нему. Тесты: 160.
Бэкенд:
- 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.
Для личного использования достаточно deploy.sh (проверяет окружение, собирает
образы и поднимает контейнеры). Раздел «Установка на сервер» переписан:
Docker через deploy.sh + вариант без Docker (systemd) и nginx, без интерактивного
установщика и его флагов.
Админка нужна на «голом» сервере, поэтому возвращаю её в основную ветку. Она
выключена, пока не заданы 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-образе (том настроек, запись истории).
Админка осталась отдельной ветвью 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 (удалены админские), все зелёные.
Ссылка на онлайн-курс в поле аудитории — одно длинное слово. CSS-фикс
(overflow-wrap/word-break) работает в современных движках, но на телефоне
пользователя переполнение оставалось: старый движок или закэшированный CSS.
Теперь фронтенд сам расставляет <wbr>: после служебных символов ссылок
(/, ?, &, =, ., _, -) и не реже, чем каждые 24 символа; экранирование
сохраняется. Перенос работает в любом браузере.
Проверено в headless Chromium (390px и 320px): переполнений нет как с
включёнными CSS-переносами, так и с принудительно отключёнными
(эмуляция старого движка).
На телефонах ссылка вида 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 уже был, так что переполнение ушло
- пары с одинаковыми ч/з показываются одним списком без пометки недели
(группы, преподаватели, аудитории); раньше на страницах преподавателей
и аудиторий такое занятие дублировалось в обеих колонках
- бейдж «Обе недели» убран как информационный шум (отсутствие пометки
и означает «любая неделя»)
- favicon: квадрат с градиентом боковой панели (#1b2dd8 → #167cf2) и совой
- PWA: manifest.json, sw.js, apple-touch-icon и иконки 192/512 —
при добавлении на главный экран смартфона появляется логотип
- tools/make_icons.py — генерация иконок из логотипа (rsvg-convert)
- удалён неиспользуемый roomPeriodCell и мёртвый favicon.png
В ячейке с несколькими занятиями (курсы по выбору) красная заметка,
занимающая строку(и) целиком, относится ко всем занятиям ячейки
(пара отменяется целиком) и раздаётся каждому занятию. Заметка,
написанная в одной строке с предметом/аудиторией, остаётся
привязанной к своему занятию. Многострочные standalone-заметки
сохраняют переводы строк.
Кейс: ПИЭ-13МО, понедельник, 5-я пара. Diff по 33 группам: затронуто
только поле note в multi-lesson ячейках; порядок «inline->ячейка».
В интерфейсе в объединённом блоке видно только верхне-левую ячейку;
значения, оставшиеся в остальных ячейках мержа (занесли текст, а сверху
наложили пустой мерж), в таблице не видны. Раньше парсер их отдавал:
реальный кейс — ПИЭ-41БО, вторник, 1 пара, знаменатель (мерж AH14:AI15).
По листу таких ячеек ровно одна; по 33 группам diff только этот слот.