Neuroredes
Módulos do Produto

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.

Capítulo 01

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).
Capítulo 02

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
GeralDashboardClientSimpleHorizontalBarChart (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
PerGroupDashboardClientTopHorizontalStackChart + 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
RankingDashboardClientColorfulHorizontalBars + 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.

Capítulo 03

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.

contexts/DashboardContext.tsx typescript
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).

Capítulo 04

Fluxo de dados

Em ordem, do request HTTP até a renderização:

  1. Usuário entra em /surveys/[id]/dashboard/<aba>. O middleware autentica a sessão Supabase.
  2. layout.tsx (Server Component) resolve params.id, chama getCachedSurveyData(Number(id)) — uma versão envolvida em cache() do React, para deduplicar entre o layout e qualquer chamada concomitante.
  3. fetchSurveyDashboardById em services/dashboard.service.ts chama supabase.auth.getUser(). Sem usuário ou sem NEXT_PUBLIC_DASHBOARD_PROCESSOR_URL configurada, retorna null.
  4. POST ao Lambda dashboard-processor com { surveyId }. O service não envia forceRefresh em nenhum caminho (ver Cap. 07 e Cap. 09).
  5. Lambda decide cache hit × recálculo (ver Cap. 06) e devolve o JSON do Neurocalc com um campo _cache_info indicando a origem (database_cache ou lambda_neurocalc).
  6. O service serializa o retorno como SurveyDashboard | null e devolve para o layout. Qualquer erro de rede, status não-OK ou exceção é engolido no try/catch — o layout recebe null.
  7. DashboardProvider envolve as children com surveyRaw; a aba ativa lê via useDashboard() e renderiza.
services/dashboard.service.ts typescript
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.

Capítulo 05

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.
Destaque

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. NULL indica cache invalidado.
graph_result
jsonb, payload do gráfico. Mesma linha, coluna distinta — o graph-processor usa este campo.
created_at
timestamp, default now(). Atenção: o upsert do Lambda re-grava este campo a cada recálculo, perdendo a data de criação original.
updated_at
timestamp, default now(). 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.

mydb.invalidate_survey_cache (resumo) sql
-- 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';
Atenção

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).

Capítulo 06

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
405Método diferente de POST.
400body 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.
500Erro em qualquer um dos fetchSurvey, fetchRespondents, fetchAllResponses ou exceção no Neurocalc.

Decisão cache × recálculo

lambda/dashboard-processor/src/index.ts typescript
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_N e inter_relations_by_attribute_N. Detalhe em neurocalc.html Cap. 05.
survey_info
{ id, title, start_date, end_date, created_at } — espelho dos metadados de mydb.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ótulo resolvido via RPC get_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.

Capítulo 07

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.

Capítulo 08

Integração com outros módulos

  • Resposta Pública: única origem de escrita em mydb.survey_responses e em mydb.survey_respondents.finished_filling. Cada upsert dispara trigger_invalidate_cache, que NULLa o neurocalc_result da 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 em neurocalc.html.
  • Gráfico: compartilha a mesma linha em mydb.survey_cache — escreve na coluna graph_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-processor recebe do client o JSON já calculado que o dashboard renderiza. Não há cache compartilhado nem nova chamada ao dashboard-processor: o relatório gera a partir do mesmo payload exibido na tela.
  • Respondentes: mudanças em mydb.survey_respondents (criar, desativar, excluir) disparam trigger_invalidate_cache_respondents e invalidam o cache da pesquisa. Como as edges de gerenciamento estão dormentes, o caminho prático é exclusão via delete-survey ou 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_id aparece) ficam aqui.
Capítulo 09

Pegadinhas

Comportamentos não-óbvios encontrados no código atual. Em ordem do mais ao menos visível em produção:

  1. Falha silenciosa. fetchSurveyDashboardById engole qualquer erro (sessão inválida, env não configurada, Lambda 500, timeout) com console.log e retorna null. O layout injeta surveyData={null} no provider, e as abas renderizam vazio sem toast, sem redirect e sem mensagem. useDashboard() não distingue "carregando" de "deu ruim".
  2. Pesquisa sem respondentes quebra o tipo. O Lambda devolve { survey: {}, respondents: {}, responses: {} } nesse caso — sem overall_rating, sem question_*, sem attribute_names. O tipo SurveyDashboard assume essas chaves; qualquer acesso a surveyRaw.overall_rating.total numa pesquisa vazia estoura no client. Não há fallback documentado.
  3. forceRefresh órfão. O parâmetro existe no handler do Lambda e funciona quando disparado por curl, 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 em survey_responses ou survey_respondents da pesquisa.
  4. Cache stale como NULL, não como linha ausente. A função de invalidação NULLa neurocalc_result e graph_result sem apagar a linha. Quem auditar pela presença de linha não vê diferença; o sinal real é coluna NULL. A coluna updated_at é re-gravada pela própria invalidação, então não serve como prova de quando o cache passou a estar fresco.
  5. 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.
  6. NavBar some no detalhe individual. NavBarWrapper checa o pathname contra /surveys/[id]/dashboard/individuais/\d+$ e retorna null nesse caso. A view de detalhe é "modal-like" e a saída é via botão dedicado ou navegação do navegador.
  7. Dataset bruto vai para Storage a cada recálculo. O Lambda chama uploadDatasetToStorage em relatorios/dataset-{surveyId}.json antes 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.
  8. Duas tabelas survey_cache coexistem. Lambda escreve em mydb.survey_cache; public.survey_cache tem 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.
  9. 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.
  10. Pesquisa em rascunho renderiza dashboard. Não há gate de status nem de end_date no Lambda nem no service. Para qualquer surveyId que tenha respondentes e respostas, o dashboard renderiza, independente do estado da pesquisa em mydb.surveys.
  11. UPSERT do Lambda re-grava created_at. saveNeurocalcResult sempre passa created_at: new Date().toISOString() no upsert. A cada recálculo a data de criação original é perdida — created_at e updated_at ficam praticamente iguais. Para auditar há quanto tempo a pesquisa tem cache, não confiar nessas colunas.
  12. Function URL pública sem JWT check. O Lambda valida apenas surveyId e forceRefresh; não exige autenticação Supabase no handler. Quem descobrir a URL da Function URL pode chamar o endpoint com qualquer surveyId e pagar tempo de CPU via forceRefresh: true. A frente do produto não envia o flag, mas a defesa fica restrita a obscuridade da URL.