# Logs do Sistema — Rotas e Controllers

> Rotas do CMS. O módulo não expõe API pública.

---

## 1. Rotas

### 1.1 Tela de logs

| Método | URI | Nome | Controller@action | Middleware | Linha |
|---|---|---|---|---|---|
| GET | `/cms/log` | `log.index` | `Admin\ActivityLogController@index` | `web` + `check.logged` | `v6/routes/basepro.php:71` |
| GET | `/cms/log/create` | `log.create` | `Admin\ActivityLogController@create` (**inexistente**) | idem | 71 |
| POST | `/cms/log` | `log.store` | `Admin\ActivityLogController@store` (**inexistente**) | idem | 71 |
| GET | `/cms/log/{log}` | `log.show` | `Admin\ActivityLogController@show` (**inexistente**) | idem | 71 |
| GET | `/cms/log/{log}/edit` | `log.edit` | `Admin\ActivityLogController@edit` (**inexistente**) | idem | 71 |
| PUT/PATCH | `/cms/log/{log}` | `log.update` | `Admin\ActivityLogController@update` (**inexistente**) | idem | 71 |
| DELETE | `/cms/log/{log}` | `log.destroy` | `Admin\ActivityLogController@destroy` (**inexistente**) | idem | 71 |

> A rota é gerada por `Route::resource('log', 'Admin\ActivityLogController')`
> (`v6/routes/basepro.php:71`), mas o controller implementa **apenas**
> `index()`. As outras seis actions resultam em erro de método inexistente
> (HTTP 500). Ver `08-GAPS-E-DIVIDAS.md`.

### 1.2 Endpoint AJAX da listagem

| Método | URI | Nome | Controller@action | Middleware | Linha |
|---|---|---|---|---|---|
| GET | `/cms/datatable/logs` | `logs` | `Prefecture\DatatableController@get_logs` | `web` | `v6/routes/basepro.php:404` |

O grupo `datatable` está dentro do grupo `cms` e **não** tem
`check.logged`; a proteção efetiva vem do fato de a tela exigir sessão.
O nome da rota (`logs`) está na lista de rotas "abertas" do
`check.route.permission` (a entrada `'datatable'` do array `$openRoutes`
cobre o segmento do nome).

### 1.3 Rotas do registro de módulos (dentro do mesmo slug duplicado)

| Método | URI | Nome | Controller@action | Middleware | Linha |
|---|---|---|---|---|---|
| GET | `/cms/modulos` | `modulos.index` | `Admin\ModulosController@index` | `web` + `check.logged` | `v6/routes/basepro.php:40` |
| POST | `/cms/modulos` | `modulos.update` | `Admin\ModulosController@update` | `web` + `check.logged` | `v6/routes/basepro.php:41` |

> A tela `/cms/modulos` é onde a duplicidade fica **visível**: duas linhas
> com o mesmo rótulo "Logs do Sistema" e o mesmo `id` HTML
> (`id="{{ $modulo->slug }}"`, em `v6/resources/views/admin/modulos/form.blade.php:40`).

---

## 2. Controllers

### 2.1 `Admin\ActivityLogController`

`v6/app/Http/Controllers/Admin/ActivityLogController.php` (completo):

```php
namespace App\Http\Controllers\Admin;

use App\Admin\ActivityLog;
use Illuminate\Http\Request;
use Sentinel;

class ActivityLogController extends Controller
{
    public function index()
    {
        $title = "Logs";
        return view('admin.activities-logs.index', compact('title'));
    }
}
```

Observações:

- **Nenhuma** checagem de permissão no corpo. A única proteção é o
  middleware `check.logged` da rota.
- O `use App\Admin\ActivityLog;` e o `use Sentinel;` são **imports não
  utilizados**.
- A classe estende `App\Http\Controllers\Admin\Controller`, que **não**
  registra nenhum middleware (ao contrário de `Prefecture\Controller`).

### 2.2 `Prefecture\DatatableController@get_logs` e `struct_logs`

`v6/app/Http/Controllers/Prefecture/DatatableController.php:523-527`:

```php
public function get_logs()
{
    $logs = ActivityLog::orderBy('created_at', 'desc')->orderBy('created_at')->limit(5000)->get();
    return DataTables::of($this->struct_logs($logs))->make(true);
}
```

`v6/app/Http/Controllers/Prefecture/DatatableController.php:529-556`:

```php
public function struct_logs($data)
{
    $datatable = [];
    $id = 1;
    foreach ($data as $value) {
        $obj = json_decode($value->properties, true);
        $result = '';
        foreach ($obj as $chave => $value2) {
            if (is_array($value2)) {
                $value2 = json_encode($value2);
            }
            $result .= $chave . ': ' . $value2 . ' | ';
        }
        array_push($datatable, [
            'value'     => $id++,
            'data'      => date('d/m/Y H:i:s', strtotime($value->created_at)),
            'descricao' => $value->description,
            'detalhes'  => $result,
            'ip'        => $value->ip_address ?? '',
            'navegador' => $value->browser ?? '',
        ]);
    }
    return $datatable;
}
```

Pontos técnicos:

- `orderBy('created_at','desc')->orderBy('created_at')` — o segundo
  `orderBy` é **redundante** e sobrescreve a direção do primeiro no SQL
  final (o segundo call sem direção reseta para `asc`). O resultado
  efetivo é ordenação **crescente** por `created_at`, com o `LIMIT 5000`
  aplicado sobre os **mais antigos**, não os mais recentes. Isso é
  exatamente o oposto do que o nome do método sugere.
