245 lines
28 KiB
Markdown
245 lines
28 KiB
Markdown
# 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` вычисляется сервером.
|
||
|
||
## Офлайн-режим
|
||
Типичный обход идёт там, где нет связи. Схема клиента:
|
||
|
||
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: <uuid>` —
|
||
повтор после обрыва связи не создаёт дубликат. Рекомендуется сначала отправить фото, затем `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>`). Парсите 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}` — календарь без сохранения (для конструктора).
|
||
|
||
## Отчёты (инженер/модератор/админ)
|
||
`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`).
|
||
|
||
## 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`.
|
||
Приложение проверяет её при запуске и предлагает скачать обновление, если на сервере версия новее.
|