# Novidades — Migração para Laravel Moderno

> Plano de reconstrução do changelog em **PHP 8.x + Laravel 11/12**.

---

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

| Item | Regra |
|---|---|
| Conteúdo de cada versão | título, número da versão, data de publicação, ícone e descrição em HTML rico |
| Timeline | ordem decrescente, um bloco por versão |
| Modal de detalhe | a descrição completa é aberta sem sair da listagem |
| Número da versão como texto | aceitar "5.0", "6.0.1", "6.0-beta" |
| Seletor de ícones | o operador escolhe um ícone de uma lista |
| Versão no rodapé | o rodapé do CMS deve continuar mostrando a versão e linkando para o changelog |

---

## 2. Schema de migração Laravel

### 2.1 Opção recomendada: unificar `change_logs` e `update_logs`

```php
Schema::create('release_notes', function (Blueprint $table) {
    $table->id();
    $table->string('version', 20)->unique();      // "6.0.0"
    $table->string('title');
    $table->string('icon')->default('fas fa-code-branch');
    $table->timestamp('released_at')->index();
    $table->text('body')->nullable();              // HTML sanitizado
    $table->boolean('is_current')->default(false); // marca a versão do rodapé
    $table->foreignId('author_id')->nullable()->constrained('users')->nullOnDelete();
    $table->timestamps();
    $table->softDeletes();
});
```

Com isso:
- o rodapé passa a ler `release_notes.where('is_current', true)->first()`;
- o changelog passa a ler `release_notes.orderBy('released_at','desc')`;
- o comando `update:logs` passa a **criar/fechar** a release note
  (`is_current`) em vez de inserir numa tabela paralela.

### 2.2 Opção mínima: manter as duas tabelas

```php
Schema::create('change_logs', function (Blueprint $table) {
    $table->id();
    $table->string('version', 20)->index();
    $table->string('title');
    $table->string('icon');
    $table->timestamp('released_at');
    $table->text('body')->nullable();
    $table->timestamps();
    $table->softDeletes();
});
```

### 2.3 Migração dos dados

```php
DB::table('change_logs')->orderBy('id')->get()->each(function ($row) {
    ReleaseNote::updateOrCreate(
        ['version' => $row->numero_versao],
        [
            'title'       => $row->title,
            'icon'        => $row->icon,
            'released_at' => $row->data_versao,
            'body'        => $row->descricao,
            'created_at'  => $row->created_at,
            'updated_at'  => $row->updated_at,
        ]
    );
});
```

> `numero_versao` pode conter "5.0" em vários registros (o seed usa 5.0
> para 20+ versões). Com `unique`, o `updateOrCreate` **sobrescreveria**
> linhas. Usar `firstOrNew` por `title` + `released_at`, ou remover a
> unicidade no primeiro passo e aplicá-la depois da revisão manual.

---

## 3. Modelos

```php
// app/Models/ReleaseNote.php
final class ReleaseNote extends Model
{
    use HasFactory, SoftDeletes;

    protected $fillable = ['version', 'title', 'icon', 'released_at', 'body', 'is_current', 'author_id'];

    protected $casts = [
        'released_at' => 'datetime',
        'is_current'  => 'boolean',
    ];

    public function scopeCurrent(Builder $q): Builder
    {
        return $q->where('is_current', true);
    }

    public function scopePublished(Builder $q): Builder
    {
        return $q->orderByDesc('released_at');
    }

    public function author(): BelongsTo { return $this->belongsTo(User::class, 'author_id'); }
}
```

> `SoftDeletes` passa a ser usado de fato (a coluna `deleted_at` já
> existe em `change_logs`).

### 3.1 Enum de ícones

```php
enum ChangelogIcon: string
{
    case CodeBranch = 'fas fa-code-branch';
    case Calendar   = 'far fa-calendar-check';
    case FileAlt    = 'far fa-file-alt';
    case Bullhorn   = 'far fa-bullhorn';
    case CheckCircle= 'fa fa-check-circle';
    case Shield     = 'fas fa-lock';
}
```

A v7 deve validar o ícone contra essa lista (ou contra as classes
disponíveis no `fontawesome-iconpicker`), eliminando a entrada de texto
livre.

---

