Redmine/docs/DESIGN-2.0.md
Gemini 7bf8cdab4e feat(2.0): Fase 3 — testador de templates (preview + dry-run com tradução de 422)
- 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>
2026-07-03 08:58:17 -03:00

100 lines
4.8 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.