Mirobody
인간 건강의 표준 레지스트리, 약 2천 개 임상·생활 지표를 하나의 언어로 정리
디지털 각도계로 실시간 움직임 진폭을 측정·세션별 기록합니다. Etec Prof. Carmine Biagio Tundisi
A amplitude de movimento medida por um goniômetro digital, em tempo real, e acompanhada sessão a sessão. TCC da Etec Prof. Carmine Biagio Tundisi.
건강GitHub @NathanCarvalhodaCunha온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
저장소 정보는 아직 수집하지 않았습니다.
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
Sistema de acompanhamento de fisioterapia para clínicas - TCC de Ensino Medio Técnico.
저장소 README에서 가져옴
README 전체를 불러오는 중입니다.
README 전문 보기 ↗Sistema de acompanhamento de fisioterapia para clínicas: cadastro de pacientes e profissionais, registro de sessões e sincronização de medições de amplitude de movimento com um goniômetro digital.
Projeto de conclusão de curso (TCC) — Ensino Médio Técnico.
Site: rehabit.com.br
Login/ (autenticação) e Software/ (aplicação, pós-login).Cada tela tem uma variante de tema claro e uma escura (ex.: login.html / login-escuro.html), como páginas HTML estáticas separadas.
index.html Página inicial: apresentação do projeto (inicio.css, inicio.js, imagens em assets/inicio/)
Login/ Telas de autenticação (login, cadastro, esqueci a senha, redefinir senha)
Software/ Aplicação principal (dashboard, pacientes, sessões, dispositivo, configurações)
Firmware/ Código do goniômetro (ESP32 + MPU6050), teste de hardware e guias de montagem
rehabit-api/ Backend Spring Boot (API REST + banco H2 embarcado)
Rehabit.sql Script de referência do schema do banco
iniciar-rehabit.bat Compila e sobe o backend, depois abre o site
Pré-requisito: JDK 17+ instalado.
iniciar-rehabit.bat
O script compila o backend (Maven Wrapper, não precisa de Maven instalado), sobe a API em http://localhost:8080 e abre a tela de login no navegador padrão.
Para rodar manualmente:
cd rehabit-api/rehabit-api
mvnw.cmd -q package -DskipTests
java -jar target/rehabit-api-1.0.0.jar
E abra Login/login.html diretamente no navegador (o frontend é servido como arquivo local, sem servidor de desenvolvimento).
O botão Entrar na demonstração da página inicial abre
Login/login.html?demo com a conta da Ana Paula Ribeiro já preenchida
(Login/demonstracao.js): fisioterapeuta de
ortopedia da Clínica Movimento Fisioterapia, uma clínica completa para
apresentar e testar o sistema, com cinco fisioterapeutas de especialidades
diferentes e vinte e um pacientes com anamnese, metas, meses de sessões com a
evolução da amplitude e da dor (cada uma com a consulta em que o paciente
compareceu, que a aba Realizadas da agenda lista), faltas, uma consulta
remarcada e a agenda de hoje e das próximas duas semanas. As telas da página
inicial são as da Ana, que tem dez desses pacientes: de três a quatro
consultas por dia útil, então a semana dela aparece cheia — realizadas antes de
hoje, marcadas de hoje em diante.
A conta sobe sempre, sem variável nenhuma no Render (só
REHABIT_DEMO=false a desliga; quem já tinha REHABIT_DEMO=true segue igual).
A Ana tem foto: um retrato ilustrado, como os da conta da apresentação (veja
Fotos, abaixo), em src/main/resources/demonstracao/, posto depois de cada
montagem pelo mesmo caminho das fotos enviadas pelo site.
A conta fica sempre em dia. As datas são relativas ao dia em que ela é
montada: o histórico termina na véspera, e a agenda começa no próprio dia, com
as consultas de hoje marcadas. Como ninguém altera a conta, essas datas iam
ficando para trás — semanas depois, o histórico parava no dia da montagem, as
consultas "marcadas" eram de dias que já tinham passado e o início mostrava a
semana sem consultas e o mês sem sessões nem amplitude média. Por isso, na
subida da API e todo dia às 3h30 (horário de Brasília), a API confere a agenda
dela: se sobrou consulta marcada para um dia que já passou, a clínica é
apagada (com as fotos) e montada de novo com as datas do dia
(GeradorDeClinica.refazerSeVencida). Assim a semana tem consultas e o mês
tem sessões e amplitude média — menos nos primeiros dias do mês, antes da
primeira sessão dele, como numa clínica de verdade. Sem consulta no fim de
semana, a montada no sábado vale até segunda. Em dia, nada acontece — nem se a
API reiniciar no meio do dia; como a conta é só de leitura, refazê-la não
apaga nada que alguém tenha feito nela. Quem estiver logado na hora em que ela
é refeita precisa entrar de novo (a senha é a mesma). Se refazer falhar, nada
se perde: a API sobe assim mesmo, com a conta que já existia, avisa no log e
tenta de novo na madrugada seguinte (ou na próxima subida).
| Conta | |
|---|---|
| Clínica | [email protected] |
| Ana Paula Ribeiro (ortopedia) — a do botão | [email protected] |
| Rafael Moura (esportiva) | [email protected] |
| Juliana Castro (neurofuncional) | [email protected] |
| Carlos Eduardo Tanaka (reumatologia) | [email protected] |
| Fernanda Oliveira Lima (gerontologia) | [email protected] |
Todas entram com a senha de REHABIT_DEMO_SENHA (padrão: Rehabit@2026), e
essa senha é pública, porque o botão a preenche. Por isso a conta é só de
leitura: a API recusa com 403 qualquer alteração vinda da clínica de
demonstração ou de um profissional dela (JwtAuthenticationFilter), e as telas
mostram um aviso no topo. Se trocar REHABIT_DEMO_SENHA no Render, troque
também no demonstracao.js. Os e-mails usam o domínio
reservado .example, e os telefones são fictícios: para testar o lembrete no
WhatsApp de verdade, use uma conta comum.
O goniômetro da demonstração é simulado. Como a senha é pública, a conta
não pareia aparelho nenhum: com um goniômetro de verdade ali, qualquer
visitante mandaria Zerar ou Reiniciar para ele. Para a tela Dispositivo e o
registro de sessão não ficarem parados, o Software/goniometro.js simula o
aparelho no navegador (um movimento de abdução do ombro, no formato e no ritmo
do aparelho real), e os botões Zerar, Identificar, captura e Reiniciar agem
sobre a simulação. As telas avisam que é uma simulação. Para mostrar o aparelho
de verdade, use a conta da apresentação (abaixo) ou uma conta comum.
Para a banca do TCC (27/10/2026) há uma segunda clínica: a Clínica Alvorada
Fisioterapia, de Atibaia, com quatro fisioterapeutas e catorze pacientes. Os
pacientes têm anamnese, metas, meses de sessões, faltas, uma consulta
remarcada, altas e um inativo, e a agenda cobre as próximas três semanas, com
boa parte deles marcada às terças, de manhã e à tarde. Ela é montada pelo
mesmo gerador da demonstração
(GeradorDeClinica),
mas:
REHABIT_APRESENTACAO=false a desliga;rehabit.apresentacao.senha no application.properties, na clínica e nos
quatro profissionais. Ela está neste
repositório, que é público: para uma senha só da equipe, defina
REHABIT_APRESENTACAO_SENHA no Render (mínimo de 8
caracteres e fora da lista das senhas comuns). E ela volta sozinha: na
subida e toda madrugada, uma conta que esteja com outra senha — trocada pela
tela, por exemplo — volta para a da configuração, e só ela tem as sessões
encerradas;.example, como
os da demonstração, então não há recuperação de senha por e-mail (é por
isso que a senha volta sozinha). A primeira versão usava apelidos do Gmail
da equipe ([email protected]); uma conta criada com eles passa para
os e-mails novos no lugar, com tudo o que tem;ManutencaoDaClinica
na subida da API e todo dia às 3h40 (horário de Brasília). A consulta de um
paciente do roteiro que passou ainda marcada vira realizada, com a sessão,
a medição e o aviso no sino; e a agenda de quem está em tratamento anda, para
ter sempre três semanas pela frente — só com dias novos, então uma consulta
que a equipe cancelou não volta, e um paciente reativado não ganha consultas
do tempo em que estava de alta. No dia da banca, o histórico chega até a
véspera sem recriar nada. O que a equipe marcou, cancelou, remarcou ou
registrou pela tela fica como está, e paciente cadastrado por ela não entra
nisso.| Conta | |
|---|---|
| Clínica | [email protected] |
| Mariana Costa Bertolini (ortopedia) | [email protected] |
| Lucas Henrique Prado (esportiva) | [email protected] |
| Patrícia Yamamoto Reis (neurofuncional) | [email protected] |
| Renato Aguiar Fonseca (gerontologia) | [email protected] |
A equipe tem os acessos num PDF à parte, fora do repositório.
O log da subida mostra Clínica Alvorada Fisioterapia criada. A criação é
idempotente: se o e-mail ou o CNPJ da clínica já estão cadastrados, nada é
criado, e a conta nunca é apagada nem refeita pela API — mas, se alguém a
excluir pela tela, a madrugada seguinte (ou a próxima subida) a cria de novo.
A tabela tb14_conta_do_roteiro fixa qual é a clínica: na primeira vez, só
vale a que tem o e-mail e o CNPJ do roteiro (o CNPJ está neste repositório
público; o e-mail é .example, que o cadastro pela tela não aceita), e daí em
diante a manutenção segue o id dela, mesmo que a equipe troque o e-mail ou o
CNPJ pela tela. Só os profissionais dessa clínica são tocados: um e-mail do
roteiro que outra conta venha a usar fica de fora.
Fotos. A clínica tem um logo (o sol nascendo sobre a Pedra Grande) e cada
profissional, um retrato ilustrado — não são rostos de pessoas reais: estilo
Avataaars, de Pablo Stanley (livre para uso pessoal e comercial), gerado com a
biblioteca DiceBear. As imagens ficam em
src/main/resources/apresentacao/ e entram na conta pelo mesmo caminho das
fotos enviadas pelo site (tipo conferido, metadados removidos, gravação no
Cloudinary ou no disco). A subida põe a foto só onde falta: a que a equipe
trocou pela tela fica, e uma que sumiu do disco (servidor recriado sem
Cloudinary) volta. Depois do deploy, saia e entre de novo nas contas: o avatar
do cabeçalho vem do login.
As datas contam a partir do dia em que a conta é criada, e daí em diante ela segue atendendo (veja acima): não é preciso recriá-la antes da banca, nem parear o goniômetro de novo.
Os pacientes são fictícios, porque dado de saúde de pessoas reais não pode ir para um repositório público. Os telefones também são inventados. Para mostrar o lembrete no WhatsApp ao vivo, troque o telefone de um paciente pelo de alguém da equipe antes de começar.
O site fica em https://rehabit.com.br, publicado pela
Vercel (projeto rehabit, plano Hobby, entrando com o
GitHub NathanCarvalhodaCunha). Cada merge na main vai para o ar sozinho, e
cada branch enviado ganha um endereço de prévia *.vercel.app. A Vercel serve
a raiz do repositório como site estático (preset Other, sem build), e o
.vercelignore tira dela o que não é página. A API continua
no Render: o front escolhe a API pelo endereço (localhost usa a local,
qualquer outro usa a do Render), então trocar de hospedagem não muda código.
A API não vai para o ar junto com o site. A Vercel publica o merge em
segundos; a API depende do deploy do Render, que leva alguns minutos — e pode
não acontecer. Depois de 14/10/2026 nenhum deploy do Render foi ao ar: em
21/10 o site passou a mandar o botão da demonstração para uma conta que a API
antiga não tinha, e o login dava "E-mail ou senha inválidos"; e a conta da Ana,
sem a rotina que a renova, ficou com a semana sem consultas e o mês sem
sessões. Para conferir
qual commit da API está no ar, abra
https://rehabit-api-4tex.onrender.com/api/health: o campo versao é o
commit (vem de RENDER_GIT_COMMIT), e uptimeSegundos pequeno quer dizer que
ela acabou de subir. O workflow
manter-api-acordada.yml faz essa
conta sozinho: uma hora depois do último merge, se a API no ar não tiver o
último commit dela, ele falha, e o GitHub manda e-mail. O que fazer: no painel
do Render, serviço da API → Events (o deploy aparece como falho, cancelado
ou nem aparece) → Manual Deploy → Deploy latest commit; se o deploy falhar,
o motivo está no log dele. Confira também, em Settings, se o Auto-Deploy
está em On Commit.
O domínio foi registrado no registro.br e usa o DNS de lá, no modo avançado, com dois registros:
| Tipo | Nome | Valor |
|---|---|---|
| A | rehabit.com.br | 216.198.79.1 |
| CNAME | www.rehabit.com.br | 0720e30296230fa6.vercel-dns-017.com |
O www redireciona para o endereço sem www (308, configurado em Domains
na Vercel). O GitHub Pages (nathancarvalhodacunha.github.io/Rehabit) não é
mais uma cópia do site: o arquivo CNAME da raiz dá a ele o domínio
rehabit.com.br, e o GitHub responde a qualquer endereço do github.io com um
redirecionamento para o mesmo caminho em rehabit.com.br. A cópia não recebia
a CSP nem o HSTS do vercel.json e concorria na busca. Para desligar o Pages de
vez: Settings → Pages → Unpublish site (aí o CNAME deixa de ter efeito).
O CNAME está no .vercelignore, então não vira endereço no site.
Ao pôr o site em um endereço novo, dois ajustes fora do repositório:
*.vercel.app não fazem login: teste o login em localhost.REHABIT_APP_URL para a pasta Login/ do endereço novo
(https://rehabit.com.br/Login), que é o link do e-mail de "esqueci a
senha". A variável só vale depois de um deploy da API.O que precisa estar configurado no Render, e o que muda para quem usa:
| Variável | Para quê |
|---|---|
REHABIT_JWT_SECRET | Obrigatória. Chave que assina as sessões e os tokens dos goniômetros. Gere com openssl rand -base64 64 (mínimo de 32 bytes). Sem ela a API não sobe no Render nem no perfil cloud: o repositório não tem mais valor-padrão, porque quem conhece a chave assina token de qualquer conta. Trocá-la encerra todas as sessões e desliga todos os goniômetros, que precisam ser pareados de novo. |
REHABIT_PROXIES_CONFIAVEIS | Quantos proxies ficam na frente da API, para achar o IP real no X-Forwarded-For (os limites de tentativas são por IP). Em branco: 1 no Render, 0 fora dele. |
REHABIT_LIMITES_ATIVO | false desliga os limites de tentativas (login, cadastro, e-mails, códigos, pareamento, upload). Só para diagnóstico. |
Outros pontos:
ddl-auto=update também no
PostgreSQL de produção: cria tabelas e colunas novas sozinho, mas nunca
apaga nem renomeia nada, e um erro de mapeamento pode alterar o esquema
real. O caminho seguro é passar a migrações versionadas (Flyway) e trocar
para validate — fica como próximo passo.AUTO_SERVER): para olhar o banco
com outra ferramenta, pare a API antes.Rehabit.sql não traz mais a linha de exemplo com hash de senha e
e-mail pessoal. Ela continua no histórico do git: se aquela senha for usada
em algum lugar, troque-a./api/uploads) exige login; a foto do cadastro vai
por /api/auth/cadastro/foto, que exige o e-mail já confirmado. Só entra
JPG, PNG, GIF ou WEBP, reconhecido pelo conteúdo, e os metadados (GPS,
câmera) saem antes de gravar. O campo foto dos cadastros só aceita URL que
a própria API devolveu, e foto trocada é apagada quando ninguém mais a usa.
As fotos continuam num endereço público, mas impossível de adivinhar (UUID
ou id do Cloudinary). O site reduz antes de enviar a imagem com mais de
2048 px de lado ou mais de 4 MB (o plano gratuito do Cloudinary recusa
acima de 25 megapixels). Se o Cloudinary recusar um envio, a tela mostra o
motivo e o log do Render traz a resposta dele numa linha "Cloudinary
recusou...": HTTP 400 é a imagem; 401/403 é a CLOUDINARY_URL; 420 é o
limite de envios da conta.vercel.json só deixa o site falar
com https://rehabit-api-4tex.onrender.com (e mostrar imagens dele e do
Cloudinary). Mudou o endereço da API? Troque também em connect-src e
img-src, senão o site para de carregar dados. O mesmo vale para o
firmware, que só confia nas autoridades certificadoras listadas nele (veja
o guia do goniômetro).vendor/npm/<pacote>@<versão>/, servidas pelo próprio site: a CSP não
confia em script de nenhum CDN. Para atualizar uma, copie o arquivo do
pacote publicado no npm para a pasta da versão nova, troque o caminho e o
nas páginas — confere que o arquivo é
byte a byte o do npm.O aparelho é um ESP32 com um MPU6050 preso ao segmento móvel da articulação.
Ele estima a gravidade com um filtro complementar (acelerômetro + giroscópio)
e mede o ângulo entre a gravidade de agora e a gravidade na pose marcada como
zero — com o braço pendurado ao lado do tronco marca 0°, na horizontal marca
90°, acima da cabeça ~180°. Como o zero vem dessa pose e não de um eixo
escolhido no código, não importa em que orientação a placa é amarrada no
braço. Em compensação a medida é sempre positiva (não distingue para que lado
a articulação abriu) e é relativa à gravidade, não ao tronco: o paciente
precisa estar ereto. O ângulo vai para a API; o navegador recebe cada leitura por SSE
(GET /api/goniometro/stream), sem ficar perguntando de tempos em tempos.
Quando o SSE não sobe — proxy que corta streaming, rede corporativa — o
cliente cai sozinho para polling e continua funcionando.
O aparelho não guarda credencial nenhuma: ele se pareia com a clínica por um código de 6 dígitos e passa a usar um token só dele, que a clínica pode revogar a qualquer momento. A telemetria, por isso, não carrega a clínica no corpo — ela sai do token, e é o que impede um goniômetro de escrever na clínica de outro.
O caminho de volta usa a resposta do próprio POST de telemetria: o ESP32 não abre porta nenhuma, só lê o que veio junto. É assim que os botões da tela Dispositivo chegam ao aparelho (zerar/tara, identificar, iniciar e parar captura, reiniciar) e é assim que o servidor dita o ritmo de amostragem — 10 Hz gravando, 2,5 Hz com alguém olhando, 0,5 Hz ocioso, para poupar bateria.
Na prática, dentro do sistema:
As leituras soltas vivem em memória enquanto a tela está aberta — são centenas
por minuto e só interessam naquele momento. O que vai para o banco é o cadastro
do aparelho, a amplitude que o profissional escolheu gravar na sessão e, quando
ela veio de uma gravação, a curva daquele movimento (uma lista de pares
[ms desde o início, ângulo], decimada acima de 1200 pontos para uma captura
longa continuar cobrindo o movimento inteiro em vez de ser cortada no meio).
A curva é buscada no servidor pela identidade da captura, nunca enviada pelo navegador: se o profissional corrigir a amplitude à mão, o vínculo se desfaz e a sessão é salva sem curva — melhor não ter gráfico do que pendurar no paciente o traçado de um movimento que não corresponde ao número registrado.
Para montar e gravar o aparelho, veja
Firmware/goniometro-esp32-GUIA.md. Para
testar a placa logo depois de soldar, antes do firmware principal, veja
Firmware/teste-hardware-GUIA.md.
Duas telas dependem de e-mail: a confirmação do endereço no cadastro e a recuperação de senha. Sem provedor configurado a API continua funcionando — ela escreve o conteúdo do e-mail (com o código) no console do backend, o que basta para desenvolver e demonstrar, e o cadastro passa a exigir só as checagens de endereço (formato, domínio descartável e DNS).
Há dois caminhos de saída, escolhidos por qual estiver configurado.
Desde 26/09/2025 o plano gratuito do Render bloqueia a saída nas portas de SMTP (25, 465 e 587). Ou seja: lá o Gmail por SMTP não funciona, com senha de app certa ou errada — a conexão morre antes de chegar ao servidor. Por isso o envio de produção usa a API HTTP do Brevo, que responde em HTTPS na porta 443.
O plano gratuito do Brevo dá 300 e-mails por dia e aceita verificar só um endereço remetente — não exige domínio próprio, então serve uma conta Gmail comum.
xkeysib-.| Variável | Valor |
|---|---|
BREVO_API_KEY | a chave xkeysib-... |
MAIL_FROM | o endereço verificado no passo 1 |
REHABIT_APP_URL | endereço público da pasta Login/, para o link do e-mail de recuperação |
Se BREVO_API_KEY estiver definida e MAIL_FROM não, a API avisa no log e
volta a escrever no console — o Brevo recusaria o envio de um remetente não
verificado.
Fora do Render as portas de SMTP funcionam normalmente. O jeito mais simples
é copiar rehabit-api/rehabit-api/config/application.properties.exemplo para
application.properties na mesma pasta e preencher. O Spring Boot lê essa
pasta sozinho ao subir o jar, sem recompilar e sem mexer em variável de
ambiente — e o arquivo está no .gitignore, então a senha não vai parar no
repositório. Com Gmail, use uma
senha de app: a senha
normal da conta não funciona.
# rehabit-api/rehabit-api/config/application.properties
[email protected]
spring.mail.password=abcd efgh ijkl mnop
| Variável | Para que serve | Padrão |
|---|---|---|
BREVO_API_KEY | Chave da API do Brevo. Definida, é o caminho usado. | (vazio) |
MAIL_FROM | Endereço remetente. Obrigatório com o Brevo, e precisa estar verificado lá. | o MAIL_USERNAME |
MAIL_FROM_NAME | Nome exibido como remetente. | Rehabit |
MAIL_USERNAME | Conta do SMTP. Usada quando não há chave do Brevo. | (vazio) |
MAIL_PASSWORD | Senha de app da conta acima. | (vazio) |
MAIL_HOST / MAIL_PORT | Servidor SMTP. | smtp.gmail.com / 587 |
REHABIT_APP_URL | Endereço público da pasta Login/, usado para montar o link do e-mail de recuperação. Vazio = o e-mail vai só com o código. | (vazio) |
REHABIT_VALIDAR_EMAIL_DNS | Checar no DNS se o domínio do e-mail recebe mensagens. | true |
REHABIT_CONFIRMAR_CADASTRO | Exigir o código de confirmação no cadastro. Vazio = liga sozinho quando há provedor. | (vazio) |
A linha que o backend escreve ao subir diz qual caminho está valendo:
Envio de e-mail por BREVO..., Envio de e-mail por SMTP... ou
Envio de e-mail desligado....
O erro vai inteiro para o log do backend, com uma dica do que conferir. Os que aparecem na configuração inicial:
| No log | O que é |
|---|---|
Brevo respondeu 401: Key not found | A chave não é reconhecida. Ou é a chave de SMTP (xsmtpsib-), que só vale para o relay SMTP, ou é a versão mascarada — o Brevo mostra a chave inteira uma única vez, e o que aparece na tela depois não funciona. Nos dois casos, gere uma chave de API v3 nova. |
Brevo respondeu 401: unrecognised IP address | A chave está certa; a conta do Brevo tem restrição de IP ligada. Com a API hospedada no Render, o IP de saída vem de uma faixa compartilhada e muda, então autorizar o IP que aparece na mensagem resolve só até a próxima troca — o caminho estável é desligar a restrição. |
Brevo respondeu 400: sender is not valid | O endereço em MAIL_FROM não está verificado na conta do Brevo. |
Brevo respondeu 402 ou 429 | Passou dos 300 e-mails do dia. |
MailConnectException: Couldn't connect to host (por SMTP) | O bloqueio de portas do Render. Use o Brevo. |
AuthenticationFailedException: 535 (por SMTP) | Senha de app do Gmail errada. |
O formato da chave também é conferido no arranque: se ela tiver cara de chave de SMTP ou de chave mascarada, o log avisa antes de alguém tentar se cadastrar.
a@b.src/main/resources/emails-descartaveis.txt).Três rotas da API recebem um token antirrobô da Cloudflare: o login
(onde um robô tentaria adivinhar senha), o envio do código de cadastro e
o e-mail de recuperação de senha — essas duas gastam da cota de 300
e-mails por dia do Brevo, e um robô martelando qualquer uma delas esgotaria a
cota e deixaria todo mundo sem e-mail até o dia seguinte. O /register não
precisa: com o envio de e-mail ligado ele exige o código, que só sai por uma
delas.
No login, no cadastro e no "esqueci a senha", a caixinha "Confirme que é
humano" da Cloudflare fica no formulário, acima do botão, e o botão só acende
quando ela fica verde ("Sucesso!"). Quem decide se precisa clicar é a
Cloudflare (o widget está no modo Managed): quase sempre a caixinha se
marca sozinha em cerca de um segundo, e só pede o clique quando desconfia —
não existe modo que obrigue o clique toda vez. Quem faz isso é
Login/turnstile.js; cada página só põe um
<div class="rh-captcha" data-rh-captcha> antes do botão.
Cada token vale uma vez só. Depois de usado, a caixinha se confere de novo
sozinha e o botão espera outra vez. Os dois "Enviar de novo" (no pop-up do
código de cadastro e no "esqueci a senha") usam o token que ela já renovou;
se ela estiver escondida ou coberta pelo pop-up e ainda sem token, entra um
widget invisível que só aparece, num cartão no pé da tela, se a Cloudflare
pedir o clique. O token vai no cabeçalho X-Turnstile-Token, e a API
confere com a Cloudflare antes de fazer qualquer coisa.
Se a verificação não consegue rodar (script da Cloudflare bloqueado, chave ou
domínio recusados), o botão é liberado e a chamada vai sem token: quem
responde é a API, com a mensagem dela, em vez de um botão morto sem
explicação. Já o veredito "parece robô" (erro 600xxx) mantém o botão
apagado.
| Onde o site está aberto | O que acontece |
|---|---|
rehabit.com.br | Caixinha com a site key de Login/turnstile.js (vazia = sem verificação) |
localhost | Caixinha com a chave de teste da Cloudflare, que aprova sempre (com a faixa "Somente para teste") |
file:// | Sem verificação — o Turnstile não roda sem endereço; a caixinha some e o botão fica liberado |
Do lado da API, sem TURNSTILE_SECRET_KEY a verificação fica desligada,
como o envio de e-mail sem provedor: rodando local e nos testes nada muda, e
o log de arranque diz qual é o caso. Com a chave definida:
Precisa de uma conta gratuita na Cloudflare; não precisa de domínio próprio nem de cartão. A ordem importa: com a chave secreta no Render e o site publicado sem a site key, ninguém consegue entrar.
rehabit.com.br, modo Managed (o github.io
só redireciona para ele e não precisa estar no widget).CHAVE_DO_SITE em Login/turnstile.js e leve
para a main. Ela é pública: só funciona nos hostnames cadastrados no
widget. A Vercel publica sozinha em segundos.[Turnstile].Verificação antirrobô (Cloudflare Turnstile) desligada. Merge na
main não garante que ele publicou (já levou dias); se a linha não
aparecer, rode Manual Deploy → Deploy latest commit.TURNSTILE_SECRET_KEY com a
secret key do widget. Ela nunca vai para o repositório. Depois do
redeploy o log passa a dizer ligada.Para desligar de volta, basta apagar a variável no Render.
| Sintoma | O que é |
|---|---|
| Todo mundo recebe "Não conseguimos confirmar que você não é um robô" | O site publicado está sem a site key, com a site key de outro widget, ou o hostname não está cadastrado no widget. Olhe os avisos [Turnstile] no console. Para destravar na hora, apague a TURNSTILE_SECRET_KEY no Render. |
| A caixinha mostra "Falha" e o botão não acende | A Cloudflare reprovou o navegador (console: [Turnstile] erro 600010). Recarregar a página costuma resolver; em navegador controlado por automação é o esperado. |
Console: [Turnstile] erro 110200 | Hostname não autorizado: cadastre o endereço do site no widget. |
Console: [Turnstile] erro 110100 ou 110110 | Site key inválida — confira o que foi copiado para Login/turnstile.js. |
Log da API: invalid-input-secret | A TURNSTILE_SECRET_KEY está errada. Enquanto isso todo pedido passa — a verificação está ligada só no nome. |
Log da API: Não consegui conferir o token antirrobô com a Cloudflare | A Cloudflare não respondeu a tempo; o pedido passou. Se for constante, é rede do Render. |
O plano gratuito do Render derruba o container depois de ~15 minutos sem
tráfego. A primeira chamada depois disso paga o religamento inteiro. Medido
no mesmo POST /api/auth/login:
| Estado da instância | TTFB |
|---|---|
| Dormindo (spin-down) | 146,7 s |
| Acordada | 0,48 s |
DNS e TLS levaram 20 ms e 79 ms nos dois casos — a rede nunca foi o problema, e os assets externos somam só 212 KB, que não explicam minutos.
Por muito tempo isso pareceu um defeito "do primeiro acesso em cada aparelho",
e a confusão tinha uma razão de ser: a tela de login não chama a API ao
carregar, então o HTML aparece rápido e a espera toda cai no botão Entrar.
Como a sessão fica no localStorage com token de 30 dias, um aparelho já
usado entra direto e nunca refaz o login — quem faz a chamada que acorda o
servidor é sempre o aparelho novo. O gatilho real é o tempo parado; o aparelho
novo só é quem paga a conta.
A primeira tentativa foi um workflow do GitHub batendo em /api/health a cada
10 min, e ela não funcionou. O agendador do GitHub Actions é "melhor
esforço": na virada de cada hora, sob carga, ele atrasa e descarta execuções.
Pedindo 108 por dia, entregou 5 a 6, com intervalos de 2 a 7 horas, e todas
encontraram a API dormindo (124 a 146 s de resposta). Ninguém percebeu por
dias porque cada execução saía verde: o 200 acabava chegando, e o aviso de
"instância dormindo" ficava escondido embaixo do check.
Hoje o relógio é o da própria API:
ManterApiAcordada (rehabit-api/.../service/) chama o próprio
/api/health a cada 5 min, pela URL pública. Tem de ser a pública, e
não localhost: o Render só conta como tráfego o que passa pelo proxy de
entrada dele. A URL vem de RENDER_EXTERNAL_URL, que o Render define sozinho,
então não há nada para configurar. Fora do Render essa variável não existe e o
ping fica desligado, para que rodar a API local não saia batendo na produção.
O /api/health mostra se está funcionando, sem precisar dos logs do
Render:
{"status":"ok","uptimeSegundos":5400,"autoPing":true,"ultimoAutoPingSegundosAtras":42}
autoPing: false em produção quer dizer que ele está desligado.
ultimoAutoPingSegundosAtras acima de ~300, com a instância de pé há mais de
10 min, quer dizer que ele parou de funcionar.
O workflow manter-api-acordada.yml
virou rede de segurança e alarme. Ele acorda a instância se ela parar por
outro motivo (o auto-ping não roda com o processo parado) e falha, com
e-mail do GitHub, se encontrar a API dormindo ou o auto-ping parado. Antes
de uma apresentação, dá para rodá-lo na mão: aba Actions → Manter a API
acordada → Run workflow.
O loader avisa quando a espera passa do normal (5 s e 25 s), para o caso em que alguém ainda pegar a instância subindo, logo depois de um deploy.
Horas do plano gratuito. Acordada 24 h, a instância gasta ~720 a 744 horas
por mês das 750 gratuitas. Isso só fecha se ela for o único web service
gratuito da conta no Render (o front-end está na Vercel, então não
conta). Se houver outro, as horas acabam antes do fim do mês e o Render suspende
os serviços: nesse caso, defina REHABIT_KEEPALIVE_ATIVO=false no Render e
desative o workflow. A solução definitiva, se um dia houver orçamento, é o plano
pago, que não hiberna.
Distribuído sob a licença MIT, em nome da Equipe Rehabit — veja LICENSE.
integritytests/seguranca-frontimg-src só aceita a conta do Rehabit,
https://res.cloudinary.com/xuo5tlgh/: assim uma injeção de HTML não usa o
Cloudinary de outra pessoa para mandar dados para fora. O nome da conta é o
que vem depois do @ no CLOUDINARY_URL do Render; se a conta mudar,
troque no vercel.json e em tests/seguranca-front/api-simulada.js (o
teste estático reprova se o img-src voltar a aceitar o Cloudinary
inteiro).index.html, liberado na CSP
pelo hash do conteúdo. Mexeu nele, recalcule o hash — tests/seguranca-front
acusa a diferença.includeSubDomains: não dá para garantir daqui que todo
subdomínio do domínio do site tem HTTPS. Confirmado isso, pode acrescentar.mvn test na API e os de tests/ no site (veja
tests/README.md). O relatório da auditoria de outubro de 2026 — o que foi
encontrado, como foi corrigido e o que ainda depende de configuração — está
em docs/seguranca/auditoria-2026-10.md.REHABIT_JWT_SECRET ser obrigatória e nova, e o hash da linha antiga
do Rehabit.sql. Para repetir: gitleaks git --log-opts="--all" num
clone completo (git clone --mirror).github.io não recebia os cabeçalhos de
segurança do vercel.json (CSP, HSTS). Agora ela só redireciona para
rehabit.com.br, pelo arquivo CNAME (veja
Onde o site está publicado).