---
id: SVC-ACTIVITYLOGGER
tipo: servico
onda: 2
status: concluido
unidades: [SVC-ACTIVITYLOGGER]
fontes:
  - V2/Services/ActivityLogger/
depende_de: []
consumidores: [SVC-FEEDRSS, CORE-CMS, MOD-PUBLICACOES-NOTICIAS, MOD-CONEXOES-ASSOCIADOS, MOD-CONEXOES-UNIDADEREGIONAL, MOD-CONEXOES-PARCEIROSINSTITUCIONAIS, MOD-INFRAESTRUTURA-CERTIFICADODIGITAL, MOD-INFRAESTRUTURA-GESTAOPERMISSOES, MOD-INSTITUCIONAL-SOBRENOS, MOD-INSTITUCIONAL-HISTORIA, MOD-INSTITUCIONAL-FALECONOSCO, MOD-INSTITUCIONAL-ASSOCIESE, MOD-LAYOUTS-BASICPAGE, MOD-SUPPORT-STRUCTURALLANDINGEDITOR, MOD-SUPPORT-SUPPORTCONFIGS]
criticidade: alta
dono: SvcMailOps
---

# Serviço ActivityLogger

## 0. Ficha de identidade

| Campo | Valor |
|---|---|
| Efeito primário | Grava uma linha em `activity_log` (via `spatie/laravel-activitylog` 2.8) descrevendo uma ação manual sobre um model — com autor Sentinel, IP, user-agent e propriedades — e renderiza esse histórico como timeline no CMS e como selo de última atualização no portal. |
| Entrada | `ActivityLoggerService::log(Model $model, string $description, array $properties = [], string $snapshotId = null)`; `getHistory(Model $model)`; `@include/@component` das duas views. |
| Saída | Linha em `activity_log`; HTML da timeline (`v2-service-activitylogger::cms.timeline`); HTML do selo (`v2-service-activitylogger::portal.last-updated`). |
| Efeitos colaterais | `INSERT` em `activity_log` (sem limite de tamanho no payload, `properties` como texto JSON); leitura de `request()->ip()`/`userAgent()`; consulta por sujeito a cada render da timeline. |
| Particularidade central | **Nada é auditado automaticamente**: o trait `LogsActivity` está comentado (`app/Http/Model/Admin/User.php:39`); toda auditoria depende de uma chamada explícita, e o serviço não calcula diff de atributos. |
| Como se integra | `app(\V2\Services\ActivityLogger\ActivityLoggerService::class)->log($model, 'updated', [...])`; injetado no construtor de dezenas de controllers; views no namespace `v2-service-activitylogger`. |
| Se removido, quebra | timeline de histórico em ~52 arquivos/116 chamadas, selo "Atualizado em" do portal, auditoria de permissões, FeedRSS (auditoria), painel de suporte e histórico de configurações. |
| Superfície pública | `ActivityLoggerService::log`, `ActivityLoggerService::getHistory`, 2 views Blade; a tabela `activity_log`; nenhum controller/rota própria. |

## 1. Efeito

### 1.1 Transformação que o serviço realiza

`log($news, 'updated', ['motivo' => 'Ajuste de texto'])` produz **uma** linha em `activity_log` com:

- `description = 'updated'`;
- `subject_type = <morph class do model>` e `subject_id = $news->getKey()`;
- `causer_type = App\Admin\User` e `causer_id = <id do usuário Sentinel>` **se** houver usuário logado (`ActivityLoggerService.php:22,33-35`);
- `properties` JSON com `motivo`, `v2_audit = true`, `transaction_snapshot_id`, `ip_address`, `user_agent` (`ActivityLoggerService.php:26-31`);
- `log_name = 'default'` (config do pacote).

`getHistory($news)` devolve todas as atividades cujo sujeito é aquele model, ordenadas por `created_at DESC`, já com `causer` carregado (`ActivityLoggerService.php:46-52`). A view `cms.timeline` transforma essa coleção em uma timeline Bootstrap com ícones por tipo, autor, data relativa, até quatro campos alterados e, para o e-mail de suporte, um JSON de debug (`V2/Services/ActivityLogger/Views/cms/timeline.blade.php:15-221`).

### 1.2 Efeitos sobre dados e sistema

| Efeito | Recurso afetado | Ponto no código | Fonte |
|---|---|---|---|
| Insere linha de auditoria | `activity_log` | `activity()->...->log()` | `V2/Services/ActivityLogger/ActivityLoggerService.php:24-37` |
| Associa autor Sentinel | `causer_type`/`causer_id` | `causedBy($user)` | `ActivityLoggerService.php:33-35` |
| Associa sujeito polimórfico | `subject_type`/`subject_id` | `performedOn($model)` | `ActivityLoggerService.php:25` |
| Grava IP e user-agent da requisição | `properties.ip_address`, `properties.user_agent` | `request()->ip()`, `request()->userAgent()` | `ActivityLoggerService.php:29-30` |
| Marca a linha como auditoria V2 | `properties.v2_audit = true` | merge de propriedades | `ActivityLoggerService.php:27` |
| Propaga `transaction_snapshot_id` do model ou parâmetro | `properties.transaction_snapshot_id` | `??` em cascata | `ActivityLoggerService.php:28` |
| Consulta histórico por sujeito (eager `causer`) | `activity_log` | `Activity::forSubject($model)->with('causer')->latest()->get()` | `ActivityLoggerService.php:48-51` |
| Renderiza timeline (opcionalmente consultando o banco na view) | HTML CMS | `app(...)->getHistory($model)` dentro do Blade | `timeline.blade.php:8-13` |
| Renderiza selo de atualização a partir de `updated_at` | HTML portal | `$model->updated_at` | `Views/portal/last-updated.blade.php:11-19` |
| `@push('head-styles')` injeta CSS do selo | stack de estilos do layout do portal | `@push`/`@endpush` | `last-updated.blade.php:23-32` |

### 1.3 Contrato observável

| Entrada | Resultado |
|---|---|
| `log($model, 'created')` com usuário logado | 1 linha; `description='created'`; `causer_id` preenchido; `properties` com 4 chaves padrão; timeline mostra ícone `+` verde e badge "Created" |
| `log($model, 'deleted')` | timeline mostra ícone de lixeira vermelho; badge "Deleted" |
| `log($model, 'qualquer_outra_coisa')` | timeline usa ícone de lápis azul; badge `Qualquer_outra_coisa` (só a 1ª letra é maiúscula) |
| `log($model, $d, ['new' => [...], 'old' => [...]])` | timeline mostra até 4 campos com valor anterior riscado → valor atual; +N alterações restantes |
| `log($model, $d, ['attributes' => [...]])` | mesmo comportamento de `new`, usando `attributes` como valores atuais |
| `log($model, $d, ['motivo' => '...'])` | bloco itálico amarelo com o motivo abaixo dos campos |
| `getHistory($model)` sem atividades | `collect()`; timeline não renderiza **nada** (nem o título) |
| Timeline sem `$model` e sem `$history` | `$history = collect()`; não renderiza |
| Timeline com `$history` já carregado | não consulta o banco (`timeline.blade.php:9`) |
| Selo portal com `updated_at` | `Atualizado em: dd/mm/aaaa às HH:MM` com `<time datetime="W3C">` |
| Selo portal sem `updated_at` | `Atualizado em: Modo Prévia` |
| Usuário sem papel Sentinel chamando `log()` | linha sem causer (`causer_id` nulo) — o serviço não exige autenticação |
| Suporte abrindo a timeline | botão `JSON (Suporte)` com `properties` completo em `<pre>` |

