Files
portifolio-evandrodevbr/docs/superpowers/specs/2026-09-04-portfolio-hardening-design.md
T

19 KiB

Design: endurecimento técnico do portfólio

Status: aprovado para planejamento; implementação não iniciada Data: 2026-09-04

Objetivo

Reduzir fragilidade operacional, JavaScript global e código sem consumidores sem redesenhar o portfólio. O resultado mantém a identidade preto/papel/verde ácido, a tipografia e a narrativa visual, enquanto torna projetos, foco, movimento, dependências, analytics, deploy e validação previsíveis.

Estado de checkout que faz parte do alvo

A implementação futura trabalhará no checkout atual, sem worktree. Essa decisão é deliberada porque as alterações não commitadas abaixo são parte do estado-alvo e haverá um único editor; a pesquisa paralela é somente leitura:

  • data/project-health.json modificado pelo usuário: não regenerar nem reverter nesta iniciativa.
  • lib/portfolio-data.ts modificado pelo usuário: evoluir a partir do conteúdo atual, preservando a atualização apenas em desenvolvimento.
  • globals.css na raiz, não rastreado: não editar, mover, importar ou apagar. O stylesheet ativo é app/globals.css.
  • lib/portfolio-data.load.test.ts não rastreado: manter como baseline e atualizar suas expectativas somente quando o contrato de projetos mudar.

Não usar branch, commit, reset, checkout, stash ou comandos Git destrutivos. Rollback será por arquivo e por tarefa, usando os snapshots lidos antes de cada edição.

Escopo e arquitetura atual observada

  • Next.js App Router com React 19 e TypeScript estrito.
  • app/page.tsx carrega projetos no servidor por loadLiveFeaturedProjects() e repassa dados para CinematicHome.
  • filterLiveProjects() hoje transforma um snapshot ausente/expirado em lista vazia, ocultando todo projeto.
  • app/layout.tsx envolve todas as rotas em Lenis/GSAP, contexto de animação, transição de rota e overlays; PageIntro cria um portal modal que cobre a página por tempo fixo.
  • A home soma motion local, GSAP/ScrollTrigger redundante, cursores/overlays e loops contínuos de canvas.
  • GoogleAnalytics carrega GA em produção sem consentimento e usa rewrites que camuflam script/coleta.
  • next.config.ts marca diretórios públicos com nomes estáveis como immutable e mantém CSP somente em relatório com unsafe-inline e unsafe-eval.
  • Não existe workflow em .github/workflows.
  • pnpm-lock.yaml resolve Next 16.2.9, contém overrides históricos e foi gerado com faixas, embora package.json não fixe o conjunto de dependências.
  • nixpacks.toml já escolhe Node 22 e pnpm 9, mas instala com --no-frozen-lockfile.
  • Há componentes e testes sem consumidores de produção, incluindo dois caminhos de contato, biblioteca de demos/magicui e infraestrutura de animação não consumida.

Mapa de capacidades

Módulo Responsabilidade Depende de
Projetos e saúde de demo Sempre fornecer conteúdo; anexar estado observacional do link JSON estático, snapshot opcional
Shell acessível Entregar conteúdo/foco sem overlay bloqueante e sem scroll engine global layout, CSS global
Movimento e performance Preservar efeitos essenciais, remover duplicação e pausar trabalho invisível hooks locais, Intersection Observer, Page Visibility
Dependências e deploy Fixar runtime/dependências e produzir lock reproduzível código morto já removido
Analytics e headers Consentimento opt-in, revogação e carregamento direto de GA layout, next/script, configuração de headers
CI Instalação congelada e gates determinísticos lock final e scripts do pacote
Documentação operacional Descrever apenas comandos e comportamento realmente presentes implementação concluída

Direção de dependência: conteúdo estático → projeção com saúde → home; shell/CSS → componentes; código vivo → manifesto de dependências → lock/deploy/CI. Nenhum componente de UI consulta rede para decidir se conteúdo existe.

Decisões

Substituir o contrato de “projetos vivos” por uma projeção que mantém todo projeto com featured !== false e anexa o estado da demo:

export type DemoAvailability = "available" | "unavailable" | "unknown" | "none";

export type PortfolioProjectView = PortfolioProject & {
  demoAvailability: DemoAvailability;
  demoCheckedAt: string | null;
};

