- docker-compose.yml: профили docs (Nextcloud), office (ONLYOFFICE Document Server), ai (Ollama + скачивание модели) - автонастройка: адреса Nextcloud, ONLYOFFICE, портала и модели подставляются из .env или по именам сервисов во внутренней сети Docker; значения из интерфейса важнее - локальная модель в своей сети (имя сервиса, частный IP) подключается без ключа API - .env.example, docs/DEPLOY.md (раздел 7а), README
32 KiB
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_token>. 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 вычисляется сервером.
Офлайн-режим
Типичный обход идёт там, где нет связи. Схема клиента:
- Загрузка. При наличии сети:
GET /orders?include_items=true&status=new,in_progress— наряды с чек-листами в локальную БД. Справочники (/equipment,/locations) — по необходимости. Запомнить курсорmax(updated_at)по всем полученным нарядам. - Инкрементально.
GET /orders?include_items=true&updated_since=<курсор>— вернёт только изменённое (ответы, статусы, комментарии, фото двигаютupdated_at). Курсор — значениеupdated_atиз ответа сервера, а не часы телефона. - Работа без сети. Ответы копятся локально вместе с моментом ввода (
answered_at), фото — в очереди с сгенерированным UUID. - Отправка.
POST /orders/{id}/sync:Ответ:{"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: <uuid>— повтор после обрыва связи не создаёт дубликат. Рекомендуется сначала отправить фото, затемsyncсsubmit(для обязательных фото иначе наряд не сдастся).
Push (Android, FCM)
- Админ кладёт JSON-ключ сервисного аккаунта Firebase на сервер и указывает путь в «Настройки сервера».
- Приложение при входе и в
onNewTokenвызываетPOST /push/device{token, platform:"android"}; при выходе —DELETE /push/deviceс тем же телом. - Сообщения содержат
notification{title, body}иdata{url}(/#/order/<id>). Парсите 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_…. Тело:{"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.
Приложение проверяет её при запуске и предлагает скачать обновление, если на сервере версия новее.