# Auditoria geral de código — julho/2026

Revisão do sistema em busca de **erros de código, inconsistências, itens faltantes e
problemas de lógica**. Cada achado traz a causa, o impacto real e o que foi feito.

**Resumo:** 2 bugs **P0** (quebrariam produção), 2 **P1**, 4 **P2** e 3 **P3**.
**TODOS foram corrigidos** — inclusive os P3 e o P0-2 latente no motor antigo.
Foram acrescentadas **duas travas permanentes** no `static_check.php` para que os dois
bugs mais graves não possam voltar.

---

## P0 — Quebra produção

### P0-1 · Motor de regras: 5 rotas dariam erro fatal 500

**Onde:** `app/Controllers/RulesController.php` (métodos `edit`, `toggle`, `run`,
`duplicate`, `delete`).

**O quê:** declarei um 3º argumento (`$id`) nas assinaturas:

```php
public function edit(Request $request, Context $context, $id)   // ERRADO
```

Mas o `Router` (`app/Support/Router.php`) chama
`call_user_func($handler, $request, $context)` — passa **só 2 argumentos**. Os parâmetros
de rota vêm por `$request->param('id')`. No PHP 8 isso é `ArgumentCountError` → **500**.

**Impacto:** **editar, pausar, executar, duplicar e excluir** qualquer regra dariam erro
fatal. O motor rodaria, mas a tela seria inutilizável.

**Por que não peguei antes:** meu verificador só checava se o método *existe*, não a
assinatura. Todos os outros 100+ controllers do sistema seguem a convenção correta — só o
que eu escrevi por último fugiu dela.

**Corrigido:** as 5 assinaturas voltaram a `(Request $request, Context $context)` e cada
método lê `$id = (int) $request->param('id')`.

---

### P0-2 · Regra de passagem NOVA dispararia nas 200 passagens mais ANTIGAS

**Onde:** `RuleEngine::scanPassages()` (e, latente, em `AlertEngine::scanPassages()`).

**O quê:** a "rede de segurança" do worker relê passagens acima do watermark:

```sql
WHERE h.id > :wm ORDER BY h.id ASC LIMIT 200
```

Uma regra **recém-criada** tem `ultimo_evento_id = 0`. Logo, `WHERE id > 0 LIMIT 200`
devolve as **200 passagens mais antigas do histórico** — e a regra dispara em todas.

**Impacto:** **grave.** No motor novo as ações incluem **abrir porta**. Criar uma regra
"acesso liberado → abre o portão" faria o portão **abrir 200 vezes** no primeiro ciclo do
worker (e mandar 200 WhatsApps). No motor antigo o dano é menor (só notificação), mas o
bug é o mesmo.

**Corrigido nos DOIS motores:**

- **Motor novo** (`RuleEngine::getPassageState()`): quando a regra nunca teve estado, o
  watermark nasce com o `MAX(id)` **atual** da tabela de eventos.
- **Motor antigo** (`AlertRuleRepository::getState()`): mesma correção, no ponto onde o
  estado é criado. Cobre também **regras antigas que nunca chegaram a rodar**.
  (Fiz aqui, e não só na criação da regra, porque o `getState()` é o único caminho por onde
  o estado nasce — cobre criação, primeira varredura e primeiro evento em tempo real.)

Regra nova, nos dois motores, só reage ao que acontecer **daqui para a frente**.

---

## P1 — Comportamento errado

### P1-3 · Varredura de passagens era O(n²) e contava disparos em dobro

**Onde:** `RuleEngine::runPeriodic()`.

**O quê:** o worker chamava `scanPassages($r)` **uma vez por regra**, mas o `onPassage()`
que ela invoca **já percorre todas as regras**. Com N regras de passagem, cada evento era
varrido N vezes e o contador `fired` inflava.

**Corrigido:** virou `scanPassagesAll()` — roda **uma vez por ciclo**, partindo do **menor**
watermark entre as regras. O `onPassage` continua cuidando do watermark de cada uma.

### P1-4 · Ação agendada de regra PAUSADA ainda disparava

**Onde:** `RuleEngine::runQueue()`.

**O quê:** a fila de ações com atraso (`regra_fila`) executava sem olhar se a regra ainda
estava ativa. Pausar uma regra *"acende a luz e, 10 min depois, abre o portão"* **não
impedia o portão de abrir**.

**Corrigido:** a fila faz `LEFT JOIN regra` e, se a regra estiver pausada (ou removida), a
ação vira `cancelado` e o motivo aparece no histórico.

---

## P2 — Corrigidos

### P2-5 · "Executar agora" escolhia o degrau errado em regras de nível
`RuleEngine::runNow()` pegava o **maior** limiar como "mais severo". Certo para *offline*
(30 min > 5 min) e *nível acima* (38 °C > 30 °C) — **errado para nível abaixo**: em água,
**15% é pior que 30%**. O teste manual rodava o degrau *menos* grave.
**Corrigido:** a escolha agora depende da direção do gatilho.

