diff --git a/.gitignore b/.gitignore index b8e4304..243799a 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ *.swp *.tmp *.bak +._* +.DS_Store diff --git a/index.json b/index.json old mode 100644 new mode 100755 index 86afb9c..a7ba63e --- a/index.json +++ b/index.json @@ -1,6 +1,6 @@ { "version": "1.0.0", - "last_updated": "2026-05-26", + "last_updated": "2026-06-11", "records": [ { "id": "KB-INFRA-001", @@ -384,6 +384,192 @@ "severity": "medium", "path": "records/plugin-dev/KB-PLUGIN-020-mcprotocol-bff-roadmap.md", "summary": "id: KB-PLUGIN-020" + }, + { + "id": "KB-PLUGIN-021", + "title": "mcprotocol — Roadmap de Resources e caso de uso N1", + "domain": "plugin-dev", + "tags": [ + "mcp", + "resources", + "n1", + "knowledge-base", + "roadmap" + ], + "status": "draft", + "severity": "medium", + "path": "records/plugin-dev/KB-PLUGIN-021-mcprotocol-resources-n1-roadmap.md", + "summary": "id: KB-PLUGIN-021" + }, + { + "id": "KB-PLUGIN-022", + "title": "mcprotocol — Padrões de validação e ergonomia em tool inputs", + "domain": "plugin-dev", + "tags": [ + "mcp", + "tools", + "validation", + "ergonomics", + "llm", + "patterns" + ], + "status": "active", + "severity": "medium", + "path": "records/plugin-dev/KB-PLUGIN-022-mcprotocol-tool-input-patterns.md", + "summary": "id: KB-PLUGIN-022" + }, + { + "id": "KB-PLUGIN-023", + "title": "mcprotocol — InputValidation extraída como classe utilitária compartilhada", + "domain": "plugin-dev", + "tags": [ + "mcp", + "tools", + "validation", + "refactor", + "patterns" + ], + "status": "active", + "severity": "medium", + "path": "records/plugin-dev/KB-PLUGIN-023-mcprotocol-inputvalidation-extracted.md", + "summary": "id: KB-PLUGIN-023" + }, + { + "id": "KB-PLUGIN-024", + "title": "mcprotocol — Bug duplo no glpi_delete_item (sucesso silencioso + ProjectTask não exposto em v2)", + "domain": "plugin-dev", + "tags": [ + "mcp", + "bug", + "glpi", + "rest-api", + "delete", + "error-handling" + ], + "status": "active", + "severity": "high", + "path": "records/plugin-dev/KB-PLUGIN-024-mcprotocol-delete-item-silent-success-bug.md", + "summary": "id: KB-PLUGIN-024" + }, + { + "id": "KB-PLUGIN-025", + "title": "mcprotocol — Comportamento de timezone em datas sem hora explícita", + "domain": "plugin-dev", + "tags": [ + "mcp", + "tools", + "validation", + "timezone", + "gotcha" + ], + "status": "active", + "severity": "low", + "path": "records/plugin-dev/KB-PLUGIN-025-mcprotocol-date-timezone-handling.md", + "summary": "id: KB-PLUGIN-025" + }, + { + "id": "KB-PLUGIN-026", + "title": "mcprotocol — Snapshot de roadmap v1.2 (pós-ProjectTask + InputValidation)", + "domain": "plugin-dev", + "tags": [ + "mcp", + "roadmap", + "snapshot", + "planning" + ], + "status": "active", + "severity": "medium", + "path": "records/plugin-dev/KB-PLUGIN-026-mcprotocol-roadmap-snapshot-v1.2.md", + "summary": "id: KB-PLUGIN-026" + }, + { + "id": "KB-PLUGIN-027", + "title": "mcprotocol — Fluxo de release Dev → Prod (manual)", + "domain": "plugin-dev", + "tags": [ + "mcp", + "release", + "workflow", + "forgejo", + "mindplace" + ], + "status": "active", + "severity": "medium", + "path": "records/plugin-dev/KB-PLUGIN-027-mcprotocol-dev-prod-release-flow.md", + "summary": "id: KB-PLUGIN-027" + }, + { + "id": "KB-PLUGIN-028", + "title": "GLPI 11 — Plugin com menu próprio precisa registrar rights via Profile", + "domain": "plugin-dev", + "tags": [ + "glpi11", + "plugin", + "profile", + "rights", + "menu", + "sidebar", + "gotcha" + ], + "status": "active", + "severity": "high", + "path": "records/plugin-dev/KB-PLUGIN-028-glpi11-plugin-rights-and-menu-visibility.md", + "summary": "id: KB-PLUGIN-028" + }, + { + "id": "KB-PLUGIN-029", + "title": "GLPI 11 — Itemtypes administrativos vs. ativos (semântica de vínculo com Ticket)", + "domain": "plugin-dev", + "tags": [ + "glpi11", + "plugin", + "design", + "architecture", + "ticket", + "contract", + "taxonomy" + ], + "status": "active", + "severity": "medium", + "path": "records/plugin-dev/KB-PLUGIN-029-glpi11-administrative-vs-asset-itemtypes.md", + "summary": "id: KB-PLUGIN-029" + }, + { + "id": "KB-PLUGIN-030", + "title": "\"GLPI 11 — Criação de tabelas exige classe Migration (Executing direct queries is not allowed!)\"", + "domain": "plugin-dev", + "tags": [ + "glpi11", + "plugin", + "install", + "database", + "ddl", + "migration", + "gotcha" + ], + "status": "active", + "severity": "high", + "path": "records/plugin-dev/KB-PLUGIN-030-glpi11-migration-class-required-for-ddl.md", + "summary": "id: KB-PLUGIN-030" + }, + { + "id": "KB-PLUGIN-031", + "title": "\"Runbook — Workflow de desenvolvimento e deploy DEV de plugins GLPI\"", + "domain": "plugin-dev", + "tags": [ + "runbook", + "workflow", + "dev", + "deploy", + "forgejo", + "scaffold", + "validation", + "console" + ], + "status": "active", + "severity": "high", + "path": "records/plugin-dev/KB-PLUGIN-031-plugin-dev-workflow-runbook.md", + "summary": "id: KB-PLUGIN-031" } ] } diff --git a/records/plugin-dev/KB-PLUGIN-021-mcprotocol-resources-n1-roadmap.md b/records/plugin-dev/KB-PLUGIN-021-mcprotocol-resources-n1-roadmap.md new file mode 100644 index 0000000..781a32e --- /dev/null +++ b/records/plugin-dev/KB-PLUGIN-021-mcprotocol-resources-n1-roadmap.md @@ -0,0 +1,87 @@ +--- +id: KB-PLUGIN-021 +title: mcprotocol — Roadmap de Resources e caso de uso N1 +domain: plugin-dev +tags: + - mcp + - resources + - n1 + - knowledge-base + - roadmap +status: draft +severity: medium +created_at: 2026-05-26 +updated_at: 2026-05-26 +applies_to: + - glpi-11 + - mcprotocol-plugin +related_records: + - KB-PLUGIN-019 + - KB-PLUGIN-020 +--- + +# mcprotocol — Roadmap de Resources e caso de uso N1 + +## Contexto + +A spec MCP define 3 primitivos: **tools**, **resources** e **prompts**. Hoje o plugin mcprotocol implementa apenas `tools`. A evolução natural mais valiosa pro caso GLPI é adicionar **resources**, especialmente para habilitar **LLM como N1** (atendimento de primeiro nível). + +## Por que Resources e não Prompts + +- **Resources (alto valor)** — GLPI tem KB articles, tickets com histórico, anexos e CIs. Tudo isso é "documento que o LLM deveria ler", não "função que ele chama". Resources transforma N chamadas de tool em uma anexação direta no contexto. +- **Prompts (baixo valor)** — ITSM tem workflows mas usuários já falam linguagem natural. ROI marginal. + +## Caso de uso central: LLM como N1 + +Fluxo tradicional N1: +1. Recebe chamado +2. Procura KB +3. Lê artigos +4. Aplica procedimento ou escala + +Com resources + tools: +- Cliente MCP anexa KB articles relevantes como resources +- LLM lê no contexto sem fazer N chamadas de tool +- Usa tools (`glpi_ticket_*`) pra agir no chamado +- Caso de "N1 autônomo": webhook → busca KB+tickets similares → resources → LLM decide aplicar ou escalar + +## Resources prioritárias + +| URI pattern | Origem | Caso de uso | +|---|---|---| +| `kb://{id}` | `glpi_knowbaseitems` | Artigos da base de conhecimento | +| `ticket://{id}` | `glpi_tickets` (+ followups, solutions) | Ticket completo com histórico | +| `document://{id}` | `glpi_documents` | Anexos (PDFs, prints, manuais) | +| `computer://{id}` | `glpi_computers` (+ items) | CI técnico do ativo | + +## Implementação sugerida + +Criar `src/KBResources.php` (e companheiros) seguindo o padrão `ToolRegistry`: + +```php +class KBResources { + public static function getResources(): array { /* ... */ } + public static function readResource(string $uri) { /* ... */ } +} +``` + +Adicionar `ResourceRegistry` análogo ao `ToolRegistry`. Em `Server.php`, implementar: +- `resources/list` — lista URIs disponíveis (paginar para KB grandes) +- `resources/read` — retorna conteúdo por URI + +## Evolução futura (fora deste escopo) + +- **SSE / Streamable HTTP completo** — notificações server-push (novo ticket, SLA estourando) +- **Prompts** — depois de resources, talvez 2-3 prompts simbólicos (triagem, fechamento padrão) +- **Multi-tenancy / escopo OAuth2 fino** — separar permissões por escopo (tools_ticket, tools_inventory, etc.) +- **stdio→HTTP bridge** — pacote NPM `@mindplace/glpi-mcp-bridge` para compatibilidade com clientes que só falam stdio + +## Status + +**Roadmap** — não implementado. Prioridade após conclusão do roadmap de tools (KB-PLUGIN-020). + +## Referências + +- KB-PLUGIN-019 — Arquitetura BFF do mcprotocol +- KB-PLUGIN-020 — Roadmap de tools pendentes +- Spec MCP: https://spec.modelcontextprotocol.io diff --git a/records/plugin-dev/KB-PLUGIN-022-mcprotocol-tool-input-patterns.md b/records/plugin-dev/KB-PLUGIN-022-mcprotocol-tool-input-patterns.md new file mode 100644 index 0000000..2652222 --- /dev/null +++ b/records/plugin-dev/KB-PLUGIN-022-mcprotocol-tool-input-patterns.md @@ -0,0 +1,167 @@ +--- +id: KB-PLUGIN-022 +title: mcprotocol — Padrões de validação e ergonomia em tool inputs +domain: plugin-dev +tags: + - mcp + - tools + - validation + - ergonomics + - llm + - patterns +status: active +severity: medium +created_at: 2026-05-26 +updated_at: 2026-05-26 +applies_to: + - glpi-11 + - mcprotocol-plugin +related_records: + - KB-PLUGIN-019 + - KB-PLUGIN-020 +--- + +# mcprotocol — Padrões de validação e ergonomia em tool inputs + +## Contexto + +Tools do MCP são chamadas por LLMs que **não conhecem IDs internos** do GLPI nem o formato exato esperado pelos campos. Sem padrões de entrada bem desenhados, o LLM precisa fazer várias chamadas exploratórias (listar estados → encontrar ID → criar projeto), gastando turns e tokens. Pior: quando erra, recebe mensagens de erro do ORM crípticas (`SQL error`, `boolean false`) e não consegue se autocorrigir. + +Este registro documenta os padrões implementados no `ProjectTools::handleCreate` que resolvem esses problemas, **aplicáveis a qualquer tool de criação/atualização** no plugin. + +## Padrões implementados + +### 1. `resolveReference` — Aceita ID ou nome + +**Problema:** LLM não sabe que `projectstates_id=2` é "Processing". + +**Solução:** helper que aceita int (validando existência) ou string (resolvendo por nome). + +```php +private static function resolveReference( + string $table, // ex: 'glpi_projectstates' + $value, // int OU string + string $field, // nome do campo (pra mensagem de erro) + array &$errors // acumulador de erros +): ?int +``` + +**Caso int / string numérica:** +- Valida existência via `SELECT id FROM {$table} WHERE id = ?` +- Se não existe: `"{$field}: ID {$id} não existe em {$table}."` + +**Caso string (nome):** +- Busca via `SELECT id FROM {$table} WHERE name = ?` +- Se não encontra: lista até 20 nomes disponíveis na mensagem de erro + +**Exemplo de erro útil pra LLM:** +``` +projectstates_id: nome 'AbacaxiDoido' não encontrado em glpi_projectstates. +Disponíveis: New, Processing, Closed. +``` + +A LLM lê isso e **se autocorrige** na próxima chamada — sem precisar de uma tool separada `glpi_projectstate_list`. + +### 2. `normalizeDate` — Normaliza formato + +**Problema:** Datas vêm em formatos variados (`2026-06-01`, `2026-06-01 00:00:00`, ISO 8601, etc.). GLPI espera `Y-m-d H:i:s`. + +**Solução:** helper aceita os 2 formatos mais comuns e normaliza: + +```php +private static function normalizeDate( + string $value, + string $field, + array &$errors +): ?string +``` + +- `YYYY-MM-DD` → preenche `00:00:00` +- `YYYY-MM-DD HH:MM:SS` → mantém +- Qualquer outro: erro claro com formato esperado + +### 3. Validação prévia agregada (fail-loud-once) + +**Problema:** Validar campo por campo lançando exception faz a LLM corrigir 1 erro por chamada — N chamadas pra N erros. + +**Solução:** acumula erros num array e lança UMA exception no final: + +```php +$errors = []; +// ... várias validações que appendam em $errors +if (!empty($errors)) { + throw new \Exception("Falha na validação: " . implode(" | ", $errors)); +} +``` + +A LLM recebe **todos os problemas de uma vez**, corrige tudo e tenta de novo. + +### 4. Response inclui `applied` (debug & confiança) + +**Problema:** depois de resolver nomes → IDs, normalizar datas, etc., a LLM não sabe o que efetivamente foi gravado. + +**Solução:** resposta de sucesso inclui o input final aplicado no ORM: + +```php +return [ + 'status' => 'success', + 'id' => $newId, + 'message' => 'Projeto criado com sucesso.', + 'applied' => $input // mostra o que realmente entrou no DB +]; +``` + +A LLM confirma `projectstates_id: 2` (resolvido de "Processing"), `plan_start_date: 2026-06-01 00:00:00` (normalizado), e segue confiante. + +### 5. Schema usa `oneOf` pra polimorfismo + +Campos que aceitam int ou string declaram isso explicitamente no `inputSchema`: + +```php +'projectstates_id' => [ + 'description' => 'Estado. Aceita ID (int) ou nome (ex: "New", "Processing", "Closed").', + 'oneOf' => [ + ['type' => 'integer'], + ['type' => 'string'] + ] +] +``` + +A descrição já lista exemplos comuns — a LLM começa certo na primeira tentativa. + +## Quando aplicar + +**Sempre que uma tool tem campo que referencia outra tabela GLPI:** + +- `users_id` → `glpi_users` (login ou ID) +- `groups_id` → `glpi_groups` (nome ou ID) +- `entities_id` → `glpi_entities` (nome ou ID) +- `itilcategories_id` → `glpi_itilcategories` +- `locations_id` → `glpi_locations` +- `tickets_states`, `priority`, `urgency`, `impact` (esses já são enums numéricos GLPI — pode aceitar string como atalho: "Alta"=4) + +**Sempre que aceitar data:** usar `normalizeDate`. + +**Sempre que houver múltiplas validações:** agregar erros. + +**Sempre na resposta de sucesso:** retornar `applied`. + +## Anti-padrões a evitar + +| Anti-padrão | Por quê é ruim | +|---|---| +| Aceitar só ID int | LLM precisa fazer N chamadas exploratórias | +| Mensagem de erro do tipo "Erro ao criar projeto" | LLM não sabe o que corrigir | +| Lançar exception no primeiro erro | LLM corrige 1 problema por vez | +| Não validar antes do ORM | Erros de SQL/ORM são incompreensíveis pra LLM | +| Não retornar input aplicado | LLM perde rastreabilidade do que foi gravado | + +## Referências de implementação + +- `src/ProjectTools.php` — handler `handleCreate` (referência canônica deste padrão) +- `src/ToolRegistry.php` — registro central de tools +- Spec MCP — Tool Input Schemas: https://spec.modelcontextprotocol.io/specification/server/tools + +## Próxima evolução + +Extrair os helpers (`resolveReference`, `normalizeDate`) para uma classe utilitária `src/InputValidation.php` quando o segundo handler precisar do mesmo padrão (provavelmente `glpi_project_update` ou `glpi_ticket_create` com `users_id`/`itilcategories_id`). diff --git a/records/plugin-dev/KB-PLUGIN-023-mcprotocol-inputvalidation-extracted.md b/records/plugin-dev/KB-PLUGIN-023-mcprotocol-inputvalidation-extracted.md new file mode 100644 index 0000000..e5a0b41 --- /dev/null +++ b/records/plugin-dev/KB-PLUGIN-023-mcprotocol-inputvalidation-extracted.md @@ -0,0 +1,85 @@ +--- +id: KB-PLUGIN-023 +title: mcprotocol — InputValidation extraída como classe utilitária compartilhada +domain: plugin-dev +tags: + - mcp + - tools + - validation + - refactor + - patterns +status: active +severity: medium +created_at: 2026-05-26 +updated_at: 2026-05-26 +applies_to: + - glpi-11 + - mcprotocol-plugin +related_records: + - KB-PLUGIN-022 + - KB-PLUGIN-019 +--- + +# mcprotocol — InputValidation extraída como classe utilitária compartilhada + +## Contexto + +KB-PLUGIN-022 documentou os padrões `resolveReference`, `normalizeDate`, validação agregada e response com `applied` como métodos privados em `ProjectTools`. Esse KB previu que **na introdução do segundo handler** os helpers seriam extraídos para uma classe utilitária. Esse momento chegou ao implementar `glpi_projecttask_create`. + +## Decisão + +Extraído `src/InputValidation.php` (namespace `GlpiPlugin\Mcprotocol`) com métodos **estáticos públicos** reutilizáveis em qualquer handler de tool: + +| Método | Responsabilidade | +|---|---| +| `resolveReference($table, $value, $field, &$errors, $nameCol='name')` | Aceita int ou string e devolve ID validado. Suporta coluna de busca customizada (ex: `name` em `glpi_users`). | +| `normalizeDate($value, $field, &$errors)` | Aceita `YYYY-MM-DD` ou `YYYY-MM-DD HH:MM:SS`, normaliza pro formato GLPI. | +| `validateDateRange($start, $end, $startField, $endField, &$errors)` | Garante `end >= start` quando ambos informados. | +| `validateIntRange($value, $field, $min, $max, &$errors)` | Valida inteiro em intervalo (ex: `percent_done` 0-100). | + +## Mudanças realizadas + +- **Novo arquivo:** `src/InputValidation.php` +- **Refatorado:** `src/ProjectTools.php` — removeu helpers privados, agora chama `InputValidation::*`. Também ganhou método `buildInputFromArgs($args, $isUpdate)` que centraliza a montagem do input para create/update (DRY). +- **Estendido:** `glpi_project_update` agora aceita os mesmos campos do create (datas, estado, tipo) — antes só aceitava name/content/percent_done. +- **Novo arquivo:** `src/ProjectTaskTools.php` — usa `InputValidation` desde o início, com 2 tools: `glpi_projecttask_create` e `glpi_projecttask_get`. +- **Atualizado:** `src/ToolRegistry.php` — registra `ProjectTaskTools::getTools()` no merge. + +## Convenções de uso + +1. **Acumular erros** — qualquer handler que valida múltiplos campos deve usar a assinatura `&$errors` e lançar UMA exception ao final. Nunca múltiplas. + +2. **Coluna de busca por nome** — `resolveReference` aceita `$nameCol`. Para tabelas onde o campo descritivo não se chama `name`, passar explicitamente: + ```php + InputValidation::resolveReference('glpi_users', $args['users_id'], 'users_id', $errors, 'name'); + ``` + +3. **Validação prévia ao ORM** — todos os helpers rodam ANTES de `$obj->add()` ou `$obj->update()`. Se houver erro, exception é lançada e o ORM nunca é chamado. Evita erros de SQL/ORM crípticos. + +4. **Response com `applied`** — handlers devem sempre retornar o input efetivamente aplicado: + ```php + return ['status'=>'success', 'id'=>$newId, 'message'=>'...', 'applied'=>$input]; + ``` + +## Tabelas comuns para `resolveReference` + +| Tabela | Coluna de nome | Uso típico | +|---|---|---| +| `glpi_projectstates` | `name` | Estado de projeto/tarefa | +| `glpi_projecttypes` | `name` | Tipo de projeto | +| `glpi_projecttasktypes` | `name` | Tipo de tarefa | +| `glpi_users` | `name` (login) | Responsável | +| `glpi_groups` | `name` | Grupo | +| `glpi_entities` | `name` | Entidade | +| `glpi_itilcategories` | `name` | Categoria de chamado | +| `glpi_locations` | `name` | Localização | + +## Próximos handlers candidatos a usar `InputValidation` + +- `glpi_ticket_create` — `users_id_recipient`, `users_id_lastupdater`, `itilcategories_id`, datas +- `glpi_ticket_update` — idem + estados (1=New, 2=Processing, etc. já são enums numéricos GLPI) +- Futuras tools de Computer/Asset (`glpi_computer_create`, etc.) + +## Validação end-to-end + +Padrão validado em produção criando o projeto VALGROUP (28 tarefas em hierarquia) através das tools. Erros de validação retornados claramente, datas normalizadas, estados resolvidos por nome ("New" → 1, "Processing" → 2). Aplicado conforme esperado. diff --git a/records/plugin-dev/KB-PLUGIN-024-mcprotocol-delete-item-silent-success-bug.md b/records/plugin-dev/KB-PLUGIN-024-mcprotocol-delete-item-silent-success-bug.md new file mode 100644 index 0000000..d5d2e00 --- /dev/null +++ b/records/plugin-dev/KB-PLUGIN-024-mcprotocol-delete-item-silent-success-bug.md @@ -0,0 +1,163 @@ +--- +id: KB-PLUGIN-024 +title: mcprotocol — Bug duplo no glpi_delete_item (sucesso silencioso + ProjectTask não exposto em v2) +domain: plugin-dev +tags: + - mcp + - bug + - glpi + - rest-api + - delete + - error-handling +status: active +severity: high +created_at: 2026-05-26 +updated_at: 2026-05-26 +applies_to: + - glpi-11 + - mcprotocol-plugin +related_records: + - KB-PLUGIN-019 + - KB-PLUGIN-016 +--- + +# mcprotocol — Bug duplo no glpi_delete_item (sucesso silencioso + ProjectTask não exposto em v2) + +## Resumo + +Ao tentar deletar uma `ProjectTask` via `glpi_delete_item`, a tool retorna sucesso ao cliente mas o item **continua existindo no banco**. Investigação revelou dois bugs sobrepostos. + +## Evidência + +Comando que reproduz: + +```json +{"method":"tools/call","params":{ + "name":"glpi_delete_item", + "arguments":{"itemtype":"ProjectTask","id":30,"force_purge":true} +}} +``` + +Resposta MCP: aparenta sucesso (sem JSON-RPC error). +Banco: registro ID 30 segue em `glpi_projecttasks` com `is_deleted=0`. + +## Causa raiz #1 — REST API v2 do GLPI não expõe ProjectTask + +Verificado por probes: + +| Endpoint | HTTP | +|---|---| +| `GET /api.php/v2/Project` | 200 ✅ | +| `GET /api.php/v2/ProjectTask` | 404 ❌ | +| `DELETE /api.php/v2/Project/4/ProjectTask/30` | 404 ❌ | +| `DELETE /api.php/v2/Project/ProjectTask/30` | 404 ❌ | +| `DELETE /api.php/v2/Assistance/ProjectTask/30` | 404 ❌ | + +`ProjectTask` simplesmente **não está no roteamento público da v2** desta build do GLPI 11. Não há caminho documentado. Pode ser limitação da própria release (não confirmado se evolui em versões futuras). + +## Causa raiz #2 — `makeRequest` não valida HTTP status + +Em `src/Server.php`, o handler de `glpi_delete_item` faz: + +```php +$response = $this->makeRequest('DELETE', "/$itemtype/{$args['id']}$purge"); +break; +``` + +O método `makeRequest()` retorna o body da resposta sem checar o código HTTP. Quando a API retorna `{"status":"ERROR_ITEM_NOT_FOUND"}` com HTTP 404, esse JSON vira o `content[0].text` da resposta MCP — que do ponto de vista do JSON-RPC parece sucesso (não tem campo `error` no envelope JSON-RPC). + +O cliente (Python, LLM, etc.) que checa apenas `if "error" in response` é enganado. + +## Impacto + +- **Alto:** silenciosamente perde operações de delete sem alertar o usuário/LLM +- LLM acredita que removeu, segue trabalhando com base nessa premissa falsa +- Afeta qualquer itemtype que não esteja na v2 do GLPI (ProjectTask confirmado; outros podem estar afetados — `KnowbaseItem`, `Document_Item`, etc., precisam ser testados) + +## Mitigações temporárias + +1. **Não usar `glpi_delete_item` para ProjectTask** — usar SQL direto ou criar tool de domínio (`glpi_projecttask_delete`) com ORM `(new ProjectTask())->delete(['id' => $id], true)`. +2. **Cliente checa `applied`** — tools de domínio do plugin retornam `applied`. Para tools genéricas, o cliente deve fazer `GET` após `DELETE` pra confirmar remoção (verificação ativa). + +## Correções recomendadas + +### Correção #1 (rápida, alta prioridade) — Validar HTTP status no `makeRequest` + +Em `src/Server.php`, modificar `makeRequest` para propagar 4xx/5xx como exception: + +```php +private function makeRequest(string $method, string $path, ?array $body = null): array { + // ... cURL setup ... + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + $decoded = json_decode($response, true) ?? []; + if ($httpCode >= 400) { + $msg = $decoded['title'] ?? $decoded['status'] ?? 'Unknown error'; + $detail = $decoded['detail'] ?? ''; + throw new \Exception("GLPI API {$method} {$path} → HTTP {$httpCode}: {$msg}" . ($detail ? " ({$detail})" : '')); + } + return $decoded; +} +``` + +Assim qualquer 404/422/500 da API vira erro JSON-RPC visível para o cliente. + +### Correção #2 (evolução) — Criar tools de domínio para ProjectTask + +Adicionar em `src/ProjectTaskTools.php`: + +```php +'glpi_projecttask_delete' => [ + 'description' => 'Remove uma tarefa de projeto. Bypassa REST API v2 (não exposta).', + 'inputSchema' => [...], + 'handler' => [self::class, 'handleDelete'] +], +``` + +Handler usa ORM diretamente (não a REST API): + +```php +public static function handleDelete(array $args) { + $task = new \ProjectTask(); + if (!$task->can($args['id'], DELETE)) throw new \Exception("Acesso Negado."); + if (!$task->delete(['id' => $args['id']], (bool)($args['force_purge'] ?? true))) { + throw new \Exception("Erro ao remover tarefa."); + } + return ['status'=>'success', 'id'=>$args['id'], 'message'=>'Tarefa removida.']; +} +``` + +### Correção #3 (defensiva) — Probe na lista de tools + +Documentar em `glpi_delete_item` quais itemtypes são conhecidamente quebrados na v2. Ou no startup do plugin, fazer um `OPTIONS` em itemtypes comuns e marcar quais funcionam. + +## Como reproduzir + +```bash +# 1. criar uma ProjectTask +curl -s -X POST .../mcp.php -H "Authorization: Bearer $TOKEN" \ + -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"glpi_projecttask_create","arguments":{"projects_id":4,"name":"X"}}}' + +# 2. tentar deletar (parece sucesso) +curl -s -X POST .../mcp.php -H "Authorization: Bearer $TOKEN" \ + -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"glpi_delete_item","arguments":{"itemtype":"ProjectTask","id":,"force_purge":true}}}' + +# 3. confirmar que o registro continua +docker exec glpi11-mariadb mariadb -uglpi -pglpi_local_dev glpi \ + -e "SELECT id,name,is_deleted FROM glpi_projecttasks WHERE id=;" +``` + +## Status + +- Bug **identificado** e **documentado** +- Correção #1 (`makeRequest`) priorizada — afeta TODAS as tools genéricas REST +- Correção #2 (`glpi_projecttask_delete`) pode ser implementada junto da próxima rodada do roadmap +- Correção #3 (probe/documentação) — backlog + +## Lições + +1. **Tools wrapper REST nunca confiam em corpo da resposta sem checar HTTP status** — sempre validar. +2. **Tools de domínio (ORM) são mais robustas** — não dependem do estado da REST API, usam o GLPI diretamente em PHP. +3. **`applied` em response** ajuda o cliente confiar — mas não substitui checagem ativa pós-delete. +4. **REST API v2 do GLPI tem buracos** — não cobre todos os itemtypes. Validar caso a caso antes de assumir que `glpi_*_item` genérico funciona. diff --git a/records/plugin-dev/KB-PLUGIN-025-mcprotocol-date-timezone-handling.md b/records/plugin-dev/KB-PLUGIN-025-mcprotocol-date-timezone-handling.md new file mode 100644 index 0000000..6b1f612 --- /dev/null +++ b/records/plugin-dev/KB-PLUGIN-025-mcprotocol-date-timezone-handling.md @@ -0,0 +1,150 @@ +--- +id: KB-PLUGIN-025 +title: mcprotocol — Comportamento de timezone em datas sem hora explícita +domain: plugin-dev +tags: + - mcp + - tools + - validation + - timezone + - gotcha +status: active +severity: low +created_at: 2026-05-26 +updated_at: 2026-05-26 +applies_to: + - glpi-11 + - mcprotocol-plugin +related_records: + - KB-PLUGIN-022 + - KB-PLUGIN-023 +--- + +# mcprotocol — Comportamento de timezone em datas sem hora explícita + +## Resumo + +`InputValidation::normalizeDate` aceita `YYYY-MM-DD` e expande para `YYYY-MM-DD 00:00:00` **sem informação de timezone**. O PHP do container GLPI está em `date.timezone=UTC`, então o valor enviado ao MariaDB é tratado como UTC e gravado como tal em colunas `timestamp`. + +Para usuários sem TZ pessoal configurado (caso padrão do GLPI), esse comportamento é **transparente** — a UI exibe a data exatamente como recebida. Para usuários com TZ regional configurado (ex: `America/Sao_Paulo`), a data aparece deslocada nas horas correspondentes (`-03:00` → "24/05 21:00" em vez de "25/05 00:00"). + +## Detalhamento técnico + +Cadeia de TZ no ambiente atual: + +| Camada | TZ | +|---|---| +| Container app (`glpi11-app`) — clock | `-03` (São Paulo) | +| PHP (`date.timezone`) | `UTC` | +| Container MariaDB (`glpi11-mariadb`) — clock | `-03` | +| MariaDB session (`@@session.time_zone`) | `SYSTEM` (= `-03`) | +| GLPI config (`glpi_configs.timezone`) | `0` (UTC default) | +| Usuário `glpi.timezone` | `NULL` (sem override) | + +Quando `normalizeDate('2026-05-25')` retorna `2026-05-25 00:00:00`: + +1. PHP envia a string para o ORM do GLPI +2. ORM passa adiante para MariaDB +3. MariaDB com sessão em `SYSTEM` interpretaria a string como horário local — mas o GLPI executa `SET time_zone='+00:00'` ao conectar (verificado em `Glpi\System\Diagnostic`), então a sessão fica UTC +4. Valor gravado: `2026-05-25 00:00:00 UTC` ✅ + +Confirmado por probe: +```sql +SET @@session.time_zone='+00:00'; +SELECT plan_start_date FROM glpi_projecttasks WHERE id=33; +-- 2026-05-25 00:00:00 ← correto +``` + +REST API v2 retorna com sufixo explícito: +```json +"plan_start_date": "2026-05-25T00:00:00+00:00" +``` + +## Gotcha — Usuários com TZ regional + +Cenário problemático: + +1. Admin cria usuário "joao" e seta `joao.timezone = 'America/Sao_Paulo'` no perfil +2. LLM cria tarefa via plugin: `plan_start_date: "2026-05-25"` +3. Banco grava: `2026-05-25 00:00:00 UTC` +4. UI do João renderiza convertendo pra SP: `24/05/2026 21:00:00` + +O João vê **dia 24** em vez de **dia 25**. Confusão garantida. + +## Mitigações + +### Curto prazo — documentação + +Adicionar nota na descrição do schema dos campos `plan_start_date`/`plan_end_date`: + +> "Datas sem hora são interpretadas como 00:00 UTC. Para usuários em fusos +> regionais, recomenda-se enviar com hora explícita no fuso de interesse, +> ex: '2026-05-25 03:00:00' para meia-noite em São Paulo (UTC-3)." + +### Médio prazo — normalização inteligente + +Estender `InputValidation::normalizeDate` para aceitar um TZ contextual e, quando data vier sem hora, expandir como meia-noite naquele TZ convertido para UTC: + +```php +public static function normalizeDate( + string $value, + string $field, + array &$errors, + ?string $tz = null // novo parâmetro opcional +): ?string { + $tz = $tz ?? self::resolveContextTz(); + if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $value)) { + $dt = new \DateTimeImmutable("$value 00:00:00", new \DateTimeZone($tz)); + return $dt->setTimezone(new \DateTimeZone('UTC'))->format('Y-m-d H:i:s'); + } + // ... resto igual +} + +private static function resolveContextTz(): string { + // Ordem: user preference → glpi config → UTC + $userTz = $_SESSION['glpi_tz'] ?? null; + if ($userTz) return $userTz; + global $CFG_GLPI; + return $CFG_GLPI['timezone'] ?? 'UTC'; +} +``` + +Assim a data "2026-05-25" enviada por um LLM operando em nome de um usuário com TZ=SP resultaria em `2026-05-25 03:00:00 UTC`, que renderiza corretamente como `25/05/2026 00:00` em SP. + +### Longo prazo — schema MCP com TZ embutido + +Para datas com hora, padronizar input em ISO 8601 com TZ explícito: +`"2026-05-25T00:00:00-03:00"` ou `"2026-05-25T00:00:00Z"`. + +`normalizeDate` aceitaria também esse formato e converteria para UTC. + +## Diagnóstico de campo + +Comando para auditar o estado real vs. UI: + +```bash +# 1. Confirma TZ do user atual +SELECT name, timezone FROM glpi_users WHERE name = 'X'; + +# 2. Lê data forçando UTC (valor real armazenado) +SET @@session.time_zone='+00:00'; +SELECT plan_start_date FROM WHERE id=N; + +# 3. Lê data no TZ do user (o que a UI vai mostrar) +SET @@session.time_zone='America/Sao_Paulo'; +SELECT plan_start_date FROM WHERE id=N; +``` + +## Status + +- **Comportamento atual:** correto para o caso padrão (sem TZ regional) +- **Não há bug** — é uma gap de UX prevista da extensão de schema +- **Mitigação curto prazo:** documentação no schema +- **Mitigação ideal:** TZ contextual no `normalizeDate` (backlog do plugin) + +## Lições + +1. `timestamp` no MariaDB sempre armazena em UTC, independente da sessão +2. Strings de data sem TZ explícito dependem do `@@session.time_zone` para interpretação +3. GLPI já normaliza a sessão para UTC ao conectar — bom comportamento +4. Mas o **input** do plugin precisa de TZ contextual para ser semanticamente correto diff --git a/records/plugin-dev/KB-PLUGIN-026-mcprotocol-roadmap-snapshot-v1.2.md b/records/plugin-dev/KB-PLUGIN-026-mcprotocol-roadmap-snapshot-v1.2.md new file mode 100644 index 0000000..24246cb --- /dev/null +++ b/records/plugin-dev/KB-PLUGIN-026-mcprotocol-roadmap-snapshot-v1.2.md @@ -0,0 +1,147 @@ +--- +id: KB-PLUGIN-026 +title: mcprotocol — Snapshot de roadmap v1.2 (pós-ProjectTask + InputValidation) +domain: plugin-dev +tags: + - mcp + - roadmap + - snapshot + - planning +status: active +severity: medium +created_at: 2026-05-26 +updated_at: 2026-05-26 +applies_to: + - glpi-11 + - mcprotocol-plugin +related_records: + - KB-PLUGIN-019 + - KB-PLUGIN-020 + - KB-PLUGIN-021 + - KB-PLUGIN-022 + - KB-PLUGIN-023 + - KB-PLUGIN-024 + - KB-PLUGIN-025 +--- + +# mcprotocol — Snapshot de roadmap v1.2 (pós-ProjectTask + InputValidation) + +## Contexto + +Esta KB é um **ponto de salvamento** do estado do plugin mcprotocol após a rodada de evolução de tools de Project + ProjectTask + extração da camada de validação. Serve de referência canônica para o estado atual e backlog priorizado. + +## Estado atual das tools + +### Tools de domínio (ORM direto) + +| Tool | Status | Notas | +|---|---|---| +| `glpi_ticket_get` | ✅ | | +| `glpi_ticket_create` | ✅ | | +| `glpi_ticket_update` | ✅ | | +| `glpi_ticket_add_followup` | ✅ | | +| `glpi_ticket_add_solution` | ✅ | | +| `glpi_project_get` | ✅ | Retorna tarefas com `projecttasks_id` (hierarquia) | +| `glpi_project_create` | ✅ | Aceita estado/tipo por nome, datas, validação prévia | +| `glpi_project_update` | ✅ | **Estendido** com os mesmos campos do create + `percent_done` | +| `glpi_projecttask_get` | ✅ NOVO | | +| `glpi_projecttask_create` | ✅ NOVO | Suporta hierarquia (parent task), responsável, duração, estado | +| `glpi_ticket_search_by_status` | ✅ | | + +### Tools genéricas (REST API v2) + +| Tool | Status | Observação | +|---|---|---| +| `glpi_get_items` | ⚠️ | Funcional, mas confiável só para itemtypes expostos na v2 | +| `glpi_get_item` | ⚠️ | Idem | +| `glpi_add_item` | ⚠️ | Idem | +| `glpi_update_item` | ⚠️ | Idem | +| `glpi_delete_item` | ❌ | Bug do "silent success" — ver KB-PLUGIN-024 | + +### Infraestrutura + +| Componente | Status | +|---|---| +| `src/Server.php` | ✅ Funcional. **Pendente:** validar HTTP status em `makeRequest` (KB-024 Correção #1) | +| `src/ToolRegistry.php` | ✅ Registra Ticket + Project + ProjectTask | +| `src/InputValidation.php` | ✅ NOVO — helpers compartilhados (KB-PLUGIN-023) | +| `src/TicketTools.php` | ✅ Ainda usa padrão antigo — candidato a migrar pro `InputValidation` | +| `src/ProjectTools.php` | ✅ Refatorado pra usar `InputValidation` | +| `src/ProjectTaskTools.php` | ✅ NOVO | +| `ajax/mcp.php` | ✅ Endpoint stateless com JWT | + +## Casos reais executados + +| Cliente | Projeto | Tarefas | Resultado | +|---|---|---|---| +| **VALGROUP** | Implantação GLPI | 7 macros + 21 subs (hierarquia) | ✅ Criado end-to-end via plugin | +| **TCERJ** | Implantação GLPI | 6 tarefas (datas replanejadas) | ✅ Criado end-to-end via plugin | + +Ambos comprovam o padrão de tools nativo funcionando em fluxo real de comercial → projeto operacional. + +## Bugs conhecidos + +| ID | Severidade | Resumo | +|---|---|---| +| KB-PLUGIN-024 | Alta | `glpi_delete_item` retorna sucesso silencioso quando API retorna 404 | +| KB-PLUGIN-024 | Média | `ProjectTask` não exposto na REST v2 do GLPI (precisa tool de domínio) | +| KB-PLUGIN-025 | Baixa | Datas sem hora explícita podem desalinhar em UI de usuários com TZ regional | + +## Roadmap priorizado + +### Próxima rodada (P1) + +1. **Correção #1 do KB-024** — `makeRequest` deve validar HTTP status e propagar 4xx/5xx como exception. Afeta todas as tools genéricas REST. Impacto alto, esforço baixo. + +2. **`glpi_projecttask_delete`** — Correção #2 do KB-024. Tool de domínio bypassa REST API, usa ORM direto. + +3. **`glpi_projecttask_update`** — completar o CRUD de tarefas. Necessário para casos como "atualizar percent_done", "remarcar datas". + +4. **`glpi_project_get_open_tasks`** — Tool semântica para casos de uso N1/PM. Filtra tarefas com `percent_done < 100` e `projectstates_id != Closed`. + +### Médio prazo (P2) + +5. **Migrar `TicketTools` para `InputValidation`** — eliminar dívida técnica, padronizar erros, ergonomia de nomes vs IDs em campos como `users_id_recipient`, `itilcategories_id`. + +6. **Tools do roadmap original (KB-020)** ainda pendentes: + - `glpi_ticket_get_pending_approvals` + - `glpi_ticket_assign_to_me` + - `glpi_ticket_add_private_note` + - `glpi_computer_get_unassigned` + - `glpi_user_get_my_assets` + +7. **TZ contextual em `normalizeDate`** — Correção média do KB-025, expande data sem hora no TZ do GLPI/usuário. + +### Longo prazo (P3 — visão estratégica) + +8. **Resources MCP** (KB-PLUGIN-021) — habilitar caso de uso "LLM como N1": + - `kb://{id}` — artigos da KB do GLPI + - `ticket://{id}` — ticket completo com followups/solutions + - `document://{id}` — anexos + - `computer://{id}` — CI técnico +9. **SSE / Streamable HTTP completo** — notificações push (novo ticket, SLA estourando) +10. **OAuth2 com escopos finos** — separar permissões por escopo de tool + +## Métricas atuais + +- **Tools registradas:** 16 (11 domínio + 5 genéricas) +- **Casos reais validados:** 2 projetos, 34 tarefas +- **KBs do plugin:** 14 (incluindo este snapshot) +- **Bugs ativos:** 3 (1 alta, 1 média, 1 baixa) +- **Versão atual do plugin:** 1.1.0 (próxima release: 1.2.0) + +## Próxima versão proposta — v1.2.0 + +Conteúdo já no branch `main` do dev (a fazer push): + +- Adicionada `src/InputValidation.php` com helpers compartilhados +- Adicionada `src/ProjectTaskTools.php` com `glpi_projecttask_get` e `glpi_projecttask_create` +- Estendida `src/ProjectTools.php` — `update` agora aceita datas, estado, tipo +- Atualizado `src/ToolRegistry.php` registrando `ProjectTaskTools` +- KBs documentando padrões (022, 023), bugs (024, 025) e snapshot (026) + +Conteúdo para v1.2.1+ (correções dos bugs documentados): + +- `Server.php::makeRequest` validando HTTP status +- `ProjectTaskTools::handleDelete` (`glpi_projecttask_delete`) +- `glpi_projecttask_update` diff --git a/records/plugin-dev/KB-PLUGIN-027-mcprotocol-dev-prod-release-flow.md b/records/plugin-dev/KB-PLUGIN-027-mcprotocol-dev-prod-release-flow.md new file mode 100644 index 0000000..85befff --- /dev/null +++ b/records/plugin-dev/KB-PLUGIN-027-mcprotocol-dev-prod-release-flow.md @@ -0,0 +1,172 @@ +--- +id: KB-PLUGIN-027 +title: mcprotocol — Fluxo de release Dev → Prod (manual) +domain: plugin-dev +tags: + - mcp + - release + - workflow + - forgejo + - mindplace +status: active +severity: medium +created_at: 2026-05-26 +updated_at: 2026-05-26 +applies_to: + - glpi-11 + - mcprotocol-plugin + - mindplace-marketplace +related_records: + - KB-PLUGIN-013 + - KB-PLUGIN-018 + - KB-PLUGIN-026 +--- + +# mcprotocol — Fluxo de release Dev → Prod (manual) + +## Contexto + +O ecossistema Mindplace usa **dois Forgejos**: um de **desenvolvimento** (lab) e um de **produção** (Mindtek). O fluxo de release é **híbrido**: push automático para dev via `git push` normal, push para prod via script semi-automatizado. + +A versão do plugin é a **mesma** entre dev e prod — não há suffixo `-dev`, `-rc`, etc. A diferença é qual Forgejo recebeu o push. + +## Topologia + +``` +┌─────────────────────────────┐ +│ Workspace local (CT100) │ +│ /opt/projects/GLPI11/ │ +│ docker/glpi/ │ +│ marketplace/mcprotocol │ +└──────────────┬──────────────┘ + │ + │ git push origin main + ▼ +┌─────────────────────────────────────────┐ +│ Forgejo DEV (lab, CT101) │ +│ git@192.168.100.101: │ +│ administrador/mcprotocol.git │ +│ Uso: desenvolvimento, homologação │ +└──────────────┬──────────────────────────┘ + │ + │ homologação OK ─► bin/mindplace-release.sh + ▼ +┌─────────────────────────────────────────┐ +│ Forgejo PROD (Mindtek) │ +│ https://servicedesk.mindtek.com.br/git │ +│ /rodolpho.lopes/mcprotocol │ +│ Uso: release oficial, marketplace │ +└──────────────┬──────────────────────────┘ + │ + │ Mindplace catálogo lê do prod + ▼ +┌─────────────────────────────────────────┐ +│ Mindplace (catálogo) │ +│ plugins.json no repo "mindplace" │ +│ + Banco Postgres /api/admin/sync │ +└─────────────────────────────────────────┘ +``` + +## Fluxo passo a passo + +### 1. Desenvolvimento +- Editar código no plugin (`src/`, `setup.php`, etc.) +- Testar localmente contra `https://glpi.lab.coretoai.com/` + +### 2. Push em Dev +```bash +cd /opt/projects/GLPI11/docker/glpi/marketplace/mcprotocol +git add +git commit -m "feat/fix/chore: ..." +git push origin main # vai pro Forgejo lab (192.168.100.101) +``` + +### 3. Homologação +- Testar end-to-end via curl ou cliente MCP apontando para o GLPI lab +- Iterar até estável + +### 4. Bump de versão (quando for liberar release) +```php +// setup.php +define('PLUGIN_MCPROTOCOL_VERSION', 'X.Y.Z'); // semver +``` +Commit + push para dev primeiro. + +### 5. Push em Prod (manual, via script) +```bash +cd /opt/projects/MindPlace +./bin/mindplace-release.sh ./docker/glpi/plugins/mcprotocol +# Ou se o plugin estiver em outro caminho: +./bin/mindplace-release.sh /opt/projects/GLPI11/docker/glpi/marketplace/mcprotocol +``` + +O script executa 5 fases: +1. Cria/valida repo no Forgejo de prod (`servicedesk.mindtek.com.br/git/rodolpho.lopes/mcprotocol`) +2. `git push` do código pro Forgejo de prod +3. Gera ZIP do plugin (vide KB-PLUGIN-018 sobre wrapper conventions) +4. Cria release `vX.Y.Z` + upload do ZIP como release asset +5. Atualiza `plugins.json` no repo `mindplace` (catálogo central) + +Pré-requisitos: +- `FORGEJO_TOKEN` em `/opt/projects/MindPlace/bin/.forgejo-token` (gitignored) +- Token com escopo `write:repository, write:user` no Forgejo de prod +- Acesso ao repo `mindplace` (catálogo) + +### 6. Detecção pelo Mindplace +- **Hoje:** `GET /api/admin/sync` chama o endpoint do Mindplace, que faz `upsert` na tabela `App` (Prisma) para cada repo do Forgejo de prod. +- **Em prod**: este fluxo funciona corretamente — o catálogo reflete os plugins. +- **Gaps conhecidos** (vide diagnóstico em CT100, lab): + - Sync é pull manual, não tem trigger por webhook + - Schema `App` não tem campo `version` — não detecta release nova + - `forgejoService.getLatestRelease()` existe mas o endpoint `/sync` não o usa + - Não há sistema de notificação de update + +## Convenções + +| Item | Convenção | +|---|---| +| Versão | SemVer (`MAJOR.MINOR.PATCH`) | +| Dev e prod com versão idêntica | Sim — mesma versão em ambos os ambientes | +| Branch principal | `main` em ambos Forgejos | +| Mensagem de commit | Prefixos `feat:`, `fix:`, `chore:`, `docs:` (Conventional Commits) | +| Bump de versão | Commit isolado tipo `chore(release): bump X.Y.Z → X.Y.W` | +| Tag de release | Criada **só** no Forgejo de prod, pelo script (`vX.Y.Z`) | + +## Quando usar dev vs prod + +| Situação | Forgejo | +|---|---| +| Desenvolvimento em andamento, testes da lab | Dev (192.168.100.101) | +| Validação com cliente piloto interno | Dev | +| Pronto pra ir pra marketplace (catálogo público) | Prod (servicedesk.mindtek.com.br) | +| Hotfix urgente em cliente | Pula homologação? Avaliar caso a caso | + +## Recuperação + +### Reescrita de histórico no dev +- Forçar push com `--force-with-lease` (preferível) ou `--force` (irreversível para colaboradores) +- Sempre comunicar a equipe se há mais de 1 colaborador no repo + +### Rollback de release em prod +1. Reverter o commit problemático no repo do plugin +2. Bump de PATCH (ex: 1.1.1 → 1.1.2 com fix) +3. Re-rodar `mindplace-release.sh` +4. **Não deletar release antiga** — Mindplace pode ter clientes apontando para ela + +## Roadmap para automação da Fase 4 (futuro) + +Gaps que valem ser fechados quando houver bandwidth: + +1. **Webhook Forgejo → Mindplace** — disparar sync quando push acontece no prod +2. **Schema `App.version`** — capturar versão atual + permitir comparação +3. **Notificação de update** — e-mail/push pra clientes com licenças ativas quando há release nova +4. **Cron de sync periódico** — backup pra caso webhook falhe (ex: a cada 15min) + +Estes gaps estão documentados também no diagnóstico do plugin (KB-PLUGIN-026, seção "Próximos passos do Mindplace"). + +## Status atual + +- **Fluxo 1-3 (dev):** ✅ funcionando, automação simples via `git push` +- **Fluxo 5 (prod):** ✅ funcionando em produção, validado com plugins anteriores +- **Fluxo 6 (Mindplace catalog):** ✅ funcionando em produção, embora seja pull manual +- **Detecção automática de versão / notificação:** ⏳ backlog diff --git a/records/plugin-dev/KB-PLUGIN-028-glpi11-plugin-rights-and-menu-visibility.md b/records/plugin-dev/KB-PLUGIN-028-glpi11-plugin-rights-and-menu-visibility.md new file mode 100644 index 0000000..98c73e6 --- /dev/null +++ b/records/plugin-dev/KB-PLUGIN-028-glpi11-plugin-rights-and-menu-visibility.md @@ -0,0 +1,543 @@ +--- +id: KB-PLUGIN-028 +title: GLPI 11 — Plugin com menu próprio precisa registrar rights via Profile +domain: plugin-dev +tags: + - glpi11 + - plugin + - profile + - rights + - menu + - sidebar + - gotcha +status: active +severity: high +created_at: 2026-06-01 +updated_at: 2026-06-01 +applies_to: + - glpi-11 + - estimate-plugin + - webapplications-plugin +related_records: + - KB-PLUGIN-013 +--- + +# GLPI 11 — Plugin com menu próprio precisa registrar rights via Profile + +## Sintoma + +Plugin instalado, ativo e habilitado no marketplace, classes carregando corretamente via autoload, **mas o menu não aparece** na sidebar do GLPI (nem o item dentro de Gerência / Helpdesk / Administração). + +Não há erro no log — o menu é simplesmente omitido silenciosamente. + +## Causa raiz + +GLPI executa `Session::haveRight($rightname, READ)` antes de renderizar qualquer entrada de menu de um itemtype. Se o **right não existir** na tabela `glpi_profilerights` (jamais foi cadastrado pelo plugin), `haveRight()` retorna `0` e o menu é ocultado. + +Verificação: +```sql +SELECT COUNT(*) FROM glpi_profilerights WHERE name LIKE '%plugin_meuplugin%'; +-- retorna 0 → você está com o problema +``` + +## Solução padrão GLPI 11 + +Plugins que expõem itemtypes próprios DEVEM ter uma classe `Profile` que: +1. Estende `\Profile` +2. Expõe `getAllRights()` listando os rights +3. Implementa `initProfile()` para registrar os rights em `glpi_profilerights` +4. Implementa `createFirstAccess($profile_id)` para conceder full access ao perfil que instalou + +### Esqueleto da classe `src/Profile.php` + +```php +getType() === 'Profile' && $item->getField('interface') === 'central') { + return self::createTabEntry(MeuItemtype::getTypeName(2)); + } + return ''; + } + + public static function displayTabContentForItem(CommonGLPI $item, $tabnum = 1, $withtemplate = 0) + { + if ($item->getType() === 'Profile') { + self::addDefaultProfileInfos($item->getID(), [ + 'plugin_meuplugin_x' => 0, + ]); + (new self())->showForm($item->getID()); + } + return true; + } + + public static function getAllRights($all = false): array + { + return [ + [ + 'itemtype' => MeuItemtype::class, + 'label' => MeuItemtype::getTypeName(2), + 'field' => 'plugin_meuplugin_x', + ], + ]; + } + + public static function initProfile(): void + { + $dbu = new DbUtils(); + foreach ((new self())->getAllRights(true) as $data) { + if ($dbu->countElementsInTable('glpi_profilerights', ['name' => $data['field']]) === 0) { + ProfileRight::addProfileRights([$data['field']]); + } + } + } + + public static function createFirstAccess($profiles_id): void + { + self::addDefaultProfileInfos($profiles_id, [ + 'plugin_meuplugin_x' => READ + CREATE + UPDATE + DELETE + PURGE, + ], true); + } + + public static function addDefaultProfileInfos($profiles_id, array $rights, bool $drop_existing = false): void + { + $dbu = new DbUtils(); + $profileRight = new ProfileRight(); + foreach ($rights as $name => $value) { + $exists = $dbu->countElementsInTable('glpi_profilerights', [ + 'profiles_id' => $profiles_id, 'name' => $name, + ]) > 0; + if ($exists && $drop_existing) { + $profileRight->deleteByCriteria(['profiles_id' => $profiles_id, 'name' => $name]); + $exists = false; + } + if (!$exists) { + $profileRight->add([ + 'profiles_id' => $profiles_id, + 'name' => $name, + 'rights' => $value, + ]); + } + } + } +} +``` + +### Registro em `setup.php` + +```php +$PLUGIN_HOOKS['change_profile']['meuplugin'] = [ + 'GlpiPlugin\\Meuplugin\\Profile', 'initProfile' +]; + +Plugin::registerClass('GlpiPlugin\\Meuplugin\\Profile', [ + 'addtabon' => ['Profile'] +]); +``` + +### Chamada no `hook.php` (install) + +```php +function plugin_meuplugin_install(): bool +{ + // ... criação de tabelas ... + + \GlpiPlugin\Meuplugin\Profile::initProfile(); + if (isset($_SESSION['glpiactiveprofile']['id'])) { + \GlpiPlugin\Meuplugin\Profile::createFirstAccess( + (int) $_SESSION['glpiactiveprofile']['id'] + ); + } + return true; +} +``` + +## Gotcha — Instalação via CLI + +`bin/console glpi:plugin:install -f ` roda **sem sessão GLPI**, então `$_SESSION['glpiactiveprofile']['id']` é `null` e `createFirstAccess()` não é executado. Resultado: rights são registrados na tabela mas com valor `0` (sem permissão real). + +### Mitigação A — Web-first install + +Recomendado: ativar o plugin pela primeira vez **via interface web** logado como Super-Admin. A sessão está ativa e o `createFirstAccess()` concede full access automaticamente. + +### Mitigação B — Forçar via SQL após CLI install + +Quando install foi via CLI e você precisa destravar rapidamente: + +```sql +UPDATE glpi_profilerights + SET rights = 31 -- READ+CREATE+UPDATE+DELETE+PURGE + WHERE name IN ('plugin_meuplugin_x', 'plugin_meuplugin_y') + AND profiles_id IN (SELECT id FROM glpi_profiles WHERE interface='central'); +``` + +Depois faça logout/login no GLPI pra a sessão recarregar os rights. + +### Mitigação C — install.php que concede pra todos os admins + +Pode-se estender o `install` pra dar full access automaticamente a todos os profiles com `interface='central'`: + +```php +foreach ($DB->request(['FROM' => 'glpi_profiles', 'WHERE' => ['interface' => 'central']]) as $p) { + \GlpiPlugin\Meuplugin\Profile::createFirstAccess((int) $p['id']); +} +``` + +Mas isso pode ser intrusivo — perfis de Observer/Read-Only ganhariam create/delete por padrão. Avaliar caso a caso. + +## Conflito de nomes — `Profile` colide com dropdown + +Se o plugin já tem uma classe `Profile` pra outro conceito (ex: catálogo de perfis de executor, perfis de licenciamento, etc.), há colisão com a classe `Profile` exigida pra rights. + +### Solução + +Renomear a classe que NÃO é a de rights pra algo semanticamente mais claro. Exemplos do plugin Estimate: +- `Profile` (executor) → `ExecutorProfile` +- Tabela mantém `glpi_plugin_estimate_profiles` via override `getTable()` + +```php +class ExecutorProfile extends CommonDropdown +{ + public static function getTable($classname = null) + { + return 'glpi_plugin_estimate_profiles'; + } +} +``` + +A classe `Profile` (extends `\Profile`) **sempre** fica reservada pra gestão de rights. + +## Checklist de validação + +Antes de jogar a culpa no menu/cache, confira: + +```sql +-- 1. Right existe? +SELECT name, COUNT(*) c FROM glpi_profilerights + WHERE name LIKE '%plugin_meuplugin%' GROUP BY name; + +-- 2. Profile atual tem o right ativo (>0)? +SELECT pr.name, pr.rights + FROM glpi_profilerights pr + JOIN glpi_profiles p ON p.id = pr.profiles_id + WHERE p.id = ? AND pr.name LIKE '%plugin_meuplugin%'; +``` + +Se ambos retornam valores válidos (>0), o menu **vai aparecer** após logout/login (sessão precisa recarregar o cache de rights). + +## Lições + +1. **Menu silenciosamente oculto é sintoma clássico de right ausente.** +2. CLI install é parcial — sempre validar com web install ou SQL update. +3. Classe `Profile` (extends `\Profile`) é **convenção rígida** do GLPI — qualquer conceito de "perfil" no domínio do plugin precisa de outro nome. +4. Logout/login é necessário pra a sessão recarregar rights (não basta refresh). + +## Bônus — Páginas `front/` no GLPI 11 NÃO usam `include('inc/includes.php')` + +No GLPI 9/10, todo plugin começava com: +```php +include('../../../inc/includes.php'); +``` + +No **GLPI 11 (Symfony)**, isso quebra porque: + +1. O marketplace pode estar em `/var/glpi/marketplace//` (fora do tree do GLPI core que está em `/var/www/glpi/`) +2. `dirname(__DIR__, 3)` ou `../../../` resolve para path errado +3. O `LegacyFileLoadController` já bootou GLPI/autoload antes de invocar o arquivo + +### Padrão correto GLPI 11 + +Começar direto sem include: + +```php +/front/.php line 7 +``` + +### Gotcha — Funções SQL agregadas no query builder do GLPI + +`$DB->request()` é um query builder que **escapa tudo como nome de coluna** por padrão. Passar `'COUNT(*) AS cnt'` como string em `SELECT` gera SQL inválido: + +``` +MySQL query error: Unknown column 'COUNT(*)' in 'SELECT' +``` + +Porque a query final fica: +```sql +SELECT `plugin_estimate_states_id`, `COUNT(*)` AS `cnt` FROM ... + ^^^^^^^^^^^^ escapado como coluna +``` + +**Solução:** usar `Glpi\DBAL\QueryExpression` pra funções SQL: + +```php +use Glpi\DBAL\QueryExpression; + +$DB->request([ + 'SELECT' => [ + 'plugin_estimate_states_id', + new QueryExpression('COUNT(*) AS ' . $DB->quoteName('cnt')), + ], + 'FROM' => self::getTable(), + 'WHERE' => ['is_deleted' => 0], + 'GROUPBY' => 'plugin_estimate_states_id', +]); +``` + +Vale pra `COUNT(*)`, `SUM()`, `AVG()`, `MAX()`, `MIN()`, `IF()`, `CASE WHEN`, etc. Sempre quotar nomes de coluna referenciados com `$DB->quoteName(...)`. + +### Gotcha — `name` duplicado em templates Twig customizados + +Quando você cria um template Twig pro form do itemtype e estende `generic_show_form.html.twig`, o GLPI **já renderiza automaticamente** os campos padrão (`name`, `entities_id`, datas). Adicionar `fields.textField('name', ...)` no bloco `more_fields` causa **duplicação**. + +```twig +{# ERRADO — duplica o campo Nome #} +{% block more_fields %} + {{ fields.textField('name', item.fields['name'], __('Name')) }} + ... +{% endblock %} + +{# CERTO — só campos adicionais #} +{% block more_fields %} + {# 'name' renderizado pelo generic_show_form #} + {{ fields.dropdownField('Client', 'plugin_client_id', ...) }} + ... +{% endblock %} +``` + +Outros campos auto-renderizados (não duplicar): `id`, `name`, `entities_id`, `is_recursive`, `date_creation`, `date_mod`. + +### Internacionalização de plugin (gettext .po/.mo) + +Plugins GLPI usam **gettext** com domínio por plugin. Cada `__('String', 'estimate')` busca em `/locales/.mo`. + +#### Estrutura + +``` +estimate/ +└── locales/ + ├── pt_BR.po ← fonte editável (UTF-8) + └── pt_BR.mo ← binário compilado (consumido pelo GLPI) +``` + +Sem prefixo de domínio no filename — convenção é só o código de idioma (`_.po/.mo`). + +#### Workflow + +```bash +# 1. Instalar gettext (pacote completo, não só -base) +apt-get install -y gettext # provê msgfmt + +# 2. Editar locales/pt_BR.po (formato gettext padrão) +# 3. Compilar +cd marketplace//locales +msgfmt pt_BR.po -o pt_BR.mo + +# 4. Limpar cache do GLPI +docker exec find /var/glpi/files/_cache -name "locales" -type d -exec rm -rf {} + +``` + +#### Formato `.po` + +``` +msgid "" +msgstr "" +"Language: pt_BR\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +# Termo singular +msgid "Estimate" +msgstr "Estimativa" + +# Termo plural (suporta nplurals) +msgid "Item" +msgid_plural "Items" +msgstr[0] "Item" +msgstr[1] "Itens" + +# Termo com placeholder +msgid "Add %s" +msgstr "Adicionar %s" +``` + +#### Carregamento automático + +O GLPI 11 chama `Plugin::loadLang('')` no boot — não precisa código adicional. Basta o `.mo` estar em `locales/.mo` e o usuário ter `glpilanguage` setado. + +#### Validar tradução via console + +```bash +docker exec php -r " +chdir('/var/www/glpi'); +require 'vendor/autoload.php'; +\$k = new Glpi\Kernel\Kernel('production', false); +\$k->boot(); +\$_SESSION['glpilanguage'] = 'pt_BR'; +\Session::loadLanguage(); +\Plugin::loadLang(''); +echo __('Total hours', '').PHP_EOL; +" +``` + +#### Termos que GLPI core já traduz (não precisa repetir no plugin) + +Estes são traduzidos pelo `.mo` do GLPI core. Use `__('Name')` (sem 2º arg) e herda: + +`Name`, `Description`, `Status`, `Category`, `Color`, `Date`, `Hours`, `Quantity`, +`Currency`, `Document`, `Documents`, `Notes`, `Historical`, `Profile`, `User`, +`Group`, `Entity`, `Add`, `Save`, `Delete`, `Cancel`, e a maioria dos verbos/labels comuns. + +### Constantes úteis disponíveis no front (já definidas) + +| Constante | Valor típico no container | Uso | +|---|---|---| +| `GLPI_ROOT` | `/var/www/glpi` | Path do GLPI core | +| `GLPI_MARKETPLACE_DIR` | `/var/glpi/marketplace` | Onde plugins de marketplace ficam (pode diferir de `GLPI_ROOT`) | +| `GLPI_CONFIG_DIR` | `/var/glpi/config` | Config + chaves OAuth | +| `GLPI_PLUGIN_DOC_DIR` | `/var/glpi/files/_plugins` | Storage de arquivos por plugin | + +## Bônus 2 — Sidebar de tabs no form do itemtype + +GLPI exibe um menu lateral de tabs em cada itemtype (Documento, Itens associados, Notas, Histórico, etc.). Para um plugin replicar isso: + +### Pai (Estimate.php — itemtype principal) — implementa `defineTabs()` + +```php +public function defineTabs($options = []) +{ + $ong = []; + $this->addDefaultFormTab($ong); // form principal (campos do item) + $this->addStandardTab(EstimateItem::class, $ong, $options); // tab "Itens" (filho custom) + $this->addStandardTab('Document_Item', $ong, $options); // anexos nativos + $this->addStandardTab('Notepad', $ong, $options); // notas nativas + $this->addStandardTab('Log', $ong, $options); // histórico nativo + return $ong; +} +``` + +### Filho (EstimateItem.php — entidade que aparece como tab no pai) + +```php +public function getTabNameForItem(CommonGLPI $item, $withtemplate = 0) +{ + if ($item instanceof Estimate) { + $count = (new DbUtils())->countElementsInTable(self::getTable(), [ + self::$items_id => $item->getID(), + ]); + return self::createTabEntry(self::getTypeName(2), $count); + } + return ''; +} + +public static function displayTabContentForItem(CommonGLPI $item, $tabnum = 1, $withtemplate = 0) +{ + if ($item instanceof Estimate) { + self::showForEstimate($item); // sua função de renderização + } + return true; +} +``` + +### Registro em `setup.php` + +```php +Plugin::registerClass('GlpiPlugin\\Estimate\\EstimateItem', [ + 'addtabon' => ['GlpiPlugin\\Estimate\\Estimate'] +]); +``` + +`addtabon` informa ao GLPI que esse itemtype deve aparecer como tab nos itemtypes listados. + +### Tabs nativos que "saem de graça" + +| Tab nativo | Class GLPI | O que faz | +|---|---|---| +| Documentos | `Document_Item` | Anexar arquivos ao itemtype | +| Notas | `Notepad` | Notas privadas do usuário | +| Histórico | `Log` | Audit log automático de mudanças | +| Reservas | `Reservation` | Reservar item por período (CIs) | +| Itens associados | `KnowbaseItem_Item` ou `Item_Devices` | Itens relacionados | + +### Gotcha — `count_on_tabs` + +Mostrar contador na label da tab depende de `$_SESSION['glpishow_count_on_tabs']` (config GLPI). Sempre testar com `?? true`: + +```php +if ($_SESSION['glpishow_count_on_tabs'] ?? true) { + $count = ...; +} +``` + +### Gotcha — Tabs nativos exigem rights + +`Document_Item`, `Notepad`, `Log` etc. fazem check de `canView()` no usuário corrente. No CLI (`bin/console`) a sessão não existe → `addStandardTab` retorna sem adicionar. Validar tabs sempre no **browser logado**, não no console. + +### Linkar itemtype a Ticket / Project (rastreabilidade bidirecional) + +Para que seu plugin apareça nos dropdowns "tipo de item" ao **adicionar associação** num Ticket ou Project, precisa adicionar ao `$CFG_GLPI` em `plugin_init_()`: + +```php +global $CFG_GLPI; +$CFG_GLPI['ticket_types'][] = 'GlpiPlugin\\Meuplugin\\MeuItemtype'; +$CFG_GLPI['project_asset_types'][] = 'GlpiPlugin\\Meuplugin\\MeuItemtype'; +``` + +E pra o tab **inverso** (Meu Itemtype aparecer como tab em Ticket/Project), usar `addtabon`: + +```php +Plugin::registerClass('GlpiPlugin\\Meuplugin\\MeuItemtype', [ + 'addtabon' => ['Ticket', 'Project'] +]); +``` + +E no `defineTabs()` do seu itemtype, adicionar os tabs nativos: + +```php +$this->addStandardTab('Item_Ticket', $ong, $options); +$this->addStandardTab('Item_Project', $ong, $options); +``` + +Arrays úteis em `$CFG_GLPI`: + +| Array | Para que serve | +|---|---| +| `ticket_types` | Item pode ser associado a Ticket via `Item_Ticket` | +| `project_asset_types` | Item pode ser associado a Project via `Item_Project` | +| `asset_types` | Item aparece como "ativo" em listagens genéricas | +| `link_types` | Item pode ser link de relacionamento via `Link_Itemtype` | +| `document_types` | Item pode receber Documents anexados via `Document_Item` | +| `state_types` | Item pode ter `states_id` (estado do ativo) | diff --git a/records/plugin-dev/KB-PLUGIN-029-glpi11-administrative-vs-asset-itemtypes.md b/records/plugin-dev/KB-PLUGIN-029-glpi11-administrative-vs-asset-itemtypes.md new file mode 100644 index 0000000..5dc1b28 --- /dev/null +++ b/records/plugin-dev/KB-PLUGIN-029-glpi11-administrative-vs-asset-itemtypes.md @@ -0,0 +1,213 @@ +--- +id: KB-PLUGIN-029 +title: GLPI 11 — Itemtypes administrativos vs. ativos (semântica de vínculo com Ticket) +domain: plugin-dev +tags: + - glpi11 + - plugin + - design + - architecture + - ticket + - contract + - taxonomy +status: active +severity: medium +created_at: 2026-06-01 +updated_at: 2026-06-01 +applies_to: + - glpi-11 + - estimate-plugin +related_records: + - KB-PLUGIN-028 +--- + +# GLPI 11 — Itemtypes administrativos vs. ativos (semântica de vínculo com Ticket) + +## Contexto + +Ao construir um itemtype customizado em plugin, surge a pergunta: "ele deve aparecer como tab em Ticket?". A resposta depende da **natureza semântica** do itemtype, não de praticidade técnica. + +## Taxonomia GLPI + +GLPI separa itemtypes em duas grandes categorias com fluxos de vínculo distintos: + +### Ativos (Assets / CIs) + +Coisas físicas/virtuais que recebem suporte: Computer, Monitor, NetworkEquipment, Software, Phone, etc. + +| Aspecto | Detalhe | +|---|---| +| Tab em Ticket | "Itens" (genérica) | +| Tabela de vínculo | `glpi_items_tickets` (polimórfica via `itemtype`/`items_id`) | +| Registro | `$CFG_GLPI['ticket_types'][] = MyClass::class` | +| Tab inverso (Ticket em MyClass) | `addtabon: ['Ticket']` + `addStandardTab('Item_Ticket', ...)` | +| Padrão UI | Listagem em "Itens" do Ticket junto com Computer/Monitor/etc. | + +### Administrativos + +Documentos/registros de gestão que descrevem **regras, acordos, custos**: Contract, Supplier, Document, License, Budget, etc. + +| Aspecto | Detalhe | +|---|---| +| Tab em Ticket | **Aba própria, separada** ("Contratos", "Fornecedores", "Documentos") | +| Tabela de vínculo | Dedicada (`glpi_contracts_tickets`, `glpi_documents_tickets`) | +| Registro | Específico do itemtype (nada genérico) | +| Padrão UI | Aba dedicada com semântica própria | + +## Por que essa separação importa + +**Misturar administrativo com ativo polui semanticamente o Ticket.** Quando um técnico abre "Itens" pra ver os CIs afetados pelo problema, ele espera Computer/Monitor — não Estimate, Contract ou Document. + +Sintoma observado no plugin Estimate: +- Tentamos registrar Estimate em `$CFG_GLPI['ticket_types']` +- O tab "Itens" do Ticket aceitou o itemtype no dropdown +- Mas conceitualmente é errado — Estimate é proposta comercial, não ativo afetado + +## Decisão para o plugin Estimate + +Estimate é **administrativo** — comparável a Contract: + +| Estimate | Contract | +|---|---| +| Define escopo + valor | Define escopo + valor | +| Tem cliente/fornecedor | Tem fornecedor | +| Tem prazo de validade | Tem prazo de vigência | +| Estados (rascunho/contratada/etc.) | Estados (ativo/suspenso/encerrado) | + +Logo, em v1.0 **Estimate NÃO se vincula a Ticket via `Item_Ticket`**. Vínculo bidirecional Estimate ↔ Ticket fica pra v1.1+ com tabela dedicada: + +```sql +CREATE TABLE glpi_plugin_estimate_estimates_tickets ( + id int unsigned NOT NULL AUTO_INCREMENT, + plugin_estimate_estimates_id int unsigned NOT NULL, + tickets_id int unsigned NOT NULL, + PRIMARY KEY (id), + UNIQUE KEY unicity (plugin_estimate_estimates_id, tickets_id) +); +``` + +Mais classe `Estimate_Ticket` (CommonDBRelation) + tab dedicado. + +## Decisão para Project + +Estimate ↔ Project **funciona bem** via `Item_Project`: + +- Project é conceitualmente "execução da Estimate aprovada" — a relação semântica é direta +- `glpi_items_projects` é polimórfica e aceita qualquer itemtype administrativo ou ativo +- A UI do Project não tem essa convenção rígida do Ticket (Itens = CIs) + +## Checklist de decisão para outros plugins + +Antes de adicionar `$CFG_GLPI['ticket_types'][]` no seu plugin, pergunte: + +1. **O itemtype recebe suporte?** (alguém abre ticket sobre ele) → SIM, é ativo, registre. +2. **O itemtype define escopo/custo/acordo?** → NÃO registre como ativo; considere tabela dedicada (padrão Contract). +3. **O itemtype é resultado de execução?** (Project, Change) → Use as APIs específicas do GLPI (Project, Change, Problem). + +## Tabelas/arrays equivalentes (referência rápida) + +| Vínculo desejado | Array `$CFG_GLPI` | Quando usar | +|---|---|---| +| Item ↔ Ticket (asset) | `ticket_types` | Computer, Monitor, etc. — recebem suporte | +| Item ↔ Project (qualquer) | `project_asset_types` | Estimate, Computer, qualquer coisa relacionada à execução | +| Item ↔ Document (anexo) | `document_types` | Item pode receber arquivos anexados | +| Item ↔ Asset (geral) | `asset_types` | Item aparece como ativo em listagens | +| Item ↔ State (de ativo) | `state_types` | Item tem `states_id` (estado do CI) | +| Item ↔ Link genérico | `link_types` | Relações soltas via `Link_Itemtype` | + +Para administrativos (Contract-like) não há array genérico — sempre tabela dedicada. + +## Lições + +1. **`Item_Ticket` polimórfico não significa "qualquer item linkado"** — significa "qualquer CI afetado". Misturar administrativo polui a UX. +2. Antes de registrar em `ticket_types`, classificar semanticamente o itemtype (ativo vs administrativo). +3. Para administrativos com vínculo a Ticket, padrão é **tabela dedicada de relação + UI dedicada**, igual Contract faz. +4. Project é mais permissivo — `Item_Project` aceita administrativos sem problema, porque Project é "container de trabalho" e não "registro de incidente". + +## Backlog rastreado + +- ~~**Estimate v1.1+**: implementar `glpi_plugin_estimate_estimates_tickets` + `Estimate_Ticket`~~ + → **Implementado em v1.0** com abordagem **polimórfica** (mais idiomática e flexível). Ver seção abaixo. + +## Padrão polimórfico: uma tabela serve vários itemtypes + +Após implementar Estimate, percebemos que o padrão **`CommonDBRelation` polimórfico** é mais limpo do que criar tabela dedicada por relacionamento. Uma única classe `EstimateLink` + uma única tabela `glpi_plugin_estimate_estimates_items` cobre Estimate ↔ Ticket, Estimate ↔ Project, e qualquer itemtype futuro. + +### Schema da tabela polimórfica + +```sql +CREATE TABLE `glpi_plugin___items` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `plugin___id` int unsigned NOT NULL, -- lado fixo + `itemtype` varchar(255) NOT NULL, -- lado polimórfico + `items_id` int unsigned NOT NULL, -- lado polimórfico + `date_creation` timestamp NULL DEFAULT NULL, + `date_mod` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `unicity` (`plugin___id`, `itemtype`, `items_id`), + KEY `item` (`itemtype`, `items_id`) +); +``` + +### Classe relação polimórfica + +```php +class EntityLink extends \CommonDBRelation +{ + public static $itemtype_1 = Entity::class; + public static $items_id_1 = 'plugin___id'; + public static $itemtype_2 = 'itemtype'; // polimórfico + public static $items_id_2 = 'items_id'; + public static $checkItem_2_Rights = self::HAVE_VIEW_RIGHT_ON_ITEM; + + public function getTabNameForItem(CommonGLPI $item, $withtemplate = 0) + { + if ($item instanceof Ticket || $item instanceof Project) { + return self::createTabEntry(Entity::getTypeName(2), $count); + } + if ($item instanceof Entity) { + return self::createTabEntry(__('Linked items'), $count); + } + return ''; + } + + public static function displayTabContentForItem(CommonGLPI $item, $tabnum = 1, $withtemplate = 0) + { + // Renderização específica conforme $item + } +} +``` + +### Registro + +```php +Plugin::registerClass('GlpiPlugin\\MyPlugin\\EntityLink', [ + 'addtabon' => [ + 'Ticket', + 'Project', + 'GlpiPlugin\\MyPlugin\\Entity', + ] +]); +``` + +### Vantagens da abordagem polimórfica + +| Aspecto | Tabela dedicada (Contract_Ticket) | Polimórfica (EstimateLink) | +|---|---|---| +| Schema | 1 tabela por relação | 1 tabela pra todas | +| Classes PHP | 1 classe por relação | 1 classe pra todas | +| Adicionar novo itemtype suportado | Nova tabela + classe + migration | Apenas `addtabon` | +| Padrão GLPI nativo | Contract usa | Item_Devices, Item_Project usam | +| Idiomaticidade GLPI 11 | Tradicional | Mais moderno | + +Para a maioria dos casos, **polimórfico é preferível**. Tabela dedicada faz sentido quando há colunas específicas da relação (ex: Contract_Ticket pode ter `start_date_override`). + +### Renderização do tab + +Em vez de usar `CommonDBRelation::showListForItem` (genérico), customizamos `displayTabContentForItem` com tabela Bootstrap nativa do GLPI: + +- Form de adicionar (dropdown da entidade fixa + itemtype/items_id no lado polimórfico) +- Lista com colunas relevantes do domínio +- Botão de remover por linha (delete via `?delete_link=1&id=N`) + +Tudo sem CSS customizado — só classes Bootstrap do GLPI (`table table-hover`, `btn btn-primary`, `text-end`, etc.). diff --git a/records/plugin-dev/KB-PLUGIN-030-glpi11-migration-class-required-for-ddl.md b/records/plugin-dev/KB-PLUGIN-030-glpi11-migration-class-required-for-ddl.md new file mode 100755 index 0000000..4e5b666 --- /dev/null +++ b/records/plugin-dev/KB-PLUGIN-030-glpi11-migration-class-required-for-ddl.md @@ -0,0 +1,69 @@ +--- +id: KB-PLUGIN-030 +title: "GLPI 11 — Criação de tabelas exige classe Migration (Executing direct queries is not allowed!)" +domain: plugin-dev +tags: + - glpi11 + - plugin + - install + - database + - ddl + - migration + - gotcha +status: active +severity: high +created_at: 2026-06-02 +updated_at: 2026-06-02 +applies_to: + - plugins locais GLPI 11.x + - hook.php +--- + +# GLPI 11 — Criação de tabelas exige classe Migration + +## Sintoma + +Ao tentar instalar um plugin via interface web do GLPI (ou CLI), o processo entra em loop (loading infinito na UI) ou falha silenciosamente. +Nos logs (`files/_log/php-errors.log`), o seguinte Fatal Error (Uncaught Exception) aparece: + +`glpi.CRITICAL: *** Uncaught PHP Exception Exception: "Executing direct queries is not allowed!" at DBmysql.php` + +## Causa raiz + +No GLPI 11, o método `$DB->query()` (e os seus derivados como `queryOrDie()`) bloqueia ativamente a execução direta de comandos DDL (Data Definition Language) como `CREATE TABLE`, `ALTER TABLE` ou `DROP TABLE`. Essa é uma medida de segurança e padronização. + +Qualquer plugin que tente criar suas tabelas usando `$DB->queryOrDie("CREATE TABLE ...")` dentro de `hook.php` sofrerá quebra imediata na instalação. + +## Solução padrão GLPI 11 + +A criação ou alteração de estrutura de banco de dados deve ser envelopada dentro da classe nativa `\Migration`. + +### Errado (Legacy - GLPI 9.x/10.x) +```php +function plugin_meuplugin_install() { + global $DB; + $query = "CREATE TABLE `glpi_plugin_meuplugin_table` (...)"; + $DB->queryOrDie($query, $DB->error()); +} +``` + +### Correto (GLPI 11) +```php +function plugin_meuplugin_install() { + global $DB; + $migration = new \Migration(MEUPLUGIN_VERSION); + + if (!$DB->tableExists('glpi_plugin_meuplugin_table')) { + $query = "CREATE TABLE `glpi_plugin_meuplugin_table` (...)"; + $migration->addPostQuery($query); + } + + // Essencial: executa a fila de queries da migration + $migration->executeMigration(); + + return true; +} +``` + +## Checklist de validação +Sempre procure nos arquivos de instalação (`hook.php` ou `setup.php`) se o plugin chama diretamente `$DB->query` para criar tabelas. Se encontrar, reescreva para usar `$migration->addPostQuery()`. diff --git a/records/plugin-dev/KB-PLUGIN-031-plugin-dev-workflow-runbook.md b/records/plugin-dev/KB-PLUGIN-031-plugin-dev-workflow-runbook.md new file mode 100644 index 0000000..634b75a --- /dev/null +++ b/records/plugin-dev/KB-PLUGIN-031-plugin-dev-workflow-runbook.md @@ -0,0 +1,179 @@ +--- +id: KB-PLUGIN-031 +title: "Runbook — Workflow de desenvolvimento e deploy DEV de plugins GLPI" +domain: plugin-dev +tags: + - runbook + - workflow + - dev + - deploy + - forgejo + - scaffold + - validation + - console +status: active +severity: high +created_at: 2026-06-11 +updated_at: 2026-06-11 +applies_to: + - GLPI 11.x no ambiente dev (CT 100 docker, stack GLPI11) + - qualquer plugin novo desenvolvido internamente +related_records: + - KB-INFRA-001 + - KB-INFRA-002 + - KB-PLUGIN-001 + - KB-PLUGIN-004 + - KB-PLUGIN-013 + - KB-PLUGIN-018 + - KB-PLUGIN-027 +--- + +# Runbook — Workflow de desenvolvimento e deploy DEV de plugins GLPI + +Procedimento padrão do nascimento de um plugin até sua validação no GLPI de +desenvolvimento. A **publicação em produção** (Mindplace/licenciamento) é outro +processo — ver [KB-PLUGIN-013](KB-PLUGIN-013-mindplace-plugin-publication-runbook.md). + +Primeiro plugin a seguir este fluxo de ponta a ponta: `assetinherit` (2026-06-11). + +## Mapa do ambiente + +| Componente | Onde | +|---|---| +| Servidor dev (docker) | CT 100 — `root@192.168.100.49` (SSH key do Mac já cadastrada) | +| Diretório de plugins | `/opt/projects/GLPI11/docker/glpi/plugins/` (bind → `/var/www/glpi/plugins`, ver [KB-INFRA-001]) | +| Container GLPI | `glpi11-app` (GLPI dev em `http://192.168.100.49:8081`) | +| Git de dev | Forgejo local — `git@192.168.100.101:administrador/.git` (remote `origin`) | +| Git de produção | Forgejo Mindtek — remote `production`, usado **só** na publicação | + +## Fase 1 — Scaffold do plugin + +Estrutura mínima obrigatória (a chave/diretório do plugin define TUDO — funções, +hooks, constantes): + +``` +/ +├── setup.php # plugin_init_, plugin_version_ + 4 callbacks +├── hook.php # install/uninstall e demais hooks +├── logo.png # PNG 128x128 na raiz — obrigatório [KB-INFRA-002] +├── README.md # dor que resolve, como funciona, instalação, configuração +├── CHANGELOG.md # Keep a Changelog +├── LICENSE # GPL-3.0-or-later (compatível com o GLPI) +└── .gitignore # padrão de plugins [KB-PLUGIN-013] +``` + +Checklist de armadilhas conhecidas: +- [ ] `setup.php` com os **4 callbacks**: `check_prerequisites`, `check_config`, + `install`, `uninstall` — sem eles a instalação falha genericamente + ([KB-PLUGIN-001]). +- [ ] Toda chave em `$PLUGIN_HOOKS[...]['']` é o **plugin key exato** + (= nome do diretório). Variações falham em silêncio ([KB-PLUGIN-004]). +- [ ] `logo.png` é PNG real (SVG renomeado não funciona) ([KB-INFRA-002]). +- [ ] `version` no `setup.php` em SemVer — o release de produção lê de lá. + +## Fase 2 — Repositório no Forgejo local + +Criar o repo via API (ou UI em `http://192.168.100.101:3000`): + +```bash +curl -s -u 'administrador:' -X POST \ + http://192.168.100.101:3000/api/v1/user/repos \ + -H 'Content-Type: application/json' \ + -d '{"name":"","private":true,"default_branch":"main"}' +``` + +No diretório do plugin (na máquina de dev): + +```bash +git init && git branch -M main +git add -A && git commit -m " 0.1.0: scaffold" +git remote add origin git@192.168.100.101:administrador/.git +git push -u origin main +``` + +Convenção de remotes: `origin` = Forgejo local (push contínuo de dev); +`production` = Forgejo Mindtek (adicionado apenas na publicação, [KB-PLUGIN-013]). + +## Fase 3 — Deploy no servidor dev + +```bash +ssh root@192.168.100.49 +cd /opt/projects/GLPI11/docker/glpi/plugins +git clone git@192.168.100.101:administrador/.git +git config --global --add safe.directory \ + /opt/projects/GLPI11/docker/glpi/plugins/ +chown -R www-data:www-data +``` + +O bind mount entrega o plugin no container instantaneamente — **não** precisa +recriar o container. + +## Fase 4 — Instalação e ativação (sempre via console) + +```bash +docker exec glpi11-app sh -c ' + php -l /var/www/glpi/plugins//setup.php && + php -l /var/www/glpi/plugins//hook.php && + php /var/www/glpi/bin/console plugin:install -n --username=glpi && + php /var/www/glpi/bin/console plugin:activate -n && + php /var/www/glpi/bin/console plugin:list | grep ' +``` + +Esperado: status **Habilitado**. O console dá erros legíveis; a UI só mostra +falha genérica. + +## Fase 5 — Validação funcional (teste E2E em CLI) + +⚠️ **Gotcha do GLPI 11:** em CLI, `include 'inc/includes.php'` **não** carrega +mais o core (classes como `RuleAsset` ficam indisponíveis). O bootstrap correto +para scripts de teste é o do `bin/console`: + +```php +require '/var/www/glpi/vendor/autoload.php'; +$kernel = new \Glpi\Kernel\Kernel(); +$kernel->boot(); // carrega core + plugins ativos (plugin_init roda aqui) + +// sessão CLI mínima para CommonDBTM::add()/update() +$_SESSION['glpiactiveentities'] = [0]; +$_SESSION['glpiactive_entity'] = 0; +$_SESSION['glpiactiveprofile']['interface'] = 'central'; +``` + +Padrão do teste: criar massa de dados de teste com prefixo `TEST` → +exercitar os cenários → **deletar tudo com purge no final**. Executar: + +```bash +scp test_.php root@192.168.100.49:/tmp/ +ssh root@192.168.100.49 'docker cp /tmp/test_.php glpi11-app:/tmp/ && + docker exec glpi11-app php /tmp/test_.php' +``` + +Exemplo real completo: teste E2E do `assetinherit` (6 cenários, incluindo +descoberta de comportamento que virou documentação — critério `PATTERN_EXISTS` +não casa com `users_id = 0`). + +## Fase 6 — Ciclo de iteração + +1. IDE no Mac → Remote SSH em `root@192.168.100.49`, pasta do plugin. +2. Editar in-place — GLPI é PHP, refresh no browser e a mudança aparece. +3. `git add . && git commit && git push` → Forgejo local (`origin`). +4. Mudanças feitas no Mac: `git push origin` no Mac + `git pull` no CT 100 + (lembrar `chown -R www-data:www-data` se criar arquivos novos como root). + +## Fase 7 — Promoção para produção (fora deste runbook) + +Quando validado no dev: seguir [KB-PLUGIN-013] (kill switch de licença, +release ZIP com wrapper directory [KB-PLUGIN-018], catálogo Mindplace, +serial do cliente). + +## Regra para agentes + +Ao criar/depurar plugin novo no ambiente dev, seguir este runbook na ordem. +Antes de diagnosticar erro funcional, validar Fases 3-4 (mount, ownership, +instalação via console). Para testes E2E em CLI, usar SEMPRE o bootstrap do +Kernel (Fase 5), nunca `inc/includes.php`. + +## Classificação + +- Tipo: Runbook operacional de desenvolvimento. +- Reutilização: obrigatória para todo plugin novo.