# Logs do Sistema — Regras e Portabilidade

> Regras, acoplamentos e armadilhas do módulo de logs do sistema.

---

## 1. Regras de negócio não óbvias

### 1.1 A trilha é escrita manualmente, evento a evento

Não existe observer de model ativo: o trait `LogsActivity` do
`App\Admin\User` está **comentado**
(`v6/app/Http/Model/Admin/User.php:43`). Cada controller que quiser ser
auditado precisa chamar `activity()` à mão. Consequência: **ações não
auditadas são silenciosamente invisíveis** (por exemplo, `PUT` de
fornecedor, criação de manual, alteração de módulo não geram log, ou
geram com texto inconsistente).

### 1.2 `log_name` é sempre `default`

Nenhuma chamada usa `->logName()`. O índice
`activity_log_log_name_index` (criado pela migration) não é útil para
filtragem por tipo de ação.

### 1.3 O "ID" exibido não é o id do registro

`struct_logs()` gera um índice sequencial local (`'value' => $id++`).
O `id` real de `activity_log` **nunca** chega ao front-end. Não há como
linkar um log a um registro específico pela UI.

### 1.4 `properties` é um JSON livre

A única chave usada em todo o código é `"Detalhe"`. A tela concatena
**todas** as chaves, o que significa que uma property com array é
serializada com `json_encode` dentro do texto.

### 1.5 `causer` nem sempre é preenchido

`Prefecture\SupportController@enviarEmail` chama `activity()` **sem**
`->causedBy(Sentinel::getUser())` (linha 113-116). Resultado: `causer_id` e
`causer_type` ficam **nulos**, embora a ação tenha sido feita por um
usuário autenticado. É a única gravação do CMS sem autor.

### 1.6 O `subject` é uma instância nova do model

Vários controllers fazem `activity()->performedOn($this->userLog)`, onde
`$this->userLog` é a model injetada pelo container (uma **instância
nova**, sem id). O `subject_id` fica **nulo** e o `subject_type` é
`App\Admin\User` (ou `App\Admin\Role`, `App\Admin\Permissions`).
Já `activity()->performedOn($user)` (com o usuário real, em
`UserController@store`) grava o `subject_id` correto.

### 1.7 Estado ativo/inativo registrado como "Inativou"/"Restaurou"

O par `activations`/`users` do Sentinel não tem auditoria própria; o
`UserController` traduz a remoção/criação da ativação em "Inativou
usuário"/"Restaurou usuário".

### 1.8 A versão do sistema não vem do log

O rodapé do CMS mostra `UpdateLogs::all()->last()->version`. A tabela
`update_logs` é mantida pelo comando `update:logs`, que **incrementa o
patch** da última versão. Nada disso tem relação com `activity_log`.

---

## 2. Acoplamentos

### 2.1 Dependências de saída

| Dependência | Tipo | Onde |
|---|---|---|
| `spatie/laravel-activitylog` | externo | `activity()` em ~25 pontos do CMS |
| `App\Http\Model\Admin\ActivityLog` | interno | leitura |
| `App\Observers\ActivityObserver` | interno | IP + user-agent |
| `modulos` / `resources` | interno | visibilidade do menu |
| `update_logs` | interno (conceito irmão) | rodapé do CMS e do portal |

### 2.2 Dependências de entrada

| Consumidor | Dependência | Risco |
|---|---|---|
| **Todos** os controllers auditados | `activity()` | a API do pacote vira dependência global |
| `ActivityLogController` | tabela `activity_log` | — |
| `DatatableController@get_logs` | `struct_logs()` (formato do array) | se o formato mudar, o JS quebra |
| `admin/role/form.blade.php` | `resources.modulo_id` + `modulos.status` | linha do recurso oculta |
| `vendor/adminlte/page.blade.php` | `$modulos['logs-do-sistema']` | item de menu oculto |
| `admin/modulos/form.blade.php` | `modulos` (todas as linhas) | linhas duplicadas na tela |

### 2.3 Efeito de remover o pacote

`activity()` é chamado em ~25 pontos espalhados por controllers de
módulos **distintos** (usuários, perfis, permissões, login, suporte).
Uma refatoração do pacote de log impacta todos esses módulos.

---

## 3. Armadilhas

### 3.1 [CRÍTICO] O slug duplicado torna o módulo invisível

Detalhado em `00-RESUMO.md` seção 5.1 e `02-MODELAGEM.md` seção 2.3.
Resumo do encadeamento:

1. `modulos` tem duas linhas com `slug = 'logs-do-sistema'` (ids 7 e 32).
2. `pluck('status','slug')` (AdminLTE linha 124 e `ModulosServiceProvider`
   linha 23) resolve a chave duplicada em favor da **última** linha → o
   valor é `false` (id 32).
3. `v6/resources/views/vendor/adminlte/page.blade.php:284` exige
   `$modulos['logs-do-sistema']` → o item de menu **nunca** aparece.
4. O mesmo `pluck` no `ModulosServiceProvider` entrega `false` ao portal
   público e ao dashboard.
5. `resources.id = 36` (`logs-do-sistema`) aponta para `modulo_id = 32`
   (desativado) → a linha não aparece na matriz de permissões.
6. A permissão exigida no menu, `registro-de-atividades.index`, **não
   existe** em `resources` → mesmo que o módulo fosse ativo, a checagem
   falharia para todos, exceto `super-admin` (que passa por
   `has_access`).

**Correção mínima:** migration que mova `resources.modulo_id` do 32 para o
7, apague a linha 32 (ou vice-versa, ative o 32) e adicione
`UNIQUE (slug)` em `modulos`. A primeira opção preserva a semântica de
"módulo ativo = id 7" que o resto do código assume.

### 3.2 [ALTO] `Route::resource('log', ...)` gera 6 actions inexistentes

`v6/routes/basepro.php:71` cria 7 rotas; o controller implementa só
`index()`. `GET /cms/log/create`, `POST /cms/log`, `GET /cms/log/{log}`,
`GET /cms/log/{log}/edit`, `PUT /cms/log/{log}` e
`DELETE /cms/log/{log}` resultam em **erro de método inexistente**.

### 3.3 [ALTO] A ordenação da listagem é invertida

```php
ActivityLog::orderBy('created_at', 'desc')->orderBy('created_at')->limit(5000)->get();
```

O segundo `orderBy('created_at')` (sem direção) **sobrescreve** a direção do
primeiro no array `orders` do Builder. O SQL final fica
`ORDER BY "created_at" asc, "created_at" asc` — ou seja, **crescente**. Com
`LIMIT 5000`, a tela mostra os **5000 registros mais antigos**, não os mais
recentes. O `desc` inicial é inútil.

### 3.4 [MÉDIO] `LIMIT 5000` sem paginação e sem ORDER BY correto

Mesmo corrigindo o `orderBy`, o limite de 5000 corta o histórico sem
aviso. Não há `total` exibido, não há paginação, não há filtro por período
ou por usuário.

### 3.5 [MÉDIO] `json_decode` de `properties` nulo

`struct_logs()` faz `json_decode($value->properties, true)` sem checar
`null`. Se `properties` for `NULL` (registros criados sem
`withProperty`), o `foreach (null)` emite warning — e em PHP 8.1+ é
`Warning: foreach() argument must be of type array|object`. O pacote
spatie sempre grava `properties` como `{}` no v2, mas registros
inseridos manualmente podem ter `NULL`.

### 3.6 [MÉDIO] Regex de limpeza do user-agent desatualizado

`v6/app/Observers/ActivityObserver.php:20-24` remove
`/Chrome\/130\.0\.0\.0\s*/` — **uma versão específica**. Qualquer Chrome
com outra versão deixa o número no campo `browser`. O mesmo vale para
`/Safari\/537\.36\s*/` e `/AppleWebKit\/537\.36\s*\(KHTML, like Gecko\)\s*/`.

### 3.7 [MÉDIO] `Request::ip()` depende de `TrustProxies`

Se o `TrustProxies` não estiver configurado com os proxies reais, o
`ip_address` gravado é o do balanceador, e não o do operador.

### 3.8 [BAIXO] Nenhuma permissão restringe a leitura

Não há checagem de permissão no corpo de `ActivityLogController@index`.
Qualquer usuário autenticado lê a trilha **inteira** — inclusive os
`Detalhe` de operações sensíveis (e-mails de reset de senha, usuários
criados).

### 3.9 [BAIXO] Nenhuma rotação/purga configurada

`config/activitylog.php` define `delete_records_older_than_days = 365`, e o
pacote disponibiliza o comando `activitylog:clean`
(`vendor/spatie/laravel-activitylog/src/CleanActivitylogCommand.php`), mas o
scheduler do projeto agenda **apenas** `reload:evacina` (mensal)
(`v6/app/Console/Kernel.php:26-29`). A tabela `activity_log` cresce
indefinidamente.