Regras:

  1. featured === false continua sendo a única regra de exclusão editorial.
  2. Sem demoUrl, o estado é none e não há CTA de demo.
  3. Sem entrada de snapshot, o estado é unknown; o card permanece e o URL configurado não é descartado.
  4. Resposta final 200 com Content-Type HTML produz available.
  5. Qualquer resultado explícito diferente produz unavailable.
  6. unavailable renderiza a evidência inteira e um controle não navegável com texto “Demo indisponível na última verificação”.
  7. unknown mantém o link e o anota como “Disponibilidade não verificada”.
  8. Em desenvolvimento, snapshot ausente ou mais antigo que quinze minutos pode disparar uma atualização em memória; falha de rede retorna os projetos com o último estado conhecido/unknown.
  9. Em produção, render nunca chama demos externas. O snapshot versionado é observacional e conteúdo não depende de cron, validade temporal ou rede.

app/page.tsx deixa de usar ISR de cinco minutos para simular monitoramento. scripts/check-projects.mjs continua como comando manual de atualização atômica, não como requisito de disponibilidade do site.

2. Acessibilidade não depende de animação

  • Remover PageIntro e seu teste; não substituir por outro splash.
  • Manter o skip link e garantir que nenhum overlay o cubra.
  • Colocar o reset de cor de links na camada base do Tailwind, permitindo que text-white, text-slate-* e estados hover da 404 prevaleçam.
  • Em prefers-reduced-motion: reduce, desativar smooth scroll e reduzir animações/transições CSS a uma atualização praticamente instantânea.
  • Fallbacks estáticos permanecem com o mesmo conteúdo e ações essenciais.

3. Um efeito por intenção, não camadas redundantes

A assinatura visual preservada é: paleta, fontes, hero “command center”, globo, starfield, notebook 3D quando elegível, cartões, divisores e tipografia de marquee.

Serão removidos:

  • Lenis e ticker GSAP do layout global.
  • Transição de rota global.
  • AnimationProvider, pois nenhum componente de produção consome seu contexto.
  • ReelDirector e CinematicSection, que repetem revelações já feitas por componentes locais.
  • Cursor customizado e seu MutationObserver, que reaplica listeners sobre a árvore.
  • canvas de film grain; a textura passa a CSS estático.
  • loops JS de marquee e divisores; tornam-se animações CSS/SVG sem estado React por frame.

GSAP/ScrollTrigger permanece somente na história do notebook, onde controla uma timeline scroll-driven real. Motion permanece nos componentes que já implementam feedback/reveal local. Nenhuma biblioteca nova será adicionada.

Loops restantes obedecem aos dois sinais:

const shouldRun = isIntersecting && !document.hidden && !shouldReduceMotion;

HeroStarfield já segue esse contrato e será preservado. NotebookStory combinará interseção e visibilitychange; o globo manterá flags separadas para interseção e visibilidade do documento para não reativar fora da viewport.

4. Hero terá somente a variante ativa

Hero mantém exclusivamente “command-center”. Remover pesos, seleção aleatória, cinco blocos editoriais não alcançáveis, layouts alternativos e branches de SideVisual. HeroScene perde o parâmetro variant, hoje ignorado. O conteúdo visível, CTAs, globo, telemetria, links sociais e fallback de reduced motion permanecem.

5. Remoção de código morto é um corte limpo

Excluir, após confirmar todos os callers na execução:

  • ContactForm, ContactInfo e usePortfolioData; Contact é o caminho ativo.
  • CompanyLogos e seus três arquivos de teste.
  • todos os demos na raiz de components/ e components/magicui/, sem importadores.
  • AnimationProvider, seu teste, app/lib/animations/config.ts, app/lib/animations/variants.ts e app/hooks/useScrollReveal.ts.
  • FloatingElements, HomeButton, HorizontalScroll e CinematicText, sem importadores.
  • componentes de performance substituídos/removidos na decisão 3 e seus testes de implementação órfãos.

Não manter aliases, reexports ou shims.

6. Testes protegem comportamento observável

  • Reescrever os testes de saúde para provar que todos os projetos editoriais permanecem e apenas o estado da demo muda.
  • Preservar um teste de desenvolvimento que prova atualização em memória sem permitir fetch em produção.
  • Cobrir o CTA disponível, indisponível e desconhecido de Projects por nome/estado acessível.
  • Remover PBTs de CompanyLogos junto do componente.
  • Remover PBTs do Footer que repetem um componente estático; Footer.test.tsx já cobre nomes acessíveis e segurança de links.
  • Substituir três PBTs de Experience por testes determinísticos de filtro editorial e ordem cronológica; hierarquia de heading não precisa de geração aleatória.
  • Remover a asserção que fixa classes internas do Footer e a asserção da hero que inspeciona props serializadas de motion.
  • Remover fast-check quando seu último consumidor desaparecer.

