Dashboard
A tela /surveys/[id]/dashboard é a leitura analítica de uma pesquisa.
O Server Component chama o Lambda dashboard-processor, que devolve o
payload do Neurocalc — ou diretamente do cache em mydb.survey_cache,
quando válido. As cinco sub-rotas (geral · organização · por grupo · ranking ·
individuais) consomem o mesmo payload via DashboardContext e recalculam
filtros no client, sem nova chamada ao Lambda.
Onde mora o módulo
A tela vive em app/(main-noheader)/surveys/[id]/dashboard/. O route
group (main-noheader) indica que o header global do app não é
montado — é uma view imersiva, mesma escolha do módulo de
Gráfico. A entrada é autenticada (middleware exige
sessão Supabase) e o consumo do Lambda parte de um Server Component.
| Arquivo | Responsabilidade |
|---|---|
app/(main-noheader)/surveys/[id]/dashboard/page.tsx |
Server Component. Apenas redireciona para /surveys/[id]/dashboard/geral. |
app/(main-noheader)/surveys/[id]/dashboard/layout.tsx |
Server Component. Resolve params.id, chama fetchSurveyDashboardById envolvido em cache() do React e injeta o payload em <DashboardProvider>. Renderiza <NavBarWrapper> e as children das sub-rotas. |
app/(main-noheader)/surveys/[id]/dashboard/loading.tsx |
Fallback do Suspense. Mostra apenas Carregando… centralizado. |
app/(main-noheader)/surveys/[id]/dashboard/{geral,organizacao,por-grupo,ranking,individuais}/page.tsx |
Client Components. Cada um consome useDashboard() e delega para o cliente específico da aba em components/Dashboard/<Aba>/ClientComponent/. |
app/(main-noheader)/surveys/[id]/dashboard/individuais/[participantId]/page.tsx |
Detalhe de um respondente. Renderiza IndividualsSingleClient; o NavBarWrapper esconde a barra de abas neste path (ver Cap. 09). |
services/dashboard.service.ts |
Único caller do Lambda. Lê NEXT_PUBLIC_DASHBOARD_PROCESSOR_URL, valida a sessão, faz POST com { surveyId }. |
contexts/DashboardContext.tsx |
Provider DashboardProvider e hook useDashboard(). Expõe apenas surveyRaw. |
components/Dashboard/ |
Subdiretórios por aba (Geral/, Organization/, PerGroup/, Ranking/, Individuals/), mais NavBar/, FiltersPanel/ e Shared/ (CustomLegend, Modal). |
types/dashboard.type.ts |
Tipo SurveyDashboard + sub-tipos (OverallRating, QuestionData, SynchronyBreakdown, QuestionParticipantConnection). |
utils/dashboard-values.ts |
Paleta de cores do Neurocalc (COLORS com sync_high/mid/low e async equivalentes; CATEGORICAL_COLORS para atributos). Constantes de tipografia dos gráficos. |
../lambda/dashboard-processor/src/index.ts |
Handler HTTP do Lambda. Decide cache hit × recalc, monta dataset, chama Neurocalc, persiste em mydb.survey_cache. |
mydb.survey_cache |
Cache do payload. Colunas neurocalc_result jsonb e graph_result jsonb coexistem na mesma linha (a mesma tabela serve o graph-processor). |
Composição da tela
O layout monta <NavBarWrapper> no topo (com um botão
Voltar para pesquisas apontando para /surveys e cinco
abas) e renderiza a sub-rota ativa logo abaixo. Cada aba é uma página Client
que lê useDashboard() e delega para um client component
próprio em components/Dashboard/<Aba>/ClientComponent/.
| Aba | Componentes principais | O que renderiza e quais dados consome |
|---|---|---|
Geral/geral |
GeralDashboardClient → SimpleHorizontalBarChart (Recharts) |
Distribuição dos respondentes por overall_rating.areas ou attribute_N. Filtros locais: participantes (total / answered / notAnswered) e seletor de grupo (área / atributo 1–7). |
Organização/organizacao |
OrganizationDashboardClient → 2× MainVerticalStackChart |
Dois stacked bars verticais sobre question_*.relations: o primeiro mostra a quebra macro (sincronia / assincronia / PCR), o segundo detalha alto/médio/baixo. Seletor de pergunta. |
Por grupo/por-grupo |
PerGroupDashboardClient → TopHorizontalStackChart + BottomTableChart |
O topo mostra sincronia within-group a partir de relations_by_attribute_N; a tabela inferior é um heatmap de sincronia inter-grupos a partir de inter_relations_by_attribute_N, colorida em gradiente. Filtros: pergunta, atributo, critério (Síncrono / Assíncrono / PCR), ordem, toggle de categorias ocultas. |
Ranking/ranking |
RankingDashboardClient → ColorfulHorizontalBars + CustomLegend |
Barras horizontais ordenadas por sync_total ou sync_high_mid calculados sobre question_*.connections_ranking. Multi-seleção de atributos (filtra participantes), ordem ASC/DESC, slider de limite, separação de ranking positivo × negativo via in_degree vs out_degree. Cor de cada barra deriva do valor do atributo selecionado. |
Individuais/individuais |
IndividualsListClient (lista) → IndividualsSingleClient (detalhe) |
Lista paginada (10/página) de respondentes com busca por nome e filtros de atributo; cada card linka para /individuais/[participantId]. O detalhe renderiza DisplayIndividualChart (NewHorizontalBarStack + NewVerticalBarStack) com a quebra de conexões do participante e a sua participação dentro de cada grupo. |
Reusados em quase todas as abas: FiltersPanel (lista de checkboxes
multi-seleção por valor de atributo, ignorando vazios e "null"),
CustomLegend (legendas com headers e tamanho configurável) e
Shared/Modal. As cores do Neurocalc vivem em
utils/dashboard-values.ts: sync_high/mid/low em tons
de azul, async_high_null/high_low/mid_null em laranja/marrom,
pcr em cinza neutro.
Estado e filtros
DashboardContext é deliberadamente magro. O único valor exposto
é o payload bruto do Lambda — não há estado global de filtros, não há
cache de derivações, não há setters. Cada aba mantém o próprio
useState + useMemo e recalcula tudo no client a
partir do mesmo surveyRaw.
type DashboardContextType = {
surveyRaw: SurveyDashboard | null;
};
export function DashboardProvider({ children, surveyData }: {
children: ReactNode;
surveyData: SurveyDashboard | null;
}) {
return (
<DashboardContext.Provider value={{ surveyRaw: surveyData }}>
{children}
</DashboardContext.Provider>
);
}
useDashboard() lança erro se chamado fora do provider — ou seja,
as páginas das sub-rotas dependem do layout.tsx ter executado o
fetch antes. Como o layout é assíncrono, o Next aguarda a resolução antes de
renderizar as children: as abas nunca recebem surveyRaw em
estado pendente, só null (erro/sem sessão) ou objeto completo.
Mudar qualquer filtro — pergunta, atributo, critério, ordenação, paginação — é cálculo no client. Não dispara nova chamada ao Lambda. Isso mantém a tela barata e responsiva, mas significa que dados frescos só aparecem em recarregamento de página (ou em reentrada após o cache ter sido invalidado por um trigger; ver Cap. 05).
Fluxo de dados
Em ordem, do request HTTP até a renderização:
-
Usuário entra em
/surveys/[id]/dashboard/<aba>. O middleware autentica a sessão Supabase. -
layout.tsx(Server Component) resolveparams.id, chamagetCachedSurveyData(Number(id))— uma versão envolvida emcache()do React, para deduplicar entre o layout e qualquer chamada concomitante. -
fetchSurveyDashboardByIdemservices/dashboard.service.tschamasupabase.auth.getUser(). Sem usuário ou semNEXT_PUBLIC_DASHBOARD_PROCESSOR_URLconfigurada, retornanull. -
POSTao Lambdadashboard-processorcom{ surveyId }. O service não enviaforceRefreshem nenhum caminho (ver Cap. 07 e Cap. 09). -
Lambda decide cache hit × recálculo (ver Cap. 06) e devolve o JSON do
Neurocalc com um campo
_cache_infoindicando a origem (database_cacheoulambda_neurocalc). -
O service serializa o retorno como
SurveyDashboard | nulle devolve para o layout. Qualquer erro de rede, status não-OK ou exceção é engolido notry/catch— o layout recebenull. -
DashboardProviderenvolve as children comsurveyRaw; a aba ativa lê viauseDashboard()e renderiza.
const DASHBOARD_PROCESSOR_URL = process.env.NEXT_PUBLIC_DASHBOARD_PROCESSOR_URL;
export async function fetchSurveyDashboardById(
surveyId: number,
): Promise<SurveyDashboard | null> {
const supabase = await createClient();
const { data: { user }, error } = await supabase.auth.getUser();
if (error || !user) return null;
if (!DASHBOARD_PROCESSOR_URL) return null;
const response = await fetch(DASHBOARD_PROCESSOR_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ surveyId }),
});
if (!response.ok) return null;
return (await response.json()) as SurveyDashboard | null;
}
O cache() do React no layout garante que, num mesmo request, se
algo mais precisar do mesmo payload, não faz duas chamadas ao Lambda. Isso é
relevante quando uma sub-rota deseja acessar o payload durante a sua própria
renderização — mas no padrão atual ela apenas lê do contexto.
mydb.survey_cache e public.survey_cache
Existem duas tabelas físicas chamadas survey_cache
no banco — uma em mydb, outra em public — com
schemas idênticos. O Lambda do dashboard escreve e lê apenas a versão de
mydb. A versão de public coexiste com uma policy de
RLS bem escopada por papel e não é consumida no fluxo do dashboard.
| Tabela | Quem lê/escreve | Policy de SELECT |
|---|---|---|
mydb.survey_cache |
Lambda dashboard-processor e graph-processor via service role (bypassa RLS). |
auth.role() = 'authenticated' — qualquer login lê tudo via PostgREST com schema('mydb'). Plus policy ALL para service_role. |
public.survey_cache |
Ninguém no fluxo do dashboard. | survey_cache_select_consolidated: ADMIN (existe em mydb.users) lê tudo; SURVEYOR lê só linhas onde surveys.company_id = surveyor.company_id; VIEWER lê só o seu survey_id em mydb.survey_viewers. |
A policy de public.survey_cache é a única RLS realmente
escopada por papel em todo o projeto (ver
Cap. 02 — Schema do Backend).
Como o Lambda usa service role, essa escopagem não está no
caminho crítico do dashboard — está pronta caso o frontend passe a ler
via PostgREST, mas hoje é defesa em profundidade ociosa.
Schema das duas tabelas (5 colunas):
- survey_id
integer, primary key. Identifica a pesquisa.- neurocalc_result
jsonb, payload do dashboard.NULLindica cache invalidado.- graph_result
jsonb, payload do gráfico. Mesma linha, coluna distinta — ograph-processorusa este campo.- created_at
timestamp, defaultnow(). Atenção: o upsert do Lambda re-grava este campo a cada recálculo, perdendo a data de criação original.- updated_at
timestamp, defaultnow(). Atualizado em cada upsert e em cada invalidação por trigger.
Não há TTL nem job de limpeza. População atual (consulta MCP em 2026-05-22):
31 linhas em mydb.survey_cache, uma por pesquisa que já passou
pelo Lambda. Existe também mydb.survey_processing_results com
coluna processing_status default 'pending' — a
função de invalidação marca 'stale' lá também, mas nenhum
Lambda lê o flag (já documentado em Cap. 08
— Cache & invalidação do Neurocalc).
Função de invalidação
Dois triggers AFTER INSERT OR UPDATE OR DELETE FOR EACH ROW
apontam para a mesma função mydb.invalidate_survey_cache():
trigger_invalidate_cache em mydb.survey_responses e
trigger_invalidate_cache_respondents em
mydb.survey_respondents. A função zera as duas colunas
jsonb da linha da pesquisa correspondente.
-- Resolve survey_id direto (survey_respondents) ou via join (survey_responses)
IF TG_TABLE_NAME = 'survey_responses' THEN
SELECT survey_id INTO v_survey_id
FROM mydb.survey_respondents
WHERE id = COALESCE(NEW.respondent_id, OLD.respondent_id);
END IF;
UPDATE mydb.survey_cache
SET graph_result = NULL,
neurocalc_result = NULL,
updated_at = NOW()
WHERE survey_id = v_survey_id
AND (graph_result IS NOT NULL OR neurocalc_result IS NOT NULL);
UPDATE mydb.survey_processing_results
SET processing_status = 'stale', updated_at = NOW()
WHERE survey_id = v_survey_id AND processing_status <> 'stale';
Invalidação NULLa as colunas, não deleta a linha. Um
select * from mydb.survey_cache continua mostrando a entrada
após a invalidação — o sinal de stale é
neurocalc_result IS NULL. O Lambda usa exatamente essa
condição: getCachedNeurocalcResult retorna null
quando !data?.neurocalc_result, forçando o recálculo no
próximo POST.
As escritas que disparam essa invalidação vêm da edge save-responses
(única origem de gravação em survey_responses e de atualização
de finished_filling; ver
Cap. 07 — Integração da Resposta
Pública) e da edge delete-survey, que apaga em cascata. Mudanças
em mydb.survey_respondents via edges de gerenciamento também
disparam, mas essas edges estão dormentes (ver
respondents.html).
Lambda dashboard-processor
Catálogo geral, deploy e secrets vivem em
Cap. 02 — Lambdas HTTP. Aqui o foco é o que o
handler faz entre receber o surveyId e devolver o JSON.
Entrada e validação
O handler aceita apenas POST. O corpo é parseado como JSON; se
falhar, devolve 400. surveyId é convertido para
número e validado contra NaN. forceRefresh é
opcional e default false.
| Status | Quando ocorre |
|---|---|
405 | Método diferente de POST. |
400 | body não-JSON ou surveyId ausente/inválido. |
200 (cached) | Cache hit em mydb.survey_cache com neurocalc_result não-nulo. |
200 (empty) | Pesquisa sem respondentes — retorna { survey: {}, respondents: {}, responses: {} } e não persiste no cache. |
200 (computed) | Recálculo completo. Persiste em mydb.survey_cache via upsert. |
500 | Erro em qualquer um dos fetchSurvey, fetchRespondents, fetchAllResponses ou exceção no Neurocalc. |
Decisão cache × recálculo
const forceRefresh = body.forceRefresh === true;
if (!forceRefresh) {
const cached = await supabaseService.getCachedNeurocalcResult(surveyId);
if (cached) {
return ok({
...cached.neurocalc_result,
_cache_info: { cached: true, cached_at: cached.cached_at, source: "database_cache" },
});
}
}
// ...recalcula e persiste com onConflict: "survey_id"
getCachedNeurocalcResult faz
client.schema("mydb").from("survey_cache").select("neurocalc_result, updated_at").eq("survey_id", surveyId).maybeSingle()
e devolve null se a linha não existe ou se
neurocalc_result é NULL (cache invalidado).
No caminho de recálculo: lê surveys, survey_respondents e
survey_responses (paginação 500 em 500 via range()),
constrói o dataset com respostas codificadas como número concatenado
(formato detalhado em neurocalc.html),
sobe o dataset bruto em
Storage no bucket relatorios como
dataset-{surveyId}.json (falha não-fatal), resolve nomes de
atributos via RPC get_survey_attributes, chama
createJSON(dataset) em utils/neurocalc.ts e faz
upsert do resultado em mydb.survey_cache com
onConflict: "survey_id".
Credenciais
O Lambda lê SUPABASE_URL da env e o nome do secret de
SUPABASE_SECRET_NAME (default supabase-service-key),
busca o valor no AWS Secrets Manager com cache em memória
de 5 minutos, e cria um cliente Supabase com service role. Isso
bypassa qualquer RLS — incluindo a policy escopada de
public.survey_cache descrita no Cap. 05.
Formato de retorno
Chaves do topo do payload devolvido ao frontend:
- question_1..question_N
- Métricas por pergunta. Inclui
text,relations(breakdown sync/async/PCR),connections_ranking(por participante),relations_by_attribute_Neinter_relations_by_attribute_N. Detalhe em neurocalc.html Cap. 05. - survey_info
{ id, title, start_date, end_date, created_at }— espelho dos metadados demydb.surveys.- overall_rating
{ total, answered, notAnswered, areas, attribute_1..7, whoDidntAnswered }— contagens agregadas da pesquisa, base das telas Geral e Individuais.- attribute_names
- Mapa
attribute_N → rótuloresolvido via RPCget_survey_attributes. Defaults caem em "Atributo N" quando a RPC não retorna. - _cache_info
{ cached, cached_at | processed_at, source }.sourceé"database_cache"em hit ou"lambda_neurocalc"em recálculo. O frontend não consome esse campo no fluxo atual.
CORS é gerenciado pela configuração da Function URL no CDK; o handler
não emite cabeçalhos Access-Control-Allow-*. Logs vão
para CloudWatch.
Permissões por papel
Não há UI condicional por papel dentro do dashboard. ADMIN, SURVEYOR e
VIEWER veem as mesmas abas e os mesmos filtros. A diferença é qual
survey_id cada um consegue acessar — gateada pela camada de
rota, não pelo dashboard em si.
| Papel | Acesso a /surveys/[id]/dashboard/* |
forceRefresh |
|---|---|---|
| ADMIN | Qualquer pesquisa do produto. | Não disparável: o service não envia o flag. |
| SURVEYOR | Apenas pesquisas da própria empresa (surveys.company_id = surveyor.company_id). |
Não disparável. |
| VIEWER | Apenas o survey_id ao qual está vinculado em mydb.survey_viewers. O acesso geral a análise (excel, grafo, dashboard) é o único caminho desse papel — ver Cap. 06 da Autenticação. |
Não disparável. |
| Anônimo | Bloqueado pelo middleware (sem sessão → redirect para /login). Como reforço, o service também retorna null se supabase.auth.getUser() falhar. |
— |
A gate de "qual pesquisa cada papel pode abrir" não é feita pelo Lambda — o
Lambda aceita qualquer surveyId válido vindo de um service-role
client. A gate vive nas rotas pais e nas RPCs SECURITY DEFINER
que listam pesquisas. O dashboard apenas assume que, se a rota foi resolvida,
o usuário pode ler o payload.
Integração com outros módulos
-
Resposta Pública:
única origem de escrita em
mydb.survey_responsese emmydb.survey_respondents.finished_filling. Cada upsert disparatrigger_invalidate_cache, que NULLa oneurocalc_resultda pesquisa. A próxima abertura do dashboard recalcula. Ver Cap. 07 da página. -
Neurocalc: o motor de
sincronia/assincronia. O Lambda do dashboard apenas chama
createJSON()sobre o dataset montado a partir do banco. O algoritmo, as métricas por respondente, as métricas por grupo e o formato de saída moram emneurocalc.html. -
Gráfico: compartilha a mesma
linha em
mydb.survey_cache— escreve na colunagraph_result. A função de invalidação zera as duas colunas em conjunto: rasgar respostas re-processa também o grafo no próximo acesso. -
Relatórios (Excel): o
Lambda
excel-processorrecebe do client o JSON já calculado que o dashboard renderiza. Não há cache compartilhado nem nova chamada aodashboard-processor: o relatório gera a partir do mesmo payload exibido na tela. -
Respondentes: mudanças
em
mydb.survey_respondents(criar, desativar, excluir) disparamtrigger_invalidate_cache_respondentse invalidam o cache da pesquisa. Como as edges de gerenciamento estão dormentes, o caminho prático é exclusão viadelete-surveyou inserção pelo stepper de criação. -
Autenticação & Papéis:
a gate de "qual pesquisa cada papel pode ver" e a definição de papel
(derivada da tabela-perfil onde o
supabase_auth_idaparece) ficam aqui.
Pegadinhas
Comportamentos não-óbvios encontrados no código atual. Em ordem do mais ao menos visível em produção:
-
Falha silenciosa.
fetchSurveyDashboardByIdengole qualquer erro (sessão inválida, env não configurada, Lambda 500, timeout) comconsole.loge retornanull. O layout injetasurveyData={null}no provider, e as abas renderizam vazio sem toast, sem redirect e sem mensagem.useDashboard()não distingue "carregando" de "deu ruim". -
Pesquisa sem respondentes quebra o tipo. O Lambda
devolve
{ survey: {}, respondents: {}, responses: {} }nesse caso — semoverall_rating, semquestion_*, semattribute_names. O tipoSurveyDashboardassume essas chaves; qualquer acesso asurveyRaw.overall_rating.totalnuma pesquisa vazia estoura no client. Não há fallback documentado. -
forceRefreshórfão. O parâmetro existe no handler do Lambda e funciona quando disparado porcurl, mas o frontend não envia o campo em nenhum lugar. Nenhum botão UI dispara. O único caminho prático para invalidar o cache é via trigger — qualquer escrita emsurvey_responsesousurvey_respondentsda pesquisa. -
Cache stale como NULL, não como linha ausente. A função
de invalidação NULLa
neurocalc_resultegraph_resultsem apagar a linha. Quem auditar pela presença de linha não vê diferença; o sinal real é colunaNULL. A colunaupdated_até re-gravada pela própria invalidação, então não serve como prova de quando o cache passou a estar fresco. - Filtros não invalidam request. Mudar pergunta, atributo ou ordenação em qualquer aba é cálculo puro no client. O payload do Lambda é buscado uma única vez (no layout) e nunca refeito sem recarregar a página. Dados frescos só aparecem em F5 — e mesmo aí, só se o cache da pesquisa estiver invalidado.
-
NavBar some no detalhe individual.
NavBarWrappercheca o pathname contra/surveys/[id]/dashboard/individuais/\d+$e retornanullnesse caso. A view de detalhe é "modal-like" e a saída é via botão dedicado ou navegação do navegador. -
Dataset bruto vai para Storage a cada recálculo. O
Lambda chama
uploadDatasetToStorageemrelatorios/dataset-{surveyId}.jsonantes de gravar o cache; falha do upload é não-fatal. O arquivo é sobrescrito com a mesma chave e não consta caller no frontend — comportamento provável de debug/auditoria, não usado pelas telas. -
Duas tabelas
survey_cachecoexistem. Lambda escreve emmydb.survey_cache;public.survey_cachetem a RLS bem escopada (única do projeto) mas ninguém a lê no fluxo do dashboard. Não inferir do schema duplicado que existe sincronização — são tabelas independentes. -
survey_processing_results.processing_statusé flag morto. A função de invalidação marca'stale'lá também, mas nenhum Lambda lê o valor (já anotado em neurocalc.html Cap. 08). Mantido por compatibilidade. -
Pesquisa em rascunho renderiza dashboard. Não há gate
de
statusnem deend_dateno Lambda nem no service. Para qualquersurveyIdque tenha respondentes e respostas, o dashboard renderiza, independente do estado da pesquisa emmydb.surveys. -
UPSERT do Lambda re-grava
created_at.saveNeurocalcResultsempre passacreated_at: new Date().toISOString()no upsert. A cada recálculo a data de criação original é perdida —created_ateupdated_atficam praticamente iguais. Para auditar há quanto tempo a pesquisa tem cache, não confiar nessas colunas. -
Function URL pública sem JWT check. O Lambda valida
apenas
surveyIdeforceRefresh; não exige autenticação Supabase no handler. Quem descobrir a URL da Function URL pode chamar o endpoint com qualquersurveyIde pagar tempo de CPU viaforceRefresh: true. A frente do produto não envia o flag, mas a defesa fica restrita a obscuridade da URL.