### 3.10 [BAIXO] Texto de log errado no `RoleController@destroy`

`->log('Exclusão de permissão')` ao excluir um **perfil** (role). Mesmo
texto em `PermissionController@destroy`, onde está correto.

### 3.11 [BAIXO] `UpdateLogs` tem `description` no `$fillable` sem coluna

`v6/app/Http/Model/Prefecture/UpdateLogs.php:11-19` lista `description` no
`$fillable`, mas a tabela `update_logs` **não tem** essa coluna. Um
`fill()`/`create()` com esse campo causaria erro de coluna inexistente.

### 3.12 [BAIXO] Import não utilizado

`v6/app/Http/Controllers/Admin/ActivityLogController.php` importa
`App\Admin\ActivityLog` e `Sentinel` sem uso.

---

## 4. Código duplicado entre v5 e v6

| Arquivo | v5 vs v6 |
|---|---|
| `app/Http/Controllers/Admin/ActivityLogController.php` | **idêntico** |
| `resources/views/admin/activities-logs/index.blade.php` | **idêntico** |
| `app/Http/Model/Admin/ActivityLog.php` | **idêntico** |
| `public/assets/js/custom/config/logs/datatable.js` | **idêntico** |

O que **mudou** na v6 (e afeta o módulo):

| Mudança | Efeito |
|---|---|
| Tabela `modulos` (migration `2024_11_01_150206`) | introduce a **duplicidade** do slug `logs-do-sistema` |
| `resources.modulo_id` (migration `2024_11_05_104026`) | o recurso passa a depender do status do módulo |
| `v6/resources/views/vendor/adminlte/page.blade.php` | o item de menu passa a depender de `$modulos['logs-do-sistema']` (novo na v6) |
| `v6/app/Providers/ModulosServiceProvider.php` | novo: distribui `$modulos` para as views |
| `Admin\ModulosController` + `admin/modulos/*` | novo: tela de gestão dos módulos |
| `2025_01_16_210444_add_ip_address_to_activity_log.php` | **nova**: colunas `ip_address` e `browser` (não existiam na v5) |
| `v6/app/Observers/ActivityObserver.php` | **novo**: preenche IP e navegador |

> Na v5 o item de menu de logs provavelmente **existia** (o layout AdminLTE
> era menos dependente do registro de módulos). A v6 introduziu a
> dependência de `$modulos[...]` e, com a duplicidade, **quebrou** o acesso
> pelo menu.

---

## 5. Riscos de migração de schema

| Risco | Detalhe | Mitigação |
|---|---|---|
| Duplicidade em `modulos.slug` | Sem `UNIQUE`, qualquer seed/migration pode recriar | adicionar `UNIQUE (slug)` e consolidar os ids 7/32 |
| FK `resources.modulo_id` | Apagar a linha errada quebra a FK | decidir o id sobrevivente **antes** de apagar; mover a FK se necessário |
| `activity_log.properties` em `text` | Converter para `jsonb` exige reescrever as linhas | migration `USING properties::jsonb` após validar que todas as linhas são JSON válido |
| Volume | Milhões de linhas sem índice em `created_at` | criar índice **antes** da migração |
| `update_logs` x `activity_log` | Risco de migração cruzada de semântica | manter tabelas separadas na v7 |
| `delete_records_older_than_days` | Purga não agendada | agendar `activitylog:clean` no `schedule()` do Laravel 11 |

---

## 6. Perguntas em aberto para o negócio

1. **Quem pode ler a trilha de auditoria?** Hoje qualquer operador
   autenticado. Deve haver um perfil restrito (ex.: "auditor")?
2. **A trilha precisa ser pública?** Há interesse em um portal de
   transparência com os logs de publicação (uma "transparência das
   publicações" cada vez mais comum)? Isso mudaria a natureza do módulo.
3. **Qual é o prazo de retenção legally exigido?** O pacote sugere 365
   dias; a LGPD e a legislação municipal podem exigir outros prazos.
4. **Devemos auditar também as tentativas de acesso negado?** Hoje não há
   log de `flash('Você não tem permissão...')`.
5. **O que fazer com o `update_logs`?** Ele é a "versão do sistema", mas o
   rodapé do CMS já mostra a mesma informação que a tela de Novidades
   (`changelog.index`) — as duas coisas coexistem e confuse.
6. **A duplicidade do slug foi intencional?** A migration sugere engano
   (blocos "Configurações" x "Sidebar"), mas é preciso confirmar com o
   negócio antes de consolidar.