7. Runtime e dependências serão reproduzíveis

  • engines.node: 22.x.
  • packageManager: [email protected].
  • Next.js: 16.3.0, menor versão estável da mesma major que eliminou as três vulnerabilidades high mantidas por 16.2.11 e 16.2.12.
  • Todas as dependências diretas e de desenvolvimento terão versão exata, sem ^, ~ ou comparadores.
  • @types/node acompanha o runtime em 22.20.1.
  • @types/three passa para devDependencies.
  • Remover pacotes cujo último consumidor foi apagado: @gsap/react, class-variance-authority, lenis, ogl, tailwindcss-animate, tw-animate-css, fast-check, @types/resize-observer-browser e resize-observer-polyfill.
  • Manter GSAP, cobe, Motion, React Three Fiber, Drei e Three porque ainda há consumidores ativos.
  • Regenerar pnpm-lock.yaml com pnpm 9.15.9 e sem bloco overrides.
  • Corrigir advisories atualizando o pacote direto proprietário da cadeia; não usar audit --force, exceções ou novos overrides.
  • Nixpacks usa pnpm install --frozen-lockfile.

8. CI terá um caminho único

Criar .github/workflows/ci.yml para push e pull_request, em Ubuntu, Node 22 e pnpm 9.15.9. Ordem:

  1. checkout;
  2. instalação congelada com cache do pnpm;
  3. pnpm lint;
  4. pnpm typecheck;
  5. pnpm test -- --runInBand;
  6. pnpm build;
  7. pnpm audit --prod --audit-level=high;
  8. pnpm audit --dev --audit-level=high.

Não adicionar Playwright, Cypress ou outro browser ao projeto. O smoke visual/interativo usa o Chromium disponível no harness durante a implementação; CI fica restrita aos gates aprovados.

9. Analytics exige opt-in e permite revogação

Aplicar consentimento a todos os visitantes, sem geolocalização de IP: é menor, previsível e não arrisca carregar GA antes de uma decisão quando a obrigação legal se aplica.

  • GoogleAnalytics começa sem <Script> e sem requisição a Google.
  • Persistir somente "granted" ou "denied" em localStorage sob uma chave versionada.
  • Exibir ações “Aceitar analytics” e “Recusar” quando não há escolha.
  • Manter um controle “Preferências de privacidade” acessível após a escolha.
  • Somente granted monta https://www.googletagmanager.com/gtag/js?id=G-QTT9QT0T3S e a configuração de GA.
  • Consentimento de publicidade permanece negado; somente analytics_storage pode ser concedido.
  • Revogação chama gtag("consent", "update", ...) com estados negados, define window["ga-disable-G-QTT9QT0T3S"] = true, expira cookies _ga e _ga_* visíveis no domínio atual e desmonta o loader.
  • Remover todos os rewrites de analytics/ad-block bypass e usar endpoints oficiais diretamente.

A ordem segue a orientação oficial do Google: default negado antes de coleta, update após escolha e persistência/revogação controladas pela aplicação.

10. CSP e cache mudam somente quando compatíveis

Não introduzir nonces nesta rodada. A documentação do Next informa que nonces exigem renderização dinâmica, desabilitam otimização estática/ISR e alteram cache/custo; isso conflita com a home estática e seria endurecimento desproporcional.

  • Manter CSP em Report-Only.
  • Adicionar default-src 'self' e form-action 'self'.
  • Remover unsafe-eval em produção; mantê-lo somente em desenvolvimento, onde o Next documenta a necessidade para debugging.
  • Manter unsafe-inline por enquanto para compatibilidade observada com scripts/estilos do Next e JSON-LD inline.
  • Manter origens Google apenas porque GA opt-in ainda as usa.
  • Remover regras immutable para /static, /fonts, /assets e /logos; esses nomes públicos não têm hash garantido. Assets gerados pelo Next em /_next/static já recebem cache apropriado do framework.

11. Documentação será derivada do estado final

  • README troca badge Next 15 por 16.3.0, remove Lenis e bibliotecas apagadas, documenta Node 22/pnpm 9.15.9, scripts reais, arquitetura e validação.
  • docs/project-health-cron.md será substituído por docs/project-health.md, descrevendo snapshot opcional, atualização manual/desenvolvimento e a garantia de cards sempre visíveis.
  • Deployment documentado no README usa somente nixpacks.toml: Node 22, pnpm 9 e lock congelado.
  • Nenhum dado comercial, cliente, métrica, review, endereço ou claim novo será criado.

