Files
scoreodonto.com/documentação/PENDENCIAS_E_DEPLOY.md
T
VPS 4 Builder 37cc818f4c docs: centraliza e reescreve a documentação em documentação/
- reúne todos os .md de ScoreOdonto numa pasta única (fonte única, dentro do repo versionado pelo Gitea)
- arquitetura.md: reescrito p/ o estado REAL (3 VPS, banco compartilhado dev/prod, sem staging/listener/registry fantasmas)
- deploy.md: reescrito p/ o estado-ALVO (Gitea CI/CD: push->build->registry->deploy por tag)
- DEPLOY-PRODUCAO.md: runbook operacional executável (fluxo manual real de hoje)
- VERDADE-HOJE.md: auditoria (codigo/banco/containers/deploy/versao/rollback)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 06:45:51 +02:00

22 KiB
Raw Blame History

ScoreOdonto — Pendências, Tutoria e Deploy (dev)

Documento de handoff. Última atualização: 2026-06-12. Contexto completo da arquitetura de agenda/workspaces: ver memória do projeto project-agenda-workspace-architecture.md.


1. Deploy no container de dev (IMPORTANTE)

dev.scoreodonto.com NÃO é Vite HMR. É servido pelo container scoreodontocom-scoreodonto-frontend-1 (projeto compose scoreodontocom, de docker-compose.yml) rodando nginx sobre um vite build estático. Edições no fonte não aparecem sozinhas — exigem rebuild + recreate manual.

Banco é de PRODUÇÃO (compartilhado)

  • Backend de dev (VPS 4) e produção (VPS 1) usam o MESMO Postgres na VPS 3 (10.99.0.3:5432/scoreodonto). Não há banco de dev separado.
  • Migrações aditivas idempotentes rodam no startup (runMigrations() em server.js).
  • Qualquer dado criado "testando" em dev é dado real de produção → sempre limpar.
  • A constraint vinculos_role_check é DROP+ADD a cada boot. Ao deployar na prod, o server.js de lá PRECISA incluir 'protetico', senão o ADD falha se já houver vínculo protético criado via dev.

Comandos de deploy

cd /home/deploy/stack/scoreodonto.com
# Frontend (server.js NÃO é hot-mount; só plugins/sessions/media são):
docker compose build scoreodonto-frontend && docker compose up -d --force-recreate scoreodonto-frontend
# Backend:
docker compose build scoreodonto-backend  && docker compose up -d --force-recreate scoreodonto-backend
  • Verificar: o hash do bundle em https://dev.scoreodonto.com/ (/assets/index-*.js) muda.
  • vite build NÃO faz type-check bloqueante → erros TS não quebram o build, mas podem estourar em runtime. Conferir após deploy.
  • Logs de migração: docker logs --tail 20 scoreodontocom-scoreodonto-backend-1 | grep -i migration (esperado: [MIGRATION] all migrations OK).

Efeito colateral relevante

Quando esta sessão começou, o dev servia um build antigo (index-DWdouzmk.js). Os rebuilds passaram a publicar o working tree atual, que inclui vários arquivos untracked (WIP de outra sessão): OrthoView, OrthoReports, OrthoPhotoWorkspace, TutoriaSection, SuperAdminView, GestaoEquipeView, Tutor*View, plugins/, etc. → Bugs nesses arquivos só passaram a aparecer agora por causa do deploy, não por edição.


2. TUTORIA no modal de tratamento de orto — CAUSA RAIZ RESOLVIDA

Relato do usuário: "ferrou com a tutoria no modal que administra o tratamento de orto."

CAUSA RAIZ (confirmada): as telas WIP (TutoriaSection, OrthoPhotoWorkspace, PhotoAnnotator, TutorPainelView, TutorCandidaturaView, AdminGestaoTutoresView) chamavam 24 métodos do HybridBackend que não existiam em services/backend.ts (só getCurrentUserName existia) → crash HybridBackend.X is not a function assim que o rebuild publicou esses componentes. Além disso o backend não tinha nenhuma tabela nem endpoint de orto/tutoria.

