# Logs do Sistema — Migração para Laravel Moderno

> Plano de reconstrução do módulo de logs do sistema em **PHP 8.x +
> Laravel 11/12**.

---

## 1. Decisões de equivalência (o que NÃO pode mudar)

| Item | Regra |
|---|---|
| Mensagens de log | Os textos em `description` (`Listou usuários`, `Cadastro de usuário`, `Login no sistema`, ...) são legíveis por humanos e podem ser referenciados em relatórios. Devem ser preservados ou mapeados 1:1. |
| Detalhe | A chave `Detalhe` dentro de `properties` é usada por toda a leitura. Deve continuar existindo. |
| IP + navegador | Colunas `ip_address` e `browser` devem continuar sendo preenchidas automaticamente. |
| Visibilidade | O item de menu "Logs do sistema" **deve voltar a aparecer** — é um requisito funcional, não uma melhoria. |
| Ciclo de vida | A trilha é **somente leitura** no CMS; não há CRUD de log. |

---

## 2. Schema de migração Laravel

```php
// 0001_01_01_000010_create_activity_logs_table.php
Schema::create('activity_logs', function (Blueprint $table) {
    $table->id();
    $table->string('log_name')->default('default')->index();   // 'default' | 'auth' | ...
    $table->string('action')->index();                          // enum: created, updated, deleted, accessed, ...
    $table->text('description');
    $table->nullableMorphs('subject');                          // subject_type + subject_id
    $table->nullableMorphs('causer');                           // causer_type + causer_id
    $table->jsonb('properties')->nullable();                    // {"Detalhe": "..."}
    $table->string('ip_address', 45)->nullable();
    $table->string('browser')->nullable();
    $table->timestamp('created_at')->nullable()->index();
    $table->timestamp('updated_at')->nullable();
});
```

### 2.1 Consolidação da duplicidade de `modulos`

```php
// 0001_01_01_000020_consolidate_duplicate_module_slugs.php
public function up(): void
{
    // 1) Move a FK do recurso para o módulo "ativo" (id mais baixo da duplicidade)
    DB::table('resources')
        ->where('slug', 'logs-do-sistema')
        ->update(['modulo_id' => 7]);

    // 2) Remove a linha duplicada (a de status = false e sem dependências)
    DB::table('modulos')->where('slug', 'logs-do-sistema')->where('status', false)->delete();

    // 3) Impede novas duplicidades
    Schema::table('modulos', function (Blueprint $table) {
        $table->unique('slug');
    });
}

public function down(): void
{
    Schema::table('modulos', function (Blueprint $table) {
        $table->dropUnique(['slug']);
    });

    DB::table('modulos')->insert([
        'name' => 'Logs do Sistema', 'slug' => 'logs-do-sistema',
        'status' => false, 'created_at' => now(), 'updated_at' => now(),
    ]);
}
```

> **Antes de executar**, auditar todos os pares `(name, slug)` duplicados em
> `modulos` e decidir, para cada um, qual id sobrevive. O script acima
> trata apenas o caso conhecido; a v7 deve ter um comando de auditoria
> (`php artisan modules:audit-duplicates`).

### 2.2 Cópia dos dados existentes

```php
DB::statement('
    INSERT INTO activity_logs
        (log_name, action, description, subject_type, subject_id,
         causer_type, causer_id, properties, ip_address, browser, created_at, updated_at)
    SELECT
        COALESCE(log_name, \'default\'),
        \'legacy\',
        description,
        subject_type, subject_id,
        causer_type, causer_id,
        properties::jsonb,
        ip_address, browser, created_at, updated_at
    FROM activity_log
');
```

> `properties::jsonb` falha se alguma linha tiver JSON inválido. Validar
> antes:
> `SELECT id FROM activity_log WHERE properties IS NOT NULL AND NOT (properties ~ '^\s*[\{\[]')`.

---

## 3. Modelos

```php
// app/Models/ActivityLog.php
final class ActivityLog extends Model
{
    use HasFactory;

    public const UPDATED_AT = null;   // a trilha é append-only

    protected $table = 'activity_logs';

    protected $fillable = [
        'log_name', 'action', 'description',
        'subject_type', 'subject_id',
        'causer_type', 'causer_id',
        'properties', 'ip_address', 'browser',
    ];

    protected $casts = [
        'properties' => 'array',
        'created_at' => 'datetime',
    ];

    public function subject(): MorphTo { return $this->morphTo(); }
    public function causer(): MorphTo  { return $this->morphTo(); }
    public function causerName(): ?string
    {
        return $this->causer_type === User::class
            ? User::find($this->causer_id)?->name
            : null;
    }
}
```

### 3.1 Gravar por **evento de domínio**, não por chamada manual

