VeriDepth API Catalog

http://192.168.224.11/VeriDepthServer  ·  БД: VeriDepthDB @ 192.168.224.10
Аутентификация

Все запросы требуют заголовок Authorization: Bearer <токен>

Токены хранятся в src/Presentation.ViewModels/ServerApiCredentials.cs

КонстантаПрефиксПрава
CalibrationBearercal_Чтение и запись калибровок
TrainingEvidenceBearertek_Загрузка captures, records
TrainingEvidenceInventoryBearerteinv_Чтение, листинг, summary
DistributionBearerdist-Загрузка дистрибутивов

Флаги DB: CanUploadTrainingEvidence, CanHardDeleteTrainingEvidence, CanUploadDistributions. Превышение лимита → 429.

Calibration API  /api/calibrations/v1
GET /api/calibrations/v1/devices/{serial}/active Активный профиль калибровки

Возвращает полный активный профиль (включая profileJson) для серийного номера прибора.

ПутьОписание
serial*Серийный номер прибора (напр. 0454628)
200: { "schemaVersion":"calibration-profile/v1", "calibrationId":"cal-...", "deviceSerial":"0454628",
       "computedAt":"2026-06-18T01:44:53Z", "rmsStereoPx":0.803, "baselineMm":3.556,
       "k5Passed":true, "isRecommendedActive":true, "profileJson":{...} }
404: { "errorCode":"calibration_not_found" }
GET /api/calibrations/v1/devices/by-eeprom/{eepromSha}/active Активный профиль по SHA заводской EEPROM

Поиск калибровки по SHA заводской EEPROM прибора. Используется клиентом, когда серийник из EEPROM невалиден (например, прибор 1/11 репортит серийник "11" — номер модели, не уникальный).
Формат eepromSha: 64 hex-символа нижнего регистра, SHA-256 от строки "<block1_hex>\r\n<block2_hex>" (два 80-байтных EEPROM-блока, формат legacy_eeprom_block_pair).
Таблица соответствий: dbo.DeviceEepromKey. Текущие записи: прибор 0454628 (sha 00585a32…).

200: полный active-профиль (тот же формат что /devices/{serial}/active)
404: { "errorCode":"calibration_not_found" }  — нет маппинга или нет active-калибровки
422: { "errorCode":"calibration_schema_invalid" }  — eepromSha не 64 hex

Б-044 / 2026-07-01. Клиентский резолвер: если /devices/{serial}/active → 404, вычислить eepromSha и обратиться к этому эндпоинту.

GET /api/calibrations/v1/devices/by-eeprom/{eepromSha}/identity Личность прибора по SHA EEPROM (без калибровки)

Возвращает серийный номер прибора по SHA заводской EEPROM, БЕЗ требования наличия активной калибровки — в отличие от /active, отвечает 200 даже если калибровки ещё нет. Используется для онбординга нового прибора (клиент узнаёт свой серийник по хэшу EEPROM до первой калибровки).
Формат eepromSha: тот же, что у /active (64 hex нижнего регистра).

200: { "deviceSerial":"0454628", "eepromSha":"00585a32...", "createdAt":"2026-07-01T12:46:55Z",
       "canonicalEepromSha":"...", "hashConvention":"sha256(bytes-canonical(left,right))/v1",
       "enrolment": { "romReceivedAtUtc":"2026-08-27T19:13:04Z",
                      "identityRegisteredAtUtc":"2026-07-01T12:46:55Z",
                      "activeProfileBoundAtUtc":"2026-08-18T06:02:23Z",
                      "activeProfileBoundAtSource":"activation" } }
404: { "errorCode":"eeprom_key_not_found",
       "message":"... Read the serial number printed on the device label and register it ...",
       "deviceSerialRequiredFrom":"printed_label_or_operator",
       "registerRoute":"POST /devices/by-eeprom/{eepromSha}/register",
       "enrolment": { "romReceivedAtUtc":"2026-08-27T19:13:04Z",
                      "identityRegisteredAtUtc":null, "activeProfileBoundAtUtc":null } }
422: { "errorCode":"calibration_schema_invalid" }  — eepromSha не 64 hex