CORRIGIDO nesta sessão:

  • services/backend.ts: adicionados os 24 métodos via helper _ortoJson (chama o endpoint real e degrada com fallback seguro se a rota faltar → fim do crash).
  • backend/server.js: 9 tabelas (ortho_photo_session, ortho_photo, ortho_photo_annotation, ortho_tutor_config, ortho_tutor_candidatura, ortho_tutor_profile, ortho_tutoring_request, ortho_tutoring_message, ortho_tutoring_photo_package) + 25 endpoints + upload real de fotos via multer no volume persistente /app/media, servidas por /api/orto/fotos/:id/raw.
  • backend/package.json: + multer.
  • Validado: migrations OK, 9 tabelas no banco, endpoints respondendo (200/401/404). Frontend e backend rebuildados → live no dev.

FEITO depois:

  • OrthoDetailPanel agora renderiza OrthoPhotoWorkspace + TutoriaSection (no lugar do PhotoCompare placeholder) — a tutoria/fotos vivem no modal do paciente.
  • Sessões mensais: orto é mensal → ortho_photo_session ganhou rotulo+data_ref; o workspace cria sessão por mês (input month) + especiais (INICIAL/FINALIZAÇÃO/ PÓS-CONTENÇÃO); tabs dinâmicas ordenadas. Validado end-to-end.
  • Exportar documentação: botão EXPORTAR → modal seleciona "completa" (tudo) ou fotos específicas (checkbox por foto, agrupado por sessão) → abre view de impressão (navegador salva como PDF). Sem dependência nova.

FEITO depois (cont.):

  • Tratamentos de orto persistem: tabelas ortho_tratamento + ortho_evolucao (triagem/diagnóstico/flags em JSONB); endpoints GET/POST/PUT + /evolucoes + /finalizar. backend.ts troca o mock t1/t2 por chamadas reais (mappers snake↔camel). Botão "NOVO TRATAMENTO" abre modal de criação (paciente/plano/diagnóstico/triagem). Adicionar evolução atualiza fio_atual/ultima_consulta; finalizar grava contenção. Validado end-to-end.
  • Comparador cross-sessão (mesmo slot, sessão A × B / mês a mês) com modos lado a lado / slider / sobreposição.

FEITO depois (cont.):

  • MercadoPago real: POST /tutoria/candidaturas/:id/pagamento cria uma preference (Checkout Pro) com a taxa_inscricao da config, external_reference=cand:<id>, back_urls e notification_url → devolve init_point. POST /tutoria/mp-webhook (público) consulta o pagamento e, se approved, ativa o tutor automaticamente (ativarCandidaturaTutor, reaproveitado pelo confirmar-pagamento do admin). Sem mp_access_token na config → fallback honesto (ativação manual). Admin configura token/taxa em AdminGestaoTutoresView. Caveat: não testado contra a API real do MP (sem credenciais); fallback e webhook-noop validados.

FEITO depois (cont.):

  • GestaoEquipeView corrigida (estava roteada em /gestao-equipe e quebrava igual a tutoria — chamava 12 métodos inexistentes). Backend: tabelas setores/funcoes + colunas vinculos.setor_id/funcao_id; endpoints membros (GET com nomes, POST com setor/função, +PUT, DELETE) e setores/funções CRUD (todos tenantGuard). backend.ts: os 12 métodos. Validado end-to-end + isolamento de tenant (403).
  • PDF nativo (jspdf adicionado às deps): o EXPORTAR agora gera um PDF real (fotos agrupadas por sessão, 3/linha, legenda do slot) via canvas→jsPDF; botão IMPRIMIR mantém o print→PDF como alternativa. Caveat: geração é client-side (jsPDF+canvas), validei só install/tsc/build, não a renderização no navegador.

FEITO depois (cont.):

  • Planejamento visual no PhotoAnnotator: grupo PLANEJ. com vetor de movimentação (haste + barra de origem + cabeça larga, arrastar da posição atual → destino), marca de extração (X, clique no dente) e linha média (referência vertical tracejada, clique). Persistem como anotações (JSONB) — sem mudança no backend.

