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

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

345 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ScoreOdonto — Pendências, Tutoria e Deploy (dev)
> Documento de handoff. Última atualização: 2026-06-12.
> Contexto completo da arquitetura de agenda/workspaces: ver memória do projeto
> `project-agenda-workspace-architecture.md`.
---
## 1. Deploy no container de dev (IMPORTANTE)
**`dev.scoreodonto.com` NÃO é Vite HMR.** É servido pelo container
`scoreodontocom-scoreodonto-frontend-1` (projeto compose `scoreodontocom`,
de `docker-compose.yml`) rodando **nginx sobre um `vite build` estático**.
Edições no fonte **não aparecem sozinhas** — exigem rebuild + recreate manual.
### Banco é de PRODUÇÃO (compartilhado)
- Backend de dev (VPS 4) e produção (VPS 1) usam o **MESMO Postgres na VPS 3**
(`10.99.0.3:5432/scoreodonto`). Não há banco de dev separado.
- Migrações aditivas idempotentes rodam no startup (`runMigrations()` em `server.js`).
- Qualquer dado criado "testando" em dev é **dado real de produção** → sempre limpar.
- A constraint `vinculos_role_check` é DROP+ADD a cada boot. Ao deployar na prod,
o `server.js` de lá PRECISA incluir `'protetico'`, senão o ADD falha se já houver
vínculo protético criado via dev.
### Comandos de deploy
```bash
cd /home/deploy/stack/scoreodonto.com
# Frontend (server.js NÃO é hot-mount; só plugins/sessions/media são):
docker compose build scoreodonto-frontend && docker compose up -d --force-recreate scoreodonto-frontend
# Backend:
docker compose build scoreodonto-backend && docker compose up -d --force-recreate scoreodonto-backend
```
- Verificar: o hash do bundle em `https://dev.scoreodonto.com/` (`/assets/index-*.js`) muda.
- `vite build` NÃO faz type-check bloqueante → erros TS não quebram o build, mas
podem estourar em runtime. Conferir após deploy.
- Logs de migração: `docker logs --tail 20 scoreodontocom-scoreodonto-backend-1 | grep -i migration`
(esperado: `[MIGRATION] all migrations OK`).
### Efeito colateral relevante
Quando esta sessão começou, o dev servia um **build antigo** (`index-DWdouzmk.js`).
Os rebuilds passaram a publicar o **working tree atual**, que inclui vários arquivos
**untracked** (WIP de outra sessão): `OrthoView`, `OrthoReports`, `OrthoPhotoWorkspace`,
`TutoriaSection`, `SuperAdminView`, `GestaoEquipeView`, `Tutor*View`, `plugins/`, etc.
→ Bugs nesses arquivos só passaram a **aparecer agora** por causa do deploy, não por edição.
---
## 2. TUTORIA no modal de tratamento de orto — ✅ CAUSA RAIZ RESOLVIDA
**Relato do usuário:** "ferrou com a tutoria no modal que administra o tratamento de orto."
**CAUSA RAIZ (confirmada):** as telas WIP (`TutoriaSection`, `OrthoPhotoWorkspace`,
`PhotoAnnotator`, `TutorPainelView`, `TutorCandidaturaView`, `AdminGestaoTutoresView`)
chamavam **24 métodos do `HybridBackend` que não existiam** em `services/backend.ts`
(só `getCurrentUserName` existia) → crash `HybridBackend.X is not a function` assim que
o rebuild publicou esses componentes. Além disso o **backend não tinha nenhuma tabela
nem endpoint** de orto/tutoria.
**CORRIGIDO nesta sessão:**
- `services/backend.ts`: adicionados os 24 métodos via helper `_ortoJson` (chama o
endpoint real e **degrada com fallback seguro** se a rota faltar → fim do crash).
- `backend/server.js`: **9 tabelas** (`ortho_photo_session`, `ortho_photo`,
`ortho_photo_annotation`, `ortho_tutor_config`, `ortho_tutor_candidatura`,
`ortho_tutor_profile`, `ortho_tutoring_request`, `ortho_tutoring_message`,
`ortho_tutoring_photo_package`) + **25 endpoints** + upload real de fotos via `multer`
no volume persistente `/app/media`, servidas por `/api/orto/fotos/:id/raw`.
- `backend/package.json`: + `multer`.
- Validado: migrations OK, 9 tabelas no banco, endpoints respondendo (200/401/404).
Frontend e backend rebuildados → live no dev.
**FEITO depois:**
- [x] `OrthoDetailPanel` agora renderiza `OrthoPhotoWorkspace` + `TutoriaSection`
(no lugar do `PhotoCompare` placeholder) — a tutoria/fotos vivem no modal do paciente.
- [x] **Sessões mensais**: orto é mensal → `ortho_photo_session` ganhou `rotulo`+`data_ref`;
o workspace cria sessão por mês (input `month`) + especiais (INICIAL/FINALIZAÇÃO/
PÓS-CONTENÇÃO); tabs dinâmicas ordenadas. Validado end-to-end.
- [x] **Exportar documentação**: botão EXPORTAR → modal seleciona "completa" (tudo) ou
fotos específicas (checkbox por foto, agrupado por sessão) → abre view de impressão
(navegador salva como PDF). Sem dependência nova.
**FEITO depois (cont.):**
- [x] **Tratamentos de orto persistem**: tabelas `ortho_tratamento` + `ortho_evolucao`
(triagem/diagnóstico/flags em JSONB); endpoints GET/POST/PUT + `/evolucoes` +
`/finalizar`. `backend.ts` troca o mock t1/t2 por chamadas reais (mappers snake↔camel).
Botão "NOVO TRATAMENTO" abre modal de criação (paciente/plano/diagnóstico/triagem).
Adicionar evolução atualiza `fio_atual`/`ultima_consulta`; finalizar grava contenção.
Validado end-to-end.
- [x] Comparador cross-sessão (mesmo slot, sessão A × B / mês a mês) com modos
**lado a lado / slider / sobreposição**.
**FEITO depois (cont.):**
- [x] **MercadoPago real**: `POST /tutoria/candidaturas/:id/pagamento` cria uma
preference (Checkout Pro) com a `taxa_inscricao` da config, `external_reference=cand:<id>`,
`back_urls` e `notification_url` → devolve `init_point`. `POST /tutoria/mp-webhook`
(público) consulta o pagamento e, se `approved`, ativa o tutor automaticamente
(`ativarCandidaturaTutor`, reaproveitado pelo confirmar-pagamento do admin).
Sem `mp_access_token` na config → fallback honesto (ativação manual). Admin configura
token/taxa em `AdminGestaoTutoresView`. **Caveat:** não testado contra a API real do MP
(sem credenciais); fallback e webhook-noop validados.
**FEITO depois (cont.):**
- [x] **GestaoEquipeView corrigida** (estava roteada em `/gestao-equipe` e quebrava igual a
tutoria — chamava 12 métodos inexistentes). Backend: tabelas `setores`/`funcoes` +
colunas `vinculos.setor_id/funcao_id`; endpoints membros (GET com nomes, POST com
setor/função, **+PUT**, DELETE) e setores/funções CRUD (todos `tenantGuard`).
`backend.ts`: os 12 métodos. Validado end-to-end + isolamento de tenant (403).
- [x] **PDF nativo** (`jspdf` adicionado às deps): o EXPORTAR agora gera um PDF real
(fotos agrupadas por sessão, 3/linha, legenda do slot) via canvas→jsPDF; botão
IMPRIMIR mantém o print→PDF como alternativa. **Caveat:** geração é client-side
(jsPDF+canvas), validei só install/tsc/build, não a renderização no navegador.
**FEITO depois (cont.):**
- [x] **Planejamento visual** no `PhotoAnnotator`: grupo PLANEJ. com **vetor de
movimentação** (haste + barra de origem + cabeça larga, arrastar da posição atual →
destino), **marca de extração** (X, clique no dente) e **linha média** (referência
vertical tracejada, clique). Persistem como anotações (JSONB) — sem mudança no backend.
**FEITO depois (cont.):**
- [x] **Type-gaps zerados — `tsc` do frontend agora passa com 0 erros.** Bug real
corrigido em `FinanceiroView` (`getPacientes` retorna `{data,total}` → usava o objeto
inteiro como lista, quebrando a busca → agora `results.data`); `TutorPainelView` ganhou
os campos financeiros na interface `Solicitacao`. Login/RxConfig limparam junto.
**FEITO depois (cont.):**
- [x] **Calibração de escala (mm)** no `PhotoAnnotator`: ferramenta ESCALA (arrastar sobre
distância conhecida → informa o valor real em mm) define mm/px; os **vetores de
movimentação passam a exibir a magnitude em mm**. Rótulos renderizados fora do SVG
(HTML posicionado por `imgDim`) p/ não distorcer com o viewBox. Persiste no JSONB.
- [x] `RxConfigView` usava props inexistentes do `PageHeader` (`subtitle`/`icon`) → trocado
por `description` (subtítulo agora aparece). **Resta 1 erro tsc pré-existente** em
`LoginView:490` (comparação morta no fluxo de cadastro/auth) — deixado de propósito:
é lógica de auth e mascarar esconderia a intenção do autor; funciona em runtime.
- [x] Comparador ganhou modo **EVOLUÇÃO**: mesmo slot em todas as sessões/meses lado a
lado (faixa horizontal cronológica) — além de lado a lado / slider / sobreposição.
- [x] Vínculo com o cadastro de pacientes: o modal de Novo Tratamento tem busca no
cadastro (`getPacientes`) que preenche `paciente_id` + nome + nascimento; digitar
livre cria paciente avulso (sem vínculo). Frontend-only, validado por tsc/build.
---
## 2b. Apuração original (histórico)
**Apuração (fatos):**
- Não foi editado nenhum arquivo de orto/tutoria nesta sessão. Todos estão **untracked**
(`git status``??`): `OrthoView.tsx`, `OrthoReports.tsx`, `TutoriaSection.tsx`,
`TutorPainelView.tsx`, `TutorCandidaturaView.tsx`, `AdminGestaoTutoresView.tsx`.
- `OrthoView.tsx` **não tem nenhuma referência a "tutor"**.
- `TutoriaSection.tsx` **não é importada em lugar nenhum** (código solto/WIP).
- Quem referencia tutor: **`OrthoReports.tsx`** (e os Tutor*View).
- Único toque indireto possível: `services/backend.ts` (eu alterei `getCurrentRole`,
mas só com fallback p/ superadmin quando não há workspace — não afeta dentista/tutoria;
`getCurrentUserName`, usado pela TutoriaSection, NÃO foi alterado).
- Provável causa: o rebuild publicou a versão WIP da tutoria que antes não estava no ar.
**PENDENTE — preciso do sintoma para diagnosticar:**
- [ ] Onde exatamente? (OrthoView do paciente / OrthoReports / "Meus Tratamentos" / outro)
- [ ] O que acontece? (crash/tela branca / botão sumiu / dado não carrega / erro console)
- [ ] Mensagem de erro no console do navegador?
→ Com o sintoma, achar a causa raiz em `OrthoReports.tsx` / `TutoriaSection.tsx` e corrigir.
---
## 3. Status das fases (arquitetura agenda/workspaces)
- [x] **Fase 0** — Cadastro: 5 cards de perfil com preço (Clínica R$499 separada). `LoginView.tsx`.
- [x] **Fase 1** — Workspace pessoal automático no `/api/register` (dentista/protético/paciente);
`clinicas.tipo`+`owner_id`; `protetico` adicionado ao `vinculos_role_check`. Fim do beco "aguardando-convite".
- [x] **Fase 2** — Identidade global: `dentistas.usuario_id` + backfill por email; criação seta usuario_id.
- [x] **Fase 3** — Anti-overbooking global: `POST /api/agendamentos` resolve identidade do
profissional e bloqueia sobreposição em QUALQUER clínica → 409 `{conflito, ocupado, podeRegistrarInteresse}`.
- [~] **Fase 4** — Fila de interesse:
- [x] Backend: tabela `interesses_agenda`, `POST/GET /api/interesses`, hook no DELETE de
agendamento que notifica interessados como `enviado_por='sistema'` e marca `liberado`.
- [x] Frontend Parte 1: AgendaView mostra painel "Horário ocupado" no 409 → registrar interesse (com minutos de espera).
- [x] Frontend Parte 2: dentista vê "Pedidos de interesse" (sino + badge + modal; GET retorna `clinica_nome`).
- [x] `POST /api/clinicas` passou a setar `owner_id`+`tipo`.
- [ ] **FALTA**: endpoint **"tentar liberar"** (clínica pede → dentista decide mover/cancelar o ocupante).
- [ ] **FALTA**: **re-notificação periódica** do sistema enquanto o dentista demora (job/cron),
sempre rotulada como "notificação do sistema" (impessoal).
- [ ] **Fase 5** — Odontodelivery: diretório (`disponivel_diretorio` já existe) → pedido de horário ao freelancer.
### Regras de negócio confirmadas
- Profissional não pode estar em 2 lugares ao mesmo tempo (invariante: 1 agendamento por slot).
- Conflito não é "não" seco: vira fila de INTERESSE (não trava o slot). Clínica pode: registrar
interesse com tempo de espera (expira), escolher outro horário, ou pedir pro dentista liberar.
- Todos têm workspace (pessoal automático no cadastro).
---
## 4. Bugs pré-existentes encontrados (não introduzidos nesta sessão)
- [x] **`notificacoes.lida` é smallint** (confirmado no banco: `int2`), mas havia código
usando boolean → corrigido (2026-06-12). Locais: `POST /api/superadmin/notificacoes`
inseria `false` (server.js:4385 e 4393) e o marcar-como-lida usava `true/false`
(`POST /api/notificacoes/read-all` server.js:1584 e `.../:id/read` server.js:1601).
Todos trocados para `1/0`. Os demais INSERT (270, 2188) já usavam `0`.
- [x] ~~`POST /api/agendamentos` default `status='Agendado'` viola `agendamentos_status_check`~~
**NÃO procede mais.** A constraint atual é case-insensitive e aceita `agendado`
(`lower(status) IN ('importado','agendado','confirmado','reagendado','remarcar',
'cancelado','falta','atendido','pendente','concluido','faltou')`). O default do código
é `rest.status || 'agendado'` → válido. Há registros `agendado` em uso no banco.
- [x] ~~`PUT /api/agendamentos/:id` não passa pelo check de overlap~~ **NÃO procede.**
O PUT chama `findOverbookingConflict(...)` e retorna 409 em conflito (server.js:~2013).
---
## 4c. Integração Plugin RX ↔ servidor dental (2026-06-12) — MODELO GLOBAL
**Modelo correto (confirmado pelo usuário):** config é **GLOBAL, só do superadmin**
(não por clínica, não por usuário admin da conta). Hierarquia desejada no RX:
`scoreodonto / conta-mestre (consulttclinic@gmail.com) / clinicaID / paciente`.
Mapa: conta-mestre = `api_token` global · clinicaID = `images.clinic_name` ·
paciente = `images.patient_name`.
**Menu — feito antes:** `Sidebar.tsx` voltou a mostrar **PLUGINS** ao superadmin
(allowlist linha 67 estava sem `'plugins'`).
**FASE 0 — scoreodonto realinhado p/ GLOBAL (feito, deploy dev):**
- [x] `settings.rx_config_global` = `{ rx_url, api_token }`. **Sem login/senha, sem cache**:
o backend usa o `api_token` estático direto como Bearer (`_rxAdminFetch`). `api_tokens`
do RX são aceitos pelo `authenticateToken` (viram admin, **não expiram**).
- [x] `PUT /api/rx/config` agora é **`superadminGuard`** (só superadmin grava); token preservado
se vier vazio. `GET /api/rx/config` (authGuard) devolve `{rx_url, has_token, configured}` (sem token).
- [x] `POST /api/rx/devices` cria no RX com **`clinic_name = clinicaId`** (e-mail default único
por clínica+PC: `<clinicaId>.<pcSlug>@rx.scoreodonto.com`). `GET /api/rx/devices?clinicaId=`
filtra por clinic_name.
- [x] **Revertido** o que era do modelo errado: bypass do `tenantGuard` (removido) e seletor de
clínica do `RxConfigView` (removido).
- [x] `RxConfigView`: card **CONFIGURAÇÃO GERAL DO RX** (URL + token) editável **só superadmin**;
clínica vê status read-only + cadastra seus PCs. Superadmin vê PCs de **todas** as clínicas.
- [x] `backend.ts`: `getRxConfig()` (0 args, `{rx_url,has_token,configured}`), `putRxConfig({rx_url,api_token})`,
`createRxDevice` sem clinicName. `RXPlugin.tsx`: `getRxConfig()` sem arg.
**FASE 1 — servidor RX (dev.rx, `dental-server/routes/images.js`) (feito, restart dev):**
- [x] Filtro **`?clinic=<clinicaId>`** (= `clinic_name`) em `GET /api/images`, `/images/patients`
e `/images/by-patient`. Novo `GET /api/images/unique-clinics` (clinic_name + contagens).
- [x] Validado: `node --check` ok; `rx_dev_api` reiniciado (porta 3000); endpoints → 401 (auth ok).
⚠️ `npm run dev` = `node server.js` (SEM nodemon) → mudanças no RX exigem `docker restart rx_dev_api`.
**Validado geral:** tsc 0 erros; scoreodonto backend+frontend rebuildados no dev (bundle `govSqkSI`).
**FASE 2/3 — leitura por proxy (feito, deploy dev bundle `BVGGJsCu`):**
- [x] Backend: `GET /api/rx/patients?clinicaId=&search=` e `GET /api/rx/images?clinicaId=&paciente=`
→ proxy p/ RX `/images/patients` e `/images/by-patient` com `clinic=<clinicaId>` + api_token.
Isolamento `_rxClinicScopeOk`: superadmin vê tudo; demais só clínicas com vínculo (senão 403).
- [x] `backend.ts`: `getRxPatients`, `getRxPatientImages`. `RXPlugin` consome esses (lista de
pacientes + imagens do paciente). Thumbnails seguem públicos (`/api/images/:id/thumbnail`).
**PENDENTE:**
- [ ] **Falta o `api_token`** colado em CONFIGURAÇÃO GERAL DO RX (superadmin) → sem ele, criar PC
dá 400 e a leitura volta vazia. Gerar via `POST /api/auth/tokens` do RX (usuário `is_admin`).
- [ ] **Imagem em tamanho cheio** (`/images/:id/file-url`) ainda é autenticada → falta proxy
`GET /api/rx/image/:id/file` (galeria mostra thumbnail público; full-res é follow-up).
- [ ] **GTOs / upload / trigger-sync** do RXPlugin ainda usam `rxFetch` direto (token vazio) —
já estavam quebrados antes; migrar p/ proxy se forem usados.
- [ ] **Ativação do plugin** ainda é localStorage + toggle só superadmin (PluginsView) → rever
p/ ativação por clínica no backend.
- [ ] **Promover FASES 13 p/ prod** (rx.scoreodonto.com + scoreodonto prod) após validar no dev.
---
## 4d. Plugins: Locação de Salas + Marketplace de Profissionais (2026-06-12) — deploy dev
Dois plugins novos no padrão do RX (catálogo `PluginsView` + `pluginRegistry` localStorage
+ `pluginMenuItems` no Sidebar). Bundle `index-BylSJWKo.js`; `tsc` 0 erros; migrations OK.
**`locacao-salas`** (teal, view `/salas`, `views/plugins/SalasPlugin.tsx`):
- Tabelas `salas` + `sala_reservas`. CRUD do locador, marketplace (tipo/cidade/UF),
valores por hora/turno/diária/semanal/mensal.
- Reserva em transação com anti-conflito TRIPLO → 409: sala ocupada, reserva do próprio
solicitante em outra sala, e **agenda global** (`findOverbookingConflict` via
`dentistas.usuario_id`) — sala entra na invariante "1 lugar por vez".
- Fluxo: pendente → confirmar/recusar (locador) / cancelar (ambos). Notificações
(`notificarUsuario`) + `audit_log` (`entidade='sala'|'sala_reserva'`).
**`profissionais-marketplace`** (laranja, view `/marketplace-profissionais`,
`views/plugins/ProfissionaisPlugin.tsx`) — decisão: **Delivery (nível 8) virou "Profissionais"**:
- `usuarios` + `cep`, `raio_atuacao_km`, `bio_profissional` (raio é informativo, sem geodistância).
- `GET /api/profissionais/marketplace` inclui **biomédico** (o diretório antigo não incluía)
e filtra por CEP-prefixo. `GET/PUT /api/profissionais/perfil` (opt-in `disponivel_diretorio`).
- Tabela `propostas_profissional`: clínica→profissional (`POST /api/propostas` com `tenantGuard`,
anti-duplicata pendente), aceitar/recusar (profissional) / cancelar (clínica) + notificações.
- **Consolidado com o Diretório antigo** (bundle `DZJWcMDu`): com o plugin ativo, o item
"DIRETÓRIO PROF." some do menu e a rota `/diretorio-profissionais` renderiza o plugin;
com o plugin inativo, o `DiretorioProfissionaisView` antigo continua intacto (sem regressão).
`DiretorioProfissionaisView`/`/api/profissionais-disponiveis` podem ser removidos quando
o plugin virar padrão.
**Como testar:** superadmin → PLUGINS → ativar (lembrete: ativação é **localStorage por
navegador** — pendência 4c vale aqui também) → itens aparecem no menu dos papéis operacionais.
Propostas exigem workspace ativo (clínica/consultório); aba MEU PERFIL só para
dentista/biomedico/protetico.
**⚠️ LIMPEZA pós-teste (banco é de PRODUÇÃO):** dados de teste ficam em `salas`,
`sala_reservas`, `propostas_profissional`, colunas novas de `usuarios` e respingos em
`notificacoes`/`audit_log`. Limpar com:
```sql
DELETE FROM sala_reservas; DELETE FROM salas; DELETE FROM propostas_profissional;
-- e reverter perfis de teste:
-- UPDATE usuarios SET disponivel_diretorio=false, raio_atuacao_km=NULL, bio_profissional=NULL WHERE id='...';
```
**Fluxos institucionais de locação** (2026-06-13, bundle `CvP3Q2X_`):
- `sala_reservas` + `clinica_id`/`clinica_nome`/`profissional_usuario_id`/`profissional_nome`.
- **Clínica aluga sala**: ReservaModal ganhou "RESERVAR COMO: PESSOAL | <unidade ativa>";
como unidade, pode designar o profissional (`GET /api/dentistas?clinicaId=`, só os com
`usuario_id`). O **anti-conflito roda sobre o OCUPANTE** (designado ou solicitante) —
`COALESCE(profissional_usuario_id, solicitante_id)` nas reservas + agenda global.
Designado é notificado. Membros da unidade veem/cancelam as reservas dela
(`GET /api/sala-reservas/minhas?clinicaId=`, vínculo validado).
- **Consultório/clínica anuncia a própria sala**: checkbox "ANUNCIAR PELA UNIDADE: <nome>"
no cadastro/edição (grava `salas.clinica_id`; PUT aceita desvincular). Marketplace e
cards mostram "POR <unidade>" (join `clinicas`).
- **Dentista aluga sala**: fluxo pessoal original, inalterado.
**Alinhado com `/superadmin/financeiro`** (bundle `CXf4eiwZ`): a Tabela de Preços ganhou a
seção **PLUGINS (ADD-ONS)** — chaves `plugin:locacao-salas` e `plugin:profissionais-marketplace`
no mesmo `settings.pricing` (0 = incluído no plano). `GET /api/superadmin/pricing` agora mescla
defaults (chaves novas aparecem em configs antigas) e ganhou a linha **biomédico** que faltava.
Os cards da Área de Plugins exibem o preço configurado (chip "R$ X/MÊS" / "INCLUÍDO NO PLANO").
A COBRANÇA efetiva do add-on (bloquear plugin sem pagamento) segue o modelo manual atual de
pagamentos por usuário — não implementada.
**PENDENTE (roadmap acordado):**
- [ ] Cobrança efetiva dos add-ons (ativação por clínica no backend + verificação de pagamento —
junto com a pendência 4c de tirar a ativação do localStorage).
- [ ] Score Professional (motor de pontuação — alimentado por reservas/propostas/faltas).
- [ ] Geodistância real (CEP→lat/lng; raio passa a filtrar de verdade).
- [ ] Fotos das salas (upload via `/app/media`, como as fotos de orto).
- [ ] Pagamento da locação (MercadoPago já existe na tutoria — reaproveitar).
- [ ] Proposta aceita → criar `vinculos` automaticamente (hoje o aceite é só status+notificação).
---
## 5. Estado de versionamento
- **NADA foi commitado** nesta sessão. Tudo está no working tree, rodando contra o banco de prod.
- Regra do projeto: **commit/push só com autorização explícita** — o push dispara o pipeline de
produção via webhook do Gitea (pode sobrescrever trabalho em andamento).
- Arquivos que ESTA sessão editou: `backend/server.js`, `frontend/App.tsx`,
`frontend/components/Sidebar.tsx`, `frontend/services/backend.ts`, `frontend/views/AgendaView.tsx`,
`frontend/views/LoginView.tsx`, `frontend/views/LandingPage.tsx`.
(Demais arquivos modificados/untracked são WIP de sessões anteriores.)