Skip to content

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

CEITI → локальные уведомления iPhone

Готовый сервер

Адрес: https://ceiti-scriptable.onrender.com — он уже указан в scriptable/scriptable.js. На iPhone вставьте скрипт в Scriptable и выберите subgroup: 1 или subgroup: 2. Сервис размещён на Render Free во Франкфурте. После простоя он засыпает; скрипт делает до трёх попыток загрузки с timeout 60 секунд на попытку. iOS может завершить долгую автоматизацию раньше, поэтому запуски утром и вечером лучше дублировать через несколько минут. SQLite на бесплатном тарифе не сохраняется после пересоздания/засыпания сервиса. После запуска расписание загружается из CEITI заново; при одновременном сбое CEITI кэша может не оказаться.

Python 3.11+, FastAPI, httpx, Pydantic, SQLite/aiosqlite и Scriptable. Сервер получает живой JSON CEITI и обновляет кэш каждые 10 минут. iPhone заранее планирует локальные уведомления; сервер не отправляет APNs push.

Что исследовано

Проверено 28 сентября 2026 года:

  • Сайт группы — Next.js.
  • Каталог групп возвращает name и _id.
  • JavaScript сайта вызывает https://orar-api.ceiti.md/v1/orar?_id=<ID>&tip=class.
  • ID P-2434R при проверке: 6aa7d03f1eed62fa3d1347dd. В коде он не зафиксирован: ищется по имени при обновлении.
  • Ответ содержит data.monday…friday, номера пар, ветви both/par/impar, а также periods[].starttime/endtime. Времена не захардкожены.
  • groupids.entireclass="1" — вся группа. Иначе учитывается Grupa 1 или Grupa 2. Неизвестная структура отклоняется, последний корректный кэш сохраняется.
  • Проверенный bundle: https://orar.ceiti.md/_next/static/chunks/app/page-4c152a9e13b7f862.js. Компонент Schedule показывает оба варианта недели одновременно, без выбора по сегодняшней дате. Для ячейки с par/impar они имеют приоритет над both, как на сайте.

API не сообщает календарную дату действия, праздники и текущую академическую неделю. Поэтому проект не использует чётность ISO-недели и не обещает подтверждённое расписание на произвольную дату. Он проецирует последний опубликованный недельный шаблон на выбранный день. Архив прошлых расписаний не ведётся.

Неделя: уже настроено для этого пользователя

Пользователь подтвердил: неделя с понедельника 2026-09-28 — impar. В локальном .env уже записано:

WEEK_ANCHOR_DATE=2026-09-28
WEEK_ANCHOR_TYPE=impar

От этой точки применяется чередование каждые 7 календарных дней, включая переход года. Это явно заданная политика на основе подтверждения пользователя, не найденная в сайте формула. Если CEITI прерывает чередование на каникулах, обновите опорную неделю после подтверждения или задайте исключения:

WEEK_OVERRIDES={"2026-10-05":"impar"}
CLOSED_DATES=["2026-12-25"]

Это примеры синтаксиса, не подтверждённые каникулы или недели CEITI. Даты WEEK_OVERRIDES должны быть понедельниками. Исключение действует только на указанную неделю и не сдвигает последующее чередование. Для постоянного сдвига измените anchor. После изменения .env перезапустите сервер.

В .env.example anchor намеренно пуст. Если он не настроен, дни с зависимыми от недели парами имеют complete=false. Scriptable удаляет старые уведомления этих дней и не создаёт потенциально ошибочные новые. Для дней, одинаковых на обеих неделях, уведомления работают. Выходные пустые; официально подтверждённые отмены всего дня можно добавить в CLOSED_DATES.

Установка и запуск

  1. Установите Python 3.11 или новее с python.org. На Windows включите добавление Python в PATH. Проверьте python --version (или py --version).
  2. Откройте терминал в каталоге проекта, где находится main.py.
  3. Создайте окружение:
python -m venv .venv

Windows cmd:

.venv\Scripts\activate

Windows PowerShell:

.\.venv\Scripts\Activate.ps1

Linux/macOS:

source .venv/bin/activate

Если активация PowerShell запрещена политикой, используйте .\.venv\Scripts\python.exe -m pip и .\.venv\Scripts\python.exe -m uvicorn вместо изменения системной политики.

pip install -r requirements.txt

Для новой копии проекта скопируйте .env.example в .env: copy .env.example .env в cmd, Copy-Item .env.example .env в PowerShell или cp .env.example .env на Linux/macOS. В текущей папке .env уже создан — не перезаписывайте подтверждённую неделю.

uvicorn main:app --host 0.0.0.0 --port 8000

Работайте с одним worker: он ведёт цикл обновления и локальную блокировку кэша. Сервер обновляет расписание при запуске, затем каждые REFRESH_SECONDS (600 по умолчанию, максимум 900), в том числе утром. Запрос также обновит просроченный кэш. SQLite сохраняется в data/ceiti.sqlite3.

В этом рабочем каталоге зависимости уже установлены в .venv; Python runtime не был доступен через PATH, поэтому окружение создано встроенным Python 3.12.

Проверка API

Откройте Swagger и расписание сегодня.

Метод Путь Назначение
GET /health Процесс жив; не гарантия доступности CEITI
GET /api/config Часовой пояс, настройки недели и кэша
GET /api/schedule/today Сегодня в Кишинёве
GET /api/schedule/tomorrow Завтра в Кишинёве
GET /api/schedule/date/2026-09-28 Проекция шаблона на дату
GET /api/schedule/next Ближайшая ещё не начавшаяся пара за 8 дней
GET /api/schedule/plan Сегодня и завтра из одной версии кэша + время вечернего уведомления

Расписание принимает group=P-2434R&subgroup=1 (либо 2). /plan дополнительно принимает summary_hour=19&summary_minute=40. DEFAULT_GROUP задаёт группу фонового прогрева; явный параметр group выбирает группу запроса, по умолчанию P-2434R. Другие успешно загруженные группы тоже обновляются в фоне.

API ограничивает даты диапазоном −7…+14 дней от текущей даты; это не исторический архив. Даты вне диапазона, неправильные даты и подгруппы дают 422, неизвестная группа — 404. Без сети и кэша — 503, сервер продолжает работу. При неизвестной неделе /next возвращает 409, если не может достоверно выбрать следующую пару.

Ответ включает complete, stale, fetched_at, warnings, week_type, lessons. Каждая пара содержит date, lesson_number, start, end, start_at, end_at, subject, room, teacher, subgroup, week_type. Поля start_at/end_at — ISO 8601 со смещением Кишинёва. Scriptable использует эти абсолютные моменты, поэтому часовой пояс iPhone не меняет момент уведомления. Локальные даты вычисляет Python ZoneInfo; пакет tzdata обеспечивает работу и на Windows.

Реально полученный пример P-2434R / Grupa 1 на понедельник 28 сентября:

Пара Время Предмет Кабинет Преподаватель
1 08:00–09:20 Inform 207(17) Sarapanovscaia I.
2 09:30–10:50 L.rom 244 Pozneac S.
3 11:30–12:50 L.eng(A) 315 Chinisciuc M.
4 13:00–14:20 Pv 207(17) Sarapanovscaia I.

Это снимок для документации, не источник данных приложения.

iPhone: Scriptable

  1. Установите Scriptable из App Store.
  2. Нажмите +, назовите скрипт CEITI, вставьте весь файл scriptable/scriptable.js.
  3. В начале измените serverUrl на HTTPS URL доступного сервера, group и subgroup (1 или 2). Настройки времени уведомлений находятся рядом в CONFIG.
  4. Запустите кнопкой ▶. Разрешите сеть и уведомления. Посмотрите журнал: там будет количество запланированных уведомлений.
  5. В настройках уведомлений iOS разрешите Scriptable показ на экране блокировки и звук; проверьте влияние Focus и сводки уведомлений.