FEITO depois (cont.):

  • Type-gaps zerados — tsc do frontend agora passa com 0 erros. Bug real corrigido em FinanceiroView (getPacientes retorna {data,total} → usava o objeto inteiro como lista, quebrando a busca → agora results.data); TutorPainelView ganhou os campos financeiros na interface Solicitacao. Login/RxConfig limparam junto.

FEITO depois (cont.):

  • Calibração de escala (mm) no PhotoAnnotator: ferramenta ESCALA (arrastar sobre distância conhecida → informa o valor real em mm) define mm/px; os vetores de movimentação passam a exibir a magnitude em mm. Rótulos renderizados fora do SVG (HTML posicionado por imgDim) p/ não distorcer com o viewBox. Persiste no JSONB.
  • RxConfigView usava props inexistentes do PageHeader (subtitle/icon) → trocado por description (subtítulo agora aparece). Resta 1 erro tsc pré-existente em LoginView:490 (comparação morta no fluxo de cadastro/auth) — deixado de propósito: é lógica de auth e mascarar esconderia a intenção do autor; funciona em runtime.
  • Comparador ganhou modo EVOLUÇÃO: mesmo slot em todas as sessões/meses lado a lado (faixa horizontal cronológica) — além de lado a lado / slider / sobreposição.
  • Vínculo com o cadastro de pacientes: o modal de Novo Tratamento tem busca no cadastro (getPacientes) que preenche paciente_id + nome + nascimento; digitar livre cria paciente avulso (sem vínculo). Frontend-only, validado por tsc/build.

2b. Apuração original (histórico)

Apuração (fatos):

  • Não foi editado nenhum arquivo de orto/tutoria nesta sessão. Todos estão untracked (git status??): OrthoView.tsx, OrthoReports.tsx, TutoriaSection.tsx, TutorPainelView.tsx, TutorCandidaturaView.tsx, AdminGestaoTutoresView.tsx.
  • OrthoView.tsx não tem nenhuma referência a "tutor".
  • TutoriaSection.tsx não é importada em lugar nenhum (código solto/WIP).
  • Quem referencia tutor: OrthoReports.tsx (e os Tutor*View).
  • Único toque indireto possível: services/backend.ts (eu alterei getCurrentRole, mas só com fallback p/ superadmin quando não há workspace — não afeta dentista/tutoria; getCurrentUserName, usado pela TutoriaSection, NÃO foi alterado).
  • Provável causa: o rebuild publicou a versão WIP da tutoria que antes não estava no ar.

PENDENTE — preciso do sintoma para diagnosticar:

  • Onde exatamente? (OrthoView do paciente / OrthoReports / "Meus Tratamentos" / outro)
  • O que acontece? (crash/tela branca / botão sumiu / dado não carrega / erro console)
  • Mensagem de erro no console do navegador?

→ Com o sintoma, achar a causa raiz em OrthoReports.tsx / TutoriaSection.tsx e corrigir.


3. Status das fases (arquitetura agenda/workspaces)

  • Fase 0 — Cadastro: 5 cards de perfil com preço (Clínica R$499 separada). LoginView.tsx.
  • Fase 1 — Workspace pessoal automático no /api/register (dentista/protético/paciente); clinicas.tipo+owner_id; protetico adicionado ao vinculos_role_check. Fim do beco "aguardando-convite".
  • Fase 2 — Identidade global: dentistas.usuario_id + backfill por email; criação seta usuario_id.
  • Fase 3 — Anti-overbooking global: POST /api/agendamentos resolve identidade do profissional e bloqueia sobreposição em QUALQUER clínica → 409 {conflito, ocupado, podeRegistrarInteresse}.
  • [~] Fase 4 — Fila de interesse:
    • Backend: tabela interesses_agenda, POST/GET /api/interesses, hook no DELETE de agendamento que notifica interessados como enviado_por='sistema' e marca liberado.
    • Frontend Parte 1: AgendaView mostra painel "Horário ocupado" no 409 → registrar interesse (com minutos de espera).
    • Frontend Parte 2: dentista vê "Pedidos de interesse" (sino + badge + modal; GET retorna clinica_nome).
    • POST /api/clinicas passou a setar owner_id+tipo.
    • FALTA: endpoint "tentar liberar" (clínica pede → dentista decide mover/cancelar o ocupante).
    • FALTA: re-notificação periódica do sistema enquanto o dentista demora (job/cron), sempre rotulada como "notificação do sistema" (impessoal).
  • Fase 5 — Odontodelivery: diretório (disponivel_diretorio já existe) → pedido de horário ao freelancer.

