Treinamentos · Referência técnica
Contratos do módulo de treinamentos — endpoints, schema do editor, eventos xAPI e fluxo do participante
Treinamentos · Referência técnica
Visão Geral
Esta referência cobre o contrato técnico do módulo de treinamentos — endpoints, schema do editor (BlockNote + bloco Mux) e o vocabulário de eventos xAPI gravados no LRS interno (xapi_statements). A documentação voltada ao usuário final está em Treinamentos EAD.
Endpoints
Admin (trainings:*)
| Método | Rota | Permissão |
|---|---|---|
GET | /api/trainings | trainings:view |
POST | /api/trainings | trainings:create |
GET | /api/trainings/{id} | trainings:view |
PUT | /api/trainings/{id} | trainings:edit |
DELETE | /api/trainings/{id} | trainings:delete |
POST | /api/trainings/{id}/publish | trainings:edit |
POST | /api/trainings/{tid}/modules | trainings:edit |
PUT | /api/trainings/{tid}/modules/{id} | trainings:edit |
DELETE | /api/trainings/{tid}/modules/{id} | trainings:edit |
PUT | /api/trainings/{tid}/modules/reorder | trainings:edit |
GET | /api/trainings/{tid}/modules/{id}/content | trainings:view |
PUT | /api/trainings/{tid}/modules/{id}/content | trainings:edit |
PUT | /api/trainings/{tid}/modules/{id}/quiz | trainings:edit |
DELETE | /api/trainings/{tid}/modules/{id}/quiz | trainings:edit |
POST | /api/trainings/{tid}/enrollments | trainings:participants |
POST | /api/trainings/{tid}/enrollments/send-invites | trainings:participants |
Auth do participante (anônimas)
| Método | Rota | Body |
|---|---|---|
POST | /api/participant-auth/token | { token } |
POST | /api/participant-auth/login | { email, password } |
GET | /api/participant-auth/me | — (requer participant:access) |
Player do participante (participant:access)
| Método | Rota |
|---|---|
POST | /api/participant/trainings/{id}/launch |
GET | /api/participant/trainings/{id} |
GET | /api/participant/trainings/{tid}/modules/{id} |
POST | /api/participant/trainings/{tid}/modules/{id}/complete |
POST | /api/participant/trainings/{tid}/quizzes/{id}/submit |
POST | /api/participant/trainings/{id}/complete |
POST | /api/participant/xapi/statements |
Schema do Editor
O conteúdo de cada módulo é persistido como JSON do BlockNote na coluna Content (jsonb) de TrainingModule. O backend não interpreta o documento — apenas armazena e devolve.
O schema do editor é estendido com um bloco custom muxVideo:
BlockNoteSchema.create({
blockSpecs: { ...defaultBlockSpecs, muxVideo: MuxBlock }
})Bloco muxVideo
| Propriedade | Tipo | Default | Descrição |
|---|---|---|---|
playbackId | string | "" | Identificador do asset publicado no Mux |
title | string | "" | Título exibido em metadata/relatórios |
content: "none" — bloco atômico, sem filhos.
Persistência
GET /api/trainings/{tid}/modules/{id}/contentretorna{ content: string \| null, quiz: QuizPayload \| null }. O campocontenté a string JSON inteira do BlockNote.PUT .../contentaceita{ content: string }— o frontend serializa viaJSON.stringify(editor.document).- O auto-save dispara após 1500ms sem digitação. Falhas não são reenviadas — o usuário precisa modificar algo de novo para retentar.
Schema do Quiz
Cada módulo pode ter no máximo um quiz. Questões aceitam três tipos:
questionType | Validação backend |
|---|---|
single_choice | Resposta correta se selectedOptionIds == [correctId] |
multiple_choice | Resposta correta se o conjunto de selecionados é igual ao conjunto de corretas |
true_false | Mesma regra de single_choice (alternativas fixas: Verdadeiro / Falso) |
Options é serializado como JSON string:
[
{ "id": "uuid", "label": "...", "isCorrect": true, "feedback": "opcional" }
]O endpoint GET /api/participant/trainings/.../modules/{id} retira os campos isCorrect e feedback antes de devolver para o participante (ver StripCorrectness em GetModulePlayer/Endpoint.cs).
A nota é calculada como round((points_earned / points_total) * 100). A aprovação compara contra Training.Evaluation.PassingScore.
Vocabulário xAPI
Statements são imutáveis, gravados em xapi_statements. Schema segue o padrão actor · verb · object · result · context.
Verbos suportados
| Verbo | URI | Origem |
|---|---|---|
registered | http://adlnet.gov/expapi/verbs/registered | Backend (matrícula) |
invited | http://id.tincanapi.com/verb/invited | Backend (envio de convite) |
launched | http://adlnet.gov/expapi/verbs/launched | Backend (/launch) |
initialized | http://adlnet.gov/expapi/verbs/initialized | Frontend (mount do módulo) |
progressed | http://adlnet.gov/expapi/verbs/progressed | Frontend (Mux 25/50/75/90%) |
experienced | http://adlnet.gov/expapi/verbs/experienced | Frontend (Mux play/ended) |
answered | http://adlnet.gov/expapi/verbs/answered | Frontend (por questão, antes do submit) |
attempted | http://adlnet.gov/expapi/verbs/attempted | Backend (/quizzes/.../submit) |
passed / failed | http://adlnet.gov/expapi/verbs/passed+failed | Backend (resultado da tentativa) |
completed | http://adlnet.gov/expapi/verbs/completed | Backend (módulo e treinamento) |
satisfied | http://adlnet.gov/expapi/verbs/satisfied | Backend (certificado emitido) |
Endpoint de passthrough do frontend
POST /api/participant/xapi/statements aceita statements granulares emitidos pelo player. Allowlist de verbos: initialized, progressed, experienced, answered. Tipos de objeto aceitos: module, video, question, training.
Body:
{
"verb": "progressed",
"objectType": "video",
"objectId": "<module guid>",
"objectName": "Aula 1 — LGPD",
"trainingId": "<training guid>",
"moduleId": "<module guid>",
"result": { "duration": "PT45S", "score": { "raw": 45, "min": 0, "max": 180, "scaled": 0.25 } },
"extras": { "muxPlaybackId": "...", "milestone": 25, "blockId": "..." }
}O backend valida que o objectId pertence a um treinamento em que o participante está matriculado, anexa trainingId/enrollmentId/moduleId ao Context e grava como XApiStatement. Verbos fora do allowlist retornam 400. Tipos de objeto inválidos retornam 400. Objetos não-pertencentes ao treinamento retornam 404.
Granularidade de tracking de vídeo
O bloco MuxBlock em modo read-only registra:
| Evento | Verbo | Quando |
|---|---|---|
play | experienced (phase: started) | Primeiro play do vídeo |
progress | progressed | Ao cruzar 25%, 50%, 75% e 90% (não-repetível por sessão) |
ended | experienced (phase: ended, result.completion: true) | Vídeo terminou |
Cada evento carrega muxPlaybackId no extras para correlação posterior com o painel do Mux.
Autenticação do Participante
JWT separado do JWT do tenant. Claims:
| Claim | Origem |
|---|---|
tenant_id | Tenant dono do treinamento |
participant_id | TrainingParticipant.Id |
participant_email / participant_name | Cópia das colunas do participante |
enrollment_id / training_id | Apenas no fluxo de link mágico |
permissions | Sempre participant:access |
Expiração: 8 horas. O AccessToken da matrícula tem expiração própria (ExpiresAt) configurada no momento da matrícula.
participant:access não está em Permissions.All — não pode ser atribuída a roles do tenant.
Erros & Edge Cases
| Cenário | Endpoint | Resposta |
|---|---|---|
| Token de matrícula expirado | POST /participant-auth/token | 400 com mensagem de expiração |
| Login com senha mas sem matrículas ativas | POST /participant-auth/login | 400 "Nenhuma matrícula ativa…" |
| Tentar concluir módulo com quiz | POST .../modules/{id}/complete | 400 "Este módulo possui quiz" |
| Submeter quiz já aprovado | POST .../quizzes/{id}/submit | 400 "Quiz já foi aprovado" |
Submeter quiz com MaxAttempts atingido | idem | 400 "Limite de tentativas atingido" |
AllowRetake = false e segunda tentativa | idem | 400 "Esta avaliação não permite retentativas" |
| Concluir treinamento com módulos pendentes | POST .../complete | 400 "Existem N módulo(s) não concluído(s)" |
| Verbo xAPI fora do allowlist | POST /participant/xapi/statements | 400 "Verbo não permitido" |
Configuração
| Chave | Default | Uso |
|---|---|---|
App:TrainingPortalUrl | https://app.lexios.dev | Base do link de acesso enviado por e-mail ({base}/treinamento/acesso?token=...) |
App:FrontendUrl | — | Fallback caso TrainingPortalUrl esteja ausente |
Em desenvolvimento local, ambos apontam para http://localhost:3000 (ver appsettings.Development.json).