feat: implement CMS for content management (WEB-02/03/04/06)

Introduces Filament management for site settings, services, portfolio cases, and testimonials with admin-only policies, validated media uploads, and deterministic seeding. Includes comprehensive design and task documentation for the implementation.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-29 06:18:42 -03:00
parent 7a70b44931
commit 7a858b52af
29 changed files with 1214 additions and 0 deletions

View File

@@ -0,0 +1,97 @@
## Context
Fase 0 entregou Laravel 13, Filament 5 em `/admin`, design tokens, autenticação interna e pipeline CI. O repositório possui apenas home placeholder em [routes/web.php](../../routes/web.php). Esta change implementa a **metade CMS** da Fase 1 (SPEC §18): persistência e gestão Filament de conteúdo, sem rotas públicas.
Padrões existentes a seguir:
- Resources Filament com form/table separados ([app/Filament/Resources/Users/](../../app/Filament/Resources/Users/))
- Policies admin-only ([app/Policies/UserPolicy.php](../../app/Policies/UserPolicy.php))
- `declare(strict_types=1);` em todo PHP próprio
## Goals / Non-Goals
**Goals:**
- Assessora admin gerencia settings, serviços, portfólio e depoimentos no Filament.
- Dados persistidos conforme SPEC §8.2, prontos para consumo pela change `build-public-site`.
- Feature tests locais em PostgreSQL (jsonb, constraints reais).
- Seed determinístico parcial para desenvolvimento e testes futuros.
**Non-Goals:**
- Rotas públicas, home, SEO, sitemap, snapshots visuais, briefing.
- Actions de publicação com auditoria (`PublishPortfolioCase` etc.) — adiadas até Fase 5.
- Variantes responsivas de imagem, page builder, categorias dinâmicas.
## Decisions
### 1. Publicação via `published_at` único
**Decisão:** `published_at` nullable timestamp é a única fonte de verdade. Scope `published()` no model (`whereNotNull('published_at')`). Sem coluna `is_published`.
**Alternativa rejeitada:** boolean `is_published` + `published_at` — viola SPEC §1.1 (não persistir status derivável).
### 2. Mídia em disco `public` local, S3 em produção
**Decisão:** uploads de capa/galeria/foto usam disco Laravel configurável via `FILESYSTEM_DISK`. Local: `public` + `php artisan storage:link`. Produção: `s3`. Path gerado com UUID/hash, nunca nome original.
**Alternativa rejeitada:** blobs no PostgreSQL — proibido por SPEC §6.4.
### 3. Validação de mídia centralizada
**Decisão:** classe em `app/Support/` (ex.: `PublicImageUploadRules`) com allowlist MIME (`image/jpeg`, `image/png`, `image/webp`), extensões correspondentes, limite 10 MB, alt text obrigatório quando imagem presente. Reutilizada nos Resources Filament.
### 4. Sem Actions de domínio nesta fase
**Decisão:** Filament Resources salvam diretamente via Eloquent. Actions (`PublishPortfolioCase`, `UnpublishPortfolioCase`) só serão criadas quando auditoria (Fase 5) exigir efeito colateral transacional.
**Rationale:** SPEC §9.5 — não criar Action para CRUD sem regra adicional.
### 5. Auditoria de publicação adiada
**Decisão:** publicar/despublicar **não** grava `audit_logs` nesta change.
**Desvio documentado:** ADM-02 lista publicação como auditável, mas tabela `audit_logs` é Fase 5. Retomar em change de hardening.
### 6. Autorização admin-only para CMS
**Decisão:** Policies espelham `UserPolicy``$user->isAdmin()` para view/create/update/delete em conteúdo do site. Assistant recebe 403 em todos os Resources de conteúdo.
**Referência:** WEB-02 — "admin pode gerenciar; assistant não pode".
### 7. Site settings como singleton Filament Page
**Decisão:** model `SiteSetting` com método `instance()` (primeiro registro ou create default). Filament `ManageSiteSettings` page (não Resource de lista) — evita CRUD genérico de chave/valor (SPEC §WEB-06).
### 8. Slug automático a partir do título
**Decisão:** slug gerado no model (observer ou mutator) com `Str::slug()`, unique constraint no banco. Admin pode editar slug no form.
### 9. Navegação Filament
**Decisão:** grupo `Conteúdo do site` com sort order conforme SPEC §5.2:
1. Configurações (singleton page)
2. Serviços
3. Portfólio
4. Depoimentos
## Risks / Trade-offs
| Risco | Mitigação |
|---|---|
| Uploads locais sem S3 em dev | `Storage::fake()` nos testes; documentar `storage:link` no README |
| Galeria ordenada complexa no Filament | Relation manager com `sort_order` e reorderable |
| jsonb `social_links` difere SQLite | phpunit.xml já migrado para PostgreSQL |
| Conteúdo criado sem rotas públicas | seed + testes Filament validam CMS; rotas vêm em `build-public-site` |
| Auditoria ausente em publicação | desvio explícito; retomar Fase 5 |
## Migration Plan
1. Rodar migrations em ordem: `site_settings``services``portfolio_cases` + `portfolio_images``testimonials`.
2. Executar seed de conteúdo após migrations.
3. Sem rollback de dados em produção (greenfield); `migrate:rollback` suportado em dev.
## Open Questions
- Nenhuma bloqueante. Fontes tipográficas finais da home ficam para `build-public-site`.