Commit Graph

111 Commits

Author SHA1 Message Date
37296f758f feat: capturar origem de marketing do briefing de contato (MAN-105) (#37)
* 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>
2026-08-10 09:12:26 -03:00
49fbc0fdfa fix(seed): impedir ContentSeeder de sobrescrever conteúdo em produção (MAN-103) (#36)
* fix(seed): impedir ContentSeeder de sobrescrever conteúdo em produção

O migrate do Dokploy roda `db:seed --class=ContentSeeder --force` em todo
deploy, tanto em staging quanto em produção (mesmo compose, só o .env
muda). Como ContentSeeder usa updateOrCreate/delete incondicionais, cada
deploy em produção revertia edições feitas pela dona no Filament
(SiteSetting, Services, PortfolioCases), republicava os 3 casos e
serviços fictícios, apagava fotos reais da galeria e reenviava as
fixtures de imagem para o bucket R2 de produção — além de publicar os
depoimentos reais via TestimonialsSeeder, sem autorização explícita.

Adiciona uma guarda de ambiente no início de ContentSeeder::run(),
seguindo o mesmo padrão já usado em AppServiceProvider::
freezeClockWhenConfigured(): em APP_ENV=production o método retorna
sem efeito colateral (exit 0), preservando o passo migrate&&seed&&...
do docker-compose.deploy.yml. TestimonialsSeeder não é tocado — ele
continua sendo o único caminho de publicação de depoimentos em
produção, documentado como passo manual em docs/deployment/dokploy.md.
Staging (APP_ENV=staging) continua recebendo conteúdo de demonstração
normalmente, preservando a revisão visual.

Atualiza docs/deployment/dokploy.md, que afirmava (incorretamente,
desde o commit 10a1f0d) que os workflows de deploy eram "migrate-only"
e que ContentSeeder nunca rodava em staging/produção.

Adiciona teste de regressão cobrindo: banco vazio em produção (nenhum
registro criado, nenhuma fixture enviada ao disco), banco com conteúdo
editado pela dona em produção (seeding duas vezes não altera nada) e
comportamento inalterado fora de produção (semeia o conteúdo de demo
normalmente).

Co-Authored-By: Claude noreply@anthropic.com
AI-Assisted: yes
AI-Tool: claude-code

* fix(seed): trocar guarda de produção do ContentSeeder para allow-list

O guard anterior (`App::environment('production')`) é uma comparação
exata e sensível a maiúsculas contra o valor literal de APP_ENV, que é
digitado manualmente no editor de texto livre do Dokploy sem validação,
enum ou valor padrão garantido por este repositório. Se esse valor
algum dia ficar vazio, vier com case diferente ('Production',
'PRODUCTION'), espaço em branco ou for simplesmente digitado errado, o
guard falha aberto: o ContentSeeder roda seu caminho destrutivo em
produção sem nenhuma outra checagem no caminho de deploy que pegasse o
erro — exatamente o cenário que esta guarda deveria evitar.

Inverte a lógica para uma allow-list dos ambientes conhecidos e
seguros ('local', 'staging', 'testing'), então qualquer valor não
reconhecido de APP_ENV — incluindo 'production', vazio, mal digitado
ou com case diferente — falha fechado (no-op) em vez de falhar aberto
(sobrescrever dados reais). 'testing' entra na lista porque é o valor
de APP_ENV usado pelo job `feature` do CI (ver .github/workflows/ci.yml),
que roda testes que chamam ContentSeeder diretamente — sem esse valor
a suíte quebraria em CI mesmo passando localmente (APP_ENV=local via
.env).

Adiciona cobertura de regressão via data provider cobrindo tanto os
três ambientes permitidos quanto uma lista de valores não permitidos
('production', string vazia, 'Production', 'PRODUCTION', espaço à
direita, valor arbitrário desconhecido), para que a guarda seja testada
como allow-list e não apenas contra o literal 'production'.

Co-Authored-By: Claude noreply@anthropic.com
AI-Assisted: yes
AI-Tool: claude-code

* docs(deploy): corrigir descrição da guarda do ContentSeeder no runbook

O texto descrevia a guarda antiga (deny-list de 'production') e ainda
afirmava, de forma incorreta, que a publicação dos cinco depoimentos
reais era "um passo manual e deliberado em todo ambiente (incluindo
staging)". Isso nunca foi verdade: o ContentSeeder chama o
TestimonialsSeeder internamente e só pula essa chamada quando a guarda
não casa com o ambiente atual — em staging a guarda casa, então os
depoimentos são publicados/republicados automaticamente a cada deploy,
sem nenhum passo manual envolvido (o próprio teste
test_content_seeder_still_seeds_demo_content_outside_production já
provava isso).

Atualiza as duas passagens para descrever a allow-list
('local'/'staging'/'testing') introduzida no commit anterior e para
deixar explícito que staging publica depoimentos automaticamente,
reservando o comando manual apenas para produção. Também documenta o
trade-off do fail-closed: um APP_ENV de staging digitado errado agora
faz o conteúdo de demonstração parar de ser resemeado silenciosamente.

Co-Authored-By: Claude noreply@anthropic.com
AI-Assisted: yes
AI-Tool: claude-code

---------

Co-authored-by: manoel.neto <manoel.neto@creditas.com>
2026-08-10 09:12:18 -03:00
2e2860396e docs: reconciliar a stack declarada do site público com a implementação (#35)
Conclusão da auditoria pedida na MAN-117.

A pergunta da issue era se "Vue + Livewire" faz sentido na arquitetura
atual. Vue não aparece em lugar nenhum: não está no package.json, não há
nenhum arquivo .vue, e a própria SPEC §11.3 já proibia framework SPA.
Metade da premissa da issue não tem base no repositório.

Livewire existe, mas não onde a SPEC dizia. O pacote entra apenas como
dependência transitiva de filament/support e opera somente dentro do
painel /admin. O site público é Blade renderizado no servidor mais JS
vanilla progressivo: o layouts.public carrega apenas
@vite(['resources/css/app.css', 'resources/js/app.js']), sem
@livewireScripts, e não há uma única diretiva wire: ou x-data em
resources/views. Alpine só existe empacotado dentro do runtime do
Livewire, duas indireções abaixo do Filament — nunca chega a uma página
pública.

Mesmo assim, cinco lugares afirmavam o contrário: §0 metadados, §5.3,
§9.1, §11 inteira e a ADR-002, que está marcada como Aceita descrevendo
uma arquitetura que a área que ela nomeia não implementa.

A correção adota o que existe em vez de construir o que estava
declarado. O motivo é o próprio documento: a §11.2 já registrava
Blade + Controller como resultado aceito, os requisitos que a §11.2 pede
do formulário (loading state, botão desabilitado, aria-busy) já são
atendidos por resources/js/app.js, e nada nas Fases 2 a 5 pede
reatividade no site público. Adotar Livewire agora colocaria um bundle
global de JS e CSS em páginas cujo resultado visual está fixado por
baselines de snapshot, a serviço de um requisito que ninguém escreveu.

Como a §0.2 proíbe alterar silenciosamente uma decisão do documento, a
correção não reescreve a ADR-002 fingindo que ela sempre disse outra
coisa: o texto dela é emendado apontando para uma ADR-015 nova, que
registra a decisão explicitamente. E a §22 ganha uma linha nomeando a
condição objetiva que justificaria reabrir o assunto — a mesma que a
§11.1 já previa em prosa, "se houver necessidade de consulta dinâmica".

Fica de fora deste commit, para evitar conflito com trabalho em curso no
mesmo arquivo: a linha equivalente em openspec/config.yaml, que também
carrega uma afirmação desatualizada sobre a cidade de atuação e será
corrigida de uma vez só.

Co-Authored-By: Claude noreply@anthropic.com
AI-Assisted: yes
AI-Tool: claude-code

Co-authored-by: manoel.neto <manoel.neto@creditas.com>
2026-08-10 08:58:42 -03:00
54cd6ba0da perf: cachear assets estáticos e reservar dimensões do logotipo (#34)
A auditoria de Lighthouse da MAN-109 mostrou que todas as páginas
públicas estouram a meta de LCP da SPEC §6.6 (≤ 2,5 s) em mobile
simulado: home 4,4 s, portfólio 4,2 s, serviços 3,6 s, sobre 3,0 s e
contato 2,6 s. CLS e TBT passam com folga, e acessibilidade, boas
práticas e SEO marcam 100 em todas elas.

Duas causas apontadas pela auditoria são corrigidas aqui.

O Caddyfile não definia Cache-Control, então tudo sob /storage e /brand
voltava com cacheLifetimeMs 0 e era rebaixado a cada visita — cerca de
256 KiB por page view repetido à toa. Agora /build e /storage recebem um
ano imutável, o que é seguro porque ambos usam nomes que mudam junto com
o conteúdo: o Vite gera hash no nome e o PublicImageUploadRules grava
cada upload sob um UUID novo. /brand fica em um dia, porque esses
arquivos são versionados na imagem sob nome fixo e um cache longo demais
prenderia um logotipo antigo no navegador depois de uma troca de marca.

O componente de logotipo também não declarava dimensões, o que a SPEC
§6.4 exige para reservar espaço e evitar deslocamento de layout. Passa a
declarar width/height intrínsecos dos arquivos versionados. Um logotipo
enviado pelo painel tem dimensões arbitrárias e por isso não recebe
atributo nenhum — melhor sem do que errado. As classes CSS continuam
governando o tamanho renderizado nos dois casos.

Efeito medido na home: unsized-images sai da lista de diagnósticos e
uses-long-cache-ttl cai de 10 recursos para 2.

Adiciona scripts/perf/lighthouse.sh com a receita da medição, incluindo
o passo media:generate-variants — rodar a auditoria sem ele infla o LCP
da home em cerca de 2,5 s, porque as imagens originais são servidas em
tamanho cheio.

Fica de fora, para PR próprio: reencodar os arquivos de marca, que hoje
pesam 82 KB para renderizar em 41x40 px. Isso muda pixels no cabeçalho
de todas as páginas e exige regerar os 16 baselines visuais no ambiente
de paridade com o CI.

Nenhum gate de performance foi adicionado ao CI. A SPEC §14.1 enumera os
cinco jobs bloqueantes e a §22 define os gatilhos objetivos para
adicionar capacidade; nenhum gatilho aponta para isso, e um gate criado
agora nasceria vermelho.

Co-Authored-By: Claude noreply@anthropic.com
AI-Assisted: yes
AI-Tool: claude-code

Co-authored-by: manoel.neto <manoel.neto@creditas.com>
2026-08-10 03:04:22 -03:00
4001cd0e5d docs: adicionar runbook de atualização de conteúdo (MAN-106 AC4) (#33)
* docs: adicionar runbook de atualização de conteúdo (MAN-106 AC4)

A dona do negócio precisa editar textos, fotos, casos de portfólio,
depoimentos e dados de contato pelo painel admin sem depender de
engenharia. Documenta as regras que mais confundem (publicação não é
agendamento; home exige publicado + destaque, exceto depoimentos),
os campos editáveis por tela, requisitos de imagem/alt text e o que
não deve ser mexido sem apoio técnico.

Também esclarece o status do depoimentos.md: é material bruto
histórico que nunca foi lido por código (confirmado por busca no
repo), então recebe uma nota de status no topo do arquivo em vez de
ser movido ou apagado, já que ainda é citado por documentos técnicos
antigos como origem do conteúdo.

Co-Authored-By: Claude noreply@anthropic.com
AI-Assisted: yes
AI-Tool: claude-code

* docs: reforçar avisos sobre resumo institucional e publicação em Configurações

Ajusta o runbook de atualização de conteúdo: deixa explícito que o
resumo institucional pode ser salvo em branco sem erro (produzindo um
parágrafo vazio silencioso na página Sobre) e que a tela Configurações
não tem passo de publicação — toda alteração entra no ar assim que é
salva.

Co-Authored-By: Claude noreply@anthropic.com
AI-Assisted: yes
AI-Tool: claude-code

* docs: corrigir regra de imagem opcional para capa de caso e galeria

A regra geral "sem imagem, texto alternativo pode ficar em branco"
(seção Imagens) estava sendo lida como "a imagem em si é opcional" para
todo campo do painel. Isso é falso para a capa de um caso de portfólio
e para a imagem de um item de galeria: as colunas cover_image_path,
cover_image_alt (portfolio_cases) e path, alt_text (portfolio_images)
são NOT NULL no banco, mas o formulário do Filament nunca chama
->required() nesses uploads. Quem seguisse a doc e tentasse salvar sem
imagem passaria pela validação do formulário e só então bateria num
erro de banco não tratado, em vez da mensagem amigável de campo
obrigatório usada para título, resumo etc. Confirmado em
database/migrations/2026_07_28_230932_create_portfolio_cases_table.php,
database/migrations/2026_07_28_230933_create_portfolio_images_table.php
e app/Support/PublicImageUploadRules.php — os demais campos de imagem
do painel (logo, foto de depoimento, imagem da página Sobre, imagem de
SEO, capa de serviço) são de fato nullable no banco, então a ressalva
fica restrita a portfólio.

Co-Authored-By: Claude noreply@anthropic.com
AI-Assisted: yes
AI-Tool: claude-code

* docs: avisar que Excluir apaga registros para sempre, sem lixeira

O runbook só ensinava publicar/despublicar (preencher ou apagar
"Publicado em") como forma de mostrar/esconder conteúdo em Portfólio,
Depoimentos e Serviços, e nunca mencionava que cada linha dessas
tabelas também tem um botão "Excluir" (e ação de exclusão em massa)
bem ao lado do de editar. Nenhum model usa SoftDeletes (confirmado em
app/Models) e as tabelas de Portfólio, Depoimentos e Serviços usam
DeleteAction/DeleteBulkAction (app/Filament/Resources/{PortfolioCases,
Testimonials,Services}/Tables/*.php e ImagesRelationManager.php), ou
seja, um clique em Excluir apaga o registro para sempre, sem
possibilidade de recuperação — inclusive apagando em cascata a galeria
inteira de um caso de portfólio (cascadeOnDelete em
portfolio_images.portfolio_case_id). Quem seguisse só a instrução de
"apagar o Publicado em" não tinha como saber que existia esse outro
controle, irreversível, na mesma linha.

Co-Authored-By: Claude noreply@anthropic.com
AI-Assisted: yes
AI-Tool: claude-code

* docs: explicar como converter foto HEIC do iPhone para o formato aceito

A instrução para formato de imagem recusado dizia só "converta a
imagem antes de enviar", sem dizer como. HEIC é o formato padrão de
fotos do iPhone e está listado no próprio texto como um dos formatos
recusados — é exatamente o erro que uma pessoa não técnica enviando
foto do celular vai encontrar primeiro, e o guia não pressupõe em
nenhum outro lugar que ela saiba converter formato de imagem. Adiciona
duas instruções concretas e não técnicas: ajustar o iPhone para salvar
fotos novas em JPEG, e usar o WhatsApp para converter fotos que já
estão em HEIC.

Co-Authored-By: Claude noreply@anthropic.com
AI-Assisted: yes
AI-Tool: claude-code

* docs: avisar que o e-mail de Contato recebe os briefings do site

O runbook descrevia o campo E-mail de Configurações → Contato só como
algo que vira link clicável no rodapé e na página /contato. Não
mencionava que esse mesmo endereço é o destino de todo envio do
formulário de contato: ContactController::dispatchEmails() (app/Http/
Controllers/PublicSite/ContactController.php) chama
Mail::to($settings->email)->send(new ContactBriefing(...)) para cada
briefing enviado por um visitante. Alguém lendo só esta doc poderia
trocar esse valor por um e-mail apenas decorativo e parar
silenciosamente de receber notificações de leads, sem nenhum erro
visível no admin.

Co-Authored-By: Claude noreply@anthropic.com
AI-Assisted: yes
AI-Tool: claude-code

* docs: documentar a tela Usuários para quem realmente pode acessá-la

O runbook só citava "Usuários" na negativa ("se você não é
Administradora, peça a uma Administradora"), mas toda política de
autorização do painel (PortfolioCasePolicy, ServicePolicy,
TestimonialPolicy, SiteSettingPolicy, UserPolicy — todas checando
$user->isAdmin()) garante que quem consegue seguir qualquer outra
seção deste guia é, por construção, uma Administradora com acesso
total a Usuários. A doc nunca explicava os campos da tela (Nome,
E-mail, Papel, Ativo, Senha) nem como usá-la para dar acesso a alguém,
apesar de seções anteriores (Como entrar no painel) já dependerem de a
leitora saber fazer exatamente isso. Adiciona uma seção "Usuários"
ligando os campos ao comportamento já descrito em outras seções (Papel
Assistente remove todo acesso; Ativo desligado bloqueia login via
User::canAccessPanel()) e avisa sobre um risco real e não coberto:
como UserPolicy não impede autoedição, uma Administradora pode trocar
o próprio Papel ou desligar o próprio Ativo e ficar travada fora do
painel sem nenhuma forma de recuperação self-service. Também reescreve
o bullet de Usuários em "O que NÃO mexer" para não instruir a mesma
leitora a evitar uma tela que, pela própria lógica de acesso do
sistema, é dela.

Co-Authored-By: Claude noreply@anthropic.com
AI-Assisted: yes
AI-Tool: claude-code

---------

Co-authored-by: manoel.neto <manoel.neto@creditas.com>
2026-08-10 03:03:30 -03:00
f5dd28089c fix(deploy): remover --force inexistente de media:generate-variants (#32)
O serviço `migrate` do docker-compose.deploy.yml encadeia três comandos
artisan com `&&`, e o último é `php artisan media:generate-variants
--force`. Esse comando não declara nenhuma opção — sua assinatura é
apenas `media:generate-variants`. O artisan responde `The "--force"
option does not exist.` e sai com código 1.

Como todos os outros serviços dependem do migrate por
`condition: service_completed_successfully`, o one-shot falhando impede
`web`, `queue` e `scheduler` de subirem. A stack inteira fica fora do ar
e o Dokploy reporta `status=error` com `errorMessage: null`, sem nada no
log do CI que aponte a causa.

O deploy de staging falhou assim em quatro commits seguidos: 58f24a6,
949f6ae, e9b4534 e fc3d5c4. A opção entrou em 10a1f0d.

Reproduzido dentro da própria imagem de produção:

    docker run --rm --entrypoint sh amare-app:ci \
      -c 'php artisan media:generate-variants --force; echo "EXIT=$?"'
    The "--force" option does not exist.
    EXIT=1

Além de remover a opção, adiciona um teste que percorre todas as chamadas
`php artisan` do compose de deploy e verifica que cada comando existe e
que cada opção passada está definida — considerando também as opções
globais do console, como `--no-interaction`. O teste falha se `--force`
voltar.

Co-Authored-By: Claude noreply@anthropic.com
AI-Assisted: yes
AI-Tool: claude-code

Co-authored-by: manoel.neto <manoel.neto@creditas.com>
2026-08-10 02:55:41 -03:00
eed8240487 fix(contato): normalizar e-mail e telefone no formulário público de briefing (#31)
* 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>
2026-08-10 02:48:55 -03:00
fc3d5c444f docs: atualizar estado da autenticação do gh no CLAUDE.md (#30)
O gh passou a autenticar como manoel-freitas, então gh pr create já
funciona neste repositório. Registra o estado atual e mantém a instrução
de verificar com gh auth status antes de assumir, já que a conta padrão
da máquina não enxerga o repositório.

Co-Authored-By: Claude noreply@anthropic.com
AI-Assisted: yes
AI-Tool: claude-code

Co-authored-by: manoel.neto <manoel.neto@creditas.com>
2026-08-10 02:33:22 -03:00
e9b4534df9 test: restaurar baselines visuais removidos por engano na reconciliação (#29)
O commit 58f24a6 ("chore: reconcile main deployment and agent docs")
apagou oito baselines de regressão visual — home, services, portfolio e
portfolio-detail, em desktop e mobile. O VisualRegressionTest continua
declarando os oito cenários, então as quatro telas mais importantes do
site ficaram sem proteção contra regressão visual.

Os arquivos voltam exatamente como estavam em 42b282c. Aquele commit e o
atual HEAD não têm nenhuma diferença que afete renderização: 58f24a6
tocou apenas AGENTS.md, docker-compose.deploy.yml, tasks.md e a própria
remoção dos snapshots. Nenhum Blade, CSS, token ou asset mudou, então os
baselines restaurados descrevem a renderização atual.

O job browser do CI, que roda em paridade com o ambiente de geração,
é a verificação desta mudança.

Co-Authored-By: Claude noreply@anthropic.com
AI-Assisted: yes
AI-Tool: claude-code

Co-authored-by: manoel.neto <manoel.neto@creditas.com>
2026-08-10 02:24:15 -03:00
949f6ae19f Docs/fix timezone drift (#27)
* docs: add CLAUDE.md with repository architecture guidance

Imports AGENTS.md as the primary contract and records the cross-file facts
that are not discoverable from any single file: the public-site request spine
(controller -> Application query -> DTO -> Blade), the layouts.public view
composer, what the architecture test actually enforces, the CI-parity
constraint on visual baselines, and the determinism trio behind them.

Co-Authored-By: Claude <noreply@anthropic.com>
AI-Assisted: yes
AI-Tool: claude-code
AI-Harness: creditas-agentic-harness
AI-Harness-Command: /init

* docs: corrigir timezone normativo para America/Fortaleza

SPEC.md e openspec/config.yaml declaravam APP_TIMEZONE=America/Sao_Paulo,
divergindo da implementação (config/app.php, .env.example), dos quatro jobs
de CI e de openspec/specs/visual-regression/spec.md, que fixa
America/Fortaleza e cita SPEC §13.5 como autoridade. As baselines visuais
foram geradas sob America/Fortaleza.

Cidade de atuação permanece São Paulo (capital) — é copy pública, não
timezone de runtime.

Co-Authored-By: Claude <noreply@anthropic.com>
AI-Assisted: yes
AI-Tool: claude-code
AI-Harness: creditas-agentic-harness
AI-Harness-Command: /init

* docs: corrigir referências mortas no AGENTS.md

- Design Context apontava para `.impeccable.md` na raiz, que não existe. O
  sistema de design vive em DESIGN.md/PRODUCT.md, tokens em
  resources/css/tokens.css, briefs por superfície em .impeccable/surfaces/ e
  a skill vendorizada em .github/skills/impeccable/SKILL.md.
- Domain docs afirmava CONTEXT.md na raiz. Não existe: o domínio está em
  SPEC.md §8 e PRODUCT.md, capacidades em openspec/specs/, e
  docs/adr/README.md é apenas índice das ADRs decididas em SPEC.md §21.
- Issue tracker: registra que o repo não traz .mcp.json, portanto o MCP do
  Linear precisa estar habilitado na sessão.

Co-Authored-By: Claude <noreply@anthropic.com>
AI-Assisted: yes
AI-Tool: claude-code
AI-Harness: creditas-agentic-harness
AI-Harness-Command: /init

* docs: registrar autenticação do remote no CLAUDE.md

O repo é do account `manoel-freitas`, mas a identidade SSH padrão da máquina é
outro account (`manoel-freitas-neto`) que não enxerga o repo — o push falha com
`Repository not found`, que parece repo inexistente e é problema de acesso. A
chave certa é `~/.ssh/id_github_pessoal`, já fixada em `core.sshCommand`.

Registra também que o `gh` autentica por token, não por chave SSH, então
`gh pr create` continua falhando mesmo com o push funcionando.

Co-Authored-By: Claude <noreply@anthropic.com>
AI-Assisted: yes
AI-Tool: claude-code
AI-Harness: creditas-agentic-harness
AI-Harness-Command: /init

---------

Co-authored-by: manoel.neto <manoel.neto@creditas.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-08-10 02:04:52 -03:00
58f24a6d5a chore: reconcile main deployment and agent docs
Squash merge of the validated main reconciliation changes.
2026-08-07 22:22:24 -03:00
4061662c4b chore: setup agent skills config (#25) 2026-08-07 21:48:48 -03:00
42b282c1f2 feat: adicionar cadência editorial à home (#23)
* feat: adicionar cadência editorial à home

* fix: reconcile pr23 ci with current main

* test: atualizar baselines visuais da home para cadência editorial
2026-08-07 21:40:35 -03:00
457845758c test: regenerar baselines visuais em ambiente CI-parity (#24)
* test: regenerar baselines visuais em ambiente CI-parity

* chore: bump league/commonmark para 2.9.0
2026-08-07 21:21:41 -03:00
0aee15e848 Merge pull request #22 from manoel-freitas/feat/public-site-motion
feat: animação editorial fluida no site público
2026-08-06 14:31:31 -03:00
b372eaea4f docs: add public motion visual evidence 2026-08-06 14:04:47 -03:00
7380998e8e feat: add fluid editorial motion to public site 2026-08-06 14:03:51 -03:00
4b440b7e5b Merge pull request #21 from manoel-freitas/fix/seed-resilient
fix(deploy): array command form + resilient seed
2026-08-06 11:27:48 -03:00
5949fad9b1 fix(deploy): array command form for dokploy + resilient seed
sh -c via folded scalar broke dokoy compose deploy; use array command.
Seed+media best-effort so web starts even if R2 seed errors.
2026-08-06 11:23:13 -03:00
d054d04ab9 Merge pull request #20 from manoel-freitas/fix/seed-resilient
fix(deploy): keep staging web up when content seed is best-effort
2026-08-06 11:16:20 -03:00
db4453b165 fix(deploy): keep staging web up when content seed is best-effort
Seed+media step made non-fatal so a transient R2 write issue
doesn't block web/queue/scheduler from starting. Seed output
preserved in dokploy deployment logs for diagnosis.
2026-08-06 11:11:45 -03:00
946939ba94 Merge pull request #19 from manoel-freitas/fix/seed-to-active-disk
fix(deploy): seed content on staging deploy
2026-08-06 11:00:37 -03:00
10a1f0dc7c fix(deploy): run seed + media:generate-variants after migrate
staging R2 was empty — migrate ran but no seeder, so ContentSeeder
never placed fixtures on the configured disk (r2) and media:generate-variants
had nothing for image paths. Staging deploy now seeds content.
2026-08-06 10:55:58 -03:00
c04790ee1f Merge pull request #18 from manoel-freitas/fix/seed-to-active-disk
fix: seed content images onto active filesystem disk
2026-08-06 10:21:14 -03:00
edf1c48050 fix: seed content images onto active filesystem disk
Preview/prod use FILESYSTEM_DISK=r2; seeder wrote only to public,
so media.hellomanoel.com URLs 404/403. Put fixtures on active disk.
2026-08-06 10:16:23 -03:00
3c711a74f3 Merge pull request #17 from manoel-freitas/fix/seed-fixtures-in-image
fix: ship seed image fixtures inside Docker image
2026-08-06 10:05:22 -03:00
88d63f349d fix: ship seed image fixtures inside Docker image
Move demo JPEGs from tests/fixtures (dockerignored) to
database/fixtures so ContentSeeder works in the container.
2026-08-06 10:03:26 -03:00
e0390ff333 Merge pull request #16 from manoel-freitas/feat/visual-placeholders
feat: visual placeholders, type scale, CTAs and about image
2026-08-06 09:54:52 -03:00
321f0cee11 fix: restore lazy loading and CI visual baselines
Drop test-wide eager override that broke MediaImageComponentTest.
Replace local Chromium baselines with CI FrankenPHP host captures.
2026-08-06 09:50:48 -03:00
1d23e16625 merge: origin/main into feat/visual-placeholders
Resolve conflicts keeping DESIGN.md type scale, main blade fallbacks,
and StableScreenshot visual helper. Regenerate browser baselines.
2026-08-06 09:42:37 -03:00
33788af172 feat: visual placeholders, type scale, CTAs and about image
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.
2026-08-06 09:38:13 -03:00
119711d6f0 chore: mark browser CI gate tasks complete in tasks.md 2026-08-06 09:09:08 -03:00
f09ea83071 fix: stabilize and diagnose browser CI gate (#15)
* fix: stabilize and diagnose browser CI gate

- Replace networkidle screenshot wait (5s client timeout, flaky with
  long-lived connections) with readyState + fonts-loaded wait and fixed
  settle in a StableScreenshot helper.
- Export standalone diff/expected/actual PNGs on visual mismatch so CI
  artifacts are directly viewable (vendor only writes an HTML diff view).
- Create .env in CI test jobs; without it Laravel's env bootstrap emits a
  file_get_contents warning on every test.
- Bump checkout/cache actions to v5 (Node 20 deprecation).
- Gitignore tests/Browser/Screenshots.

* docs: track browser CI gate task status in tasks.md
2026-08-06 08:59:03 -03:00
2fe262ab59 Hardening public blades: overflow, fallbacks, branded errors, JS robustness (#14)
* 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
2026-08-06 00:22:53 -03:00
018b43e24f docs: sync SPEC.md and openspec with ratified decisions 2026-08-05 23:42:37 -03:00
9fac784f6a feat: frontend audit — formulário de contato, a11y, SEO, performance e conteúdo (#13)
* 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
2026-08-05 23:27:14 -03:00
8a12b7d82c chore: add husky pre-commit and pre-push hooks (#12) 2026-08-05 23:02:46 -03:00
af8484c74e fix: keep motion entrance at full opacity for contrast
Axe sampled hero/page-open text mid opacity fade (~0.58), reporting
false WCAG failures. Animate transform/clip-path only.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-05 20:28:40 -03:00
35bc5a59d9 feat: Dossiê vivo motion for the public site (#10)
* feat: add Dossiê vivo motion to the public site

Introduce a focal Home opening, chapter index, and restrained continuity
reveals while keeping reduced-motion and visual baselines intact.

Co-authored-by: Cursor <cursoragent@cursor.com>

* test: refresh visual baselines from CI Ubuntu screenshots

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-05 20:16:51 -03:00
27711ad0c2 feat: add production testimonials seeder (#11)
Canonical upsert for five authorized couples so staging/prod
can load depoimentos without DatabaseSeeder/ContentSeeder.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-05 20:11:33 -03:00
af31738021 test: align Livewire temp-disk assertion with PHPUnit disk swap
FileUploadConfiguration: :disk() always returns tmp-for-tests under PHPUnit; assert config + isUsingS3() instead.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-05 19:53:19 -03:00
286522d8a2 fix: keep Livewire temp uploads local when using R2
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>
2026-08-05 00:11:32 -03:00
3e68192cf3 fix: join Dokploy network so Compose reaches managed Postgres (#9)
Migrate could not resolve the database internal hostname because the
Compose stack used an isolated project network. Attach services to the
external dokploy-network used by Dokploy Postgres.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-04 23:15:52 -03:00
1a31dd1cf6 fix: surface Dokploy API errors and normalize panel URL (#8)
curl -f hid 404 bodies; strip accidental /api on DOKPLOY_URL so compose.deploy diagnostics are actionable.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-04 23:14:12 -03:00
9060036024 test: refresh visual baselines from CI Ubuntu screenshots (#7)
Heritage Editorial redesign snaps were generated outside CI and fail
assertScreenshotMatches on FrankenPHP/Ubuntu Chromium. Promote the
browser-failure-artifacts PNGs from run 30729267028 into Pest snaps.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-04 22:23:58 -03:00
7f4ea01f4b feat: prepare Dokploy staging and production deploy pipeline (#6)
Publish immutable FrankenPHP images to GHCR, auto-deploy staging after CI,
and promote the same digest to production with smoke, backup, and rollback docs.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-01 23:40:13 -03:00
cf1589c916 feat: recreate public frontend with Heritage Editorial identity (#5)
* 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>
2026-08-01 23:39:00 -03:00
637be0664a docs: define Heritage Editorial design system
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-01 22:32:34 -03:00
e4c1424d9d addd github impeccable 2026-08-01 22:06:48 -03:00
1d858835b4 docs: capture durable product context
Record confirmed audience, positioning, constraints, and evidence boundaries for future design work.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-01 22:06:00 -03:00