- на сайте логотип лежит внутри ссылки и как строчный элемент добавлял 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.
Админка нужна на «голом» сервере, поэтому возвращаю её в основную ветку. Она
выключена, пока не заданы 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-образе (том настроек, запись истории).