Regras de negócio confirmadas

  • Profissional não pode estar em 2 lugares ao mesmo tempo (invariante: 1 agendamento por slot).
  • Conflito não é "não" seco: vira fila de INTERESSE (não trava o slot). Clínica pode: registrar interesse com tempo de espera (expira), escolher outro horário, ou pedir pro dentista liberar.
  • Todos têm workspace (pessoal automático no cadastro).

4. Bugs pré-existentes encontrados (não introduzidos nesta sessão)

  • notificacoes.lida é smallint (confirmado no banco: int2), mas havia código usando boolean → corrigido (2026-06-12). Locais: POST /api/superadmin/notificacoes inseria false (server.js:4385 e 4393) e o marcar-como-lida usava true/false (POST /api/notificacoes/read-all server.js:1584 e .../:id/read server.js:1601). Todos trocados para 1/0. Os demais INSERT (270, 2188) já usavam 0.
  • POST /api/agendamentos default status='Agendado' viola agendamentos_status_check NÃO procede mais. A constraint atual é case-insensitive e aceita agendado (lower(status) IN ('importado','agendado','confirmado','reagendado','remarcar', 'cancelado','falta','atendido','pendente','concluido','faltou')). O default do código é rest.status || 'agendado' → válido. Há registros agendado em uso no banco.
  • PUT /api/agendamentos/:id não passa pelo check de overlap NÃO procede. O PUT chama findOverbookingConflict(...) e retorna 409 em conflito (server.js:~2013).

4c. Integração Plugin RX ↔ servidor dental (2026-06-12) — MODELO GLOBAL

Modelo correto (confirmado pelo usuário): config é GLOBAL, só do superadmin (não por clínica, não por usuário admin da conta). Hierarquia desejada no RX: scoreodonto / conta-mestre (consulttclinic@gmail.com) / clinicaID / paciente. Mapa: conta-mestre = api_token global · clinicaID = images.clinic_name · paciente = images.patient_name.

Menu — feito antes: Sidebar.tsx voltou a mostrar PLUGINS ao superadmin (allowlist linha 67 estava sem 'plugins').

FASE 0 — scoreodonto realinhado p/ GLOBAL (feito, deploy dev):

  • settings.rx_config_global = { rx_url, api_token }. Sem login/senha, sem cache: o backend usa o api_token estático direto como Bearer (_rxAdminFetch). api_tokens do RX são aceitos pelo authenticateToken (viram admin, não expiram).
  • PUT /api/rx/config agora é superadminGuard (só superadmin grava); token preservado se vier vazio. GET /api/rx/config (authGuard) devolve {rx_url, has_token, configured} (sem token).
  • POST /api/rx/devices cria no RX com clinic_name = clinicaId (e-mail default único por clínica+PC: <clinicaId>.<pcSlug>@rx.scoreodonto.com). GET /api/rx/devices?clinicaId= filtra por clinic_name.
  • Revertido o que era do modelo errado: bypass do tenantGuard (removido) e seletor de clínica do RxConfigView (removido).
  • RxConfigView: card CONFIGURAÇÃO GERAL DO RX (URL + token) editável só superadmin; clínica vê status read-only + cadastra seus PCs. Superadmin vê PCs de todas as clínicas.
  • backend.ts: getRxConfig() (0 args, {rx_url,has_token,configured}), putRxConfig({rx_url,api_token}), createRxDevice sem clinicName. RXPlugin.tsx: getRxConfig() sem arg.