### 1.4 O que o serviço deliberadamente NÃO faz

- **Não audita automaticamente**: não há observer, trait ativo, `static::saved` genérico nem middleware. `//use LogsActivity;` está comentado no model de usuário (`app/Http/Model/Admin/User.php:39`).
- **Não calcula diff**: se o chamador não passar `old`/`new`/`attributes`, a timeline mostra apenas autor, descrição e data.
- **Não filtra segredos**: o payload é gravado como veio; o mascaramento de senha de e-mail é feito pelo chamador (`V2/Modules/Support/StructuralLandingEditor/Services/UserConfigsService.php:150-163`).
- **Não separa logs de aplicação de auditoria**: `activity_log` é auditoria; logs técnicos vão para o canal Laravel (`Log::error/warning`) e o histórico de e-mails para `v2_mail_logs` (§8).
- **Não aplica retenção**: `config/activitylog.php:5` define 365 dias, mas `activitylog:clean` não está no scheduler nem em cron (§3.5).
- **Não tem permissão própria**: qualquer usuário que alcance a tela vê o histórico; o JSON de debug é restrito ao e-mail de suporte.
- **Não reverte nem versiona conteúdo**: guarda apenas o que o chamador enviar.

### 1.5 Efeito por perfil

| Perfil | Efeito |
|---|---|
| Público | vê apenas o selo `portal.last-updated` (quando o módulo inclui); nunca vê a timeline. |
| CMS (editor) | causa auditoria ao salvar/alterar/excluir; vê a timeline no rodapé das telas de edição. |
| Suporte (`config('v2.support.email')`) | vê o botão `JSON (Suporte)` com o payload integral; acessa telas restritas de suporte. |
| Super-admin | vê a timeline como qualquer usuário; **não** recebe o botão JSON (a checagem é só por e-mail de suporte, `timeline.blade.php:54`). |
| Processo de console | `log()` funciona (linha sem IP/UA, sem causer) mas `getHistory` não é renderizado. |

## 2. Abordagem

### 2.1 Estratégia e por quê

Fachada mínima sobre o helper `activity()` do Spatie 2.8. O pacote já fornece tabela (`activity_log`), model (`Spatie\Activitylog\Models\Activity`), relação polimórfica `subject`/`causer` e cast `properties => collection` (`vendor/spatie/laravel-activitylog/src/Models/Activity.php:11-30`). A V2 acrescenta: (a) padronização de propriedades (`v2_audit`, IP, UA, snapshot); (b) vínculo com Sentinel (o pacote usaria o guard default do Laravel); (c) duas views padronizadas. `[I]` A opção por auditoria manual (sem trait) dá controle sobre o texto do evento e evita registrar diffs automáticos de conteúdo grande/HTML; o custo é depender da disciplina de cada controller.

### 2.2 Mecânica passo a passo

```mermaid
flowchart TD
  A["Controller/Service chama log(model, description, properties)"] --> B["Sentinel::getUser()"]
  B --> C["activity()->performedOn(model)"]
  C --> D["withProperties(properties + v2_audit + snapshot + ip + ua)"]
  D --> E{"usuário existe?"}
  E -->|"sim"| F["causedBy(user)"]
  E -->|"não"| G["sem causer"]
  F --> H["log(description) -> INSERT activity_log"]
  G --> H
  I["Blade da tela"] --> J{"history definido ou model?"}
  J -->|"model"| K["getHistory(model)"]
  J -->|"history"| L["usa coleção"]
  J -->|"nenhum"| M["collect()"]
  K --> N["timeline: agrupa por description, formata properties"]
  L --> N
  M --> N
  N --> O{"history vazio?"}
  O -->|"sim"| P["nada renderizado"]
  O -->|"não"| Q["section .v2-service-timeline"]
```

1. O chamador injeta `ActivityLoggerService` e chama `log()` **depois** de persistir a mudança (padrão nos controllers: salvar → `log()` → flash/redirect).
2. `log()` obtém o usuário Sentinel (`ActivityLoggerService.php:22`).
3. Constrói a atividade com `performedOn($model)` e `withProperties(array_merge($properties, [padrões]))` (`:24-31`).
4. Se há usuário, `causedBy($user)` (`:33-35`); `$activity->log($description)` persiste (`:37`).
5. A view, ao ser incluída, decide: usa `$history` se veio; senão consulta `getHistory($model)`; senão coleção vazia (`timeline.blade.php:8-13`).
6. Itera as atividades e, para cada uma, calcula tema por `description` (`:34-43`), autor (`:60-67`), rótulo traduzido (`:70-74`), payload (`:86-108`), até 4 campos (`:142-192`), motivo (`:209-213`) e JSON de suporte (`:196-207`).

### 2.3 Trechos de código pivô

```php
// V2/Services/ActivityLogger/ActivityLoggerService.php:20-38 — o único ponto de escrita.
// As chaves padrão sobrescrevem homônimas enviadas pelo chamador (array_merge).
public function log(Model $model, string $description, array $properties = [], string $snapshotId = null)
{
    $user = Sentinel::getUser();

    $activity = activity()
        ->performedOn($model)
        ->withProperties(array_merge($properties, [
            'v2_audit' => true,
            'transaction_snapshot_id' => $snapshotId ?? $model->transaction_snapshot_id ?? null,
            'ip_address' => request()->ip(),
            'user_agent' => request()->userAgent()
        ]));

    if ($user) {
        $activity->causedBy($user);
    }

    $activity->log($description);
}
```

O que mostra: (1) o parâmetro é `Model` — string/array causam `TypeError`; (2) `array_merge` coloca os padrões por último, então o chamador não consegue sobrescrever `v2_audit`, `ip_address`, `user_agent` nem `transaction_snapshot_id`; (3) sem usuário, a linha é gravada sem autor.

```php
// V2/Services/ActivityLogger/ActivityLoggerService.php:46-52 — leitura sem limite,
// ordenada por created_at, com eager loading do autor.
public function getHistory(Model $model)
{
    return Activity::forSubject($model)
        ->with('causer')
        ->latest()
        ->get();
}
```

O que mostra: não há `take()`/paginação — a timeline materializa **todo** o histórico do registro. O `forSubject` filtra por `subject_type` + `subject_id` (`vendor/spatie/laravel-activitylog/src/Models/Activity.php:98-102`), colunas sem índice composto na migration (§6).

```php
// V2/Services/ActivityLogger/Views/cms/timeline.blade.php:8-13 — a view consulta o
// banco quando recebe só o model; o id do widget deriva do model.
@php
    if (!isset($history)) {
        $history = isset($model) ? app(\V2\Services\ActivityLogger\ActivityLoggerService::class)->getHistory($model) : collect();
    }
    $widgetId = 'timeline-' . ($model->id ?? uniqid());
@endphp
```