127.0.0.1 на телефоне — сам телефон. Для постоянной работы используйте размещённый HTTPS сервер. Для теста в одной Wi-Fi сети можно использовать IP компьютера с портом 8000, сервер с --host 0.0.0.0 и разрешённый входящий порт Windows Firewall; компьютер должен оставаться включённым. iOS может ограничивать незашифрованный HTTP, поэтому HTTPS предпочтителен.

Скрипт планирует:

  • Перед первой фактической парой дня — за 10 минут до её начала. О каждой следующей — за 5 минут до окончания предыдущей фактической пары: например, о третьей в 10:45, перед окончанием второй в 10:50 и большой переменой до 11:30. Окна не заполняются вымышленными парами; напоминание привязано к предыдущей существующей паре. Настройка: beforePreviousLessonEndMinutes: 5.
  • Одно уведомление за 15 минут до самого позднего окончания дня.
  • В 19:40 краткое расписание завтра или «Завтра отдыхаем».
  • Дополнительно завтрашние пары и конец дня, чтобы пропуск утреннего запуска не оставил телефон совсем без напоминаний.

Прошедшие моменты пропускаются. После 19:40 скрипт не отправляет запоздалую вечернюю сводку, но обновляет завтрашние пары. Дата вечерней сводки приходит от сервера, без ручных вычислений UTC в JavaScript.

При успешной загрузке и проверке ответа удаляются только pending-уведомления с префиксом ceiti_, затем создаются новые. Изменение предмета заменяет текст, отмена пары удаляет её уведомление. Другие уведомления Scriptable не затрагиваются. Delivered-уведомления не удаляются. Этот префикс зарезервирован для одной установки проекта: несколько разных CEITI-скриптов на одном телефоне будут заменять друг друга. Не запускайте две автоматизации одновременно.

При сетевой ошибке существующие уведомления сохраняются. При stale=true используются последние успешно полученные данные с предупреждением в журнале. Это обеспечивает работу при сбое, но не гарантирует актуальность отмен. При ошибке iOS во время создания уведомлений возможен частично обновлённый набор — повторите запуск.

Для быстрой проверки разрешений можно создать отдельный временный Scriptable-скрипт:

const n = new Notification();
n.identifier = "ceiti_test";
n.title = "Проверка CEITI";
n.body = "Локальные уведомления работают";
n.setTriggerDate(new Date(Date.now() + 60000));
await n.schedule();
Script.complete();

Заблокируйте телефон на минуту. Для проверки замены запустите основной скрипт дважды: идентификаторы в await Notification.allPending() должны оставаться уникальными. Фактическую доставку на вашем iPhone проверьте этим шагом.

Автоматизация через «Команды» / Shortcuts

Сначала вручную запустите Scriptable и выдайте разрешения. Затем:

  1. Откройте «Команды» → создайте обычную команду Обновить CEITI.
  2. Добавьте действие, найдя Scriptable в списке приложений, затем Run Script / Запустить скрипт. Выберите скрипт CEITI. Не выбирайте похожее действие JavaScript для Safari.
  3. Если у действия есть параметр Run In App, отключите его для фонового выполнения. Запустите команду вручную и проверьте журнал Scriptable.
  4. «Автоматизация» / Automation → + → «Время суток» / Time of Day → 06:30 → ежедневно.
  5. Выберите Run Immediately / Немедленный запуск. На старых версиях интерфейса выключите Ask Before Running / Спрашивать до запуска.
  6. Выберите созданную команду Обновить CEITI; если предлагается пустая автоматизация, добавьте действие «Запустить команду» и выберите её. Альтернативно можно добавить действие Scriptable Run Script прямо в автоматизацию.
  7. Повторите для 07:30, 09:10, 11:00, 12:30, 14:00 и 19:30. Вечерний запуск должен быть раньше 19:40, потому что именно он планирует уведомление на 19:40.
  8. Проверьте хотя бы один автоматический запуск с заблокированным экраном. Названия зависят от языка iOS и версии Scriptable.

