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>
This commit is contained in:
VPS 4 Builder
2026-06-16 06:45:51 +02:00
parent 0824810c4d
commit 37cc818f4c
11 changed files with 435 additions and 0 deletions
@@ -0,0 +1,165 @@
# Auditoria Funcional — ScoreOdonto (estado atual + roadmap V1/V2/V3)
> Documento mestre da auto-auditoria (read-only). Gerado a partir do código em 2026-06-15.
> **Nota de nomenclatura:** NÃO existe módulo "Delivery" separado. O antigo "Delivery" foi
> renomeado e é o marketplace de **Profissionais** (propostas clínica→profissional).
> Onde o prompt de benchmark cita "Delivery", leia-se **Profissionais**.
---
## PARTE I — Arquitetura funcional atual
**Papéis (9):** `superadmin, admin, donoclinica, donoconsultorio, dentista, biomedico, protetico, funcionario, paciente`.
**Modelo multi-tenant:** workspaces = linhas em `clinicas` (tipo `clinica` ou `pessoal`); acesso via `vinculos`; isolamento por `clinica_id` (auditado e endurecido em 8 rodadas de segurança). É um diferencial maduro.
### Inventário de telas (40+), por domínio
| Domínio | Telas | Acesso |
|---|---|---|
| Núcleo clínico | Dashboard, Agenda, Pacientes, Ortodontia (OrthoView/PhotoWorkspace/Reports/Annotator), Meus Tratamentos, Lançar GTO | dono/admin/funcionário/dentista/bio/protético |
| Catálogos | Dentistas, Especialidades, Procedimentos, Planos/Convênios | dono/admin/funcionário |
| Financeiro/Comercial | Financeiro, Relatórios, Orçamentos, **Contratos (motor)**, Gestão de Leads | dono/admin/funcionário |
| Marketplace/Recursos | Locação de Salas, **Profissionais (marketplace)**, Diretório Prof., RX/Radiografias, Dispositivos RX | quase todos |
| Tutoria (orto) | Ser Tutor, Painel Tutor, Gestão Tutores | dentista/admin |
| Gestão | Minhas Clínicas, Equipe/Usuários (RBAC), Plugins, Configurações, Notificações | dono/admin |
| Onboarding/Auth | Landing, Login, OnboardingChat, Cadastro Dentista, Criar Clínica, Aguardando Convite, Contato Público | público |
| Superadmin | SuperAdminView (stats/usuários/clínicas/logs/financeiro/notificações), UpdateDb, Sync | superadmin |
### Menu (real)
Visão Geral · Agenda · Pacientes · Lançar GTO · Ortodontia · Financeiro · Relatórios · Orçamentos · Contratos · Gestão de Leads · Meus Tratamentos · Minhas Clínicas · Dentistas · Especialidades · Procedimentos · Planos/Convênios · Equipe/Usuários · Diretório Prof. · Tutoria (3) · Notificações · Plugins · Configurações.
Menu base + filtro `accessByRole` + RBAC por cargo (`funcoes.permissoes/nivel`; array vazio herda o menu do papel). Paciente vê só Tratamentos/Notificações/Config.
Não há: menus contextuais (botão direito), menus dentro de tabelas, breadcrumbs. Navegação é hash-route + cards/modais.
### Backend
≈150 endpoints. Guards: `authGuard / tenantGuard / superadminGuard / adminGuard`. Auditoria via `audit_log`. Tempo real em `/api/ws`.
---
## PARTE II — Gap analysis
### Forte (vantagem competitiva)
- Multitenant/multiempresa (workspaces universais; isolamento por clínica auditado).
- Agenda — anti-overbooking GLOBAL (mesma pessoa não dobra entre clínicas), faltas/reagendar/confirmar/reencaminhar, pendências "a reagendar", presença em tempo real, importar Google.
- Prontuário clínico — odontograma FDI + fotos (avaliação); Ortodontia completa (tratamento/evoluções/documentação fotográfica com anotador/escala + PDFs de tutoria); RX (plugin + laudos); GTO/guias.
- Contratos — motor universal (modelos+variáveis, gerador, assinatura interna com IP/hash, vigência/alertas, 28 modelos de catálogo). Acima da média de mercado.
- Marketplace (salas anti-conflito triplo + **Profissionais** com propostas/diretório).
- RBAC por cargo/nível + realtime.
### Parcial
- Financeiro — só lançamentos receita/despesa + dashboard. Falta contas a pagar/receber estruturado, vencimentos, baixa.
- CRM — só Leads + conversão→paciente. Sem funil/oportunidades/campanhas.
- Convênios — "Planos/Convênios" é catálogo simples; sem regras de cobertura/repasse/glosa.
- Comunicação — notificações in-app/realtime + webhooks WhatsApp na infra; sem disparo de WhatsApp/SMS/e-mail pelo app; sem lembretes automáticos de consulta.
- Relatórios — só `productivity`.
- RH — dentistas/horários/equipe/funções; sem escalas/ponto/folha; repasse só na tutoria (manual).
- Mobile — web responsivo; sem PWA/app.
### Ausente
- Estoque (produtos, lotes, validade, inventário, compras, fornecedores).
- Financeiro avançado: fluxo de caixa, DRE, centros de custo, conciliação bancária, comissões/rateios/repasse estruturados, gateways PIX/cartão/boleto, carnês/parcelamento.
- Prontuário: anamnese estruturada, periodontograma, assinatura do paciente no prontuário, consentimento (TCLE).
- Agenda: lista de espera, recorrência, bloqueios, recursos/equipamentos como agendáveis.
- CRM: funil, campanhas, automações (WhatsApp), pipeline de orçamento.
- **Profissionais:** atendimento geolocalizado/geodistância (gap já mapeado), avaliações/reputação. (Não confundir com "Delivery" — é a evolução do módulo Profissionais.)
### Obsoleto / dívida técnica
- `UpdateDbView`/`Sync` (legado de planilha) — manter só superadmin.
- `/fotos/:id/raw` públicas por id (mitigado com URL assinada; ideal evoluir).
- Orçamento gera um "Contrato" (documento) — convém unificar o fluxo orçamento→contrato no motor.
---
## PARTE III — Roadmap (V1 / V2 / V3)
Matriz: Impacto (I) · Retorno financeiro (R) · Esforço (E) — escala 15.
### V1 — Quick wins (alto I, baixo E)
| Item | I | R | E |
|---|---|---|---|
| Anamnese + TCLE estruturados no prontuário | 5 | 3 | 2 |
| Lembretes automáticos de consulta (WhatsApp/e-mail via webhook existente) | 5 | 4 | 2 |
| Agenda: bloqueios + lista de espera (reaproveitar "interesses"/anti-overbooking) | 4 | 3 | 2 |
| Contas a pagar/receber sobre o `financeiro` atual (status/vencimento/baixa) | 5 | 4 | 3 |
| Orçamento → Contrato → Financeiro (ligar os 3 que já existem) | 5 | 5 | 3 |
| Pacote de relatórios essenciais (produção, faltas, financeiro por período, conversão de leads) | 4 | 3 | 2 |
### V2 — Diferenciação e receita (alto R)
| Item | I | R | E |
|---|---|---|---|
| Comissões/Repasse profissional (agenda→procedimento→financeiro→profissional) | 5 | 5 | 4 |
| Gateways de pagamento (PIX/cartão/boleto) + carnês/parcelamento | 4 | 5 | 4 |
| CRM com funil + WhatsApp + automações (sobre Leads) | 5 | 5 | 4 |
| Fluxo de caixa + centros de custo | 4 | 4 | 3 |
| Periodontograma + evolução clínica padronizada | 4 | 2 | 3 |
| Contratos: partes externas por link + PDF server-side + Clicksign | 4 | 3 | 4 |
### V3 — Plataforma
| Item | I | R | E |
|---|---|---|---|
| Estoque completo (lotes/validade/inventário/compras/fornecedores) + integração financeira | 4 | 3 | 5 |
| DRE + conciliação bancária | 3 | 4 | 5 |
| RH: escalas/ponto/produção/folha | 3 | 3 | 5 |
| App/PWA mobile | 4 | 3 | 4 |
| Profissionais: atendimento/geodistância + avaliações/reputação | 3 | 3 | 4 |
| BI/Dashboards configuráveis + alertas automatizados | 3 | 3 | 4 |
---
## Mapa de conexões entre telas
### Já existe
- Agenda → Novo Agendamento → busca Paciente → cria agendamento.
- Lead → Converter → Paciente (+ 1º agendamento).
- Agendamento → Solicitar Avaliação → novo slot vinculado.
- Paciente → tile Orçamentos → gera contrato/print.
- Sala → Reserva → anti-conflito.
- Profissional (marketplace) → Proposta → notificação.
### Falta ligar (o "tecido conjuntivo" — maior ganho de V1/V2)
- Procedimento realizado (agenda) → lançamento Financeiro → Comissão → Repasse profissional.
- Orçamento aprovado → Contrato (motor) → Financeiro (parcelas).
- Contrato assinado → vigência → cobrança recorrente.
- Estoque ← consumo por procedimento.
> Diagnóstico central: módulos fortes, pouco conectados. O maior retorno não é criar
> módulos novos, e sim LIGAR agenda↔financeiro↔contrato↔profissional.
---
## Síntese por cenário
- Clínica/Consultório: prontos; faltam financeiro avançado + comissões.
- Dentista autônomo: workspace "pessoal" já funciona; falta repasse/produção.
- Biomédico: áreas/especialidades ok; harmonização (contrato já criado); falta agenda estética dedicada.
- Locação de salas: maduro; falta integrar reserva→financeiro/contrato de locação (o motor já tem o modelo).
- **Profissionais (marketplace, ex-"Delivery"):** maduro; falta avaliações/reputação, fechamento via contrato e geodistância.
- Multiempresa/Multitenant: o ponto mais forte — base sólida para escalar.
---
# DEEP-DIVE 1 — Financeiro + Comissões/Repasse (2026-06-15)
## A) Checklist da tela Financeiro
- **Objetivo:** registrar/acompanhar lançamentos (receita/despesa) por status.
- **Acesso:** Menu → Financeiro; dono/admin/funcionário (escopo por clínica, `tenantGuard`).
- **Dados:** lista (paciente, descrição, valor, vencimento, status) + KPIs Recebido/Pendente/Atrasado.
- **Tabela `financeiro`:** `id, clinica_id, pacientenome(texto), descricao, valor, datavencimento, status(Pago/Pendente/Atrasado), formapagamento, tipo(RECEITA/DESPESA)`. 0 linhas.
- **Botões:** Filtrar(status) · Novo lançamento · linha: Confirmar pagamento, Gerar recibo (download), Excluir.
- **Integrações:** PARCIAL — `LancarProcedimentoModal → saveFinanceiro` (procedimento gera 1 lançamento). Nenhuma outra.
- **Limitações:** soma por status; "Atrasado" manual.
## B) Diagnóstico — Financeiro é uma "ilha"
A `financeiro` não tem chaves de ligação: `pacientenome` é texto (não FK); não há `profissional_id`, `agendamento_id`, `procedimento_id`, `orcamento_id`, `contrato_id`. Despesa = só `tipo=DESPESA` (sem fornecedor/centro de custo/recorrência). **Comissão/Repasse inexistente** (`procedimentos.valorparticular` sem %; `dentistas` sem repasse). A maior alavanca é a FK **`profissional_id`** — sem ela, não há comissão, repasse nem relatório de produção.
## C) Fluxo conectado proposto
Agenda (paciente+dentista+procedimento) → concluído → LANÇAMENTO (com FKs) → regra de COMISSÃO → REPASSE por período. Orçamento aprovado → Contrato (motor) → parcelas (contas a receber). Despesa → fornecedor/centro de custo → fluxo de caixa/DRE.
## D) Evolução do modelo
- **V1:** FKs em `financeiro` (`paciente_id, profissional_id, procedimento_id, agendamento_id, contrato_id, origem`); `pago_em`; job de "Atrasado"; contas a pagar (fornecedor/centro de custo/recorrente); relatórios essenciais.
- **V2:** `comissao_regras` (por procedimento/profissional/convênio; %/fixo/produção; base bruta/líquida); `comissao_valor` no lançamento; `repasses` (consolida por profissional/período) formalizado pelos modelos "Contrato por Comissão/Produção/Percentual" do motor.
## E) Regras de comissão por cenário
Dentista da clínica (% líquido por especialidade/convênio); autônomo (100%, ou taxa de sala); sala alugada (receita aluguel, liga `sala_reservas`); biomédico/harmonização (% + material parametrizável); Profissionais/marketplace (contrato de comissão define %); multiempresa (repasse consolidado por unidade).
> Backlog acionável do V1 Financeiro: ver `BACKLOG-FINANCEIRO-V1.md`.
+156
View File
@@ -0,0 +1,156 @@
# Backlog V1 — Financeiro (ScoreOdonto)
> Derivado do `AUDITORIA-FUNCIONAL-SCOREODONTO.md` (Deep-dive 1). Apenas planejamento — nada implementado.
> Padrões do projeto a seguir em toda história: `tenantGuard` + escopo `clinica_id`, soft delete onde fizer sentido,
> `registrarAudit` nas mutações, helpers `buildInsert/buildUpdate`, frontend `useToast`/`useConfirm`,
> rebuild da imagem DEV (frontend nginx + backend) — ver `feedback_deploy_workflow`.
> Estimativa: P (≤0,5 dia) · M (12 dias) · G (3+ dias).
Estado atual relevante: tabela `financeiro(id, clinica_id, pacientenome[texto], descricao, valor, datavencimento, status[Pago/Pendente/Atrasado], formapagamento, tipo[RECEITA/DESPESA])`, 0 linhas. Único elo: `LancarProcedimentoModal → saveFinanceiro`.
---
## ÉPICO 1 — Ligar o Financeiro (FKs de origem) — ✅ IMPLEMENTADO (DEV)
**Por quê:** sem `profissional_id`/`paciente_id` não há comissão, repasse, nem relatório por paciente/profissional. É o pré-requisito de tudo.
> **Status (DEV):** H1.1 ✅ (FKs + índices + soft delete + cron "Atrasado" — também cobre H2.2/H2.4) · H1.2 ✅ (`FinanceiroView` envia `paciente_id`) · H1.3 ✅ (`LancarProcedimentoModal` envia `paciente_id`/`procedimento_id`/`origem='procedimento'`). **Extra entregue:** prontuário de procedimentos realizados (`procedimentos_realizados` + fotos antes/depois) com regra de **sem comissão** em reabertura pelo mesmo profissional e em "refazer na garantia".
### H1.1 — Adicionar chaves de ligação ao lançamento — **M**
- **Como** sistema, **quero** que cada lançamento referencie paciente/profissional/origem **para** rastrear e relatar.
- **Migração:** `ALTER TABLE financeiro ADD COLUMN IF NOT EXISTS paciente_id TEXT, profissional_id TEXT, procedimento_id TEXT, agendamento_id TEXT, contrato_id TEXT, origem TEXT DEFAULT 'manual', pago_em TIMESTAMPTZ;` + índices `(clinica_id, status, datavencimento)` e `(clinica_id, profissional_id)`.
- **Backend:** `POST/PUT /api/financeiro` aceitam os novos campos (já é `buildInsert`); manter `clinica_id` do `tenantGuard`.
- **Critérios:** lançamento salva e retorna os campos; antigos (sem FK) continuam válidos; `node --check` + E2E (criar com `profissional_id` e ler).
- **Risco:** `pacientenome` legado permanece (não migrar dado, base vazia em DEV).
### H1.2 — Vincular paciente real (não só nome) no lançamento — **M**
- **Como** recepção, **quero** selecionar o paciente cadastrado **para** o financeiro aparecer no histórico do paciente.
- **Frontend:** no modal de novo lançamento (`FinanceiroView`), trocar campo de nome livre por **busca de paciente** (reusar `getPacientes` + componente do `AppointmentModal`); preencher `paciente_id` + `pacientenome` (snapshot).
- **Critérios:** ao gravar, `paciente_id` preenchido; busca por nome/CPF/telefone funciona; manter compatível com lançamento sem paciente (despesa).
### H1.3 — Procedimento concluído gera lançamento com FKs — **M**
- **Como** sistema, **quero** que ao concluir um procedimento na agenda o lançamento já venha com `paciente_id/profissional_id/procedimento_id/agendamento_id`.
- **Backend/Frontend:** `LancarProcedimentoModal` (que já chama `saveFinanceiro`) passa as FKs; valor sugerido = `procedimentos.valorparticular`.
- **Critérios:** lançamento criado a partir de procedimento tem `origem='procedimento'` + as 4 FKs; valor pré-preenchido.
---
## ÉPICO 2 — Contas a Receber / a Pagar
**Por quê:** o Financeiro hoje é uma lista plana; falta gestão de vencimentos, baixa e despesas estruturadas.
### H2.1 — Baixa de pagamento com data e meio — **P** — ✅ IMPLEMENTADO (DEV)
> Modal `BaixaPagamentoModal` (data + forma) no `FinanceiroView` substitui o "marcar Pago" direto; `useConfirm` p/ valores ≥ R$ 1.000. `PUT /api/financeiro/:id` carimba `pago_em` (se ausente) e registra `registrarAudit` (`baixa_pagamento`, idempotente). **Bug latente corrigido:** `enforceUppercase` (service) mandava `PAGO`/`PIX`, violando os CHECK do banco (`Pago`/`Pix`); backend agora normaliza `status`/`formapagamento`/`tipo` no POST e no PUT (`normalizeFinanceiro`). Era a causa de a tabela `financeiro` ficar sempre vazia.
- **Como** financeiro, **quero** confirmar pagamento registrando `pago_em` e a forma **para** ter caixa real.
- **Backend:** `PUT /api/financeiro/:id` aceita `status='Pago'` + `pago_em=NOW()` (ou data informada). (PUT já é tenant-scoped.)
- **Frontend:** botão "Confirmar pagamento" abre mini-form (data + forma) em vez de marcar direto; `useConfirm` para valores altos.
- **Critérios:** ao confirmar, grava `pago_em` e `formapagamento`; KPI Recebido considera `pago_em`.
### H2.2 — Job de "Atrasado" automático — **P**
- **Como** sistema, **quero** marcar lançamentos vencidos como Atrasado **para** o KPI ser fiel.
- **Backend:** cron (boot + 6h, padrão `processarVigenciasContratos`): `UPDATE financeiro SET status='Atrasado' WHERE status='Pendente' AND datavencimento < CURRENT_DATE AND deleted_at IS NULL`.
- **Pré-req:** adicionar `deleted_at` ao `financeiro` (soft delete) — hoje DELETE é físico (`H2.4`).
- **Critérios:** lançamento vencido vira Atrasado sem ação manual.
### H2.3 — Despesas com fornecedor e centro de custo — **M** — ✅ IMPLEMENTADO (DEV)
> Migração `fornecedor/centro_custo/recorrente`. `NovoLancamentoModal` ganhou toggle **Receita/Despesa**: em DESPESA o paciente fica opcional e aparecem fornecedor + centro de custo (datalist com presets) + recorrente. Normalização de enums já cobre o casing. **H3.3 entregue junto:** `porCategoria` (despesas por centro de custo) no `GET /api/financeiro/relatorio` + bloco no `ReportsView`.
- **Como** financeiro, **quero** classificar despesas (fornecedor, categoria/centro de custo, recorrente) **para** análise de saída.
- **Migração:** `ALTER TABLE financeiro ADD COLUMN IF NOT EXISTS fornecedor TEXT, centro_custo TEXT, recorrente BOOLEAN DEFAULT false;`
- **Frontend:** quando `tipo=DESPESA`, exibir fornecedor + centro de custo + recorrente.
- **Critérios:** despesa salva com classificação; filtro por categoria no relatório (Épico 3).
### H2.4 — Soft delete + auditoria do lançamento — **P**
- **Migração:** `ALTER TABLE financeiro ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ;`
- **Backend:** `DELETE /api/financeiro/:id``UPDATE ... SET deleted_at=NOW()`; GET filtra `deleted_at IS NULL`; `registrarAudit`.
- **Critérios:** excluir não apaga fisicamente; some das listas; audit registra autor.
---
## ÉPICO 3 — Relatórios financeiros essenciais
**Por quê:** hoje só existe `/api/reports/productivity`.
> **Status (DEV):** H3.1 ✅ + H3.2 ✅ — endpoint único `GET /api/financeiro/relatorio?inicio&fim&pacienteId&profissionalId` (tenantGuard, escopo `clinica_id`, soft delete) retornando `resumo` (recebido/a_receber/atrasado/despesa), `porMes`, `porForma` e `lancamentos`. UI em `ReportsView` (componente `RelatorioFinanceiro`): KPIs + barras por mês + por forma de pagamento + tabela + export CSV, com filtros de profissional (select) e paciente (busca). H3.3 (despesas por categoria/centro de custo) depende de H2.3 — pendente.
### H3.1 — A receber/recebido por período — **M**
- **Backend:** `GET /api/financeiro/relatorio?inicio=&fim=` (tenantGuard) → totais por status, por dia/mês, por forma de pagamento.
- **Frontend:** seção em `ReportsView` (ou aba no Financeiro) com gráfico simples (reaproveitar padrão do Dashboard).
- **Critérios:** filtros de período; soma confere com lançamentos.
### H3.2 — Financeiro por paciente e por profissional — **M**
- **Backend:** `GET /api/financeiro/relatorio?pacienteId=` e `?profissionalId=` (usa as FKs do Épico 1).
- **Critérios:** lista lançamentos do paciente/profissional no período; base para o repasse (V2).
### H3.3 — Despesas por categoria/centro de custo — **P** — ✅ IMPLEMENTADO (DEV, junto com H2.3)
- **Backend:** agregação por `centro_custo` no relatório (`porCategoria`). ✓
- **Critérios:** total por categoria no período. ✓ (bloco "Despesas por centro de custo" no `ReportsView`)
---
## ÉPICO 4 — Orçamento → Contrato → Financeiro
**Por quê:** os 3 módulos existem isolados; ligá-los fecha o ciclo comercial.
> **Status (DEV):** H4.1 ✅ — orçamento **Assinado** gera contas a receber automaticamente (entrada + N parcelas mensais a partir de `data_inicio`, `origem='orcamento'`, FKs paciente/profissional/`contrato_id`), idempotente via `orcamentos.financeiro_gerado`; helper `gerarFinanceiroOrcamento`; toast no `OrcamentoModal`.
> **H4.2 ✅** — contrato (motor) com **cobrança recorrente**: campos `contratos.{cobranca_automatica,valor_recorrente,periodicidade(mensal|anual),dia_cobranca}` + `financeiro.competencia`. Helper `gerarCobrancaContratoCompetencia` (idempotente por contrato_id+competência, origem='contrato', dia clamp 28) e `gerarCobrancasRecorrentes` rodando no cron `processarVigenciasContratos` (boot+6h) sobre contratos **vigentes**. Endpoint manual `POST /api/contratos/instancias/:id/gerar-cobranca`. UI: config no `GeradorModal` + botão "Gerar cobrança do mês" nos contratos vigentes (`ContratosView`).
> **Renovação automática ✅** — no mesmo cron, contratos `renovacao='automatica'` com `data_fim` vencido estendem a vigência por nova duração igual à anterior (`data_inicio=data_fim+1`, `data_fim += (data_fimdata_inicio)+1`; fallback +12 meses sem início) e reabilitam o aviso de vencimento.
### H4.1 — Orçamento aprovado gera contas a receber — **M** — ✅
- **Como** recepção, **quero** que ao aprovar um orçamento as parcelas entrem no Financeiro **para** não relançar manualmente.
- **Backend:** ao mudar status do orçamento para "Aprovado/Assinado", criar N lançamentos `RECEITA` (`origem='orcamento'`, `paciente_id`, vencimentos por parcela) a partir de `valor_total/entrada/parcelas`.
- **Critérios:** aprovar orçamento de 3x cria 3 lançamentos pendentes com vencimentos; idempotente (não duplica).
### H4.2 — Contrato (motor) com cobrança recorrente — **M** (ponte p/ V2)
- **Como** sistema, **quero** que um contrato vigente com vigência/parcelas gere lançamentos **para** cobrança recorrente.
- **Backend:** estender o cron de vigência (`processarVigenciasContratos`) para emitir lançamentos conforme `valores`/periodicidade do contrato (`contrato_id` na FK).
- **Critérios:** contrato mensal gera lançamento por competência; não duplica.
---
## Sequência sugerida (entrega incremental)
1. **H1.1 → H2.4 → H2.2** (FKs + soft delete + atrasado) — base sólida e segura.
2. **H1.2 → H1.3** (paciente real + procedimento→lançamento) — liga clínica↔financeiro.
3. **H2.1 → H2.3** (baixa + despesas) — caixa fiel.
4. **H3.1 → H3.2 → H3.3** (relatórios) — visão gerencial.
5. **H4.1 → H4.2** (orçamento/contrato→financeiro) — fecha o ciclo e prepara o V2 (comissão/repasse).
## Definição de pronto (toda história)
`node --check` (backend) + build OK (frontend) · endpoints com `tenantGuard` + escopo `clinica_id` · mutações com `registrarAudit` · E2E mínimo (criar/ler/escopo cross-tenant 403) · rebuild + recreate DEV · validado em `dev.scoreodonto.com`.
## Riscos / observações
- Não migrar dados legados (base de DEV vazia); em PROD, planejar backfill de `paciente_id` por `pacientenome` (fuzzy) — fora do V1.
- `formapagamento` é texto livre hoje; padronizar enum (PIX/Cartão/Boleto/Dinheiro) facilita relatórios e o V2 (gateways).
- Comissão/Repasse é **V2** — depende do Épico 1 (FK `profissional_id`) entregue aqui.
---
## V2 — Comissão / Repasse — ✅ IMPLEMENTADO (DEV)
Decisões: base **selecionável** (recebido=caixa via status Pago/pago_em · produção=competência via vencimento); percentual **por procedimento** com **regra padrão** de fallback (`comissao_regras.procedimento_id NULL`); **fechamento persistido** (snapshot imutável `repasses` + `registrarAudit`).
- **Tabelas:** `comissao_regras (clinica_id, procedimento_id NULL=padrão, percentual)` (unique por clínica+proc); `repasses (snapshot por profissional: período, base, base_valor, comissao_valor, detalhes JSONB, status, criado_por)`.
- **Cálculo** (`computeComissoes`): RECEITA, `sem_comissao=false`, `profissional_id NOT NULL`; pct = regra do procedimento → senão padrão → senão 0; agrupa por profissional com detalhe por lançamento. Garantia/reabertura do mesmo profissional (sem_comissao) já saem de fora.
- **Endpoints:** `GET/PUT/DELETE /api/comissoes/regras`, `GET /api/comissoes/relatorio`, `POST /api/repasses/fechar`, `GET /api/repasses[/:id]`, `POST /api/repasses/:id/cancelar`.
- **Frontend:** `ComissoesView` (menu COMISSÕES, owner-only) com abas Relatório/Fechamento (toggle recebido/produção, expandível por profissional, botão Fechar), Regras de % (padrão + por procedimento) e Histórico (lista de fechamentos + cancelar).
- **Gaps (✅ implementados DEV):** (1) **override de % por profissional** (`comissao_regras.profissional_id`; prioridade prof > procedimento > padrão; unique inclui profissional); (2) **custo de laboratório** por procedimento (`comissao_regras.custo_lab`) deduzido do valor antes da comissão (base líquida); (3) **marcar repasse como pago** (`POST /api/repasses/:id/pagar`) que gera **DESPESA** no financeiro (origem='repasse', centro_custo='COMISSÕES', sem_comissao, Pago) e grava `repasses.pago_em/financeiro_id`; repasse pago não pode ser cancelado.
- **Estorno ao cancelar (✅):** cancelar repasse **pago** agora soft-deleta a DESPESA gerada (`financeiro.deleted_at`) e zera `repasses.financeiro_id`; auditoria registra `despesa_estornada`. UI: botão cancelar disponível p/ status fechado e pago (confirm avisa do estorno).
- **GTO → Financeiro (✅ — gap descoberto):** `PUT /api/gto-builder/:id/finalizar` agora gera **RECEITA** (convênio a receber, vencimento +30d, origem='gto', FKs paciente/profissional) — idempotente via `gto_builders.financeiro_id`. **A tela `LancarGTO` não chamava os endpoints** (`handleFinalize` era stub); agora persiste builder+itens+finaliza (service `createGtoBuilder/addGtoItem/finalizarGto`). Assim a produção por convênio entra no financeiro e na comissão (profissional=dentistaid).
- **Convênios padronizados (✅):** GTO passou a usar **convênios reais** da tabela `planos` (menu PLANOS/CONVÊNIOS) via dropdown no `LancarGTO` (carrega `getPlanos`); `credenciadaid` agora guarda o **id do plano** (obrigatório). O lançamento GTO grava `financeiro.convenio_id` e a descrição traz o nome do convênio (`GTO <id> — <nome>`).
- **Relatório por convênio (✅):** `GET /api/financeiro/relatorio` agora retorna `porConvenio` (RECEITA agrupada por `convenio_id`, join `planos`, com total/recebido/a_receber/qtd; sem convênio = "Particular"); bloco "Receitas por convênio" no `ReportsView`. Respeita os filtros de período/paciente/profissional.
- **Comprovante do repasse (✅):** `repasses.comprovante_{filename,mimetype,nome}` + `POST /api/repasses/:id/comprovante` (multer memória, imagem/PDF, MEDIA_ROOT/repasses/<id>/) e `GET /api/repasses/comprovante/:id/raw` (URL assinada `signMedia`/`verifyMedia`); listagem retorna `comprovante_url`. UI no Histórico (`ComissoesView`): anexar (clip) / ver (PDF) nos repasses pagos.
- **Competência por conclusão (✅):** `financeiro.data_competencia` (DATE) gravada na origem — procedimento realizado usa `data_conclusao` (campo "Concluído em" no prontuário; default hoje), GTO usa a data de finalização. `computeComissoes` base **produção** passou a usar `COALESCE(data_competencia, datavencimento)` (fallback p/ vencimento em lançamentos antigos). Recebido segue por `pago_em`.
- **Gaps remanescentes:** glosa por item de GTO; recurso/protocolo de glosa com prazos.
---
## GLOSAS DE CONVÊNIO — ✅ IMPLEMENTADO (DEV)
**Por quê:** convênio nega (glosa) parte do recebível; precisa marcar/gerenciar e recorrer.
- **Modelo:** `financeiro.valor_glosa` (acumulado) + tabela `glosas (financeiro_id, valor, motivo, status[glosado|recuperado], autor)`. Líquido a receber = `valor valor_glosa`.
- **Endpoints:** `GET /api/glosas[?status=]`, `GET /api/glosas/recebiveis` (RECEITA de convênio/GTO não totalmente glosada), `POST /api/glosas` (valida não exceder o restante; incrementa `valor_glosa`), `POST /api/glosas/:id/recuperar` (recurso ganho → devolve valor), `DELETE /api/glosas/:id` (correção). Todos tenantGuard + `registrarAudit`.
- **Frontend:** `GlosasView` (menu GLOSAS, visível p/ donos e funcionário) — aba "Recebíveis/Marcar glosa" (busca + modal valor+motivo) e aba "Glosas" (filtro glosado/recuperado, recuperar/excluir).
- **Glosa no relatório (✅):** `GET /api/financeiro/relatorio``resumo.glosa` + `resumo.a_receber_liquido` (= a_receber+atrasado glosa) e coluna `glosa` em `porConvenio`; UI mostra KPI Glosa (com líquido) e coluna no relatório por convênio.
- **Glosa por item + recurso (✅):** `glosas.{gto_item_id,item_descricao,protocolo,prazo_recurso}`; status agora `glosado|recurso|recuperado`. `GET /api/glosas/itens/:financeiroId` lista os `gto_items` da guia (via `gto_builders.financeiro_id`) com flag `ja_glosado`; POST aceita item/protocolo/prazo e bloqueia item já glosado (409). `POST /api/glosas/:id/recorrer` (protocolo+prazo, mantém valor_glosa); recuperar/excluir tratam status recurso. UI: seletor de item no `GlosaModal` + protocolo/prazo, filtro "Em recurso", ação Recorrer e exibição de item/protocolo/prazo.
---
## Varredura final do módulo financeiro (DEV)
- **Isolamento multi-tenant:** todos os endpoints novos (financeiro/relatorio, comissoes/*, repasses/*, glosas/*, contratos/.../gerar-cobranca) usam `tenantGuard`; teste cross-tenant retornou **403** em todos (exceto os `*/raw` de mídia, protegidos por URL assinada). Mutações por `:id` validam `clinica_id` antes de alterar.
- **Índices adicionados:** `idx_financeiro_contrato_comp (contrato_id,competencia)`, `idx_financeiro_convenio`, `idx_glosas_financeiro`, `idx_glosas_item`, `idx_gto_builders_financeiro` (além dos já existentes `idx_financeiro_clinica/_prof`, `idx_glosas_clinica`, `idx_repasses_clinica`, `uq_comissao_regra2`).
- **Limitações conhecidas (futuro):** (1) glosa não reduz automaticamente a base de comissão "recebido" se um recebível glosado for marcado Pago com valor cheio — ao dar baixa em recebível de convênio com glosa, reduzir o valor pago; (2) orçamento `financeiro_gerado=true` evita duplicar parcelas, mas não regenera se o valor mudar após assinado (falta ação "regenerar").
## Contrato para dentista que atende convênios (✅)
Novo modelo global `cmod_odo_14`**"Atendimento a Convênios / Repasse (Dentista Credenciado)"** (tipo `dentista-clinica`), com cláusulas de: objeto (convênios atendidos), guias/GTO e faturamento, repasse (% sobre produção, base recebido/produção, prazo), custo de laboratório, glosas (responsável + dedução do repasse + registro de protocolo/prazo), obrigações, ausência de vínculo, vigência/rescisão, LGPD e foro. Novas variáveis em `VARIAVEIS_CONTRATO`: `{{CONVENIOS}}`, `{{PERCENTUAL_REPASSE}}`, `{{BASE_REPASSE}}`, `{{PRAZO_REPASSE}}`, `{{RESPONSAVEL_GLOSA}}`, `{{CUSTO_LABORATORIO}}` — aparecem automaticamente no Gerador de contrato (data-driven). Liga-se ao financeiro: repasse (base recebido/produção), custo de lab e glosas implementados no módulo.
@@ -0,0 +1,60 @@
# Checklist de Deploy para Produção — Pacote Financeiro + Salas (jun/2026)
> Consolida a rodada: **Financeiro V1 (Épicos 14)**, **V2 Comissão/Repasse**, **Glosas**, **GTO→Financeiro**, **modelo de contrato p/ convênio** e o **redesenho de Salas** (perfil Dono de Sala, sala como workspace, financeiro de locação, painel/relatório/agenda).
> O **procedimento canônico de deploy** é o `DEPLOY-PRODUCAO.md` — este arquivo é só o checklist desta entrega.
## 0. Arquitetura (sem ambiente isolado)
- **VPS4** = **desenvolvimento + builder**. O ambiente de teste é **`dev.scoreodonto.com`** (servido pela VPS4 via Traefik da VPS1) e é onde se **buildam** as imagens que vão pra produção.
- **VPS1** = **produção** (`scoreodonto.com`) — só recebe a imagem pronta e sobe.
- **VPS3** = **dados** (Postgres `scoreodonto`, Dragonfly, Gitea). **Dev e prod apontam para o MESMO banco `scoreodonto`.**
- ⚠️ Consequência (confirmada): **dev e prod compartilham o banco** → as migrações desta entrega **já foram aplicadas** ao banco real durante o desenvolvimento (rodam no boot do backend de dev). Promover é, na prática, **levar o CÓDIGO** para a VPS1 — o schema já está migrado. **Não há ambiente nem banco isolado** entre dev e produção.
## 1. Fluxo de promoção (resumo do DEPLOY-PRODUCAO.md §2)
1. `git push origin main` (na VPS4) — já feito (commit `3e64514`).
2. Bump de versão (`update-version.js``frontend/constants.ts`).
3. **Build da imagem Docker na VPS4** (`npm install` + `vite build` dentro do Dockerfile) — peso fora da produção.
4. **Envia a imagem pronta para a VPS1** (registry privado pela WireGuard — recomendado — ou `docker save | ssh | docker load`).
5. **VPS1:** `docker compose up -d` (troca o container pela imagem nova). Backend leva ~1030s no boot (reconecta Postgres/Dragonfly + roda migrações).
- *(Os scripts `deploy-prod-builder.sh` e `deploy-to-prod.sh` estão **DEPRECADOS** — ver seção "Fluxo ANTIGO" do `DEPLOY-PRODUCAO.md`. Não usar.)*
## 2. Pré-requisitos
- [ ] **Backup do banco** VPS3 (`pg_dump scoreodonto` — custom format `-Fc`). Como dev/prod compartilham o banco, esse backup é a **única rede de segurança**.
- [ ] Código em `origin/main` (Gitea) — ✅ `3e64514`.
- [ ] `node --check backend/server.js` OK; build do frontend OK.
- [ ] Validado em `dev.scoreodonto.com`.
## 3. Mudanças de banco (migrações idempotentes no boot — todas ADITIVAS)
> Já aplicadas no banco (dev=prod). Listadas para auditoria/rollback. Nenhuma destrutiva.
- **Novas tabelas:** `procedimentos_realizados`, `procedimento_realizado_foto`, `comissao_regras`, `repasses`, `glosas`.
- **ALTER `financeiro`:** `paciente_id, profissional_id, procedimento_id, agendamento_id, contrato_id, realizado_id, origem, sem_comissao, pago_em, deleted_at, fornecedor, centro_custo, recorrente, convenio_id, valor_glosa, data_competencia, competencia`.
- **ALTER `orcamentos`:** `financeiro_gerado`. **ALTER `contratos`:** `cobranca_automatica, valor_recorrente, periodicidade, dia_cobranca`. **ALTER `gto_builders`:** `financeiro_id`. **ALTER `sala_reservas`:** `financeiro_id`.
- **CHECK `usuarios_role_check` / `vinculos_role_check`** ganham `donosala` (DROP+ADD no boot — superset, seguro).
- **Índices:** troca `uq_comissao_regra``uq_comissao_regra2`; novos `idx_financeiro_contrato_comp`, `idx_financeiro_convenio`, `idx_glosas_*`, `idx_gto_builders_financeiro`.
- **Seed (boot):** `cmod_odo_14` "Atendimento a Convênios / Repasse" (ON CONFLICT DO NOTHING).
## 4. Backend — env / infra (VPS1)
- [ ] `DATABASE_URL``…@10.99.0.3/scoreodonto`. `JWT_SECRET` estável (login + URLs assinadas de fotos/comprovantes).
- [ ] **Volume de mídia** persistente (`MEDIA_PATH`): grava em `…/procedimentos/<id>/` e `…/repasses/<id>/`.
- [ ] **Cron** `processarVigenciasContratos` (boot + 6h): Atrasado, vigência, **renovação automática**, **cobrança recorrente** — conferir nos logs.
## 5. Frontend
- [ ] Imagem rebuildada com as views novas (`ComissoesView`, `GlosasView`, `ProcedimentosProntuario`, `SalaHomeView`) e alterações (`FinanceiroView`, `ReportsView`, `OrcamentoModal`, `ContratosView`, `LancarGTO`, `SalasPlugin`, `OnboardingChat`, `Sidebar`, `App.tsx`, `PluginsView`, `pluginRegistry`, `services/backend.ts`).
- [ ] Conferir: locação **sempre ativa** (MINHAS/ALUGAR SALAS aparecem) e **seletor de workspace** lista Clínicas × Salas.
## 6. Verificação pós-deploy (VPS1)
```bash
# versão no ar
curl -s https://scoreodonto.com | grep -o 'V[0-9]*\.[0-9]*\.[0-9]*' | head -1
# logs do backend (Docker ROOTLESS na VPS1) — espere "[MIGRATION] all migrations OK"
ssh deploy@10.99.0.1 "DOCKER_HOST=unix:///run/user/1000/docker.sock docker logs scoreodontocom-scoreodonto-backend-1 --tail 40"
```
Smoke tests: criar **lançamento financeiro** (valida `normalizeFinanceiro` — antes nenhum salvava pela UI); **GTO → recebível**; **comissão/repasse/comprovante**; **glosa**; **contrato de convênio**; **orçamento→parcelas**; **fluxo de salas** (Dono de Sala, reserva→recebível, painel); **isolamento cross-tenant 403**.
## 7. Rollback
- Migrações **só adicionam** → reverter o **código** (imagem anterior na VPS1) é seguro; colunas/tabelas extras ficam inertes. Restaurar o dump da §2 só se necessário (`pg_restore -d "<url>" --clean <arquivo>`).
## 8. Riscos específicos desta entrega
- **`normalizeFinanceiro` (crítico):** corrige o bug em que `enforceUppercase` mandava `PAGO`/`PIX` violando o CHECK capitalizado — antes a tabela `financeiro` ficava vazia. Garantir que entrou na imagem.
- **Banco único dev/prod:** dado de teste de desenvolvimento pode aparecer; os E2E desta rodada sempre limparam, mas conferir contagens de `financeiro`, `salas`, `repasses`, `glosas`.
- **Contratos de convênio:** textos seguem padrão de mercado — recomendar revisão por advogado.
- **Locação sempre ativa:** todos os papéis (menos superadmin) passam a ver MINHAS/ALUGAR SALAS.
@@ -0,0 +1,497 @@
# CRM ORTODONTIA + TUTORIA ORTODÔNTICA
---
# LISTA DE TAREFAS
## FASE 1 — Documentação Fotográfica (armazenamento local)
### Backend
- [x] Tabelas orto_photo_session, orto_photo, orto_photo_annotation no PostgreSQL
- [x] multer configurado — upload local em /app/media/orto-fotos/
- [x] express.static servindo /app/media em /media
- [x] POST /api/orto/:tratamentoId/sessoes — criar/buscar sessão por tipo
- [x] GET /api/orto/:tratamentoId/sessoes — listar sessões com fotos
- [x] POST /api/orto/sessoes/:sessaoId/fotos — upload de foto para slot
- [x] DELETE /api/orto/fotos/:fotoId — excluir foto
- [x] GET /api/orto/:tratamentoId/completude — % de documentação preenchida
### Frontend
- [x] OrthoPhotoWorkspace — substitui PhotoCompare placeholder
- [x] Tabs de sessão (Inicial / 3m / 6m / 12m / Finalização / Pós-contenção)
- [x] Grid de 15 slots (5 extraoral + 5 intraoral + 5 complementar)
- [x] Upload por clique no slot (file input)
- [x] Visualizador de foto (modal fullscreen)
- [x] Exclusão de foto com confirmação
- [x] Indicador de completude (%)
- [x] Comparador lado a lado (selecionar 2 slots e comparar)
- [x] backend.ts — métodos getOrtoSessoes, uploadOrtoFoto, deleteOrtoFoto, getOrtoCompletude
## FASE 2 — Tutoria Integrada
- [x] Tabelas ortho_tutor_profile, ortho_tutoring_request, ortho_tutoring_message, ortho_tutoring_photo_package
- [x] Botão "Solicitar Tutoria" no painel do paciente (TutoriaSection.tsx)
- [x] Lista de tutores disponíveis com SLA e preço
- [x] Formulário de solicitação (escolha tutor, tipo, descrição)
- [x] Pacote automático gerado (todas as fotos do tratamento incluídas)
- [x] Chat inline por caso (CLINICA ↔ TUTOR)
- [x] Painel do tutor — receber, responder, fechar (TutorPainelView.tsx)
- [x] Status tracking: AGUARDANDO → EM_ANALISE → RESPONDIDO → FECHADO
- [x] Menu "PAINEL TUTOR" no Sidebar
- [x] Rota /tutoria-painel no App.tsx
## FASE 3 — Planejamento Visual e PDFs
- [x] Marcações clínicas nas fotos — PhotoAnnotator.tsx (SVG overlay)
- Ferramentas: Seta, Linha, Círculo, Retângulo, Texto, Caneta livre
- Cores (6) + espessura (P/M/G) + desfazer + limpar
- Anotações persistidas no banco (orto_photo_annotation)
- Botão ANOTAR no viewer de fotos
- [x] PDF — Documentação fotográfica (grade 15 slots + fotos base64)
- [x] PDF — Evolução ortodôntica (timeline cronológico)
- [x] PDF — Caso clínico para tutoria (triagem + diagnóstico + fotos)
- [x] jsPDF instalado (client-side, sem servidor)
- [x] OrthoReports.tsx integrado no OrthoDetailPanel
- [ ] Planejamento visual (vetores de movimentação) — Fase 4
- [ ] Tutor anota diretamente no painel de tutoria — próximo passo
## MARKETPLACE DE TUTORES (GLOBAL)
- [x] Fluxo de candidatura self-service para dentistas (TutorCandidaturaView)
- [x] Formulário acadêmico em 3 passos (dados pessoais / formação / perfil público)
- [x] Status tracking: PENDENTE → AGUARDANDO_PAGAMENTO → ATIVO / REJEITADO
- [x] Painel admin de gestão (AdminGestaoTutoresView) — 3 abas: Candidaturas / Ativos / Configurações
- [x] Aprovação/rejeição com campo de observação
- [x] Configuração de taxa de inscrição + chave PIX fallback
- [x] Integração MercadoPago — pagamento único (PIX / cartão / boleto)
- [x] Webhook automático — ativa perfil ao detectar pagamento aprovado
- [x] Confirmação manual de pagamento pelo admin (fallback)
- [x] Tutor global — disponível para TODAS as clínicas do sistema
- [x] Criação automática de ortho_tutor_profile após ativação
- [x] Tabelas: tutor_candidatura, tutor_config, tutor_pagamento
- [x] Sidebar: "SER TUTOR ORTO" (dentistas) + "GESTÃO TUTORES" (admin)
## FASE 4 — Marketplace e IA
- [ ] Marketplace público de tutores
- [ ] Sistema de pagamento
- [ ] IA clínica (sugestão de diagnóstico, sequência de fios)
- [ ] Rede colaborativa entre clínicas
- [ ] App mobile para registro fotográfico
## MIGRAÇÃO STORAGE
- [ ] Substituir multer local por Wasabi S3
- [ ] Manter URLs compatíveis (alterar apenas o driver de storage)
- [ ] Migração das fotos existentes para o Wasabi
---
## Visão Estratégica
Projeto de CRM Ortodôntico integrado com Marketplace de Tutoria Ortodôntica, Documentação Fotográfica, Planejamento Clínico e Colaboração entre Profissionais.
---
# MÓDULO CRM ORTODONTIA
## Componentes Principais
- PatientHeader
- ClinicalTriage
- Diagnosis
- TreatmentPlan
- Timeline
- NewEvolution
- PhotoCompare
- ContencaoTermo
- ClinicalRequests
- MaintenanceReport
- Finalization
## Fluxo do Tratamento
Triagem → Diagnóstico → Planejamento → Evoluções → Controle Fotográfico → Relatórios → Finalização → Contenção
---
# MÓDULO DE TUTORIA ORTODÔNTICA
## Objetivo
Permitir que ortodontistas experientes ofereçam:
- Segunda opinião
- Planejamento ortodôntico
- Mentoria clínica
- Discussão de casos
- Encaminhamento de pacientes
## Perfis
### Tutor Ortodôntico
- Publicar portfólio
- Receber solicitações
- Definir preços
- Definir SLA
- Receber pagamentos
### Solicitante
- Enviar casos
- Escolher tutor
- Acompanhar status
- Receber pareceres
---
# INTEGRAÇÃO DA TUTORIA AO CRM
A tutoria não será um sistema isolado.
Será incorporada ao painel do paciente:
Paciente
├── Triagem
├── Diagnóstico
├── Plano
├── Evoluções
├── Fotografias
├── Tutoria Ortodôntica
├── Relatórios
└── Finalização
## Solicitar Tutoria
Botão:
[ Solicitar Tutoria ]
### Tipos
- Segunda opinião
- Planejamento completo
- Discussão de caso
- Alinhadores
- Orto-cirúrgico
- Encaminhamento
---
# PACOTE AUTOMÁTICO PARA O TUTOR
Ao solicitar uma tutoria o sistema gera automaticamente:
## Dados Clínicos
- Triagem
- Diagnóstico
- Planejamento
- Evoluções
## Dados Ortodônticos
- Classe de Angle
- Fio atual
- Cooperação
- Status do tratamento
## Arquivos
- Fotografias
- Radiografias
- Tomografias
- STL
- PDFs
---
# MÓDULO DE DOCUMENTAÇÃO FOTOGRÁFICA
## Objetivo
Transformar as fotografias em uma documentação ortodôntica completa.
---
# WORKSPACE FOTOGRÁFICO
Novo módulo:
OrthoPhotoWorkspace
Recursos:
- Upload
- Organização
- Comparação
- Anotações
- Compartilhamento
- Exportação
---
# SESSÕES FOTOGRÁFICAS
Cada paciente pode possuir:
- Inicial
- Controle 3 meses
- Controle 6 meses
- Controle 12 meses
- Finalização
- Pós contenção
---
# TEMPLATE ORTODÔNTICO PADRÃO
## Extraoral
- Frontal sorrindo
- Frontal repouso
- Perfil direito
- Perfil esquerdo
- Perfil sorrindo
## Intraoral
- Frontal oclusão
- Lateral direita
- Lateral esquerda
- Oclusal superior
- Oclusal inferior
## Complementares
- Linha média
- Overjet
- Overbite
- Mordida cruzada
- Mordida aberta
---
# SISTEMA DE SLOTS
Cada fotografia ocupa posição específica.
Garantindo padronização clínica.
---
# EDITOR DE IMAGENS
Ferramentas:
- Zoom
- Crop
- Rotação
- Contraste
- Brilho
- Espelhamento
---
# MARCAÇÕES CLÍNICAS
Permitir desenhar diretamente nas imagens.
Tipos:
- Seta
- Linha
- Texto
- Círculo
- Área destacada
Exemplos:
- Linha média desviada
- Apinhamento
- Torque inadequado
- Mordida cruzada
---
# COMPARADOR FOTOGRÁFICO
Modos:
- Inicial × Atual
- Inicial × Final
- 6 meses × 12 meses
Visualizações:
- Slider
- Lado a lado
- Sobreposição
---
# CHECKLIST DE DOCUMENTAÇÃO
Validação automática:
- Frontal
- Perfil direito
- Perfil esquerdo
- Oclusal superior
- Oclusal inferior
- Oclusão frontal
Indicador:
Documentação Completa (%)
---
# TUTORIA SOBRE FOTOGRAFIAS
## Enviar para Tutor
O sistema envia:
- Fotos
- Diagnóstico
- Planejamento
- Timeline
- Evoluções
---
# RESPOSTA DO TUTOR
Tutor pode:
- Escrever parecer
- Marcar fotografias
- Desenhar vetores
- Apontar correções
- Criar planejamento visual
---
# PLANEJAMENTO VISUAL
Ferramenta Premium.
Permite:
- Vetores de movimentação
- Correção de linha média
- Planejamento de fechamento de espaços
- Estratégia de extrações
- Planejamento de alinhadores
---
# RELATÓRIOS
## PDF Documentação
Documentação completa organizada.
## PDF Evolução
Comparação cronológica.
## PDF Tutoria
- Caso clínico
- Fotografias
- Comentários
- Marcações
- Planejamento
---
# BANCO DE DADOS
## ortho_tutor_profile
Cadastro do tutor.
## ortho_tutoring_request
Solicitações de tutoria.
## ortho_tutoring_message
Chat do caso.
## ortho_tutoring_financial
Controle financeiro.
## ortho_photo_session
Sessões fotográficas.
## ortho_photo
Fotografias.
## ortho_photo_annotation
Marcações sobre imagens.
## ortho_photo_compare
Comparações.
## ortho_tutoring_photo_package
Pacotes enviados para tutores.
---
# ROADMAP
## Fase 1
- Persistência das fotos
- Sessões fotográficas
- Upload real
- Comparação
## Fase 2
- Tutoria integrada
- Chat
- Envio de documentação
## Fase 3
- Planejamento visual
- Marcações do tutor
- PDFs avançados
## Fase 4
- Marketplace público
- IA clínica
- Rede colaborativa de ortodontistas
---
# DIFERENCIAL
CRM Ortodontia
+
Documentação Fotográfica Profissional
+
Tutoria Ortodôntica
+
Planejamento Visual
+
Marketplace de Especialistas
+
Colaboração Entre Clínicas
+158
View File
@@ -0,0 +1,158 @@
# DEPLOY-PRODUCAO — Runbook operacional (estado de HOJE)
> 🛑 **LEIA PRIMEIRO.** Procedimento **real e executável hoje**, passo a passo, sem adivinhação.
> Reflete o mecanismo atual: **a produção (VPS1) builda a si mesma** via `git pull` + `docker compose build` + `up -d`.
> O fluxo-alvo (registry + deploy por tag, prod sem build) está em `deploy.md` e ainda não existe. A realidade da infra está em `arquitetura.md`.
>
> ⚠️ **Regra de ouro:** antes de qualquer build/deploy/push, confirme com o operador **DEV ou PRODUÇÃO**.
> `dev.scoreodonto.com` = VPS4 (descartável, mas **mesmo banco** da prod). `scoreodonto.com` = VPS1 (real).
> **Nunca** `git push --force` na `main`. **Dev e prod compartilham o banco** (VPS3) — o backup é a única rede de segurança.
---
## Mapa rápido (quem é quem)
| Onde | Host | Papel | Socket Docker |
|------|------|-------|----------------|
| VPS4 | `10.99.0.4` | dev + builder (código aqui) | `/var/run/docker.sock` (root) |
| VPS1 | `10.99.0.1` | produção | `unix:///run/user/1000/docker.sock` (rootless) |
| VPS3 | `10.99.0.3` | Postgres + Gitea | — |
- Repo: `ssh://git@10.99.0.3/ruicesar/scoreodonto.com.git` (Gitea).
- Diretório do repo nas duas máquinas: `/home/deploy/stack/scoreodonto.com`.
- Project Compose: `scoreodontocom` (containers `scoreodontocom-*`).
---
## FASE 0 — Pré-voo (na VPS4)
```bash
cd /home/deploy/stack/scoreodonto.com
# 1. Sanidade do backend
node --check backend/server.js
# 2. Validar a mudança em DEV (já no ar em https://dev.scoreodonto.com)
# Conferir o bundle servido (deve mudar a cada rebuild):
curl -s https://dev.scoreodonto.com/ | grep -oE 'index-[A-Za-z0-9_-]+\.js' | head -1
```
```bash
# 3. BACKUP DO BANCO (rede de segurança única — dev/prod compartilham o banco)
# Rodar na VPS3. Usar as credenciais do ambiente/segredo, nunca em texto puro.
ssh deploy@10.99.0.3 'pg_dump -Fc scoreodonto -f /home/deploy/backups/scoreodonto_$(date +%Y%m%d_%H%M).dump'
```
---
## FASE 1 — Versão (na VPS4) ⚠️ passo manual
O bump **não é automático** (`npm run build` é só `vite build`). É preciso incrementar **à mão**:
```bash
cd /home/deploy/stack/scoreodonto.com
node update-version.js # incrementa o patch em frontend/constants.ts (ex.: V1.0.12 → V1.0.13)
grep APP_VERSION frontend/constants.ts
```
> Hoje a versão **não fica visível no navegador** (está no bundle JS). Até a Etapa 3 (versionamento visível), anote a versão que está promovendo.
---
## FASE 2 — Commit & push para o Gitea (na VPS4)
> Só execute com autorização explícita do operador (é o repo de produção).
```bash
cd /home/deploy/stack/scoreodonto.com
git add -A
git commit -m "feat: <descrição> (bump vX.Y.Z)"
git push origin main
```
> ❗ Push **não** dispara deploy automático hoje (não há listener/webhook ativo). A promoção é a FASE 3.
---
## FASE 3 — Promover para produção (na VPS1)
```bash
ssh deploy@10.99.0.1
export DOCKER_HOST=unix:///run/user/1000/docker.sock # docker rootless da VPS1
cd /home/deploy/stack/scoreodonto.com
# 1. Trazer o código aprovado
git pull origin main
# 2. Carimbo de versão visível (lido pelos build args do compose) — em PROD use APP_ENV=PROD
export APP_VERSION=$(grep -oE 'V[0-9]+\.[0-9]+\.[0-9]+' frontend/constants.ts | head -1)
export GIT_COMMIT=$(git rev-parse --short HEAD)
export BUILD_TIME=$(date -u +'%Y-%m-%d %H:%M UTC')
export APP_ENV=PROD
# 3. Buildar as imagens (frontend faz vite build; backend npm install)
docker compose build scoreodonto-frontend scoreodonto-backend
# 4. Subir (troca os containers pelas imagens novas)
docker compose up -d scoreodonto-frontend scoreodonto-backend nginx
```
> Os 4 `export` carimbam versão/commit/data/ambiente na imagem. Sem eles, o build ainda funciona mas mostra `dev`/`unknown` no rodapé. Em **DEV** (VPS4) use `APP_ENV=DEV`.
O backend leva ~1030s no boot (reconecta Postgres/Dragonfly e roda as migrações idempotentes).
---
## FASE 4 — Verificação pós-deploy (na VPS1)
```bash
export DOCKER_HOST=unix:///run/user/1000/docker.sock
# Containers no ar
docker ps --format '{{.Names}} | {{.Status}}' | grep score
# Backend: esperar a linha de migração + carimbo de versão
docker logs scoreodontocom-scoreodonto-backend-1 --tail 40 | grep -iE 'migration|VERSION'
# alvo: [MIGRATION] all migrations OK
# [VERSION] ScoreOdonto API vX.Y.Z · commit ... · build ... · PROD
# Confirmar a versão no ar SEM acessar container (a forma definitiva):
curl -s https://scoreodonto.com/api/version
# {"name":"ScoreOdonto API","version":"vX.Y.Z","commit":"...","build":"...","environment":"PROD"}
# E visualmente: a versão aparece no rodapé do menu lateral (Sidebar).
```
**Smoke tests mínimos:** abrir `https://scoreodonto.com`, logar, e exercitar o que mudou (ex.: lançamento financeiro, agenda, salas). Conferir ausência de erro 500 no `docker logs`.
---
## FASE 5 — Rollback (se algo quebrar)
As migrações são **aditivas** → reverter o **código** é seguro (colunas/tabelas extras ficam inertes).
```bash
ssh deploy@10.99.0.1
export DOCKER_HOST=unix:///run/user/1000/docker.sock
cd /home/deploy/stack/scoreodonto.com
git log --oneline -5 # achar o commit bom anterior
git checkout <commit-anterior> # ou: git reset --hard <commit-anterior>
docker compose build scoreodonto-frontend scoreodonto-backend
docker compose up -d scoreodonto-frontend scoreodonto-backend
```
> Hoje o rollback **rebuilda** a versão anterior (as imagens são `:latest`, não há tag por versão). Quando a **Etapa 6** entregar deploy por tag + registry, o rollback vira só `docker compose pull` da tag anterior — sem rebuild. Restaurar o dump do banco (FASE 0) só em caso extremo:
> `pg_restore -d "postgresql://scoreodonto_user:<senha>@10.99.0.3:5432/scoreodonto" --clean <arquivo>.dump`
---
## Resumo em uma tela
```
VPS4: validar em dev → node --check → backup do banco (VPS3)
→ node update-version.js → commit → git push origin main (com OK do operador)
VPS1: git pull → docker compose build → docker compose up -d
→ verificar logs ([MIGRATION] all migrations OK) + smoke test
Rollback: git checkout <commit-bom> → build → up -d
```
+344
View File
@@ -0,0 +1,344 @@
# 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
```bash
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:**
- [x] `OrthoDetailPanel` agora renderiza `OrthoPhotoWorkspace` + `TutoriaSection`
(no lugar do `PhotoCompare` placeholder) — a tutoria/fotos vivem no modal do paciente.
- [x] **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.
- [x] **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.):**
- [x] **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.
- [x] 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.):**
- [x] **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.):**
- [x] **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).
- [x] **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.):**
- [x] **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.):**
- [x] **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.):**
- [x] **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.
- [x] `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.
- [x] 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.
- [x] 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)
- [x] **Fase 0** — Cadastro: 5 cards de perfil com preço (Clínica R$499 separada). `LoginView.tsx`.
- [x] **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".
- [x] **Fase 2** — Identidade global: `dentistas.usuario_id` + backfill por email; criação seta usuario_id.
- [x] **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:
- [x] Backend: tabela `interesses_agenda`, `POST/GET /api/interesses`, hook no DELETE de
agendamento que notifica interessados como `enviado_por='sistema'` e marca `liberado`.
- [x] Frontend Parte 1: AgendaView mostra painel "Horário ocupado" no 409 → registrar interesse (com minutos de espera).
- [x] Frontend Parte 2: dentista vê "Pedidos de interesse" (sino + badge + modal; GET retorna `clinica_nome`).
- [x] `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)
- [x] **`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`.
- [x] ~~`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.
- [x] ~~`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):**
- [x] `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**).
- [x] `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).
- [x] `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.
- [x] **Revertido** o que era do modelo errado: bypass do `tenantGuard` (removido) e seletor de
clínica do `RxConfigView` (removido).
- [x] `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.
- [x] `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):**
- [x] Filtro **`?clinic=<clinicaId>`** (= `clinic_name`) em `GET /api/images`, `/images/patients`
e `/images/by-patient`. Novo `GET /api/images/unique-clinics` (clinic_name + contagens).
- [x] 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`):**
- [x] 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).
- [x] `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:
```sql
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 | <unidade ativa>";
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: <nome>"
no cadastro/edição (grava `salas.clinica_id`; PUT aceita desvincular). Marketplace e
cards mostram "POR <unidade>" (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.)
@@ -0,0 +1,233 @@
# Arquitetura RX — scoreodonto.com → rx.scoreodonto.com → clientWindows.exe → Wasabi
> Documento de **revisão / engenharia reversa** (gerado em 2026-06-12, sem alterar código).
> Fonte da verdade: `rx.scoreodonto.com/dental-server` (servidor) lido linha a linha.
> O `.exe` do client Windows está em repo separado (Gitea) — aqui seu comportamento é
> descrito pelo **protocolo** que o servidor expõe/aceita (Socket.IO + `machine_token`).
---
## 1. Visão geral (quem é quem)
```
┌──────────────────────────┐ ┌───────────────────────────────────────────┐
│ CLÍNICA (PC com sensor) │ │ rx.scoreodonto.com (dental-server) │
│ │ │ Node + Express + Socket.IO │
│ clientWindows.exe │ WSS │ │
│ - captura RX do sensor │────────▶│ io.on('send-image') │
│ - auth: machine_token │ socket │ → storage.saveImage() │
│ - emite 'send-image' │ .io │ → INSERT images (Postgres dental_images) │
└──────────────────────────┘ │ │
│ HTTP API (/api/images, /api/admin/...) │
┌──────────────────────────┐ HTTPS │ - lista pacientes/imagens │
│ scoreodonto.com (plugin)│────────▶│ - cria devices (/api/admin/clinics) │
│ - superadmin: config │ proxy │ │
│ global (rx_url+token) │ no nosso│ │ saveImage / getImageBuffer │
│ - backend proxeia leitura│ backend │ ▼ │
└──────────────────────────┘ │ ┌─────────────────────────────────────┐ │
│ │ WASABI (S3) bucket │ │
│ │ key: clinic/client/patient/file.png │ │
│ └─────────────────────────────────────┘ │
│ (+ cache local em ./uploads como fallback)│
└───────────────────────────────────────────┘
```
Quatro peças:
1. **clientWindows.exe** — instalado no PC da clínica; captura a radiografia e **envia via Socket.IO**.
2. **rx.scoreodonto.com / dental-server** — recebe, persiste metadados no Postgres e o binário no Wasabi; serve listagens e imagens.
3. **Wasabi** — storage S3-compatível (objeto = `clinic/client/patient/arquivo`).
4. **scoreodonto.com (plugin RX)** — administra/visualiza via API, com token global (nosso trabalho recente).
---
## 2. Banco de dados do RX (Postgres `dental_images`, em `database.js`)
| Tabela | Campos-chave | Papel |
|---|---|---|
| **clinics_devices** | `clinic_name`, `email` (UNIQUE), `password_hash`, `pc_name`, `machine_token` (UUID UNIQUE), `is_active`, `last_ip` | **1 linha por (clínica, PC)**. Credencial do client Windows. Criada por `POST /api/admin/clinics`. |
| **images** | `image_guid`, `patient_name`, `client_name` (=PC), `clinic_name`, `filename`, `thumb_filename`, `doctor`, `remark`, `enabled`, `created_at` | 1 linha por radiografia. **Filtros de leitura hoje: `client_name` e `patient_name`** (e agora `clinic_name`, ver §7). |
| **users** | `username`, `email`, `password_hash`, `is_admin` | Usuários admin/web (painel). |
| **api_tokens** | `name`, `token` | **Tokens estáticos de API** (viram admin, não expiram). |
| **system_config** | `key`, `value` | Key-value. Guarda `wasabi_config`. |
| **gtos / gto_images** | `gto_number`, `patient_name`, … | Documentos GTO por paciente. |
---
## 3. clientWindows.exe — como envia os RX (protocolo)
O envio NÃO é HTTP de upload; é **Socket.IO** (`server.js`):
1. **Conexão / auth do socket** (`io.use(...)`, server.js:823):
- O client conecta com `handshake.auth.token = machine_token` (UUID gerado no cadastro do device).
- Regex confirma UUID → busca em `clinics_devices` → se `is_active`, autoriza.
- Opcional: se `email`+`password` também vierem, são validados (compat).
- Resultado: `socket.user = { role:'client_device', clinicId, clinicName, pcName }`
**vindos do registro do device** (não do client). Atualiza `last_ip`.
- Também aceita `CLIENT_API_KEY` (sensor legado) e JWT (painel web / app novo).
2. **Envio da imagem — DIRECT UPLOAD pro Wasabi (fluxo REAL de produção)**:
Confirmado pelos logs do client (`🌐 Fazendo Direct Upload pro Wasabi (Bypass VPS)`).
O binário **NÃO** trafega pela VPS — vai direto pro Wasabi via URL pré-assinada:
1. Client monitora `C:\ProgramData\RF\Dental Sensor\Images` (Chokidar) e gera thumb local.
2. `socket.emit('request-presigned-urls', { metadata:{guid}, patientData:{name,...} })`
→ servidor responde `{ originalUrl, thumbnailUrl, filename, thumbFilename }`
(`storage.getUploadPresignedUrl`, server.js:1218). **A key do Wasabi já é fixada aqui.**
3. Client faz **PUT direto no Wasabi** (original + thumb) usando as presigned URLs.
4. `socket.emit('image-upload-direct', {...})` → servidor só **grava os metadados** em
`images` (server.js:1258). Não recebe os bytes.
- Em ambos (2 e 4) o eixo de clínica é o MESMO:
`clinicName = socket.user?.clinicName || clientInfo?.clinicName || 'Clínica não identificada'`
→ vem do **`clinic_name` do device** (machine_token). `clientName` = PC; `patientName` = paciente.
> ⚙️ Existe também o caminho legado `socket.on('send-image')` (base64 pela VPS, server.js:1031)
> e `POST /api/images/upload` (upload manual). Mas o client Windows real usa o **Direct Upload**.
> Há ainda `POST /api/client/login` (JWT 2h `client_device`) para fluxos HTTP.
3.1. **⚠️ Estado real dos dados (2026-06-12, banco compartilhado dev+prod):**
- **1 device**: `clinic_name="Consultt Clinic"`, `pc="RX"`.
- `images`: **2552 imgs / 503 pacientes com `clinic_name = NULL`** (subiram antes do clinic_name
ser setado → Wasabi key `desconhecido/RX/paciente/…`) + **222 imgs / 37 pacientes** sob
`"Consultt Clinic"`. → A maioria das radiografias está "órfã" de clínica.
---
## 4. Wasabi storage (`storage.js`) — a lógica que NÃO pode quebrar
**Config**: lida do banco em `system_config.wasabi_config`
(`{ wasabiAccessKey, wasabiSecretKey, wasabiBucket, wasabiRegion, wasabiEndpoint }`),
carregada por `loadConfigFromDb()` (server.js:1428, e ao salvar em `routes/system.js`).
SDK: `@aws-sdk/client-s3` com `forcePathStyle:true`, endpoint `s3.wasabisys.com`.
**Chave do objeto (CRÍTICO):**
```
key = sanitize(clinic_name) / sanitize(client_name) / sanitize(patient_name) / filename
```
`sanitizePathSegment` troca `/\?#%*:"<>|` por `_`. (storage.js:103, 120, 153, 220, 298, 340, 366, 414)
**saveImage** (storage.js:139):
1. grava local em `./uploads/<filename>`;
2. se Wasabi ativo → `Upload` (lib-storage) para a key acima;
3. se subiu OK → **apaga o local** (Wasabi vira fonte). Se falhar → mantém local (fallback).
**getImageBuffer** (storage.js:190): local primeiro → senão Wasabi (key vinda dos args
ou de um `SELECT` em `images` por `filename`/`thumb_filename`) → fallback key na raiz do bucket
(arquivos legados) → cacheia local. `getImageUrl`/`getDownloadPresignedUrl` geram **URL assinada (1h)**.
**deleteImage**: apaga local (uploads + processed) e a key no Wasabi (+ key raiz, redundância legada).
> Wasabi é **opcional**: sem config, tudo funciona só em disco local. Por isso "desligar/errar"
> a config do Wasabi não derruba o servidor — só muda onde o binário vive.
---
## 5. Autenticação — matriz completa
| Quem | Como autentica | Onde |
|---|---|---|
| **client Windows (socket)** | `machine_token` (UUID) — suficiente; email+senha opcionais | `io.use` server.js:823 |
| **client Windows (HTTP)** | `POST /api/client/login` → JWT 2h `role:client_device` | client-auth.js |
| **sensor legado** | `CLIENT_API_KEY` (`rf-dental-secure-key-2026` default) | io.use / `x-api-key` |
| **admin / painel web** | usuário+senha → JWT 7d (`is_admin`) **OU** `api_token` estático | auth.js `authenticateToken` |
| **público** | sem auth | só `GET /api/images/:id/thumbnail` |
`authenticateToken` (auth.js:12): tenta JWT; se falhar, procura o token em `api_tokens`
se achar, injeta um **admin mock** (`is_admin:true`). É isso que o **scoreodonto usa**
(token global) para chamar `/api/admin/clinics` e as listagens.
---
## 6. Integração scoreodonto.com (plugin RX) — estado atual
- **Config GLOBAL** (só superadmin) em `settings.rx_config_global = { rx_url, api_token }`
no backend do scoreodonto. O `api_token` é um **token de `api_tokens` do RX** (admin, não expira).
- **Criar PC** (`POST /api/rx/devices` no nosso backend) → `POST {rx_url}/api/admin/clinics`
com **`clinic_name = clinicaId`** (a clínica do scoreodonto) + `pc_name` + email/senha por PC.
Devolve o `machine_token` que vai no client Windows.
- **Ler** (`GET /api/rx/patients`, `/api/rx/images`) → proxeia `/api/images/patients` e
`/api/images/by-patient` com `?clinic=<clinicaId>` + Bearer `api_token`. Isolamento por vínculo.
- **Thumbnails**: públicos, carregados direto de `rx_url`.
---
## 7. ⚠️ Pontos de atenção para NÃO quebrar o que já funciona
1. **`clinic_name` é o eixo de tudo** (Wasabi key + filtro de listagem). Os clients Windows
**já em produção** sobem imagens com o `clinic_name` que foi gravado **no cadastro do device deles**
(provavelmente o nome legível da clínica).
2. **Descompasso scoreodonto × RX:** o plugin novo cria device com `clinic_name = clinicaId`
(ID opaco do scoreodonto). Logo:
- Imagens **antigas** estão sob `clinic_name = <nome legível>` no Wasabi e no `images`.
- Imagens **novas** (via plugin) ficariam sob `clinic_name = <clinicaId>`.
- Filtrar por `?clinic=<clinicaId>` **não acha as antigas** (clinic_name diferente) — mas
**não quebra** o envio existente (cada client continua com o seu clinic_name).
- → Para enxergar clínicas já ativas, é preciso um **mapeamento clinicaId ↔ clinic_name atual**
(tabela/coluna de-para), NÃO renomear objetos no Wasabi (arriscado e custoso).
3. **Hierarquia desejada (4 níveis) × Wasabi (3 níveis):**
- Desejado: `scoreodonto / consulttclinic@gmail.com / clinicaID / paciente`.
- Wasabi real: `clinic_name / client_name / paciente / arquivo` (3 níveis, sem o nível "conta").
- A "conta-mestre" (email) é **identidade de auth** (api_token/users.email), **não faz parte da key**.
- Realizar 4 níveis exigiria mudar o esquema de key — **risco alto para objetos já gravados**.
Caminho seguro: manter 3 níveis e usar `clinic_name = clinicaId` só para o que for novo,
com de-para para o legado.
4. **`npm run dev` do RX = `node server.js` (sem nodemon)** → mudanças no dental-server exigem
`docker restart rx_dev_api`. (Em prod o deploy é via `deploy-prod.sh` / webhook.)
5. **Não tocar**: `io.on('send-image')`, `storage.saveImage/getImageBuffer`, o esquema da key,
nem o cadastro de `clinics_devices` dos PCs que já enviam. Toda evolução deve ser **aditiva**
(novos endpoints/colunas), preservando o caminho do client Windows.
---
## 8. Endpoints-chave (referência rápida)
| Endpoint | Auth | Uso |
|---|---|---|
| `WSS send-image` | machine_token | client Windows envia RX |
| `POST /api/admin/clinics` | admin/api_token | cria device (clinic_name, email, pc_name) → machine_token |
| `GET /api/admin/clinics` | admin/api_token | lista devices |
| `GET /api/images?clinic=&client=&search=` | admin | lista imagens (filtros) |
| `GET /api/images/patients?clinic=&search=&page=` | admin | pacientes agrupados |
| `GET /api/images/by-patient?name=&clinic=` | admin | imagens de um paciente |
| `GET /api/images/:id/thumbnail` | **público** | miniatura |
| `GET /api/version` · `/download-client` | público | versão / instalador do client |
| `POST /api/auth/login` → JWT · `POST /api/auth/tokens` → api_token | admin | obter credenciais de API |
---
## 9. Multi-tenant 2 níveis: `tenant_id` / `clinica_id` (implementado 2026-06-12, DEV)
**Hierarquia** (espelha o scoreodonto, que tem conta multi-clínica no plano):
`tenant_id (conta/dono = usuarios.id "u_…") / clinica_id (clinicas.id "c_…") / pc / paciente / arquivo`.
O `tenant_id` é resolvido pelo **vínculo `dono*`** da clínica (owner_id está vazio hoje).
Para a Consultt: `tenant_id=u_1780870994865_3876n` (Rui Cesar) / `clinica_id=c_1780871001549_koqa3o`.
- Colunas `tenant_id` **e** `clinica_id` em `clinics_devices` e `images` (+índices); `clinic_name`
vira rótulo. Backfill: device + **2776 imgs** → tenant+clínica da Consultt.
- Verificado: `?tenant_id=` (conta) e `?clinica_id=` (clínica) isolam; tenant/clínica inexistente → 0.
- scoreodonto: cadastro de device resolve+envia `tenant_id`; proxy de leitura libera **conta**
(`tenant_id` = própria conta do dono) ou **clínica** (`clinica_id` com vínculo).
(Resto desta seção descreve a base por `clinica_id`.)
- **Schema RX**: coluna `clinica_id` em `clinics_devices` e `images` (+ índices). `clinic_name`
vira só rótulo legível. Backfill: device + **2776 imgs** existentes → `c_1780871001549_koqa3o`.
- **Fluxo**: `POST /api/admin/clinics` grava `clinica_id`; o socket (machine_token) resolve
`socket.user.clinicaId` do device; `image-upload-direct` (e o legado `send-image`) gravam
`images.clinica_id`. **Wasabi key ainda é `clinic_name/…`** (mudança física = fase de
reconciliação, pendente da resposta sobre os PNGs de origem).
- **Isolamento**: listagens (`/api/images`, `/patients`, `/by-patient`, `/unique-clinics`)
filtram por `?clinica_id=`. Verificado: tenant real retorna dados, tenant inexistente → 0.
- **scoreodonto**: `/api/rx/*` envia/filtra por `clinica_id` (e `clinic_name`=nome_fantasia no
cadastro); proxy de leitura valida vínculo (`_rxClinicScopeOk`) → 403 fora do tenant.
- **Pendente**: reconciliação física no Wasabi (`clinica_id/…`) + remoção do `send-image`
legado (após confirmar que nenhum `.exe` em campo usa versão pré-Direct-Upload).
### TL;DR
O client Windows manda RX por **Socket.IO autenticado por `machine_token`**; o servidor grava
metadados no Postgres e o arquivo no **Wasabi sob `clinic_name/client_name/patient_name/arquivo`**.
O scoreodonto administra isso com um **token de API global**, filtrando por `clinic_name = clinicaId`.
O risco de integração está em **`clinic_name`**: alinhar o ID do scoreodonto com o `clinic_name`
que os PCs já usam, sem renomear nada no Wasabi nem mexer no caminho de envio.
@@ -0,0 +1,93 @@
# RX — Backlog de robustez, segurança e limpeza
> Tarefas derivadas da revisão de 2026-06-12 (multi-tenant + client.exe + storage).
> Prioridade: **P1** (faria primeiro) · **P2** · **P3**. Marcar `[x]` ao concluir.
> Regra do projeto: nada vai pra prod sem autorização; dev.rx e prod.rx **compartilham
> DB+Wasabi** (ver T3.1 — separar é o que mais reduz risco).
---
## 3. Robustez do RX / resync
- [ ] **T3.1 (P1) — Separar DEV de PROD (DB + Wasabi).** Hoje compartilham o mesmo
Postgres (`dental_images`) e o mesmo bucket → testes mexem em dado real. Opções:
banco `dental_images_dev` e/ou prefixo/bucket Wasabi por ambiente (`RX_ENV`).
- [x] **T3.2 (P1) — Healthcheck de reconciliação Wasabi↔banco** por `clinica_id`/`tenant_id`.
FEITO (dev): `GET /api/admin/rx-health` (RX) — contagens por (tenant,clínica) + cron +
`?clinica_id=&checkLimit=N` (HEAD-check de ausentes). Proxy `GET /api/rx/health`
(scoreodonto, superadmin). Validado: Consultt 2723 imgs/513 pac, 0 ausentes em 15. Commit 016305e.
- [ ] **T3.3 (P2) — Índice único parcial** `images(original_filename) WHERE enabled=1`
como trava de duplicata no banco. ⚠️ Criar CONCURRENTLY e fora do boot (se houver
duplicata, a criação no boot derrubaria o servidor).
- [ ] **T3.4 (P2) — Remover `send-image` legado** após confirmar que todo `.exe` em campo
é ≥ Direct Upload (1.3.x). Elimina um caminho de upload + base64 pela VPS.
- [ ] **T3.5 (P2) — Throttle/progresso no resync.** `trigger-sync` reescaneia todos os PNGs;
em clínica grande é pesado → lote + reporte de progresso.
- [ ] **T3.6 (P3) — `check-image-exists`**: garantir que checa linha no banco **E** objeto no
Wasabi (senão objeto sumido nunca é reenviado).
## 4. Client.exe
- [ ] **T4.1 (P1) — Code signing do instalador NSIS** (evita SmartScreen nas clínicas).
- [ ] **T4.2 (P1) — `machine_token` em repouso**: proteger no config local (DPAPI/cripto).
- [ ] **T4.3 (P2) — Retry/backoff + fila persistente** nos PUT do Wasabi e na `uploadQueue`
(sobreviver a restart / internet ruim).
- [ ] **T4.4 (P2) — Telemetria pro `/clients`**: client envia `last_sync`, profundidade da
fila e últimos erros → página mostra saúde real.
- [ ] **T4.5 (P2) — `lookupPatientInfo` tolerante a versão** do banco do sensor RF.
- [ ] **T4.6 (P3) — Pipeline de auto-update**: automatizar envio de `Setup x.y.z.exe` +
`latest.yml` pro `updates/` do prod ao rebuildar.
## 5. Segurança / hardening
- [ ] **T5.1 (P1) — `api_token` global criptografado em repouso** (hoje texto puro em
`settings.rx_config_global`) + rotação + idealmente token de escopo reduzido (não-god).
- [x] **T5.2 (P1) — Auditar escopo GLOBAL do `tenantGuard`** — FEITO (dev). 🔴 Achado crítico:
a família `/api/sync/*` (sync GLOBAL legado) usava `tenantGuard`**burlável sem clinicaId**;
`DELETE /api/sync/truncate-all` fazia `TRUNCATE pacientes,agendamentos,financeiro,leads,guias`
de **todas as clínicas** → qualquer usuário logado (até funcionário) apagava o banco inteiro.
**Corrigido**: os 13 endpoints `/api/sync/*``superadminGuard`. Provado: não-superadmin
agora recebe **403** (antes truncava). 2º achado: **`/api/settings/google-credentials`** GET+POST
(credenciais OAuth GLOBAIS da plataforma) também era `tenantGuard` → qualquer logado sobrescrevia →
**`superadminGuard`** (403 p/ não-super; front só chama p/ superadmin). **Auditoria completa**:
todos os demais `tenantGuard` (clinica-sync, dashboard, agendamentos, avaliacoes, financeiro,
guias, leads/:id/converter, DELETE :id, rx/devices) exigem `req.clinicaId` + `WHERE clinica_id` → seguros.
- [ ] **T5.3 (P1) — Auth do `remote-crud-patient`** (socket escreve no banco local do client):
validar papel/escopo forte no servidor.
- [ ] **T5.4 (P2) — Defesa em profundidade no RX server**: validar tenant nos endpoints admin
(não confiar só no proxy do scoreodonto).
- [ ] **T5.5 (P2) — Revisar os 9 dias de WIP** (`auth.js`, `database.js`, `storage.js`, páginas)
antes do prod.
- [!] **T5.6 (P3→P0) — Auditar histórico do git por segredos** — FEITO. 🔴🔴🔴 ACHADOS CRÍTICOS:
- `dental-server/bkp/docs/ENV-CONFIG.txt` tem o **`JWT_SECRET` REAL** (confirmado == o do
servidor) commitado → **qualquer um com acesso ao repo forja JWT de admin do RX**.
- `INSTRUCOES-SERVIDOR-VPS.txt` tem o **`DB_PASSWORD`/`REDIS_PASSWORD` reais** em texto puro.
AÇÃO OBRIGATÓRIA (decisão do usuário, não automatizei):
1. **Rotacionar JWT_SECRET** (invalida tokens, força re-login) — fecha a forja.
2. **Rotacionar senha do Postgres/Redis** (10.99.0.3).
3. Remover os segredos dos arquivos versionados + **scrub do histórico** (git filter-repo/BFG).
`.secrets` já está no .gitignore (feito antes); isso NÃO resolve os já commitados → rotação é mandatória.
## 6. Limpeza / tech-debt
- [ ] **T6.1 (P1) — Consolidar os 4 caminhos de upload** (`image-upload-direct`, `send-image`,
`/api/images/upload`, `/upload-file`) → manter 12, remover o resto.
- [ ] **T6.2 (P2) — Simplificar `storage.js` pós-resync** (remover fallback legado quando todos
os objetos estiverem na key nova).
- [ ] **T6.3 (P2) — Remover diretórios mortos** (`bkp/`, `public_legacy/`, `old-root-files/`).
- [ ] **T6.4 (P3) — Upgrade Node 20 → 22** (AWS SDK exigirá ≥22 em 2027).
- [ ] **T6.5 (P3) — Log levels** (info/debug/error) no servidor RX.
- [ ] **T6.6 (P3) — Passada final de comentários/padronização** no fluxo RX.
---
### Log
- **2026-06-12** — T3.2 concluída (healthcheck de reconciliação, read-only).
- **2026-06-12** — T5.2 concluída. 🔴 Fechado RCE-like: `/api/sync/truncate-all` (e família) +
`/api/settings/google-credentials` deixavam qualquer logado truncar o banco / trocar OAuth global →
agora `superadminGuard` (403 p/ não-super). Mudanças no **scoreodonto (dev), não commitadas**.
- **2026-06-12** — T3.6 auditada: `check-image-exists` já está correto (checa banco+Wasabi, apaga órfão). Sem fix.
- **2026-06-12** — T5.6 🔴🔴🔴: JWT_SECRET REAL + senha de DB/Redis commitados (ENV-CONFIG.txt,
EXEMPLO-ENV.txt, INSTRUCOES-SERVIDOR-VPS.txt, test-db2.js). **Passo 3 FEITO** (redigidos do working
tree, commits 4616f20+2b679a5). **PENDENTE/MANDATÓRIO (usuário): rotacionar JWT_SECRET + senha
Postgres/Redis + scrub do histórico** — até lá, a exposição persiste no histórico do Gitea.
+46
View File
@@ -0,0 +1,46 @@
# Qual é a verdade hoje? — Auditoria de infra ScoreOdonto
> Resposta direta às 6 perguntas essenciais, **verificada em 16/jun/2026**. Sem teoria, sem fantasma.
> Detalhes em `arquitetura.md` (real), `deploy.md` (alvo) e `DEPLOY-PRODUCAO.md` (runbook).
---
### 1. Onde está o código?
- **Fonte da verdade:** Gitea na **VPS3**, repo `ruicesar/scoreodonto.com` (`ssh://git@10.99.0.3/ruicesar/scoreodonto.com.git`).
- **Cópia de trabalho (dev):** VPS4 em `/home/deploy/stack/scoreodonto.com` (onde se programa).
- **Cópia de produção:** VPS1 em `/home/deploy/stack/scoreodonto.com` (HEAD verificado: `19dbbae`).
- **Documentação:** centralizada em `scoreodonto.com/documentação/` (este diretório).
### 2. Onde está o banco?
- **PostgreSQL** no host da **VPS3** (`10.99.0.3:5432`), banco `scoreodonto`, usuário `scoreodonto_user`.
- ⚠️ **DEV e PROD usam o MESMO banco.** Não há isolamento. Backup (`pg_dump -Fc`) é a única rede de segurança.
- Cache **DragonflyDB** (`10.99.0.3:6379`, índice `/1`). Mensageria **NATS** (`:4222`) e **Temporal** (`:7233`).
### 3. Onde estão os containers?
- **Produção (VPS1, docker rootless `unix:///run/user/1000/docker.sock`):**
- `scoreodontocom-scoreodonto-frontend-1` — Up ~5 dias (imagem `:latest` buildada na própria VPS1).
- `scoreodontocom-scoreodonto-backend-1` — Up ~5 dias (`:8018`).
- `traefik-traefik-1` — borda TLS (Up ~13 dias). Também: rx, newwhats, subdominio, mercado.
- **Dev (VPS4, docker root `/var/run/docker.sock`):**
- `scoreodontocom-nginx-1` (`0.0.0.0:8020->80`, alvo do Traefik para `dev.`), `scoreodonto-frontend-1` (`:3013`), `scoreodonto-backend-1` (`:8018`).
- **Roteamento:** Traefik na VPS1 → `scoreodonto.com` (containers locais) e `dev.scoreodonto.com``http://10.99.0.4:8020`.
### 4. Quem faz deploy?
- **Hoje: ninguém automático.** Não há listener nem webhook ativo (o antigo `listener-scoreodonto.js` porta 9002 foi deletado; serviço desligado). **`git push` NÃO dispara deploy.**
- O deploy é **manual**: na VPS1, `git pull` + `docker compose build` + `docker compose up -d` (passo a passo em `DEPLOY-PRODUCAO.md`).
- Ou seja: **a produção builda a si mesma** hoje (o oposto do alvo "prod não builda", que depende do registry — Etapas 46).
### 5. Como versiona?
- `APP_VERSION` em `frontend/constants.ts` (dev hoje **`V1.0.13`**), incrementado por `update-version.js` (patch++).
- ⚠️ **Bump é manual** — nada chama `update-version.js` no build (`npm run build` = só `vite build`). Por isso a versão fica parada se ninguém rodar o script.
- ✅ **Versão agora é visível** (Etapa 3, 16/jun): rodapé do Sidebar + endpoint `GET /api/version` + log de boot `[VERSION] …`. Carimbo (commit/data/ambiente) injetado no build via build args. **No ar em DEV; produção passa a exibir após o próximo deploy.**
### 6. Como volta uma versão (rollback)?
- **Hoje:** na VPS1, `git checkout <commit-bom>` + `docker compose build` + `up -d` (rebuilda a versão anterior; imagens são `:latest`, sem tag por versão).
- Seguro porque as migrações são **aditivas** (voltar código não quebra o schema). Restaurar dump só em caso extremo.
- **Alvo (Etapa 6):** rollback = `docker compose pull` da tag anterior no registry, **sem rebuild**.
---
## Veredito de uma linha
> O código está salvo no Gitea (VPS3) e roda em containers na VPS1 (prod) e VPS4 (dev), ambos no **mesmo banco** da VPS3. **Não há CI/CD automático**: deploy e rollback são manuais via `git pull` + `docker compose build/up` na VPS1, e a **versão é invisível e bumpada à mão** — as duas dores que as Etapas 36 vão eliminar.
+131
View File
@@ -0,0 +1,131 @@
# Arquitetura ScoreOdonto — Estado REAL
> Documento de **estado atual verificado** (16/jun/2026). Descreve apenas o que existe e está rodando hoje.
> Sem referências históricas, sem staging, sem listeners desativados. Para o fluxo-alvo de deploy ver `deploy.md`; para o passo a passo operacional ver `DEPLOY-PRODUCAO.md`.
---
## 1. Visão geral
O ScoreOdonto roda sobre **3 VPS** ligadas por uma rede privada **WireGuard** (`10.99.0.0/24`):
| VPS | IP interno | Papel |
|------|-------------|-------------------------------|
| VPS1 | 10.99.0.1 | **Produção** (borda + apps) |
| VPS3 | 10.99.0.3 | **Dados & Controle** (Postgres, Gitea) |
| VPS4 | 10.99.0.4 | **Desenvolvimento & Builder** |
```
Internet (HTTPS)
┌──────────────────────────── VPS1 (10.99.0.1) ─────────────────────────────┐
│ Traefik (TLS Let's Encrypt, entrypoint websecure) │
│ • scoreodonto.com / www ─────► containers de PRODUÇÃO (local) │
│ • dev.scoreodonto.com ─────► http://10.99.0.4:8020 (via WireGuard) │
│ │
│ Produção (imagens buildadas): │
│ scoreodontocom-scoreodonto-frontend-1 (SPA em nginx) │
│ scoreodontocom-scoreodonto-backend-1 (Node :8018) │
│ (também hospeda: rx, newwhats, subdominio, mercado) │
└────────────────────────────────┬──────────────────────────────────────────┘
│ WireGuard
┌───────────────────────┼───────────────────────┐
▼ ▼
┌──── VPS3 (10.99.0.3) ────┐ ┌──── VPS4 (10.99.0.4) ──────────┐
│ PostgreSQL :5432 │◄────────────│ Desenvolvimento + Builder │
│ db: scoreodonto │ (dev E │ nginx (borda dev) :8020→80 │
│ Dragonfly :6379 (/1) │ prod │ └─► frontend :3013 │
│ NATS :4222 │ usam o │ └─► backend (/api):8018 │
│ Temporal :7233 │ MESMO │ Código-fonte em: │
│ Gitea :3000 │ banco) │ /home/deploy/stack/ │
│ repo: ruicesar/ │ │ scoreodonto.com/ │
│ scoreodonto.com │ │ Docker: socket ROOT │
│ Registry: NÃO existe │ │ (/var/run/docker.sock) │
│ (porta 5000 fechada) │ │ │
└───────────────────────────┘ └─────────────────────────────────┘
```
---
## 2. VPS1 — Produção (10.99.0.1)
- **Traefik** (container `traefik-traefik-1`) é a borda: termina TLS (Let's Encrypt, resolver `le`, entrypoint `websecure`) e roteia por `Host`.
- `scoreodonto.com` e `www.scoreodonto.com` → containers de produção locais.
- `dev.scoreodonto.com``http://10.99.0.4:8020` (encaminha para a VPS4 pela WireGuard). Config dinâmica em `traefik/dynamic/dev.scoreodonto.yml`.
- **Containers de produção** (imagens já buildadas, project `scoreodontocom`):
- `scoreodontocom-scoreodonto-frontend-1` — SPA React servido por nginx.
- `scoreodontocom-scoreodonto-backend-1` — API Node/Express na porta `8018`.
- **Docker rootless**: o daemon roda no socket do usuário `deploy` (`unix:///run/user/1000/docker.sock`). Comandos `docker` na VPS1 precisam dessa variável `DOCKER_HOST`.
- Produção **não builda** o ScoreOdonto: ela sobe imagens prontas. (O `docker-compose.yml` tem `build:`, mas o fluxo correto é trazer a imagem pronta da VPS4 — ver `deploy.md`.)
## 3. VPS3 — Dados & Controle (10.99.0.3)
Serviços instalados **direto no host** (não em container):
- **PostgreSQL** `:5432` — banco `scoreodonto`, usuário `scoreodonto_user`. **Fonte de dados única.**
- **DragonflyDB** `:6379` — cache (índice `/1` reservado ao ScoreOdonto).
- **NATS** `:4222` e **Temporal** `:7233` — mensageria / workflows.
- **Gitea** `:3000`**fonte oficial da verdade do código**. Repo `ruicesar/scoreodonto.com`, acesso por `ssh://git@10.99.0.3/ruicesar/scoreodonto.com.git`. É o motor de CI/CD (ver `deploy.md`).
- **Registry Docker privado:** **ainda não existe** (porta 5000 fechada). Planejado para a VPS3.
## 4. VPS4 — Desenvolvimento & Builder (10.99.0.4)
- **Esta é a máquina de trabalho.** Código-fonte em `/home/deploy/stack/scoreodonto.com/`.
- **Docker é ROOT** aqui (socket `/var/run/docker.sock`) — diferente da VPS1.
- Containers em execução (project `scoreodontocom`):
- `scoreodontocom-nginx-1` — borda do dev, publica `0.0.0.0:8020->80`. É o alvo do Traefik para `dev.scoreodonto.com`.
- `scoreodontocom-scoreodonto-frontend-1` — SPA (porta interna `3013`).
- `scoreodontocom-scoreodonto-backend-1` — API Node (porta interna `8018`).
- **O dev NÃO é Vite HMR.** O container de frontend roda uma **imagem buildada** (`vite build``nginx:alpine`); editar `frontend/` ou `backend/` exige **rebuild + recreate** do container para refletir em `dev.scoreodonto.com`. (Existe um override `docker-compose.dev.yml` com Vite/HMR, mas não é o que está no ar.)
- O `nginx/default.conf` da VPS4 faz: `/api/` e `/socket.io/``scoreodonto-backend:8018`; `/``scoreodonto-frontend:3013`.
### Caminho de uma requisição
- **Produção:** navegador → `https://scoreodonto.com` → Traefik (VPS1, TLS) → frontend/backend de produção (VPS1) → Postgres (VPS3).
- **Dev:** navegador → `https://dev.scoreodonto.com` → Traefik (VPS1, TLS) → `10.99.0.4:8020` (nginx VPS4) → frontend/backend de dev (VPS4) → **mesmo Postgres** (VPS3).
---
## 5. ⚠️ Banco compartilhado entre DEV e PROD
**Dev e produção apontam para o MESMO banco `scoreodonto` na VPS3.** O backend de dev usa `DATABASE_URL=postgresql://scoreodonto_user:<senha>@10.99.0.3:5432/scoreodonto`.
Consequências práticas:
- Migrações rodadas no boot do backend de dev **já alteram o banco real**. As migrações do projeto são **idempotentes e aditivas** (só criam tabelas/colunas), o que torna isso tolerável — mas **não há isolamento**.
- A **única rede de segurança** é o backup (`pg_dump scoreodonto -Fc`) antes de mudanças sensíveis.
- Isolar um banco `scoreodonto_dev` é o conserto estrutural recomendado para o futuro (fora do escopo deste documento).
---
## 6. Stack técnica e portas
| Componente | Tecnologia | Porta interna |
|------------|-----------|----------------|
| Frontend | React + Vite + TypeScript + Tailwind (build estático servido por nginx) | 3013 |
| Backend | Node.js + Express + driver `pg` nativo (**sem ORM**) | 8018 |
| Borda dev (VPS4) | nginx:alpine | 8020 → 80 |
| Banco | PostgreSQL 18 | 5432 |
| Cache | DragonflyDB (índice `/1`) | 6379 |
| Mensageria | NATS / Temporal | 4222 / 7233 |
| Git/CI | Gitea | 3000 |
| Borda prod | Traefik (TLS Let's Encrypt) | 443 (websecure) |
**Imagens (Dockerfiles):**
- Frontend: multi-stage — `node:20-alpine` (build `npm run build`) → `nginx:alpine` (serve `dist/`).
- Backend: `node:20-slim`, `npm install --omit=dev`, `CMD node server.js`.
---
## 7. Versionamento (estado atual e limitação)
- A versão vive em `frontend/constants.ts` (`APP_VERSION`, hoje `V1.0.12`) e é incrementada (patch) por `update-version.js` antes do build.
- **Limitação crítica:** a versão **não aparece** no HTML servido — fica embutida no bundle `index-<hash>.js`. Hoje só dá para saber a versão no ar inspecionando o container. É por isso que não se consegue confirmar "subiu mesmo?" pelo navegador. **Tornar a versão visível** (rodapé + endpoint + log de boot) é uma melhoria planejada (ver lista de afazeres / `deploy.md`).
---
## 8. O que NÃO existe (para encerrar fantasmas)
- ❌ **Não existe staging.** Não há `staging.scoreodonto.com`, nem stack `*-staging`, nem banco `scoreodonto_staging`. O único ambiente de teste é `dev.scoreodonto.com` (VPS4), com o banco de produção.
- ❌ **Não existe listener de deploy ativo** para o scoreodonto.com. O antigo `listener-scoreodonto.js` (porta 9002) foi deletado e o serviço está desligado; não há webhook do scoreodonto.com registrado no Gitea. Hoje, `git push` **não dispara** deploy automático. (O fluxo-alvo para reativar isso está em `deploy.md`.)
- ❌ **Não existe registry privado** ainda (planejado para a VPS3).
+100
View File
@@ -0,0 +1,100 @@
# Deploy ScoreOdonto — Como um commit vira produção (estado-ALVO)
> Este documento responde **uma** pergunta: **"como um commit vira produção?"**
> Descreve o **fluxo-alvo** com **Gitea como motor de CI/CD**. O que já existe hoje está marcado como ✅; o que ainda será construído está marcado como 🔜.
> Para a realidade da infra ver `arquitetura.md`. Para o procedimento manual executável **hoje** ver `DEPLOY-PRODUCAO.md`.
---
## 1. Princípios (inegociáveis)
1. **Gitea (VPS3) é a fonte única da verdade** e o motor de CI/CD.
2. **Produção (VPS1) nunca builda e nunca recebe código-fonte** — só consome **imagens versionadas** prontas.
3. **Build acontece fora da produção** (na CI / na VPS4).
4. **Deploy é um ato deliberado por TAG**, nunca automático em todo push (a `main` é usada para desenvolvimento e o banco é compartilhado — promover sem querer é perigoso).
5. **Rollback é trocar a imagem** por uma tag anterior — sem rebuild, sem recompilar, sem copiar arquivo.
6. **Toda imagem é rastreável**: carrega versão semântica + commit + data de build.
---
## 2. O fluxo-alvo, em uma frase
> **Push** abastece o desenvolvimento e (🔜) **produz uma imagem versionada no registry**;
> **criar uma tag `vX.Y.Z`** é o que **promove** essa imagem para produção.
```
git push ──► Gitea (VPS3) ──► CI: build da imagem ──► Registry privado (VPS3)
(na main) (versão+commit+data) imagem :vX.Y.Z
criar tag vX.Y.Z ────┘
VPS1: docker compose pull + up -d
(troca o container pela imagem da tag)
Produção rodando vX.Y.Z
```
---
## 3. As duas metades do fluxo
### Metade A — Integração contínua (push → imagem) 🔜
1. Desenvolvedor trabalha na VPS4 e valida em `https://dev.scoreodonto.com`.
2. `git push origin main` para o Gitea (VPS3).
3. A **CI do Gitea** (Gitea Actions) builda a imagem Docker (frontend multi-stage + backend), injetando **versão semântica, commit curto e data de build**.
4. A imagem é enviada ao **Registry privado da VPS3** com tags como `:main`, `:<commit-curto>`.
5. **Nada vai para produção aqui.** Esta metade só produz e guarda o artefato.
### Metade B — Entrega por tag (tag → produção) 🔜
1. Quando uma versão está aprovada, cria-se uma **tag semântica**: `git tag v1.0.15 && git push origin v1.0.15`.
2. A CI marca a imagem correspondente como `:v1.0.15` no registry (promoção do artefato já testado — sem rebuild).
3. A VPS1 recebe o gatilho de entrega (job de deploy / acionamento manual aprovado) e roda:
`docker compose pull && docker compose up -d` apontando para `:v1.0.15`.
4. A versão visível (rodapé/endpoint) confirma `v1.0.15` no ar.
---
## 4. Versionamento visível (pré-requisito da confiança) ✅ FEITO
> Implementado em 16/jun/2026 (Etapa 3). O build carimba versão/commit/data/ambiente via build args (`GIT_COMMIT`, `BUILD_TIME`, `APP_ENV`, `APP_VERSION`) → `import.meta.env.VITE_*` no frontend e `process.env` no backend. Visível no **rodapé do Sidebar**, no endpoint **`GET /api/version`** e na **linha de boot** `[VERSION] …` dos logs.
Cada build carimba e **exibe**:
```
ScoreOdonto
v1.0.15
commit: 7a9b3c1
build: 2026-06-16 14:32 UTC
ambiente: PRODUÇÃO
```
Visível no **rodapé** da aplicação, em um **endpoint de diagnóstico** (ex.: `GET /api/version`) e na **linha de boot** dos logs. Hoje a versão fica escondida no bundle `index-<hash>.js` (ver `arquitetura.md` §7) — esta é a primeira melhoria de maior valor.
---
## 5. Rollback 🔜
- Listar as tags/imagens no registry da VPS3.
- Na VPS1, apontar o compose para a tag anterior (`:v1.0.14`) e `docker compose up -d`.
- Recuperação em minutos, **sem rebuild**. O banco é aditivo (ver `arquitetura.md` §5), então voltar o código é seguro; restaurar dump só em caso extremo.
---
## 6. Estado de hoje (honesto) ✅/❌
- ✅ `git push` para o Gitea **funciona** (guarda o código).
- ❌ **Não há build automático**, **não há registry**, **não há deploy automático**. O antigo listener (porta 9002) foi deletado e não há webhook do scoreodonto.com no Gitea.
- ➡️ Enquanto a CI acima não existe, a promoção para produção é **manual** e está documentada passo a passo em **`DEPLOY-PRODUCAO.md`** (build na VPS4 → enviar imagem para a VPS1 → `up -d`).
---
## 7. Roadmap para sair do "hoje" e chegar no "alvo"
| Etapa | Entrega | Status |
|-------|---------|--------|
| 3 | Versionamento visível (rodapé + endpoint + log) | ✅ |
| 4 | Registry Docker privado na VPS3 | 🔜 |
| 5 | Pipeline de build no Gitea (push → imagem → registry), **sem deploy** | 🔜 |
| 6 | Deploy por TAG (`vX.Y.Z` → produção na VPS1) + rollback por imagem | 🔜 |
Quando as Etapas 36 estiverem prontas, este documento deixa de ter marcações 🔜 e passa a descrever o fluxo corrente.