### P2-6 · Canais silenciosamente ignorados
Na ação **Notificar**, os destinos *"uma pessoa"* e *"todos os moradores"* só funcionam com
**WhatsApp** e **Web Push** (Pushover e e-mail só têm destinatário global). Se o operador
marcasse Pushover + destino "moradores", **nada acontecia e nada era dito**.
**Corrigido:** o histórico agora registra explicitamente o que foi ignorado e por quê.

### P2-7 · `POST /alerts/preview` sem CSRF
Única rota POST do sistema sem proteção CSRF (foi liberada por ser "só leitura").
**Corrigido:** CSRF adicionado na rota **e** o `_csrf` no JS que a chama (sem isso eu teria
quebrado a prévia ao adicionar o gate).

### P2-8 · `.env.example` **não existia**
O sistema lê **116 chaves** de ambiente (`Config::env`), e não havia nenhum modelo. Numa
instalação nova, era adivinhação.
**Corrigido:** `.env.example` gerado a partir dos `api/config/*.php`, agrupado por módulo,
com os defaults seguros e **os campos sensíveis em branco**. Todas as integrações nascem
inertes (`*_ENABLED=false`).

---

## P3 — Fechados

### P3-9 · `RuleRepository::save()` estourava SQL cru sem a migração v2
Sem o `migration_regra_v2.sql`, o INSERT referenciava colunas inexistentes
(`escalonamento`, `modo`) e o usuário levava um erro de SQL na cara.
**Corrigido:** novo `hasV2()` — o save **degrada com elegância** (grava sem esses campos) e
a tela `/regras` mostra um banner claro: *"falta rodar a migração; degraus e 'uma vez só'
não serão salvos"*.

### P3-10 · Ability órfã `prereg.create`
Verificada: **nenhuma** rota, menu ou controller a consome (o pré-cadastro que existe é o
público, sem gate). **Não removi** — o ganho é nulo e o risco, embora pequeno, não é zero.
**Marquei com um comentário** explicando que está reservada/não usada, para o próximo leitor
não perder tempo.

### P3-11 · `go2rtc` fora do `WorkerHealthRepository`
**Não é bug.** É um **binário** (não um worker PHP) — não tem como bater heartbeat.
Registrado aqui só para não ser reaberto numa próxima auditoria.

---

## Travas novas (para os bugs não voltarem)

Os dois bugs mais graves passaram porque o `static_check.php` **não olhava para isso**.
Duas checagens permanentes foram adicionadas:

- **Check 10 — assinatura dos métodos de rota.** Falha se algum método declarar um argumento
  obrigatório além de `Request`/`Context`. Pega o **P0-1** na hora. *(O check 2 só verificava
  se o método existe — por isso não acusou nada.)*
- **Check 11 — toda rota `POST` tem `$csrf`.** Pega o **P2-7**. Hoje: **208 rotas POST, todas
  com CSRF.**

Rode `php api/scripts/static_check.php` no servidor antes de publicar.

---

## O que passou limpo

Estas checagens rodaram sobre **todo** o repositório e não acharam nada:

- **483 arquivos PHP** — balanceamento de chaves/parênteses ✓
- **451 rotas** → todos os métodos de controller existem ✓
- **Rotas duplicadas ou sombreadas** (literal atrás de `{id}`) — nenhuma ✓
- **Views** referenciadas pelos controllers — todas existem ✓
- **82 abilities** usadas (rotas + menu + views) — todas definidas ✓
- **Camadas**: `App\` nunca referencia `Web\` (só em comentários) ✓
- **HY093** (placeholder repetido no mesmo `prepare`) — nenhum ✓
- **Tabelas** referenciadas em SQL — todas no `schema.sql` ✓
  (as "faltantes" eram do `import_automacao.php`, que lê o banco **legado** `nexus`)
- **Migrações vs `schema.sql`** — o schema cobre 100% das migrações ✓
- **JS inline** de todas as views — `node --check` limpo ✓
- **Workers** — todos no `install`, `uninstall` e `WorkerHealth` ✓
- **`.bat`** — todos em **CRLF** ✓
- **Rotas públicas** (sem `$auth`) — todas intencionais e por token ✓
- **Mural**: a tela `rede` (que traz PII) está corretamente **bloqueada** no link público ✓

---

## Ação necessária no servidor

1. Subir os arquivos alterados (com os `.bat` em **modo binário** — LF quebra o CMD).
2. Rodar, se ainda não rodou: `migration_regra.sql` e depois `migration_regra_v2.sql`.
   (Sem a v2 a tela funciona, mas avisa que degraus e "uma vez só" não são salvos.)
3. Se ainda não houver `.env`, copiar de `.env.example` e preencher.
4. Rodar `php api/scripts/static_check.php` — deve terminar em **RESULTADO: OK**.

> O bug P0-2 (regra nova disparando em eventos antigos) está corrigido **também no motor de
> alertas**, então já é seguro criar regras nos dois. Ainda assim, o caminho é `/regras` — o
> motor de alertas está sendo aposentado.