При частых заменах расписания добавьте отдельные Time of Day автоматизации каждые 30–60 минут в учебное время. У Shortcuts нет надёжного бесконечного фонового цикла: не используйте многочасовой Wait. Запуск в 06:30 и несколько раз днём надёжнее единственного запуска перед парой, поскольку уведомления планируются заранее.

Обновление Python-кэша не обновляет iPhone само по себе. Отмена, опубликованная после последнего запуска Scriptable, может остаться в уже запланированном уведомлении до следующего успешного запуска. iOS может задержать/пропустить автоматизацию или скрыть уведомление в Focus. Строгая доставка мгновенных изменений требует другого механизма, например собственного APNs-приложения; этот проект реализует именно локальные уведомления Scriptable.

Основание: документация Notification и автоматизации Apple. Notification поддерживает идентификаторы, выборочное удаление и будущие триггеры без постоянно работающего скрипта.

Размещение сервера

Этот экземпляр опубликован на Render Free; ниже инструкции для переноса или создания другого экземпляра. Сохраните .env отдельно от Git. В облаке задайте настройки через Environment Variables, включая обе WEEK_ANCHOR_*.

Render

Создайте Web Service из своего репозитория, Python 3, build pip install -r requirements.txt, start uvicorn main:app --host 0.0.0.0 --port $PORT --workers 1. Задайте health check /health. Подключите persistent disk /var/data, переменную DATABASE_PATH=/var/data/ceiti.sqlite3. Используйте постоянно работающий сервис, чтобы фоновое обновление не останавливалось. SQLite без постоянного диска теряется при пересоздании. В Scriptable укажите полученный HTTPS домен. См. FastAPI на Render и persistent disks.

Railway

Создайте проект из GitHub-репозитория с этим Dockerfile. Подключите Volume /data, задайте DATABASE_PATH=/data/ceiti.sqlite3 и WEEK_ANCHOR_*. Команда Dockerfile использует PORT платформы. В Networking создайте публичный домен, проверьте /health и /api/schedule/plan. Оставьте одну реплику и отключите сон сервиса, если он включён. См. FastAPI и Volumes.

VPS с Docker

Установите Docker, скопируйте проект на сервер и создайте .env с подтверждённой неделей. В каталоге проекта:

docker build -t ceiti-notifications .
docker volume create ceiti-data
docker run -d --name ceiti --restart unless-stopped --env-file .env -e DATABASE_PATH=/data/ceiti.sqlite3 -v ceiti-data:/data -p 127.0.0.1:8000:8000 ceiti-notifications
docker logs -f ceiti

Направьте DNS своего домена на VPS. Настройте HTTPS reverse proxy Caddy/Nginx к 127.0.0.1:8000, откройте 80/443. Пример Caddyfile после установки Caddy:

schedule.example.com {
    reverse_proxy 127.0.0.1:8000
}

Замените домен своим. Проверьте HTTPS с телефона. API содержит публичное расписание и не хранит пользовательские аккаунты; для широкого публичного использования ограничьте частоту запросов на reverse proxy. База и background worker рассчитаны на один экземпляр.

Диагностика

Логи Python идут в stdout: запросы CEITI, запись кэша, ошибки обновления и HTTP-запросы API. При недоступности источника выполняются 3 попытки с timeout, затем используется прежний снимок. После ошибки запросы в течение минуты используют кэш, чтобы не перегружать CEITI. Если кэша нет, API честно возвращает 503, а не пустое расписание. /health продолжает отвечать.

Структура: main.py — lifecycle и HTTP; config.py — настройки; database.py — SQLite; models/lesson.py — валидация; services/ceiti_client.py — реальный API; cache_service.py — обновление; schedule_service.py — календарь и фильтрация; api/routes.py — endpoints; scriptable/scriptable.js — iPhone.

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages