* feat: aplicar identidade Heritage Editorial ao painel Filament
O painel admin ainda usava os defaults de fábrica do Filament (Amber,
Zinc, Inter Variable, dark mode ligado) enquanto o site público já
seguia o design system Heritage Editorial há várias entregas — a
inconsistência ficava evidente para quem navegava entre as duas áreas
e não havia nenhum teste travando a configuração do painel.
- Cores primary/gray/danger/warning/success viram arrays explícitos de
11 tons (não Color::hex()/string), a única forma de preservar os
hexadecimais exatos do DESIGN.md — Color::hex() decompõe a cor e
remonta lightness/chroma por uma tabela fixa, perdendo a cor real.
Os tons foram escolhidos verificando empiricamente com
ButtonComponentColorMap qual shade o botão sólido realmente usa
(600/500/50), e o tom 50 do primary é um verde-oliva claro (não
branco puro) porque BadgeComponent usa bg-color-50 diretamente e um
branco puro tornaria o badge quase invisível sobre o Papel Marfim.
- EB Garamond self-hosted via LocalFontProvider (sans/mono/serif), sem
reintroduzir uma requisição externa ao fonts.bunny.net — o hook
HEAD_END reaproveita o mesmo <x-fonts /> do site público.
- Novo tema Vite (resources/css/filament/admin/theme.css) zera radius
e shadow só no escopo do Filament, incluindo a variável --radius
"bare" (usada em ~227 regras do próprio Filament) e um override
manual para o CSS pré-compilado do tooltip Tippy.js, que não é
alcançado pelo @theme.
- Dark mode desativado: o Heritage Editorial é uma paleta única.
- Teste de regressão (AdminPanelBrandParityTest) pinando cores, fontes
e configuração do tema, incluindo uma renderização real de
/admin/login — foi essa renderização que pegou um bug real: passar
a família já entre aspas simples quebrava a declaração CSS
(--font-family: ''EB Garamond''), silenciosamente caindo para
ui-sans-serif.
Bundle público verificado byte a byte: nenhuma declaração CSS
existente mudou de valor (apenas classes novas e não usadas pelo site
público foram adicionadas ao app.css, um efeito colateral inerte de
ter uma segunda entrada Tailwind no mesmo build do Vite).
Co-Authored-By: Claude noreply@anthropic.com
AI-Assisted: yes
AI-Tool: claude-code
* fix(build): copiar CSS do Filament no estágio frontend e neutralizar sombras
Dois defeitos encontrados ao revisar a paridade visual do painel.
O primeiro impedia o build da imagem. O `resources/css/filament/admin/
theme.css` importa o CSS não compilado do próprio Filament, e o estágio
`frontend` do Dockerfile copia apenas `package.json`, `vite.config.js`,
`resources` e `public` — nunca `vendor`. O `npm run build` falhava na
resolução do import, o que derrubaria os jobs `container` e `browser` do
CI. Agora a subárvore `vendor/filament` é copiada do estágio do composer,
em vez de todo o `vendor`, para manter o contexto pequeno.
O segundo é silencioso e mais interessante. O plugin do Tailwind compartilha
um único contexto entre todas as entradas do build, então o `@source
app/Filament/**` da entrada do painel torna visível o uso de `shadow-*`
do Filament e o Tailwind passa a emitir seus valores padrão de
`--shadow-sm/md/lg` também no bundle público. Verificado por diff do
`app.css` construído com e sem a entrada do tema.
Nada no site público usa utilitário de sombra hoje, então a renderização
não muda e os baselines visuais seguem válidos. Mas deixar valores reais
de sombra definidos no CSS que vai para produção permitiria que um
`shadow-sm` futuro em elemento público violasse a Tonal Layer Rule do
DESIGN.md em silêncio — e o HeritageEditorialTokensTest só inspeciona
arquivos de origem, então não pegaria.
Os tokens de sombra passam a ser fixados em transparente no `@theme` do
`app.css`, com teste que falha se algum deixar de ser. A regra continua
verdadeira no artefato que realmente ship.
Co-Authored-By: Claude noreply@anthropic.com
AI-Assisted: yes
AI-Tool: claude-code
---------
Co-authored-by: manoel.neto <manoel.neto@creditas.com>
* feat: gates de qualidade da Fase 0 (auth, npm audit, cobertura, compose local)
Implementa os itens pendentes de SPEC.md §18 mapeados pela mudança OpenSpec
complete-foundation-parity (grupos 1, 2 e 3 de tasks.md):
- MustVerifyEmail em User, com canAccessPanel exigindo is_active E
hasVerifiedEmail(). Seeds de admin/assistant local passam a marcar
email_verified_at. Verificação de e-mail permanece administrada apenas
pelo admin (seed/edição) — não há rota de auto-verificação registrada
(->emailVerification() não foi chamado) — decisão deliberada, já que
usuários são criados pelo admin, não se autocadastram.
- Reset de senha seguro: página customizada
App\Filament\Pages\Auth\RequestPasswordReset substitui a página padrão do
Filament, que vaza a existência de contas via notificação de erro
distinguível em dois casos — Password::INVALID_USER (e-mail inexistente)
e Password::RESET_THROTTLED (só ocorre para usuário existente, quando um
token foi criado há pouco). Ambos os casos agora respondem com a mesma
notificação de sucesso.
- declare(strict_types=1) adicionado aos 15 arquivos de app/ que ainda não
tinham: AdminPanelProvider, Controller e 13 páginas de Filament Resources.
- npm audit adicionado ao script `quality` do composer e ao job `static` do
CI, com --audit-level=high (documentado inline no workflow). O gate é hoje
vazio na prática — package.json não tem `dependencies` de runtime — mas
protege contra regressões futuras.
- Gate de cobertura Domain/Application >= 80% no job `unit` do CI. Escopo
via phpunit.coverage.xml dedicado (o phpunit.xml global continua cobrindo
todo app/ para os demais usos). O job `unit` ganhou serviço de postgres e
passou a rodar Unit+Architecture+Feature juntas, porque as classes de
App\Application\Queries\Marketing só são exercitadas por testes Feature
hoje — um gate baseado só em Unit ficaria bem abaixo de 80%. Medido
localmente com pcov: 98,1% (nenhum teste novo foi necessário para atingir
o limiar).
- Serviço `app` (FrankenPHP) no docker-compose.yml para paridade local com
staging/produção, dependente de postgres saudável, com DB_HOST/DB_PORT
sobrescritos para resolver o serviço postgres pelo nome (o padrão
127.0.0.1 do .env só funciona para processos no host). Validado com um
smoke isolado (projeto/portas dedicados via overlay `!override`, sem
tocar o container amare-postgres compartilhado por outra worktree):
depends_on/healthcheck funcionou, `php artisan migrate` rodou dentro do
container via DB_HOST=postgres, e /up respondeu 200. README atualizado
(PHP 8.4 canônico, composer.json mantém ^8.3 por design).
Fora do escopo: deploy hello-world em staging (Dokploy com falha, item de
infra não relacionado a este change) — L2353 e o critério de saída da Fase
0 continuam sem marcar. Também ficam pendentes, por dependerem de uma
execução real de CI/PR (task 3.4, 6.1, 6.2 do OpenSpec): prova de que o CI
falha com quebra intencional de audit/cobertura, e evidência de deploy/
rollback em staging.
Co-Authored-By: Claude noreply@anthropic.com
AI-Assisted: yes
AI-Tool: claude-code
* fix: fecha lockout de admin-criado e documenta pcov/compose no auth de usuários
Correção de acompanhamento ao commit anterior (gates de qualidade da Fase 0),
achada por revisão adicional após o primeiro commit:
- CreateUser::handleRecordCreation agora define email_verified_at ao criar um
usuário pelo painel. Sem isso, UserForm não expõe esse campo (nem está no
#[Fillable] de User — de propósito, para nunca virar mass-assignable via
formulário), então todo usuário criado pelo admin nascia com
email_verified_at nulo e, com canAccessPanel() agora exigindo e-mail
verificado, ficava trancado para sempre — sem rota de auto-verificação e
sem recuperação via reset de senha (o callback do Filament pula o envio
para quem falha canAccessPanel(), mas ainda reporta sucesso). Usa
forceFill() (não atribuição direta, que o PHPStan rejeita pelo tipo
Carbon/string do cast) para contornar o guard de mass assignment só nesse
ponto. Regressão coberta por teste que cria um usuário via
Livewire::test(CreateUser::class) — não pela factory, que não passa pelo
mesmo caminho — e confirma login em seguida; teste falha sem a correção
(verificado manualmente revertendo e rodando de novo).
- Confirmado que DatabaseSeeder::run() (commit anterior) já funciona sem
ajuste: User::query()->updateOrCreate() pareceria sofrer o mesmo guard de
#[Fillable], mas `php artisan db:seed` executa dentro de
Model::unguarded() (Illuminate\Database\Console\Seeds\SeedCommand), o que
levanta o guard durante o seed inteiro. Adicionado teste de regressão que
roda via $this->seed(DatabaseSeeder::class) (o mesmo caminho de
`composer setup`) para travar esse comportamento — chamar a mesma lógica
fora do comando db:seed reproduz o guard bloqueando o campo, confirmando
que a dependência do unguard() é real, não coincidência.
- README: nota de que `docker compose up -d postgres` agora exige o `.env`
do passo 1, porque o serviço `app` referencia `.env` via `env_file` e o
Compose valida o arquivo inteiro antes de subir qualquer serviço.
- AGENTS.md: documentado `composer test:coverage` (usado pelo job `unit` do
CI) e que `docker compose up -d` sem argumento também sobe o serviço `app`
agora, não só o Postgres.
Nota para quem for escrever o próximo teste de recurso Filament neste repo:
Livewire::test(...)->fillForm([...]) é um no-op silencioso aqui —
app()->runningUnitTests() retorna false no setup de testes deste projeto,
então o guard de Filament\Schemas\Concerns\InteractsWithSchemas::
fillFormDataForTesting() nunca aplica o estado, e o teste falha depois com
erros de validação "required" que parecem um bug no formulário, não no
teste. Use ->set('data.campo', valor) (como os testes de Login já fazem)
até isso ser investigado — fora do escopo deste change.
Co-Authored-By: Claude noreply@anthropic.com
AI-Assisted: yes
AI-Tool: claude-code
* fix(auth): verificar usuários existentes ao adotar MustVerifyEmail
O `canAccessPanel` passou a exigir `hasVerifiedEmail()`, mas nenhuma
migration preenchia `email_verified_at` para contas que já existem. Toda
conta criada antes desta mudança tem o campo nulo e ficaria trancada fora
do /admin no instante do deploy.
E não haveria volta pelo aplicativo: nenhuma rota de verificação
self-service é registrada, e o fluxo de reset de senha deliberadamente
não notifica quem falha no `canAccessPanel` — ainda reportando sucesso.
Recuperar exigiria shell no contêiner.
Verificar essas contas é o padrão correto, não um atalho. Não existe
cadastro público: toda conta existente foi criada por um admin, pelo
painel ou pelo snippet de tinker do runbook de deploy. Esse ato é a
verificação — exatamente o raciocínio que o CreateUser aplica às contas
criadas de agora em diante.
A verificação é datada pelo `created_at` da conta, não pelo momento em
que a migration roda, para não inventar um histórico. O `down()` é um
no-op documentado: anular as colunas recriaria justamente a falha que
esta migration existe para evitar, e a divisão original entre nulos e
não-nulos não está registrada em lugar nenhum.
Co-Authored-By: Claude noreply@anthropic.com
AI-Assisted: yes
AI-Tool: claude-code
---------
Co-authored-by: manoel.neto <manoel.neto@creditas.com>
* feat: capturar origem de marketing do briefing de contato (MAN-105)
Adiciona tracking simples da origem (UTM ou referrer, primeiro-toque
vence) para satisfazer WEB-05: a origem viaja apenas com o e-mail
interno do briefing, sem Lead model, tabela ou superfície de CRM
(Fase 2 fora de escopo). Middleware no grupo "web" cobre só as rotas
públicas (o painel Filament tem sua própria stack de middleware);
lógica de captura/sanitização isolada em App\Domain\Contact para não
poluir controller nem view.
Fail-open por construção: qualquer falha ao capturar ou ler a origem é
logada sem dados pessoais e a submissão segue normalmente. Respeita
LGPD (§12.5): nada além da origem é guardado, valores são truncados
para não inflar a sessão, e um referrer apontando para o próprio host
é descartado por não ser sinal de marketing. Valores não confiáveis
(query string, referrer) são sanitizados antes de entrar na sessão e
no e-mail em HTML.
Co-Authored-By: Claude noreply@anthropic.com
AI-Assisted: yes
AI-Tool: claude-code
* fix(contato): capturar apenas a origem do referrer, não a URL completa
O fallback de referrer em MarketingOrigin::capture() armazenava a URL
inteira (incluindo query string e fragmento), não apenas o site de
origem. Um link de campanha de e-mail personalizado (ex.:
?email=...&subscriber_id=...) chegava intacto à sessão (persistida em
banco via SESSION_DRIVER=database) e ao corpo do e-mail de briefing
lido por um humano — indo além do que SPEC.md WEB-05 pede ("origem de
marketing") e do §12.5 ("coletar apenas dados necessários").
Agora o referrer é reduzido a esquema+host antes de sanitizar,
descartando path, query string e userinfo.
Co-Authored-By: Claude noreply@anthropic.com
AI-Assisted: yes
AI-Tool: claude-code
* test(contato): cobrir first-touch quando a navegação seguinte traz UTM concorrente
test_first_touch_wins_when_later_navigation_has_no_utm não exercitava
de fato a guarda "primeira captura vence" em CaptureMarketingOrigin —
as duas navegações seguintes não carregavam nenhum sinal (nem UTM, nem
referrer), então MarketingOrigin::capture() já retornava [] e o
early-return por ausência de sinal (não a guarda de sessão) é quem
impedia a sobrescrita. Confirmado removendo a guarda: a suíte
continuava passando.
Este teste novo usa uma segunda navegação com UTM diferente
(utm_source=facebook), o que só passa se a guarda de sessão estiver
ativa — mutação verificada manualmente antes de escrever a asserção.
Co-Authored-By: Claude noreply@anthropic.com
AI-Assisted: yes
AI-Tool: claude-code
* fix(contato): não capturar origem em sitemap.xml e robots.txt
O middleware estava anexado ao grupo `web` inteiro, que também cobre
`/sitemap.xml` e `/robots.txt`. Um crawler chegando em
`/sitemap.xml?utm_source=...` consumia o slot de primeiro toque com
tráfego que nunca vai enviar um briefing, e ainda iniciava sessão em uma
rota técnica.
As duas rotas passam a ser ignoradas por nome, com teste cobrindo que
elas não gravam nada na sessão e que uma visita humana seguinte ainda é
capturada normalmente.
Co-Authored-By: Claude noreply@anthropic.com
AI-Assisted: yes
AI-Tool: claude-code
---------
Co-authored-by: manoel.neto <manoel.neto@creditas.com>
* fix(contato): normalizar e-mail e telefone no formulário público de briefing
SPEC.md §12.3 exige normalização de e-mail e telefone no formulário
público, mas o ContactBriefingRequest só validava, sem normalizar
(MAN-104).
E-mail: aplica trim + lowercase em prepareForValidation — evita
duplicatas triviais ("Maria@X.com" vs "maria@x.com") e mantém a
detecção de duplicidade por hash consistente.
Telefone: normaliza para o formato brasileiro legível
"(DD) 9XXXX-XXXX"/"(DD) XXXX-XXXX", extraído para o value object
App\Domain\Contact\BrazilianPhoneNumber por concentrar lógica de
decisão (contagem de dígitos, remoção de DDI 55) que merece teste
isolado. Optou-se pelo formato com máscara em vez de dígitos puros
porque o Mailable ContactBriefing e seus templates (html/text) apenas
imprimem o valor do campo "Telefone/WhatsApp" em uma tabela/lista,
sem link "tel:" nem formatação na view — "(11) 98888-7777" é o que
fica legível para quem recebe o briefing por e-mail, enquanto
"11988887777" é opaco de bater o olho.
Para não descartar informação que o destinatário precisa, o
normalizador só reformata quando a string é composta exclusivamente
por caracteres de telefone; anotações como "(WhatsApp)" ou "falar com
João" junto do número são preservadas como estão. Entradas que não
batem com 10/11 dígitos (após remover DDI) também são preservadas,
apenas com espaços internos colapsados.
Honeypot, rate limit e aceite de privacidade não foram tocados.
Cobertura: teste de unidade para o value object (formatos válidos,
DDI, anotações, formatos não reconhecidos) e teste de feature
provando que uma submissão com e-mail maiúsculo/padded e telefone
"sujo" chega normalizada nos Mailables ContactBriefing e
ContactBriefingConfirmation.
Co-Authored-By: Claude noreply@anthropic.com
AI-Assisted: yes
AI-Tool: claude-code
* fix(contato): evitar fabricar DDD/9º dígito em números não brasileiros
O normalizador aceitava qualquer string de 10 ou 11 dígitos como se
fosse um telefone brasileiro, sem checar se os dois primeiros dígitos
formam um DDD plausível (11-99, nunca com zero em nenhuma posição) ou
se um número de 11 dígitos tem o 9º dígito obrigatório do celular.
Isso fazia números estrangeiros como '2025551234' (EUA) virarem
'(20) 2555-1234' — um número brasileiro plausível, porém inventado,
que destrói silenciosamente o contato real na tabela do e-mail de
briefing. Um '+55' explícito sem DDD (ex.: '+55 98888-7777') também
era lido como DDD 55 em vez de número incompleto.
Passa a validar a forma do DDD e o 9º dígito do celular antes de
formatar, preservando o texto original quando a checagem falha —
mesmo comportamento já usado para números com contagem de dígitos
fora do padrão. Mantém DDD 55 (Rio Grande do Sul) funcionando
normalmente quando o DDD é digitado de fato.
Limitação residual conhecida e aceita: DDDs americanos que colidem
estruturalmente com um DDD brasileiro válido (ex. '2125551234', área
212 de Nova York, bate com DDD 21) continuam sendo formatados como
brasileiros — não há como distinguir os dois casos só pela forma dos
dígitos. Da mesma forma, um '55' sem o '+' explícito (ex.
'55988887777') é ambiguamente tratado como DDD 55 real, já que nada
no texto indica se é código de país ou área.
Co-Authored-By: Claude noreply@anthropic.com
AI-Assisted: yes
AI-Tool: claude-code
---------
Co-authored-by: manoel.neto <manoel.neto@creditas.com>
Replace tiny JPEG stubs with per-slot Unsplash editorial placeholders,
align public type scale to DESIGN.md display/headline tokens, add
final-cta on all subpages, about_image_path on site settings, and harden
visual regression with solid-color VisualContentSeeder fixtures.
* fix: prevent long strings from overflowing public layout
* fix: fall back to default hero copy and tighten hero line-height
* fix: skip blank testimonials and guard empty portfolio metadata
* feat: add branded 419, 429 and 503 error pages
* fix: reset submit state on bfcache restore, swap label while sending, trap mobile menu focus
* chore: track hardening task status
* fix: restore contact submit state after bfcache
* fix: allow contact links to wrap long unbroken strings
* chore: add husky pre-commit and pre-push hooks
* feat: frontend audit — formulário de contato, a11y, SEO, performance e conteúdo
* test: regenerate visual baselines from CI environment
Avoid browser PUT to R2 (CORS) by defaulting Filament/Livewire temp disk to local, and document Dokploy tinker + R2 CORS gotchas.
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat: recreate public frontend with Heritage Editorial identity
Replace placeholder visual system with EB Garamond/olive tokens, brand assets, editorial home narrative, São Paulo settings, real testimonials, and regenerated visual baselines.
Co-authored-by: Cursor <cursoragent@cursor.com>
* docs: archive recreate-public-frontend and sync Heritage Editorial specs
Merge delta requirements into main OpenSpec capabilities and move the completed change into the dated archive.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Add production provider deps/config so transactional email and CMS
media can use Resend and a dedicated r2 disk with custom-domain URLs.
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat: ship public site with SEO and visuals
Publish CMS content on public routes with responsive media,
accessibility checks, and deterministic visual baselines.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: make browser visual CI deterministic for media
Mount host public storage into FrankenPHP so seeded fixtures are served,
and replace PNG-as-JPG fixtures with real JPEGs so Chromium can render them.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: serve public media on same-origin storage paths
Pest Browser hosts on 127.0.0.1:port while Storage::url used
http://localhost, so screenshots captured broken images. Use relative
/storage URLs for media and absolutize only OG tags via url().
Co-authored-by: Cursor <cursoragent@cursor.com>
* test: refresh visual baselines from CI Ubuntu screenshots
Media now loads on same-origin /storage paths, so baselines must
capture the rendered fixtures. Use full-page snapshots from the CI
runner to keep Pest's exact snapshot match stable across environments.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Implementa gestão Filament de configurações, serviços, portfólio e depoimentos com policies admin-only, upload validado e seed determinístico.
Co-authored-by: Cursor <cursoragent@cursor.com>