Site do projeto de pesquisa (trabalho de graduandos em Estatística — UFMT) "Análise de Desempenho Acadêmico em Cálculo I e VGA — UFMT", construído com Next.js 16 (App Router), Supabase (Postgres + Auth) e pronto pra deploy na Vercel.
- 📊 Visualizações interativas (Chart.js) com dados reais carregados do banco
- 🏫 Painel admin protegido por senha (Supabase Auth em produção, senha demo local)
- 📝 CRUD de notas (adicionar, editar, excluir, filtrar aprovados/reprovados)
- 📥 Importador de CSV/TSV com pré-visualização e batch upsert com dedup
- 🌓 Modo demo — funciona sem Supabase, lendo
data/seed.json - 🔒 Row Level Security (RLS) — leitura pública, escrita só pra autenticados
- 📱 Responsivo (mobile-first)
- ⚡ SSG/SSR híbrido, deploy direto na Vercel
npm install
npm run dev
# http://localhost:3001Acesse /admin/login e use a senha ufmt2024 (ou o valor de ADMIN_DEMO_PASSWORD).
Modo demo é o estado padrão. Sem
NEXT_PUBLIC_SUPABASE_URL+NEXT_PUBLIC_SUPABASE_ANON_KEYsetadas, o site lê dedata/seed.json(committed, ~3KB) e roda sem Supabase. O painel admin aceita mutações em memória (UI otimista) mas não persiste nada no banco — é pra navegar e demonstrar, não pra uso sério.Para dados reprodutíveis fim-a-fim:
git clone + npm i + npm run devreproduz toda a análise sem credenciais. Isso é feature, não bug — é o que oPRODUCT.mdpede ("reprodutibilidade científica").
1. Crie um projeto no supabase.com
No SQL Editor do dashboard, execute o conteúdo de supabase/schema.sql.
Isso cria as tabelas, índices, view e policies de RLS.
Você tem duas opções:
(a) Via interface web (recomendado)
- Acesse
/admin→ faça login → use a aba "Importar CSV/TSV" para subir o arquivodata/seed.csv(gerado a partir da planilha original).
(b) Via SQL direto
-- Insere as notas da planilha (execute após rodar schema.sql)
insert into public.notas (disciplina_id, aluno_id, nota_final) values
('x-calculo1', 'aluno_1', 0.74),
('x-calculo1', 'aluno_2', 0.00),
-- ... etc
;No Supabase Auth → Users → "Add user" → email/senha. Esse será o login do painel.
Copie .env.example para .env.local e preencha (veja ./.env.example):
NEXT_PUBLIC_SUPABASE_URL=...
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ... # ou NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=sb_publishable_...
ADMIN_DEMO_PASSWORD= # vazio em produção (não usado se Supabase ativo)Em produção (Vercel), defina NEXT_PUBLIC_SUPABASE_URL e NEXT_PUBLIC_SUPABASE_ANON_KEY
em Environment Variables. Deixe ADMIN_DEMO_PASSWORD vazio — o caminho demo não executa
quando Supabase está ativo.
npm run build
npm startnpm i -g vercel
vercel link
vercel env add NEXT_PUBLIC_SUPABASE_URL
vercel env add NEXT_PUBLIC_SUPABASE_ANON_KEY
vercel --prodOu via dashboard: vercel.com/new → importe o repo → configure as 2 env vars acima → Deploy.
Quando você ativar o Supabase (setando NEXT_PUBLIC_SUPABASE_URL e
NEXT_PUBLIC_SUPABASE_ANON_KEY na Vercel), a página pública e o admin
passam a ler de public.notas no banco — não mais de data/seed.json
(o seed.json vira só fallback se o Supabase falhar).
Para popular o banco com as 215 notas da planilha original:
# 1. Rodar o schema uma vez no SQL Editor do Supabase
# (cria turmas, disciplinas, notas, view v_stats_disciplina, RLS)
psql "$SUPABASE_DB_URL" -f supabase/schema.sql
# 2. Seed via SECRET key (bypassa RLS — só local, nunca no browser)
XLSX_PATH=/path/Notas_Calc_VGA.xlsx \
SUPABASE_URL=https://xxx.supabase.co \
SUPABASE_SECRET_KEY=sb_secret_... \
npm run db:seedO script é idempotente (upsert onConflict): re-rodá-lo substitui
as notas existentes pelas da planilha sem duplicar. Útil para resetar
o DB a qualquer momento.
Após o seed, v_stats_disciplina no Supabase retorna as mesmas
médias, medianas e taxas de aprovação do resumo_expandido_formatado.docx.
API REST pública, somente leitura, versionada por path (/api/v1/), com envelope
consistente { data, meta } / { error: { code, message } }. Usa exclusivamente o client
anon key (RLS de leitura pública) e herda o fallback Supabase → seed.json: se o banco
cair, a API continua respondendo. Respostas são cacheadas na edge da Vercel
(s-maxage=3600, stale-while-revalidate=86400 — dados novos aparecem em até 1h após import).
CORS liberado para qualquer origem (Access-Control-Allow-Origin: *).
Especificação completa: public/openapi.yaml (OpenAPI 3.1).
| Endpoint | Descrição |
|---|---|
GET /api/v1/health |
Disponibilidade da API e do banco |
GET /api/v1/disciplinas |
Lista disciplinas (slug, nome, nº de registros) |
GET /api/v1/disciplinas/{slug}/estatisticas |
Estatísticas descritivas + histograma. Filtro: ?turma=x |
GET /api/v1/disciplinas/{slug}/registros |
Notas individuais anonimizadas. ?turma=, ?page= (≥1), ?limit= (1–200) |
GET /api/v1/estatisticas/comparativo |
Comparação lado a lado. ?disciplinas=calculo-1,vga (2–5 slugs), ?turma= |
GET /api/v1/disciplinas/{slug}/resumo-ia |
Resumo interpretativo gerado por IA (lido do banco). ?turma=. 404 NOT_GENERATED se nunca gerado |
Nota de modelagem: o dado não tem semestre letivo — tem turmas (
x,y). Os slugs de disciplina (calculo-1,vga) agregam todas as turmas; use?turma=para filtrar.
curl https://<dominio>/api/v1/disciplinas
curl "https://<dominio>/api/v1/disciplinas/calculo-1/estatisticas?turma=x"
curl "https://<dominio>/api/v1/disciplinas/vga/registros?page=1&limit=50"
curl "https://<dominio>/api/v1/estatisticas/comparativo?disciplinas=calculo-1,vga"Exemplo de resposta (/disciplinas/calculo-1/estatisticas):
{
"data": {
"disciplina": { "slug": "calculo-1", "nome": "Cálculo I" },
"turma": null,
"n": 103,
"media": 2.02,
"mediana": 0.4,
"desvio_padrao": 2.87,
"minimo": 0,
"maximo": 9.63,
"quartis": { "q1": 0, "q2": 0.4, "q3": 3.52 },
"taxa_aprovacao": 0.21,
"distribuicao": [ { "faixa": "0-1", "count": 66 }, { "faixa": "1-2", "count": 3 } ]
},
"meta": { "generated_at": "2026-07-09T12:00:00.000Z", "version": "v1" }
}Erros: 400 BAD_REQUEST (parâmetro inválido), 404 NOT_FOUND/NOT_GENERATED (recurso inexistente),
500 INTERNAL_ERROR. Tipos TypeScript das respostas em lib/api/types.ts.
Escrita continua restrita ao painel admin (não passa pela API pública).
O endpoint /resumo-ia serve resumos interpretativos gerados por LLM, mas nenhuma rota
pública chama o LLM. A arquitetura é gerar pouco, servir muito: um admin autenticado
dispara a geração no painel (/admin/resumos), uma Server Action busca as estatísticas
agregadas (nunca notas individuais), chama o provedor uma única vez e grava o resultado na
tabela resumos_ia (RLS: leitura pública, escrita só admin). O público lê do banco — custo
zero por visitante, latência baixa e a API key fora de alcance de tráfego externo.
A geração é idempotente via data_hash (SHA-256 das estatísticas): se os dados não mudaram,
a action reaproveita o resumo salvo sem chamar o LLM. O badge no admin usa o mesmo hash para
indicar resumo atualizado/desatualizado, e o campo atualizado do endpoint expõe isso ao consumidor.
O provedor é desacoplado em lib/ia/provider.ts (formato OpenAI /chat/completions via
fetch): trocar Groq por outro provedor é mudar LLM_BASE_URL/LLM_MODEL/LLM_API_KEY
no ambiente, sem tocar em código. Erros (incluindo 429 do free tier) viram mensagem
amigável no admin e nunca gravam conteúdo parcial.
curl "https://ufmt-stats.vercel.app/api/v1/disciplinas/calculo-1/resumo-ia"npm run hash-data # gera data/seed.json.sha256O hash do data/seed.json é commitado em data/seed.json.sha256 para permitir que revisores
verifiquem que os bytes exatos produzem os números exatos da análise. Para um paper com claims
publicáveis, isso é parte da reprodutibilidade científica.
app/
page.tsx # Página pública (home com hero, gráficos, etc.)
layout.tsx # Layout raiz com banner de modo demo
api/v1/ # API REST pública (health, disciplinas, estatísticas)
admin/
(auth)/
login/ # Tela de login (Supabase Auth ou senha demo)
(protected)/
page.tsx # Dashboard com KPIs
notas/ # CRUD de notas
turmas/ # Cadastro de turmas e disciplinas
importar/ # Importador de CSV/TSV com batch upsert
globals.css # Tokens OKLCH, brand, print, motion
components/
Navbar.tsx # Nav pública sticky
PrintHeader.tsx # Cabeçalho print-only
charts/
Charts.tsx # Boxplot, Histograma, Barras (Chart.js)
StatusBreakdown.tsx
Seo/
AcademicArticleJsonLd.tsx
data/
seed.json # Dados da planilha (fallback demo)
seed.json.sha256 # Hash SHA-256 do seed (reprodutibilidade)
lib/
api/ # Camada da API pública (envelope, validação Zod, queries)
data.ts # Camada de dados (lê seed.json ou Supabase)
stats.ts # Cálculos estatísticos (describe, approval, bin)
supabase.ts # Flag e env Supabase (server + client)
supabase-server.ts # Cliente Supabase server
supabase-browser.ts # Cliente Supabase browser
admin-auth.ts # Guard de rotas /admin/*
types.ts # Tipos compartilhados
scripts/
print-css-tweak.py # Script de ajuste de print (debug histórico)
docs/
CHANGELOG-impeccable.md # Histórico de polish de design
supabase/
schema.sql # Schema + RLS + view de stats
seed-notas.sql # Seed SQL opcional
proxy.ts # Matcher Next 16 para /admin/*
- Next.js 16.2.9 — App Router, Server Components, Server Actions
- React 19 + TypeScript strict
- Tailwind CSS 3.4 — utility-first, com tokens OKLCH customizados
- Supabase — Postgres + Auth + RLS (
@supabase/ssr) - Chart.js 4 — visualizações client-side com tokens OKLCH via
color-mix()
MIT — pesquisa acadêmica, dados anônimos.