Plataforma para organização de casos, educação de pacientes e apoio à discussão clínica em doenças valvares, baseada em diretrizes internacionais.
Translation pending
HealthTypeScript · JavaScriptGitHub @augustohcdoOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
Loading the full README. The saved excerpt is shown below.
Plataforma de apoio a pacientes e médicos no acompanhamento de valvopatias cardíacas: jornada do paciente, gestão de casos clínicos, biblioteca de referência, integrações FHIR com hospitais e apoio de IA clínica baseado em diretrizes (SBC, ACC/AHA, ESC). • Vite + React 18 + TypeScript • Tailwind CSS + shadcn/ui • React Router, TanStack Query, React Hook Form + Zod • Supabase — banco de dados (Postgres + RLS), autenticação, edge functions • Vitest para testes Pré-requisitos: Node.js 20.19+ (exigido por Vite 8, ESLint 10 e React Router 7 — está travado em engines no package.json).Read the full README ↗
Plataforma de apoio a pacientes e médicos no acompanhamento de valvopatias cardíacas: jornada do paciente, gestão de casos clínicos, biblioteca de referência, integrações FHIR com hospitais e apoio de IA clínica baseado em diretrizes (SBC, ACC/AHA, ESC).
Pré-requisitos: Node.js 20.19+ (exigido por Vite 8, ESLint 10 e React Router 7 — está travado em engines no package.json).
# instalar dependências
npm install
# copiar variáveis de ambiente e preencher com os valores do seu projeto Supabase
cp .env.example .env
# subir o servidor de desenvolvimento
npm run dev
O gerenciador de pacotes é o npm — o package-lock.json é a fonte de verdade e é o que o CI valida com npm ci.
Scripts disponíveis:
| Comando | Descrição |
|---|---|
dev | Servidor de desenvolvimento (Vite) |
build | Build de produção |
build:dev | Build em modo desenvolvimento |
preview | Preview local do build de produção |
typecheck | Checagem de tipos (app + configs de build) |
typecheck:strict | Checagem de tipos em modo strict no escopo já migrado (src/lib/) |
lint | ESLint |
test | Roda os testes uma vez (Vitest) |
test:watch | Testes em modo watch |
types:generate | Regenera os tipos do Supabase a partir do schema (ver abaixo) |
demo:seed | Popula a base fictícia de demonstração (ver abaixo) |
demo:limpar | Remove a base fictícia, sem tocar em dado real |
cnes:import | Importa o recorte cardiovascular da base pública CNES (ver abaixo) |
O weekly-export grava o banco inteiro num bucket privado toda segunda. O
procedimento para transformar aqueles arquivos em um sistema funcionando de
novo está em RECOVERY.md, junto com o registro do ensaio que o
executou de verdade — e dos três defeitos que só o ensaio encontrou.
src/
pages/
public/ páginas públicas (marketing, legal, conteúdo educativo)
auth/ login, cadastro, recuperação de senha
app/ área autenticada (paciente, médico, hospital, admin)
components/ componentes compartilhados (inclui components/ui, shadcn)
integrations/supabase/ cliente e tipos gerados do Supabase
lib/ utilitários (validação, exportação, labels clínicos)
supabase/
migrations/ migrations SQL versionadas
functions/ edge functions (Deno) — IA clínica, FHIR, integrações
O backend roda em Supabase: Postgres com Row Level Security em todas as tabelas de dados sensíveis, autenticação nativa (e-mail/senha + OAuth Google), e edge functions para integrações FHIR com hospitais parceiros, geração/consulta de chaves de API, e o assistente de IA clínica (clinical-ai), que usa a Gemini API tanto para geração de texto (gemini-3.5-flash) quanto para os embeddings da busca RAG na base de conhecimento (gemini-embedding-001).
Segredos necessários nas edge functions (Supabase Dashboard → Edge Functions → Secrets):
| Segredo | Usado por |
|---|---|
GEMINI_API_KEY | clinical-ai, knowledge-seed (geração de texto e embeddings) |
TURNSTILE_SITE_KEY | turnstile-config (entrega a site key ao frontend) |
RESEND_API_KEY | _shared/sendEmail.ts — boas-vindas e alertas operacionais |
ALERT_EMAIL_TO | destinatário dos alertas do job-watchdog; também vira o Reply-To das mensagens |
ALERT_EMAIL_FROM | remetente (opcional; o padrão é [email protected]) |
Além desses, o Supabase já provisiona automaticamente SUPABASE_URL, SUPABASE_ANON_KEY, SUPABASE_SERVICE_ROLE_KEY e SUPABASE_PUBLISHABLE_KEY.
O TURNSTILE_SECRET_KEY não é usado por edge function nenhuma: o captcha é validado pelo próprio servidor de autenticação do Supabase, configurado em Authentication → Attack Protection. Validar por conta própria seria impossível de qualquer forma — o token do Turnstile é de uso único, então quem valida primeiro impede o outro de validar.
Duas coisas distintas saem por e-mail, e ambas passam pelo Resend com o domínio envio.valvepath.com.br:
_shared/sendEmail.ts. Sem RESEND_API_KEY o envio fica inerte e devolve o motivo, sem lançar — nada quebra, só não sai e-mail.src/integrations/supabase/types.ts é gerado a partir do schema do banco, não escrito à mão. Sempre que uma migration adicionar ou alterar colunas/tabelas, regenere:
SUPABASE_ACCESS_TOKEN=sbp_xxx npm run types:generate
O token é um Personal Access Token da Management API — não confundir com a anon key nem com a service role key. Como ele tem poder praticamente total sobre o projeto, não deve ser commitado nem guardado como secret de CI. Esquecer esse passo faz o npm run typecheck falhar com erros do tipo '<coluna>' does not exist in type ....
Não existe API aberta de CRM — medido: o portal do CFM é protegido por reCAPTCHA e a API de dados abertos do SUS tem 87 rotas, nenhuma de profissionais. O que existe é a base CNES do DATASUS, um ZIP mensal público.
SUPABASE_ACCESS_TOKEN=sbp_xxx npm run cnes:import -- --competencia 202606
SUPABASE_ACCESS_TOKEN=sbp_xxx node scripts/cnes-import.mjs --limpar
Traz 36.042 profissionais da família cardiovascular (cardiologista,
cirurgião cardiovascular, radiologista intervencionista, cirurgião vascular e
torácico), com nome, número e UF do registro. É script local, não edge function:
o ZIP tem ~730 MB e os dois CSV somam ~1,8 GB, muito além do limite de uma
function — a leitura é em fluxo, via unzip -p.
O que ela é e o que não é. O número de registro no CNES é declarado pelo estabelecimento ao cadastrar o profissional, não validado pelo conselho. Ele aparece na tela de aprovação para corroborar; a conferência que vale continua sendo a do portal do CFM, feita por uma pessoa, e registrada com quem conferiu e quando. A tabela é de leitura restrita a administrador, fica fora do backup (reimportável em um comando) e nada dela é exibido a pacientes.
Para apresentar o produto com as telas cheias — painéis, gráficos, relatórios e discussão de Heart Team — existe uma base fictícia de 12 casos, com exames seriados, linha do tempo, compromissos e três médicos fictícios assinando as decisões.
SUPABASE_ACCESS_TOKEN=sbp_xxx npm run demo:seed -- --medico [email protected]
SUPABASE_ACCESS_TOKEN=sbp_xxx npm run demo:limpar
O conteúdo está em scripts/demo-data.mjs; quem insere é scripts/demo-seed.mjs.
É script e não migration de propósito: uma migration levaria paciente inventado
para dentro de toda restauração de desastre.
Três coisas valem saber antes de usar:
is_demo = true e são excluídos de admin_site_metrics() e do resumo semanal
do administrador, que passam a mostrá-los em linha própria..xlsx.user_banned. Elas existem só para a
discussão ter autoria distinta.--aplicar limpa antes de inserir, então rodar duas vezes não duplica, e
--limpar devolve o banco exatamente ao estado anterior (inclusive apagando os
laudos que o seed enviou ao bucket).
O front vai para a Vercel a cada push na main. As edge functions vão para
o Supabase, por um workflow separado. Eram coisas distintas e só a primeira
existia: durante semanas, mexer em supabase/functions/ não mudava nada em
produção, e a CI continuava verde porque ela roda os checks, não publica.
O sintoma apareceu tarde. O seed da base de conhecimento devolveu
{ ok: true, total: 11 } com 18 trechos no repositório — e, com ele, a
descoberta de que a clinical-ai respondia pela ESC/EACTS 2021 enquanto o
painel de conduta do médico anunciava a de 2025, porque o prompt corrigido
estava no Git e não estava rodando.
.github/workflows/deploy-functions.yml publica todas as functions quando
supabase/functions/** ou supabase/config.toml mudam na main, e também sob
demanda (Actions → Deploy edge functions → Run workflow).
Ele exige o segredo SUPABASE_ACCESS_TOKEN em Settings → Secrets and
variables → Actions. O token sai de
https://supabase.com/dashboard/account/tokens.
Esse token é root sobre a conta inteira do Supabase, e este repositório é
público. Três coisas o protegem, e src/test/deployFunctions.test.ts reprova
se alguma cair:
pull_request — num repositório público, um PR
de fork executaria o job com o segredo no ambiente;permissions: contents: read, e nada mais.O verify_jwt de cada função vem de supabase/config.toml e o CLI o respeita —
é o que mantém a clinical-ai exigindo JWT depois de cada deploy.
Publicado não é conferido. Em Administração → Base da IA e FHIR, o botão
Popular / atualizar base RAG compara o total que a função devolve com o que
esta versão do site espera (src/data/baseDaIA.ts). Divergiu, a tela diz
"A função publicada está desatualizada" em vermelho — as duas metades sobem
por caminhos diferentes, e é essa divergência que denuncia a que ficou para
trás.
O vercel.json da raiz existe por um motivo só, e não pode ser removido: o
preset de Vite da Vercel não adiciona o fallback de SPA sozinho. Sem o
rewrite de /(.*) para /index.html, qualquer caminho que não seja um arquivo
no disco devolve o 404 da própria Vercel, antes de chegar ao React.
Isso não aparece navegando pelo site — o React Router troca a rota no navegador,
sem passar pelo servidor. Quebra só quando o caminho é pedido à Vercel:
recarregar a página, abrir um favorito, ou clicar num link de e-mail. Foi assim
que o link de redefinir senha, o retorno do login com Google (/auth/callback)
e a confirmação de cadastro ficaram caindo em 404 sem ninguém perceber.
Os rewrites são aplicados depois da checagem de arquivos, então
/assets/*, /robots.txt e /favicon.ico continuam vindo do disco.
npm run smoke pede cada rota do app à URL de produção e falha listando as
quebradas — é a verificação que faltava, já que npm run dev tem o fallback
embutido e por isso nunca reproduz esse defeito localmente.
npm run rotas:renderizam abre cada uma das 61 rotas num navegador de
verdade e falha nas que não montam.
Existe porque o smoke acima confere o shell — e o shell é o mesmo
index.html para as 61. Uma tela que estoura ao montar devolve exatamente esse
shell, com HTTP 200, e o smoke passa. O resto da rede também não pega: os testes
de unidade cobrem funções e componentes isolados, e o ferramentas:verificar
dirige duas rotas. As outras 59 não eram abertas por nada automático — dava para
quebrar uma tela inteira com CI verde, smoke verde e quase mil testes passando.
Quatro reprovações distintas, porque cada uma aponta para causa diferente:
exceção não tratada; o error boundary global aparecendo (o pior de detectar — a
tela fica bonita, com texto educado, e o HTTP continua 200); #root vazio; e
recurso nosso que não carregou.
Redirecionada não é renderizada. A primeira versão marcava ✓ em toda rota
que não quebrasse — inclusive nas 39 de /app/*, que sem sessão param todas em
/auth/login. Ela abria a tela de login dezenas de vezes e relatava "61 de 61
renderizaram". Um verificador feito para pegar "sucesso relatado sem o trabalho
feito" fazendo exatamente isso.
São três estados, e o resumo traz os três. Hoje, sem sessão:
22 de 61 rotas renderizaram a tela pedida
39 redirecionaram (a tela pedida NÃO foi aberta)
0 quebraram
As 22 são as públicas. As 39 sem sessão continuam sem prova de que suas telas montam — o redirecionamento é o app funcionando, mas quem renderizou foi o login.
Tentei cobrir as 39 plantando uma sessão sintética no localStorage. O
redirecionamento parava e o relatório passou a dizer "60 de 61 renderizaram" —
mas a inversão não reprovava: um throw no topo de MedicoHome, conferido no
bundle servido, e a rota levava ✓ assim mesmo.
Medindo com a tela íntegra: /app/medico fica dez segundos no spinner e
nunca mostra "Área médica". O cliente do Supabase não resolve a sessão
inventada, useAuth.loading nunca vira falso, o ProtectedRoute segura o
spinner, e a tela protegida não chega a montar. As 39 "renderizadas" eram 39
spinners.
O modo foi removido. Cobrir as 39 exige sessão real — um usuário no banco e um login pelo fluxo normal —, e isso fica anotado como o que falta em vez de disfarçado de feito.
Do episódio sobrou uma melhoria que fica: o script espera a tela, não o
spinner. Antes ele olhava 500 ms depois do domcontentloaded, e as telas de
/app/ são carregadas em chunk separado — o que ele media era o fallback de
Suspense. Agora espera o .animate-spin sumir, com teto de 8 s, e reprova a
rota que continuar carregando.
npm run mobile abre um conjunto de páginas num viewport de celular (via
Playwright) e falha listando qualquer elemento mais largo que a tela. Existe
porque html, body tem overflow-x: hidden — necessário para não deixar a
página inteira arrastar de lado num toque acidental, mas isso tem um efeito
colateral sério: um elemento largo demais não vira barra de rolagem, vira
texto cortado no meio da palavra, sem aviso nenhum na tela. Já aconteceu com
um item de grid (min-width: auto por padrão) e com um botão de texto longo
(whitespace-nowrap por padrão, sem min-w-0 no container flex). Ambos os
casos deixam de existir com a rolagem visível, mas o corte silencioso é pior:
ninguém vê, e a mensagem clínica chega incompleta.
Exige o Playwright — não é dependência do projeto (seria peso para um script
sob demanda); instale com npm i -g playwright se ainda não tiver.
Assim como o smoke, fica fora do CI: exigiria Chromium em todo push, e o
ganho não paga esse custo aqui. Rode manualmente depois de qualquer mudança de
layout, sobretudo em grids e flexs com texto de tamanho variável.
O workflow em .github/workflows/ci.yml roda a cada push e pull request, em Node 22, e falha o build se qualquer etapa quebrar:
npm ci → typecheck → typecheck:strict → lint → test → build
Nenhuma credencial do Supabase é necessária: o build é estático e o cliente só é instanciado em runtime, no navegador.
O tsconfig.app.json compartilhado ainda roda com strict: false (dívida herdada). Em vez de virar a chave de uma vez, existe um tsconfig.strict.json separado que roda como etapa própria do CI (typecheck:strict), então uma regressão de tipagem falha de forma específica e isolada.
Hoje ele cobre todo o src/ (exceto arquivos de teste) com strict mais noImplicitAny, noUnusedLocals, noUnusedParameters, noFallthroughCasesInSwitch, noImplicitReturns, noImplicitOverride e noPropertyAccessFromIndexSignature.
Atenção ao editar esse arquivo: ele estende o
tsconfig.app.json, que desliga várias sub-flags destrictexplicitamente. Uma flag explícita no config pai vence ostrict: truedo filho — por isso as quatro precisam ser religadas uma a uma. Confira comnpx tsc -p tsconfig.strict.json --showConfigse tiver dúvida sobre o que está realmente valendo.
Flags ainda não adotadas, com o custo já medido: exactOptionalPropertyTypes (31 erros), verbatimModuleSyntax (20) e noUncheckedIndexedAccess (16).