O que mostra: incluir o componente duas vezes para o mesmo model gera **dois** widgets com o mesmo `id` (`timeline-<uuid>`), duplicando `aria-labelledby`/`data-bs-target` (armadilha §3.4).

```php
// timeline.blade.php:34-43 — o tema é decidido por igualdade exata de descrição;
// qualquer outro texto cai no ícone de edição.
$desc = strtolower($activity->description);
if ($desc === 'created') {
    $theme = ['icon' => 'bi-plus-lg', 'bg' => 'bg-success', 'text' => 'text-success', 'border' => 'border-success'];
} elseif ($desc === 'deleted') {
    $theme = ['icon' => 'bi-trash', 'bg' => 'bg-danger', 'text' => 'text-danger', 'border' => 'border-danger'];
} else {
    $theme = ['icon' => 'bi-pencil', 'bg' => 'bg-primary', 'text' => 'text-primary', 'border' => 'border-primary'];
}
```

O que mostra: `deleted` não distingue soft delete de hard delete; `archived`, `restored`, `read`, `updated` etc. compartilham o mesmo ícone. O vocabulário não é validado em nenhum ponto.

```php
// timeline.blade.php:86-104 — o payload exibido vem de properties.attributes OU
// properties.new; as chaves técnicas são removidas.
$changedPayload = [];
$oldPayload = [];
if(!empty($activity->properties)) {
    if (isset($activity->properties['attributes'])) {
        $changedPayload = $activity->properties['attributes'];
        $oldPayload = $activity->properties['old'] ?? [];
    } elseif (isset($activity->properties['new'])) {
        $changedPayload = $activity->properties['new'];
        $oldPayload = $activity->properties['old'] ?? [];
    }
}

$ignoredKeys = ['updated_at', 'created_at', 'v2_audit', 'transaction_snapshot_id'];
```

O que mostra: o contrato de payload aceito pela timeline é `attributes`+`old` ou `new`+`old`; `ip_address`/`user_agent`/`motivo` **não** estão em `$ignoredKeys`, então aparecem como “campos alterados” se estiverem dentro de `new`/`attributes` (na prática são irmãos de `old`/`new`, por isso não aparecem).

```php
// timeline.blade.php:70-74 — rótulo do evento; não há traduções activitylog::action.*
// no repositório nem no vendor, então o fallback sempre vence.
@php
    $actionKey = "activitylog::action.{$activity->description}";
    $actionText = Lang::has($actionKey) ? trans($actionKey) : ucfirst($activity->description);
@endphp
{{ $actionText }}
```

O que mostra: o badge exibe o texto técnico (`Updated`, `Gallery_synced`). O namespace `activitylog` não é registrado por `loadTranslationsFrom` (o pacote 2.8 não traz `lang/`), então `Lang::has` retorna `false` e `trans` nunca é usado.

```php
// V2/Services/ActivityLogger/Views/portal/last-updated.blade.php:6-19 — o selo NÃO lê
// activity_log; usa updated_at do model e trata ausência como prévia.
<div class="v2-transparency-badge text-secondary d-inline-flex align-items-center bg-light border p-2 px-3 rounded shadow-sm" style="font-size: 0.85rem;">
    <i class="bi bi-arrow-clockwise fa-spin-hover text-primary me-2" aria-hidden="true"></i>
    <span>
        <span class="visually-hidden">Data da última atualização do conteúdo:</span>
        Atualizado em: 
        @if($model->updated_at)
        <time datetime="{{ $model->updated_at->toW3cString() }}" class="text-dark fw-bold">
            {{ $model->updated_at->format('d/m/Y') }} 
            <span class="visually-hidden">às</span> 
            <small class="text-secondary fw-normal">{{ $model->updated_at->format('H:i') }}</small>
        </time>
        @else
        <span class="text-dark fw-bold">Modo Prévia</span>
        @endif
    </span>
</div>
```

O que mostra: o selo de transparência é derivado do timestamp do conteúdo, não da auditoria — editar sem alterar `updated_at` (update direto no banco) não muda o selo; e não há proteção contra `$model` nulo (`$model->updated_at` em `null` gera erro em PHP 7.4 `[I]`).

### 2.4 Algoritmos e custos

- `log()`: 1 `INSERT` por evento (Spatie não faz update/agregação). Custo O(1) + serialização do payload.
- `getHistory()`: 1 `SELECT` de todas as atividades do sujeito + 1 `SELECT` do causer (eager). Sem `LIMIT`; sem índice por sujeito → `Seq Scan` proporcional ao tamanho da tabela.
- Timeline: loop O(n) com várias formatações por linha (`diffForHumans`, `str_limit`, `json_encode` só para suporte). O container tem `max-height: 500px; overflow-y: auto` (`timeline.blade.php:28`), mas todas as linhas são renderizadas no HTML.
- `last-updated`: O(1), sem query adicional (usa o model já carregado).
- Sem cache: cada abertura de tela de edição reconsulta o histórico completo.

### 2.5 Estado e ciclo de vida

- **Tabela**: criada condicionalmente (`if (!Schema::hasTable('activity_log'))`) pela migration do serviço (`V2/Services/ActivityLogger/Migrations/2026_06_14_100000_create_activity_log_table.php:18-31`). Se já existir (por exemplo, pela migration publicada do pacote), a migration **não altera nada** — colunas ausentes não são adicionadas.
- **Linha**: imutável pela aplicação (não há update/delete no serviço); `down()` faz `DROP TABLE` (`:41-56`), diferente das outras migrations do V2 que renomeiam para backup.
- **Sujeito**: `subject_id` string(36) aceita UUIDs do V2 e IDs inteiros (convertidos para string). Não há `morphMap`, então `subject_type`/`causer_type` guardam FQCN (`App\...`), o que torna renomear classe um evento que “quebra” o histórico.
- **Causer**: `App\Admin\User` (Sentinel), tabela `users`. Deletar um usuário deixa `causer_id` órfão; `$activity->causer` fica `null` e a timeline mostra `Sistema` (`timeline.blade.php:60-67`).
- **Retenção**: configurada em 365 dias, não executada (§3.5).

## 3. Particularidades

### 3.1 O que é exclusivo deste serviço

- É a **única** fonte de auditoria com autor + sujeito + IP/UA do V2. `v2_mail_logs` é histórico operacional de envio; o log Laravel é diagnóstico técnico.
- É o único serviço que fornece **widget de UI** em dois contextos (CMS timeline e selo de portal) sem controller nem rota.
- É dono do vocabulário de eventos de auditoria (texto livre) usado por 52 arquivos.

### 3.2 Invariantes e regras não óbvias

