Адрес: 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.
- Установите Python 3.11 или новее с python.org. На Windows включите добавление Python в PATH. Проверьте
python --version(илиpy --version). - Откройте терминал в каталоге проекта, где находится
main.py. - Создайте окружение:
python -m venv .venvWindows cmd:
.venv\Scripts\activateWindows PowerShell:
.\.venv\Scripts\Activate.ps1Linux/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.
Откройте 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. |
Это снимок для документации, не источник данных приложения.
- Установите Scriptable из App Store.
- Нажмите
+, назовите скриптCEITI, вставьте весь файлscriptable/scriptable.js. - В начале измените
serverUrlна HTTPS URL доступного сервера,groupиsubgroup(1 или 2). Настройки времени уведомлений находятся рядом в CONFIG. - Запустите кнопкой ▶. Разрешите сеть и уведомления. Посмотрите журнал: там будет количество запланированных уведомлений.
- В настройках уведомлений 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 проверьте этим шагом.
Сначала вручную запустите Scriptable и выдайте разрешения. Затем:
- Откройте «Команды» → создайте обычную команду
Обновить CEITI. - Добавьте действие, найдя Scriptable в списке приложений, затем Run Script / Запустить скрипт. Выберите скрипт
CEITI. Не выбирайте похожее действие JavaScript для Safari. - Если у действия есть параметр Run In App, отключите его для фонового выполнения. Запустите команду вручную и проверьте журнал Scriptable.
- «Автоматизация» / Automation →
+→ «Время суток» / Time of Day → 06:30 → ежедневно. - Выберите Run Immediately / Немедленный запуск. На старых версиях интерфейса выключите Ask Before Running / Спрашивать до запуска.
- Выберите созданную команду
Обновить CEITI; если предлагается пустая автоматизация, добавьте действие «Запустить команду» и выберите её. Альтернативно можно добавить действие Scriptable Run Script прямо в автоматизацию. - Повторите для 07:30, 09:10, 11:00, 12:30, 14:00 и 19:30. Вечерний запуск должен быть раньше 19:40, потому что именно он планирует уведомление на 19:40.
- Проверьте хотя бы один автоматический запуск с заблокированным экраном. Названия зависят от языка 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_*.
Создайте 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.
Создайте проект из GitHub-репозитория с этим Dockerfile. Подключите Volume /data, задайте DATABASE_PATH=/data/ceiti.sqlite3 и WEEK_ANCHOR_*. Команда Dockerfile использует PORT платформы. В Networking создайте публичный домен, проверьте /health и /api/schedule/plan. Оставьте одну реплику и отключите сон сервиса, если он включён. См. FastAPI и Volumes.
Установите 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.