# API для клиентов (Android) Базовый адрес: `https://<домен>/api/v1`. Интерактивная схема: `/docs`, машинная: `/openapi.json`. Старый путь `/api/...` (без версии) работает как алиас, но в новых клиентах используйте `/api/v1`. ## Общие правила - Формат — JSON, UTF-8. Ошибки: HTTP-код + `{"detail": "текст"}` (для 422 валидации `detail` — список `{loc, msg}`). - **Время** — ISO 8601 со смещением: `2026-10-07T11:25:00+03:00`. На вход принимается любое смещение или `Z`; без смещения время считается серверным локальным. - Коды: 401 — нет/просрочен токен, 403 — нет прав, 404 — не найдено *или не видно вам*, 409 — конфликт данных, 422 — неверные данные, 429 — слишком много попыток входа (см. `Retry-After`). ## Авторизация | Запрос | Назначение | |---|---| | `POST /auth/login` `{username, password, device?}` | → `{access_token, refresh_token, expires_in}` | | `POST /auth/refresh` `{refresh_token}` | новая пара токенов; **старый refresh становится недействительным** | | `POST /auth/logout` `{refresh_token}` | завершить сессию | | `POST /auth/change-password` `{old_password, new_password}` | смена пароля, остальные сессии завершаются, возвращает новую пару | | `GET /me` | профиль, должность (`position.kind`: technician/engineer), `on_shift`, `is_admin`, `is_moderator`, `demo_mode` (сессия в демо), `crane_map` (краны — отдельная служба) | Заголовок: `Authorization: Bearer `. Access живёт 60 мин (`ACCESS_TTL_MIN`), refresh — 90 дней. Рекомендуемая схема клиента: при 401 один раз вызвать `/auth/refresh` (сериализуя параллельные вызовы!) и повторить запрос; если и refresh вернул 401 — показать экран входа. refresh хранить в `EncryptedSharedPreferences`/Keystore. Повторное предъявление уже использованного refresh трактуется как кража и отзывает **все** сессии пользователя. После 5 неудачных входов с одного IP или на один логин вход блокируется на 5 минут. ## Наряды | Запрос | Назначение | |---|---| | `GET /orders` | список видимых нарядов. Параметры: `status=new,in_progress`, `mine`, `overdue`, `to_review`, `equipment_id`, `q` (поиск по названию наряда, оборудованию или номеру; без учёта регистра), `updated_since`, `include_items`, `limit` (≤1000), `offset` | | `GET /orders/{id}` | наряд с пунктами (`items[]`: тип ответа, варианты, границы, `photo`, `photos[]`, `value`) | | `POST /orders/{id}/start` | взять в работу | | `PUT /orders/{id}/items/{item_id}` `{value}` | ответ на пункт. status: `in_progress`/`done`; choice — значение из `options`; number — число (запятая допустима); пустое значение стирает ответ | | `POST /orders/{id}/items/{item_id}/photos` | тело — файл, `Content-Type: image/jpeg\|png\|webp`; опционально `Idempotency-Key` | | `GET /photos/{id}` | файл (нужен Bearer) | | `POST /orders/{id}/submit` | сдать наряд | | `POST /orders/{id}/sync` | пакетная отправка офлайн-данных (ниже) | | `GET/POST /orders/{id}/comments`, `DELETE /comments/{id}` | комментарии | | `POST /orders/{id}/review` `{approve, comment}` | проверка (флаг `can_review` в наряде) | Права вычислять не нужно: в наряде есть `can_execute` и `can_review`. Статусы: `new → in_progress → review → completed`, `cancelled`; `is_overdue` вычисляется сервером. ## Офлайн-режим Типичный обход идёт там, где нет связи. Схема клиента: 1. **Загрузка.** При наличии сети: `GET /orders?include_items=true&status=new,in_progress` — наряды с чек-листами в локальную БД. Справочники (`/equipment`, `/locations`) — по необходимости. Запомнить курсор `max(updated_at)` по всем полученным нарядам. 2. **Инкрементально.** `GET /orders?include_items=true&updated_since=<курсор>` — вернёт только изменённое (ответы, статусы, комментарии, фото двигают `updated_at`). Курсор — значение `updated_at` из ответа сервера, а не часы телефона. 3. **Работа без сети.** Ответы копятся локально вместе с моментом ввода (`answered_at`), фото — в очереди с сгенерированным UUID. 4. **Отправка.** `POST /orders/{id}/sync`: ```json {"items": [{"item_id": 12, "value": "done", "answered_at": "2026-10-07T12:00:00+03:00"}, {"item_id": 13, "value": "4,5", "answered_at": "2026-10-07T12:01:10+03:00"}], "submit": true} ``` Ответ: `results[]` по каждому пункту (`ok` / `stale` / `error` + `detail`), `submitted`, `submit_error`, актуальный `order`. Ответы применяются независимо: ошибка одного не блокирует остальные. `stale` — на сервере уже есть **более свежий** ответ (другой ввод позже по времени), значение не изменено. Пакет можно безопасно отправить повторно. Фото: `POST .../photos` с заголовком `Idempotency-Key: ` — повтор после обрыва связи не создаёт дубликат. Рекомендуется сначала отправить фото, затем `sync` с `submit` (для обязательных фото иначе наряд не сдастся). ## Push (Android, FCM) 1. Админ кладёт JSON-ключ сервисного аккаунта Firebase на сервер и указывает путь в «Настройки сервера». 2. Приложение при входе и в `onNewToken` вызывает `POST /push/device` `{token, platform:"android"}`; при выходе — `DELETE /push/device` с тем же телом. 3. Сообщения содержат `notification{title, body}` и `data{url}` (`/#/order/`). Парсите id из `url` либо открывайте `GET /orders?updated_since=...`. Уведомления: новый наряд, наряд на проверку, возврат на доработку, комментарий. ## Графики работы (смены) `GET/POST /schedules`, `PATCH/DELETE /schedules/{id}`. Три вида (`kind`): `weekly` (дни недели), `cycle` (N через M), `pattern` — конструктор: `pattern` = список шагов `{type: "work"|"off", days, start_time, end_time, label}`, повторяется по кругу от `cycle_start`. Конец смены раньше начала — ночная смена, равные время — сутки (08:00–08:00). Пример «3 дня, отсыпной, 3 ночи, отсыпной, 3 выходных» — 5 шагов, цикл 11 дней. `POST /schedules/preview` `{schedule, from_date, days}` — календарь без сохранения (для конструктора). График назначается **сотруднику**: `schedule_id` в `POST/PATCH /users` и в `UserOut` (пусто — всегда на смене). `on_shift` считается по графику сотрудника. Наряд на должность видят и получают уведомление только сотрудники этой должности, которые сейчас на смене. `schedule_id` у должности устарел и не используется (при обновлении перенесён сотрудникам). ## Отчёты (инженер/модератор/админ) `GET /reports` — список отчётов. `GET /reports/completed-orders?from=…&to=…` — **период обязателен** (дата и время). Фильтры: `equipment_id`, `position_id` (должность исполнителя **на момент выполнения**), `user_id`, `include_review`. `format=json|pdf|csv` (pdf — файл; csv — UTF-8 с BOM, разделитель `;`, открывается в Excel). `GET /reports/execution?from=…&to=…` — «Выполнение работ» по машинам. `GET /reports/people?from=…&to=…` — «По сотрудникам»: фильтры `department_id` (с вложенными), `position_id`, `user_id`; `format=json|pdf|csv|csv_calendar`. В JSON: `days`, `totals`, `rows: [{user_id, name, position, department, done, orders_done, tasks_done, tasks_total, tasks_open, on_time_pct, overdue, returns, deviations, tasks_cancelled, tasks_paused, hours, calendar: {"2026-10-12": {n, h}}}]`. Трудоёмкость задачи — `labor_hours` (норма), если задана, иначе фактическое время «в работе»; наряда — от начала до сдачи. ## Регламентное обслуживание (узлы машин, наработка) Чтение — любому авторизованному; изменение — инженер, модератор, администратор. | Метод | Путь | Описание | |---|---|---| | GET | `/equipment/{id}/service-units` | Узлы машины с расчётом: `state` (`ok`/`soon`/`due`/`nodata`), `percent`, `remaining_hours`, `remaining_days`, `due_date`, `forecast_date`, `rule` | | POST | `/equipment/{id}/service-units` | Создать узел: `name`, `mode` (`hours`/`calendar`/`both`), `interval_hours`, `interval_value` + `interval_unit` (`day`/`week`/`month`), `warn_percent`, начальное состояние `last_service_at`, `last_service_hours` | | PATCH | `/service-units/{id}` | Изменить правило, название, `is_active` | | DELETE | `/service-units/{id}` | Удалить узел (пункты старых нарядов остаются) | | POST | `/service-units/{id}/reset` | Сброс: `{at?, hours?, note}` — обслужено отдельно от наряда / при внештатной ситуации | | GET | `/service-units/{id}/events` | История обслуживания: `initial` / `order` / `reset` | | POST | `/equipment/{id}/meter` | Показание наработки `{hours, at?, correction?, note}`; меньше текущего — только с `correction: true` | | GET | `/equipment/{id}/meter` | История показаний | | GET | `/reports/equipment-state` | Отчёт «Текущее состояние»: `equipment_id`, `location_id`, `state` (`due`/`soon`/`attention`/`ok`), `include_inactive`, `format` (`json`/`pdf`/`csv`) | Чек-лист: поля `kind` (`inspection` | `regular`) и `only_due`. Для `regular` наряд требует `equipment_id`; в наряде появляются пункты с `is_meter: true` (число, м/ч) и `service_unit_id` (статус `done` = узел обслужен). При завершении наряда сервер обновляет наработку и сбрасывает узлы. ## Инциденты и сообщения о неисправности Одна сущность с полем `kind`: `incident` — авария, оборудование остановлено (машина в состоянии `alarm`); `fault` — сообщение о неисправности: замечена проблема, но оборудование работает (состояние `fault`). Замена узлов (`replaced`) у `fault` не применяется. - `POST /api/v1/incidents` `{kind, equipment_id, title, description?, occurred_at?, units: [{unit_id, replaced}], replaced?, replacement_note?}`. - `GET /api/v1/incidents?kind=incident|fault&status=open|closed&mine&source&cranes&q&equipment_id`. - `POST /api/v1/incidents/{id}/close` — закрыть (автор или инженер). Тело (всё, кроме `note`, необязательно; для отказа незаполненное подставляется: начало работ — момент отказа, ввод — сейчас, наработка — снимок при регистрации, причина — `unknown`): `{note, repair_started_at, restored_at, parts_wait_hours, hours_at_failure, meter_at_restore, cause_unit_id | cause_unit_other, action: repair|replace|adjust|restart, nature: sudden|gradual, cause: wear|operation|external|power|defect|secondary|unknown, precursor: inspection|fault_report|none, replaced_units: [unit_id]}`. Проверка хронологии: отказ ≤ начало работ ≤ ввод ≤ сейчас. `meter_at_restore` записывается показанием счётчика; `replaced_units` сбрасывают ресурс узлов. - `PATCH /api/v1/incidents/{id}/failure` — те же поля: инженер или руководитель уточняет данные отказа (и у закрытой записи). - При регистрации сохраняется `hours_at_open` (наработка машины); при переводе неисправности в отказ — `escalated_at`, `hours_at_failure` и `precursor: fault_report`. В интерфейсе сайта `kind=incident` называется «Отказ». - `POST /api/v1/incidents/{id}/escalate` `{note}` — перевести сообщение о неисправности в инцидент (оборудование остановилось). - `GET /api/v1/equipment-health` → `{id: {state: alarm|fault|due|soon|ok, open_incidents, open_faults, due, soon}}`; приоритет состояния: авария > неисправность > требуется ТО > скоро ТО. То же состояние — в `GET /map`. ## Анализ надёжности (инженер, руководитель; настройка `reliability`) - `GET /api/v1/reliability?from&to&department_id&cranes&equipment_id&confidence=0.9&horizon_days=30` — показатели за период: `fleet` (по парку) и `rows` по машинам: `hours` (наработка; `basis: meter|calendar`), `failures`, `faults`, `escalated`, `lambda`, `lambda_upper`, `theta`, `theta_lower`, `theta_median`, `mttr`, `repair_mean`, `wait_mean`, `downtime`, `availability`, `forecast_mu`, `forecast_p`; разбивки `causes`, `natures`, `actions`, `precursors`, `cause_units`, `months`. Оценки по малоотказному опыту: λ_верх = χ²(Pα; 2n+2) / 2T (при n = 0 — −ln(1−Pα)/T). - `GET /api/v1/reliability/drift?days=180&horizon=90` — числовые пункты чек-листов с нормой: тренд, `days_to_bound`, `out_of_range`, `nu_per_30d`. - `GET /api/v1/reliability/export?kind=failures|service&from&to` — CSV (UTF-8 с BOM, `;`): отказы и неисправности / история обслуживания узлов. - `GET /api/v1/reliability/dictionaries` — справочники причин, характера, действий, признаков. В `GET /me` — флаг `reliability`. ## Инциденты извне (внешние системы и публичная форма) Внешняя система (диспетчерская, SCADA, сервис заявок) заводит инцидент по **ключу доступа**. Ключ выпускает администратор: «Администрирование → Настройки сервера → Ключи доступа к API» (ключ показывается один раз; в БД хранится только хэш; отзыв — там же). - `GET /api/v1/external/equipment` — справочник машин: `[{id, name, inventory_no, location}]`. - `POST /api/v1/external/incidents` — завести инцидент. Заголовок `X-API-Key: cl_…`. Тело: ```json {"inventory_no": "НС-402", "title": "Перегрев двигателя", "description": "t обмотки 135 °C", "occurred_at": "2026-10-09T11:20:00+03:00", "reporter_name": "SCADA цех 1", "reporter_contact": "+7 …", "external_id": "ALARM-1234", "kind": "incident"} ``` `kind`: `incident` (по умолчанию) или `fault` — сообщение о неисправности. Машина — `equipment_id` (id портала) или `inventory_no`. Повторный запрос с тем же `external_id` от того же ключа вернёт уже созданный инцидент (без дубликата). Ответ — инцидент (`source: "api"`). Ошибки: 401 — нет/неверный/отозванный ключ, 404 — машина не найдена, 422 — неверные данные. Публичная форма **`/report`** (без входа) использует `GET /api/v1/public/report-options` (места и машины) и `POST /api/v1/public/incidents` (`location_id`, `equipment_id`, `description`, `reporter_name`, `reporter_contact` — обязательны, `kind` — `fault` «работает, но есть неисправность» или `incident` «остановлено, авария»; машина должна стоять в выбранном месте или вложенном в него). Не более 5 отправок с адреса за 10 минут. Форму выключает администратор (настройка `public_incidents`). ## Задачи и поручения (бывшие «суточные задания») `GET /api/v1/tasks` — фильтры `day`, `from`, `to`, `assignee_id`, `equipment_id`, `status`, `mine` (я исполнитель), `created_by_me` (поставил я), `active`. `POST/PATCH /api/v1/tasks` — поле `labor_hours` (трудоёмкость-норма, ч, необязательно; 0–1000). `POST /api/v1/tasks/{id}/transfer` `{"assignee_id": 7, "comment": "причина"}` — исполнитель, поставивший задание или инженер; незавершённое задание у нового исполнителя становится «назначен»; в истории — событие `kind: "transfer"`. ## Оборудование: отделы и краны У оборудования поля `department_id` (за каким отделом закреплено) и `is_crane` (кран). Сотрудник с отделом видит оборудование своего отдела и вложенных, а также без отдела; администратор, модератор и сотрудник без отдела — всё. Это касается `GET /equipment`, `GET /incidents`, `GET /equipment-health`, `GET /map`, отчёта о состоянии узлов и уведомлений об инцидентах. При удалении отдела его оборудование переходит к вышестоящему отделу. Краны — отдельная служба: настройка `crane_map` (`PUT /api/v1/settings {"crane_map": true}`, в `GET /me` — `crane_map`). Тогда `GET /map` (`kind=plant`) показывает оборудование без кранов, `GET /map?kind=crane` — только краны (при выключенной настройке — 404); картинка карты кранов: `GET/PUT /map/image?kind=crane` (если своей нет — отдаётся план завода; в ответе `/map` — `own_image`). `GET /incidents?cranes=true|false` — только краны / всё, кроме кранов. Также у инцидентов фильтры `mine`, `source=staff|public|api`, `q`. ## История машины `GET /api/v1/equipment/{id}/history?types=incident,service,meter,order,task&from=2026-10-01&to=2026-10-09&q=текст` — хронология (новые сверху): `{total, counts, rows: [{at, type, type_ru, title, text, user_name, order_id?, task_id?, incident_id?}]}`. ## Отделы `GET/POST /api/v1/departments`, `GET/PATCH/DELETE /api/v1/departments/{id}` — `{name, parent_id, head_user_id, notes}`, в ответе `members` (число активных сотрудников). Править — модератор/админ. У пользователя поле `department_id` (в ответе ещё `department: {id, name}`). При удалении отдела его сотрудники остаются без отдела, вложенные отделы поднимаются на уровень выше. ## Резервные копии (администратор) - `GET /api/v1/backups` — список копий и настройки автокопирования (`auto_hours`, `keep`, `media`, `next_auto`). - `POST /api/v1/backups` `{"include_media": false, "note": "перед обновлением"}` — создать копию. - `GET /api/v1/backups/{name}` — скачать zip; `DELETE /api/v1/backups/{name}` — удалить. - `POST /api/v1/backups/upload` (тело — zip) — загрузить копию, например с другого сервера. - `POST /api/v1/backups/{name}/restore` `{"confirm": true}` — заменить ВСЕ данные продуктовой базы содержимым копии (текущее состояние сначала сохраняется копией `…_before-restore.zip`; после восстановления все входят заново). Состав zip: `manifest.json`, `db/<таблица>.json` (переносимый формат — PostgreSQL ↔ SQLite), `config/environment.json` (настройки сервера и несекретные переменные окружения; секреты не сохраняются — только их названия), `media/…` — фото и картинки, если выбрано. Автокопии: настройки `backup_auto_hours` (0 — выключено), `backup_keep`, `backup_media` в `PUT /api/v1/settings`. Копии лежат в `DATA_DIR/backups` (в Docker — том `appdata`). В демо-режиме копирование и восстановление недоступны. ## Демонстрационный режим (личный) Демо включается для конкретного пользователя: его запросы идут в отдельную тестовую базу, остальные работают с рабочей. Тип базы определяется токеном: после включения/выключения клиент получает новую пару токенов (или обновляет её через `/auth/refresh`) и должен очистить локальные данные, если `GET /me` → `demo_mode` изменился. - `GET /demo/status` (без входа) → `{active}` — есть ли демо-данные (для подсказки на экране входа). - `GET /demo/me` → `{in_demo, can_leave}`; `POST /demo/leave` — выйти из демо (возвращает токены рабочей базы). - Администратор: `GET /demo` — кто в демо и тестовые учётные записи; `POST /demo/enable {"confirm": true}` — войти самому (токены); `POST /demo/users/{id} {"on": true|false}` — включить/выключить демо другому сотруднику; `POST /demo/reset` — пересоздать тестовые данные; `POST /demo/destroy` — удалить их (все возвращаются к рабочим данным); `POST /demo/impersonate {"user_id"}` — войти под тестовым пользователем (только из демо-сессии). ## Документы `/guide.pdf` — руководство пользователя, `/presentation.pdf` и `/presentation.pptx` — презентация, `/app.apk` — приложение для Android, `/report` — публичная форма заявки (все без входа). ## Обратная связь и администраторы портала - `POST /api/v1/feedback` `{kind: idea|bug|other, text, page?, client: web|m|android}` — любой сотрудник (не больше 20 в час). - `GET /api/v1/feedback?status=open|new|taken|done|rejected`, `PATCH /api/v1/feedback/{id}` `{status?, answer?}`, `DELETE /api/v1/feedback/{id}` — только **администраторы портала**. - Администратор портала: `is_admin` и (`portal_admin` или логин `ADMIN_USERNAME`, обычно admin). В `GET /me` — `portal_admin`, `is_root`. Флаг `portal_admin` у пользователя меняет только admin; admin нельзя лишить прав администратора или отключить. - Только администраторам портала доступны `PUT /settings` (кроме отметок мастера `setup_done`), полный `GET /settings`, `/api-keys`, `/assistant/config` и `/assistant/test`. Остальные администраторы получают в `GET /settings` сокращённый ответ (`limited: true`). ## Документация оборудования (Nextcloud) Портал читает Nextcloud по WebDAV от служебной учётной записи (пароль приложения, только чтение) и сам отдаёт файлы; Office переводится в PDF через ONLYOFFICE Document Server (ConvertService). - `GET/PUT /api/v1/docs/config` — **администратор портала**: `nc_url, nc_user, nc_password, nc_root, onlyoffice_url, onlyoffice_jwt, portal_internal_url, docs_ai`. Пароль и секрет JWT в ответе не возвращаются (`nc_password_set`, `onlyoffice_jwt_set`) и не попадают в резервные копии. `POST /api/v1/docs/test` — проверка Nextcloud и ONLYOFFICE. - `GET /api/v1/docs/browse?path=` — администратор или модератор: вложенные папки (с `linked` — к каким машинам привязаны) и число файлов. - `PUT /api/v1/equipment/{id}/docs` `{path}` — администратор или модератор: привязать папку (пустой `path` — отвязать). В `EquipmentOut` — `docs_path`. - `GET /api/v1/equipment/{id}/docs?sub=` — кто видит машину: `{folder, sub, items: [{name, path, is_dir, kind: dir|pdf|image|text|office|file, size, mtime}]}`. - `GET /api/v1/equipment/{id}/docs/file?path=...&view=1` — файл; `view=1` — для просмотра: PDF, картинки, текст — inline, Office — PDF через ONLYOFFICE (кэшируется). Без `view` — скачивание оригинала. Пути `..` отклоняются (422). - `GET /api/v1/docs/raw/{token}` — без входа, только для ONLYOFFICE: подписанная ссылка на исходник, живёт 10 минут. - В `GET /me` — `docs: true`, если Nextcloud подключён. Опция AI-помощника **«Работа с документацией»** (`docs_ai`): раз в сутки портал строит индекс (имя и начало текста файлов, PDF — через pypdf, docx/xlsx/pptx/odt — из XML; файлы больше 60 МБ — только по имени), `POST /api/v1/docs/reindex` — вручную (администратор портала). `GET /api/v1/docs/search?q=` — поиск по индексу среди машин, которые видит пользователь (только при включённой опции). Помощник получает инструменты `search_docs` и `equipment_docs`. Фрагменты документов уходят провайдеру ИИ — для закрытой документации нужна локальная модель. ## Фото: «хранить более года» и автоочистка - `photo_keep` у пункта чек-листа (`ChecklistItemIn`), копируется в пункт наряда (`OrderItemOut.photo_keep`); у задания — `photo_keep` в `POST/PATCH /tasks` и `TaskOut`. Такие фото сохраняются в каталог `keep/` и очисткой не удаляются. - Настройка `photo_keep_days` (`PUT /settings`, администратор портала): раз в сутки удалять фото нарядов и заданий старше N дней, 0 — выключено (по умолчанию). `GET /photo-cleanup` — сколько удалится сейчас (ничего не удаляет), `POST /photo-cleanup` — очистить сразу. В `GET /settings` — `photo_keep_stats` (сколько фото в `keep/`). ## Мониторинг сервера `GET /api/v1/server/monitor` (администратор портала) → `{cpu: %, cpu_count, load: [1, 5, 15 мин] | null, memory: {used, total, scope: host|container}, disks: [{labels, path, used, total, free}], now}`. ## AI-помощник Включает администратор: «Администрирование → Настройки сервера → AI-помощник» (`GET/PUT /api/v1/assistant/config`: `enabled, provider (openai | anthropic | gigachat), base_url, model, api_key, scope`; ключ в ответах не возвращается и не попадает в резервные копии). Проверка: `POST /api/v1/assistant/test`. - `GET /api/v1/assistant/status` → `{enabled}` — показывать ли кнопку помощника. - `POST /api/v1/assistant/chat` `{"messages": [{"role": "user", "content": "Как зафиксировать аварию?"}]}` → `{reply, offtopic, tools}`. Помощник отвечает только о работе в портале (иначе — вежливый отказ, `offtopic: true`), знает роль пользователя и читает данные только через этот же API от имени пользователя (его задания, наряды, инструмент, оборудование, инциденты) — ничего не меняет. Ограничения: вопрос до 1000 символов, история — последние 10 сообщений, не более 30 вопросов в час на пользователя. OpenAI-совместимый провайдер подходит для OpenAI, YandexGPT (`https://llm.api.cloud.yandex.net/v1`), DeepSeek, OpenRouter и локальных моделей (Ollama: `http://localhost:11434/v1`, ключ не нужен). ## Версия Android-приложения `GET /api/v1/app/version` (без входа) → `{available, version, size, url}` — версия берётся из имени файла `checklists-<версия>.apk`. Приложение проверяет её при запуске и предлагает скачать обновление, если на сервере версия новее.