| ID | Regra | Efeito | Fonte |
|---|---|---|---|
| `RN-ACTIVITYLOGGER-01` | Não há auditoria automática; o trait `LogsActivity` está comentado | toda alteração sem `log()` explícito é invisível | `app/Http/Model/Admin/User.php:39` |
| `RN-ACTIVITYLOGGER-02` | Os 4 metadados padrão sobrescrevem chaves homônimas do chamador | `v2_audit`, `ip_address`, `user_agent`, `transaction_snapshot_id` não são customizáveis via `$properties` | `ActivityLoggerService.php:26-31` |
| `RN-ACTIVITYLOGGER-03` | `$model` é `Model`; `$description` é `string` (PHP 7.4, sem `strict_types`) | passar string como sujeito gera `TypeError` (fatal, não capturável por `catch (\Exception)`) | `ActivityLoggerService.php:20`; `AssocieSeConfigController.php:96` |
| `RN-ACTIVITYLOGGER-04` | A timeline só reconhece `created`/`deleted` para tema; o rótulo é `ucfirst(description)` | `archived`, `restored`, `read`, `signed_manually` etc. aparecem como edição | `timeline.blade.php:34-43,70-74` |
| `RN-ACTIVITYLOGGER-05` | `getHistory` não limita nem pagina | telas com histórico longo renderizam tudo | `ActivityLoggerService.php:46-52` |
| `RN-ACTIVITYLOGGER-06` | Retenção de 365 dias não é executada | `activity_log` cresce indefinidamente | `config/activitylog.php:5`; `app/Console/Kernel.php:15-28` |
| `RN-ACTIVITYLOGGER-07` | O botão JSON de debug depende **apenas** do e-mail de suporte | super-admin sem esse e-mail não vê o payload cru | `timeline.blade.php:54,196-207` |
| `RN-ACTIVITYLOGGER-08` | O selo do portal usa `updated_at`, não `activity_log` | selo e timeline podem divergir | `last-updated.blade.php:11-19` |
| `RN-ACTIVITYLOGGER-09` | A timeline consulta o banco na própria view quando não recebe `$history` | renderização de 1 + N consultas em páginas com vários widgets | `timeline.blade.php:8-13` |
| `RN-ACTIVITYLOGGER-10` | `log_name` nunca é definido pela V2 (sempre `default`) | não é possível separar auditorias por domínio no banco | `ActivityLoggerService.php:24-37`; `config/activitylog.php:3-6` |

### 3.3 Casos-limite

| Caso | Comportamento | Fonte |
|---|---|---|
| Nenhum usuário Sentinel (console, webhook, cron) | linha gravada sem `causer_id`; timeline mostra `Sistema` | `ActivityLoggerService.php:22,33-35`; `timeline.blade.php:60-67` |
| `properties` vazio | timeline exibe só autor/descrição/data | `timeline.blade.php:110-194` |
| `properties['attributes']` é lista (array sem chaves string) | timeline renderiza “Lista Atualizada — Contém N item(ns)” e até 3 títulos (`title`/`nome`/`name`) | `timeline.blade.php:107,112-140` |
| Mais de 4 campos | exibe 4 + `+ N alteração(ões)` | `timeline.blade.php:143-189` |
| Valor antigo nulo | mostra apenas o novo (sem seta) | `timeline.blade.php:159-168` |
| `properties['motivo']` | bloco itálico com o texto | `timeline.blade.php:209-213` |
| `description` com acento/espaço | chave de tradução inválida, badge mostra o texto como veio (ucfirst) | `timeline.blade.php:71-73` |
| Sujeito deletado fisicamente | `getHistory` ainda retorna pelo `subject_type`/`subject_id`; `$activity->causer` pode sumir, mas o sujeito não é resolvido na view | `ActivityLoggerService.php:48-51` |
| Model sem `id` (ex.: `new User()` no login) | `subject_id` nulo; não aparece em nenhum `getHistory($model)` | `V2/Core/Controllers/Auth/LoginCmsController.php:73-77` |
| Dois widgets do mesmo model na mesma página | IDs `timeline-<uuid>` duplicados; `aria-labelledby`/collapse apontam para o primeiro | `timeline.blade.php:12` |
| `$model` nulo no selo do portal | erro ao acessar `->updated_at` em `null` `[I]` | `last-updated.blade.php:11` |
| Payload muito grande | `properties` é `text` no Postgres (sem limite prático de 1 GB); HTML da timeline limitado a 4 campos por linha | migration `:27`; `timeline.blade.php:143-189` |
| Linha de auditoria com `properties` nulo | `!empty()` protege os blocos; nenhum erro | `timeline.blade.php:89,209` |

### 3.4 Armadilhas de reimplementação

1. **`log()` com não-Model quebra com `TypeError` e não é capturado por `catch (\Exception)`.** Existe uma chamada assim no legado: `$this->activityLogger->log('associese_config', 'updated', [...])` (`V2/Modules/Institucional/AssocieSe/Controllers/AssocieSeConfigController.php:96`). O `try/catch (\Exception)` do controller **não** protege; a transação já foi commitada e o request termina em erro 500 `[I]`. A reimplementação deve decidir entre aceitar `Model|string` (corrigindo) ou manter o erro (paridade).
2. **Não existe diff automático.** Para a timeline mostrar “antes → depois”, o chamador precisa passar `old` e (`new` ou `attributes`). Chamadas só com `['type' => ...]` mostram apenas o badge.
3. **`array_merge` na ordem errada**: manter os defaults por último é o que garante `v2_audit` verdadeiro; inverter permite que um chamador marque auditoria como falsa.
4. **Sentinel, não o guard do Laravel**: o pacote por padrão pegaria `auth()->user()`; aqui o autor é sempre o usuário Sentinel. Um reimplementação baseada em `Auth` precisa mapear.
5. **Causer com FQCN**: renomear `App\Admin\User` invalida todos os históricos (`causer_type` textual).
6. **Ordem de boot**: o serviço não tem provider; é auto-resolvido. `V2ServiceProvider::register()` só registra `PermissionService` se `ActivityLoggerService` existir (`V2/Core/Providers/V2ServiceProvider.php:353-359`), mas não registra o logger em si.

### 3.5 Dívidas e divergências

- `README.md` sugere `@component('v2-service-activitylogger::cms.timeline', ...)` com slot (`V2/Services/ActivityLogger/README.md:20-27`), mas a view **não usa `$slot`**; o conteúdo passado dentro do componente é descartado na prática (Blade 5.5 renderiza o componente e ignora o slot não impresso). Uso correto é `@include`.
- `README.md:14` exemplifica `['motivo' => ...]` — suportado pela view (`:209-213`), mas não há nenhum chamador usando `motivo` no repositório (busca não retorna).
- O nome `$isSuperAdmin` na view é enganoso: a condição é `email === config('v2.support.email')` (`timeline.blade.php:54`), não papel `super-admin`.
- `config/activitylog.php` reduz o arquivo do pacote a duas chaves; o `mergeConfigFrom` do provider repõe os defaults (`default_log_name='default'`, `enabled=true`, `activity_model` padrão) (`vendor/spatie/laravel-activitylog/src/ActivitylogServiceProvider.php:17-25`).
- `transaction_snapshot_id` é prometido pela UI de e-mail (`email.blade.php:22`) e pela coluna do model `LayoutBaseConfig`, mas nenhum chamador passa o 4º argumento de `log()`; o valor efetivo é sempre `null` (grep por `transaction_snapshot_id` só encontra migration/modelo/view).
- O login audita por fora do serviço: `activity()->performedOn(new User())->causedBy($user)->withProperty('Detalhe', ...)->log('Login no sistema (V2)')` (`LoginCmsController.php:72-78`) — sem IP/UA/`v2_audit`, com sujeito sem id e chave `Detalhe` que a timeline não renderiza (não é `new`/`attributes`).
- `ImportExcelService` grava `v2_audit` manualmente (`V2/Services/ExcelImporter/Services/ImportExcelService.php:63,183`), duplicando a convenção do serviço.

