Files
tech-service-portal/docs/API.md
T

28 KiB
Raw Blame History

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:
    {"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_…. Тело:
    {"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. Приложение проверяет её при запуске и предлагает скачать обновление, если на сервере версия новее.