Skip to content

Auditoria @audit — Header Novidades + Placeholders

Data: 2026-04-26 Escopo: especialista-headers/ (edge), header-novidades-especialista/ (UI), hooks useHeaders*, docs HEADER_NOVIDADES.md e CONTENT_SCHEDULING_PATTERN.md. Resultado:APROVADO — sem achados críticos. Dívida técnica pré-existente catalogada separadamente.


Metodologia

Aplicação dos 12 domínios do AUDIT.md sobre o feature de Header Novidades + Placeholders entregue nas Etapas 1–4.

Verificações automatizadas:

  • deno check em todas as Edge Functions tocadas
  • bun run build (frontend)
  • supabase test em especialista-headers/__tests__/42/42 passing
  • supabase linter (issues pré-existentes não relacionadas)

Resumo por Domínio

#DomínioStatusObservação
1LoggingCUDs cobertos por registrarLog(). Lazy reconciliation é best-effort idempotente — sem registrarLog é o padrão correto para mutações derivadas de leitura.
2IncidentesFalhas técnicas usam console.warn não-bloqueante; reads não logam.
3RBACrequireRole(['especialista','administrador']) + requireFeatureFlag('header_novidades') + SUB_FLAG_POR_ACAO (controle/programados/configuracoes). Cobertura validada em sub-flags-coverage.test.ts.
4RLS / silent failureUpdates seguem padrão .update().eq().select(); uso de createSupabaseSystem() documentado nos 3 pontos (busca de adesões e nome do criador) com referência ao ADR.
5Edge Function — estruturaCORS em todas as respostas, PUBLIC_ACTIONS antes do auth, formato {success, code, message}, 200 + FEATURE_DISABLED para pausa intencional, 503 reservado para erro real de leitura da flag.
6React QuerystaleTime definido (5–10 min), queryKey por contexto, invalidateQueries cross-cache (novidades ↔ placeholders ↔ ativos), optimistic updates com cancelQueries/rollback no padrão 4-hook.
7Toasts (frontend)Zero import from 'sonner', zero alert(), zero error.message cru. Tudo via olpToast + getUserFriendlyError.
8Backend error handlingInsert/update/delete verificam error; falhas em reconciliação não bloqueiam (best-effort + defesa em profundidade na RLS pública).
9React Query cacherefetchOnWindowFocus=false declarado em hooks, staleTime compatível com tabela canônica.
10Query chunking / N+1Sem .in() com arrays >100. Reorder usa loop sequencial pequeno (≤ N placeholders/headers, máx ~10 ordens).
10.5BatchingTela de gestão é tabbed (lazy mount once); requests separados por aba. Batch_init não justificado para esse volume.
10.6N+1Auto-toggle inverso usa 1 SELECT + 2 UPDATEs em massa + loop pequeno na reatribuição de ordem.
10.7Hook stabilityFunções dos hooks expostas são useMutation/useQuery (já estáveis). Sem useEffect em deps instáveis.
11Segurança geralInput validation server-side (Zod no programar/desprogramar), sem service_role no frontend, gates simétricos, ownership por sub-flag.
11.5IDORMutações restritas a especialista/admin; sem operação cross-escola exposta. Audiência de leitura por escola vinculada (consumo público) já com fail-close.
11.6Silent saveHooks expõem mutateAsync/onError para os componentes propagarem olpToast com getUserFriendlyError.
11.7Scoping permissõesn/aFeature não toca usuarios_escola_permissoes.
11.8Canary priorityFlags header_novidades (gestão) e header_novidades_consumo (consumo) segregadas; sub-flags por aba para controle granular.
12Enum / data integrityTipos RowHeaderNovidade e RowHeaderPlaceholder derivados de Database types. UI usa enums das migrations.