## 4. Integrabilidade

### 4.1 API pública

| Símbolo | Assinatura | Retorno | Exceções | Consumidores |
|---|---|---|---|---|
| `ActivityLoggerService::log` | `log(Model $model, string $description, array $properties = [], string $snapshotId = null): void` | `void` | `TypeError` (argumento inválido), `QueryException` (banco), exceções do Spatie | 52 arquivos / 116 chamadas (§9) |
| `ActivityLoggerService::getHistory` | `getHistory(Model $model): \Illuminate\Support\Collection` | coleção de `Activity` com `causer` | `QueryException`; `TypeError` se não for `Model` | `timeline.blade.php:10`, `activity-footer.blade.php:4`, `BasicPageCmsController.php:252`, `HomepageCmsController.php:38`, `SobreNosCmsController.php:39`, `CertificadoDigitalCmsController.php:343` |
| View `v2-service-activitylogger::cms.timeline` | `@include(..., ['model' => $m])` ou `['history' => $c]`; opcionais `title`, `icon` | HTML (nada se vazio) | erro se `$model` nulo e `$history` não vier | Notícias, BasicPage, SobreNos, StructuralLandingEditor, CertificadoDigital, MailAudit, `v2-cms::partials.activity-footer` |
| View `v2-service-activitylogger::portal.last-updated` | `@include(..., ['model' => $m, 'class' => '...'])`; `$class` é documentado mas **não usado** na view | HTML do selo + `@push('head-styles')` | erro se `$model` nulo | módulos de portal que exibem transparência |

### 4.2 Componentes Blade e contrato de UI

**`v2-service-activitylogger::cms.timeline`**

| Aspecto | Contrato |
|---|---|
| Props | `$model` (opcional se `$history` vier), `$history` (opcional), `$title` (default `Histórico de Alterações`), `$icon` (default `bi-clock-history`) |
| Slot | inexistente na implementação (apesar do README) |
| Estrutura | `<section class="v2-service-timeline mt-5 pt-4 border-top" aria-labelledby="<widgetId>-title">`; `<h3 id="<widgetId>-title">`; botão collapse `data-bs-target="#<widgetId>-body"`; `<div class="collapse show" id="<widgetId>-body">`; `<ul role="list">`; um `<li role="listitem">` por atividade |
| IDs de DOM | `timeline-<model-id|uniqid>` → `-title`, `-body`; `debug-json-<activity-id>` (só suporte) |
| Classes | `v2-service-timeline`, `bi bi-clock-history`, `bi-plus-lg`, `bi-trash`, `bi-pencil`, `bg-success`, `bg-danger`, `bg-primary`, `visually-hidden` |
| Eventos JS | nenhum evento próprio; depende do Bootstrap 5 (`data-bs-toggle="collapse"`, `aria-expanded`) |
| A11y | `aria-labelledby`, `role="list"`/`listitem`, ícones `aria-hidden="true"`, `<time datetime="W3C" title="dd/mm/aaaa HH:MM:ss">`, texto relativo (`diffForHumans`), rótulo oculto do autor não existe |
| Consumidores | `V2/Modules/Publicacoes/Noticias/Views/cms/includes/_editor-content.blade.php:479-483`; `V2/layouts/cms/partials/activity-footer.blade.php:14`; `V2/Modules/Support/StructuralLandingEditor/Views/cms/tabs/layoutbase/components/history-table.blade.php:5`; `.../userconfigs/components/config-history.blade.php:16` |

**`v2-service-activitylogger::portal.last-updated`**

| Aspecto | Contrato |
|---|---|
| Props | `$model` (obrigatório), `$class` (documentado, não aplicado) |
| Estrutura | `<div class="v2-transparency-badge ...">` com ícone `bi-arrow-clockwise` e `<time datetime>`; texto `Atualizado em:`; fallback `Modo Prévia` |
| A11y | `<span class="visually-hidden">Data da última atualização do conteúdo:</span>`, `aria-hidden` no ícone, `visually-hidden` para “às” |
| CSS | `@push('head-styles')` com `.fa-spin-hover` e `.v2-transparency-badge`; usa `var(--tenant-primary)` no hover (depende do tema do portal) |
| Consumidores | nenhum `@include` encontrado no repositório fora do próprio README `[L]` (`LG-ACTIVITYLOGGER-01`) |

### 4.3 JS e ciclo de vida

Não há arquivo JS do serviço. A timeline depende de:
- Bootstrap 5 bundle do layout CMS (collapse e tooltips nativos via `data-bs-*`);
- `diffForHumans()` no servidor (não atualiza em tempo real);
- `Lang`/`Sentinel`/`config()` disponíveis no escopo da view (facades globais).

Sem JS, o bloco `collapse show` permanece visível (a timeline continua legível); o botão “Expandir / Recolher” não funciona.

### 4.4 Ordem de inicialização e registro

1. Migration `2026_06_14_100000_create_activity_log_table` roda **antes** das demais do V2 (mail `2026_06_30`, feed `2026_07_20`), mas só cria a tabela se ela não existir (`:18-31`).
2. `config/app.php` registra `Spatie\Activitylog\ActivitylogServiceProvider` (dependência do pacote) e as facades `Lang`, `Sentinel` (`config/app.php:219,238`).
3. `V2ServiceProvider` carrega as views do serviço no namespace `v2-service-activitylogger` e as migrations no boot (`V2/Core/Providers/V2ServiceProvider.php:176,182-191`). Não há provider próprio do ActivityLogger.
4. `FeedRssServiceProvider` depende de `ActivityLoggerService` no `register()` (`V2/Services/FeedRSS/Providers/FeedRssServiceProvider.php:28-32`); se a classe não existir, o boot quebra.
5. `V2ServiceProvider::register()` só registra `PermissionService` condicionado a `class_exists(ActivityLoggerService::class)` (`V2/Core/Providers/V2ServiceProvider.php:353-359`).

### 4.5 Receitas de uso

**R1 — registrar uma alteração em controller (padrão canônico).**

```php
use V2\Services\ActivityLogger\ActivityLoggerService;

public function __construct(ActivityLoggerService $activityLogger) { $this->activityLogger = $activityLogger; }

public function update(Request $request, $id)
{
    $model = Modelo::findOrFail($id);
    $old = $model->only(['title', 'status']);
    $model->fill($request->only(['title', 'status']))->save();

    $this->activityLogger->log($model, 'updated', [
        'old' => $old,
        'new' => $model->only(['title', 'status']),
        'motivo' => 'Alteração via painel V2',
    ]);
}
```

Resultado: timeline com dois campos (`title`, `status`) no formato anterior → atual e o motivo em itálico; autor = usuário Sentinel; `subject_id` = id do registro.