O problema central do legado é a gravação manual. Na v7, usar um trait +
observer:

```php
// app/Support/Auditable.php
trait Auditable
{
    public static function bootAuditable(): void
    {
        static::created(fn (Model $m) => audit_log($m, 'created', "Criou {$m->getMorphClass()} #{$m->getKey()}"));
        static::updated(fn (Model $m) => audit_log($m, 'updated', "Editou {$m->getMorphClass()} #{$m->getKey()}"));
        static::deleted(fn (Model $m) => audit_log($m, 'deleted', "Excluiu {$m->getMorphClass()} #{$m->getKey()}"));
    }
}
```

```php
// app/Support/AuditLogger.php
function audit_log(?Model $subject = null, string $action = 'custom', string $description = '', array $properties = []): void
{
    ActivityLog::create([
        'log_name'    => 'default',
        'action'      => $action,
        'description' => $description,
        'subject_type'=> $subject?->getMorphClass(),
        'subject_id'  => $subject?->getKey(),
        'causer_type' => auth()->user()?->getMorphClass(),
        'causer_id'   => auth()->user()?->getKey(),
        'properties'  => $properties + ['Detalhe' => ''],
        'ip_address'  => request()->ip(),
        'browser'     => Str::limit((string) request()->userAgent(), 255),
    ]);
}
```

Para as ações que **não** são CRUD (login, listagem, inativação), usar um
evento/listener de domínio:

```php
// app/Listeners/RecordLogin.php
final class RecordLogin
{
    public function handle(Login $event): void
    {
        ActivityLog::create([
            'action'      => 'login',
            'description' => 'Login no sistema',
            'causer_type' => User::class,
            'causer_id'   => $event->user->getKey(),
            'properties'  => ['Detalhe' => "Foi efetuado login pelo usuario: {$event->user->name}"],
            'ip_address'  => request()->ip(),
            'browser'     => request()->userAgent(),
        ]);
    }
}
```

> O `Login` do Laravel 11 é um evento do framework; o Sentinel v2 não
> dispara eventos, então é preciso um listener do `Sentinel` ou um hook
> no guard.

---

## 4. Autorização

Hoje **qualquer** usuário autenticado lê a trilha inteira. Na v7:

```php
// app/Policies/ActivityLogPolicy.php
final class ActivityLogPolicy
{
    public function viewAny(User $user): bool
    {
        return $user->hasCapability('logs-do-sistema.index');
    }
}
```

E o recurso correspondente no catálogo:

| recurso | ação | descrição |
|---|---|---|
| `logs-do-sistema` | `index` | Visualizar os logs do sistema |
| `logs-do-sistema` | `export` | Exportar os logs do sistema |

> O slug `registro-de-atividades.index`, usado no menu do legado, **não
> existe** no catálogo `resources`. Na v7 o slug deve ser o mesmo do
> recurso (`logs-do-sistema`), alinhando menu, matriz de permissões e
> código.

---

## 5. Form Requests

Não há escrita pela UI. Para a **exportação** e para os **filtros**:

```php
final class ActivityLogIndexRequest extends FormRequest
{
    public function authorize(): bool
    {
        return $this->user()->can('viewAny', ActivityLog::class);
    }

    public function rules(): array
    {
        return [
            'from'      => ['nullable', 'date'],
            'to'        => ['nullable', 'date', 'after_or_equal:from'],
            'causer_id' => ['nullable', 'integer', 'exists:users,id'],
            'action'    => ['nullable', 'string', Rule::in(ActivityAction::values())],
            'subject_type' => ['nullable', 'string', 'max:191'],
            'subject_id'   => ['nullable', 'integer'],
            'per_page'  => ['nullable', 'integer', 'min:10', 'max:200'],
        ];
    }
}
```

---

## 6. Endpoints REST/JSON sugeridos

| Método | URI | Descrição | Autorização |
|---|---|---|---|
| GET | `/api/logs` | lista paginada, com filtros `from`, `to`, `causer_id`, `action`, `subject_type`, `subject_id`, busca textual | `logs-do-sistema.index` |
| GET | `/api/logs/{log}` | detalhe de um registro | `logs-do-sistema.index` |
| GET | `/api/logs/export` | exportação CSV (mesmos filtros) | `logs-do-sistema.export` |
| GET | `/api/logs/actions` | catálogo de ações para o filtro | `logs-do-sistema.index` |
| DELETE | `/api/logs/prune` | purga manual (super-admin) | `super-admin` |

A paginação **deve** ser server-side (o legado carrega 5000 linhas e
processa em PHP):