FASE 1 — servidor RX (dev.rx, dental-server/routes/images.js) (feito, restart dev):

  • Filtro ?clinic=<clinicaId> (= clinic_name) em GET /api/images, /images/patients e /images/by-patient. Novo GET /api/images/unique-clinics (clinic_name + contagens).
  • Validado: node --check ok; rx_dev_api reiniciado (porta 3000); endpoints → 401 (auth ok). ⚠️ npm run dev = node server.js (SEM nodemon) → mudanças no RX exigem docker restart rx_dev_api.

Validado geral: tsc 0 erros; scoreodonto backend+frontend rebuildados no dev (bundle govSqkSI).

FASE 2/3 — leitura por proxy (feito, deploy dev bundle BVGGJsCu):

  • Backend: GET /api/rx/patients?clinicaId=&search= e GET /api/rx/images?clinicaId=&paciente= → proxy p/ RX /images/patients e /images/by-patient com clinic=<clinicaId> + api_token. Isolamento _rxClinicScopeOk: superadmin vê tudo; demais só clínicas com vínculo (senão 403).
  • backend.ts: getRxPatients, getRxPatientImages. RXPlugin consome esses (lista de pacientes + imagens do paciente). Thumbnails seguem públicos (/api/images/:id/thumbnail).

PENDENTE:

  • Falta o api_token colado em CONFIGURAÇÃO GERAL DO RX (superadmin) → sem ele, criar PC dá 400 e a leitura volta vazia. Gerar via POST /api/auth/tokens do RX (usuário is_admin).
  • Imagem em tamanho cheio (/images/:id/file-url) ainda é autenticada → falta proxy GET /api/rx/image/:id/file (galeria mostra thumbnail público; full-res é follow-up).
  • GTOs / upload / trigger-sync do RXPlugin ainda usam rxFetch direto (token vazio) — já estavam quebrados antes; migrar p/ proxy se forem usados.
  • Ativação do plugin ainda é localStorage + toggle só superadmin (PluginsView) → rever p/ ativação por clínica no backend.
  • Promover FASES 13 p/ prod (rx.scoreodonto.com + scoreodonto prod) após validar no dev.

4d. Plugins: Locação de Salas + Marketplace de Profissionais (2026-06-12) — deploy dev

Dois plugins novos no padrão do RX (catálogo PluginsView + pluginRegistry localStorage

  • pluginMenuItems no Sidebar). Bundle index-BylSJWKo.js; tsc 0 erros; migrations OK.

locacao-salas (teal, view /salas, views/plugins/SalasPlugin.tsx):

  • Tabelas salas + sala_reservas. CRUD do locador, marketplace (tipo/cidade/UF), valores por hora/turno/diária/semanal/mensal.
  • Reserva em transação com anti-conflito TRIPLO → 409: sala ocupada, reserva do próprio solicitante em outra sala, e agenda global (findOverbookingConflict via dentistas.usuario_id) — sala entra na invariante "1 lugar por vez".
  • Fluxo: pendente → confirmar/recusar (locador) / cancelar (ambos). Notificações (notificarUsuario) + audit_log (entidade='sala'|'sala_reserva').

profissionais-marketplace (laranja, view /marketplace-profissionais, views/plugins/ProfissionaisPlugin.tsx) — decisão: Delivery (nível 8) virou "Profissionais":

  • usuarios + cep, raio_atuacao_km, bio_profissional (raio é informativo, sem geodistância).
  • GET /api/profissionais/marketplace inclui biomédico (o diretório antigo não incluía) e filtra por CEP-prefixo. GET/PUT /api/profissionais/perfil (opt-in disponivel_diretorio).
  • Tabela propostas_profissional: clínica→profissional (POST /api/propostas com tenantGuard, anti-duplicata pendente), aceitar/recusar (profissional) / cancelar (clínica) + notificações.
  • Consolidado com o Diretório antigo (bundle DZJWcMDu): com o plugin ativo, o item "DIRETÓRIO PROF." some do menu e a rota /diretorio-profissionais renderiza o plugin; com o plugin inativo, o DiretorioProfissionaisView antigo continua intacto (sem regressão). DiretorioProfissionaisView//api/profissionais-disponiveis podem ser removidos quando o plugin virar padrão.