**R2 — criar/remover com descrição fixa.**

```php
$this->activityLogger->log($instituicao, 'created', ['type' => 'instituicao_create']);
$this->activityLogger->log($instituicao, 'deleted', ['type' => 'instituicao_delete']);
```

Resultado: badges `Created` (verde) e `Deleted` (vermelho); sem bloco de campos.

**R3 — exibir a timeline em uma tela CMS.**

```blade
@include('v2-service-activitylogger::cms.timeline', [
    'model' => $news,
    'title' => '',
    'icon'  => 'bi-clock-history',
])
```

Resultado: consulta o histórico no render; se vazio, **não** emite `<section>`. Preferir passar `history` já carregado quando o controller puder evitar a consulta na view.

**R4 — usar o partial de rodapé do layout CMS.**

```blade
@include('v2-cms::partials.activity-footer', ['model' => $instituicao])
```

Resultado: carrega `getHistory` dentro de `try/catch` e só inclui a timeline se houver histórico (`V2/layouts/cms/partials/activity-footer.blade.php:1-15`).

**R5 — selo de transparência no portal.**

```blade
@include('v2-service-activitylogger::portal.last-updated', ['model' => $noticia])
```

Resultado: `Atualizado em: 20/10/2026 às 14:32` com `<time datetime="...">`; se `updated_at` nulo, `Atualizado em: Modo Prévia`.

### 4.6 Anti-uso

1. **Passar string/array como sujeito.** `log('associese_config', 'updated', [...])` lança `TypeError`, não capturado por `catch (\Exception)`, derrubando o request **depois** do commit (`AssocieSeConfigController.php:87-99`). Use um model real ou um model anônimo como em `UserConfigsService.php:157-159`.
2. **Esperar que a timeline gere o diff.** Sem `old`/`new`, não há “antes → depois”; a auditoria vira só um carimbo.
3. **Usar `@component` com slot** conforme o README: o slot não é renderizado pela view; use `@include`.
4. **Passar `$model` nulo para o selo do portal.** `last-updated.blade.php:11` acessa `$model->updated_at` sem checagem.
5. **Confiar em retenção automática.** Nada apaga linhas antigas; um reimplementação que dependa de “365 dias” precisa agendar `activitylog:clean` explicitamente.

### 4.7 Custos e limites

| Limite | Valor | Fonte |
|---|---|---|
| Tamanho de `description` | `text` | migration `:22` |
| Tamanho de `properties` | `text` (JSON) | migration `:27` |
| `subject_id`/`causer_id` | varchar(36) | migration `:23,25` |
| Índices | apenas `log_name` | migration `:29` |
| Limite de linhas por timeline | nenhum (4 campos por linha, altura 500 px com scroll) | `timeline.blade.php:28,143-189` |
| Retenção efetiva | infinita (config 365 sem job) | `config/activitylog.php:5` |
| Throttle/quotas | n/a (chamada interna) | — |

## 5. Rotas e endpoints

Não se aplica: o serviço não registra rotas nem controllers. A consulta acontece via renderização de views dentro de páginas de outros módulos; não existe endpoint JSON de auditoria.

## 6. Dados e migrations

### `activity_log`

| Coluna | Tipo | Null/default | Observação |
|---|---|---|---|
| `id` | increments (int) | PK | auto-incremento |
| `log_name` | varchar(255) | null | índice; sempre `default` na V2 |
| `description` | text | NOT NULL | texto livre (`created`, `updated`, `feed_rss_item_upserted`, …) |
| `subject_id` | varchar(36) | null | UUID/inteiro como string |
| `subject_type` | varchar(255) | null | FQCN do model |
| `causer_id` | varchar(36) | null | id Sentinel |
| `causer_type` | varchar(255) | null | `App\Admin\User` |
| `properties` | text | null | JSON; cast `collection` no model do pacote |
| `created_at`/`updated_at` | timestamps | null | `updated_at` nunca muda na prática |

Sem FK, sem soft delete, sem unique. `up()` só cria se a tabela não existir (`V2/Services/ActivityLogger/Migrations/2026_06_14_100000_create_activity_log_table.php:18-31`); `down()` faz `DROP TABLE` (`:41-56`). Model usado: `Spatie\Activitylog\Models\Activity` (`config/activitylog.php:4`; `vendor/spatie/laravel-activitylog/src/Models/Activity.php:11-30`), tabela definida por `config('activitylog.table_name')`.

### `v2_feed_rss_items` e `v2_mail_logs`

Não são do serviço, mas escrevem auditoria (`FeedRssService`) ou são histórico próprio (`v2_mail_logs`). Ver docs `feed-rss.md` e `mail-service.md`.

## 7. Configuração e permissões

| Chave | Default | Efeito | Fonte |
|---|---|---|---|
| `activitylog.table_name` | `activity_log` | tabela usada pelo model do pacote | `config/activitylog.php:4` |
| `activitylog.delete_records_older_than_days` | `365` | usado apenas por `activitylog:clean`, que não está agendado | `config/activitylog.php:5`; `app/Console/Kernel.php:15-28` |
| `activitylog.default_log_name` | `default` (default do pacote, reposto por `mergeConfigFrom`) | `log_name` de todas as linhas | `vendor/spatie/laravel-activitylog/src/ActivitylogServiceProvider.php:17-25` |
| `activitylog.enabled` | `true` (`ACTIVITY_LOGGER_ENABLED`) | desligar impede qualquer `INSERT` no pacote | `vendor/spatie/laravel-activitylog/config/activitylog.php:8` |
| `v2.support.email` | `suporte@maxima.inf.br` | libera o botão de JSON de debug na timeline | `config/v2.php:4-7`; `timeline.blade.php:54` |

Permissões: o serviço não tem RBAC. Ver a timeline depende de o usuário chegar à página de edição (RBAC do módulo). O JSON completo é visível para `Sentinel::getUser()->email === config('v2.support.email')`; não há checagem de papel `super-admin`.

## 8. Observabilidade e erros

**Auditoria (`activity_log`) × log de aplicação × histórico operacional**

| Mecanismo | Onde | O que registra | Autor/IP | Quem consulta | Retenção |
|---|---|---|---|---|---|
| `ActivityLoggerService` | `activity_log` | evento de negócio explícito sobre um model (`created`, `updated`, `deleted`, `read`, `feed_rss_item_*`, …) | sim (Sentinel, IP, UA, snapshot) | timeline CMS, histórico de configs | infinita na prática |
| `Log` (facade) | `storage/logs/laravel.log` | erro técnico (`[MailService] …`, `[News] …`, `[PageCache] …`) | não | DevOps | rotação do arquivo |
| `v2_mail_logs` | tabela | cada tentativa de envio de e-mail, com corpo e status | apenas `profile`/remetente | MailAudit | soft delete; sem purge |
| `activity()` direto | `activity_log` | login (`Login no sistema (V2)`) fora do serviço | causer sim, IP/UA não | timeline (não aparece por sujeito sem id) | idem |

