kb: KB-PLUGIN-031 runbook de workflow dev/deploy de plugins + commit dos registros 021-030 pendentes
- Novo runbook KB-PLUGIN-031: nascimento do plugin ate validacao E2E no GLPI dev (scaffold, Forgejo local, deploy CT100, console, bootstrap Kernel para testes CLI). Validado de ponta a ponta com o assetinherit. - Registros KB-PLUGIN-021..030 existiam apenas no disco (drift) e foram incluidos no versionamento; index.json sincronizado via kb-fix. - Ignora lixo AppleDouble/.DS_Store do macOS. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
60060dce60
commit
200cd6c2ce
13 changed files with 2164 additions and 1 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -2,3 +2,5 @@
|
||||||
*.swp
|
*.swp
|
||||||
*.tmp
|
*.tmp
|
||||||
*.bak
|
*.bak
|
||||||
|
._*
|
||||||
|
.DS_Store
|
||||||
|
|
|
||||||
188
index.json
Normal file → Executable file
188
index.json
Normal file → Executable file
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"last_updated": "2026-05-26",
|
"last_updated": "2026-06-11",
|
||||||
"records": [
|
"records": [
|
||||||
{
|
{
|
||||||
"id": "KB-INFRA-001",
|
"id": "KB-INFRA-001",
|
||||||
|
|
@ -384,6 +384,192 @@
|
||||||
"severity": "medium",
|
"severity": "medium",
|
||||||
"path": "records/plugin-dev/KB-PLUGIN-020-mcprotocol-bff-roadmap.md",
|
"path": "records/plugin-dev/KB-PLUGIN-020-mcprotocol-bff-roadmap.md",
|
||||||
"summary": "id: KB-PLUGIN-020"
|
"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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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`).
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -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":<NEW_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=<NEW_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.
|
||||||
|
|
@ -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 <tabela> 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 <tabela> 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
|
||||||
|
|
@ -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`
|
||||||
|
|
@ -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 <arquivos>
|
||||||
|
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
|
||||||
|
|
@ -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
|
||||||
|
<?php
|
||||||
|
namespace GlpiPlugin\Meuplugin;
|
||||||
|
|
||||||
|
use CommonGLPI;
|
||||||
|
use DbUtils;
|
||||||
|
use ProfileRight;
|
||||||
|
|
||||||
|
class Profile extends \Profile
|
||||||
|
{
|
||||||
|
public static $rightname = "profile";
|
||||||
|
|
||||||
|
public function getTabNameForItem(CommonGLPI $item, $withtemplate = 0)
|
||||||
|
{
|
||||||
|
if ($item->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 <plugin>` 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/<plugin>/` (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
|
||||||
|
<?php
|
||||||
|
Session::checkLoginUser();
|
||||||
|
|
||||||
|
$class = \GlpiPlugin\Meuplugin\Item::class;
|
||||||
|
|
||||||
|
Html::header(
|
||||||
|
\GlpiPlugin\Meuplugin\Item::getTypeName(2),
|
||||||
|
$_SERVER['PHP_SELF'],
|
||||||
|
'management',
|
||||||
|
$class
|
||||||
|
);
|
||||||
|
|
||||||
|
Search::show($class);
|
||||||
|
|
||||||
|
Html::footer();
|
||||||
|
```
|
||||||
|
|
||||||
|
Plugins de referência: `webapplications`, `splititil` (ambos no marketplace do GLPI 11).
|
||||||
|
|
||||||
|
### Sintoma quando inclui errado
|
||||||
|
|
||||||
|
```
|
||||||
|
include(): Failed opening '../../../inc/includes.php' for inclusion
|
||||||
|
(include_path='.:/usr/local/lib/php')
|
||||||
|
at <plugin>/front/<page>.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 `<plugin_dir>/locales/<lang>.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 (`<lang>_<COUNTRY>.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/<plugin>/locales
|
||||||
|
msgfmt pt_BR.po -o pt_BR.mo
|
||||||
|
|
||||||
|
# 4. Limpar cache do GLPI
|
||||||
|
docker exec <container> 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('<plugin>')` no boot — não precisa código adicional. Basta o `.mo` estar em `locales/<lang>.mo` e o usuário ter `glpilanguage` setado.
|
||||||
|
|
||||||
|
#### Validar tradução via console
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec <container> 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('<plugin>');
|
||||||
|
echo __('Total hours', '<plugin>').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_<plugin>()`:
|
||||||
|
|
||||||
|
```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) |
|
||||||
|
|
@ -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_<plugin>_<entity>_items` (
|
||||||
|
`id` int unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
`plugin_<plugin>_<entity>_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_<plugin>_<entity>_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_<plugin>_<entity>_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.).
|
||||||
69
records/plugin-dev/KB-PLUGIN-030-glpi11-migration-class-required-for-ddl.md
Executable file
69
records/plugin-dev/KB-PLUGIN-030-glpi11-migration-class-required-for-ddl.md
Executable file
|
|
@ -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()`.
|
||||||
179
records/plugin-dev/KB-PLUGIN-031-plugin-dev-workflow-runbook.md
Normal file
179
records/plugin-dev/KB-PLUGIN-031-plugin-dev-workflow-runbook.md
Normal file
|
|
@ -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/<plugin>.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):
|
||||||
|
|
||||||
|
```
|
||||||
|
<key>/
|
||||||
|
├── setup.php # plugin_init_<key>, plugin_version_<key> + 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[...]['<key>']` é 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:<senha>' -X POST \
|
||||||
|
http://192.168.100.101:3000/api/v1/user/repos \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{"name":"<key>","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 "<key> 0.1.0: scaffold"
|
||||||
|
git remote add origin git@192.168.100.101:administrador/<key>.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/<key>.git
|
||||||
|
git config --global --add safe.directory \
|
||||||
|
/opt/projects/GLPI11/docker/glpi/plugins/<key>
|
||||||
|
chown -R www-data:www-data <key>
|
||||||
|
```
|
||||||
|
|
||||||
|
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/<key>/setup.php &&
|
||||||
|
php -l /var/www/glpi/plugins/<key>/hook.php &&
|
||||||
|
php /var/www/glpi/bin/console plugin:install <key> -n --username=glpi &&
|
||||||
|
php /var/www/glpi/bin/console plugin:activate <key> -n &&
|
||||||
|
php /var/www/glpi/bin/console plugin:list | grep <key>'
|
||||||
|
```
|
||||||
|
|
||||||
|
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_<key>.php root@192.168.100.49:/tmp/
|
||||||
|
ssh root@192.168.100.49 'docker cp /tmp/test_<key>.php glpi11-app:/tmp/ &&
|
||||||
|
docker exec glpi11-app php /tmp/test_<key>.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.
|
||||||
Loading…
Reference in a new issue