docs: unifica documentação em doc/ (fonte única, sem sobreposição)
- Pasta única scoreodonto.com/doc/ com 9 docs que prestam: VERDADE-HOJE (estado, principal), DEPLOY-PRODUCAO (runbook), BANCO-DEV-ESTRATEGIA, REGISTRY, AUDITORIA-FUNCIONAL, BACKLOG-FINANCEIRO-V1, CRM_Ortodontia, RX_ARQUITETURA, RX_BACKLOG. - Removidos os docs de infra sobrepostos/desatualizados (estado consolidado em VERDADE-HOJE; histórico fica no git): arquitetura.md, deploy.md, CHECKLIST-DEPLOY-PRODUCAO.md, PENDENCIAS.md, PENDENCIAS_E_DEPLOY.md. - Pasta documentação/ eliminada. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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 1–5.
|
||||
|
||||
### 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`.
|
||||
@@ -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 (1–2 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_fim−data_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,160 @@
|
||||
# Estratégia de Banco DEV — Relatório + Plano de Implementação
|
||||
|
||||
> Documento de **decisão e plano** (16/jun/2026). A 1ª parte é o relatório técnico baseado em dados reais; a 2ª é o plano de implementação profissional, escalável para **todos os projetos atuais e futuros**.
|
||||
> Relacionado: `arquitetura.md`, `PENDENCIAS.md` (P0 isolar dev), `VERDADE-HOJE.md`.
|
||||
|
||||
---
|
||||
|
||||
# PARTE 1 — Relatório técnico (dados reais, read-only)
|
||||
|
||||
**Servidor (VPS3):** PostgreSQL **18.4** · **PostGIS 3.6.2** (suíte completa disponível).
|
||||
|
||||
**Bancos (8 no total, ~175 MB somados):**
|
||||
|
||||
| Banco | Tamanho | Projeto | DEV na VPS4? | Tabelas | Índices | PostGIS |
|
||||
|-------|--------:|---------|:------------:|--------:|--------:|:-------:|
|
||||
| newwhats | 91 MB | NewWhats (VPS1) | não | — | — | — |
|
||||
| giteadb | 22 MB | Gitea (infra) | não | — | — | não |
|
||||
| **scoreodonto** | **21 MB** | **ScoreOdonto** | **sim** | 104 | 168 | **SIM (3 geog)** |
|
||||
| mercado | 13 MB | Mercado (VPS1) | não | — | 187 | não |
|
||||
| **dental_images** | **11 MB** | **RX** | **sim** | 8 | 25 | não |
|
||||
| clube67 | 8.8 MB | Clube67 | não | — | — | — |
|
||||
| **subdominio** | **8 MB** | **Subdomínio** | **sim** | 3 | 7 | não |
|
||||
| postgres | 7.7 MB | default | — | — | — | — |
|
||||
|
||||
**Conclusões:**
|
||||
- Os 3 bancos de DEV (scoreodonto, dental_images, subdominio) somam **~40 MB**. São minúsculos.
|
||||
- **Só o ScoreOdonto usa PostGIS.** RX e Subdomínio não.
|
||||
- Sem dependências entre bancos. Sem dados de crescimento histórico (não medível sem superuser); porte indica crescimento lento.
|
||||
|
||||
**Veredito:** a proposta "mirror + dev por projeto" (6 containers PostgreSQL/PostGIS) é **viável mas exagerada** para esse porte. Um "mirror" *rodando* não agrega sobre um **template database** ou um **dump**; é só RAM e complexidade a mais (~300–600 MB RAM e 6× manutenção).
|
||||
|
||||
**Recomendação final:** **um único container PostGIS na VPS4** (rootless), multi-banco, com `*_ref` (referência/template, restaurado da VPS3) + `*_dev` (de trabalho, criado via `CREATE DATABASE ... TEMPLATE`). Reset instantâneo, custo ~50–100 MB RAM, manutenção mínima, e **escala para qualquer projeto novo**.
|
||||
|
||||
---
|
||||
|
||||
# PARTE 2 — Plano de implementação (profissional, escalável)
|
||||
|
||||
> Objetivo: desgrudar **todos** os ambientes DEV do banco de produção (VPS3), com um padrão único que serve projetos atuais e futuros. **Produção (VPS3/VPS1) nunca é alterada — só lida (pg_dump).**
|
||||
|
||||
## 2.1 Arquitetura
|
||||
|
||||
```
|
||||
VPS3 (produção, fonte da verdade)
|
||||
│ pg_dump (somente leitura, one-way)
|
||||
▼
|
||||
┌──────────────── VPS4 (rootless) — container único "pgdev" ───────────────┐
|
||||
│ postgis/postgis 18-3.6 (porta interna, rede 'soc', sem exposição pública) │
|
||||
│ │
|
||||
│ scoreodonto_ref ──TEMPLATE──► scoreodonto_dev ◄── backend dev ScoreOdonto │
|
||||
│ dental_images_ref ─TEMPLATE──► dental_images_dev ◄── backend dev RX │
|
||||
│ subdominio_ref ──TEMPLATE──► subdominio_dev ◄── backend dev Subdomínio │
|
||||
│ <novo>_ref ──TEMPLATE──► <novo>_dev ◄── (projeto futuro) │
|
||||
└────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
- **`*_ref`**: cópia fiel da produção (restaurada por `pg_dump`). Não se mexe — é a referência/"mirror".
|
||||
- **`*_dev`**: banco de trabalho, recriável do `_ref` em segundos via `TEMPLATE`. É onde rodam migrações e testes.
|
||||
- **Um container só** atende todos (Postgres lida com dezenas de bancos pequenos sem esforço). PostGIS coberto para quem precisa (ScoreOdonto); os demais simplesmente não usam.
|
||||
|
||||
## 2.2 Componentes (a criar em `/home/deploy/stack/pg-dev/`)
|
||||
|
||||
**a) `docker-compose.yml`** (container do banco DEV):
|
||||
```yaml
|
||||
services:
|
||||
pgdev:
|
||||
image: postgis/postgis:18-3.6 # confirmar tag p/ PG18+PostGIS3.6 (alt.: imresamu/postgis:18-3.6)
|
||||
container_name: pgdev
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: devadmin
|
||||
POSTGRES_PASSWORD: ${PGDEV_PASSWORD}
|
||||
volumes:
|
||||
- pgdev-data:/var/lib/postgresql/data
|
||||
networks: [soc] # mesma rede dos backends de dev (acesso por DNS 'pgdev')
|
||||
# SEM ports: publicado → só acessível internamente (segurança)
|
||||
volumes:
|
||||
pgdev-data:
|
||||
networks:
|
||||
soc:
|
||||
external: true
|
||||
```
|
||||
|
||||
**b) `pg-dev-projects.conf`** (manifesto — adicionar 1 linha por projeto novo):
|
||||
```
|
||||
# nome_projeto : banco_origem_VPS3 : usuario_origem
|
||||
scoreodonto : scoreodonto : scoreodonto_user
|
||||
rx : dental_images : clube67
|
||||
subdominio : subdominio : mercado_admin
|
||||
```
|
||||
|
||||
**c) `.env` / `secrets`** (gitignored, fora do repo): senha do `pgdev` (`PGDEV_PASSWORD`) e as senhas de origem da VPS3. **Nunca** versionar.
|
||||
|
||||
**d) `pg-dev-refresh.sh <projeto>`** — atualiza o `_ref` a partir da VPS3 e recria o `_dev`:
|
||||
```
|
||||
1. pg_dump -Fc da VPS3 (banco_origem) usando a credencial do manifesto (read-only).
|
||||
2. DROP/CREATE <projeto>_ref no pgdev; pg_restore do dump → _ref.
|
||||
3. DROP <projeto>_dev; CREATE DATABASE <projeto>_dev TEMPLATE <projeto>_ref.
|
||||
```
|
||||
|
||||
**e) `pg-dev-reset.sh <projeto>`** — reset rápido (sem ir à VPS3):
|
||||
```
|
||||
DROP <projeto>_dev; CREATE DATABASE <projeto>_dev TEMPLATE <projeto>_ref;
|
||||
```
|
||||
|
||||
## 2.3 Apontar os backends de DEV para o pgdev (sem afetar produção)
|
||||
|
||||
Parametrizar a `DATABASE_URL` no compose de cada projeto para usar variáveis com **default = produção**, e na VPS4 criar um `.env` (gitignored) que sobrescreve só no dev:
|
||||
```
|
||||
# no docker-compose.yml (default mantém produção intacta):
|
||||
DATABASE_URL=postgresql://${DB_USER}:${DB_PASS}@${DB_HOST:-10.99.0.3}:5432/${DB_NAME:-scoreodonto}
|
||||
|
||||
# .env SÓ na VPS4 (dev):
|
||||
DB_HOST=pgdev
|
||||
DB_NAME=scoreodonto_dev
|
||||
DB_USER=devadmin
|
||||
DB_PASS=<senha pgdev>
|
||||
```
|
||||
- Na VPS1 (produção) não existe esse `.env` → resolve para `10.99.0.3/scoreodonto`. **Produção nunca muda.**
|
||||
- Rebuild/recreate do backend de dev → passa a falar com `pgdev`.
|
||||
|
||||
## 2.4 Passos de implantação (quando aprovado)
|
||||
|
||||
1. Criar `/home/deploy/stack/pg-dev/` com o compose + scripts + `.env` (secrets).
|
||||
2. `docker compose up -d pgdev` (rootless) + adicionar `pgdev` à rede `soc`.
|
||||
3. Rodar `pg-dev-refresh.sh scoreodonto` (e rx, subdominio) → cria os `_ref` e `_dev`.
|
||||
4. Criar o `.env` de dev em cada projeto e recriar os backends de dev.
|
||||
5. Verificar: `dev.scoreodonto.com/api/version` sobe; conferir que escrita em dev cai em `scoreodonto_dev` (e produção em `scoreodonto` permanece intacta).
|
||||
6. (privacidade) opcional: script de anonimização de dados sensíveis nos `_dev`.
|
||||
|
||||
## 2.5 Operação do dia a dia
|
||||
|
||||
- **Quer dados atualizados da produção?** `pg-dev-refresh.sh <projeto>`.
|
||||
- **Sujou o dev e quer voltar ao limpo?** `pg-dev-reset.sh <projeto>` (segundos, via TEMPLATE).
|
||||
- **Projeto novo?** adiciona 1 linha no `pg-dev-projects.conf` + `pg-dev-refresh.sh <novo>`. Pronto — sem novo container.
|
||||
|
||||
## 2.6 Custo e ganho
|
||||
|
||||
- **Custo:** 1 container (~50–100 MB RAM, ~100 MB disco). Disco da VPS4 hoje: ~64 GB livres → folga total.
|
||||
- **Ganho:** dev deixa de poder corromper produção; resets instantâneos; PostGIS coberto; padrão único que escala para N projetos sem multiplicar containers.
|
||||
|
||||
## 2.7 O que este plano deliberadamente NÃO faz
|
||||
- Não cria 2 containers por projeto (over-engineering p/ bancos de 8–21 MB).
|
||||
- Não usa réplica streaming (exigiria config superuser na VPS3 + WAL; canhão p/ 40 MB).
|
||||
- Não toca em produção (VPS3/VPS1) — refresh é one-way, só `pg_dump`.
|
||||
|
||||
---
|
||||
|
||||
# PARTE 3 — Status de implementação (16/jun/2026)
|
||||
|
||||
**Implementado em `/home/deploy/stack/pg-dev/`** (Docker rootless da VPS4):
|
||||
- ✅ Container **`pgdev`** (`postgis/postgis:18-3.6`) na rede `soc`, sem porta pública, volume `pgdev-data` em `/var/lib/postgresql`.
|
||||
- ✅ Scripts `pg-dev-refresh.sh` / `pg-dev-reset.sh` + manifesto `pg-dev-projects.conf` + `secrets.env` (gitignored).
|
||||
- ✅ Bancos criados (ref+dev): **scoreodonto** (20 MB, PostGIS 3.6.3, 1750 pacientes), **rx** (dental_images), **subdominio**.
|
||||
|
||||
**Backends apontados (via `.env` gitignored + `DATABASE_URL`/`PG_*` parametrizados com default=produção):**
|
||||
- ✅ **ScoreOdonto** → `pgdev/scoreodonto_dev` (validado: backend conecta, migrações OK; `dev.scoreodonto.com` no ar). Exigiu ajuste de SSL no backend (não força SSL p/ host `@pgdev`).
|
||||
- ✅ **Subdomínio** → `pgdev/subdominio_dev` (env confirmado, site no ar).
|
||||
- ✅ **RX** → `pgdev/rx_dev` (`rx.scoreodonto.com` no ar). Solução: app adicionado à rede `soc` + `DB_HOST/DB_NAME` parametrizados; role `clube67` criado no pgdev com a senha do `.secrets` (assim DB_USER/senha não mudam).
|
||||
|
||||
**Produção (VPS3/VPS1) intocada.** Para um projeto novo: adicionar linha no `pg-dev-projects.conf`, `SRC_<proj>_PASS` no `secrets.env`, rodar `./pg-dev-refresh.sh <proj>` e parametrizar o compose do projeto + `.env`.
|
||||
@@ -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
|
||||
@@ -0,0 +1,142 @@
|
||||
# Deploy & Versionamento — Runbook ScoreOdonto
|
||||
|
||||
> Auditado contra o sistema em **17/jun/2026**. Cada item carrega seu **status real** — o doc
|
||||
> responde *"o que existe hoje?"*, não *"o que gostaríamos?"*.
|
||||
>
|
||||
> **Legenda de status (convenção do projeto):**
|
||||
> - ✅ **EM PRODUÇÃO** — está no ar em `scoreodonto.com` (VPS1), verificável agora.
|
||||
> - 🧪 **EM DEV** — implementado no working tree da VPS4 / `dev.scoreodonto.com`; **não commitado, não promovido**.
|
||||
> - 🎯 **ALVO** — desenhado/aprovado, **ainda não implementado**.
|
||||
>
|
||||
> **Produção AGORA:** `v1.0.15 · commit f2eb90c · 2026-06-17` (`curl https://scoreodonto.com/api/version`).
|
||||
> Os Passos 1/3/4 (versão em runtime) foram **promovidos a produção** em 17/jun (tag `v1.0.15`).
|
||||
|
||||
---
|
||||
|
||||
## 1. O modelo
|
||||
|
||||
> **Push** na `main` produz imagem por **commit** no registry; a **tag `vX.Y.Z`** **promove** essa imagem para produção. **Produção nunca builda — só executa imagens.**
|
||||
|
||||
```
|
||||
PUSH main (commit C) TAG vX.Y.Z (no commit C)
|
||||
│ │
|
||||
▼ ▼
|
||||
CI: build CI: deploy
|
||||
publica :C, :<versão>, :latest ✅ hoje: REBUILDA + promove + deploya
|
||||
▼ 🎯 alvo (Passo 5): só re-tag :C→:vX.Y.Z, SEM build
|
||||
registry ▼
|
||||
VPS1: pull + up --no-build ✅
|
||||
APP_VERSION=tag em runtime ✅ (Passo 3, em prod)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Estado por componente (verificado)
|
||||
|
||||
| Componente | Status | Detalhe |
|
||||
|---|---|---|
|
||||
| Infra VPS1/3/4 + Docker rootless | ✅ | Mapa na §3 |
|
||||
| Registry (`git.clube67.com/ruicesar/...`) | ✅ | Ver `REGISTRY.md` |
|
||||
| CI: `act_runner` "vps4-builder" builda+publica em push | ✅ | `.gitea/workflows/build.yml`; runner systemd na VPS4 |
|
||||
| Deploy por tag (job `deploy` SSH VPS4→VPS1) | ✅ | `deploy-prod.sh` na VPS1 |
|
||||
| Produção não builda (`image:` + `pull` + `up --no-build`) | ✅ | `docker-compose.prod.yml` |
|
||||
| Banco DEV isolado em `pgdev`; prod no Postgres da VPS3 | ✅ | Ver `BANCO-DEV-ESTRATEGIA.md` |
|
||||
| `GET /api/version` responde (de `process.env`) | ✅ | No ar: v1.0.15 · f2eb90c · PROD |
|
||||
| **Versão exibida em Configurações → "Sobre o sistema"** | ✅ | Passo 1 (promovido em v1.0.15; saiu do Sidebar) |
|
||||
| **Front/telas consomem `/api/version` (`useAppVersion`); app não usa `APP_VERSION`** | ✅ | Passos 1/4 (em prod desde v1.0.15) |
|
||||
| **Deploy injeta `APP_VERSION`/`APP_ENV` em runtime (`environment`)** | ✅ | Passo 3 (`compose.prod`/`deploy-prod.sh` em prod) |
|
||||
| **Tag promove sem rebuild + validação de integridade** | 🎯 | Passo 5 (§7) — único item pendente |
|
||||
|
||||
---
|
||||
|
||||
## 3. Mapa de máquinas ✅
|
||||
|
||||
| Onde | Host | Papel | Docker |
|
||||
|---|---|---|---|
|
||||
| VPS1 | `10.99.0.1` | **Produção** (Traefik + apps) | rootless (`unix:///run/user/1000/docker.sock`) |
|
||||
| VPS3 | `10.99.0.3` | Postgres (prod) + Gitea + registry | — |
|
||||
| VPS4 | `10.99.0.4` | **Dev + Builder** (código; runner CI; `pgdev`) | rootless |
|
||||
|
||||
- Repo (fonte da verdade): `ssh://git@10.99.0.3/ruicesar/scoreodonto.com.git`.
|
||||
- Project Compose: `scoreodontocom`. Diretório nas duas máquinas: `/home/deploy/stack/scoreodonto.com`.
|
||||
|
||||
---
|
||||
|
||||
## 4. De onde vem a versão (contrato `/api/version`)
|
||||
|
||||
```json
|
||||
{ "name": "ScoreOdonto API", "version": "v1.0.15", "commit": "f2eb90c",
|
||||
"build": "2026-06-17 03:37 UTC", "environment": "PROD" }
|
||||
```
|
||||
- **commit / build** = carimbados no **build** da imagem (imutáveis — identidade do artefato). ✅
|
||||
- **version / environment** = **injetados no deploy em runtime** (a partir da tag) via `environment` do `compose.prod` ✅ (Passo 3, em prod) — a mesma imagem pode ser promovida sob qualquer tag sem rebuild.
|
||||
- **Frontend** = lê tudo de `/api/version` via `useAppVersion()`; **não embute mais a versão** ✅ (Passos 1/4, em prod). Fonte única = backend.
|
||||
|
||||
---
|
||||
|
||||
## 5. Runbook — subir um release (fluxo ATUAL ✅)
|
||||
|
||||
> ⚠️ **Regra de ouro:** confirme **DEV ou PRODUÇÃO** antes de agir. `dev.scoreodonto.com` = VPS4 (banco `pgdev`, descartável). `scoreodonto.com` = VPS1 (real). **Nunca** `git push --force` na `main`. Push/tag exigem autorização explícita.
|
||||
|
||||
```bash
|
||||
# 1) VPS4: validar em DEV (build local do container de dev)
|
||||
cd /home/deploy/stack/scoreodonto.com
|
||||
node --check backend/server.js
|
||||
docker compose build scoreodonto-backend scoreodonto-frontend
|
||||
docker compose up -d --force-recreate scoreodonto-backend scoreodonto-frontend
|
||||
# conferir em https://dev.scoreodonto.com
|
||||
|
||||
# 2) ⚠️ transitório (até Passo 5): a TAG precisa casar com frontend/constants.ts
|
||||
node update-version.js # bump (ex. V1.0.14 → V1.0.15)
|
||||
|
||||
# 3) commit + push → CI builda e publica :<commit> + :<versão> + :latest (NÃO deploya)
|
||||
git add -A && git commit -m "feat: <descrição> (vX.Y.Z)"
|
||||
git push origin main
|
||||
|
||||
# 4) tag = "aprovado p/ produção" → CI (hoje REBUILDA ✅) promove + deploya na VPS1
|
||||
git tag v1.0.15 && git push origin v1.0.15
|
||||
|
||||
# 5) verificar SEM SSH:
|
||||
curl -s https://scoreodonto.com/api/version # → version/commit/environment
|
||||
```
|
||||
|
||||
**Backup do banco de PRODUÇÃO antes de releases sensíveis** (migrações rodam no boot; aditivas/idempotentes):
|
||||
```bash
|
||||
ssh deploy@10.99.0.3 'pg_dump -Fc scoreodonto -f ~/backups/scoreodonto_$(date +%Y%m%d_%H%M).dump'
|
||||
```
|
||||
|
||||
> ### ⚠️ Fase transitória — por que `APP_VERSION`/`constants.ts` ainda existem
|
||||
> Estamos entre o **modelo antigo** (versão escrita à mão em `constants.ts`) e o **novo**
|
||||
> (identidade pelo **commit**; tags só promovem). Até o **Passo 5**, o CI ainda lê `APP_VERSION`
|
||||
> para nomear a imagem → o bump manual continua **necessário para evitar colisão de tags no
|
||||
> registry** (republicar `:vX.Y.Z` com outro código quebraria *"uma tag = um artefato"*).
|
||||
> **Após o Passo 5:** `constants.ts` e `update-version.js` são removidos, a identidade passa a
|
||||
> ser o commit, e tags apenas **promovem** artefatos já construídos.
|
||||
|
||||
---
|
||||
|
||||
## 6. Rollback ✅
|
||||
|
||||
Reverter o **código** é seguro (migrações só adicionam). Por imagem, **sem rebuild**:
|
||||
```bash
|
||||
ssh deploy@10.99.0.1
|
||||
/home/deploy/stack/scoreodonto.com/deploy-prod.sh v1.0.13 # tag anterior → pull + up --no-build
|
||||
```
|
||||
Restaurar o dump do banco só em caso extremo (`pg_restore --clean`).
|
||||
|
||||
---
|
||||
|
||||
## 7. Passo 5 — "tag promove, não builda" 🎯
|
||||
|
||||
Desenho **aprovado (17/jun/2026), não aplicado**. Refina o CI existente para Build Once → Promote → Deploy:
|
||||
- `build.yml` → 2 jobs por ref: **build** (só push; publica `:<commit>`+`:latest`, sem `APP_VERSION`); **promote** (só tag; re-tag `:<commit>→:vX.Y.Z` via `docker buildx imagetools create`, sem rebuild) → deploy.
|
||||
- **Validação de integridade antes do deploy:** existe `backend:<C>` e `frontend:<C>`? digests válidos? alias `:vX.Y.Z` == digest de `:<C>`? front e back do **mesmo commit**? Só então deploy (evita deploy parcial).
|
||||
- Decisões fechadas: manter **`:<commit>`** (sem prefixo); **falhar** se o commit não foi pushado (nunca buildar na tag); **`:latest`** só conveniência.
|
||||
- Eliminar `update-version.js` + `frontend/constants.ts` (a versão passa a nascer da **tag git**).
|
||||
- Follow-ups: `GET /api/health`; **política de retenção** do registry.
|
||||
|
||||
Racional completo em `deploy.md` e na memória `project-cicd-tag-promove-nao-rebuilda`.
|
||||
|
||||
---
|
||||
|
||||
> **Sequência acordada para fechar o drift:** (1) doc honesta com status ✅/🧪/🎯 [este doc] → (2) commitar Passos 1/3/4 → (3) promover → (4) confirmar `/api/version` → (5) elevar `VERDADE-HOJE.md` a doc principal (com esta convenção e precedência) → (6) Passo 5 → (7) atualizar docs. À medida que cada item 🧪 for promovido, vira ✅ aqui.
|
||||
@@ -0,0 +1,89 @@
|
||||
# Registry de imagens — Container Registry do Gitea
|
||||
|
||||
> **Estado: ATIVO e validado** (16/jun/2026, Etapa 4). Não há serviço novo a manter: usamos o **Container Registry embutido do Gitea** (v1.26.1) na VPS3.
|
||||
|
||||
---
|
||||
|
||||
## 1. O que é
|
||||
|
||||
O Gitea já expõe um **registry OCI/Docker** nativo. Não foi preciso instalar `registry:2` nem tocar em `daemon.json`/`insecure-registries`. As imagens do ScoreOdonto ficam versionadas junto do código, no mesmo Gitea que é a fonte da verdade.
|
||||
|
||||
| Item | Valor |
|
||||
|------|-------|
|
||||
| Endereço (push/pull) | **`git.clube67.com`** (HTTPS, TLS válido via Traefik) |
|
||||
| Namespace | `ruicesar/` (dono do repo no Gitea) |
|
||||
| Alternativa interna | `10.99.0.3:3000` — **NÃO usar**: HTTP exige `insecure-registries` + restart do Docker (derruba produção) |
|
||||
| Auth | usuário Gitea + **token** com escopo `write:package` / `read:package` |
|
||||
|
||||
> Usamos sempre o domínio **HTTPS** justamente para **não** precisar reconfigurar nem reiniciar o Docker em VPS4/VPS1 (Live Restore está desligado → restart bouncearia todos os containers).
|
||||
|
||||
---
|
||||
|
||||
## 2. Login (uma vez por máquina)
|
||||
|
||||
Em cada máquina que vai dar push/pull (VPS4 builda; VPS1 consome):
|
||||
|
||||
```bash
|
||||
docker login git.clube67.com -u <usuario_gitea>
|
||||
# senha = TOKEN do Gitea (Configurações → Aplicações → Gerar Token, escopos write:package + read:package)
|
||||
```
|
||||
|
||||
A credencial fica no keystore do Docker (`~/.docker/config.json`). **Nunca** colocar token em código, doc, script ou log. Se um token vazar (ex.: colado em chat), **revogue no Gitea** e gere outro.
|
||||
|
||||
---
|
||||
|
||||
## 3. Convenção de nomes
|
||||
|
||||
```
|
||||
git.clube67.com/ruicesar/scoreodonto-backend:<versão>
|
||||
git.clube67.com/ruicesar/scoreodonto-frontend:<versão>
|
||||
```
|
||||
|
||||
- `<versão>` = semver com o mesmo valor de `APP_VERSION` (ex.: `v1.0.13`).
|
||||
- Mantém-se também a tag móvel `:latest` apontando para a última.
|
||||
- A tag semântica é o que a **Etapa 6 (deploy por tag)** usa para promover à produção.
|
||||
|
||||
---
|
||||
|
||||
## 4. Build + push (fluxo manual de hoje, na VPS4)
|
||||
|
||||
```bash
|
||||
cd /home/deploy/stack/scoreodonto.com
|
||||
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 # ou DEV para imagens de teste
|
||||
|
||||
docker compose build scoreodonto-backend scoreodonto-frontend
|
||||
|
||||
V=$(echo "$APP_VERSION" | tr 'V' 'v') # V1.0.13 -> v1.0.13
|
||||
docker tag scoreodontocom-scoreodonto-backend:latest git.clube67.com/ruicesar/scoreodonto-backend:$V
|
||||
docker tag scoreodontocom-scoreodonto-frontend:latest git.clube67.com/ruicesar/scoreodonto-frontend:$V
|
||||
docker push git.clube67.com/ruicesar/scoreodonto-backend:$V
|
||||
docker push git.clube67.com/ruicesar/scoreodonto-frontend:$V
|
||||
```
|
||||
|
||||
> ⚠️ **HTTP 499 em camada grande = timeout transitório do proxy.** O push é resumível: basta **repetir o `docker push`** que as camadas já enviadas são puladas e a faltante completa. (Visto e contornado na validação inicial.)
|
||||
|
||||
---
|
||||
|
||||
## 5. Pull (na VPS1, quando a Etapa 6 estiver pronta)
|
||||
|
||||
```bash
|
||||
export DOCKER_HOST=unix:///run/user/1000/docker.sock
|
||||
docker pull git.clube67.com/ruicesar/scoreodonto-backend:v1.0.13
|
||||
docker pull git.clube67.com/ruicesar/scoreodonto-frontend:v1.0.13
|
||||
# depois: compose apontando para essas imagens + docker compose up -d
|
||||
```
|
||||
|
||||
Rollback = `docker pull` da tag anterior + `up -d`. Sem rebuild.
|
||||
|
||||
---
|
||||
|
||||
## 6. Validação feita (16/jun/2026)
|
||||
|
||||
- `docker login git.clube67.com` (usuário `ruicesar`) → **Login Succeeded**.
|
||||
- Push de `scoreodonto-backend:v1.0.13`/`:latest` e `scoreodonto-frontend:v1.0.13`/`:latest` → OK (com 1 retry por 499 transitório).
|
||||
- Teste de pull (remover tag local + `docker pull`) → **Downloaded** OK.
|
||||
|
||||
Próximos: **Etapa 5** (automatizar build+push no Gitea Actions) e **Etapa 6** (deploy por tag na VPS1 consumindo este registry).
|
||||
@@ -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 1–2, 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.
|
||||
@@ -0,0 +1,42 @@
|
||||
# Qual é a verdade hoje? — Auditoria de infra ScoreOdonto
|
||||
|
||||
> Resposta direta às 6 perguntas essenciais, **verificada em 17/jun/2026**. Somente fatos do estado atual.
|
||||
> **Documento principal: em caso de divergência entre docs, este prevalece.**
|
||||
> Como operar → `DEPLOY-PRODUCAO.md` (runbook). Para onde vamos → `deploy.md` (arquitetura-alvo).
|
||||
|
||||
---
|
||||
|
||||
### 1. Onde está o código?
|
||||
- **Fonte da verdade:** Gitea na **VPS3**, repo `ruicesar/scoreodonto.com`.
|
||||
- **Dev (trabalho):** VPS4 em `/home/deploy/stack/scoreodonto.com`.
|
||||
- **Produção:** VPS1 roda **imagens versionadas do registry** (não código-fonte). No ar: **v1.0.15 · commit `f2eb90c`**.
|
||||
- **Documentação:** centralizada em `scoreodonto.com/documentação/`.
|
||||
|
||||
### 2. Onde está o banco?
|
||||
- **Produção:** PostgreSQL no host da **VPS3** (`10.99.0.3:5432`), banco `scoreodonto`, user `scoreodonto_user`.
|
||||
- ✅ **DEV ISOLADO:** a VPS4 usa um container PostGIS próprio **`pgdev`** (banco `scoreodonto_dev`), **NÃO mais o banco de produção**. Dev e prod **não compartilham** mais. (Ver `BANCO-DEV-ESTRATEGIA.md`.)
|
||||
- Cache **DragonflyDB** (`10.99.0.3:6379`, índice `/1`). **NATS** (`:4222`) e **Temporal** (`:7233`).
|
||||
|
||||
### 3. Onde estão os containers?
|
||||
- **Produção (VPS1, docker rootless):** `scoreodontocom-scoreodonto-{frontend,backend}-1` rodam a **imagem do registry** (`git.clube67.com/ruicesar/scoreodonto-*:<tag>`). Produção **não builda**. Também na VPS1: `traefik` (borda TLS), rx, newwhats, subdominio, mercado.
|
||||
- **Dev (VPS4, docker rootless):** `nginx-1` (`8020→80`, alvo do Traefik para `dev.`), `frontend-1` (`:3013`), `backend-1` (`:8018`) — **buildados localmente** e apontados ao `pgdev`.
|
||||
- **Roteamento:** Traefik na VPS1 → `scoreodonto.com` (containers locais) e `dev.scoreodonto.com` → `http://10.99.0.4:8020`.
|
||||
|
||||
### 4. Quem faz deploy?
|
||||
- **CI/CD por tag (Gitea Actions).** Push na `main` → o runner `act_runner` "vps4-builder" (VPS4, rootless) builda e **publica no registry** (NÃO deploya). `git tag vX.Y.Z && git push` → CI + job `deploy` promove para a VPS1 (`deploy-prod.sh`).
|
||||
- **Produção consome imagem** (`pull` + `up -d --no-build`).
|
||||
- ⚠️ Hoje a **tag rebuilda** antes de promover; o **Passo 5** mudará para a tag apenas **re-taguear** o artefato já construído por commit.
|
||||
|
||||
### 5. Como versiona?
|
||||
- A versão exibida vem do **backend em runtime**: `GET /api/version` (de `process.env`), mostrada em **Configurações → "Sobre o sistema"** e nas telas públicas (o frontend consome via `useAppVersion()`, não embute mais a versão). No ar: **v1.0.15 · `f2eb90c` · PROD**.
|
||||
- No deploy, `APP_VERSION` (= tag) e `APP_ENV=PROD` são **injetados em runtime**; `commit`/`build` vêm carimbados na imagem.
|
||||
- ⚠️ **Legado transitório:** `frontend/constants.ts` (`APP_VERSION`) + `update-version.js` ainda existem **só** para o CI nomear a imagem (bump manual evita colisão de tag). Serão **eliminados no Passo 5** (a versão nascerá da tag git).
|
||||
|
||||
### 6. Como volta uma versão (rollback)?
|
||||
- **Por imagem, sem rebuild:** na VPS1, `./deploy-prod.sh <ref-anterior>` → `pull` + `up -d --no-build`.
|
||||
- ⚠️ A referência confiável é o **commit** (`:<sha>`): tags `:vX.Y.Z` podem ter sido **republicadas** com código diferente (colisão de tags pré-Passo 5 — confirmado: `:v1.0.14` do registry ≠ o que rodava). Migrações são **aditivas** (voltar o código é seguro). Dump em `/home/deploy/backups/` só em caso extremo.
|
||||
|
||||
---
|
||||
|
||||
## Veredito de uma linha
|
||||
> O código vive no Gitea (VPS3); a **produção (VPS1) roda imagens versionadas do registry** (hoje **v1.0.15**), sem buildar; o **DEV está isolado no `pgdev`** (não toca o banco de produção). **CI/CD por tag ativo**, com **versão visível em runtime** (`/api/version` → Configurações). Pendência técnica principal: **Passo 5** (tag promove sem rebuild; eliminar o `APP_VERSION` manual).
|
||||
Reference in New Issue
Block a user