Não-objetivos

  • Redesenhar hero, paleta, tipografia, navegação ou arquitetura de informação.
  • Remover notebook 3D, React Three Fiber, Drei, Three, cobe, Motion ou GSAP usado pelo notebook.
  • Criar backend, banco, CMP externo ou coleta analytics própria.
  • Migrar para outro gerenciador de pacotes ou outra plataforma de deploy.
  • Alterar conteúdo, metadata, JSON-LD, sitemap ou robots por pesquisa SEO/GEO externa. Essa integração terá spec e plano separados.
  • Atualizar o snapshot do usuário em data/project-health.json.
  • Importar ou consolidar o globals.css não rastreado da raiz.
  • Endurecer CSP com nonce/hash sem uma prova separada de compatibilidade e impacto de renderização.

Riscos e mitigação

Risco Impacto Mitigação
Saúde antiga desabilita uma demo já recuperada Médio Exibir timestamp/estado como última verificação e manter atualização manual/dev; nunca ocultar card
Alterar o tipo de projeto quebra notebook/home Alto Introduzir tipo por interseção compatível, migrar callers no mesmo checkpoint e rodar testes focados
Remoção de wrappers muda composição/hidratação Alto Remover um wrapper por corte, verificar foco, rotas e console no browser após o shell
Remoção de efeitos achata identidade Médio Preservar hero command center, globo, starfield, notebook, Motion local e CSS visual; comparar desktop/mobile
CSS reduced motion excessivamente amplo oculta conteúdo Alto Testar com emulação reduce; regras mudam duração, não display/opacity final
Revogação não interrompe GA já carregado Alto Atualizar consent mode para denied, ativar flag ga-disable, expirar cookies e testar ausência de novas requisições após revoke
CSP passa a bloquear runtime/GA Alto Permanecer Report-Only e remover apenas unsafe-eval em produção; observar console/network antes de enforcement futuro
Lock regenerado reintroduz advisory Alto Auditar prod/dev após frozen install e atualizar cadeia proprietária sem overrides/force
Exclusão alcança arquivo ainda importado Alto Buscar referências antes de cada lote e exigir tsc no checkpoint de corte
Alterações do usuário são sobrescritas Alto Tratar quatro arquivos listados como baseline; não tocar nos dois fora de escopo e usar edições ancoradas nos dois em escopo

Rollback

Rollback é incremental, sem Git destrutivo:

  1. Antes de cada tarefa, ler e registrar os arquivos exatos daquele lote.
  2. Se o checkpoint falhar, interromper o lote seguinte.
  3. Restaurar somente os arquivos do lote com edições inversas baseadas no snapshot pré-tarefa.
  4. Para arquivos novos, removê-los somente se foram criados pelo lote e ainda não são consumidos.
  5. Para dependências, restaurar juntos package.json, pnpm-lock.yaml, tailwind.config.js e nixpacks.toml; nunca restaurar apenas o lock.
  6. Reexecutar o último checkpoint verde.
  7. Nunca incluir data/project-health.json, globals.css da raiz ou mudanças não relacionadas no rollback.

Critérios observáveis de aceite

  1. Home e /cases continuam exibindo cards independentemente de snapshot ausente, antigo ou com falha.
  2. Demo explicitamente indisponível não navega e tem estado acessível; estado desconhecido não remove o URL nem o card.
  3. Produção não faz fetch de demos durante render; desenvolvimento pode atualizar em memória e falha de rede não esvazia conteúdo.
  4. Skip link recebe foco visível imediatamente, sem splash sobreposto.
  5. Links da 404 exibem as cores Tailwind pretendidas e foco visível.
  6. Reduced motion mostra todo conteúdo essencial sem scroll suave, loops ou transições longas.
  7. Loops restantes deixam de atualizar quando seção está fora da viewport ou document.hidden é verdadeiro.
  8. Hero mostra o mesmo conteúdo command-center sem infraestrutura de variantes mortas.
  9. Não há imports para os arquivos excluídos e TypeScript resolve o workspace sem erros.
  10. package.json contém versões exatas, Node 22.x, pnpm 9.15.9 e Next 16.3.0; lock não contém overrides.
  11. Instalação Nixpacks e local usa frozen lockfile com pnpm 9.
  12. pnpm audit --prod --audit-level=high e pnpm audit --dev --audit-level=high encerram com código zero, sem force, ignore ou override.
  13. Antes de consentimento/rejeição não há request a googletagmanager.com/google-analytics.com nem cookie _ga* criado pela página.
  14. Aceite carrega GA diretamente; revogação nega consentimento, desabilita coleta futura e expira cookies acessíveis.
  15. Configuração não contém rewrites de analytics; CSP continua Report-Only compatível e cache immutable não cobre arquivos públicos sem hash.
  16. CI executa install congelado, Biome, tsc, Jest, build e auditorias prod/dev.
  17. README e documentação de project health descrevem comandos, versões e comportamento finais sem depender de cron.
  18. globals.css da raiz e data/project-health.json permanecem inalterados.

Fontes técnicas