7.9 KiB
| id | title | domain | tags | status | severity | created_at | updated_at | applies_to | related_records | |||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| KB-PLUGIN-037 | Plugin redmine — Contrato de API GLPI → Redmine (o quê, como e por quê) | plugin-dev |
|
active | high | 2026-07-02 | 2026-07-02 |
|
|
Plugin redmine — Contrato de API GLPI → Redmine
Registro de desenvolvedor do plugin redmine (marketplace Mindplace). Resume o quê
sai do GLPI, como entra no Redmine e por quê cada decisão existe. A referência
completa (payloads, exemplos HTTP) vive no repo do plugin: docs/API.md
(servicedesk.mindtek.com.br/git/rodolpho.lopes/redmine, espelho dev em
onmind.mindtek.com.br/git/rodolpho.lopes/Redmine).
O quê (modelo)
| GLPI | Redmine | Quando |
|---|---|---|
| Projeto ou Entidade (vínculo manual na aba Redmine) | Projeto | vínculo 1:1 salvo em glpi_plugin_redmine_projects / _entities |
| Chamado | 1 issue | criada no 1º lançamento de tempo (lazy) |
| TicketTask Feito com duração > 0 | 1 time entry na issue | ao salvar a task (1× por task) |
| Status do chamado | status da issue | mapeado (status_map do formconfig) |
| Técnico atribuído / autor da task | autor da issue / do time entry | via impersonation |
Por quê: o chamado nasce e morre no GLPI; para o Redmine importam as horas e o
andamento. Tarefas não concluídas ficam só no GLPI (gate no state = Feito).
Como (mecanismos-chave)
- Auth: header
X-Redmine-API-Keycom chave de admin (cifrada no banco via GLPIKey). - Autoria — impersonation: header
X-Redmine-Switch-User: <login>. Só funciona se o usuário for membro do projeto com papel que permita a ação → o plugin faz auto-membership (POST /projects/:id/memberships.json, papel =default_role_id). Falhou (403/412)? Refaz como admin e loga — nunca perde o lançamento. - Mapeamento de usuários: overrides manuais (
glpi_plugin_redmine_usermap) → auto-match por e-mail no cacheglpi_plugin_redmine_users→ lookup ao vivo no Redmine (/users.json?name=<email>) com upsert no cache (self-healing, 1.6.0). Sem par: lança como admin com log ("Sem par Redmine para o usuário GLPI #N"). Incidente real (2026-07-02): o cache ficou vazio em prod e os lançamentos saíram como admin em silêncio — motivou o self-healing e osyncUserspor upsert (nunca esvazia a tabela). - Idempotência:
glpi_plugin_redmine_tickets(1 issue por chamado) eglpi_plugin_redmine_tasks(tempo 1× por task). Se a issue vinculada foi apagada no Redmine, o plugin detecta (GET 404) e recria removendo o vínculo órfão (1.5.5+). - Tempo: GLPI grava segundos → Redmine quer horas decimais →
actiontime/3600, 2 casas (0h05 → 0.08 h; o Redmine exibe "0:05"). - Sincronizar Metadados: cacheia projects, trackers, statuses, priorities,
activities, roles, users e project_custom_fields em
glpi_plugin_redmine_configs.metadata_cache. Os formulários leem SÓ do cache — após mudanças no Redmine (novos campos, projetos), é preciso re-sincronizar.
Por quê — regras do Redmine de PRODUÇÃO que moldaram o contrato
Aprendidas em produção (2026-06-30 a 07-02); o dev era permissivo, o prod não:
- Custom fields de projeto OBRIGATÓRIOS: "Tipo de Projeto" (cf id 4) e
"Tipo de Faturamento" (cf id 5) não podem ficar vazios na criação de projeto →
POST /projects.jsonlevacustom_field_values: {"4": "...", "5": "..."}. O form das abas Redmine renderiza esses campos dinamicamente a partir demetadata_cache.project_custom_fields(dropdown p/field_format=list) — se o Redmine ganhar novos campos obrigatórios, basta re-sincronizar metadados. - Identificador: só minúsculas/números/traços →
createProjectFull()normaliza ("GNA" → "gna"); senão 422 "Identificador não é válido". - Atividade do time entry é POR PROJETO: a
default_activity_iddo formconfig precisa estar habilitada no projeto de destino, senão 422 "não está incluso na lista". - Comentário do time entry é obrigatório: task sem descrição → fallback "Tempo lançado via GLPI (chamado #N)" (1.5.4+).
- Projeto de destino precisa estar ATIVO (status 1): fechado/arquivado → 422/403. O dropdown de vínculo filtra só ativos (1.5.3+).
- Tracker é por projeto: o
default_tracker_idglobal precisa existir no projeto; vazio = o Redmine usa o default do projeto (mais seguro em multi-cliente).
Endpoints usados (resumo)
Leitura (sync): /projects.json, /trackers.json, /issue_statuses.json,
/enumerations/issue_priorities.json, /enumerations/time_entry_activities.json,
/roles.json, /users.json (paginado), /projects/:id/issue_categories.json,
/custom_fields.json (filtra customized_type=project).
Escrita: POST /projects.json (criar projeto c/ custom fields), POST /issues.json
(issue do chamado), PUT /issues/:id.json (status), POST /time_entries.json (tempo),
POST /projects/:id/memberships.json (auto-membership).
Motor de Templates de Payload (2.0)
A partir da 2.0 o corpo de cada operação (create_issue, log_time,
update_status, create_project) pode vir de um template editável em vez de
ser montado no código — o objetivo é o plugin virar produto ambiente-agnóstico
(cada Redmine tem seus campos obrigatórios/personalizados). Guia do usuário:
docs/TEMPLATES.md; contrato de dev: docs/API.md.
PluginRedmineTemplateengine(inc/templateengine.class.php): tabelaglpi_plugin_redmine_templates, renderizador mustache-like ({{ var | filtro }}; tag ocupando o valor inteiro → tipo nativo; lookup indexadomap.status[chamado.status]; filtrostruncar/sem_html/minusculo/identificador/padrao;prunede nulos), seeds equivalentes ao 1.x, descoberta de custom fields e os contextos (buildContext,buildFormContext,sampleContext).- Precedência: template ativo → renderiza e envia; erro de render → log + fallback 1.x; sem template ativo → comportamento 1.x idêntico. Ou seja, atualizar da 1.x para a 2.0 não muda nada até ativar um template.
- UI (config): editor linha-a-linha "DE → PARA" (paleta de variáveis GLPI via
autocomplete + dicionário dos campos que o Redmine espera, com obrigatoriedade e
valores), botão "Gerar template" (local, sem reload) e testador
(
ajax/template_test.php): preview do render + dry-run que cria/apaga objeto de teste e traduz os 422 em dicas. - NÃO virou template (é motor, não payload): impersonation, idempotência, membership, gates (Feito/duração), recuperação de issue órfã.
- Decisão de escopo (2026-07-03): não implementar override por vínculo nem perfis multi-Redmine sem caso real (YAGNI) — o template global cobre o cenário atual.
Gotchas de manutenção
- Dev = GLPI 11.0.7, prod = 11.0.4: desenvolver mirando 11.0.0+ (sem
fields.csrfField(), semToolbox::logError()). Ver [KB-PLUGIN-036]. - Ambiente em
production: mudou.twig→cache:clearobrigatório (o cache do GLPI não é invalidado por atualização de plugin). - Twig
mergerenumera chaves inteiras: nunca montar opções{id: nome}no Twig — montar no PHP (bug real: vínculo gravava o índice em vez do ID do projeto, 1.5.3). - Release ZIP: wrapper
redmine/como primeira entrada (entrada de diretório), senão o instalador do Mindplace extrai aninhado.
Classificação
- Tipo: contrato de integração + lições de produção.
- Reutilização: obrigatória para quem for manter/evoluir o plugin redmine; útil para qualquer integração GLPI → API externa.