## WHAT
- `AGENTS.md` e `docs/agents/issue-tracker.md`: todo trabalho e todo PR precisam de issue Linear.
- SDLC automático: trabalho feito → testes feitos ou atualizados → pre-commit verde → commit → PR, sem perguntar.
- Templates de PR idênticos no Gitea (`.gitea/PULL_REQUEST_TEMPLATE.md`) e no GitHub (`.github/pull_request_template.md`) com WHAT / WHY / HOW / Linear Issue / Comments.
## WHY
Agentes estavam soltos no tracker e paravam pra perguntar se podiam commitar. O contrato precisa ser o ciclo completo, com issue Linear e template de PR.
## HOW
Docs e templates only. Sem mudança de runtime. Pre-commit (`composer pint:check` + `composer phpstan`) passou.
## Linear Issue
- [MAN-132](https://linear.app/maneco-workspace/issue/MAN-132/exigir-issue-linear-em-todo-trabalho-e-pr)
- [MAN-133](https://linear.app/maneco-workspace/issue/MAN-133/adicionar-pr-template-no-gitea-e-no-github)
## Comments
GitHub é o mirror legado; o PR canônico é este no Gitea.
Reviewed-on: #4
Co-authored-by: manoel freitas <manoel.josefneto@gmail.com>
Co-committed-by: manoel freitas <manoel.josefneto@gmail.com>
## Summary
- Remove `cta-route="contact"` dos cards de modalidade em `/servicos` para reutilizar `PackageContactLink` (WhatsApp / briefing), alinhado à home.
- Band "Conversar com a Amare" aponta para `/briefing` em vez de `/contato` (jornada de parceiros).
- Feature test de regressão em `ServicesPageCtaTest`.
## Test plan
- [x] pest ServicesPageCtaTest + home package CTA
- [ ] CI verde
- [ ] Com whatsapp_number no CMS: CTA do card em /servicos abre wa.me
Reviewed-on: #2
Co-authored-by: manoel freitas <manoel.josefneto@gmail.com>
Co-committed-by: manoel freitas <manoel.josefneto@gmail.com>
Remove job/reusable workflow do CI. Staging volta a ser workflow
separado após CI; 1.24 não implementava o trigger.
Co-authored-by: Cursor <cursoragent@cursor.com>
capacity>1 exige rede isolada por job e containers aninhados via DNS,
sem publish de host ports que colidem entre jobs.
Co-authored-by: Cursor <cursoragent@cursor.com>
act_runner em VPS compartilhada falhava com port already
allocated; jobs usam service postgres na rede do job.
Co-authored-by: Cursor <cursoragent@cursor.com>
Gitea rejeita nomes de secret reservados GITEA_*; workflows
e runbook passam a REGISTRY_PAT / REGISTRY_USER.
Co-authored-by: Cursor <cursoragent@cursor.com>
* perf: cortar 157 KiB do caminho crítico e medir o LCP de forma reprodutível (MAN-109)
A auditoria do PR #34 mediu LCP acima da meta de 2,5 s da SPEC §6.6 em todas
as páginas no mobile, mas ficou registrada apenas como tabela num comentário
do Linear — `storage/app/lighthouse` é gitignored, então não havia artefato
para comparar depois. Esta entrega mede de novo, encontra a causa dominante e
corta o que dava para cortar.
## Fontes servidas em dobro (88 KiB)
Bunny entrega cada peso de EB Garamond em woff2 e woff, e o plugin de fontes
emitia uma regra `@font-face` para cada, woff2 primeiro e woff depois. Duas
regras com a mesma família, peso, estilo e unicode-range fazem a última
vencer: o navegador renderizava a partir dos woff e descartava os woff2
pré-carregados.
O log de rede da home prova: 3 woff em prioridade VeryHigh (88 KiB), a mais
alta da página e à frente do elemento de LCP, mais 3 woff2 em High (74 KiB)
baixados só por causa do `<link rel="preload">`. 162 KiB de tráfego para
74 KiB de fonte útil.
woff2 é suportado por todo navegador que este site atende desde 2016, então as
regras woff eram peso morto, não fallback. O plugin `amare:fonts-woff2-only`
remove as regras do CSS e do manifest e tira os arquivos do bundle. É o que
derruba o FCP de 1,51 s para 0,91 s em todas as páginas.
## Ativos de marca reencodados (102 KiB) — absorve MAN-122
O logotipo era servido a 512 px de largura para renderizar em 48 px (lockup,
cabeçalho e rodapé de toda página) e 32 px (mark, home), com `loading="eager"`.
Reencodados a 3× do maior render: lockup 149×144 (84 → 14 KiB) e mark 191×96
(42 → 10 KiB), mesmos nomes de arquivo para não invalidar cache. As variantes
`on-dark` foram reencodadas junto por consistência; nenhuma view as usa hoje.
Os 16 baselines visuais foram regenerados no runner Linux e o diff é
imperceptível a 2× de zoom — mesma forma, mesma cor, menos bytes.
## Efeito medido
Mobile, mediana de 3 execuções por página: home 4,58 → 3,39 s; portfolio
3,98 → 1,58 s; portfolio-detalhe 3,98 → 3,01 s; servicos 3,68 → 2,63 s;
sobre 3,01 → 1,58 s; contato 2,55 → 1,51 s. Desktop passa com folga em todas
(máximo 0,79 s). Acessibilidade, boas práticas e SEO seguem 100, CLS 0,000 e
TBT 0 ms.
`contato`, `portfolio` e `sobre` entraram na meta. `servicos`,
`portfolio-detalhe` e `home` continuam fora, e o que falta está identificado:
o elemento de LCP da home é a imagem do hero em JPEG q82 (143 KiB), e
`Improve image delivery` estima 0,90 s de ganho restante. O lever é variante
WebP em ResponsiveImage com `<picture>` — fora deste escopo porque mexe no
pipeline de mídia que o CMS usa para upload.
## Ferramental
- `scripts/perf/lighthouse.sh` ganha `/portfolio/{slug}`, passada desktop,
mediana de 3 execuções e uma guarda de HTTP 200 antes de auditar. Uma rota
em 404 produz relatório com score alto: a rota mais pesada do site
apareceria como ótima e ninguém notaria.
- `scripts/perf/summarize-lighthouse.mjs` extrai do JSON o elemento de LCP, a
decomposição em fases e as requests até o LCP.
- `docker/ci-runner.Dockerfile` e `scripts/test/visual-update-ci.sh` versionam
a receita de regeneração de baselines, que existia só como checklist.
- `CLAUDE.md` corrigido: Pest Browser serve a aplicação de um servidor Amp
in-process, não do FrankenPHP. A paridade que importa nos baselines é
Linux vs macOS, não o runtime HTTP.
- Sem gate de performance no CI (§14.1 e §22). No lugar, orçamento de bytes
para os ativos de marca e asserção de que o build só emite woff2.
Staging não foi medido: a origem responde 303 para blocked.teams.cloudflare.com
a partir da rede corporativa, inclusive em `/up`. Duas hipóteses seguem não
validadas por dependerem de `FILESYSTEM_DISK=r2` — os ~4 round trips a R2 por
imagem que `x-media.image` faz sem cache, e a falta de `preconnect` para a
origem cross-origin de mídia. Ambas documentadas em
docs/evidence/lighthouse/README.md.
Co-Authored-By: Claude noreply@anthropic.com
AI-Assisted: yes
AI-Tool: claude-code
* perf: servir imagens de conteúdo em webp e descrever sizes corretamente (MAN-109)
Fecha a meta de LCP ≤ 2,5 s da SPEC §6.6 em todas as páginas, nos dois presets.
Continuação direta do commit anterior: com fontes e marca resolvidas, o elemento
de LCP de toda página no mobile passou a ser a imagem do hero, e o Load Delay de
1,88 s era contenção de banda pura.
## Variantes webp
`ResponsiveImage::generate()` escreve uma variante `.webp` ao lado de cada
variante no formato original, e `x-media.image` a oferece num
`<source type="image/webp">`. O `<img>` continua apontando para o formato
original: o `<source>` é preferência, não substituição, então nada quebra em
quem não decodifica webp, e mídia antiga sem irmãos webp renderiza `<img>` puro
como antes.
O `<picture>` recebe `display: contents` porque os chamadores estilizam o `<img>`
com classes como `h-full w-full object-cover` que resolvem contra o pai grid ou
flex — um wrapper inline quebraria isso.
O nome do arquivo acrescenta a extensão em vez de trocá-la
(`photo-720.jpg.webp`): uploads usam UUID, então colisão já era improvável, mas
`photo-720.webp` colidiria com a variante webp de um `photo.png`.
## sizes que descreve a realidade
Nenhuma imagem do site ocupa a viewport inteira — todas ficam dentro de
`container-amare`, que reserva 1,5rem de padding de cada lado. Declarar `100vw`
fazia uma viewport de 412 px em DPR 1,75 pedir 721 px e pular para a variante de
960 para desenhar uma caixa de 637 px. Errar por um pixel custava um terço a mais
de bytes em toda página.
Com `calc(100vw - 3rem)` a home passa a usar a variante de 720: 74 KiB, contra
143 KiB no início da investigação. Foi também por isso que 720 entrou em
`ResponsiveImage::WIDTHS` — sem ela o salto de 480 para 960 é grande demais para
a viewport mobile mais comum.
## Efeito medido
Mobile, mediana de 3 execuções: home 3,39 → 2,49 s; portfolio-detalhe
3,01 → 2,18 s; servicos 2,63 → 2,03 s; portfolio 1,58 s; sobre 1,58 s;
contato 1,50 s. Desktop no máximo 0,65 s. Peso total da home 798 → 347 KiB.
Contra o início da investigação (`2e43fde`): home 4,58 → 2,49 s com score de
performance 83 → 98.
A home passa **em cima da linha** — a pior das três execuções deu 2,57 s. Está
documentado como aprovada por margem, não com folga, e os levers restantes estão
listados em docs/evidence/lighthouse/README.md em ordem de custo.
Os 16 baselines visuais não mudaram: nenhum seeder gera variantes e
`media:generate-variants` não roda no runner visual, então naquele ambiente
`availableVariants()` volta vazio e o componente renderiza `<img>` puro. A
cobertura do caminho com variantes fica em MediaImageComponentTest e
ResponsiveImageTest, não nos baselines — anotado como lacuna conhecida.
Co-Authored-By: Claude noreply@anthropic.com
AI-Assisted: yes
AI-Tool: claude-code
* docs: registrar a dependência do deploy e corrigir os commits nas evidências
Três correções de honestidade nos artefatos de medição.
O campo `Commit:` era gravado com o HEAD do momento da coleta, que é sempre
anterior ao commit que contém as mudanças medidas — as duas passadas pós-correção
diziam `2e43fde` e `65919d4` quando as árvores medidas viraram `65919d4` e
`9ab2beb`. Corrigido à mão nos dois arquivos, e o script passa a marcar
`+alterações não commitadas` quando a árvore está suja, para o artefato não voltar
a afirmar o que não é.
O ganho das imagens não aparece em staging nem em produção antes de
`media:generate-variants` rodar: lá o disco é `r2` e as variantes webp e a largura
720 ainda não existem para a mídia publicada. O serviço `migrate` do
docker-compose.deploy.yml roda o comando sem condição a cada deploy e ele
regenera mesmo quando já há variantes, então o primeiro deploy resolve — mas
estava implícito e agora está escrito.
E o custo que as variantes webp introduzem no lado servidor está registrado com o
número certo: `x-media.image` faz 8 `exists()` por imagem no lugar de 3, o que em
`r2` são ~9 round trips remotos por imagem e ~36 na home. Isso agrava a hipótese
não validada de TTFB em staging em vez de melhorá-la, e promove o cache desses
metadados a item mais urgente da lista.
Co-Authored-By: Claude noreply@anthropic.com
AI-Assisted: yes
AI-Tool: claude-code
* fix: cache responsive image metadata for R2
* test: atualizar baselines visuais Linux
---------
Co-authored-by: manoel.neto <manoel.neto@creditas.com>
A cidade de atuação é São Paulo, garantida por teste em quatro lugares e
exigida por openspec/specs/site-settings/spec.md. O identificador de
timezone, porém, era America/Fortaleza. O identificador passa a
acompanhar o negócio.
A mudança não altera comportamento. America/Sao_Paulo e
America/Fortaleza são UTC-3 o ano inteiro desde que o horário de verão
brasileiro foi extinto — verificado para janeiro, março e dezembro de
2026, idênticos ao segundo. Nada renderizado muda, o relógio congelado
dos testes visuais usa offset absoluto (-03:00) e os baselines seguem
válidos.
O motivo de mexer é outro: a divergência entre o timezone e a cidade
custou tempo real. Uma sessão anterior a interpretou como drift e
"corrigiu" a SPEC no sentido errado, mudando o documento normativo para
Fortaleza em vez de olhar o que o negócio é. Com os dois valores dizendo
São Paulo, não há mais o que interpretar.
Escopo: config/app.php, .env.example, os dois pontos do ci.yml,
SPEC.md (§0, §13.5, §15.4), README.md, docs/deployment/dokploy.md,
CLAUDE.md, openspec/config.yaml, openspec/specs/visual-regression/spec.md
e o withTimezone do VisualRegressionTest.
Intocados de propósito: as asserções que garantem que Fortaleza não
aparece como cidade de operação, em PublicPagesTest, SiteSettingsTest,
ContentSeederProductionGatingTest e openspec/specs/site-settings. Essas
tratam de cidade, não de fuso, e continuam corretas.
Co-Authored-By: Claude noreply@anthropic.com
AI-Assisted: yes
AI-Tool: claude-code
Co-authored-by: manoel.neto <manoel.neto@creditas.com>
A SPEC §4.1 declarava CRM de leads, conversão de lead em evento, eventos,
tarefas, fornecedores, orçamento, pagamentos manuais, documentos,
dashboard e auditoria como parte do MVP. O projeto do lançamento aprovado
com a dona do produto é o site institucional — Fases 0 e 1 — e o
repositório reflete isso: não existe model, migration ou tela para
nenhuma dessas capacidades.
Nada disso estava registrado em lugar nenhum, então a §1 e a §25 seguiam
instruindo qualquer agente que lesse o documento a implementar as Fases 2
a 5. A divergência era silenciosa e a §0.2 proíbe justamente isso.
O que muda:
- §4 ganha nota de recorte apontando para a ADR-016, e a §4.1 separa o
que entra no lançamento do que fica adiado. A distinção entre adiado e
fora do MVP fica explícita: os itens adiados seguem sendo o produto
pretendido, enquanto a §4.2 lista o que não deve ser implementado nunca.
- §18 ganha nota equivalente e as Fases 2 a 5 passam a ser marcadas como
ADIADA no próprio título.
- §21 registra a ADR-016.
- §23 é dividida em dois recortes. Sem isso o documento passaria a dizer
que o MVP nunca pode ser concluído: das dezoito condições originais,
nove pertencem às fases adiadas. A §23.1 fecha por conta própria e a
§23.2 não bloqueia o lançamento.
Uma condição foi corrigida no caminho, não só movida: a original dizia
"briefing cria leads de forma segura". Criar Lead é Fase 2. O formulário
atual envia e-mail e não persiste nada, então a condição do lançamento
passa a ser enviar o pedido com proteção contra abuso e aceite de
privacidade registrado — o que o código realmente faz.
Marca também o hello-world em staging na §18, agora que existe evidência:
o deploy passou em 7e68c0e depois da correção do PR #38, com smoke verde.
Com isso o critério de saída da Fase 0 está cumprido.
O openspec/config.yaml recebe a linha de stack corrigida que ficou de
fora do PR #35. A afirmação sobre a cidade de atuação permanece: São
Paulo está correto e é garantido por teste — a suspeita de que também
estivesse desatualizada não se confirmou.
Co-Authored-By: Claude noreply@anthropic.com
AI-Assisted: yes
AI-Tool: claude-code
Co-authored-by: manoel.neto <manoel.neto@creditas.com>
* 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>
Esta é a causa real da queda do deploy de staging, reportada pelo erro
`sh: 2: Syntax error: "&&" unexpected`.
O `command` do serviço `migrate` estava escrito como block scalar
dobrado (`command: >`). Em YAML, o dobramento junta linhas com espaço
apenas quando elas têm a mesma indentação da primeira; toda linha mais
indentada preserva a quebra. As linhas `&& ...` estavam um espaço mais à
direita, então o valor final era:
sh -c "php artisan migrate --force --no-interaction
&& php artisan db:seed --class=ContentSeeder --force --no-interaction
&& php artisan media:generate-variants"
O `sh` recebe isso como duas linhas e aborta na segunda, antes de rodar
qualquer comando — daí o `sh: 2:` no erro. Como todos os outros serviços
dependem do migrate por `condition: service_completed_successfully`, a
stack inteira nunca sobe, e o Dokploy reporta `status=error` com
`errorMessage: null` porque nada da aplicação chegou a executar.
O formato exec-array de uma linha não tem essa ambiguidade. Ele já havia
sido adotado em 5949fad exatamente por isso, e o squash de reconciliação
58f24a6 o reverteu para o formato dobrado — o mesmo commit que também
apagou os oito baselines visuais restaurados no PR #29.
O `--force` inválido em `media:generate-variants`, corrigido no PR #32,
era um segundo defeito real na mesma linha, mas não era o que derrubava o
deploy: o `sh` morria antes de chegar lá.
Acrescenta guarda de regressão: nenhum `command` do compose de deploy
pode ser block scalar. Verificada nos dois sentidos — passa com o formato
atual e falha com a mensagem certa se o formato dobrado voltar.
Co-Authored-By: Claude noreply@anthropic.com
AI-Assisted: yes
AI-Tool: claude-code
Co-authored-by: manoel.neto <manoel.neto@creditas.com>