- ajax/template_test.php: action=preview (render com sampleContext) e action=dryrun (cria+apaga objeto de teste no Redmine, traduz erros 422 em dicas acionáveis; issue temporária para log_time/update_status) - RedmineApi::rawRequest (código+corpo crus) e TemplateEngine::sampleContext - UI: botão "Testar" + painel por card (preview sempre; teste real opt-in com seletor de projeto alvo), via fetch com X-Glpi-Csrf-Token - validado E2E por HTTP: preview, dry-run cria/deleta (204), render inválido capturado, tradução de 422; sem resíduo de teste no Redmine Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
100 lines
4.8 KiB
Markdown
100 lines
4.8 KiB
Markdown
# DESIGN 2.0 — Templates de Payload (motor de mapeamento dinâmico)
|
||
|
||
> Objetivo: transformar o plugin — hoje moldado ao ambiente Mindtek — em **produto**:
|
||
> os payloads enviados ao Redmine passam a ser **templates editáveis** pelo admin,
|
||
> com variáveis do GLPI e campos descobertos do Redmine de destino.
|
||
> Linha estável (clientes atuais): branch `stable/1.x`. Evolução: `main` (2.0).
|
||
|
||
## Decisões fechadas (2026-07-02)
|
||
|
||
| Decisão | Escolha |
|
||
|---|---|
|
||
| Idioma das tags | **PT-BR** (`{{ chamado.titulo }}`) — público Mindplace |
|
||
| UX do editor | **Linha-a-linha** (campo → valor/tag → obrigatório) com toggle "ver JSON" |
|
||
| Escopo do template | **Global por operação** na 2.0; override por vínculo na 2.1 |
|
||
| Dry-run | **Preview de render sempre**; teste real opt-in contra alvo escolhido |
|
||
| Sintaxe | Mustache-like (`{{ var \| filtro }}`), **sem Twig completo** (segurança) |
|
||
|
||
## Fronteira Motor × Template
|
||
|
||
**Motor (fixo, não editável):** auth, impersonation + auto-membership + fallback admin,
|
||
idempotência (1 issue/chamado, tempo 1×/task), recuperação de issue órfã, gates
|
||
(Feito + duração>0), normalização de identifier, logs, self-healing de usuários.
|
||
|
||
**Template (editável):** o corpo JSON de cada operação — quais campos vão e com
|
||
quais valores (tags, valores fixos, custom fields do ambiente).
|
||
|
||
O admin mapeia **dados**, nunca **lógica**.
|
||
|
||
## Operações e seeds
|
||
|
||
4 operações fixas: `create_project`, `create_issue`, `log_time`, `update_status`.
|
||
O botão **"Gerar template"** produz o seed = comportamento atual do motor 1.x
|
||
+ custom fields obrigatórios descobertos via `/custom_fields.json`
|
||
(`customized_type` project/issue/time_entry conforme a operação).
|
||
|
||
Seed `create_issue` (exemplo):
|
||
```json
|
||
{ "issue": {
|
||
"project_id": "{{ vinculo.projeto_redmine }}",
|
||
"subject": "{{ chamado.titulo | truncar:255 }}",
|
||
"description": "{{ chamado.descricao | sem_html }}",
|
||
"tracker_id": "{{ config.tracker_padrao }}",
|
||
"priority_id": "{{ config.prioridade_padrao }}",
|
||
"status_id": "{{ map.status[chamado.status] }}",
|
||
"category_id": "{{ vinculo.categoria_padrao }}",
|
||
"watcher_user_ids": "{{ config.observadores }}"
|
||
} }
|
||
```
|
||
|
||
## Sintaxe das tags
|
||
|
||
- `{{ namespace.campo }}` — resolução por caminho no contexto.
|
||
- `{{ mapa[chave.dinamica] }}` — lookup indexado (ex.: `map.status[chamado.status]`).
|
||
- Filtros em pipeline: `{{ var | filtro:arg | filtro2 }}`.
|
||
- `truncar:N`, `sem_html`, `minusculo`, `identificador`, `padrao:'texto fixo'`.
|
||
|
||
**Regra de tipos:** tag ocupando o **valor inteiro** da string → rende com o **tipo
|
||
nativo** da variável (número/array/bool, sem aspas no JSON final). Tag **embutida**
|
||
em texto → interpolação de string. Valores `null`/arrays vazios pós-render são
|
||
**removidos** do payload (igual ao motor 1.x, que só inclui campos definidos).
|
||
|
||
## Contexto (paleta de variáveis)
|
||
|
||
| Namespace | Campos | Origem |
|
||
|---|---|---|
|
||
| `chamado.*` | id, titulo, descricao, status, entidade | Ticket |
|
||
| `tarefa.*` | id, horas, segundos, descricao, comentario*, data | TicketTask |
|
||
| `vinculo.*` | projeto_redmine, issue_redmine, categoria_padrao | tabelas do plugin |
|
||
| `config.*` | tracker_padrao, prioridade_padrao, atividade_padrao, observadores | formconfig |
|
||
| `map.status` | mapa status GLPI→Redmine | formconfig |
|
||
| `form.*` | campos do form de criar projeto (inclui `cf_<id>`) | aba Redmine |
|
||
|
||
\* `tarefa.comentario` = descrição sem HTML **ou** o fallback "Tempo lançado via
|
||
GLPI (chamado #N)" — o fallback é responsabilidade do motor (contexto), não do template.
|
||
|
||
## Armazenamento e retrocompatibilidade
|
||
|
||
Tabela `glpi_plugin_redmine_templates` (`operation` único, `template` MEDIUMTEXT,
|
||
`is_active`). **Sem template ativo → motor 1.x intocado.** Template ativo com erro
|
||
de **render** → log + motor 1.x (nunca perde lançamento). Template ativo válido →
|
||
payload do template é o enviado (sem retry pela via legada em erro de API — respeita
|
||
a escolha do admin e evita duplicação).
|
||
|
||
## Fases
|
||
|
||
1. **Motor (esta fase):** engine de render + tabela + seeds + wiring com fallback nas
|
||
operações do hook (`create_issue`, `log_time`, `update_status`). Invisível na UI.
|
||
2. **UI:** editor linha-a-linha + paleta + "Gerar template" + wiring do `create_project`
|
||
(contexto `form.*`).
|
||
3. **Validação (concluída):** preview de render + dry-run opcional com tradução de 422.
|
||
4. **2.1:** override por vínculo, custom fields de issue/time_entry na paleta,
|
||
perfis múltiplos (um GLPI → vários Redmines).
|
||
|
||
## Referências
|
||
|
||
- Caso de estudo: ambiente Mindtek (KB-PLUGIN-037) — nativo vs "o que o Redmine exigiu"
|
||
(custom fields 4/5 de projeto, activity/comments obrigatórios no time entry, trackers
|
||
por projeto).
|
||
- Redmine **não tem** introspecção de schema: descoberta = esqueleto fixo por operação
|
||
+ `/custom_fields.json` + dry-run com tradução de erros 422.
|