{
  "openapi": "3.1.1",
  "info": {
    "title": "Teach API",
    "description": "Сервер-оркестратор мобильного репетитора Teach: мастер предмета, уроки, разбор, предзагрузка, оценка свободных ответов. Все маршруты, кроме /health и документации, требуют заголовок `Authorization: Bearer <token>`.",
    "version": "v1"
  },
  "servers": [
    {
      "url": "http://api.tuvedu.com/"
    }
  ],
  "paths": {
    "/health": {
      "get": {
        "tags": [
          "Служебные"
        ],
        "summary": "Состояние сервера: модель, провайдер (openrouter | anthropic | stub) и версия промптов. Без токена.",
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/app/latest": {
      "get": {
        "tags": [
          "Служебные"
        ],
        "summary": "Последняя сборка приложения и ссылка на обновление. Без токена.",
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/subjects/focus": {
      "post": {
        "tags": [
          "Мастер предмета"
        ],
        "summary": "Три варианта сужения темы для мастера (роль Focus).",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FocusRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/subjects/sources": {
      "post": {
        "tags": [
          "Мастер предмета"
        ],
        "summary": "Запустить поиск источников (web search, 30–120 с). Возвращает jobId для опроса.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SourcesRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/subjects/sources/{jobId}": {
      "get": {
        "tags": [
          "Мастер предмета"
        ],
        "summary": "Статус поиска источников: running | ready (items) | failed.",
        "parameters": [
          {
            "name": "jobId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/subjects/title": {
      "post": {
        "tags": [
          "Мастер предмета"
        ],
        "summary": "Короткое имя предмета до 24 символов из формулировки темы.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FocusRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/subjects/plan": {
      "post": {
        "tags": [
          "Мастер предмета"
        ],
        "summary": "План из этапов под тему, фокус и миссию.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PlanRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/subjects": {
      "post": {
        "tags": [
          "Предмет и уроки"
        ],
        "summary": "Создать предмет и запустить стартовую диагностику в фоне. Источники из мастера передаются в sources и больше не ищутся.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SubjectDraft"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      },
      "get": {
        "tags": [
          "Предмет и уроки"
        ],
        "summary": "Предметы текущего владельца токена: id, имя, номер урока и этап плана.",
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/subjects/{id}/lesson": {
      "get": {
        "tags": [
          "Предмет и уроки"
        ],
        "summary": "Статус подготовки: preparing (stage 0..2) | failed | ready с уроком, справочниками, этапом плана и флагами предзагрузки.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/sessions/{subjectId}/recap": {
      "post": {
        "tags": [
          "Предмет и уроки"
        ],
        "summary": "Записи разбора урока. Для предмета сервера решает этап плана и готовит следующий урок (из заготовки, если она подошла). Повторная отправка того же урока идемпотентна.",
        "parameters": [
          {
            "name": "subjectId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RecapRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/subjects/{id}/prefetch": {
      "post": {
        "tags": [
          "Предмет и уроки"
        ],
        "summary": "Заготовить следующий урок, пока идёт текущий. Ответ: queued | exists | running | skipped.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/subjects/{id}/retry": {
      "post": {
        "tags": [
          "Предмет и уроки"
        ],
        "summary": "Повторить подготовку урока после сбоя (failed → preparing). Ответ: preparing | ready; уже идущую подготовку не дублирует.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/grade/free": {
      "post": {
        "tags": [
          "Оценка"
        ],
        "summary": "Оценка свободного ответа по критериям моделью роли Grade: hits[] по каждому критерию.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/GradeRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/subjects/{id}/lessons": {
      "get": {
        "tags": [
          "Предмет и уроки"
        ],
        "summary": "Все уроки предмета по порядку номеров: для восстановления вопросов старых карточек повторов.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/subjects/{id}": {
      "delete": {
        "tags": [
          "Предмет и уроки"
        ],
        "summary": "Удалить свой предмет (мягко). Повтор — тоже 204; чужой и общий предмет — 404; общий токен — 403.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/subjects/{id}/mission": {
      "put": {
        "tags": [
          "План"
        ],
        "summary": "Миссия предмета с телефона (FR-33). Новый текст пересматривает этапы после текущего; тот же текст или старая правка — 204 без действий.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MissionUpdate"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/subjects/{id}/plan": {
      "put": {
        "tags": [
          "План"
        ],
        "summary": "Правка плана человеком (FR-33): текст текущего этапа и этапы впереди. Новые, переписанные и переставленные этапы закрепляются. 409 stale — план успел измениться.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PlanEdit"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/subjects/{id}/plan/skip": {
      "post": {
        "tags": [
          "План"
        ],
        "summary": "«Дальше» (FR-33): человек закрывает текущий этап; готовый урок уходит, план пересматривается, следующий урок — на новом этапе. 409: stale | preparing | diagnostic.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PlanBase"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/subjects/{id}/plan/revert": {
      "post": {
        "tags": [
          "План"
        ],
        "summary": "«Вернуть прежний план» (FR-33): этапы до последнего изменения моделью, закреплённые. Готовый урок не пересобирается. 409: stale | nothing.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PlanBase"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/subjects/{id}/plan/history": {
      "get": {
        "tags": [
          "План"
        ],
        "summary": "Версии плана предмета от новых к старым (FR-33): автор, причина, «почему», этапы.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/subjects/{id}/talks": {
      "post": {
        "tags": [
          "Разговор"
        ],
        "summary": "Начать разговор с бадди по предмету. Дайджест предмета собирается, если его ещё нет.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/talks": {
      "post": {
        "tags": [
          "Разговор"
        ],
        "summary": "Начать разговор без серверного предмета (демо): тема, фокус, миссия, этап, глоссарий и записи в теле.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TalkDraft"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/talks/{id}/turns": {
      "post": {
        "tags": [
          "Разговор"
        ],
        "summary": "Реплика ученика → реплика бадди по SSE (delta…, done). Пустой text — вступление бадди.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TurnRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/talks/{id}/end": {
      "post": {
        "tags": [
          "Разговор"
        ],
        "summary": "Завершить разговор и извлечь записи (FR-68): уверенно / путался, шаг для повтора у «путался». Повторный вызов отдаёт тот же итог; 409 — извлечение уже идёт.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/auth/apple": {
      "post": {
        "tags": [
          "Доступ"
        ],
        "summary": "Обменять Apple identityToken на токен сервера, привязанный к этому пользователю.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AppleSignInRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/admin/me": {
      "get": {
        "tags": [
          "Доступ"
        ],
        "summary": "Кто вошёл в админку и может ли он что-то менять (токен QA — только чтение).",
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/admin/talks/{id}": {
      "get": {
        "tags": [
          "Доступ"
        ],
        "summary": "Разговор с бадди целиком: окно модели, полная расшифровка, разбор.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/sync/cards": {
      "post": {
        "tags": [
          "Синхронизация"
        ],
        "summary": "Карточки повторов с телефона: побеждает более новая (updatedAt). До 500 за раз.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SyncCardsRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/sync/answers": {
      "post": {
        "tags": [
          "Синхронизация"
        ],
        "summary": "Ответы на повторы с телефона: только добавление, повтор по answerId пропускается. До 2000 за раз.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SyncAnswersRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/sync/state": {
      "put": {
        "tags": [
          "Синхронизация"
        ],
        "summary": "Состояние аккаунта (настройки приложения, предметов, демо): побеждает более новое. Объект до 256 КБ.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SyncStateDto"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/sync/known-subjects": {
      "post": {
        "tags": [
          "Синхронизация"
        ],
        "summary": "Разовая сверка: свои предметы, которых телефон не знает (удалены раньше только на телефоне), помечаются удалёнными. Созданные за последние 5 минут не трогаются.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/KnownSubjectsRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/sync/restore": {
      "get": {
        "tags": [
          "Синхронизация"
        ],
        "summary": "Всё для нового телефона: живые свои предметы (и общие из своего состояния) с планом, уроком и справочниками, карточки (без удалённых предметов), ответы, состояние.",
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/admin/tokens": {
      "post": {
        "tags": [
          "Доступ"
        ],
        "summary": "Выдать именной токен тестировщику. Значение показывается один раз. console=true — токен QA: ещё и админка на чтение.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NewTesterTokenRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      },
      "get": {
        "tags": [
          "Доступ"
        ],
        "summary": "Список выданных токенов без самих значений: кто, когда создан, когда использовался, отозван ли.",
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/admin/tokens/{id}": {
      "delete": {
        "tags": [
          "Доступ"
        ],
        "summary": "Отозвать токен: он перестаёт работать немедленно, остальные не трогаются.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/admin/generations/summary": {
      "get": {
        "tags": [
          "Модели"
        ],
        "summary": "Сводка журнала генераций за days дней (по умолчанию 14): по ролям и моделям — вызовы, ошибки, запасной путь, формат, задержки p50/p95, токены, цена; для уроков — качество; profile — имя профиля или default (общая раскладка).",
        "parameters": [
          {
            "name": "days",
            "in": "query",
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)$",
              "type": [
                "integer",
                "string"
              ],
              "format": "int32"
            }
          },
          {
            "name": "profile",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/admin/generations": {
      "get": {
        "tags": [
          "Модели"
        ],
        "summary": "Последние строки журнала генераций; фильтры role, model, profile (имя или default), subject (id предмета) и ok (true | false), limit до 500.",
        "parameters": [
          {
            "name": "role",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "model",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "profile",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "subject",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "ok",
            "in": "query",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)$",
              "type": [
                "integer",
                "string"
              ],
              "format": "int32"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/admin/models": {
      "get": {
        "tags": [
          "Модели"
        ],
        "summary": "Модель, транспорт, лимит вывода, источник (default | profile:<имя>) и предупреждения каталога по каждой роли; owner — что получит этот владелец.",
        "parameters": [
          {
            "name": "owner",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/admin/overview": {
      "get": {
        "tags": [
          "Доступ"
        ],
        "summary": "Пульс за days дней (по умолчанию 7, 1..90): активные и новые аккаунты, уроки, разговоры, вызовы модели и цена по ролям, сбойные и зависшие предметы, последняя сборка.",
        "parameters": [
          {
            "name": "days",
            "in": "query",
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)$",
              "type": [
                "integer",
                "string"
              ],
              "format": "int32"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/admin/app/latest": {
      "get": {
        "tags": [
          "Доступ"
        ],
        "summary": "Номер последней сборки iOS: откуда он взят и что в конфигурации. Только админ.",
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      },
      "put": {
        "tags": [
          "Доступ"
        ],
        "summary": "Задать номер последней сборки iOS (0…1000000); действует сразу, без рестарта. Только админ.",
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      },
      "delete": {
        "tags": [
          "Доступ"
        ],
        "summary": "Сбросить номер из базы: дальше берётся из конфигурации. Только админ.",
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/admin/profiles": {
      "get": {
        "tags": [
          "Модели"
        ],
        "summary": "Профили моделей: переопределённые роли, заметка, назначенные владельцы.",
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/admin/profiles/{name}": {
      "put": {
        "tags": [
          "Модели"
        ],
        "summary": "Создать или заменить профиль: provider — openrouter | anthropic (без него — провайдер сервера), models — роль → id модели (только переопределённые роли), note — заметка. 400 со списком ошибок.",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "required": [
                  "models",
                  "note"
                ],
                "type": "object",
                "properties": {
                  "models": {
                    "type": [
                      "null",
                      "object"
                    ],
                    "additionalProperties": {
                      "type": "string"
                    }
                  },
                  "note": {
                    "type": [
                      "null",
                      "string"
                    ]
                  },
                  "provider": {
                    "type": [
                      "null",
                      "string"
                    ]
                  }
                }
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      },
      "delete": {
        "tags": [
          "Модели"
        ],
        "summary": "Удалить профиль. Если он назначен — 409 со списком владельцев.",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/admin/profiles/{name}/owners/{owner}": {
      "put": {
        "tags": [
          "Модели"
        ],
        "summary": "Назначить профиль владельцу (tester:<имя> или apple:<sub>); прежнее назначение заменяется.",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "owner",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/admin/profile-owners/{owner}": {
      "delete": {
        "tags": [
          "Модели"
        ],
        "summary": "Снять назначение: владелец возвращается на общую раскладку.",
        "parameters": [
          {
            "name": "owner",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/admin/subjects": {
      "get": {
        "tags": [
          "Доступ"
        ],
        "summary": "Предметы владельца (owner — tester:<имя> или apple:<sub>) по дате создания; без owner — последние предметы всех (limit, по умолчанию 50, не больше 200).",
        "parameters": [
          {
            "name": "owner",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)$",
              "type": [
                "integer",
                "string"
              ],
              "format": "int32"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/admin/subjects/{id}": {
      "get": {
        "tags": [
          "Доступ"
        ],
        "summary": "Предмет изнутри: поля, план и его версии, уроки (без содержимого), записи, справочники, разговоры (без реплик), журнал генераций.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      },
      "delete": {
        "tags": [
          "Доступ"
        ],
        "summary": "Удалить предмет вместе с уроками, записями, справочниками и разговорами. Журнал генераций остаётся.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/admin/subjects/{id}/lessons/{number}": {
      "get": {
        "tags": [
          "Доступ"
        ],
        "summary": "Урок предмета целиком (шаги, ответы) и строка журнала, которая его породила.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "number",
            "in": "path",
            "required": true,
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)$",
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/admin/subjects/{id}/undelete": {
      "post": {
        "tags": [
          "Доступ"
        ],
        "summary": "Вернуть предмет, который пользователь удалил (мягкое удаление, FR-44): снова виден владельцу.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/admin/accounts": {
      "get": {
        "tags": [
          "Доступ"
        ],
        "summary": "Аккаунты: владельцы из токенов, предметов и синхронизации; q — подстрока владельца, limit 1..500 (по умолчанию 100); недавно активные выше.",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)$",
              "type": [
                "integer",
                "string"
              ],
              "format": "int32"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/admin/accounts/{owner}": {
      "get": {
        "tags": [
          "Доступ"
        ],
        "summary": "Карточка аккаунта: токены, профиль, все предметы (и удалённые) и сводка повторов. В owner %2F заменяется на «/».",
        "parameters": [
          {
            "name": "owner",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "AheadStage": {
        "required": [
          "id",
          "t",
          "d"
        ],
        "type": "object",
        "properties": {
          "id": {
            "type": [
              "null",
              "string"
            ]
          },
          "t": {
            "type": "string"
          },
          "d": {
            "type": "string"
          }
        }
      },
      "AppleSignInRequest": {
        "required": [
          "identityToken"
        ],
        "type": "object",
        "properties": {
          "identityToken": {
            "type": "string"
          }
        }
      },
      "Criterion": {
        "required": [
          "t",
          "keys"
        ],
        "type": "object",
        "properties": {
          "t": {
            "type": "string"
          },
          "keys": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "FocusRequest": {
        "required": [
          "topic"
        ],
        "type": "object",
        "properties": {
          "topic": {
            "type": "string"
          }
        }
      },
      "GradeRequest": {
        "required": [
          "criteria",
          "text",
          "lang"
        ],
        "type": "object",
        "properties": {
          "criteria": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Criterion"
            }
          },
          "text": {
            "type": "string"
          },
          "lang": {
            "type": "string"
          }
        }
      },
      "JsonElement": { },
      "KnownSubjectsRequest": {
        "required": [
          "subjectIds"
        ],
        "type": "object",
        "properties": {
          "subjectIds": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "MissionUpdate": {
        "required": [
          "text",
          "editedAt"
        ],
        "type": "object",
        "properties": {
          "text": {
            "type": "string"
          },
          "editedAt": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "format": "int64"
          }
        }
      },
      "NewTesterTokenRequest": {
        "required": [
          "name"
        ],
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "console": {
            "type": "boolean",
            "default": false
          }
        }
      },
      "PlanBase": {
        "required": [
          "baseVersion"
        ],
        "type": "object",
        "properties": {
          "baseVersion": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "format": "int32"
          }
        }
      },
      "PlanEdit": {
        "required": [
          "baseVersion",
          "current",
          "ahead"
        ],
        "type": "object",
        "properties": {
          "baseVersion": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "format": "int32"
          },
          "current": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/StageText"
              }
            ]
          },
          "ahead": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AheadStage"
            }
          }
        }
      },
      "PlanRequest": {
        "required": [
          "topic",
          "focus",
          "mission"
        ],
        "type": "object",
        "properties": {
          "topic": {
            "type": "string"
          },
          "focus": {
            "type": "string"
          },
          "mission": {
            "type": "string"
          }
        }
      },
      "PlanStage": {
        "required": [
          "n",
          "t",
          "d"
        ],
        "type": "object",
        "properties": {
          "n": {
            "type": "string"
          },
          "t": {
            "type": "string"
          },
          "d": {
            "type": "string"
          },
          "id": {
            "type": [
              "null",
              "string"
            ]
          },
          "by": {
            "type": [
              "null",
              "string"
            ]
          }
        }
      },
      "RecapRecord": {
        "required": [
          "title",
          "note",
          "ok",
          "stepIndex"
        ],
        "type": "object",
        "properties": {
          "title": {
            "type": "string"
          },
          "note": {
            "type": "string"
          },
          "ok": {
            "type": "boolean"
          },
          "stepIndex": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "format": "int32"
          },
          "lessonNumber": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "format": "int32",
            "default": 0
          },
          "origin": {
            "type": "string",
            "default": "lesson"
          }
        }
      },
      "RecapRequest": {
        "required": [
          "records"
        ],
        "type": "object",
        "properties": {
          "records": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/RecapRecord"
            }
          },
          "durationMinutes": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "null",
              "integer",
              "string"
            ],
            "format": "int32"
          }
        }
      },
      "RefLinkDto": {
        "required": [
          "lesson",
          "step"
        ],
        "type": "object",
        "properties": {
          "lesson": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "format": "int32"
          },
          "step": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "format": "int32"
          }
        }
      },
      "RefRowDto": {
        "required": [
          "k",
          "v"
        ],
        "type": "object",
        "properties": {
          "k": {
            "type": "string"
          },
          "v": {
            "type": "string"
          },
          "sec": {
            "type": [
              "null",
              "string"
            ]
          },
          "links": {
            "type": [
              "null",
              "array"
            ],
            "items": {
              "$ref": "#/components/schemas/RefLinkDto"
            }
          }
        }
      },
      "SourceCandidate": {
        "required": [
          "id",
          "t",
          "m",
          "trust"
        ],
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "t": {
            "type": "string"
          },
          "m": {
            "type": "string"
          },
          "trust": {
            "type": "string"
          }
        }
      },
      "SourcesRequest": {
        "required": [
          "topic",
          "focus"
        ],
        "type": "object",
        "properties": {
          "topic": {
            "type": "string"
          },
          "focus": {
            "type": "string"
          }
        }
      },
      "StageText": {
        "required": [
          "t",
          "d"
        ],
        "type": "object",
        "properties": {
          "t": {
            "type": "string"
          },
          "d": {
            "type": "string"
          }
        }
      },
      "SubjectDraft": {
        "required": [
          "topic",
          "focus",
          "mission",
          "sourceIds"
        ],
        "type": "object",
        "properties": {
          "topic": {
            "type": "string"
          },
          "focus": {
            "type": "string"
          },
          "mission": {
            "type": "string"
          },
          "sourceIds": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "sources": {
            "type": [
              "null",
              "array"
            ],
            "items": {
              "$ref": "#/components/schemas/SourceCandidate"
            }
          },
          "title": {
            "type": [
              "null",
              "string"
            ]
          },
          "plan": {
            "type": [
              "null",
              "array"
            ],
            "items": {
              "$ref": "#/components/schemas/PlanStage"
            }
          },
          "durationMinutes": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "null",
              "integer",
              "string"
            ],
            "format": "int32"
          }
        }
      },
      "SyncAnswerDto": {
        "required": [
          "answerId",
          "cardId",
          "rating",
          "state",
          "due",
          "stability",
          "difficulty",
          "elapsedDays",
          "lastElapsedDays",
          "scheduledDays",
          "learningSteps",
          "reviewedAt"
        ],
        "type": "object",
        "properties": {
          "answerId": {
            "type": "string"
          },
          "cardId": {
            "type": "string"
          },
          "rating": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "format": "int32"
          },
          "state": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "format": "int32"
          },
          "due": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "format": "int64"
          },
          "stability": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?(?:[eE][+-]?\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "format": "double"
          },
          "difficulty": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?(?:[eE][+-]?\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "format": "double"
          },
          "elapsedDays": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "format": "int32"
          },
          "lastElapsedDays": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "format": "int32"
          },
          "scheduledDays": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "format": "int32"
          },
          "learningSteps": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "format": "int32"
          },
          "reviewedAt": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "format": "int64"
          }
        }
      },
      "SyncAnswersRequest": {
        "required": [
          "answers"
        ],
        "type": "object",
        "properties": {
          "answers": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SyncAnswerDto"
            }
          }
        }
      },
      "SyncCardDto": {
        "required": [
          "cardId",
          "subjectKey",
          "subjectName",
          "title",
          "note",
          "source",
          "ref",
          "fsrs",
          "createdAt",
          "updatedAt"
        ],
        "type": "object",
        "properties": {
          "cardId": {
            "type": "string"
          },
          "subjectKey": {
            "type": "string"
          },
          "subjectName": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "note": {
            "type": "string"
          },
          "source": {
            "type": "string"
          },
          "ref": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/JsonElement"
              }
            ]
          },
          "fsrs": {
            "$ref": "#/components/schemas/JsonElement"
          },
          "createdAt": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "format": "int64"
          },
          "updatedAt": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "format": "int64"
          },
          "miss": {
            "type": [
              "null",
              "boolean"
            ]
          }
        }
      },
      "SyncCardsRequest": {
        "required": [
          "cards"
        ],
        "type": "object",
        "properties": {
          "cards": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SyncCardDto"
            }
          }
        }
      },
      "SyncStateDto": {
        "required": [
          "json",
          "updatedAt"
        ],
        "type": "object",
        "properties": {
          "json": {
            "$ref": "#/components/schemas/JsonElement"
          },
          "updatedAt": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "format": "int64"
          }
        }
      },
      "TalkDraft": {
        "required": [
          "topic",
          "focus",
          "mission",
          "title",
          "stage"
        ],
        "type": "object",
        "properties": {
          "topic": {
            "type": "string"
          },
          "focus": {
            "type": "string"
          },
          "mission": {
            "type": "string"
          },
          "title": {
            "type": [
              "null",
              "string"
            ]
          },
          "stage": {
            "$ref": "#/components/schemas/PlanStage"
          },
          "glossary": {
            "type": [
              "null",
              "array"
            ],
            "items": {
              "$ref": "#/components/schemas/RefRowDto"
            }
          },
          "records": {
            "type": [
              "null",
              "array"
            ],
            "items": {
              "$ref": "#/components/schemas/RecapRecord"
            }
          }
        }
      },
      "TurnRequest": {
        "required": [
          "text"
        ],
        "type": "object",
        "properties": {
          "text": {
            "type": "string"
          },
          "voiceLang": {
            "type": [
              "null",
              "string"
            ]
          }
        }
      }
    },
    "securitySchemes": {
      "bearer": {
        "type": "http",
        "description": "Общий токен сервера (Teach:ApiToken)",
        "scheme": "bearer"
      }
    }
  },
  "security": [
    {
      "bearer": [ ]
    }
  ],
  "tags": [
    {
      "name": "Служебные"
    },
    {
      "name": "Мастер предмета"
    },
    {
      "name": "Предмет и уроки"
    },
    {
      "name": "Оценка"
    },
    {
      "name": "План"
    },
    {
      "name": "Разговор"
    },
    {
      "name": "Доступ"
    },
    {
      "name": "Синхронизация"
    },
    {
      "name": "Модели"
    }
  ]
}