openapi: 3.0.3

info:
  title: PM Backend API
  version: "1.0.0"
  description: |
    REST API сервиса **pm-backend** (`planning/pm-backend`) — управление
    проектами и папками, задачами (иерархия, связи, ресурсы), базовыми планами
    (состояниями), атрибутами, календарями, настройками КСГ, детализацией задач,
    системным журналом и экспортом.

    Сервис написан на Python (**Django 5.1 + Django REST Framework**) и запускается
    как ASGI-приложение (`config.asgi_root:application`) через gunicorn с воркерами
    `uvicorn.workers.UvicornWorker` (порт `8000`). Роутинг задан в `config/urls.py`
    и состоит из трёх групп:

    - публичный API — префикс `/api/pm/msp/` (`sarex.pm.urls`);
    - внешний API v2 — префикс `/api/pm/external/v2/` (`sarex.pm.urls_external`),
      только чтение;
    - внутренний API — префикс `/internal/pm/` (`sarex.pm.urls_internal`),
      предназначен для вызовов внутри кластера, аутентификация не требуется.

    ### Аутентификация
    Публичные (`/api/pm/msp/*`) и внешние (`/api/pm/external/v2/*`) эндпоинты
    требуют аутентификации (DRF `IsAuthenticated`). Проверка выполняется по цепочке
    (`REST_FRAMEWORK.DEFAULT_AUTHENTICATION_CLASSES`):

    1. **Zitadel** (`ZitadelJWTAuthentication`) — требуются одновременно заголовки
       `Authorization: Bearer <jwt>` и `Identity: Identity <jwt>`. При отсутствии
       любого из них — переход к следующему механизму/ошибка.
    2. **sarex-backend** (`JWTAuthentication`) — подпись токена `Authorization:
       Bearer <jwt>` проверяется публичным RSA-ключом (`AUTH_PUBLIC_KEY`,
       алгоритм `RS512`). При `SERVER_DEBUG=true` аутентификация обходится
       (возвращается анонимный пользователь).

    Внутренние эндпоинты (`/internal/pm/*`) имеют `authentication_classes=[]` и
    `AllowAny` — доступ ограничивается сетевым слоем. Отдельный публичный эндпоинт
    `/api/pm/msp/external-task-info/` также открыт (`AllowAny`).

    ### Пагинация
    По умолчанию используется DRF `LimitOffsetPagination` (`PAGE_SIZE=1000`).
    Списочные ответы оборачиваются в объект `{ count, next, previous, results }`;
    размер страницы задаётся query-параметром `limit`, смещение — `offset`. Часть
    «тяжёлых» списков (задачи, привязки ресурсов, связи задач) использует
    `ProjectTaskPagination` (`default_limit=30000`).

    ### Обработка ошибок
    Ошибки возвращаются в стандартном формате DRF: ошибки валидации — `400`
    (`{ "<field>": ["<msg>"] }` либо `{ "detail": "..." }`), нет прав — `403`
    (`{ "detail": "..." }`), не аутентифицирован — `401`, не найдено — `404`.
    Объектные права проверяются `PermissionMixin` (raw-SQL), суперпользователь
    их обходит.

    ### Замечания (расхождения кода)
    - Внешние вьюсеты v2 объявляют `http_method_names = ['get']`, поэтому доступны
      только `list`/`retrieve` (и GET-`@action`), даже если в коде есть методы
      записи.
    - Ряд bulk-эндпоинтов принимает JSON-массив (list-сериализатор).
    - Datetime-поля (`DateTimeWithoutTZFiled`) наивные — таймзона отбрасывается.
    - Внутренние эндпоинты принимают/возвращают «сырые» словари, без модельных
      сериализаторов.

  contact:
    name: pm-backend
    url: https://gitlab/planning/pm-backend

servers:
  - url: https://api.sarex.io
    description: Production (ingress)
  - url: https://stage-api.sarex.io
    description: Stage (ingress)
  - url: http://pm-backend-service.planning.svc.cluster.local:8000
    description: Внутрикластерный адрес (ClusterIP, порт 8000) — единственный способ достучаться до /internal/pm
  - url: http://localhost:8000
    description: Локальный запуск (gunicorn/uvicorn, порт 8000)

tags:
  - name: projects
    description: Проекты и папки — CRUD, копирование, экспорт, права, шаблоны
  - name: tasks
    description: Задачи проекта — CRUD, массовые операции, права, описания
  - name: resources
    description: Ресурсы и их привязки к задачам и элементам
  - name: states
    description: Состояния проекта (базовые планы)
  - name: relations
    description: Связи задач и проектов, атрибутивные связи
  - name: attributes
    description: Атрибуты проекта и значения атрибутов задач
  - name: calendars
    description: Календари
  - name: settings
    description: Настройки/состояния КСГ
  - name: detailing
    description: Детализация задач, визуальные профили
  - name: comments
    description: Комментарии к задачам
  - name: system-log
    description: Системный журнал и откаты
  - name: external
    description: Внешний API v2 (только чтение)
  - name: internal
    description: Внутренние эндпоинты (только внутри кластера, без аутентификации)

security:
  - bearerAuth: []

