Skip to content

feat(platform): streaming por seção com Suspense na home do dashboard - #182

Merged
clickmatos merged 1 commit into
mainfrom
feat/dashboard-streaming-por-secao
Aug 26, 2026
Merged

feat(platform): streaming por seção com Suspense na home do dashboard#182
clickmatos merged 1 commit into
mainfrom
feat/dashboard-streaming-por-secao

Conversation

@RamonFrancisco

Copy link
Copy Markdown

Contexto

A home do dashboard (/[tenant]/dashboard) era um único Server Component que dava await em todas as consultas antes de renderizar qualquer coisa. O TTFB da página inteira era o da query mais lenta, e o loading.tsx da rota era um skeleton genérico de página cheia que não correspondia ao layout real — desenhava 6 hero cards + 4 KPIs + um bloco + um split, e as ~14 seções apareciam todas de uma vez, com salto de layout.

Card: N/A — esta iniciativa é rastreada em GitHub Issues, não no Jira (ver CLAUDE.md do repositório). Issue de origem: #169.

Closes #169

Mudanças

  • data.ts (novo) — sete loaders envolvidos em cache() do React, chaveados por (orgId, windowDays). Oito painéis compartilham loadPayloads e quatro compartilham loadPreviousPeriod; sem dedupe seriam N round-trips idênticos por request.
  • panels/*.tsx (novo) — um Server Component async por seção (13 no total). Cada um dá await apenas nos loaders que usa e retorna null quando não há dado. Os componentes em sections/ não mudaram: continuam client components puramente de apresentação.
  • panels/skeletons.tsx (novo) — quatro formas reutilizáveis de fallback: linha de hero cards, grid de KPIs, seção simples (heading + bloco) e seção split (heading + dois blocos).
  • page.tsx — de 227 para ~180 linhas. O shell resolve apenas sessão → org → janela → membership → loadRepoSummaries (duas leituras em tabelas pré-agregadas, sem JSONB) e emite um <Suspense> por painel. A ordem no DOM é idêntica à anterior.
  • loading.tsx — reduzido ao shell (nome da org, contagem, seletor), já que cada seção agora carrega o próprio fallback.
  • Change alerts com <Suspense fallback={null}>ChangeAlert retorna null quando não há mudanças, que é o caso da maioria das orgs; um skeleton ali piscaria alertas fantasma a cada load.
  • platform/CLAUDE.md — bloco gerado automaticamente pelo next dev do Next 16 (generate-agent-files.js), que se re-adiciona a cada execução. Vai junto para manter a árvore limpa; não é edição manual.

Sobre o tamanho (705 linhas, acima do limite de 500 da RFC §2.4): o volume é majoritariamente extração mecânica, não lógica nova. As 13 seções precisam sair de page.tsx no mesmo commit em que os <Suspense> entram — dividir deixaria a rota quebrada entre PRs. Nenhuma função de cálculo (compute*) foi tocada, e page.tsx encolheu 147 linhas. Os 13 painéis novos somam ~230 linhas e a maioria tem menos de 20 cada.

Plano de Teste

  • npx tsc --noEmit, npm run lint, npm run test (267 testes) e npm run build passam
  • Abrir /[tenant]/dashboard e confirmar as mesmas seções, na mesma ordem, com os mesmos números da implementação anterior
  • Em ambiente com latência real, confirmar que o shell (nome da org + contagem + seletor) aparece antes das seções, e que cada seção preenche seu lugar sem empurrar o conteúdo abaixo
  • Confirmar que a região de alertas de mudança não exibe skeleton quando a org tem zero alertas
  • Abrir uma org com 0 repositórios e confirmar o empty state com CTA de conectar repositório
  • Logar com usuário de role member e confirmar que a seção Hyper Engineers não aparece
  • Trocar a janela de análise no WindowSelector e confirmar que todas as seções recalculam para a janela escolhida
  • Confirmar console sem erros e nenhuma resposta 4xx/5xx no happy path

QA já executado localmente (Supabase local, 2 repos reais ingeridos via POST /api/ingest, janelas 30d e 90d, Chrome via chrome-devtools MCP): 13 casos cobrindo os 8 critérios de aceite da #169, 13 passaram, 0 falhas. Medições com latência injetada:

t=109ms   28 skeletons (h-24x6 hero, h-28x8 KPI, h-72x2, h-64x2 split)
          já renderizados: IA vs Humano, Distribuição de intenção, Mapa de saúde, Linha do tempo, Hyper Engineers
t=3000ms  22 skeletons  (DORA resolveu)
t=4300ms   3 skeletons  (OrgPulse, DeliveryQuality, PRHealth resolveram)
t=7501ms   0 skeletons  (AIAgentUsage resolveu)

Dedupe verificado por pg_stat_statements: metrics/payloads = 1 call para 8 painéis; metrics/previous-period = 1 call para 4 painéis.

Impacto e Risco

  • Áreas afetadas: apenas platform/src/app/[tenant]/dashboard/. Nenhuma outra rota, nenhuma query alterada, nenhuma função de cálculo tocada.
  • Compatibilidade: sem mudança de contrato. Mesmos dados, mesmas seções, mesma ordem no DOM — verificado buildando a implementação anterior e comparando lado a lado (11 números-chave conferem).
  • Performance: é o ponto da mudança. O primeiro conteúdo deixa de esperar a query mais lenta. Com latência injetada de 3s, o tempo até o primeiro conteúdo caiu de 3,75s para 0,47s. O custo total das consultas não muda — o ganho é de ordem de renderização.
  • Segurança: nenhuma. A restrição de visibilidade por role (owner/admin para Hyper Engineers) continua resolvida no shell, antes de o boundary existir.
  • Acessibilidade (se UI): os componentes de seção não foram alterados. Os fallbacks são <div> decorativos sem texto, substituídos pelo conteúdo real — sem mudança de árvore de acessibilidade no estado final. Relatório da RFC 0025 não se aplica: nenhum componente de UI novo foi introduzido além dos blocos de skeleton, que reutilizam o Skeleton já existente em @/components/ui/skeleton.
  • Custo AWS (se infra): N/A — nenhuma mudança de infraestrutura.

Rollback

  • git revert do commit. A mudança é isolada a um diretório de rota e não tem migration, feature flag ou mudança de schema associada — reverter restaura o comportamento anterior imediatamente, sem passo manual.

Referências

Autoria Assistida por IA

  • Agente: Claude Opus 5 (1M context), via Claude Code
  • Escopo da assistência: desenho da abordagem, implementação completa (loaders com cache(), 13 painéis, skeletons, refatoração de page.tsx e loading.tsx) e execução do QA funcional contra os critérios de aceite da [FEAT] Streaming por seção + skeletons na home do dashboard #169.
  • Revisão humana: pendente — o autor humano revisa antes do merge. A geração assistida não dispensa code review.

A home era um único Server Component que dava await em todas as consultas
antes de renderizar qualquer coisa — o TTFB da página inteira era o da query
mais lenta, e o loading.tsx da rota era um skeleton genérico que não
correspondia ao layout real.

Agora o shell (nome da org, contagem de repos, WindowSelector) aguarda só
loadRepoSummaries — duas leituras em tabelas pré-agregadas — e cada seção
resolve dentro do próprio <Suspense>, com fallback na forma da seção que
substitui.

Os loaders em data.ts usam cache() do React para deduplicar por request:
oito painéis compartilham loadPayloads e quatro compartilham
loadPreviousPeriod, sem isso seriam N round-trips idênticos.

Change alerts ficam com fallback={null} porque a maioria das orgs tem zero
alertas — um skeleton ali piscaria alertas fantasma a cada load.

O bloco em platform/CLAUDE.md é gerado pelo `next dev` do Next 16 e se
re-adiciona sozinho; vai junto para manter a árvore limpa.

Closes #169

Assisted-by: Claude Opus 5 (1M context)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@RamonFrancisco RamonFrancisco added type: feature Nova funcionalidade ai-assisted PR gerado total ou parcialmente com auxílio de agente de IA (RFC 0028 §2.2.7) labels Aug 25, 2026
@vercel

vercel Bot commented Aug 25, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
clickbus-iris Ready Ready Preview Aug 25, 2026 7:15pm

Request Review

@kody-ai

kody-ai Bot commented Aug 25, 2026

Copy link
Copy Markdown

Kody Review Complete

Great news! 🎉
No issues were found that match your current review configurations.

Keep up the excellent work! 🚀

⚠️ Note: some checks couldn't be completed in this run, so auto-approval (when enabled) was skipped. Check the details at https://app.kodus.io/pull-requests and then comment @kody review on this PR to retry once the issue is resolved.

Kody Guide: Usage and Configuration
Interacting with Kody
  • Request a Review: Ask Kody to review your PR manually by adding a comment with the @kody start-review command at the root of your PR.

  • Validate Business Logic: Ask Kody to validate your code against business rules by adding a comment with the @kody -v business-logic command.

  • Provide Feedback: Help Kody learn and improve by reacting to its comments with a 👍 for helpful suggestions or a 👎 if improvements are needed.

Current Kody Configuration
Review Options

The following review options are enabled or disabled:

Options Enabled
Bug
Performance
Security
Business Logic

Access your configuration settings here.

@kody-ai

kody-ai Bot commented Aug 26, 2026

Copy link
Copy Markdown

Code Review Completed! 🔥

The code review was successfully completed based on your current configurations.

⚠️ Note: some checks couldn't be completed in this run, so auto-approval (when enabled) was skipped. Check the details at https://app.kodus.io/pull-requests and then comment @kody review on this PR to retry once the issue is resolved.

Kody Guide: Usage and Configuration
Interacting with Kody
  • Request a Review: Ask Kody to review your PR manually by adding a comment with the @kody start-review command at the root of your PR.

  • Validate Business Logic: Ask Kody to validate your code against business rules by adding a comment with the @kody -v business-logic command.

  • Provide Feedback: Help Kody learn and improve by reacting to its comments with a 👍 for helpful suggestions or a 👎 if improvements are needed.

Current Kody Configuration
Review Options

The following review options are enabled or disabled:

Options Enabled
Bug
Performance
Security
Business Logic

Access your configuration settings here.

Comment on lines +16 to +23
const [repos, payloads, contributors, previous] = await Promise.all([
loadRepoSummaries(orgId, windowDays),
loadPayloads(orgId, windowDays),
loadContributors(orgId, windowDays),
loadPreviousPeriod(orgId, windowDays),
]);

if (!repos.some((r) => r.stabilization_ratio !== null)) return null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kody code-review Bug medium

WHAT: OrgPulsePanel awaits loadPreviousPeriod (the heaviest JSONB query on the page, limit repos.length*15) plus loadPayloads/loadContributors BEFORE its no-data null gate. WHY: for organizations with zero stabilization_ratio (analyzed but no contributing data), the panel renders nothing yet still forces the most expensive query in loadPreviousPeriod to resolve on every page load. HOW: hoist the repos.some((r) => r.stabilization_ratio !== null) check to run after only loadRepoSummaries, and short-circuit the remaining three loaders when it is false.

const repos = await loadRepoSummaries(orgId, windowDays);
if (repos.length === 0 || !repos.some((r) => r.stabilization_ratio !== null)) return null;

const [payloads, contributors, previous] = await Promise.all([
  loadPayloads(orgId, windowDays),
  loadContributors(orgId, windowDays),
  loadPreviousPeriod(orgId, windowDays),
]);
Prompt for LLM

File platform/src/app/[tenant]/dashboard/panels/OrgPulsePanel.tsx:

Line 16 to 23:

WHAT: OrgPulsePanel awaits loadPreviousPeriod (the heaviest JSONB query on the page, limit repos.length*15) plus loadPayloads/loadContributors BEFORE its no-data null gate. WHY: for organizations with zero stabilization_ratio (analyzed but no contributing data), the panel renders nothing yet still forces the most expensive query in loadPreviousPeriod to resolve on every page load. HOW: hoist the `repos.some((r) => r.stabilization_ratio !== null)` check to run after only loadRepoSummaries, and short-circuit the remaining three loaders when it is false.

Suggested Code:

const repos = await loadRepoSummaries(orgId, windowDays);
if (repos.length === 0 || !repos.some((r) => r.stabilization_ratio !== null)) return null;

const [payloads, contributors, previous] = await Promise.all([
  loadPayloads(orgId, windowDays),
  loadContributors(orgId, windowDays),
  loadPreviousPeriod(orgId, windowDays),
]);

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-assisted PR gerado total ou parcialmente com auxílio de agente de IA (RFC 0028 §2.2.7) type: feature Nova funcionalidade

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEAT] Streaming por seção + skeletons na home do dashboard

2 participants