feat(platform): streaming por seção com Suspense na home do dashboard - #182
Conversation
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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Kody Review CompleteGreat news! 🎉 Keep up the excellent work! 🚀
Kody Guide: Usage and ConfigurationInteracting with Kody
Current Kody ConfigurationReview OptionsThe following review options are enabled or disabled:
|
Code Review Completed! 🔥The code review was successfully completed based on your current configurations.
Kody Guide: Usage and ConfigurationInteracting with Kody
Current Kody ConfigurationReview OptionsThe following review options are enabled or disabled:
|
| 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; |
There was a problem hiding this comment.
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.
Contexto
A home do dashboard (
/[tenant]/dashboard) era um único Server Component que davaawaitem todas as consultas antes de renderizar qualquer coisa. O TTFB da página inteira era o da query mais lenta, e oloading.tsxda 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.mddo repositório). Issue de origem: #169.Closes #169
Mudanças
data.ts(novo) — sete loaders envolvidos emcache()do React, chaveados por(orgId, windowDays). Oito painéis compartilhamloadPayloadse quatro compartilhamloadPreviousPeriod; 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áawaitapenas nos loaders que usa e retornanullquando não há dado. Os componentes emsections/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.<Suspense fallback={null}>—ChangeAlertretornanullquando 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 pelonext devdo 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.tsxno mesmo commit em que os<Suspense>entram — dividir deixaria a rota quebrada entre PRs. Nenhuma função de cálculo (compute*) foi tocada, epage.tsxencolheu 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) enpm run buildpassam/[tenant]/dashboarde confirmar as mesmas seções, na mesma ordem, com os mesmos números da implementação anteriormembere confirmar que a seção Hyper Engineers não apareceWindowSelectore confirmar que todas as seções recalculam para a janela escolhidaQA 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: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
platform/src/app/[tenant]/dashboard/. Nenhuma outra rota, nenhuma query alterada, nenhuma função de cálculo tocada.<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 oSkeletonjá existente em@/components/ui/skeleton.Rollback
git revertdo 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
?window=), resolvida no shell e repassada às seçõessrc/app/[tenant]/repos/loading.tsx,src/app/[tenant]/compare/loading.tsxAutoria Assistida por IA
cache(), 13 painéis, skeletons, refatoração depage.tsxeloading.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.