# ADR-034 — Ativação de módulos em tempo real (feature flags) e grupo suporte

| Campo | Valor |
|---|---|
| **Status** | Aceito (2026-10-16) |
| **Data** | 2026-10-15 |
| **Plano de origem** | `wiki/plan/07-core-contratos-discovery.md` |
| **Relacionadas** | ADR-013, ADR-014, ADR-020, ADR-033 |

## Contexto

O ADR-014 define que a ativação de módulo é **dado** e que o manifesto é cacheado (boot sem DB). É
preciso, porém, um **liga/desliga de módulo em tempo real**: ao desativar, todo o funcionamento
(front, back e API) é **objetivamente retirado** da estrutura, o cache é invalidado **no mesmo
instante** e o efeito vale para **todos** os usuários. Esta operação é **exclusiva da equipe de
suporte** (ADR-033).

## Decisão

1. **Estado é dado:** `core.core_module_discovery_settings` (chave `modules_activation`,
   `{modulo: bool}`). Não há "flags" arbitrárias — é a ativação de módulo do ADR-014.
   O manifesto (`core:cache-modules`) é a **config estrutural** (filesystem) e o **default** de
   ativação; a verdade em runtime é o dado, resolvido por `ModuleActivation` (Valkey/TTL curto + DB,
   fail-open).
2. **Guard de runtime:** um middleware (`CheckModuleActivation`) resolve a ativação em um **cache
   compartilhado (Valkey)** com TTL curto e **nega com 404** quando o módulo está inativo, em
   **qualquer área** (`portal`, `cms`, `api`). O efeito é imediato e global, sem `reload` do FPM.
3. **Toggle atômico (só suporte):** grava no banco, **invalida a chave do Valkey no mesmo instante**
   e limpa os caches derivados do módulo (resposta/página, manifesto/sidebar). Sem esperar deploy.
4. **Manifesto e navegação filtram** módulos inativos; módulos do grupo `Suporte` (ADR-033) só
   aparecem/acessam para a equipe de suporte.
5. **Simplicidade:** **não** é uma engine de feature flags com segmentação/rollout. É a ativação do
   ADR-014 endurecida com guard + invalidação imediata.

## Justificativa (alternativas descartadas)

- **Rebuild de `route:cache`/manifesto a cada toggle** — descartada: depende de reload do FPM e não
  é "no mesmo instante".
- **Engine de flags (segmentação, % rollout)** — descartada: overengineering para single-tenant
  (ADR-001).
- **Só esconder no menu** — descartada: a rota continuaria acessível; o corte tem de ser estrutural.
- **Sem cache compartilhado (só DB por request)** — descartada: custo desnecessário; Valkey já é
  dependência.

## Consequências

**Positivas**
- Desligar um módulo é instantâneo, global e reversível; nada de módulo "meio desligado".
- Auditoria e restrição ao suporte (ADR-033) dão segurança operacional.

**Custos / negativas**
- Mais uma consulta a cache por request (mitigada por TTL curto + Valkey).
- Rotas de módulos inativos continuam registradas (guardadas no runtime, não removidas do router).

## Exemplo mínimo (comparativo)

```php
// ❌ Reativar/desativar apagando o manifesto no deploy
//    (lento, exige FPM, não é global nem imediato)

// ✅ Estado + guard + invalidação imediata
Core::moduleActivation($module);           // resolve do cache (Valkey)
Cache::forget("module.active.{$module}");  // no toggle (só suporte)
abort(404);                                // módulo inativo em portal/cms/api
```

## Verificação

- Desativar um módulo → requisição imediata a `web`, `api` e `cms` responde **404** para todos.
- Reativar → volta a responder sem deploy/reload.
- Apenas o perfil `suporte` consegue alternar (demais recebem 403).
- Cache do módulo é invalidado no mesmo instante (sem janela de dado velho).

## Emenda 2026-10-23 — módulos essenciais e grupo `Suporte` intocável

- **Lista estática:** `config/core.php` declara `essential_modules` (mínimo `Acesso`,
  `Configuracoes`). A lista é de bootstrap, não é settings editável no painel.
- **Grupo `Suporte` é exceção dura:** **nenhum** módulo de `src/modules/Suporte/{Modulo}` pode ser
  desativado, por UI, comando ou `--force`. A UI não oferece o toggle; a Action
  `SetModuleActivation` recusa; o comando falha com mensagem. O grupo continua invisível e 404 para
  todos, exceto `suporte@maxima.inf.br` (ADR-033).
- **Demais essenciais:** a UI recusa desativar; `--force` existe **apenas no CLI de suporte** e é
  auditado. Nenhum caminho de UI concede `--force`.
- **Uma Action:** UI e `core:module:activate` usam a mesma regra; teste dedicado cobre UI, comando,
  `--force` e tentativa de auto-lockout.
- **Efeito:** a proteção vale mesmo com o módulo já inativo no banco; a Action reativa o essencial
  e registra o motivo, em vez de deixar o painel inacessível.

**Verificação da emenda (P3.S2/S7, 2026-10-25).** `SetModuleActivationTest` cobre a regra única
(motivo, módulo desconhecido com manifesto restaurado, `Suporte` recusado **com** `force`,
essencial exigindo `force`, efeito imediato no mesmo processo). `ModuleActivationCommandTest` prova
que o comando só delega (sem segunda regra) e que o CLI exige `--force` e falha no grupo `Suporte`.
`PainelModulesCmsTest` prova os dois requests reais (200 → 404 → 200), a auditoria com motivo e
`force`, a ausência de formulário para essenciais/`Suporte`, a recusa server-side (inclusive com
`force` forjado no `POST`) e o `GET` que não muta (405). O E2E `23-suporte-painel.spec.ts` confirma
na UI que `Acesso`/`Configuracoes` aparecem como **Essencial** e `Painel` como **Protegido**, sem
toggle. A recuperação de um essencial desativado pelo CLI é feita **só** por
`core:module:activate {Nome} --force --reason=…` (auditado); a UI nunca concede `force`.

## Referências

- `src/core/Internal/ModuleDiscovery/ModuleDiscoveryService.php`;
  `src/core/Internal/Http/Middleware/CheckModuleActivation.php`;
  `wiki/adr/ADR-014-autodescoberta-modulos-agrupamento.md`.