- `json_decode($value->properties, true)` pode devolver `null` se
  `properties` for `null` → `foreach (null)` gera warning em PHP 8.
- Todas as linhas são carregadas em memória (até 5000) e depois
  reprocessadas em PHP — o `DataTables::of()` recebe um array já pronto,
  sem paginação server-side.
- A coluna `value` é um **índice sequencial local** (1..N), não o `id` do
  registro. O `id` real nunca chega ao front-end.

---

## 3. Observer e gravação

### 3.1 `ActivityObserver`

`v6/app/Observers/ActivityObserver.php`, registrado em
`v6/app/Providers/AppServiceProvider.php:22`
(`Activity::observe(ActivityObserver::class)`).

```php
public function creating(Activity $activity)
{
    $activity->ip_address = Request::ip();
    $user_agent = Request::header('User-Agent');
    $cleanedUserAgent = preg_replace([
        '/Mozilla\/5\.0\s*/',
        '/Safari\/537\.36\s*/',
        '/Chrome\/130\.0\.0\.0\s*/',
        '/AppleWebKit\/537\.36\s*\(KHTML, like Gecko\)\s*/'
    ], '', $user_agent);
    ...
    $activity->browser = $cleanedUserAgent;
}
```

Observações:

- O regex remove um **Chrome/130.0.0.0** fixo — versões diferentes do
  Chrome deixam o número da versão no campo `browser`.
- O tratamento de Opera/Edge é feito em dois passos: primeiro detecta
  `OPR`/`Edg` para remover a string do Chrome e depois renomeia para
  "Opera x.y.z.w" / "Edge x.y.z.w".
- `Request::ip()` respeita `TrustProxies`; com proxy configurado
  incorretamente o IP gravado é o do proxy.

### 3.2 Como a gravação é disparada

Não há evento de domínio: **cada controller** chama `activity()` manualmente.
Exemplos:

| Controller | Linha | `description` |
|---|---|---|
| `Admin\UserController@index` | 42 | `Listou usuários` |
| `Admin\UserController@create` | 75 | `Acesso cadastro de usuário` |
| `Admin\UserController@store` | 137 | `Cadastro de usuário` |
| `Admin\UserController@edit` | 208 | `Acesso edição de usuário` |
| `Admin\UserController@update` | 280 | `Edição de usuário` |
| `Admin\UserController@inactivate` | 327 | `Inativou usuário` |
| `Admin\UserController@restore` | 445 | `Restaurou usuário` |
| `Admin\UserController@set_status` | 510 / 523 | `Restaurou usuário` / `Inativou usuário` |
| `Admin\RoleController@index` | 33 | `Listou Roles` |
| `Admin\RoleController@create` | 48 | `Acesso cadastro de roles` |
| `Admin\RoleController@store` | 83 | `Cadastro de roles` |
| `Admin\RoleController@edit` | 119 | `Acesso edição de perfil` |
| `Admin\RoleController@update` | 161 | `Edição de perfil` |
| `Admin\RoleController@destroy` | 184 | `Exclusão de permissão` (texto errado) |
| `Admin\PermissionController@index` | 39 | `Listou Permissões` |
| `Admin\PermissionController@create` | 61 | `Acesso cadastro de permissão` |
| `Admin\PermissionController@store` | 84 | `Cadastro de permissão` |
| `Admin\PermissionController@edit` | 127 | `Acesso edição de permissão` |
| `Admin\PermissionController@update` | 154 | `Edição de permissão` |
| `Admin\PermissionController@destroy` | 181 | `Exclusão de permissão` |
| `Admin\LoginController@postLogin` | 35 | `Login no sistema` |
| `Admin\ResetPasswordController@showResetForm` | 48 | `Tentou resetar senha` |
| `Prefecture\SupportController@enviarEmail` | 116 | `Página de suporte` (**sem `causedBy`**) |
| `Prefecture\SupportController@enviarEmail` (catch) | 122 | `Erro página de suporte` |

> O model `App\Admin\User` tem o trait `LogsActivity` **comentado**
> (`v6/app/Http/Model/Admin/User.php:43`), com o comentário
> "Serve para registrar os dados automaticamente no log". A auditoria
> automática por observer de model **não** está ativa.

---

## 4. Permissões exigidas

| Recurso | Permissão | Situação |
|---|---|---|
| Acesso à tela `/cms/log` | `check.logged` (middleware) | suficiente; **não** há checagem de permissão |
| Item de menu "Logs do sistema" | `registro-de-atividades.index` | **slug inexistente** em `resources` |
| Item de menu "Logs do sistema" | `$modulos['logs-do-sistema']` | sempre `false` (duplicidade) |
| Linha na matriz de permissões | `resources.modulo_id = 32` (módulo desativado) | oculta |

**Nenhum papel do CMS tem permissão de log**: não há como restringir a
leitura da trilha de auditoria a um grupo específico. O acesso é liberado
a qualquer usuário autenticado.

---

## 5. Form Requests e validações

**Nenhum.** Nem `ActivityLogController` nem `DatatableController@get_logs`
validam entrada. O endpoint AJAX aceita apenas `GET` sem parâmetros.

---

## 6. API Resources / Presenters

Nenhum. A serialização é feita à mão em `struct_logs()`, que produz um
array associativo com as chaves `value`, `data`, `descricao`, `detalhes`,
`ip`, `navegador` — consumidas pelo JS.

---

## 7. Rotas do portal público

Nenhuma. A trilha de auditoria é **exclusivamente interna**: não há
exposição pública de `activity_log`. O portal público mostra apenas a
versão do sistema (`update_logs`), via `FooterServiceProvider`.