Задача 1824 (закон Ф-11). Блок enrolment отвечает на три ОТДЕЛЬНЫХ вопроса, потому что они расходятся: ПЗУ прибора может лежать на сервере, а личности и профиля не быть — ровно так живёт прибор 32eb0ee7… с 27.08. Одно слово «известен» это состояние не описывает и отправляет инженера чинить работающую загрузку.
Серийного номера НЕТ внутри прибора. Полный скан 32 КиБ (#1823) серийника не содержит: DeviceId равен 1 у живого прибора и 11 у 0454628, и это не печатный номер. Поэтому 404 не «ошибка сервера», а требование второго шага: прочитать номер с наклейки и вызвать /register. Тело 404 говорит это словами оператора и называет маршрут.

B18 / 2026-07-07; enrolment и текст 404 — задача 1824 / 2026-09-05.

POST /api/calibrations/v1/devices/by-eeprom/{eepromSha}/register Регистрация серийника прибора по SHA EEPROM

Сохраняет соответствие SHA заводской EEPROM → серийный номер прибора (заполняет dbo.DeviceEepromKey). Используется, когда хэш ещё неизвестен серверу: пользователь вводит серийник вручную, клиент регистрирует пару, дальше /identity и /active резолвят её автоматически.
Идемпотентно: повторная регистрация того же хэша с тем же серийником → 200. С ДРУГИМ серийником → 409 (перезапись запрещена, конфликт). Серийник, уже закреплённый за ДРУГИМ прибором (другой канонический SHA; устаревшие хэши того же прибора, помеченные SupersededByEepromSha, не считаются другим прибором) → 409 serial_owned_by_other_rom (28.09): один серийник — один прибор, иначе оба получили бы одну калибровку.

Поле телаОписание
deviceSerial*Серийный номер прибора (ASCII, до 250 симв.). Источник — только ВНЕ прибора: печатная наклейка или ввод оператора. В памяти прибора серийника нет (#1823, полный скан 32 КиБ), поэтому сервер его не выводит и не угадывает.
noteОпциональная заметка (до 500 симв.)
201: { "deviceSerial":"0454628", "eepromSha":"00585a32...", "createdAt":"...", "alreadyRegistered":false }
200: { "deviceSerial":"0454628", "eepromSha":"...", "alreadyRegistered":true }  — уже была та же пара
409: { "errorCode":"eeprom_key_conflict", "deviceSerial":"<существующий>" }  — хэш занят ДРУГИМ серийником
409: { "errorCode":"serial_owned_by_other_rom", "deviceSerial":"<запрошенный>", "details":"ownerEepromSha=<канонический SHA владельца>" }  — серийник уже принадлежит ДРУГОМУ прибору (другой канонический SHA); ничего не записано
422: { "errorCode":"calibration_schema_invalid" }  — eepromSha не 64 hex, либо deviceSerial отсутствует/невалиден

B18 / 2026-07-07. Валидация серийника против ERP (ItemSn) НЕ выполняется — VeriDepthDB изолирована от IntekDB по Б-044.

POST /api/calibrations/v1/devices/by-eeprom/{eepromSha}/factory-rom Заводское ПЗУ прибора (160 байт) и заводской баланс камер

Сохраняет два 80-байтных блока «окна личности» под их SHA-256. Идемпотентно по содержимому: те же байты → 200 isNew:false, ничего не пишется; ДРУГИЕ байты под тем же хэшем → 409.

Поле телаОписание
romBase64*Байты ПЗУ в канонном порядке
sha256*SHA-256 этих байтов; должен совпасть и с путём, и с содержимым
crcValidСошлась ли CRC при чтении
deviceModelДо 128 символов
appVersionДо 128 символов (было 64 до задачи 1788 — строка сборки продукта в 81 символ девять раз получала 422)
whiteBalanceНеобязательный массив квадов по камерам (задача 1824). Продукт, который его не шлёт, кладётся как раньше — это проверяется тестом на точном запросе 1782. Поля элемента: did (256/257 у 1/11), r, gr, gb, b (0..255 либо null), source (rom | vendor_awb | white_sheet), readAtUtc, romOffset (у 1/11 "0x01D0").

null значит «не прочитано» и никогда «нули». У левой камеры живого прибора синий множитель действительно равен 0 (99, 21, 17, 0) — это измерение, а не отсутствие данных, и сервер эти два состояния различает.

201/200: { "eepromSha":"...", "romSize":160, "storedAt":"...", "isNew":true|false, "whiteBalanceStored":2 }
409: { "errorCode":"factory_rom_mismatch" }  — под этим хэшем уже лежат ДРУГИЕ байты
422: { "errorCode":"calibration_schema_invalid" | "rom_sha_path_mismatch" | "rom_sha_mismatch" | "rom_payload_invalid" }

whiteBalance — задача 1824 / 2026-09-05 (закон Ф-11, предложение #1823). Хэш личности FactoryRomIdentity НЕ меняется: он считается по двум 80-байтным блокам и только по ним, иначе добавление поля переименовало бы каждый прибор в архиве.

GET /api/calibrations/v1/devices/by-eeprom/{eepromSha}/factory-rom Что сервер знает о заводской памяти прибора
200: { "uploadId":32, "eepromSha":"...", "romSize":160, "crcValid":true,
       "algorithm":"sha256(left||right)/v1", "receivedAtUtc":"2026-08-27T19:13:04Z",
       "canonicalEepromSha":"...", "hashConvention":null,
       "whiteBalance":[ { "did":256, "r":99, "gr":21, "gb":17, "b":0,
                          "source":"rom", "readAtUtc":"...", "romOffset":"0x01D0" } ],
       "enrolment": { "romReceivedAtUtc":"...", "identityRegisteredAtUtc":null,
                      "activeProfileBoundAtUtc":null, "activeProfileBoundAtSource":null } }
404: { "errorCode":"factory_rom_not_found" }

Поля whiteBalance и enrolment добавлены задачей 1824; всё, что было раньше, сохранило имя и смысл.

POST /api/calibrations/v1/devices/by-eeprom/{eepromSha}/memory-dumps Полный дамп памяти камеры (не 160 байт, а все 32 КиБ)

Закон Ф-11: всё, что лежит в памяти прибора, должно быть записано в систему. Строка ПЗУ описывает 160-байтное «окно личности»; сюда кладётся ВСЯ память, по камерам (did 256/257 — у каждой камеры своя). Байты и маска хранятся в общем blob-хранилище и адресуются своим SHA-256, поэтому повторная отправка того же дампа ничего не дублирует (200 isNew:false).

Поле телаОписание
did*Камера: 256 / 257
dumpBase64*Байты дампа
sha256*SHA-256 дампа; расхождение → 422 memory_dump_sha_mismatch
sizeBytes*Длина дампа; расхождение → 422 memory_dump_size_mismatch
readMaskBase64*Обязательна. По байту на адрес: прочитан / нет. Без маски «прочитано 65536 из 65536» — утверждение о нашем цикле, а не о приборе: непрочитанный байт неотличим от стёртого 0xFF. Ровно этот дефект чинила #1779. Длина должна совпадать с дампом.
mirrorPeriod*Ключ обязателен, значение может быть null (зеркала не найдено). Полное чтение НЕ доказывает объём памяти: маленькая EEPROM отвечает на старшие адреса копией младших и рапортует успех на каждом.
readAtUtc*Когда снято
readerVersion*Чем снято, до 256 символов (напр. Capture.Cli camlh-dump-eeprom @ 8da5b0e9)
201/200: { "eepromSha":"...", "did":256, "sha256":"...", "sizeBytes":32768,
           "blobRef":"...", "readMaskRef":"...", "mirrorPeriod":32768, "isNew":true|false }
422: { "errorCode":"read_mask_required" | "mirror_period_required" | "read_mask_length_mismatch" |
       "memory_dump_sha_mismatch" | "memory_dump_size_mismatch" | "memory_dump_payload_invalid" }

Задача 1824 / 2026-09-05. Снимать по команде, а не при каждом подключении: 65 536 чтений регистров занимают минуты и держат камеру, а держатель может быть только один.

GET /api/calibrations/v1/devices/by-eeprom/{eepromSha}/memory-dumps Список дампов памяти прибора (по свежему на камеру)

Только метаданные: сами байты остаются в хранилище и названы через blobRef / readMaskRef, чтобы список не тащил по 64 КиБ на камеру.

200: { "eepromSha":"...", "memoryDumps":[
         { "did":256, "sha256":"...", "sizeBytes":32768, "blobRef":"...", "readMaskRef":"...",
           "mirrorPeriod":32768, "readAtUtc":"...", "readerVersion":"...", "receivedAtUtc":"..." } ] }
GET /api/calibrations/v1/health Жив ли сервис калибровок

Единственный маршрут без ключа. Используется деплоем (tools/Deploy-VeriDepthServer.ps1, шаг 7) как признак того, что приложение поднялось после копирования файлов.

200: {"status":"ok"}
GET /api/calibrations/v1/factory-roms Опись заводских ПЗУ (метаданные, без байтов)

Список всех записей ПЗУ: uploadId, eepromSha, romSize, crcValid, algorithm, deviceModel, appVersion, receivedAtUtc. Сами байты не отдаются — за ними в /devices/by-eeprom/{eepromSha}/factory-rom.

200: { "factoryRoms":[ { "uploadId":32, "eepromSha":"...", "romSize":160, ... } ] }
POST /api/calibrations/v1/devices/{serial}/calibrations/{calibrationId}/session-archive Архив сессии калибровки (ZIP) к существующему профилю

Тело — application/zip. Профиль {calibrationId} должен существовать и не быть удалён, иначе архив прицепился бы к удалённой записи. Хранится в dbo.ApiBinaryBlob со scope архивов; SHA-256 считается по принятым байтам.

201: { "calibrationId":"cal-...", "storedAt":"...", "isActive":false }
404: профиль не найден   422: неверный Content-Type или пустое тело
GET /api/training-evidence/v1/captures/{sha256}/metadata Строка съёмки как JSON — чтение обратно после загрузки

Изделие вызывает этот маршрут сразу после POST /captures, чтобы убедиться, что загрузка легла (PrototypeTrainingEvidenceUploader.VerifyCaptureAsync). Ему достаточно 2xx, тело которого СОДЕРЖИТ этот же sha, плюс необязательная строка formatVersion.

Съёмка, у которой пропал blob, всё равно отвечает 200 с hasBlob:false. Вопрос маршрута — «легла ли моя загрузка», а строка и есть загрузка; 404 здесь воспроизводил бы ровно ту ложную неудачу, ради которой маршрут и заведён (задача 1796: 574 вызова с 404 с 12 июня, пока маршрут не существовал).

200: { "sha256":"...", "sizeBytes":2624721, "contentType":"image/png",
       "formatVersion":"training-evidence-capture/png/v1", "sourceClass":null,
       "deviceId":"...", "calibrationRef":"...", "hasBlob":true }
404: { "errorCode":"capture_not_found" }

Задача 1796 / 2026-09-05.

POST /api/training-evidence/v1/captures/{sha256}/reingest Поправить метаданные уже сохранённой съёмки

Изделие вызывает его после измерения, чтобы приписать к съёмке описание и качество (TrainingEvidenceMeasurementSync.PostReingestAsync). Требует ключ с правом CanUploadTrainingEvidence.

Поле телаОписание
dryRunПо умолчанию true: кто не сказал иначе, получает предпросмотр и НИКАКОЙ записи. Изделие шлёт false явно.
formatVersionДо 80 символов; если прислан, formatVersionSource становится manual_reingest
description / notesДо 2000 символов
qualityЧисло
deviceId, hardwareDeviceId, calibrationRef, sourceClass, sourceCreatedAtUtcТоже принимаются; поле, которого нет в теле, сохраняет текущее значение (COALESCE)

applied означает, что строка изменилась, а не что мы не были в режиме предпросмотра. Соседнее приложение на этом же сервере отвечает applied = Not dryRun — и 20–21 июня выдало 2008 «успехов», не изменив ни одной строки (его база стояла с 12 июня). Здесь applied истинно только когда UPDATE прошёл и затронул строку, а rowsUpdated называет их число.

Порядок отказов: 403 раньше 400. Право ключа проверяется ПЕРВЫМ, до формата sha256. Значит ключ без CanUploadTrainingEvidence получит 403 даже на заведомо кривом хэше — и клиент, отлаживающий хэш не тем ключом, услышит жалобу не на то. Поведение оставлено как есть (так же устроен обработчик, у которого прочитана форма), но теперь об этом написано: увидели 403 — сначала проверьте ключ, а не съёмку.

200: { "sha256":"...", "dryRun":false, "applied":true, "rowsUpdated":1,
       "changes":{ "description":{"from":"...","to":"..."} },
       "before":{...}, "after":{...} }
403: нет права   400: плохой sha, неверный JSON, слишком длинное поле   404: съёмки нет

Задача 1861 / 2026-09-05. Форма прочитана у соседнего приложения (T1860), а не угадана по клиенту.

POST /api/live-sessions/v1 Создать сессию живого просмотра

Возвращает идентификатор сессии и токены; по ним работают маршруты /{sessionId}/publish и /{sessionId}/watch (рукопожатия WebSocket, авторизуются токеном в URL, а не ключом — поэтому в каталоге их нет).

201: { "sessionId":"...", "publishUrl":"...", "watchUrl":"...", "viewerUrl":"..." }
GET /api/live-sessions/v1 Список сессий этого ключа
200: { "sessions":[ { "sessionId":"...", "state":"...", "createdAt":"..." } ] }
GET /api/live-sessions/v1/{sessionId} Состояние сессии
200: { "sessionId":"...", "state":"...", "publisherConnected":true, "viewers":1 }
404: сессия не найдена
DELETE /api/live-sessions/v1/{sessionId} Остановить сессию

То же, что POST /{sessionId}/stop — два способа сказать одно и то же, оба поддерживаются.

200: { "sessionId":"...", "state":"stopped" }
POST /api/live-sessions/v1/{sessionId}/stop Остановить сессию (POST-вариант)
200: { "sessionId":"...", "state":"stopped" }
GET /api/reconstruction/v1/jobs Сводка очереди восстановления 3D

С задачи 1897 все маршруты реконструкции требуют ключ — тот же, что и загрузка съёмок. Нет ключа или он не живой → 401 unauthorized; живой ключ без права CanUploadTrainingEvidence → 403 forbidden. Проверено на живом сервере 06.09.2026: без ключа 401, ключ калибровок 403, ключ обучающих данных 200.

Ключа «только на чтение» не существует. Право одно — то же, что разрешает загружать съёмки, поэтому потребитель, которому нужны только облака, не может обойтись без права загрузки. Это осознанное состояние, а не упущение: отдельная возможность будет заведена тогда, когда появится такой потребитель, и не раньше (решение 1913). Оператор к этому не относится — страница Viewer3D.aspx сама читает модель из базы и ключа не требует.

200: { "jobs":[ { "status":"done", "count":619 } ] }
POST /api/reconstruction/v1/jobs/{sha256} Поставить съёмку в очередь на восстановление
201/200: { "jobId":123, "status":"pending" }
GET /api/reconstruction/v1/jobs/{sha256} Состояние задания по хэшу съёмки
200: { "jobId":123, "status":"done|pending|running|failed", "errorMessage":null }
404: задания нет
GET /api/reconstruction/v1/jobs/{sha256}/result Скачать облако точек (GLB)

Отдаёт model/gltf-binary из dbo.ReconstructionResult.ResultBlob — это единственное место, где хранится результат; в dbo.ApiBinaryBlob его нет.

GET /api/reconstruction/v1/jobs/{sha256}/measurements Точки измерений в 3D для страницы просмотра

Читается страницей Viewer3D.aspx. Отдаёт MeasurementPoints3dJson, посчитанный воркером; endpoints3d — те же концы отрезка, что на снимке, но в миллиметрах.

200: [ { "endpoints3d":[{"x":0.658,"y":-2.806,"z":10.736}, ...], "computedValueMm":null } ]
GET /api/calibrations/v1/devices/{serial}/calibrations История калибровок

Список всех калибровок прибора (метаданные, без profileJson). Используется клиентом для версионного резолва.

200: { "deviceSerial":"0454628", "calibrations":[
         { "calibrationId":"cal-...", "isActive":true, "uploadedAt":"...", "rmsStereoPx":0.803, "baselineMm":3.556 }
       ] }
POST /api/calibrations/v1/devices/{serial}/calibrations Загрузить новую калибровку
ЗаголовокЗначение
Content-Type*application/json
X-Content-SHA256*SHA-256 hex тела запроса

Тело — JSON схемы calibration-profile/v1. Обязательные поля: schemaVersion, calibrationId, deviceSerial, rmsStereoPx, baselineMm, k5Passed, profileJson.

201: { "calibrationId":"cal-...", "storedAt":"...", "isActive":true }
409: { "errorCode":"calibration_already_exists" }
409: { "errorCode":"device_not_registered", "deviceSerial":"..." }  — серийник не найден в реестре приборов IntekDB.ItemSn
422: { "errorCode":"calibration_schema_invalid", "details":"..." }

ItemSnID резолвится автоматически из IntekDB.dbo.ItemSn по DeviceSerial (кросс-БД, один сервер) — ручная регистрация в ItemSnCalibration не нужна, если прибор уже стоит на учёте в ERP.

DELETE /api/calibrations/v1/devices/{serial}/calibrations/{calibrationId} Soft-delete калибровки

Помечает калибровку удалённой (IsDeleted=1). Данные сохраняются в БД.

200: { "deleted":true }
404: { "errorCode":"calibration_not_found" }
Training Evidence API  /api/training-evidence/v1
POST /api/training-evidence/v1/captures Загрузить PNG-захват
ЗаголовокОписание
Content-Type*image/png
X-Content-SHA256*SHA-256 hex тела (64 символа)
X-Artifact-UidСтабильный UID артефакта (опц.)

Тело — бинарный PNG. Сервер извлекает DeviceId, CalibrationRef, HardwareDeviceId из tEXt Metadata внутри PNG. Повторная загрузка идемпотентна.

201/200: { "sha256":"...", "sizeBytes":1048576, "alreadyExisted":false,
           "extractedDeviceId":"0454628", "evidenceDeviceId":"0454628",
           "hardwareDeviceId":"intek-1_11:vid_192a_pid_0803",
           "extractedCalibrationRef":"cal-...", "artifactUid":null }

→ Просмотреть все загруженные captures

POST /api/training-evidence/v1/captures  video/x-msvideo Загрузить видео-скан stereo_scan_video/v1
ЗаголовокОписание
Content-Type*video/x-msvideo
X-Format-Version*stereo-scan-video/v1
X-Content-SHA256*SHA-256 hex тела
X-Device-Id*ID устройства (напр. 0454628)
X-Hardware-Device-IdHardware ID (опц.)
X-Calibration-RefID калибровки (опц.)
X-Artifact-UidСтабильный UID артефакта (опц.)

Тело — AVI-файл, стерео side-by-side (левый|правый кадр, RGB24). Метаданные из заголовков.

201/200: { "sha256":"...", "sizeBytes":123456, "alreadyExisted":false,
           "formatVersion":"stereo-scan-video/v1", "sourceClass":"scan",
           "evidenceDeviceId":"...", "artifactUid":"..." }
GET /api/training-evidence/v1/captures/{sha256} Скачать capture по SHA-256

Возвращает бинарный файл (PNG или AVI) с оригинальным Content-Type.

200: <binary body>
404: { "errorCode":"capture_not_found" }
POST /api/training-evidence/v1/captures/{sha256}/preview?engine={engineId} Загрузить 3D-превью PNG для движка
ПараметрТипОписание
sha256*pathSHA-256 оригинального capture (64 hex-символа)
engine*queryИдентификатор движка: cre, sgbm, hitnet и т.д. Regex: ^[a-z0-9-]{1,32}$
тело*Content-Type: image/pngPNG-рендер 3D-превью от клиентского Audit3DPreviewRenderer

Хранится в dbo.ApiBinaryBlob с scope te-preview-{engineId}. Идемпотентно (upsert по sha256+engine). Требует флаг CanUploadTrainingEvidence.

200: { "stored":true, "captureSha256":"...", "engineId":"cre", "previewSizeBytes":45678 }
400: { "errorCode":"empty_body" | "invalid_payload" | "invalid_engine" }
403: { "errorCode":"forbidden" }

→ Просмотреть галерею 3D-превью

GET /api/training-evidence/v1/captures/{sha256}/preview?engine={engineId} Скачать 3D-превью PNG для движка
ПараметрТипОписание
sha256*pathSHA-256 оригинального capture
engine*queryИдентификатор движка (тот же, что при POST)
200: <image/png binary>  — превью от указанного движка
404: { "errorCode":"preview_not_found" }  — превью ещё не загружено
400: { "errorCode":"invalid_engine" }

Для отображения в браузере без токена используйте PreviewImage.aspx?sha={sha}&engine={engineId} (admin-только).

POST /api/training-evidence/v1/records Загрузить запись измерения
ЗаголовокЗначение
Content-Type*application/json

Тело — JSON схемы training-evidence/v1. Обязательные поля:

{ "schemaVersion":"training-evidence/v1", "evidenceId":"gtpkg-...",
  "captureSha256":"<sha256 PNG или видео>",
  "deviceId":"0454628", "sourceClass":"point_to_point",
  "conditionBucket":"standard", "consent":"explicit_opt_in",
  "measurementJson":{...}, "groundTruthJson":{...} }
201/200: { "evidenceId":"...", "alreadyExisted":false }
404: { "errorCode":"capture_not_found" }
422: { "errorCode":"schema_invalid", "details":"..." }
GET /api/training-evidence/v1/records Список записей измерений
Query-параметрОписаниеПо умолч.
limitКол-во (1–500)100
offsetСмещение0
deviceIdФильтр по ID устройства—
sourceClasspoint_to_point, scan, …—
conditionBucketУсловие съёмки—
trainableForOwnDeviceBuckettrue/false—
hasGroundTruthtrue/false—
captureStatusnot_found — осиротевшие (нет живого capture)—
GET /api/training-evidence/v1/summary Статистика базы
200: { "totalRecords":26, "totalCaptures":569, "referencedCaptures":23, "unreferencedCaptures":546,
       "byDevice":{...}, "bySourceClass":{...},
       "withGroundTruth":26, "trainableForOwnDeviceBucket":22 }
Distribution API  /api/distributions/v1

⚠ Endpoint /latest возвращает только версии, у которых есть оба артефакта: windows и linux-x64.

GET /api/distributions/v1/latest Последний релиз
200: { "version":"0.9.7.25", "releasedAt":"...",
       "windowsUrl":"/api/distributions/v1/0.9.7.25/windows",
       "linuxUrl":"/api/distributions/v1/0.9.7.25/linux-x64",
       "windowsSha256":"...", "linuxSha256":"...",
       "windowsSizeBytes":156429678, "linuxSizeBytes":53124200 }
404: нет версии с обоими артефактами
GET /api/distributions/v1/latest/{platform}  |  /latest?platform={platform} Последний релиз для ОДНОЙ платформы

В отличие от /latest, не требует наличия ОБОИХ артефактов — берёт самый новый релиз, у которого есть указанная платформа. platform: windows или linux-x64. Именно этот путь использует установщик клиента (DistributionUpdateClient.cs).

200: { "version":"0.9.7.25", "platform":"windows", "downloadUrl":"...", "sha256":"...",
       "sizeBytes":78750873, "filename":"...", "releasedAt":"...", "releaseNotes":"..." }
400: distribution_schema_invalid (platform не windows/linux-x64)
404: distribution_not_found (нет ни одного релиза с этой платформой)

B24.2 / 2026-07-07. Портировано с itcworld (эталонная рабочая копия).

GET /api/distributions/v1/versions Список всех релизов

Все релизы (не только с обоими артефактами), отсортированы по семантической версии убыв.

200: { "items": [ { "version":"...", "releasedAt":"...", "releaseNotes":"...",
       "windows":{...}, "linux_x64":{...} }, ... ] }

B24.2 / 2026-07-07.

GET /api/distributions/v1/{version} Метаданные конкретной версии

Та же структура что /latest. Версия в формате N.N.N.N.

400: distribution_schema_invalid (неверный формат версии)
404: distribution_not_found
GET /api/distributions/v1/{version}/windows  |  /{version}/linux-x64 Скачать дистрибутив

Возвращает бинарный архив (zip / tar.gz) для указанной версии и платформы.

POST /api/distributions/v1/ Загрузить дистрибутив (admin)

Требует CanUploadDistributions=1.

ЗаголовокЗначение
Content-Type*application/zip (windows) или application/x-tar (linux)
X-Version*Версия, напр. 0.9.7.26
X-Platform*windows или linux-x64
X-Content-SHA256*SHA-256 hex тела
Коды ошибок

Все ошибки: { "errorCode":"...", "message":"...", "details":"..." }

errorCodeHTTPЗначение
calibration_not_found404Калибровка не найдена
calibration_already_exists409CalibrationID уже в базе
calibration_schema_invalid422Нарушение схемы calibration-profile/v1
capture_not_found404Capture по SHA256 не найден
schema_invalid400/422Неверный формат запроса
payload_integrity_mismatch400SHA256 тела не совпал с X-Content-SHA256
method_not_allowed405Метод не поддерживается для этого пути
forbidden403Ключ не имеет нужного флага
distribution_not_found404Дистрибутив не найден
server_error500Внутренняя ошибка (детали в details)
Что планируется
#ЗадачаСтатус
Ф-2Re-key 701 PNG (через пересылку, без нового endpoint)Через POST /captures
Ф-3Relink 14 осиротевших recordsЧерез re-POST /records
Ф-6Rich EndpointArray (координаты в мм)Стоп-гейт Владельца