| Situação | Sinal | Texto/detalhe | Fonte |
|---|---|---|---|
| Falha ao gravar auditoria | exceção do Spatie/banco | propaga ao chamador (não há `try/catch` no serviço) | `ActivityLoggerService.php:37` |
| Falha ao carregar histórico no rodapé CMS | `try/catch` silencioso | `$history = collect()` | `activity-footer.blade.php:3-7` |
| Falha ao carregar histórico na timeline | sem `try/catch` | erro 500 na página | `timeline.blade.php:10` |
| Sujeito sem causer | renderização | `Sistema` | `timeline.blade.php:60-67` |
| Causer sem nome | renderização | e-mail | `timeline.blade.php:63-65` |
| `TypeError` por sujeito inválido | fatal | não capturado por `\Exception` | `ActivityLoggerService.php:20`; `AssocieSeConfigController.php:96` |

Dados sensíveis: o serviço **não mascara nada**. Quem grava senha deve mascarar antes (`UserConfigsService.php:150-163` substitui por `***`). IP e user-agent são PII armazenados sem prazo.

## 9. Consumidores (mapa de integração)

Levantamento por grep: 116 chamadas `activityLogger->log(...)` em 52 arquivos, 4 inclusões das views.

| Grupo/Unidade | O que usa | Exemplos de descrição | Risco de quebra |
|---|---|---|---|
| `SVC-FEEDRSS` | `log()` | `feed_rss_item_upserted`, `feed_rss_item_removed`, `feed_rss_item_enabled`, `feed_rss_item_disabled`, `feed_rss_item_deleted` | alto — assinatura e sujeito vazio |
| `CORE-CMS` (`ConexaoTagCmsController`, `PermissionService`, `RegionalScopeService`) | `log()` | `created`, `deleted`, `updated`, `regional_access_denied` | alto — permissões auditadas |
| `MOD-CONEXOES-ASSOCIADOS` | `log()` + timeline no edit | `restored`, `created`, `updated`, `deleted`, `gallery_uploaded`, `gallery_synced` | médio |
| `MOD-CONEXOES-UNIDADEREGIONAL` | `log()` + timeline | `api_token_generated`, `api_token_revoked`, `associados_synced`, `associado_archived`, `noticia_archived` | médio |
| `MOD-CONEXOES-PARCEIROSINSTITUCIONAIS` | `log()` | `archived`, `updated` | médio |
| `MOD-INFRAESTRUTURA-CERTIFICADODIGITAL` | `log()` + `activity-footer` | `signed_manually`, `created`, `deleted` | médio |
| `MOD-INFRAESTRUTURA-GESTAOPERMISSOES` | `log()` | `updated`, `created` | alto (segurança) |
| `MOD-INSTITUCIONAL-ASSOCIESE` | `log()` | `associese_config` (**string — bug**), `created`, `updated` | alto — `TypeError` |
| `MOD-INSTITUCIONAL-FALECONOSCO` | `log()` | `read`, `replied`, `created`, `deleted` | baixo |
| `MOD-INSTITUCIONAL-SOBRENOS` / `HISTORIA` | `log()` + `activity-footer` | `created`, `updated`, `deleted` | baixo |
| `MOD-LAYOUTS-BASICPAGE` | `log()` + `activity-footer` | `created`, `updated`, `deleted`, `cache_cleared`, `highlight_toggled`, `seo_updated` | médio |
| `MOD-SUPPORT-STRUCTURALLANDINGEDITOR` | `log()` + timeline em configs/histórico | `updated`, `cache_cleared`, `category_created/updated/deleted` | médio — `old`/`new` com payload grande |
| `MOD-SUPPORT-SUPPORTCONFIGS` | `log()` | `updated` de configurações | médio |
| `MOD-PUBLICACOES-NOTICIAS` | timeline no editor | `created`, `updated`, `restored`, `super_highlight_set` | baixo |
| `MOD-PUBLICACOES-TRABALHECONOSCO` | `log()` | `status_updated`, `created`, `deleted` | baixo |
| `SVC-GALLERY` / `SVC-LANDINGSECTIONSEDITOR` / `SVC-EXCELIMPORTER` | `log()` | `imported`, `created`, `deleted` (+ `v2_audit` manual) | baixo |
| `UI-PORTAL` | selo `portal.last-updated` | — | baixo (sem uso encontrado) |

Vocabulário observado (contagem de chamadas): `updated` 30, `created` 20, `deleted` 16, `archived` 5, `restored` 4, `gallery_synced` 3, `status_updated` 2, `gallery_uploaded` 2, e um de cada: `super_highlight_set`, `signed_manually`, `seo_updated`, `replied`, `regional_access_denied`, `read`, `noticia_archived`, `imported`, `highlight_toggled`, `feed_rss_item_upserted`, `feed_rss_item_removed`, `feed_rss_item_deleted`, `category_*`, `cache_cleared`, `associese_config`, `associados_synced`, `associado_updated`, `associado_link_removed`, `associado_archived`, `api_token_generated`, `api_token_revoked`, `feed_rss_item_enabled/disabled` (variável). Não existe enum/classe de constantes.

## 10. Lacunas

| ID | Lacuna | Impacto | Onde procurar |
|---|---|---|---|
| `LG-ACTIVITYLOGGER-01` | A view `portal.last-updated` não tem `@include` no repositório (só o README) | não se sabe se algum tema/portal a consome dinamicamente | busca em satélites / `REFERENCE/` |
| `LG-ACTIVITYLOGGER-02` | Nenhuma tradução `activitylog::action.*` | rótulos em inglês/técnico; reimplementação pode querer pt-BR | `resources/lang` ausente no repo |
| `LG-ACTIVITYLOGGER-03` | Não há agendamento de `activitylog:clean` | tabela cresce sem limite; retenção de 365 dias é só intenção | `app/Console/Kernel.php`; cron do servidor |
| `LG-ACTIVITYLOGGER-04` | Não se sabe o volume real de `activity_log` nem o impacto das consultas por sujeito sem índice | risco de timeline lenta em produção | banco de produção / `EXPLAIN` |
| `LG-ACTIVITYLOGGER-05` | Não há teste automatizado do serviço nem das views | paridade manual | `tests/` |
| `LG-ACTIVITYLOGGER-06` | Chamadas com sujeito inválido (`associese_config`) indicam que o contrato pretendido poderia aceitar string; não há decisão registrada | reimplementação precisa escolher entre corrigir e manter o erro | `AssocieSeConfigController.php:96` |

## 11. Checklist de paridade