paths:
  # ==========================================================================
  # Projects (/api/pm/msp/projects)
  # ==========================================================================
  /api/pm/msp/projects/:
    get:
      tags: [projects]
      summary: Список проектов и папок
      operationId: listProjects
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Offset'
        - { name: search, in: query, schema: { type: string } }
        - { name: ids, in: query, description: CSV идентификаторов, schema: { type: string } }
        - { name: resource_id, in: query, schema: { type: string } }
        - { name: company_id, in: query, schema: { type: integer } }
        - { name: parent_id, in: query, schema: { type: integer } }
        - { name: only_children, in: query, schema: { type: boolean } }
        - { name: templates, in: query, schema: { type: boolean } }
        - { name: is_folder, in: query, schema: { type: boolean } }
        - { name: strict, in: query, schema: { type: boolean } }
        - { name: extend, in: query, schema: { type: boolean } }
        - { name: schema, in: query, description: "напр. tiny/detail", schema: { type: string } }
      responses:
        '200':
          description: Страница проектов
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/PaginatedResponse'
                  - type: object
                    properties:
                      results:
                        type: array
                        items: { $ref: '#/components/schemas/Project' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
    post:
      tags: [projects]
      summary: Создать проект/папку
      operationId: createProject
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ProjectCreate' }
      responses:
        '201':
          description: Проект создан
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Project' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /api/pm/msp/projects/{id}/:
    parameters:
      - $ref: '#/components/parameters/PathId'
    get:
      tags: [projects]
      summary: Проект по id
      operationId: retrieveProject
      parameters:
        - { name: extend, in: query, schema: { type: boolean } }
        - { name: schema, in: query, schema: { type: string } }
      responses:
        '200':
          description: Проект
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Project' }
        '404': { $ref: '#/components/responses/NotFound' }
    put:
      tags: [projects]
      summary: Полное обновление проекта
      operationId: updateProject
      requestBody:
        required: true
        content: { application/json: { schema: { $ref: '#/components/schemas/ProjectCreate' } } }
      responses:
        '200': { description: Обновлено, content: { application/json: { schema: { $ref: '#/components/schemas/Project' } } } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '404': { $ref: '#/components/responses/NotFound' }
    patch:
      tags: [projects]
      summary: Частичное обновление проекта
      operationId: partialUpdateProject
      requestBody:
        required: true
        content: { application/json: { schema: { $ref: '#/components/schemas/ProjectCreate' } } }
      responses:
        '200': { description: Обновлено, content: { application/json: { schema: { $ref: '#/components/schemas/Project' } } } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '404': { $ref: '#/components/responses/NotFound' }
    delete:
      tags: [projects]
      summary: Удалить проект
      operationId: deleteProject
      responses:
        '204': { description: Удалено }
        '404': { $ref: '#/components/responses/NotFound' }

  /api/pm/msp/projects/root_folder/:
    post:
      tags: [projects]
      summary: Создать корневую папку
      operationId: createRootFolder
      requestBody:
        required: true
        content: { application/json: { schema: { $ref: '#/components/schemas/FolderCreate' } } }
      responses:
        '201': { description: Папка создана, content: { application/json: { schema: { $ref: '#/components/schemas/Project' } } } }
        '400': { $ref: '#/components/responses/BadRequest' }

  /api/pm/msp/projects/rename/:
    post:
      tags: [projects]
      summary: Переименовать проект
      operationId: renameProject
      responses:
        '200': { description: OK }
        '400': { $ref: '#/components/responses/BadRequest' }

  /api/pm/msp/projects/import/:
    post:
      tags: [projects]
      summary: Импорт задач в проект
      operationId: importProject
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                pk: { type: integer }
                from_type: { type: string, enum: [sarex, ms_project, primavera, template] }
                update: { type: boolean }
                template_id: { type: integer, nullable: true }
              required: [pk, from_type]
      responses:
        '201': { description: Импорт запущен, content: { application/json: { schema: { type: object, properties: { uuid: { type: string, format: uuid } } } } } }
        '404': { $ref: '#/components/responses/NotFound' }

  /api/pm/msp/projects/import-from-gasprom/:
    post:
      tags: [projects]
      summary: Импорт проекта из Газпром ЦПС
      operationId: importProjectFromGaspromCps
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                project_name: { type: string }
                company_id: { type: integer }
                file_url: { type: string }
              required: [project_name, company_id, file_url]
      responses:
        '201': { description: Импорт запущен, content: { application/json: { schema: { type: object, properties: { uuid: { type: string, format: uuid } } } } } }
        '400': { $ref: '#/components/responses/BadRequest' }

  /api/pm/msp/projects/{id}/states/:
    parameters: [ { $ref: '#/components/parameters/PathId' } ]
    get:
      tags: [projects]
      summary: Состояния (базовые планы) проекта
      operationId: listProjectStates
      responses:
        '200': { description: Список состояний, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/ProjectState' } } } } }

  /api/pm/msp/projects/{id}/permissions/:
    parameters: [ { $ref: '#/components/parameters/PathId' } ]
    get:
      tags: [projects]
      summary: Права проекта
      operationId: getProjectPermissions
      responses:
        '200': { description: Права, content: { application/json: { schema: { $ref: '#/components/schemas/PermissionsResponse' } } } }
    post:
      tags: [projects]
      summary: Сохранить права проекта
      operationId: saveProjectPermissions
      requestBody: { required: true, content: { application/json: { schema: { type: object } } } }
      responses:
        '200': { description: Права, content: { application/json: { schema: { $ref: '#/components/schemas/PermissionsResponse' } } } }
        '400': { $ref: '#/components/responses/BadRequest' }

  /api/pm/msp/projects/{id}/all_permissions/:
    parameters: [ { $ref: '#/components/parameters/PathId' } ]
    get:
      tags: [projects]
      summary: Все права проекта
      operationId: getProjectAllPermissions
      responses:
        '200': { description: Права, content: { application/json: { schema: { type: object } } } }

  /api/pm/msp/projects/{id}/copy_project/:
    parameters: [ { $ref: '#/components/parameters/PathId' } ]
    post:
      tags: [projects]
      summary: Копировать проект
      operationId: copyProject
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                parent_id: { type: integer, nullable: true }
                project_name: { type: string }
                assets_mapping: { type: object }
                with_resources: { type: boolean }
      responses:
        '201': { description: Проект скопирован, content: { application/json: { schema: { $ref: '#/components/schemas/Project' } } } }
        '400': { $ref: '#/components/responses/BadRequest' }

  /api/pm/msp/projects/{id}/create_template/:
    parameters: [ { $ref: '#/components/parameters/PathId' } ]
    post:
      tags: [projects]
      summary: Создать шаблон из проекта
      operationId: createTemplateFromProject
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                project_name: { type: string }
                description: { type: string, nullable: true }
              required: [project_name]
      responses:
        '201': { description: Шаблон создан, content: { application/json: { schema: { $ref: '#/components/schemas/Project' } } } }

  /api/pm/msp/projects/{id}/export_project_to_pdf/:
    parameters: [ { $ref: '#/components/parameters/PathId' } ]
    post:
      tags: [projects]
      summary: Экспорт проекта в PDF
      operationId: exportProjectToPdf
      requestBody:
        required: true
        content: { application/json: { schema: { $ref: '#/components/schemas/ExportProject' } } }
      responses:
        '200': { description: Задача экспорта запущена }
        '400': { $ref: '#/components/responses/BadRequest' }

  /api/pm/msp/projects/{id}/project_export/:
    parameters: [ { $ref: '#/components/parameters/PathId' } ]
    post:
      tags: [projects]
      summary: Экспорт проекта (xlsx/xml)
      operationId: projectExport
      responses:
        '200': { description: Экспорт запущен }

  /api/pm/msp/projects/{id}/key_milestones/:
    parameters: [ { $ref: '#/components/parameters/PathId' } ]
    get:
      tags: [projects]
      summary: Ключевые вехи проекта
      operationId: getKeyMilestones
      responses:
        '200': { description: Вехи, content: { application/json: { schema: { type: array, items: { type: object } } } } }

  /api/pm/msp/projects/{id}/profiles/:
    parameters: [ { $ref: '#/components/parameters/PathId' } ]
    get:
      tags: [projects]
      summary: Визуальные профили проекта
      operationId: getProjectProfiles
      responses:
        '200': { description: Профили, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/VisualProfile' } } } } }

  /api/pm/msp/projects/{id}/time_markers_data/:
    parameters: [ { $ref: '#/components/parameters/PathId' } ]
    get:
      tags: [projects]
      summary: Данные временных маркеров
      operationId: getTimeMarkersData
      parameters:
        - { name: attributes, in: query, description: CSV идентификаторов атрибутов, schema: { type: string } }
        - { name: with_hierarchy, in: query, schema: { type: boolean } }
      responses:
        '200': { description: Данные, content: { application/json: { schema: { type: object } } } }

  /api/pm/msp/projects/{id}/relations/:
    parameters: [ { $ref: '#/components/parameters/PathId' } ]
    get:
      tags: [projects]
      summary: Связи/интеграции проекта
      operationId: getProjectRelations
      responses:
        '200': { description: Связи, content: { application/json: { schema: { type: array, items: { type: object } } } } }

  /api/pm/msp/projects/{id}/bulk_integration/:
    parameters: [ { $ref: '#/components/parameters/PathId' } ]
    post:
      tags: [projects]
      summary: Массовое создание интеграций
      operationId: bulkIntegration
      responses:
        '200': { description: OK }

  /api/pm/msp/projects/{id}/issues_data/:
    parameters: [ { $ref: '#/components/parameters/PathId' } ]
    get:
      tags: [projects]
      summary: Данные проблем проекта
      operationId: getIssuesData
      responses:
        '200': { description: Данные, content: { application/json: { schema: { type: object } } } }

  /api/pm/msp/projects/{id}/formula/:
    parameters: [ { $ref: '#/components/parameters/PathId' } ]
    post:
      tags: [projects]
      summary: Пересчёт по формуле
      operationId: applyFormula
      responses:
        '200': { description: OK }
        '400': { $ref: '#/components/responses/BadRequest' }

  /api/pm/msp/projects/{id}/rule/:
    parameters: [ { $ref: '#/components/parameters/PathId' } ]
    get:
      tags: [detailing]
      summary: Правило детализации проекта
      operationId: getProjectRule
      responses:
        '200': { description: Правило, content: { application/json: { schema: { type: object } } } }
    post:
      tags: [detailing]
      summary: Создать правило детализации
      operationId: createProjectRule
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                attributes: { type: array, items: { type: integer } }
                summary_fields: { type: array, items: { type: string } }
                mode: { type: string }
      responses:
        '200': { description: OK }
        '400': { $ref: '#/components/responses/BadRequest' }

  /api/pm/msp/projects/{id}/tasks/:
    parameters: [ { $ref: '#/components/parameters/PathId' } ]
    get:
      tags: [projects]
      summary: Задачи проекта
      operationId: getProjectTasks
      responses:
        '200': { description: Задачи, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/ProjectTask' } } } } }

  /api/pm/msp/projects/{id}/create_tasks/:
    parameters: [ { $ref: '#/components/parameters/PathId' } ]
    post:
      tags: [tasks]
      summary: Массовое создание задач
      operationId: bulkCreateTasks
      requestBody:
        required: true
        content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/ProjectTaskCreate' } } } }
      responses:
        '201': { description: Создано, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/ProjectTask' } } } } }
        '400': { $ref: '#/components/responses/BadRequest' }

  /api/pm/msp/projects/{id}/update_tasks/:
    parameters: [ { $ref: '#/components/parameters/PathId' } ]
    patch:
      tags: [tasks]
      summary: Массовое обновление задач
      operationId: bulkUpdateTasks
      requestBody:
        required: true
        content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/ProjectTaskUpdate' } } } }
      responses:
        '200': { description: Обновлено }
        '400': { $ref: '#/components/responses/BadRequest' }

  /api/pm/msp/projects/{id}/delete_tasks/:
    parameters: [ { $ref: '#/components/parameters/PathId' } ]
    delete:
      tags: [tasks]
      summary: Массовое удаление задач
      operationId: bulkDeleteTasks
      requestBody:
        required: true
        content: { application/json: { schema: { type: object, properties: { ids: { type: array, items: { type: integer } } } } } }
      responses:
        '204': { description: Удалено }

  /api/pm/msp/projects/{id}/system-logs/:
    parameters: [ { $ref: '#/components/parameters/PathId' } ]
    get:
      tags: [system-log]
      summary: Системные логи проекта
      operationId: getSystemLogs
      parameters:
        - { name: task_id, in: query, schema: { type: integer } }
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Offset'
      responses:
        '200':
          description: Страница логов
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/PaginatedResponse'
                  - type: object
                    properties: { results: { type: array, items: { $ref: '#/components/schemas/SystemLog' } } }

  /api/pm/msp/projects/{id}/system-log-detail/:
    parameters: [ { $ref: '#/components/parameters/PathId' } ]
    get:
      tags: [system-log]
      summary: Деталь записи журнала
      operationId: getSystemLogDetail
      parameters:
        - { name: log_id, in: query, required: true, schema: { type: integer } }
      responses:
        '200': { description: Деталь, content: { application/json: { schema: { $ref: '#/components/schemas/SystemLog' } } } }

  /api/pm/msp/projects/{id}/rollback-to-record/:
    parameters: [ { $ref: '#/components/parameters/PathId' } ]
    post:
      tags: [system-log]
      summary: Откат к записи журнала
      operationId: rollbackToRecord
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                log_id: { type: integer }
                read_only: { type: boolean }
              required: [log_id]
      responses:
        '200': { description: OK }
        '400': { $ref: '#/components/responses/BadRequest' }

  # ==========================================================================
  # Tasks (/api/pm/msp/tasks)
  # ==========================================================================
  /api/pm/msp/tasks/:
    get:
      tags: [tasks]
      summary: Список задач
      operationId: listTasks
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Offset'
        - { name: project, in: query, schema: { type: integer } }
        - { name: ids, in: query, schema: { type: string } }
        - { name: name__icontains, in: query, schema: { type: string } }
        - { name: time_start, in: query, schema: { type: string, format: date-time } }
        - { name: time_end, in: query, schema: { type: string, format: date-time } }
        - { name: status, in: query, description: CSV статусов, schema: { type: string } }
        - { name: responsible, in: query, schema: { type: string } }
        - { name: executors, in: query, schema: { type: string } }
        - { name: with_hierarchy, in: query, schema: { type: boolean, default: true } }
      responses:
        '200':
          description: Страница задач
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/PaginatedResponse'
                  - type: object
                    properties: { results: { type: array, items: { $ref: '#/components/schemas/ProjectTask' } } }
    post:
      tags: [tasks]
      summary: Создать задачу
      operationId: createTask
      requestBody:
        required: true
        content: { application/json: { schema: { $ref: '#/components/schemas/ProjectTaskCreate' } } }
      responses:
        '201': { description: Создано, content: { application/json: { schema: { $ref: '#/components/schemas/ProjectTask' } } } }
        '400': { $ref: '#/components/responses/BadRequest' }

  /api/pm/msp/tasks/{id}/:
    parameters: [ { $ref: '#/components/parameters/PathId' } ]
    get:
      tags: [tasks]
      summary: Задача по id
      operationId: retrieveTask
      responses:
        '200': { description: Задача, content: { application/json: { schema: { $ref: '#/components/schemas/ProjectTask' } } } }
        '404': { $ref: '#/components/responses/NotFound' }
    put:
      tags: [tasks]
      summary: Полное обновление задачи
      operationId: updateTask
      requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ProjectTaskCreate' } } } }
      responses:
        '200': { description: Обновлено, content: { application/json: { schema: { $ref: '#/components/schemas/ProjectTask' } } } }
        '400': { $ref: '#/components/responses/BadRequest' }
    patch:
      tags: [tasks]
      summary: Частичное обновление задачи
      operationId: partialUpdateTask
      requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ProjectTaskCreate' } } } }
      responses:
        '200': { description: Обновлено, content: { application/json: { schema: { $ref: '#/components/schemas/ProjectTask' } } } }
    delete:
      tags: [tasks]
      summary: Удалить задачу
      operationId: deleteTask
      responses:
        '204': { description: Удалено }

  /api/pm/msp/tasks/task_index/:
    patch:
      tags: [tasks]
      summary: Переиндексация задач
      operationId: updateTaskIndex
      responses:
        '200': { description: OK }

  /api/pm/msp/tasks/copy/:
    post:
      tags: [tasks]
      summary: Копировать задачи
      operationId: copyTasks
      requestBody:
        required: true
        content: { application/json: { schema: { type: object, properties: { ids: { type: array, items: { type: integer } } } } } }
      responses:
        '200': { description: OK }

  /api/pm/msp/tasks/{id}/permissions/:
    parameters: [ { $ref: '#/components/parameters/PathId' } ]
    get:
      tags: [tasks]
      summary: Права задачи
      operationId: getTaskPermissions
      responses:
        '200': { description: Права, content: { application/json: { schema: { $ref: '#/components/schemas/PermissionsResponse' } } } }
    post:
      tags: [tasks]
      summary: Сохранить права задачи
      operationId: saveTaskPermissions
      requestBody: { required: true, content: { application/json: { schema: { type: object } } } }
      responses:
        '200': { description: Права, content: { application/json: { schema: { $ref: '#/components/schemas/PermissionsResponse' } } } }

  /api/pm/msp/tasks/{id}/descriptions/:
    parameters: [ { $ref: '#/components/parameters/PathId' } ]
    get:
      tags: [tasks]
      summary: Описание задачи
      operationId: getTaskDescription
      responses:
        '200': { description: Описание, content: { application/json: { schema: { $ref: '#/components/schemas/Description' } } } }
    patch:
      tags: [tasks]
      summary: Обновить описание задачи
      operationId: updateTaskDescription
      requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/Description' } } } }
      responses:
        '200': { description: Обновлено, content: { application/json: { schema: { $ref: '#/components/schemas/Description' } } } }

  # ==========================================================================
  # Detailed tasks / descriptions
  # ==========================================================================
  /api/pm/msp/detailed-tasks/:
    get:
      tags: [detailing]
      summary: Список детализированных задач
      operationId: listDetailedTasks
      parameters:
        - { name: task, in: query, schema: { type: integer } }
        - { name: project, in: query, schema: { type: integer } }
      responses:
        '200': { description: Список, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/DetailedTask' } } } } }
    post:
      tags: [detailing]
      summary: Создать детализированную задачу
      operationId: createDetailedTask
      requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/DetailedTask' } } } }
      responses:
        '201': { description: Создано, content: { application/json: { schema: { $ref: '#/components/schemas/DetailedTask' } } } }

  /api/pm/msp/detailed-tasks/{id}/:
    parameters: [ { $ref: '#/components/parameters/PathId' } ]
    patch:
      tags: [detailing]
      summary: Обновить детализированную задачу
      operationId: updateDetailedTask
      requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/DetailedTask' } } } }
      responses:
        '200': { description: Обновлено, content: { application/json: { schema: { $ref: '#/components/schemas/DetailedTask' } } } }
    delete:
      tags: [detailing]
      summary: Удалить детализированную задачу
      operationId: deleteDetailedTask
      responses:
        '204': { description: Удалено }

  /api/pm/msp/descriptions/upload_file/:
    post:
      tags: [tasks]
      summary: Загрузить файл описания
      operationId: uploadDescriptionFile
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                file: { type: string, format: binary }
      responses:
        '200': { description: OK }

  # ==========================================================================
  # Values (actual values)
  # ==========================================================================
  /api/pm/msp/values/:
    get:
      tags: [tasks]
      summary: Фактические значения задач
      operationId: listActualValues
      parameters:
        - { name: task, in: query, schema: { type: integer } }
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Offset'
      responses:
        '200': { description: Значения, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/ActualValue' } } } } }
    post:
      tags: [tasks]
      summary: Создать фактическое значение
      operationId: createActualValue
      requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ActualValueCreate' } } } }
      responses:
        '201': { description: Создано, content: { application/json: { schema: { $ref: '#/components/schemas/ActualValue' } } } }

  /api/pm/msp/values/{id}/:
    parameters: [ { $ref: '#/components/parameters/PathId' } ]
    get:
      tags: [tasks]
      summary: Значение по id
      operationId: retrieveActualValue
      responses: { '200': { description: Значение, content: { application/json: { schema: { $ref: '#/components/schemas/ActualValue' } } } }, '404': { $ref: '#/components/responses/NotFound' } }
    put:
      tags: [tasks]
      summary: Обновить значение
      operationId: updateActualValue
      requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ActualValueCreate' } } } }
      responses: { '200': { description: Обновлено, content: { application/json: { schema: { $ref: '#/components/schemas/ActualValue' } } } } }
    delete:
      tags: [tasks]
      summary: Удалить значение
      operationId: deleteActualValue
      responses: { '204': { description: Удалено } }

  # ==========================================================================
  # Project states (base plans)
  # ==========================================================================
  /api/pm/msp/project-states/:
    get:
      tags: [states]
      summary: Список состояний
      operationId: listStates
      responses: { '200': { description: Список, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/ProjectState' } } } } } }
    post:
      tags: [states]
      summary: Создать состояние (базовый план)
      operationId: createState
      requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ProjectStateCreate' } } } }
      responses: { '201': { description: Создано, content: { application/json: { schema: { $ref: '#/components/schemas/ProjectState' } } } } }

  /api/pm/msp/project-states/{id}/:
    parameters: [ { $ref: '#/components/parameters/PathId' } ]
    get:
      tags: [states]
      summary: Состояние по id
      operationId: retrieveState
      responses: { '200': { description: Состояние, content: { application/json: { schema: { $ref: '#/components/schemas/ProjectState' } } } }, '404': { $ref: '#/components/responses/NotFound' } }
    put:
      tags: [states]
      summary: Обновить состояние
      operationId: updateState
      requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ProjectStateCreate' } } } }
      responses: { '200': { description: Обновлено, content: { application/json: { schema: { $ref: '#/components/schemas/ProjectState' } } } } }
    delete:
      tags: [states]
      summary: Удалить состояние
      operationId: deleteState
      responses: { '204': { description: Удалено } }

  /api/pm/msp/project-states/{id}/data/:
    parameters: [ { $ref: '#/components/parameters/PathId' } ]
    get:
      tags: [states]
      summary: Данные состояния (базового плана)
      operationId: getStateData
      responses: { '200': { description: Данные, content: { application/json: { schema: { type: object } } } } }
    post:
      tags: [states]
      summary: Добавить задачи в базовый план
      operationId: addTasksToState
      responses: { '200': { description: OK } }

  /api/pm/msp/project-states/{id}/edit_state/:
    parameters: [ { $ref: '#/components/parameters/PathId' } ]
    patch:
      tags: [states]
      summary: Редактировать состояние
      operationId: editState
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                id: { type: integer }
                time_start: { type: string, format: date-time, nullable: true }
                time_end: { type: string, format: date-time, nullable: true }
                progress: { type: number, nullable: true }
                planned_value: { type: number, nullable: true }
                unit: { type: string, nullable: true }
              required: [id]
      responses: { '200': { description: OK } }

  # ==========================================================================
  # Resources
  # ==========================================================================
  /api/pm/msp/resources/:
    get:
      tags: [resources]
      summary: Список ресурсов
      operationId: listResources
      parameters:
        - { name: ids, in: query, schema: { type: string } }
        - { name: projects, in: query, schema: { type: string } }
        - { name: type, in: query, schema: { type: string } }
        - { name: parent, in: query, schema: { type: integer } }
        - { name: company, in: query, schema: { type: integer } }
        - { name: full, in: query, schema: { type: boolean } }
        - { name: extend, in: query, schema: { type: boolean } }
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Offset'
      responses: { '200': { description: Список, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/Resource' } } } } } }
    post:
      tags: [resources]
      summary: Создать ресурс
      operationId: createResource
      requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ResourceCreate' } } } }
      responses: { '201': { description: Создано, content: { application/json: { schema: { $ref: '#/components/schemas/Resource' } } } } }

  /api/pm/msp/resources/{id}/:
    parameters: [ { $ref: '#/components/parameters/PathId' } ]
    get: { tags: [resources], summary: Ресурс по id, operationId: retrieveResource, responses: { '200': { description: Ресурс, content: { application/json: { schema: { $ref: '#/components/schemas/Resource' } } } }, '404': { $ref: '#/components/responses/NotFound' } } }
    put: { tags: [resources], summary: Обновить ресурс, operationId: updateResource, requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ResourceCreate' } } } }, responses: { '200': { description: Обновлено, content: { application/json: { schema: { $ref: '#/components/schemas/Resource' } } } } } }
    patch: { tags: [resources], summary: Частичное обновление ресурса, operationId: partialUpdateResource, requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ResourceCreate' } } } }, responses: { '200': { description: Обновлено, content: { application/json: { schema: { $ref: '#/components/schemas/Resource' } } } } } }
    delete: { tags: [resources], summary: Удалить ресурс, operationId: deleteResource, responses: { '204': { description: Удалено } } }

  /api/pm/msp/resources/bulk_create/:
    post:
      tags: [resources]
      summary: Массовое создание ресурсов
      operationId: bulkCreateResources
      requestBody: { required: true, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/ResourceCreate' } } } } }
      responses: { '201': { description: Создано } }

  /api/pm/msp/resources/resource_planning/:
    get:
      tags: [resources]
      summary: Ресурсное планирование
      operationId: resourcePlanning
      parameters:
        - { name: projects, in: query, schema: { type: string } }
        - { name: resource_type, in: query, schema: { type: string, default: human } }
        - { name: start, in: query, required: true, schema: { type: string, format: date-time } }
        - { name: end, in: query, required: true, schema: { type: string, format: date-time } }
        - { name: scale, in: query, schema: { type: string, default: D } }
        - { name: group, in: query, schema: { type: string } }
      responses: { '200': { description: Данные, content: { application/json: { schema: { type: object } } } } }

  /api/pm/msp/resources/{id}/bind_task/:
    parameters: [ { $ref: '#/components/parameters/PathId' } ]
    post: { tags: [resources], summary: Привязать задачу к ресурсу, operationId: bindTaskToResource, requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ResourceTaskRelation' } } } }, responses: { '201': { description: Создано } } }

  /api/pm/msp/resources/{id}/bind_elements/:
    parameters: [ { $ref: '#/components/parameters/PathId' } ]
    post: { tags: [resources], summary: Привязать элементы к ресурсу, operationId: bindElementsToResource, responses: { '201': { description: Создано } } }

  # ---- Resource relations ----
  /api/pm/msp/resources-tasks/:
    get:
      tags: [resources]
      summary: Список привязок ресурс-задача
      operationId: listResourceTaskRelations
      parameters:
        - { name: projects, in: query, schema: { type: string } }
        - { name: tasks, in: query, schema: { type: string } }
        - { name: resources, in: query, schema: { type: string } }
      responses: { '200': { description: Список, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/ResourceTaskRelation' } } } } } }
    post: { tags: [resources], summary: Создать привязку ресурс-задача, operationId: createResourceTaskRelation, requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ResourceTaskRelation' } } } }, responses: { '201': { description: Создано } } }

  /api/pm/msp/resources-tasks/{id}/:
    parameters: [ { $ref: '#/components/parameters/PathId' } ]
    get: { tags: [resources], summary: Привязка по id, operationId: retrieveResourceTaskRelation, responses: { '200': { description: Привязка, content: { application/json: { schema: { $ref: '#/components/schemas/ResourceTaskRelation' } } } } } }
    patch: { tags: [resources], summary: Обновить привязку, operationId: updateResourceTaskRelation, requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ResourceTaskRelation' } } } }, responses: { '200': { description: Обновлено } } }
    delete: { tags: [resources], summary: Удалить привязку, operationId: deleteResourceTaskRelation, responses: { '204': { description: Удалено } } }

  /api/pm/msp/resources-tasks/assign/:
    patch: { tags: [resources], summary: Назначение ресурса на задачи, operationId: assignResourceToTasks, responses: { '200': { description: OK } } }

  /api/pm/msp/resources-tasks/bulk_update/:
    patch:
      tags: [resources]
      summary: Массовое обновление привязок ресурс-задача
      operationId: bulkUpdateResourceTaskRelations
      requestBody: { required: true, content: { application/json: { schema: { type: object, properties: { ids: { type: array, items: { type: integer } }, profile: { type: integer } } } } } }
      responses: { '200': { description: OK } }

  /api/pm/msp/resources-tasks/bulk_delete/:
    delete:
      tags: [resources]
      summary: Массовое удаление привязок ресурс-задача
      operationId: bulkDeleteResourceTaskRelations
      requestBody: { required: true, content: { application/json: { schema: { type: object, properties: { ids: { type: array, items: { type: integer } } } } } } }
      responses: { '204': { description: Удалено } }

  /api/pm/msp/resources-elements/:
    get:
      tags: [resources]
      summary: Список привязок ресурс-элемент
      operationId: listResourceElementRelations
      parameters:
        - { name: resources, in: query, schema: { type: string } }
        - { name: instances, in: query, schema: { type: string } }
      responses: { '200': { description: Список, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/ResourceElementRelation' } } } } } }
    post: { tags: [resources], summary: Создать привязку ресурс-элемент, operationId: createResourceElementRelation, requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ResourceElementRelation' } } } }, responses: { '201': { description: Создано } } }

  # ==========================================================================
  # Relations (task/project)
  # ==========================================================================
  /api/pm/msp/task-relations/:
    get:
      tags: [relations]
      summary: Список связей задач
      operationId: listTaskRelations
      parameters: [ { name: project, in: query, schema: { type: integer } } ]
      responses: { '200': { description: Список, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/Task2TaskRelation' } } } } } }
    post: { tags: [relations], summary: Создать связь задач, operationId: createTaskRelation, requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/Task2TaskRelation' } } } }, responses: { '201': { description: Создано } } }

  /api/pm/msp/task-relations/{id}/:
    parameters: [ { $ref: '#/components/parameters/PathId' } ]
    put: { tags: [relations], summary: Обновить связь задач, operationId: updateTaskRelation, requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/Task2TaskRelation' } } } }, responses: { '200': { description: Обновлено } } }
    delete: { tags: [relations], summary: Удалить связь задач, operationId: deleteTaskRelation, responses: { '204': { description: Удалено } } }

  /api/pm/msp/task-relations/bulk_delete/:
    delete:
      tags: [relations]
      summary: Массовое удаление связей задач
      operationId: bulkDeleteTaskRelations
      requestBody: { required: true, content: { application/json: { schema: { type: object, properties: { ids: { type: array, items: { type: integer } } } } } } }
      responses: { '204': { description: Удалено } }

  /api/pm/msp/project-relations/:
    get: { tags: [relations], summary: Список связей проектов, operationId: listProjectRelations, parameters: [ { name: project, in: query, schema: { type: integer } } ], responses: { '200': { description: Список, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/Project2ProjectRelation' } } } } } } }
    post:
      tags: [relations]
      summary: Создать связь(и) проектов
      operationId: createProjectRelation
      requestBody: { required: true, content: { application/json: { schema: { oneOf: [ { $ref: '#/components/schemas/Project2ProjectRelation' }, { type: array, items: { $ref: '#/components/schemas/Project2ProjectRelation' } } ] } } } }
      responses: { '201': { description: Создано } }

  /api/pm/msp/project-relations/{id}/:
    parameters: [ { $ref: '#/components/parameters/PathId' } ]
    patch: { tags: [relations], summary: Обновить связь проектов, operationId: updateProjectRelation, requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/Project2ProjectRelation' } } } }, responses: { '200': { description: Обновлено } } }
    delete: { tags: [relations], summary: Удалить связь проектов, operationId: deleteProjectRelation, responses: { '204': { description: Удалено } } }

  /api/pm/msp/entity-relations/:
    get: { tags: [relations], summary: Список сущностных связей, operationId: listEntityRelations, parameters: [ { name: project_id, in: query, schema: { type: integer } } ], responses: { '200': { description: Список, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/EntityRelation' } } } } } } }
    post: { tags: [relations], summary: Создать сущностную связь, operationId: createEntityRelation, requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/EntityRelation' } } } }, responses: { '201': { description: Создано } } }

  /api/pm/msp/entity-relations/{id}/:
    parameters: [ { $ref: '#/components/parameters/PathId' } ]
    patch: { tags: [relations], summary: Обновить сущностную связь, operationId: updateEntityRelation, requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/EntityRelation' } } } }, responses: { '200': { description: Обновлено } } }
    delete: { tags: [relations], summary: Удалить сущностную связь, operationId: deleteEntityRelation, responses: { '204': { description: Удалено } } }

  # ==========================================================================
  # Attributes
  # ==========================================================================
  /api/pm/msp/project-attribute/:
    get:
      tags: [attributes]
      summary: Атрибуты проекта
      operationId: listProjectAttributes
      parameters: [ { name: project, in: query, schema: { type: integer } } ]
      responses: { '200': { description: Список, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/ProjectAttributeRelation' } } } } } }
    post:
      tags: [attributes]
      summary: Привязать атрибут(ы) к проекту
      operationId: createProjectAttribute
      requestBody: { required: true, content: { application/json: { schema: { oneOf: [ { $ref: '#/components/schemas/ProjectAttributeRelation' }, { type: array, items: { $ref: '#/components/schemas/ProjectAttributeRelation' } } ] } } } }
      responses: { '201': { description: Создано } }

  /api/pm/msp/project-attribute/{id}/:
    parameters: [ { $ref: '#/components/parameters/PathId' } ]
    put: { tags: [attributes], summary: Обновить атрибут проекта, operationId: updateProjectAttribute, requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ProjectAttributeRelation' } } } }, responses: { '200': { description: Обновлено } } }
    delete: { tags: [attributes], summary: Удалить атрибут проекта, operationId: deleteProjectAttribute, responses: { '204': { description: Удалено } } }

  /api/pm/msp/task-value/:
    get:
      tags: [attributes]
      summary: Значения атрибутов задач
      operationId: listTaskValues
      parameters:
        - { name: tasks, in: query, description: CSV, schema: { type: string } }
        - { name: attributes, in: query, description: CSV, schema: { type: string } }
      responses: { '200': { description: Список, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/TaskAttributeValue' } } } } } }
    post:
      tags: [attributes]
      summary: Создать значение(я) атрибутов задач
      operationId: createTaskValue
      requestBody: { required: true, content: { application/json: { schema: { oneOf: [ { $ref: '#/components/schemas/TaskAttributeValue' }, { type: array, items: { $ref: '#/components/schemas/TaskAttributeValue' } } ] } } } }
      responses: { '201': { description: Создано } }

  # ==========================================================================
  # Calendars
  # ==========================================================================
  /api/pm/msp/calendars/:
    get: { tags: [calendars], summary: Список календарей, operationId: listCalendars, responses: { '200': { description: Список, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/Calendar' } } } } } } }
    post: { tags: [calendars], summary: Создать календарь, operationId: createCalendar, requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/Calendar' } } } }, responses: { '201': { description: Создано, content: { application/json: { schema: { $ref: '#/components/schemas/Calendar' } } } } } }

  /api/pm/msp/calendars/{id}/:
    parameters: [ { $ref: '#/components/parameters/PathId' } ]
    get: { tags: [calendars], summary: Календарь по id, operationId: retrieveCalendar, responses: { '200': { description: Календарь, content: { application/json: { schema: { $ref: '#/components/schemas/Calendar' } } } } } }
    patch: { tags: [calendars], summary: Изменить календарь, operationId: updateCalendar, requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/Calendar' } } } }, responses: { '200': { description: Обновлено } } }
    delete: { tags: [calendars], summary: Удалить календарь, operationId: deleteCalendar, responses: { '204': { description: Удалено } } }

  # ==========================================================================
  # Project settings (KSG)
  # ==========================================================================
  /api/pm/msp/project-settings/:
    get: { tags: [settings], summary: Список настроек КСГ, operationId: listProjectSettings, responses: { '200': { description: Список, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/ProjectSettings' } } } } } } }
    post: { tags: [settings], summary: Создать настройку КСГ, operationId: createProjectSettings, requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ProjectSettings' } } } }, responses: { '201': { description: Создано } } }

  /api/pm/msp/project-settings/{id}/:
    parameters: [ { $ref: '#/components/parameters/PathId' } ]
    get: { tags: [settings], summary: Настройка по id, operationId: retrieveProjectSettings, responses: { '200': { description: Настройка, content: { application/json: { schema: { $ref: '#/components/schemas/ProjectSettings' } } } } } }
    patch: { tags: [settings], summary: Изменить настройку, operationId: updateProjectSettings, requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ProjectSettings' } } } }, responses: { '200': { description: Обновлено } } }
    delete: { tags: [settings], summary: Удалить настройку, operationId: deleteProjectSettings, responses: { '204': { description: Удалено } } }

  /api/pm/msp/project-settings/{id}/apply/:
    parameters: [ { $ref: '#/components/parameters/PathId' } ]
    post:
      tags: [settings]
      summary: Применить глобальную настройку
      operationId: applyProjectSettings
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                project_id: { type: integer }
                connect_missing_attributes: { type: boolean }
              required: [project_id]
      responses: { '200': { description: OK } }

  # ==========================================================================
  # Visual profiles
  # ==========================================================================
  /api/pm/msp/visual-profiles/:
    get: { tags: [detailing], summary: Список визуальных профилей, operationId: listVisualProfiles, responses: { '200': { description: Список, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/VisualProfile' } } } } } } }
    post: { tags: [detailing], summary: Создать визуальный профиль, operationId: createVisualProfile, requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/VisualProfile' } } } }, responses: { '201': { description: Создано } } }

  /api/pm/msp/visual-profiles/{id}:
    parameters: [ { $ref: '#/components/parameters/PathId' } ]
    patch: { tags: [detailing], summary: Изменить визуальный профиль, operationId: updateVisualProfile, requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/VisualProfile' } } } }, responses: { '200': { description: Обновлено } } }
    delete: { tags: [detailing], summary: Удалить визуальный профиль, operationId: deleteVisualProfile, responses: { '204': { description: Удалено } } }

  # ==========================================================================
  # Comments
  # ==========================================================================
  /api/pm/msp/comments/:
    get: { tags: [comments], summary: Список комментариев, operationId: listComments, responses: { '200': { description: Список, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/Comment' } } } } } } }
    post: { tags: [comments], summary: Создать комментарий, operationId: createComment, requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/CommentCreate' } } } }, responses: { '201': { description: Создано } } }

  # ==========================================================================
  # Descriptions resource
  # ==========================================================================
  /api/pm/msp/descriptions/:
    get: { tags: [tasks], summary: Список описаний, operationId: listDescriptions, parameters: [ { name: task, in: query, schema: { type: integer } } ], responses: { '200': { description: Список, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/Description' } } } } } } }
    post: { tags: [tasks], summary: Создать описание, operationId: createDescription, requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/Description' } } } }, responses: { '201': { description: Создано } } }

  # ==========================================================================
  # Task info (public/service)
  # ==========================================================================
  /api/pm/msp/task-info/:
    get:
      tags: [tasks]
      summary: Задачи по профилям (not_started/in_progress/completed)
      operationId: getTaskInfo
      parameters:
        - { name: projects, in: query, required: true, description: CSV идентификаторов, schema: { type: string } }
        - { name: date, in: query, required: true, schema: { type: string, format: date-time } }
        - { name: document, in: query, required: true, schema: { type: string } }
        - { name: base_plane, in: query, schema: { type: string } }
      responses: { '200': { description: Данные, content: { application/json: { schema: { type: object } } } } }

  /api/pm/msp/external-task-info/:
    get:
      tags: [tasks]
      summary: Инфо о фоновой задаче через брокер
      operationId: getExternalTaskInfo
      security: []
      parameters: [ { name: task_id, in: query, schema: { type: string, format: uuid } } ]
      responses: { '200': { description: Данные, content: { application/json: { schema: { type: object } } } } }

  # ==========================================================================
  # External API v2 (read-only)
  # ==========================================================================
  /api/pm/external/v2/projects/:
    get:
      tags: [external]
      summary: Список проектов (внешний)
      operationId: externalListProjects
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Offset'
        - { name: search, in: query, schema: { type: string } }
        - { name: ids, in: query, schema: { type: string } }
        - { name: parent_id, in: query, schema: { type: integer } }
      responses: { '200': { description: Список, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/Project' } } } } } }

  /api/pm/external/v2/projects/{id}/:
    parameters: [ { $ref: '#/components/parameters/PathId' } ]
    get: { tags: [external], summary: Проект (внешний), operationId: externalRetrieveProject, responses: { '200': { description: Проект, content: { application/json: { schema: { $ref: '#/components/schemas/Project' } } } }, '404': { $ref: '#/components/responses/NotFound' } } }

  /api/pm/external/v2/projects/{id}/project_data/:
    parameters: [ { $ref: '#/components/parameters/PathId' } ]
    get: { tags: [external], summary: Данные проекта (внешний), operationId: externalProjectData, responses: { '200': { description: Данные, content: { application/json: { schema: { type: object } } } } } }

  /api/pm/external/v2/tasks/:
    get:
      tags: [external]
      summary: Список задач (внешний)
      operationId: externalListTasks
      parameters:
        - { name: extend, in: query, schema: { type: boolean } }
        - { name: ids, in: query, schema: { type: string } }
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Offset'
      responses: { '200': { description: Список, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/ProjectTask' } } } } } }

  /api/pm/external/v2/tasks/{id}/:
    parameters: [ { $ref: '#/components/parameters/PathId' } ]
    get: { tags: [external], summary: Задача (внешний), operationId: externalRetrieveTask, responses: { '200': { description: Задача, content: { application/json: { schema: { $ref: '#/components/schemas/ProjectTask' } } } } } }

  /api/pm/external/v2/values/:
    get: { tags: [external], summary: Фактические значения (внешний), operationId: externalListValues, responses: { '200': { description: Список, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/ActualValue' } } } } } } }

  /api/pm/external/v2/values/{id}/:
    parameters: [ { $ref: '#/components/parameters/PathId' } ]
    get: { tags: [external], summary: Значение (внешний), operationId: externalRetrieveValue, responses: { '200': { description: Значение, content: { application/json: { schema: { $ref: '#/components/schemas/ActualValue' } } } } } }

  /api/pm/external/v2/project-states/:
    get:
      tags: [external]
      summary: Состояния проекта (внешний)
      operationId: externalListStates
      parameters: [ { name: project_id, in: query, schema: { type: integer } } ]
      responses: { '200': { description: Список, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/ProjectState' } } } } } }

  /api/pm/external/v2/project-states/{id}/:
    parameters: [ { $ref: '#/components/parameters/PathId' } ]
    get: { tags: [external], summary: Состояние (внешний), operationId: externalRetrieveState, responses: { '200': { description: Состояние, content: { application/json: { schema: { $ref: '#/components/schemas/ProjectState' } } } } } }

  /api/pm/external/v2/project-states/{id}/data/:
    parameters: [ { $ref: '#/components/parameters/PathId' } ]
    get: { tags: [external], summary: Данные состояния (внешний), operationId: externalStateData, responses: { '200': { description: Данные, content: { application/json: { schema: { type: object } } } } } }

  # ==========================================================================
  # Internal API (no auth, cluster-only)
  # ==========================================================================
  /internal/pm/auto_scheduling/:
    post:
      tags: [internal]
      summary: Интегрированное автопланирование
      operationId: internalAutoScheduling
      security: []
      requestBody: { required: true, content: { application/json: { schema: { type: object, properties: { project_id: { type: integer } }, required: [project_id] } } } }
      responses: { '200': { description: OK } }

  /internal/pm/integrated_base_plan/:
    post:
      tags: [internal]
      summary: Создать/обновить интегрированные состояния
      operationId: internalIntegratedBasePlan
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                project_id: { type: integer }
                user_id: { type: integer }
                linked_project: { type: integer }
                name: { type: string }
                tasks: { type: array, items: { type: object } }
                task_ids: { type: array, items: { type: integer } }
                state_id: { type: integer }
      responses: { '200': { description: OK } }

  /internal/pm/detailed_tasks/:
    post:
      tags: [internal]
      summary: Обновить значения атрибутов задачи
      operationId: internalDetailedTasks
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                task_id: { type: integer }
                result_fields: { type: object }
                company_id: { type: integer }
              required: [task_id, company_id]
      responses: { '200': { description: OK } }

  /internal/pm/sync_tasks/:
    post:
      tags: [internal]
      summary: Синхронизация задач + автопланирование
      operationId: internalSyncTasks
      security: []
      requestBody: { required: true, content: { application/json: { schema: { type: object, properties: { project_id: { type: integer } }, required: [project_id] } } } }
      responses: { '200': { description: OK } }

  /internal/pm/base_plans/:
    get:
      tags: [internal]
      summary: Получить базовый план
      operationId: internalGetBasePlan
      security: []
      parameters: [ { name: base_plan_id, in: query, required: true, schema: { type: integer } } ]
      responses:
        '200': { description: Данные, content: { application/json: { schema: { type: object } } } }
        '400': { $ref: '#/components/responses/BadRequest' }

  /internal/pm/tasks_dates/:
    get:
      tags: [internal]
      summary: Даты и прогресс задач проекта
      operationId: internalTasksDates
      security: []
      parameters: [ { name: project_id, in: query, required: true, schema: { type: integer } } ]
      responses:
        '200': { description: Данные, content: { application/json: { schema: { type: object } } } }
        '400': { $ref: '#/components/responses/BadRequest' }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: |
        JWT в заголовке `Authorization: Bearer <token>`. В режиме sarex-backend
        подпись проверяется публичным ключом (`RS512`); в режиме Zitadel
        дополнительно требуется заголовок `identity`.
    identityToken:
      type: apiKey
      in: header
      name: identity
      description: |
        Заголовок `identity` (`Identity <jwt>`) для режима Zitadel. При его
        наличии полезная нагрузка берётся из этого токена.

  parameters:
    PathId:
      name: id
      in: path
      required: true
      schema: { type: integer }
    Limit:
      name: limit
      in: query
      required: false
      schema: { type: integer, minimum: 0, default: 1000 }
    Offset:
      name: offset
      in: query
      required: false
      schema: { type: integer, minimum: 0, default: 0 }

  responses:
    BadRequest:
      description: Некорректный запрос / ошибка валидации
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ValidationError' }
    Unauthorized:
      description: Токен не предоставлен или невалиден
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Detail' }
    Forbidden:
      description: Недостаточно прав
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Detail' }
    NotFound:
      description: Ресурс не найден
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Detail' }

  schemas:
    # --- Общие ------------------------------------------------------------
    Detail:
      type: object
      properties:
        detail: { type: string }
      required: [detail]

    ValidationError:
      type: object
      description: |
        Ошибка валидации DRF — либо словарь `{ "<field>": ["<msg>", ...] }`,
        либо `{ "detail": "..." }`.
      additionalProperties:
        type: array
        items: { type: string }

    PaginatedResponse:
      type: object
      description: Обёртка DRF LimitOffsetPagination
      properties:
        count: { type: integer }
        next: { type: string, format: uri, nullable: true }
        previous: { type: string, format: uri, nullable: true }
        results: { type: array, items: { type: object } }
      required: [count, results]

    # --- Проекты ----------------------------------------------------------
    Project:
      type: object
      properties:
        id: { type: integer, readOnly: true }
        name: { type: string }
        company: { type: integer }
        parent: { type: integer, nullable: true }
        is_folder: { type: boolean }
        resource_id: { type: string, nullable: true }
        document_id: { type: string, nullable: true }
        bundle_id: { type: string, nullable: true }
        description: { type: string, nullable: true }
        settings: { type: object }
        target_date: { type: string, format: date-time, nullable: true }
        project_type: { type: string, nullable: true }
        calendar: { type: integer, nullable: true }
        status: { type: string, readOnly: true }
        creator: { type: integer, readOnly: true }
        permissions: { type: object, readOnly: true }
        restrictions: { type: object, readOnly: true }
        time_start: { type: string, format: date-time, readOnly: true, nullable: true }
        time_end: { type: string, format: date-time, readOnly: true, nullable: true }
        total_cost: { type: number, readOnly: true, nullable: true }
        current_cost: { type: number, readOnly: true, nullable: true }
        progress: { type: number, readOnly: true, nullable: true }
        relations: { type: array, items: { type: object }, readOnly: true }
        rule: { type: object, nullable: true }
        created_at: { type: string, format: date-time, readOnly: true }
        updated_at: { type: string, format: date-time, readOnly: true }
      required: [name, company]

    ProjectCreate:
      type: object
      properties:
        name: { type: string }
        company_id: { type: integer }
        parent: { type: integer, nullable: true }
        is_folder: { type: boolean }
        resource_id: { type: string, nullable: true }
        ms_file: { type: string, nullable: true }
        description: { type: string, nullable: true }
        document_id: { type: string, nullable: true }
        bundle_id: { type: string, nullable: true }
        settings: { type: object, default: {} }
        target_date: { type: string, format: date-time, nullable: true }
        project_type: { type: string, nullable: true }
        calendar: { type: integer, nullable: true }
      required: [name, company_id]

    FolderCreate:
      type: object
      properties:
        company: { type: integer }
        company_name: { type: string }
        creator: { type: integer }
        service_account: { type: string, format: uuid }
        folder_name: { type: string, nullable: true }
      required: [company, company_name, creator, service_account]

    PermissionsResponse:
      type: object
      properties:
        direct_permissions: { type: array, items: { type: object } }
        inherited_permissions: { type: array, items: { type: object } }

    # --- Задачи -----------------------------------------------------------
    ProjectTask:
      type: object
      properties:
        id: { type: integer, readOnly: true }
        project: { type: integer }
        parent: { type: integer, nullable: true }
        index_prefix: { type: string, nullable: true }
        task_id: { type: string, nullable: true }
        name: { type: string }
        time_start: { type: string, format: date-time, nullable: true }
        time_end: { type: string, format: date-time, nullable: true }
        deadline: { type: string, format: date-time, nullable: true }
        responsible: { type: array, items: { type: integer } }
        executors: { type: array, items: { type: integer } }
        task_type: { type: string }
        unit: { type: string, nullable: true }
        local_index: { type: integer, nullable: true }
        constraint_date: { type: string, format: date-time, nullable: true }
        constraint_type: { type: string, nullable: true }
        cost: { type: number, nullable: true }
        progress: { type: number, nullable: true }
        status: { type: string, nullable: true }
        duration: { type: number, nullable: true }
        planned_duration: { type: number, nullable: true }
        cipher: { type: string, nullable: true }
        is_active: { type: boolean }
        current_cost: { type: number, nullable: true }
        planned_value: { type: number, nullable: true }
        calendar: { type: integer, nullable: true }
        distribution: { type: object, nullable: true }
        actual_value: { type: number, readOnly: true, nullable: true }
        total_cost: { type: number, readOnly: true, nullable: true }
        attributes: { type: array, items: { type: object } }
        permissions: { type: object }

    ProjectTaskCreate:
      type: object
      properties:
        project: { type: integer }
        parent: { type: integer, nullable: true }
        name: { type: string }
        time_start: { type: string, format: date-time }
        time_end: { type: string, format: date-time }
        deadline: { type: string, format: date-time, nullable: true }
        responsible: { type: array, items: { type: integer } }
        executors: { type: array, items: { type: integer } }
        task_type: { type: string, default: fix_value }
        unit: { type: string, nullable: true }
        cost: { type: number, nullable: true }
        progress: { type: number, nullable: true }
        status: { type: string, nullable: true }
        is_active: { type: boolean, default: true }
        calendar: { type: integer, nullable: true }
      required: [project, name, time_start, time_end]

    ProjectTaskUpdate:
      type: object
      description: Плоский сериализатор для массового обновления
      properties:
        id: { type: integer }
        name: { type: string }
        time_start: { type: string, format: date-time }
        time_end: { type: string, format: date-time }
        responsible: { type: array, items: { type: integer } }
        executors: { type: array, items: { type: integer } }
        cost: { type: number }
        progress: { type: number }
        status: { type: string }
      required: [id]

    ActualValue:
      type: object
      properties:
        id: { type: integer, readOnly: true }
        value: { type: number }
        description: { type: string, nullable: true }
        task: { type: integer }
        time_start: { type: string, format: date-time }
        time_end: { type: string, format: date-time }
        creator: { type: integer, readOnly: true }
        created_at: { type: string, format: date-time, readOnly: true }
        updated_at: { type: string, format: date-time, readOnly: true }

    ActualValueCreate:
      type: object
      properties:
        value: { type: number }
        description: { type: string, nullable: true }
        task: { type: integer }
        time_start: { type: string, format: date-time }
        time_end: { type: string, format: date-time }
      required: [value, task, time_start, time_end]

    DetailedTask:
      type: object
      properties:
        id: { type: integer, readOnly: true }
        task: { type: integer }
        weight: { type: number, nullable: true }
        data: { type: object }

    Description:
      type: object
      properties:
        id: { type: integer, readOnly: true }
        task: { type: integer }
        text: { type: string }
      required: [task, text]

    # --- Состояния --------------------------------------------------------
    ProjectState:
      type: object
      properties:
        id: { type: integer, readOnly: true }
        name: { type: string }
        project: { type: integer }
        creator: { type: integer, readOnly: true }
        is_locked: { type: boolean, readOnly: true }

    ProjectStateCreate:
      type: object
      properties:
        name: { type: string }
        project: { type: integer }
      required: [name, project]

    # --- Ресурсы ----------------------------------------------------------
    Resource:
      type: object
      properties:
        id: { type: integer, readOnly: true }
        name: { type: string }
        company: { type: integer }
        calendar: { type: integer, nullable: true }
        project: { type: integer, nullable: true }
        resource_type: { type: string }
        parent: { type: integer, nullable: true }
        cost: { type: number, nullable: true }
        path: { type: string, readOnly: true }
        is_leaf: { type: boolean, readOnly: true }
        elements: { type: array, items: { type: integer } }
        projects: { type: array, items: { type: object } }

    ResourceCreate:
      type: object
      properties:
        name: { type: string }
        company: { type: integer }
        calendar: { type: integer, nullable: true }
        project: { type: integer, nullable: true }
        resource_type: { type: string }
        parent: { type: integer, nullable: true }
        cost: { type: number, nullable: true }
      required: [name, company]

    ResourceTaskRelation:
      type: object
      properties:
        id: { type: integer, readOnly: true }
        resource: { type: integer }
        task: { type: integer }
        profile: { type: integer, nullable: true }

    ResourceElementRelation:
      type: object
      properties:
        id: { type: integer, readOnly: true }
        resource: { type: integer }
        instance: { type: string }
        model_name: { type: string }
        attributes: { type: object }
        path: { type: string, nullable: true }

    # --- Связи ------------------------------------------------------------
    Task2TaskRelation:
      type: object
      properties:
        id: { type: integer, readOnly: true }
        target: { type: integer }
        source: { type: integer }
        type: { type: string }
        lag: { type: number, nullable: true }

    Project2ProjectRelation:
      type: object
      properties:
        id: { type: integer, readOnly: true }
        successor: { type: integer }
        predecessor: { type: integer }
        mode: { type: string, nullable: true }

    EntityRelation:
      type: object
      properties:
        id: { type: integer, readOnly: true }
        project: { type: integer }
        entity_name: { type: string }
        attribute_ids: { type: array, items: { type: integer } }
        settings: { type: object }
        entity_type: { type: string }

    # --- Атрибуты ---------------------------------------------------------
    ProjectAttributeRelation:
      type: object
      properties:
        id: { type: integer, readOnly: true }
        project: { type: integer }
        asset_id: { type: string, nullable: true }
        attribute_id: { type: integer }
        is_system: { type: boolean }
        default_value: { nullable: true }
        mode: { type: string, nullable: true }

    TaskAttributeValue:
      type: object
      properties:
        id: { type: integer, readOnly: true }
        task: { type: integer }
        attribute_id: { type: integer }
        type: { type: string, description: "Тип значения (вход): integer/float/string/datetime/date" }
        values: { description: Значение(я) атрибута }
        integer_value: { type: integer, nullable: true }
        float_value: { type: number, nullable: true }
        string_value: { type: string, nullable: true }
        datetime_value: { type: string, format: date-time, nullable: true }
        date_value: { type: string, format: date, nullable: true }
        assets: { type: array, items: { type: string } }

    # --- Календари --------------------------------------------------------
    Calendar:
      type: object
      properties:
        id: { type: integer, readOnly: true }
        name: { type: string }
        company: { type: integer }
        mask: { type: array, items: { type: integer } }
        work_time_start: { type: string }
        work_time_end: { type: string }
        work_day_duration: { type: number, readOnly: true }
        is_system: { type: boolean }
        holidays: { type: array, items: { type: string, format: date } }
        exception_holidays: { type: array, items: { type: string, format: date } }
        creator: { type: integer, readOnly: true }
        projects_count: { type: integer, readOnly: true }
        created_at: { type: string, format: date-time, readOnly: true }
        updated_at: { type: string, format: date-time, readOnly: true }
      required: [name, mask, work_time_start, work_time_end]

    # --- Настройки --------------------------------------------------------
    ProjectSettings:
      type: object
      properties:
        id: { type: integer, readOnly: true }
        name: { type: string }
        project: { type: integer, nullable: true }
        state: { type: integer, nullable: true }
        image: { type: string, nullable: true }
        is_default: { type: boolean }
        visibility: { type: string, enum: [global, private, public] }
        company_id: { type: integer, nullable: true }
        service_accounts: { type: array, items: { type: string } }
        creator: { type: integer, readOnly: true }
        created_at: { type: string, format: date-time, readOnly: true }
        updated_at: { type: string, format: date-time, readOnly: true }

    VisualProfile:
      type: object
      properties:
        id: { type: integer, readOnly: true }
        name: { type: string }
        color: { type: string, description: "HEX-цвет, напр. #RRGGBB" }
        project: { type: integer }
        behavior: { type: object }

    Comment:
      type: object
      properties:
        id: { type: integer, readOnly: true }
        text: { type: string }
        task: { type: integer }
        creator: { type: integer, readOnly: true }
        created_at: { type: string, format: date-time, readOnly: true }
        updated_at: { type: string, format: date-time, readOnly: true }

    CommentCreate:
      type: object
      properties:
        text: { type: string }
        task: { type: integer }
      required: [text, task]

    # --- Системный журнал -------------------------------------------------
    SystemLog:
      type: object
      properties:
        id: { type: integer, readOnly: true }
        project_id: { type: integer }
        user_id: { type: integer, nullable: true }
        created_at: { type: string, format: date-time, readOnly: true }
        message_data: { type: object }
        action: { type: string }
        entity: { type: string }
        changes: { type: object, description: "Только в detail-варианте" }

    # --- Экспорт ----------------------------------------------------------
    ExportProject:
      type: object
      properties:
        export_fields: { type: array, items: { type: string } }
        export_attrs: { type: array, items: { type: integer } }
        fields_order: { type: array, items: { type: string } }
        data: { type: array, items: { type: object } }
        page_format: { type: string }
        page_size: { type: string }
        left_table_percent_width: { type: number, minimum: 0, maximum: 100 }
        base_plan_ids: { type: array, items: { type: integer } }
        margins: { type: array, items: { type: number } }
        expanded_tasks: { type: array, items: { type: integer } }
        header_text: { type: string }
        footer_text: { type: string }
        display_relations: { type: boolean }
        date_from: { type: string, format: date }
        date_to: { type: string, format: date }
        fields_width: { type: object }
        project_name: { type: string }
        show_counter: { type: boolean }
        font_size: { type: integer }
        conditional_formatting: { type: array, items: { type: object } }
      required: [export_fields, data]