```php
public function index(ActivityLogIndexRequest $request): AnonymousResourceCollection
{
    return ActivityLogResource::collection(
        ActivityLog::query()
            ->when($request->filled('from'), fn ($q) => $q->where('created_at', '>=', $request->date('from')))
            ->when($request->filled('to'),   fn ($q) => $q->where('created_at', '<=', $request->date('to')))
            ->when($request->filled('causer_id'), fn ($q) => $q->where('causer_id', $request->integer('causer_id')))
            ->when($request->filled('action'), fn ($q) => $q->where('action', $request->string('action')))
            ->latest('created_at')
            ->paginate($request->integer('per_page', 50))
    );
}
```

---

## 7. API Resources

```php
final class ActivityLogResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id'          => $this->id,
            'action'      => $this->action,
            'description' => $this->description,
            'details'     => $this->properties['Detalhe'] ?? null,
            'properties'  => $this->properties,
            'subject'     => $this->whenLoaded('subject', fn () => [
                'type' => $this->subject_type,
                'id'   => $this->subject_id,
            ]),
            'causer'      => [
                'id'   => $this->causer_id,
                'name' => $this->causerName(),
            ],
            'ip_address'  => $this->ip_address,
            'browser'     => $this->browser,
            'created_at'  => $this->created_at?->toIso8601String(),
        ];
    }
}
```

O campo `details` preserva a leitura de "Detalhes" que a tela do legado
mostrava; `properties` devolve o JSON completo para telas mais ricas.

---

## 8. Views Blade modernas

| Tela legada | v7 |
|---|---|
| `admin/activities-logs/index.blade.php` | `<x-audit-log-table>` (Livewire) com filtros de período, autor e ação, paginação server-side e botão de exportação |
| Coluna "Detalhes" (texto concatenado) | componente `<x-audit-log-details :properties="..." />` com lista chave/valor expansível |
| Ausência de detalhe por registro | rota `/cms/logs/{id}` ou modal com o registro completo (inclui `subject_id` real) |

Sugestão de implementação com **Livewire 3**:

```php
final class AuditLogTable extends Component
{
    public ?string $from = null;
    public ?string $to = null;
    public ?int $causerId = null;
    public ?string $action = null;
    public string $search = '';

    public function render(): View
    {
        return view('livewire.audit-log-table', [
            'logs' => ActivityLog::query()
                ->when($this->from, fn ($q) => $q->where('created_at', '>=', $this->from))
                ->when($this->to,   fn ($q) => $q->where('created_at', '<=', $this->to))
                ->when($this->causerId, fn ($q) => $q->where('causer_id', $this->causerId))
                ->when($this->action, fn ($q) => $q->where('action', $this->action))
                ->when($this->search, fn ($q) => $q->where('description', 'ilike', "%{$this->search}%"))
                ->latest('created_at')
                ->paginate(50),
        ]);
    }
}
```

---

## 9. Fila, cache e storage

| Recurso | Proposta v7 |
|---|---|
| Fila | A gravação do log pode ser **síncrona** (o legado é síncrono e isso é desejável para auditoria). Se o volume crescer, usar `ShouldQueue` com uma fila dedicada. |
| Cache | Nenhum. Não cachear a trilha. |
| Storage | Nenhum. |
| Purga | Agendar `activitylog:clean` (ou um comando próprio `logs:prune`) no `schedule()` do Laravel 11 (`routes/console.php` ou `bootstrap/app.php`). |
| Particionamento | Para volume alto, partir `activity_logs` por mês (`PARTITION BY RANGE (created_at)`) ou arquivar para `storage/app/auditoria/`. |
| Configuração | `config/logging.php` com canal `audit` próprio, para não misturar com o log da aplicação. |

---

## 10. Riscos e decisões de equivalência funcional

| Risco | Impacto | Mitigação |
|---|---|---|
| Trocar os textos de `description` | relatórios externos que citam "Listou usuários" quebram | manter os textos ou criar um mapa `legacy => novo` |
| Migrar de `properties` (text) para `jsonb` | linhas com JSON inválido quebram a migration | validar antes; `properties IS NULL` vira `'{}'` |
| Mover a FK `resources.modulo_id` do id 32 para o 7 | se o id errado sobreviver, o menu continua oculto | teste automatizado que verifica `$modulos['logs-do-sistema'] === true` |
| Trocar `activity_log` por `activity_logs` | consultas externas (BI, scripts) quebram | criar uma **view** de compatibilidade `activity_log` com o nome antigo |
| Adicionar `action` (enum) | registros antigos não têm o valor | usar `'legacy'` no backfill |
| Deixar de gravar `causer` em toda ação | lacuna de auditoria | o `AuditLogger` sempre preenche `causer` a partir de `auth()->user()` |
| Apagar `update_logs` junto com `activity_log` | o rodapé do CMS perde a versão | manter `update_logs` (ou `system_versions`) separado |