- [ ] `PC-ACTIVITYLOGGER-01` — `log($model, 'created')` autenticado grava 1 linha com `subject_type`/`subject_id`, `causer_type=App\Admin\User`, `causer_id`, `description='created'` (`ActivityLoggerService.php:24-37`).
- [ ] `PC-ACTIVITYLOGGER-02` — `properties` contém `v2_audit=true`, `transaction_snapshot_id`, `ip_address`, `user_agent`; valores homônimos do chamador são sobrescritos (`ActivityLoggerService.php:26-31`).
- [ ] `PC-ACTIVITYLOGGER-03` — sem usuário Sentinel, a linha é gravada com `causer_id` nulo (`ActivityLoggerService.php:33-35`).
- [ ] `PC-ACTIVITYLOGGER-04` — `getHistory($model)` retorna somente atividades daquele sujeito, em `created_at DESC`, com `causer` carregado e **sem limite** (`ActivityLoggerService.php:46-52`).
- [ ] `PC-ACTIVITYLOGGER-05` — timeline sem histórico não renderiza `<section>` (`timeline.blade.php:15,221`).
- [ ] `PC-ACTIVITYLOGGER-06` — timeline com `created` usa ícone `bi-plus-lg`/`bg-success`; `deleted` usa `bi-trash`/`bg-danger`; demais usam `bi-pencil`/`bg-primary` (`timeline.blade.php:34-43`).
- [ ] `PC-ACTIVITYLOGGER-07` — timeline com `old`+`new` exibe no máximo 4 campos e `+ N alteração(ões)` para o restante (`timeline.blade.php:143-189`).
- [ ] `PC-ACTIVITYLOGGER-08` — `properties['attributes']` tem precedência sobre `properties['new']` (`timeline.blade.php:90-97`).
- [ ] `PC-ACTIVITYLOGGER-09` — chaves `updated_at`, `created_at`, `v2_audit`, `transaction_snapshot_id` nunca aparecem como campos alterados (`timeline.blade.php:99-105`).
- [ ] `PC-ACTIVITYLOGGER-10` — `properties['motivo']` aparece em bloco itálico com ícone `bi-chat-quote` (`timeline.blade.php:209-213`).
- [ ] `PC-ACTIVITYLOGGER-11` — botão `JSON (Suporte)` só aparece quando o e-mail Sentinel é igual a `config('v2.support.email')`; o JSON é `JSON_PRETTY_PRINT` de `properties` (`timeline.blade.php:54,196-206`).
- [ ] `PC-ACTIVITYLOGGER-12` — selo do portal usa `updated_at` e exibe `Modo Prévia` quando nulo; emite `<time datetime>` W3C (`last-updated.blade.php:11-19`).
- [ ] `PC-ACTIVITYLOGGER-13` — migration não recria `activity_log` se já existir; `down()` remove a tabela (`Migration:18-31,41-56`).
- [ ] `PC-ACTIVITYLOGGER-14` — namespace de views `v2-service-activitylogger` resolve `cms.timeline` e `portal.last-updated` (`V2ServiceProvider.php:176,187`).
- [ ] `PC-ACTIVITYLOGGER-15` — descrição desconhecida é exibida como `ucfirst(description)` (sem tradução) (`timeline.blade.php:70-74`).

## 12. Checklist de extração

| Item | Resposta |
|---|---|
| A1–A4 | §1.1–1.2. |
| A5 | §1.3 (linha por entrada, incluindo vazio/prévia). |
| A6 | §1.4 (sem auditoria automática, sem diff, sem retenção, sem permissão). |
| A7 | §1.5. |
| B1–B3 | §2.1–2.2. |
| B4 | §2.3 com 6 trechos (service 20-38, 46-52, timeline 8-13, 34-43, 86-104, 70-74, last-updated 6-19 — 7 no total). |
| B5–B6 | §2.4–2.5. |
| B7 | §2.5: insert sem transação com o domínio; falha propaga; views degradam de formas diferentes (rodapé silencia, timeline não). |
| C1–C2 | §3.1. |
| C3 | §3.2 (`RN-ACTIVITYLOGGER-01..10`). |
| C4–C5 | §3.3. |
| C6 | §3.4. |
| C7 | §3.5 (README com `@component`, `$class` não usado, `$isSuperAdmin` enganoso, login fora do serviço, snapshot nunca preenchido). |
| D1–D3 | §4.1, §4.4. |
| D4 | §4.2 (duas views; sem slots reais). |
| D5–D6 | §4.5–4.6. |
| D7 | §4.7. |
| D8 | §9. |
| D9 | Extensão: nova `description` é aceita sem registro; novos widgets usam `@include`; não há hook de formatação de payload. |
| E1–E6 | Não se aplica (sem rotas); §5 justifica. |
| E7 | Não se aplica. |
| F1–F6 | §6; cardinalidade cresce com o número de ações auditadas, sem purge. |
| G1 | As telas são dos módulos; a timeline aparece embutida (§4.2). |
| G2–G4 | Não se aplica (sem formulário próprio); estados: vazio (nada), com histórico, com JSON expandido. |
| G5 | JS de terceiros (Bootstrap collapse) — §4.3. |
| G6 | §4.2 (a11y da timeline/selo). |
| H1–H2 | O selo mostra apenas data de atualização; não há headings (usa `<div>`/`<span>`). |
| H3–H6 | Não se aplica ao serviço; o selo não altera SEO. |
| I1–I8 | §4.2–4.3; strings sem i18n (texto fixo em pt-BR na view). |
| J1–J2 | §7. |
| J3–J5 | Não se aplica (sem `ModuleProvider`/flag); `activitylog.enabled` desliga a gravação inteira. |
| K1–K5 | §8; segredos não são mascarados pelo serviço; IP/UA retidos sem prazo. |
| L1–L3 | Não se aplica (sem seeder; a tabela nasce vazia). |
| M1–M4 | §11, §10, §13, lint/índices emitidos. |

## 13. Evidências consultadas

- `V2/Services/ActivityLogger/ActivityLoggerService.php` (1-53, integral).
- `V2/Services/ActivityLogger/Migrations/2026_06_14_100000_create_activity_log_table.php` (1-57, integral).
- `V2/Services/ActivityLogger/Views/cms/timeline.blade.php` (1-222, integral).
- `V2/Services/ActivityLogger/Views/portal/last-updated.blade.php` (1-32, integral).
- `V2/Services/ActivityLogger/README.md` (1-36).
- `config/activitylog.php` (1-6); `vendor/spatie/laravel-activitylog/config/activitylog.php:1-44`; `src/ActivityLogger.php:15-185`; `src/Models/Activity.php:11-102`; `src/ActivitylogServiceProvider.php:9-58`.
- `app/Http/Model/Admin/User.php:36-75`; `V2/Core/Controllers/Auth/LoginCmsController.php:72-78`.
- `V2/layouts/cms/partials/activity-footer.blade.php:1-15`; `V2/Modules/Publicacoes/Noticias/Views/cms/includes/_editor-content.blade.php:470-486`; `V2/Modules/Support/StructuralLandingEditor/Views/cms/tabs/layoutbase/components/history-table.blade.php:5`; `.../userconfigs/components/config-history.blade.php:16`.
- `V2/Core/Providers/V2ServiceProvider.php:168-191,353-359`; `V2/Services/FeedRSS/Providers/FeedRssServiceProvider.php:26-33`; `V2/Services/FeedRSS/Services/FeedRssService.php:61-67,85-90,191-196,218-222`.
- `V2/Modules/Institucional/AssocieSe/Controllers/AssocieSeConfigController.php:80-110`; `V2/Modules/Support/StructuralLandingEditor/Services/UserConfigsService.php:145-165`.
- `app/Console/Kernel.php:15-28`; `config/v2.php:4-7`.
- Varredura `grep -rn "activityLogger->log"` (116 chamadas) e `grep -rn "v2-service-activitylogger"` (4 inclusões) em `V2/`.