Pontos Fortes Observados

  1. Lazy Reconciliation Inversa (placeholders ↔ novidades) — padrão limpo, idempotente, com snapshot persistente em ativo_antes_auto_off. Cobre as 4 transições sem cron novo nem janela de inconsistência.
  2. Semântica HTTP correta para flags pausadas200 + code:'FEATURE_DISABLED' evita retry no client e diferencia pausa intencional de incidente. Reservou 503 apenas para falha real de leitura da flag.
  3. Defesa em profundidade temporallist_active aplica filtro temporal explícito (publicar_em <= now() AND despublicar_em > now()) além da RLS, garantindo que a janela seja respeitada mesmo se a reconciliação atrasar.
  4. Sub-flags por aba — admin pode pausar Controle/Programados/Configurações independentemente sem derrubar o módulo inteiro.
  5. Validação Zod server-side em programar_header (rejeita despublicar_em <= publicar_em).

Recomendações (Backlog — não bloqueantes)

  • 🟢 Considerar batch_init se a tela de gestão evoluir para >3 requests no mount (hoje são 3 separados por tab — lazy mount).
  • 🟢 Documentar visualmente (diagrama) o ciclo programado → ativo → desativado no HEADER_NOVIDADES.md.

Dívida Técnica Pré-existente (Não Relacionada)

Os seguintes itens foram observados durante o saneamento TS mas não são parte da feature auditada:

  • _shared/supabase-client.ts: incompatibilidade de tipos SchemaName em alguns casts (as any aplicado nas funções de auth/password). Solução de fundo seria atualizar tipagem do @supabase/supabase-js.
  • Linter Supabase reporta 6× RLS Enabled No Policy (categoria 5 do ADR-011 — tabelas escola_anotacoes, portal-*), 1× Function Search Path Mutable, 1× Extension in Public, 4× Public Bucket Allows Listing (banners/thumbnails — design intencional).

→ Catalogados para PR de saneamento separado.


Conclusão

Feature de Header Novidades + Placeholders aprovada sem achados críticos. Padrões do projeto totalmente respeitados:

  • SSOT: HEADER_NOVIDADES.md + CONTENT_SCHEDULING_PATTERN.md
  • Memórias internas (Lovable Memory, não expostas no VitePress): features/header-novidades/architecture, architecture/content-scheduling-pattern

Build limpo, 42/42 testes verdes, frontend sem regressões.


Adendo — Refator pós-feedback (2026-04-26, mesmo dia)

Após validação em produção, o usuário identificou que o auto-toggle inverso (reconciliarPlaceholdersVsNovidades) causava UX confusa: especialista ativava um placeholder na tela de Configurações e o backend o desativava na mesma request porque havia novidades ativas no sistema.

Mudanças aplicadas:

  1. Removido reconciliarPlaceholdersVsNovidades() e suas 11 chamadas em especialista-headers/index.ts. Especialista agora é fonte da verdade absoluta de headers_placeholders.ativo.
  2. Adicionado helper escolaTemHeaderVisivel(escolaId) que reaproveita o filtro de adesão de list_active.
  3. Modificado list_active_placeholders para aceitar params.escola_id e suprimir resultado quando a escola tem ≥1 header de novidade visível (decisão de consumo por escola, não mais global).
  4. Frontend: content-context.tsx envia escola_id e isola queryKey por escola. CardPlaceholder migrado para forwardRef (resolve warning React reportado no console). Banner explicativo da aba Configurações atualizado para refletir o novo modelo.
  5. Coluna deprecada: headers_placeholders.ativo_antes_auto_off mantida no schema (zero migration agora) — drop em PR de cleanup posterior.

Princípios reforçados:

  • SSOT: especialista decide ativo, backend apenas filtra no consumo.
  • Audiência por escola: mesma lógica de adesão (escola_olimpiadas status='ativa') entre list_active (fail-close) e list_active_placeholders (fail-open, intencional — placeholder neutro é menos prejudicial que fallback hardcoded).

Testes: placeholders-crud.test.ts atualizado com 3 novos asserts:

  • Helper de reconciliação NÃO pode reaparecer (regressão).
  • list_active_placeholders aceita escola_id.
  • escolaTemHeaderVisivel reaproveita filtro de adesão. Exit 0 em todos os 42+3 testes Deno.