Админка нужна на «голом» сервере, поэтому возвращаю её в основную ветку. Она выключена, пока не заданы 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-образе (том настроек, запись истории).
479 lines
29 KiB
Markdown
479 lines
29 KiB
Markdown
# Расписание
|
||
|
||
Публичное расписание учебных групп. Источник данных — Яндекс.Таблица (xlsx на
|
||
Яндекс.Диске): файл скачивается по публичной ссылке, разбирается без сторонних
|
||
библиотек и отдаётся как JSON API и как сайт, который можно «установить» на
|
||
главный экран телефона (PWA).
|
||
|
||
Умеет:
|
||
|
||
- расписание по группам, преподавателям и аудиториям с разделением
|
||
числитель/знаменатель и бейджем текущей недели;
|
||
- занятость всех аудиторий по дням и поиск свободных на конкретную пару;
|
||
- совпадающие недели показываются одним списком, различающиеся — двумя
|
||
колонками, пустая — «Окно»;
|
||
- примечания из таблицы (красный текст) — отдельной строкой в карточке;
|
||
- PWA: манифест, service worker, иконки — «на главный экран».
|
||
|
||
## Архитектура
|
||
|
||
```
|
||
Яндекс.Диск (xlsx)
|
||
│ скачивание по публичной ссылке
|
||
▼
|
||
api — FastAPI, порт 8000
|
||
• качает и разбирает xlsx стандартной библиотекой (zipfile + xml.etree)
|
||
• кэш в памяти: TTL + проверка даты изменения файла + stale-while-error
|
||
• один воркер, поэтому кэш общий
|
||
▲ SCHEDULE_API=http://api:8000 (внутри docker-сети)
|
||
│
|
||
frontend — FastAPI, порт из .env (по умолчанию 8080)
|
||
• отдаёт index.html / app.js / style.css / иконки
|
||
• /api/* — тонкий прокси к api (браузер не знает адрес бэкенда)
|
||
▲
|
||
nginx — порты 80/443, https://<домен сайта> → 127.0.0.1:<порт фронта>
|
||
```
|
||
|
||
Роли:
|
||
|
||
- **docker** изолирует оба сервиса, перезапускает их при падении
|
||
(`restart: unless-stopped`) и следит за здоровьем (healthcheck на
|
||
`/healthz`); frontend стартует, только когда api стал healthy.
|
||
- Оба порта публикуются **только на 127.0.0.1** (жёстко задано в
|
||
`docker-compose.yml`): из интернета контейнеры напрямую недоступны.
|
||
- **nginx** — единственная дверь из интернета: принимает https и проксирует на
|
||
фронт. Его конфиг генерирует `deploy/nginx-site.sh` (это делает установщик).
|
||
|
||
## Установка на сервер
|
||
|
||
Скачайте zip с проектом, распакуйте и запустите одну команду:
|
||
|
||
```bash
|
||
unzip schedule.zip && cd schedule
|
||
bash install.sh
|
||
```
|
||
|
||
Установка требует root — скрипт сам перезапустится через sudo. До запуска DNS
|
||
домена уже должен указывать на сервер, а порты 80/443 — быть открытыми:
|
||
сертификат Let's Encrypt выпускается прямо в процессе установки.
|
||
|
||
Скрипт спросит:
|
||
|
||
| Вопрос | Что ответить |
|
||
|---|---|
|
||
| Как ставить: Docker или систему | Docker — рекомендуется (разница ниже) |
|
||
| Публичная ссылка на таблицу | ссылка Я.Диска, единственная обязательная настройка |
|
||
| Домен сайта | например `sched.example.ru`; пусто = без nginx, сайт на 127.0.0.1 |
|
||
| Порт api и порт сайта | по умолчанию 8000 и 8080 |
|
||
| Пользователь служб (только для «в систему») | от кого работают сервисы |
|
||
| Почта (только если задан домен) | для уведомлений Let's Encrypt |
|
||
|
||
Ответы можно передать и флагами: `--mode`, `--link`, `--domain`, `--api-port`,
|
||
`--front-port`, `--user`, `--email`. `install.sh --dry-run` показывает план
|
||
установки, ничего не меняя.
|
||
|
||
### Docker или система — что выбрать
|
||
|
||
**Docker** — рекомендуется: сервисы изолированы, автоматически перезапускаются,
|
||
обновляются одной командой, не зависят от Python в системе. Установщик поставит
|
||
Docker (если нет), запишет `.env`, соберёт образы, запустит контейнеры и
|
||
дождётся их готовности.
|
||
|
||
**Система (без Docker)** — если Docker не нужен: установщик проверит Python
|
||
3.11+, создаст `.venv`, поставит зависимости и зарегистрирует две службы
|
||
systemd — `schedule-api` и `schedule-frontend` (автозапуск после ребута,
|
||
перезапуск при падении).
|
||
|
||
В обоих случаях, если домен задан, установщик дополнительно поставит nginx и
|
||
certbot, выпустит сертификат Let's Encrypt (webroot — продление автоматическое,
|
||
сайт не останавливается) и настроит конфиг: http → https, прокси на порт
|
||
фронта. Снаружи виден только nginx.
|
||
|
||
### Ручная установка (без скрипта)
|
||
|
||
Установщик делает ровно следующее — при желании те же шаги выполняются руками.
|
||
|
||
**Docker:**
|
||
|
||
```bash
|
||
cp .env.example .env && nano .env # SCHEDULE_LINK (+ порты при желании)
|
||
sudo ./deploy.sh # собрать и запустить
|
||
curl -s http://127.0.0.1:8080/api/status # {"has_data":true} — работает
|
||
```
|
||
|
||
**Система:**
|
||
|
||
```bash
|
||
sudo apt install -y python3 python3-venv python3-pip # Python 3.11+
|
||
python3 -m venv .venv
|
||
.venv/bin/pip install -r requirements.txt
|
||
cp .env.example .env && nano .env # SCHEDULE_LINK
|
||
```
|
||
|
||
Две службы systemd (`/etc/systemd/system/`), затем
|
||
`sudo systemctl daemon-reload && sudo systemctl enable --now schedule-api schedule-frontend`:
|
||
|
||
```ini
|
||
# schedule-api.service
|
||
[Unit]
|
||
Description=Schedule API (FastAPI)
|
||
After=network-online.target
|
||
Wants=network-online.target
|
||
[Service]
|
||
User=www-data
|
||
WorkingDirectory=/путь/к/проекту/src/backend
|
||
ExecStart=/путь/к/проекту/.venv/bin/python -m uvicorn main:app --host 127.0.0.1 --port 8000
|
||
Restart=always
|
||
RestartSec=3
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
|
||
# schedule-frontend.service
|
||
[Unit]
|
||
Description=Schedule frontend (FastAPI)
|
||
After=network-online.target schedule-api.service
|
||
Wants=network-online.target schedule-api.service
|
||
[Service]
|
||
User=www-data
|
||
WorkingDirectory=/путь/к/проекту/src/frontend
|
||
Environment=SCHEDULE_API=http://127.0.0.1:8000
|
||
ExecStart=/путь/к/проекту/.venv/bin/python -m uvicorn main:app --host 127.0.0.1 --port 8080
|
||
Restart=always
|
||
RestartSec=3
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
```
|
||
|
||
**nginx + сертификат** (общий шаг для обоих способов; `.env` уже должен быть):
|
||
|
||
```bash
|
||
sudo apt install -y nginx certbot
|
||
# временный http-конфиг домена с location /.well-known/acme-challenge/ → /var/www/html
|
||
sudo certbot certonly --webroot -w /var/www/html -d sched.example.ru
|
||
sudo deploy/nginx-site.sh --domain sched.example.ru --cert sched.example.ru --port 8080
|
||
curl -sI https://sched.example.ru/ | head -3 # HTTP/2 200 — готово
|
||
```
|
||
|
||
`deploy/nginx-site.sh` генерирует финальный конфиг (http → https, прокси на
|
||
`FRONTEND_PORT` из `.env`, таймауты под холодный кэш), включает его, проверяет
|
||
`nginx -t` и перечитывает nginx. Посмотреть конфиг, ничего не меняя:
|
||
`deploy/nginx-site.sh --print --domain sched.example.ru`.
|
||
|
||
### Обслуживание
|
||
|
||
- **Обновление**: Docker — `cd <каталог проекта> && sudo ./deploy.sh`;
|
||
система — заменить файлы проекта и
|
||
`sudo systemctl restart schedule-api schedule-frontend`.
|
||
- **Диагностика**: Docker — `sudo docker compose ps` и
|
||
`sudo docker logs api --tail 50`; система — `systemctl status schedule-api`
|
||
и `journalctl -u schedule-api -u schedule-frontend`. Состояние кэша —
|
||
`/api/status` (смотреть поле `error`).
|
||
- **Ротация логов контейнеров** — настроить сразу: без лимита логи растут
|
||
вечно и однажды забивают диск. `/etc/docker/daemon.json`:
|
||
|
||
```json
|
||
{ "log-driver": "json-file", "log-opts": { "max-size": "10m", "max-file": "3" } }
|
||
```
|
||
|
||
затем `sudo systemctl restart docker`.
|
||
- **Диск**: следить `df -h /`; раз в месяц
|
||
`sudo docker system prune -af && sudo journalctl --vacuum-size=200M`.
|
||
|
||
## Локальная разработка
|
||
|
||
Нужны Python 3.11+ и bash (Linux, macOS или WSL; на Windows без WSL — Docker).
|
||
|
||
```bash
|
||
python3 --version # должен быть 3.11 или новее
|
||
cp .env.example .env # вписать SCHEDULE_LINK
|
||
make run # создаст .venv, поставит зависимости, поднимет оба сервиса
|
||
```
|
||
|
||
Открыть <http://127.0.0.1:8080> (API — <http://127.0.0.1:8000>). Остановить —
|
||
`Ctrl+C`; отвязать от терминала — `nohup make run &`.
|
||
|
||
Данные скачиваются сразу на старте (несколько секунд) и дальше обновляются по
|
||
интервалу.
|
||
|
||
Полезные команды (`make help` — весь список):
|
||
|
||
| Команда | Что делает |
|
||
|---|---|
|
||
| `make venv` | создать `.venv` и установить зависимости |
|
||
| `make run` (или `make dev`) | запустить API и сайт |
|
||
| `make run-api` / `make run-front` | запустить только один из сервисов |
|
||
| `make test` | прогнать тесты |
|
||
| `make nginx-print` / `make nginx-install` | показать / установить конфиг nginx |
|
||
|
||
Без make — то же самое руками:
|
||
|
||
```bash
|
||
python3 -m venv .venv
|
||
.venv/bin/pip install -r requirements-dev.txt
|
||
.venv/bin/python -m uvicorn main:app --reload --port 8000 # в каталоге src/backend
|
||
.venv/bin/python -m uvicorn main:app --reload --port 8080 # в каталоге src/frontend
|
||
```
|
||
|
||
### Тесты
|
||
|
||
```bash
|
||
make test # 121 тест, сеть не нужна
|
||
```
|
||
|
||
## Зависимости
|
||
|
||
**Системные требования**
|
||
|
||
| Что | Зачем |
|
||
|---|---|
|
||
| Python 3.11+ | локальная разработка и режим «в систему» |
|
||
| Docker + docker compose v2 | режим Docker (опционально) |
|
||
|
||
**Python-пакеты** — закреплены в `requirements.txt` (рабочие) и
|
||
`requirements-dev.txt` (тесты):
|
||
|
||
| Пакет | Версия | Назначение |
|
||
|---|---|---|
|
||
| fastapi | 0.141.1 | веб-фреймворк обоих сервисов |
|
||
| uvicorn | 0.52.4 | ASGI-сервер, которым запускаются сервисы |
|
||
| python-dotenv | 1.2.3 | чтение `.env` |
|
||
| requests | 2.34.2 | скачивание файла с Я.Диска и прокси к API |
|
||
| pytest | 9.1.1 | тесты — только dev |
|
||
| httpx2 | 2.12.0 | HTTP-клиент (наследник httpx) для `TestClient` — только dev |
|
||
|
||
Больше ничего не нужно: xlsx разбирается стандартной библиотекой, базы данных
|
||
нет, кэш — в памяти процесса. httpx2 в рабочем коде не используется и нужен
|
||
только тестам; модуль импортируется как `httpx2` — httpx перешёл под опеку
|
||
Pydantic и был переименован в августе 2026, `import httpx` больше не работает.
|
||
|
||
### Если установка не идёт
|
||
|
||
- Проверьте `python3 --version`: нужен 3.11 или новее. Если под `python3`
|
||
скрывается старая версия, создайте окружение нужной явно:
|
||
`python3.12 -m venv .venv`.
|
||
- Debian/Ubuntu: если `make venv` падает на создании окружения, доставьте
|
||
пакет venv: `sudo apt install python3-venv` (для нестандартной версии —
|
||
например, `python3.12-venv`).
|
||
|
||
### Если pip не находит пакеты: зеркала
|
||
|
||
Если pip настроен на корпоративное/национальное зеркало, оно может отставать
|
||
от PyPI — свежих пакетов там ещё нет. Диагностика и обход:
|
||
|
||
```bash
|
||
pip index versions httpx2 # есть ли пакет на текущем индексе
|
||
pip install -r requirements-dev.txt -i <зеркало> # разово через другое зеркало
|
||
pip config set global.index-url <зеркало> # закрепить зеркало навсегда
|
||
```
|
||
|
||
Зеркала, проверенные 07.10.2026 (пакеты `httpx2`/`httpcore2` на месте;
|
||
URL подставлять как есть в `-i` / `index-url`):
|
||
|
||
| Зеркало | URL |
|
||
|---|---|
|
||
| PyPI, официальный | `https://pypi.org/simple` |
|
||
| TUNA (Tsinghua) | `https://pypi.tuna.tsinghua.edu.cn/simple` |
|
||
| Huawei Cloud | `https://mirrors.huaweicloud.com/repository/pypi/simple` |
|
||
| BFSU | `https://mirrors.bfsu.edu.cn/pypi/web/simple` |
|
||
| SJTU | `https://mirror.sjtu.edu.cn/pypi/web/simple` |
|
||
| USTC | `https://mirrors.ustc.edu.cn/pypi/simple` |
|
||
| SUSTech | `https://mirrors.sustech.edu.cn/pypi/simple` |
|
||
|
||
Примечания: Alibaba Cloud (`https://mirrors.aliyun.com/pypi/simple`) обычно
|
||
тоже свежий, но из части сетей недоступен; Яндекс-зеркало (`mirror.yandex.ru`)
|
||
PyPI не раздаёт вовсе. Зеркала отстают по-разному, поэтому для свежих пакетов
|
||
надёжнее разово ставить с официального индекса.
|
||
|
||
## Переменные окружения
|
||
|
||
| Переменная | По умолчанию | Где | Описание |
|
||
|---|---|---|---|
|
||
| `SCHEDULE_LINK` | — | api | публичная ссылка Я.Диска, **обязательна** |
|
||
| `SCHEDULE_INTERVAL_MINUTES` | `15` | api | как часто перечитывать таблицу, минут |
|
||
| `SCHEDULE_CACHE_TTL` | `900` | api | устаревшее: интервал в секундах (если не задан предыдущий) |
|
||
| `SCHEDULE_RETRY_AFTER` | `60` | api | пауза между попытками при сбое источника |
|
||
| `SCHEDULE_API` | `http://127.0.0.1:8000` | frontend | адрес бэкенда (в docker: `http://api:8000`) |
|
||
| `SCHEDULE_PROXY_TIMEOUT` | `150` | frontend | таймаут прокси к бэкенду, сек |
|
||
| `ADMIN_PATH` | — | api+frontend | секретный путь админки; пусто = админка выключена |
|
||
| `ADMIN_LOGIN` / `ADMIN_PASSWORD` | — | api | логин и пароль администратора |
|
||
| `ADMIN_MAX_ATTEMPTS` | `3` | api | сколько неудачных попыток входа допускается |
|
||
| `ADMIN_LOCKOUT_MINUTES` | `15` | api | на сколько минут блокировать вход после них |
|
||
| `SETTINGS_FILE` | `data/settings.json` | api | где хранить ссылку, интервал и историю входов |
|
||
| `API_PORT` | `8000` | docker | внешний порт api (публикуется на 127.0.0.1) |
|
||
| `FRONTEND_PORT` | `8080` | docker | внешний порт frontend (публикуется на 127.0.0.1) |
|
||
|
||
`.env` ищется вверх от каталога модуля: и backend, и frontend находят общий
|
||
`.env` в корне репозитория; `load_dotenv` не перезаписывает уже выставленные
|
||
переменные окружения.
|
||
|
||
## Админка
|
||
|
||
Необязательная страница для управления источником данных. Отдельного входа на
|
||
сайте нет: панель открывается по секретному пути `ADMIN_PATH`, а пока
|
||
`ADMIN_LOGIN`/`ADMIN_PASSWORD` пусты, админка выключена и наружу ничего не
|
||
торчит (маршруты `/api/admin/*` не регистрируются).
|
||
|
||
Включить: заполнить в `.env` четыре переменные и перезапустить службу.
|
||
|
||
```ini
|
||
ADMIN_PATH=/panel-8f3a91x7 # придумайте длинный и неочевидный
|
||
ADMIN_LOGIN=admin
|
||
ADMIN_PASSWORD=<длинный пароль>
|
||
```
|
||
|
||
Что умеет:
|
||
|
||
- **посмотреть состояние**: сколько групп, когда обновлялось, дата файла на
|
||
Диске, изменился ли файл при последнем чтении (`sha256`), есть ли ошибки;
|
||
- **вставить новую ссылку** на таблицу: она проверяется скачиванием и разбором
|
||
до сохранения — нерабочая не применится, данные на сайте не пострадают;
|
||
- **задать интервал обновления** в минутах (`1…1440`);
|
||
- **обновить сейчас**: скачивает таблицу принудительно, не дожидаясь интервала
|
||
(по интервалу файл перечитывается, только если на Диске изменилась дата);
|
||
- **видеть входы**: последняя попытка (откуда, когда, успешна ли) и последний
|
||
успешный вход — и в панели, и в логе службы.
|
||
|
||
Защита входа: после `ADMIN_MAX_ATTEMPTS` неудач (по умолчанию 3) вход с этого
|
||
адреса блокируется на `ADMIN_LOCKOUT_MINUTES` минут (по умолчанию 15). Сессия —
|
||
подписанная cookie на 12 часов.
|
||
|
||
Настройки (ссылка, интервал, история входов) хранятся в `SETTINGS_FILE`
|
||
(по умолчанию `data/settings.json` в корне проекта; в docker — в томе
|
||
`schedule_data`). Если файл недоступен для записи, изменения живут в памяти до
|
||
перезапуска, а панель предупредит об этом.
|
||
|
||
## Структура кода
|
||
|
||
```
|
||
install.sh интерактивная установка на сервер: Docker или systemd + nginx
|
||
src/
|
||
backend/
|
||
main.py FastAPI: маршруты, create_app(cache), /healthz
|
||
admin.py админка: cookie-сессия, блокировка входа, ссылка, интервал, force
|
||
schedule/
|
||
xlsx.py чтение xlsx: общие строки, объединённые ячейки,
|
||
скрытые столбцы, красный текст, серийные даты
|
||
source.py скачивание файла по публичной ссылке Я.Диска
|
||
lessons.py разбор ячеек занятий (общий конвейер ролей)
|
||
parsing.py лист → расписание, недели, индексы преподавателей/аудиторий
|
||
cache.py ScheduleCache: блокировка, интервал, stale-while-error, force
|
||
settings.py хранилище настроек админки (ссылка, интервал, история входов)
|
||
errors.py ScheduleError
|
||
frontend/
|
||
main.py FastAPI: статика + прокси /api/* (таймаут 150 c)
|
||
admin.html страница админки (вне static: прямой доступ закрыт)
|
||
static/
|
||
index.html, app.js, style.css
|
||
manifest.json PWA: имя, иконки, standalone-режим
|
||
sw.js минимальный service worker (без кэша) для установки приложения
|
||
img/ логотипы и иконки приложения
|
||
tools/
|
||
make_icons.py генерация иконок приложения из логотипа (нужен rsvg-convert)
|
||
deploy/
|
||
nginx-site.sh генерация и включение конфига nginx (используется install.sh)
|
||
tests/
|
||
conftest.py общие фикстуры
|
||
xlsx_builder.py генерация xlsx в памяти (без openpyxl)
|
||
fixtures.py общая тестовая таблица
|
||
test_lessons.py все форматы ячеек
|
||
test_xlsx.py чтение книги
|
||
test_parsing.py разбор листов, недели, индексы
|
||
test_api.py API и поведение кэша (интервал/stale, 404/400/502)
|
||
test_admin.py админка: вход, блокировка, история, ссылка/интервал/force
|
||
test_settings.py хранилище настроек админки
|
||
test_deploy.py проверка, что образы копируют все нужные файлы
|
||
```
|
||
|
||
Данные админки (`data/settings.json`) — рабочие, в git не попадают.
|
||
|
||
## Как устроена таблица-источник
|
||
|
||
Лист «Бакалавры и магистры» — это сетка:
|
||
|
||
```
|
||
столбцы 0–1 даты «числителя»: начало и конец недели (серийные номера Excel)
|
||
столбцы 2–3 даты «знаменателя»
|
||
столбец 4 день недели (указан один раз на блок дня)
|
||
столбец 5 номер пары и время (ячейка объединена на две строки пары)
|
||
столбцы 6+ группы — по одному столбцу на группу (скрытые столбцы пропускаются)
|
||
```
|
||
|
||
Каждый день занимает блок строк, и строки идут парами: «числитель» /
|
||
«знаменатель». У каждой пары строк — своя неделя семестра, а даты в ячейках —
|
||
это соседние недели по порядку. Поэтому недели семестра собираются со всего
|
||
листа (повторы отбрасываются по паре «чётность + дата»), а текущая чётность
|
||
(«Сейчас» на бейдже) определяется по сегодняшней дате.
|
||
|
||
## Как разбирается ячейка занятия
|
||
|
||
Ячейка свободного формата, и её содержимое проходит единый конвейер
|
||
(`schedule/lessons.py`) без особых веток на число строк:
|
||
|
||
1. Красные фрагменты (в xlsx помечаются как `⟪...⟫` ещё при чтении строк)
|
||
выносятся в поле `note` и из остального текста убираются, даже если внутри
|
||
них спрятано другое занятие; обрамляющие скобки у таких пометок снимаются.
|
||
2. Маркер курса по выбору «к.в. [- N]» может быть отдельной строкой или
|
||
префиксом предмета. Если в ячейке несколько курсов, а маркер один, его
|
||
получают все курсы этой ячейки.
|
||
3. Каждая строка относится к одной из ролей:
|
||
- **аудитория** — `ауд. N`, `каб. N`, URL, `онлайн`, `зал…` (включая
|
||
корпус: «юридический корпус, 3 этаж ауд. 22»);
|
||
- **преподаватель** — фамилия с инициалами (`Смирнов А.В.`), полное ФИО,
|
||
организация (`ПравоТех`, `НПО Криста`); преподавателей может быть
|
||
несколько (через `,` или `/`); имя допускается в одной строке
|
||
с предметом и (или) аудиторией;
|
||
- **предмет** — всё, что стоит до преподавателя/аудитории (название может
|
||
переноситься на несколько строк);
|
||
- **заметка** — время («начало в 14:05»), даты («с 8.09.26»),
|
||
«ссылка в эл. университете» и прочие примечания.
|
||
4. Результат приводится к единому виду: время `ЧЧ:ММ-ЧЧ:ММ`, аудитория `ауд. N`.
|
||
|
||
В одной ячейке может быть несколько занятий: они разделяются пустой строкой,
|
||
новым маркером «к.в.» или началом нового предмета сразу после строки
|
||
аудитории/преподавателя. Если пустой строкой (или красной заметкой, снятой с
|
||
целой строки) от предмета оторвался только преподаватель и/или аудитория, этот
|
||
кусок достраивает предыдущее занятие, а не создаёт занятие без предмета — иначе
|
||
в расписании аудитории оставались бы одни группы.
|
||
|
||
Итог по паре: `{time, ч: {raw, lessons[]}, з: {raw, lessons[]}}`, где
|
||
`lesson = {marker, subject, teacher, room, note}`.
|
||
|
||
## Как отображается
|
||
|
||
- Если занятия числителя и знаменателя совпадают, пара показывается одним
|
||
списком без пометки недели (одинаково для групп, преподавателей и
|
||
аудиторий); иначе — две колонки «числитель»/«знаменатель», пустая
|
||
заменяется на «Окно».
|
||
- Подпись недели появляется только там, где недели действительно
|
||
различаются; бейдж «Сейчас» подсвечивает текущую неделю.
|
||
- Длинные ссылки в карточке переносятся по служебным символам и каждые
|
||
24 символа (`<wbr>`), поэтому карточка не вылезает за экран даже в старых
|
||
браузерах, где нет `overflow-wrap`.
|
||
- Приложение можно «установить» на главный экран (манифест + service
|
||
worker + иконки приложения); на iOS иконку берёт `apple-touch-icon`.
|
||
|
||
## API
|
||
|
||
Полный список методов отдаёт сам бэкенд: `GET /`.
|
||
|
||
| Метод | Путь | Описание |
|
||
|---|---|---|
|
||
| GET | `/groups` | список групп (сначала БО, потом МО) |
|
||
| GET | `/schedule/{group}` | расписание группы с разбором на занятия |
|
||
| GET | `/schedule/{group}/{day}` | расписание группы на конкретный день |
|
||
| GET | `/today?group={group}` | расписание группы на сегодня |
|
||
| GET | `/teachers` | список преподавателей |
|
||
| GET | `/teacher/{name}` | расписание преподавателя (+ `full_name`) |
|
||
| GET | `/rooms` | список аудиторий (занятые ∪ лист «аудитории») |
|
||
| GET | `/room/{label}` | занятость аудитории |
|
||
| GET | `/rooms-day?day={день}` | занятость всех аудиторий за день |
|
||
| GET | `/rooms-free?day={день}&period={N}` | свободные аудитории |
|
||
| GET | `/week` | недели семестра и текущая чётность |
|
||
| GET | `/status` | состояние кэша (`last_updated`, `table_modified`, `error`, …) |
|
||
| GET | `/healthz` | живость процесса (не обращается к таблице) |
|
||
|
||
Фронт проксирует только то, что использует, под префиксом `/api/...`:
|
||
`groups`, `schedule/{group}`, `teachers`, `teacher/{name}`, `rooms`,
|
||
`room/{label}`, `rooms-day`, `rooms-free`, `week`, `status`.
|
||
Остальное (`today`, `schedule/{group}/{day}`) доступно только на бэкенде.
|