## 4. Form Requests

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

    public function rules(): array
    {
        return [
            'version'     => ['required', 'string', 'max:20', 'regex:/^\d+(\.\d+)*(-[a-z0-9.]+)?$/i'],
            'title'       => ['required', 'string', 'min:3', 'max:120'],
            'icon'        => ['required', 'string', Rule::enum(ChangelogIcon::class)],
            'released_at' => ['required', 'date', 'after_or_equal:1970-01-01'],
            'body'        => ['required', 'string', 'min:3'],
        ];
    }

    protected function prepareForValidation(): void
    {
        if ($this->filled('body')) {
            $this->merge(['body' => Purifier::clean($this->input('body'))]);
        }
    }
}
```

O `UpdateReleaseNoteRequest` herda as regras, com
`Rule::unique('release_notes','version')->ignore($this->route('releaseNote'))`.

> A sanitização do HTML (`mews/purifier`) é **obrigatória**: o HTML é
> injetado no modal via `innerHTML`.

---

## 5. Policies

```php
final class ReleaseNotePolicy
{
    public function viewAny(User $u): bool   { return $u->hasCapability('changelog.index'); }
    public function create(User $u): bool    { return $u->hasCapability('changelog.create'); }
    public function view(User $u, ReleaseNote $n): bool { return $u->hasCapability('changelog.show'); }
    public function update(User $u, ReleaseNote $n): bool { return $u->hasCapability('changelog.edit'); }
    public function delete(User $u, ReleaseNote $n): bool { return $u->hasCapability('changelog.delete'); }
}
```

E o recurso correspondente, que **deve** ser criado no catálogo:

| recurso | ação | descrição |
|---|---|---|
| `changelog` | `index` | Listar novidades do sistema |
| `changelog` | `create` | Cadastrar novidade |
| `changelog` | `edit` | Editar novidade |
| `changelog` | `delete` | Excluir novidade |
| `changelog` | `show` | Ver novidade |

> O slug **`changelog`** deve ser mantido: é o que a view do legado já
> usa (`has_access(['changelog.index'])`).

---

## 6. Endpoints REST/JSON sugeridos

| Método | URI | Descrição | Autorização |
|---|---|---|---|
| GET | `/api/release-notes` | lista paginada, ordem por `released_at desc` | `changelog.index` |
| POST | `/api/release-notes` | cria | `changelog.create` |
| GET | `/api/release-notes/{note}` | detalhe com o corpo completo | `changelog.show` |
| PUT | `/api/release-notes/{note}` | atualiza | `changelog.edit` |
| DELETE | `/api/release-notes/{note}` | exclui (soft) | `changelog.delete` |
| GET | `/api/release-notes/current` | release note marcada como atual (rodapé) | público (interno) |
| GET | `/api/changelog-icons` | catálogo de ícones permitidos | `changelog.index` |

---

## 7. API Resources

```php
final class ReleaseNoteResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id'          => $this->id,
            'version'     => $this->version,
            'title'       => $this->title,
            'icon'        => $this->icon,
            'released_at' => $this->released_at?->toIso8601String(),
            'summary'     => Str::limit(strip_tags((string) $this->body), 250),
            'body'        => $this->body,
            'is_current'  => $this->is_current,
        ];
    }
}
```

O campo `summary` reproduz o `str_limit(strip_tags(...), 250)` do
legado; `body` traz o HTML completo (já sanitizado).

---

## 8. Views Blade modernas

| Tela legada | v7 |
|---|---|
| `changelog/index.blade.php` (timeline Insignia) | `<x-changelog-timeline :notes="..." />` com Alpine.js |
| `changelog/modal.blade.php` + `changelog-modal.js` | `<x-changelog-modal>` Alpine (`x-data`, `x-show`), sem `innerHTML` manual |
| `changelog/form.blade.php` | `<x-release-note-form :note="..." />` com o `iconpicker` mantido |
| `changelog/create.blade.php` / `edit.blade.php` | uma única view, controlada pelo componente |

Exemplo de componente Alpine (substitui o JS imperativo):

```blade
<div x-data="{ open: false, note: null }">
    @foreach ($notes as $note)
        <button @click="open = true; note = {{ Js::from($note) }}">
            {{ Str::limit($note->title, 50) }}
        </button>
    @endforeach

    <div x-show="open" x-cloak class="modal">
        <h4 x-text="note?.title"></h4>
        <div x-html="note?.body"></div>   {{-- body já sanitizado no servidor --}}
    </div>
</div>
```

> `x-html` continua sendo necessário para o HTML rico; a segurança
> depende da sanitização no servidor (FormRequest), não do front.

### 8.1 Componente de input

O `components/inputs/*` do legado pode ser mantido ou substituído por
componentes Blade próprios:

```blade
<x-input name="title" label="Título" required maxlength="120" />
<x-input name="version" label="Número da Versão" required pattern="^\d+(\.\d+)*$" />
<x-date-time name="released_at" label="Data de publicação" />
<x-icon-picker name="icon" label="Ícone" :options="ChangelogIcon::values()" />
<x-rich-text name="body" label="Descrição" />
```

---

## 9. Fila, cache e storage

| Recurso | Proposta v7 |
|---|---|
| Fila | Nenhuma (operação síncrona e barata) |
| Cache | Cachear a release note "atual" (`Cache::rememberForever('release.current')`), invalidado no `update` |
| Storage | As imagens do changelog (`/storage/img/changelog/*.png`) devem ser migradas para o disk `public` com `Storage::disk('public')` |
| Sessão | Não utilizada |

> O seed referencia imagens por caminho **absoluto** (`/storage/img/changelog/*.png`)
> dentro do HTML do corpo. Na v7, o ideal é migrar essas imagens para
> `Storage` e referenciá-las por URL do disk.

---

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

| Risco | Impacto | Mitigação |
|---|---|---|
| Unificar as duas tabelas | o rodapé e a timeline passam a ler a mesma fonte | testar o rodapé (`$systemVersion->version`) |
| `unique` em `version` | o seed tem 20+ entradas com `5.0` | revisar o seed antes de aplicar a unicidade; considerar `unique(version, released_at)` |
| Trocar `str_limit` por `Str::limit` | o corte pode diferir em 1 caractere (o legado usava `mb_strlimit`) | comportamento equivalente esperado |
| Sanitizar o HTML | descrições antigas podem ser alteradas | re-sanitizar no backfill e revisar manualmente as 28 versões do seed |
| Remover a rota `changelog.show` | links externos quebram | manter um redirect para a âncora da timeline |
| Criar o recurso `changelog` | perfis passam a poder receber a permissão | comunicação; o módulo deixa de ser exclusivo do super-admin |
| Trocar `float` por `string` na versão | já é o estado atual | — |
| `SoftDeletes` | a exclusão deixa de ser definitiva | é uma **mudança** de comportamento: avisar o cliente |