Como testar: superadmin → PLUGINS → ativar (lembrete: ativação é localStorage por navegador — pendência 4c vale aqui também) → itens aparecem no menu dos papéis operacionais. Propostas exigem workspace ativo (clínica/consultório); aba MEU PERFIL só para dentista/biomedico/protetico.

⚠️ LIMPEZA pós-teste (banco é de PRODUÇÃO): dados de teste ficam em salas, sala_reservas, propostas_profissional, colunas novas de usuarios e respingos em notificacoes/audit_log. Limpar com:

DELETE FROM sala_reservas; DELETE FROM salas; DELETE FROM propostas_profissional;
-- e reverter perfis de teste:
-- UPDATE usuarios SET disponivel_diretorio=false, raio_atuacao_km=NULL, bio_profissional=NULL WHERE id='...';

Fluxos institucionais de locação (2026-06-13, bundle CvP3Q2X_):

  • sala_reservas + clinica_id/clinica_nome/profissional_usuario_id/profissional_nome.
  • Clínica aluga sala: ReservaModal ganhou "RESERVAR COMO: PESSOAL | "; como unidade, pode designar o profissional (GET /api/dentistas?clinicaId=, só os com usuario_id). O anti-conflito roda sobre o OCUPANTE (designado ou solicitante) — COALESCE(profissional_usuario_id, solicitante_id) nas reservas + agenda global. Designado é notificado. Membros da unidade veem/cancelam as reservas dela (GET /api/sala-reservas/minhas?clinicaId=, vínculo validado).
  • Consultório/clínica anuncia a própria sala: checkbox "ANUNCIAR PELA UNIDADE: " no cadastro/edição (grava salas.clinica_id; PUT aceita desvincular). Marketplace e cards mostram "POR " (join clinicas).
  • Dentista aluga sala: fluxo pessoal original, inalterado.

Alinhado com /superadmin/financeiro (bundle CXf4eiwZ): a Tabela de Preços ganhou a seção PLUGINS (ADD-ONS) — chaves plugin:locacao-salas e plugin:profissionais-marketplace no mesmo settings.pricing (0 = incluído no plano). GET /api/superadmin/pricing agora mescla defaults (chaves novas aparecem em configs antigas) e ganhou a linha biomédico que faltava. Os cards da Área de Plugins exibem o preço configurado (chip "R$ X/MÊS" / "INCLUÍDO NO PLANO"). A COBRANÇA efetiva do add-on (bloquear plugin sem pagamento) segue o modelo manual atual de pagamentos por usuário — não implementada.

PENDENTE (roadmap acordado):

  • Cobrança efetiva dos add-ons (ativação por clínica no backend + verificação de pagamento — junto com a pendência 4c de tirar a ativação do localStorage).
  • Score Professional (motor de pontuação — alimentado por reservas/propostas/faltas).
  • Geodistância real (CEP→lat/lng; raio passa a filtrar de verdade).
  • Fotos das salas (upload via /app/media, como as fotos de orto).
  • Pagamento da locação (MercadoPago já existe na tutoria — reaproveitar).
  • Proposta aceita → criar vinculos automaticamente (hoje o aceite é só status+notificação).

5. Estado de versionamento

  • NADA foi commitado nesta sessão. Tudo está no working tree, rodando contra o banco de prod.
  • Regra do projeto: commit/push só com autorização explícita — o push dispara o pipeline de produção via webhook do Gitea (pode sobrescrever trabalho em andamento).
  • Arquivos que ESTA sessão editou: backend/server.js, frontend/App.tsx, frontend/components/Sidebar.tsx, frontend/services/backend.ts, frontend/views/AgendaView.tsx, frontend/views/LoginView.tsx, frontend/views/LandingPage.tsx. (Demais arquivos modificados/untracked são WIP de sessões anteriores.)