Compare commits

...

3 Commits

Author SHA1 Message Date
VPS 4 Builder 471c2c2603 feat(ci): Passo 5 — Build Once / Promote / Deploy no build.yml
build-and-promote / build (push) Has been skipped
build-and-promote / promote (push) Successful in 1m31s
- build.yml dividido em 2 jobs por ref:
  • build (só push na main): builda 1x e publica por COMMIT (:<sha> + :latest), sem APP_VERSION.
  • promote (só tag vX.Y.Z): re-tagueia :<sha> -> :vX.Y.Z via 'docker buildx imagetools create'
    (SEM rebuild), valida integridade (imagem do commit existe; digest :versao == :commit;
    front+back do mesmo commit) e deploya na VPS1. Falha se o commit não foi pushado.
- Versão semântica nasce da TAG e é injetada no deploy em runtime (compose.prod).
- Removidos frontend/constants.ts e update-version.js (fim do APP_VERSION manual).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 12:34:33 +02:00
VPS 4 Builder 5049ce9b8b docs: unifica documentação em doc/ (fonte única, sem sobreposição)
build-and-push / build (push) Successful in 1m10s
build-and-push / deploy (push) Has been skipped
- 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>
2026-06-17 12:04:08 +02:00
VPS 4 Builder 0f50835114 docs: reconcilia deploy/versionamento com o estado real (pós v1.0.15)
- VERDADE-HOJE.md: reescrito p/ a verdade de 17/jun — banco DEV isolado (pgdev, não
  compartilha mais), produção roda imagem v1.0.15, versão em runtime via /api/version.
  Declarado documento principal (prevalece em divergência).
- deploy.md: §5 rollback  (existe); §6 corrigida — remove o "não há CI/registry/deploy"
  (era falso/contraditório); CI/CD por tag ATIVO, falta só o Passo 5.
- DEPLOY-PRODUCAO.md: status 🧪 dos Passos 1/3/4 (promovidos em v1.0.15).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 11:34:51 +02:00
18 changed files with 119 additions and 834 deletions
+65 -34
View File
@@ -1,24 +1,28 @@
name: build-and-push
name: build-and-promote
# Etapa 5: a cada push na main (ou tag vX.Y.Z), builda as imagens com o carimbo
# de versão e envia ao Container Registry do Gitea. NÃO faz deploy (Etapa 6 separada).
# Passo 5 — Build Once → Promote → Deploy:
# • push na main → job BUILD: builda 1x e publica por COMMIT (:<sha> + :latest). NÃO deploya.
# • tag vX.Y.Z → job PROMOTE: re-tagueia a imagem do commit como :vX.Y.Z (SEM rebuild),
# valida integridade e deploya na VPS1.
# A versão semântica NÃO entra no build: nasce da tag e é injetada no deploy em runtime
# (docker-compose.prod.yml). Não há mais APP_VERSION/constants.ts.
on:
push:
branches: [main]
tags: ['v*']
jobs:
# ───────────────────────── BUILD (só em push de branch) ─────────────────────────
build:
# Roda direto no host VPS4 (runner registrado com label de host + acesso ao Docker).
if: startsWith(github.ref, 'refs/heads/')
runs-on: self-hosted
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Calcular versão/commit/data
- name: Vars (commit + data)
id: vars
run: |
echo "version=$(grep -oE 'V[0-9]+\.[0-9]+\.[0-9]+' frontend/constants.ts | head -1 | tr 'V' 'v')" >> "$GITHUB_OUTPUT"
echo "commit=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT"
echo "build_time=$(date -u +'%Y-%m-%d %H:%M UTC')" >> "$GITHUB_OUTPUT"
@@ -27,65 +31,92 @@ jobs:
if [ -n "${{ secrets.REGISTRY_TOKEN }}" ]; then
echo "${{ secrets.REGISTRY_TOKEN }}" | docker login git.clube67.com -u ruicesar --password-stdin
else
echo "REGISTRY_TOKEN não definido — usando a credencial já presente no host (~/.docker/config.json)."
echo "REGISTRY_TOKEN ausente — usando a credencial já presente no host."
fi
- name: Build (compose, com carimbo de versão)
- name: Build (carimba commit/data; versão vem da tag no deploy, não aqui)
env:
# Fixa o nome do projeto p/ a imagem buildada bater com o passo de tag
# (em modo host o diretório do checkout muda o project name por padrão).
COMPOSE_PROJECT_NAME: scoreodontocom
APP_VERSION: ${{ steps.vars.outputs.version }}
GIT_COMMIT: ${{ steps.vars.outputs.commit }}
BUILD_TIME: ${{ steps.vars.outputs.build_time }}
APP_ENV: PROD
run: docker compose build scoreodonto-backend scoreodonto-frontend
- name: Tag + push (com retry p/ 499 transitório)
- name: Publicar por COMMIT (:<sha> + :latest), com retry p/ 499
run: |
set -e
V="${{ steps.vars.outputs.version }}"
REG=git.clube67.com/ruicesar
C="${{ steps.vars.outputs.commit }}"
for app in backend frontend; do
SRC="scoreodontocom-scoreodonto-${app}:latest"
DST="git.clube67.com/ruicesar/scoreodonto-${app}"
docker tag "$SRC" "$DST:$V"
DST="${REG}/scoreodonto-${app}"
docker tag "$SRC" "$DST:$C"
docker tag "$SRC" "$DST:latest"
for tag in "$V" "$C" "latest"; do
for tag in "$C" "latest"; do
n=0
until docker push "$DST:$tag"; do
n=$((n+1)); [ $n -ge 4 ] && { echo "push falhou após 4 tentativas: $DST:$tag"; exit 1; }
n=$((n+1)); [ $n -ge 4 ] && { echo "push falhou: $DST:$tag"; exit 1; }
echo "retry $n (499 transitório) em $DST:$tag"; sleep 3
done
done
done
echo "Publicado: scoreodonto-{backend,frontend}:$C (+ :latest). Push NÃO deploya."
- name: Resumo
run: |
echo "Imagens publicadas:"
echo " git.clube67.com/ruicesar/scoreodonto-backend:${{ steps.vars.outputs.version }}"
echo " git.clube67.com/ruicesar/scoreodonto-frontend:${{ steps.vars.outputs.version }}"
echo "Deploy só acontece em TAG (job deploy)."
# Deploy automático SÓ em tags vX.Y.Z (a tag = 'aprovado para produção').
# Push na main NÃO deploya — só builda. Etapa 6.
deploy:
needs: build
# ─────────────────────── PROMOTE (só em tag vX.Y.Z) ───────────────────────
promote:
if: startsWith(github.ref, 'refs/tags/v')
runs-on: self-hosted
steps:
- name: Checkout
- name: Checkout (na tag)
uses: actions/checkout@v4
- name: Promover a tag para produção (VPS1)
- name: Vars (tag + commit)
id: vars
run: |
echo "version=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT"
echo "commit=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT"
- name: Login no registry
run: |
if [ -n "${{ secrets.REGISTRY_TOKEN }}" ]; then
echo "${{ secrets.REGISTRY_TOKEN }}" | docker login git.clube67.com -u ruicesar --password-stdin
fi
- name: Integridade — a imagem do commit existe? (nunca buildar na tag)
run: |
set -e
TAG="${GITHUB_REF_NAME}"
echo "Promovendo $TAG para a produção (VPS1)..."
# Garante que o mecanismo de deploy está atualizado na VPS1
REG=git.clube67.com/ruicesar
C="${{ steps.vars.outputs.commit }}"
for app in backend frontend; do
if ! docker buildx imagetools inspect "${REG}/scoreodonto-${app}:$C" >/dev/null 2>&1; then
echo "ERRO: ${REG}/scoreodonto-${app}:$C não existe no registry."
echo " Faça 'git push origin main' (que builda o commit $C) ANTES de criar a tag."
exit 1
fi
done
echo "OK: backend:$C e frontend:$C presentes — mesmo commit $C (front+back atômicos)."
- name: Promover — re-tag commit → versão (SEM rebuild) + conferir digest
run: |
set -e
REG=git.clube67.com/ruicesar
C="${{ steps.vars.outputs.commit }}"; V="${{ steps.vars.outputs.version }}"
for app in backend frontend; do
DST="${REG}/scoreodonto-${app}"
docker buildx imagetools create -t "$DST:$V" "$DST:$C"
dC=$(docker buildx imagetools inspect "$DST:$C" --format '{{.Manifest.Digest}}')
dV=$(docker buildx imagetools inspect "$DST:$V" --format '{{.Manifest.Digest}}')
[ "$dC" = "$dV" ] || { echo "ERRO: digest divergiu em $app ($V=$dV != $C=$dC)"; exit 1; }
echo "$app: $V -> $C (digest $dV)"
done
echo "Promovido sem rebuild: a imagem :$V E a imagem :$C (mesmo artefato)."
- name: Deploy na VPS1 (pull :versao + up --no-build; versao injetada em runtime)
run: |
set -e
V="${{ steps.vars.outputs.version }}"
scp -o StrictHostKeyChecking=accept-new \
docker-compose.prod.yml deploy-prod.sh \
deploy@10.99.0.1:/home/deploy/stack/scoreodonto.com/
ssh -o StrictHostKeyChecking=accept-new deploy@10.99.0.1 \
"/home/deploy/stack/scoreodonto.com/deploy-prod.sh $TAG"
"/home/deploy/stack/scoreodonto.com/deploy-prod.sh $V"
@@ -8,8 +8,8 @@
> - 🧪 **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.14 · commit 081985c · 2026-06-16` (`curl https://scoreodonto.com/api/version`).
> Todo o trabalho de versionamento abaixo marcado 🧪 está no working tree da VPS4 (não commitado).
> **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`).
---
@@ -26,7 +26,7 @@
▼ 🎯 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)
APP_VERSION=tag em runtime (Passo 3, em prod)
```
---
@@ -41,11 +41,11 @@
| 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`) | ✅ | Em prod, valores vêm do **build** da imagem |
| **Versão exibida em Configurações → "Sobre o sistema"** | 🧪 | Passo 1. Em **produção a versão está no Sidebar** |
| **Front/telas consomem `/api/version` (`useAppVersion`); app não usa `APP_VERSION`** | 🧪 | Passos 1/4. Em **prod o front usa `APP_VERSION` do bundle** |
| **Deploy injeta `APP_VERSION`/`APP_ENV` em runtime (`environment`)** | 🧪 | Passo 3 (`compose.prod`/`deploy-prod.sh` no working tree) |
| **Tag promove sem rebuild + validação de integridade** | 🎯 | Passo 5 (§7) |
| `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 |
---
@@ -65,12 +65,12 @@
## 4. De onde vem a versão (contrato `/api/version`)
```json
{ "name": "ScoreOdonto API", "version": "v1.0.14", "commit": "081985c",
"build": "2026-06-16 08:06 UTC", "environment": "PROD" }
{ "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** **hoje (✅)** também vêm do build da imagem (build arg no CI). **🧪 (Passo 3):** passam a ser injetados no **deploy** em runtime (a partir da tag), liberando a mesma imagem para qualquer tag sem rebuild.
- **Frontend** **🧪 (Passos 1/4):** lê tudo de `/api/version` via `useAppVersion()`; deixa de embutir versão. **Em produção** ainda embute `APP_VERSION` no bundle.
- **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.
---
+42
View File
@@ -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).
@@ -1,60 +0,0 @@
# Checklist de Deploy para Produção — Pacote Financeiro + Salas (jun/2026)
> Consolida a rodada: **Financeiro V1 (Épicos 14)**, **V2 Comissão/Repasse**, **Glosas**, **GTO→Financeiro**, **modelo de contrato p/ convênio** e o **redesenho de Salas** (perfil Dono de Sala, sala como workspace, financeiro de locação, painel/relatório/agenda).
> O **procedimento canônico de deploy** é o `DEPLOY-PRODUCAO.md` — este arquivo é só o checklist desta entrega.
## 0. Arquitetura (sem ambiente isolado)
- **VPS4** = **desenvolvimento + builder**. O ambiente de teste é **`dev.scoreodonto.com`** (servido pela VPS4 via Traefik da VPS1) e é onde se **buildam** as imagens que vão pra produção.
- **VPS1** = **produção** (`scoreodonto.com`) — só recebe a imagem pronta e sobe.
- **VPS3** = **dados** (Postgres `scoreodonto`, Dragonfly, Gitea). **Dev e prod apontam para o MESMO banco `scoreodonto`.**
- ⚠️ Consequência (confirmada): **dev e prod compartilham o banco** → as migrações desta entrega **já foram aplicadas** ao banco real durante o desenvolvimento (rodam no boot do backend de dev). Promover é, na prática, **levar o CÓDIGO** para a VPS1 — o schema já está migrado. **Não há ambiente nem banco isolado** entre dev e produção.
## 1. Fluxo de promoção (resumo do DEPLOY-PRODUCAO.md §2)
1. `git push origin main` (na VPS4) — já feito (commit `3e64514`).
2. Bump de versão (`update-version.js``frontend/constants.ts`).
3. **Build da imagem Docker na VPS4** (`npm install` + `vite build` dentro do Dockerfile) — peso fora da produção.
4. **Envia a imagem pronta para a VPS1** (registry privado pela WireGuard — recomendado — ou `docker save | ssh | docker load`).
5. **VPS1:** `docker compose up -d` (troca o container pela imagem nova). Backend leva ~1030s no boot (reconecta Postgres/Dragonfly + roda migrações).
- *(Os scripts `deploy-prod-builder.sh` e `deploy-to-prod.sh` estão **DEPRECADOS** — ver seção "Fluxo ANTIGO" do `DEPLOY-PRODUCAO.md`. Não usar.)*
## 2. Pré-requisitos
- [ ] **Backup do banco** VPS3 (`pg_dump scoreodonto` — custom format `-Fc`). Como dev/prod compartilham o banco, esse backup é a **única rede de segurança**.
- [ ] Código em `origin/main` (Gitea) — ✅ `3e64514`.
- [ ] `node --check backend/server.js` OK; build do frontend OK.
- [ ] Validado em `dev.scoreodonto.com`.
## 3. Mudanças de banco (migrações idempotentes no boot — todas ADITIVAS)
> Já aplicadas no banco (dev=prod). Listadas para auditoria/rollback. Nenhuma destrutiva.
- **Novas tabelas:** `procedimentos_realizados`, `procedimento_realizado_foto`, `comissao_regras`, `repasses`, `glosas`.
- **ALTER `financeiro`:** `paciente_id, profissional_id, procedimento_id, agendamento_id, contrato_id, realizado_id, origem, sem_comissao, pago_em, deleted_at, fornecedor, centro_custo, recorrente, convenio_id, valor_glosa, data_competencia, competencia`.
- **ALTER `orcamentos`:** `financeiro_gerado`. **ALTER `contratos`:** `cobranca_automatica, valor_recorrente, periodicidade, dia_cobranca`. **ALTER `gto_builders`:** `financeiro_id`. **ALTER `sala_reservas`:** `financeiro_id`.
- **CHECK `usuarios_role_check` / `vinculos_role_check`** ganham `donosala` (DROP+ADD no boot — superset, seguro).
- **Índices:** troca `uq_comissao_regra``uq_comissao_regra2`; novos `idx_financeiro_contrato_comp`, `idx_financeiro_convenio`, `idx_glosas_*`, `idx_gto_builders_financeiro`.
- **Seed (boot):** `cmod_odo_14` "Atendimento a Convênios / Repasse" (ON CONFLICT DO NOTHING).
## 4. Backend — env / infra (VPS1)
- [ ] `DATABASE_URL``…@10.99.0.3/scoreodonto`. `JWT_SECRET` estável (login + URLs assinadas de fotos/comprovantes).
- [ ] **Volume de mídia** persistente (`MEDIA_PATH`): grava em `…/procedimentos/<id>/` e `…/repasses/<id>/`.
- [ ] **Cron** `processarVigenciasContratos` (boot + 6h): Atrasado, vigência, **renovação automática**, **cobrança recorrente** — conferir nos logs.
## 5. Frontend
- [ ] Imagem rebuildada com as views novas (`ComissoesView`, `GlosasView`, `ProcedimentosProntuario`, `SalaHomeView`) e alterações (`FinanceiroView`, `ReportsView`, `OrcamentoModal`, `ContratosView`, `LancarGTO`, `SalasPlugin`, `OnboardingChat`, `Sidebar`, `App.tsx`, `PluginsView`, `pluginRegistry`, `services/backend.ts`).
- [ ] Conferir: locação **sempre ativa** (MINHAS/ALUGAR SALAS aparecem) e **seletor de workspace** lista Clínicas × Salas.
## 6. Verificação pós-deploy (VPS1)
```bash
# versão no ar
curl -s https://scoreodonto.com | grep -o 'V[0-9]*\.[0-9]*\.[0-9]*' | head -1
# logs do backend (Docker ROOTLESS na VPS1) — espere "[MIGRATION] all migrations OK"
ssh deploy@10.99.0.1 "DOCKER_HOST=unix:///run/user/1000/docker.sock docker logs scoreodontocom-scoreodonto-backend-1 --tail 40"
```
Smoke tests: criar **lançamento financeiro** (valida `normalizeFinanceiro` — antes nenhum salvava pela UI); **GTO → recebível**; **comissão/repasse/comprovante**; **glosa**; **contrato de convênio**; **orçamento→parcelas**; **fluxo de salas** (Dono de Sala, reserva→recebível, painel); **isolamento cross-tenant 403**.
## 7. Rollback
- Migrações **só adicionam** → reverter o **código** (imagem anterior na VPS1) é seguro; colunas/tabelas extras ficam inertes. Restaurar o dump da §2 só se necessário (`pg_restore -d "<url>" --clean <arquivo>`).
## 8. Riscos específicos desta entrega
- **`normalizeFinanceiro` (crítico):** corrige o bug em que `enforceUppercase` mandava `PAGO`/`PIX` violando o CHECK capitalizado — antes a tabela `financeiro` ficava vazia. Garantir que entrou na imagem.
- **Banco único dev/prod:** dado de teste de desenvolvimento pode aparecer; os E2E desta rodada sempre limparam, mas conferir contagens de `financeiro`, `salas`, `repasses`, `glosas`.
- **Contratos de convênio:** textos seguem padrão de mercado — recomendar revisão por advogado.
- **Locação sempre ativa:** todos os papéis (menos superadmin) passam a ver MINHAS/ALUGAR SALAS.
-76
View File
@@ -1,76 +0,0 @@
# Pendências & Tarefas a Fazer — ScoreOdonto
> Backlog operacional/infra após as 6 etapas de CI/CD (16/jun/2026). Ordenado por prioridade.
> Contexto da infra em `arquitetura.md`; fluxo de deploy em `deploy.md`; verdade atual em `VERDADE-HOJE.md`.
---
## 🔴 P0 — Estrutural
### 1. Isolar os bancos DEV (dev parar de usar o banco de produção)
> **Estratégia decidida + plano completo em `BANCO-DEV-ESTRATEGIA.md`**: um único container `postgis/postgis` na VPS4 (rootless) com `*_ref` (template da VPS3) + `*_dev`. Resolve scoreodonto, RX e subdomínio (e futuros) numa instância só. O texto abaixo é o contexto original.
**Problema:** dev (VPS4) e produção (VPS1) apontam para o **mesmo** banco na VPS3 (scoreodonto, dental_images, subdominio). Qualquer teste em dev escreve no dado real. É a maior dívida estrutural restante.
**Bloqueador descoberto (16/jun/2026):** o role `scoreodonto_user` **não pode criar banco** (`CREATEDB=false`, `SUPER=false`) e **não há shell na VPS3** (`deploy@10.99.0.3` recusa a chave). → Precisa de **ação do operador com acesso superuser** ao Postgres da VPS3 (usuário `postgres`), OU conceder a permissão: `ALTER ROLE scoreodonto_user CREATEDB;`.
**Passos:**
1. **(operador/superuser na VPS3)** Criar o banco:
`CREATE DATABASE scoreodonto_dev OWNER scoreodonto_user;`
2. **Popular com cópia da produção** (dados realistas p/ dev):
- Já existe dump recente em `/home/deploy/backups/scoreodonto_*.dump` (VPS4, formato `-Fc`).
- Restaurar: `pg_restore -d "postgresql://scoreodonto_user:<senha>@10.99.0.3:5432/scoreodonto_dev" --no-owner /caminho/scoreodonto_*.dump`
(ou via container: `docker run --rm -v <dump>:/b.dump -e PGPASSWORD=… postgres:18 pg_restore -h 10.99.0.3 -U scoreodonto_user -d scoreodonto_dev --no-owner /b.dump`).
3. **Apontar SÓ o dev para `scoreodonto_dev`** (sem tocar na prod):
- No `docker-compose.yml`, parametrizar: `DATABASE_URL=…@10.99.0.3:5432/${SCOREODONTO_DB:-scoreodonto}?schema=public` (default mantém a prod intacta).
- Na VPS4, criar `.env` (gitignored, dev-only) com `SCOREODONTO_DB=scoreodonto_dev`.
- Rebuild + recreate do backend de dev.
4. **Verificar:** `dev.scoreodonto.com/api/version` sobe; conferir que escrita em dev cai em `scoreodonto_dev` e **não** em `scoreodonto` (contagens divergem após um teste).
5. **(privacidade)** Avaliar anonimizar dados sensíveis de pacientes no `scoreodonto_dev`.
**Ganho:** dev deixa de poder corromper o dado de produção; fim da “única rede de segurança é o backup”.
---
## 🟠 P1 — Segurança
### 2. Rotacionar o token do Gitea exposto no chat
O token usado nos `docker login` (VPS4 e VPS1) e na descoberta foi colado no chat → comprometido. **Revogar no Gitea** (Configurações → Aplicações) e gerar um novo. Os logins já estão no keystore do Docker; só re-logar quando o token for trocado.
### 3. Criar o secret `REGISTRY_TOKEN` no repo
Adicionar em `git.clube67.com` → repo → Settings → Actions → Secrets um token (escopo `write:package`/`read:package`). Hoje o workflow funciona com a credencial do host como fallback; o secret deixa o CI independente disso.
### 4. Tirar credenciais em texto puro do `docker-compose.yml`
O `DATABASE_URL` e `DRAGONFLY_URL` têm **senha em texto puro** versionada no repo. Mover para `.env`/secrets (e idealmente rotacionar as senhas). Dívida de segurança pré-existente.
---
## 🟡 P2 — Operacional / Cosmético
### 5. Retenção/limpeza de imagens no registry
O registry vai acumular imagens (`:vX.Y.Z`, `:<commit>`, `:latest`) a cada deploy. Definir política de retenção (ex.: manter N últimas + releases) — a VPS3 tem disco finito. Avaliar limpeza automática.
### 6. Limpar a árvore git suja da VPS1
O repo em `/home/deploy/stack/scoreodonto.com` na VPS1 está **sujo** (vários `.md` e `.dockerignore` deletados, HEAD antigo `19dbbae`). Não afeta o deploy (que usa imagem), mas convém reconciliar/limpar para não confundir.
### 7. Ruído de attestation no registry
O BuildKit publica manifests de *provenance/attestation* que aparecem como “tags” `sha256:…` no registry. Inócuo; desligar com `provenance=false`/`--provenance=false` no build se incomodar.
### 8. Padronizar o case da versão (V vs v)
A CI mostra `v1.0.14` (minúsculo, por `tr 'V' 'v'`); builds manuais mostram `V1.0.14`. Cosmético — alinhar para um padrão único.
### 9. Backup automatizado do banco
Hoje o `pg_dump` é manual (via container `postgres:18`). Agendar backup periódico do `scoreodonto` (e, após P0, do `scoreodonto_dev`) com retenção. Evolução: WAL archiving / PITR.
---
## 🔵 Futuro (visão de produto — fora do escopo de infra)
Itens herdados do mapa de produto, ainda não implementados:
- **Score Professional** (motor de pontuação) — não existe.
- **Estoque**, **Teleatendimento**.
- Entidade formal **Pessoa × Empresa**.
- Monitoramento/observabilidade (Grafana já existe na stack — integrar métricas do ScoreOdonto).
---
_Atualizar este arquivo conforme as pendências forem resolvidas. As 6 etapas de CI/CD (documentação, versão visível, registry, pipeline, deploy por tag) estão **concluídas** — ver `deploy.md`._
-344
View File
@@ -1,344 +0,0 @@
# 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.)
-45
View File
@@ -1,45 +0,0 @@
# Qual é a verdade hoje? — Auditoria de infra ScoreOdonto
> Resposta direta às 6 perguntas essenciais, **verificada em 16/jun/2026**. Sem teoria, sem fantasma.
> Detalhes em `arquitetura.md` (real), `deploy.md` (alvo) e `DEPLOY-PRODUCAO.md` (runbook).
---
### 1. Onde está o código?
- **Fonte da verdade:** Gitea na **VPS3**, repo `ruicesar/scoreodonto.com` (`ssh://git@10.99.0.3/ruicesar/scoreodonto.com.git`).
- **Cópia de trabalho (dev):** VPS4 em `/home/deploy/stack/scoreodonto.com` (onde se programa).
- **Cópia de produção:** VPS1 em `/home/deploy/stack/scoreodonto.com` (HEAD verificado: `19dbbae`).
- **Documentação:** centralizada em `scoreodonto.com/documentação/` (este diretório).
### 2. Onde está o banco?
- **PostgreSQL** no host da **VPS3** (`10.99.0.3:5432`), banco `scoreodonto`, usuário `scoreodonto_user`.
- ⚠️ **DEV e PROD usam o MESMO banco.** Não há isolamento. Backup (`pg_dump -Fc`) é a única rede de segurança.
- Cache **DragonflyDB** (`10.99.0.3:6379`, índice `/1`). Mensageria **NATS** (`:4222`) e **Temporal** (`:7233`).
### 3. Onde estão os containers?
- **Produção (VPS1, docker rootless `unix:///run/user/1000/docker.sock`):**
- `scoreodontocom-scoreodonto-frontend-1` — roda **imagem versionada do registry** (`git.clube67.com/ruicesar/scoreodonto-frontend:<tag>`), promovida por deploy-por-tag. Produção **não builda** mais.
- `scoreodontocom-scoreodonto-backend-1` — idem (`:8018`).
- `traefik-traefik-1` — borda TLS (Up ~13 dias). Também: rx, newwhats, subdominio, mercado.
- **Dev (VPS4, docker root `/var/run/docker.sock`):**
- `scoreodontocom-nginx-1` (`0.0.0.0:8020->80`, alvo do Traefik para `dev.`), `scoreodonto-frontend-1` (`:3013`), `scoreodonto-backend-1` (`:8018`).
- **Roteamento:** Traefik na VPS1 → `scoreodonto.com` (containers locais) e `dev.scoreodonto.com``http://10.99.0.4:8020`.
### 4. Quem faz deploy?
- **CI/CD por tag (Gitea Actions).** `git tag vX.Y.Z && git push origin vX.Y.Z` → o runner `vps4-builder` builda as imagens, publica no registry e o job `deploy` promove para a VPS1 (`deploy-prod.sh`). **Push na main NÃO deploya — só builda.**
- **Produção não builda mais:** consome a **imagem versionada** do registry (`docker compose -f docker-compose.yml -f docker-compose.prod.yml pull && up -d --no-build`).
- Deploy manual também disponível: na VPS1, `./deploy-prod.sh <tag>`. Ver `deploy.md` e `REGISTRY.md`.
### 5. Como versiona?
- `APP_VERSION` em `frontend/constants.ts` (dev hoje **`V1.0.13`**), incrementado por `update-version.js` (patch++).
- ⚠️ **Bump é manual** — nada chama `update-version.js` no build (`npm run build` = só `vite build`). Por isso a versão fica parada se ninguém rodar o script.
-**Versão visível** (Etapa 3): rodapé do Sidebar + `GET /api/version` + log de boot `[VERSION] …`. Carimbo (versão/commit/data/ambiente) injetado no build. **No ar em DEV e em PRODUÇÃO** (prod hoje: `v1.0.14 · 081985c · PROD`).
### 6. Como volta uma versão (rollback)?
- **Por imagem, sem rebuild:** na VPS1, `./deploy-prod.sh <tag-anterior>` (ex.: `./deploy-prod.sh v1.0.13`) — puxa a imagem da tag anterior do registry e sobe. Minutos, sem recompilar.
- Seguro porque as migrações são **aditivas** (voltar código não quebra o schema). Restaurar o dump (`/home/deploy/backups/`) só em caso extremo.
---
## Veredito de uma linha
> O código está salvo no Gitea (VPS3) e roda em containers na VPS1 (prod) e VPS4 (dev), ambos no **mesmo banco** da VPS3. **CI/CD por tag ativo**: `git tag vX.Y.Z` → CI builda imagem → deploy automático na VPS1, com **versão visível** no rodapé/`/api/version` e **rollback por imagem** (`deploy-prod.sh <tag-anterior>`). Pendência estrutural restante: **isolar um banco `scoreodonto_dev`** (dev e prod ainda compartilham o banco).
-131
View File
@@ -1,131 +0,0 @@
# Arquitetura ScoreOdonto — Estado REAL
> Documento de **estado atual verificado** (16/jun/2026). Descreve apenas o que existe e está rodando hoje.
> Sem referências históricas, sem staging, sem listeners desativados. Para o fluxo-alvo de deploy ver `deploy.md`; para o passo a passo operacional ver `DEPLOY-PRODUCAO.md`.
---
## 1. Visão geral
O ScoreOdonto roda sobre **3 VPS** ligadas por uma rede privada **WireGuard** (`10.99.0.0/24`):
| VPS | IP interno | Papel |
|------|-------------|-------------------------------|
| VPS1 | 10.99.0.1 | **Produção** (borda + apps) |
| VPS3 | 10.99.0.3 | **Dados & Controle** (Postgres, Gitea) |
| VPS4 | 10.99.0.4 | **Desenvolvimento & Builder** |
```
Internet (HTTPS)
┌──────────────────────────── VPS1 (10.99.0.1) ─────────────────────────────┐
│ Traefik (TLS Let's Encrypt, entrypoint websecure) │
│ • scoreodonto.com / www ─────► containers de PRODUÇÃO (local) │
│ • dev.scoreodonto.com ─────► http://10.99.0.4:8020 (via WireGuard) │
│ │
│ Produção (imagens buildadas): │
│ scoreodontocom-scoreodonto-frontend-1 (SPA em nginx) │
│ scoreodontocom-scoreodonto-backend-1 (Node :8018) │
│ (também hospeda: rx, newwhats, subdominio, mercado) │
└────────────────────────────────┬──────────────────────────────────────────┘
│ WireGuard
┌───────────────────────┼───────────────────────┐
▼ ▼
┌──── VPS3 (10.99.0.3) ────┐ ┌──── VPS4 (10.99.0.4) ──────────┐
│ PostgreSQL :5432 │◄────────────│ Desenvolvimento + Builder │
│ db: scoreodonto │ (dev E │ nginx (borda dev) :8020→80 │
│ Dragonfly :6379 (/1) │ prod │ └─► frontend :3013 │
│ NATS :4222 │ usam o │ └─► backend (/api):8018 │
│ Temporal :7233 │ MESMO │ Código-fonte em: │
│ Gitea :3000 │ banco) │ /home/deploy/stack/ │
│ repo: ruicesar/ │ │ scoreodonto.com/ │
│ scoreodonto.com │ │ Docker: socket ROOT │
│ Registry: NÃO existe │ │ (/var/run/docker.sock) │
│ (porta 5000 fechada) │ │ │
└───────────────────────────┘ └─────────────────────────────────┘
```
---
## 2. VPS1 — Produção (10.99.0.1)
- **Traefik** (container `traefik-traefik-1`) é a borda: termina TLS (Let's Encrypt, resolver `le`, entrypoint `websecure`) e roteia por `Host`.
- `scoreodonto.com` e `www.scoreodonto.com` → containers de produção locais.
- `dev.scoreodonto.com``http://10.99.0.4:8020` (encaminha para a VPS4 pela WireGuard). Config dinâmica em `traefik/dynamic/dev.scoreodonto.yml`.
- **Containers de produção** (imagens já buildadas, project `scoreodontocom`):
- `scoreodontocom-scoreodonto-frontend-1` — SPA React servido por nginx.
- `scoreodontocom-scoreodonto-backend-1` — API Node/Express na porta `8018`.
- **Docker rootless**: o daemon roda no socket do usuário `deploy` (`unix:///run/user/1000/docker.sock`). Comandos `docker` na VPS1 precisam dessa variável `DOCKER_HOST`.
- Produção **não builda** o ScoreOdonto: ela sobe imagens prontas. (O `docker-compose.yml` tem `build:`, mas o fluxo correto é trazer a imagem pronta da VPS4 — ver `deploy.md`.)
## 3. VPS3 — Dados & Controle (10.99.0.3)
Serviços instalados **direto no host** (não em container):
- **PostgreSQL** `:5432` — banco `scoreodonto`, usuário `scoreodonto_user`. **Fonte de dados única.**
- **DragonflyDB** `:6379` — cache (índice `/1` reservado ao ScoreOdonto).
- **NATS** `:4222` e **Temporal** `:7233` — mensageria / workflows.
- **Gitea** `:3000`**fonte oficial da verdade do código**. Repo `ruicesar/scoreodonto.com`, acesso por `ssh://git@10.99.0.3/ruicesar/scoreodonto.com.git`. É o motor de CI/CD (ver `deploy.md`).
- **Registry de imagens:** o **Container Registry embutido do Gitea** está ativo e em uso, acessível via HTTPS em `git.clube67.com` (namespace `ruicesar/`). Não há `registry:2` separado. Ver `REGISTRY.md`.
## 4. VPS4 — Desenvolvimento & Builder (10.99.0.4)
- **Esta é a máquina de trabalho.** Código-fonte em `/home/deploy/stack/scoreodonto.com/`.
- **Docker é ROOT** aqui (socket `/var/run/docker.sock`) — diferente da VPS1.
- Containers em execução (project `scoreodontocom`):
- `scoreodontocom-nginx-1` — borda do dev, publica `0.0.0.0:8020->80`. É o alvo do Traefik para `dev.scoreodonto.com`.
- `scoreodontocom-scoreodonto-frontend-1` — SPA (porta interna `3013`).
- `scoreodontocom-scoreodonto-backend-1` — API Node (porta interna `8018`).
- **O dev NÃO é Vite HMR.** O container de frontend roda uma **imagem buildada** (`vite build``nginx:alpine`); editar `frontend/` ou `backend/` exige **rebuild + recreate** do container para refletir em `dev.scoreodonto.com`. (Existe um override `docker-compose.dev.yml` com Vite/HMR, mas não é o que está no ar.)
- O `nginx/default.conf` da VPS4 faz: `/api/` e `/socket.io/``scoreodonto-backend:8018`; `/``scoreodonto-frontend:3013`.
### Caminho de uma requisição
- **Produção:** navegador → `https://scoreodonto.com` → Traefik (VPS1, TLS) → frontend/backend de produção (VPS1) → Postgres (VPS3).
- **Dev:** navegador → `https://dev.scoreodonto.com` → Traefik (VPS1, TLS) → `10.99.0.4:8020` (nginx VPS4) → frontend/backend de dev (VPS4) → **mesmo Postgres** (VPS3).
---
## 5. ⚠️ Banco compartilhado entre DEV e PROD
**Dev e produção apontam para o MESMO banco `scoreodonto` na VPS3.** O backend de dev usa `DATABASE_URL=postgresql://scoreodonto_user:<senha>@10.99.0.3:5432/scoreodonto`.
Consequências práticas:
- Migrações rodadas no boot do backend de dev **já alteram o banco real**. As migrações do projeto são **idempotentes e aditivas** (só criam tabelas/colunas), o que torna isso tolerável — mas **não há isolamento**.
- A **única rede de segurança** é o backup (`pg_dump scoreodonto -Fc`) antes de mudanças sensíveis.
- Isolar um banco `scoreodonto_dev` é o conserto estrutural recomendado para o futuro (fora do escopo deste documento).
---
## 6. Stack técnica e portas
| Componente | Tecnologia | Porta interna |
|------------|-----------|----------------|
| Frontend | React + Vite + TypeScript + Tailwind (build estático servido por nginx) | 3013 |
| Backend | Node.js + Express + driver `pg` nativo (**sem ORM**) | 8018 |
| Borda dev (VPS4) | nginx:alpine | 8020 → 80 |
| Banco | PostgreSQL 18 | 5432 |
| Cache | DragonflyDB (índice `/1`) | 6379 |
| Mensageria | NATS / Temporal | 4222 / 7233 |
| Git/CI | Gitea | 3000 |
| Borda prod | Traefik (TLS Let's Encrypt) | 443 (websecure) |
**Imagens (Dockerfiles):**
- Frontend: multi-stage — `node:20-alpine` (build `npm run build`) → `nginx:alpine` (serve `dist/`).
- Backend: `node:20-slim`, `npm install --omit=dev`, `CMD node server.js`.
---
## 7. Versionamento (estado atual e limitação)
- A versão vive em `frontend/constants.ts` (`APP_VERSION`, hoje `V1.0.12`) e é incrementada (patch) por `update-version.js` antes do build.
- **Limitação crítica:** a versão **não aparece** no HTML servido — fica embutida no bundle `index-<hash>.js`. Hoje só dá para saber a versão no ar inspecionando o container. É por isso que não se consegue confirmar "subiu mesmo?" pelo navegador. **Tornar a versão visível** (rodapé + endpoint + log de boot) é uma melhoria planejada (ver lista de afazeres / `deploy.md`).
---
## 8. O que NÃO existe (para encerrar fantasmas)
-**Não existe staging.** Não há `staging.scoreodonto.com`, nem stack `*-staging`, nem banco `scoreodonto_staging`. O único ambiente de teste é `dev.scoreodonto.com` (VPS4), com o banco de produção.
-**Não existe listener de deploy ativo** para o scoreodonto.com. O antigo `listener-scoreodonto.js` (porta 9002) foi deletado e o serviço está desligado; não há webhook do scoreodonto.com registrado no Gitea. Hoje, `git push` **não dispara** deploy automático. (O fluxo-alvo para reativar isso está em `deploy.md`.)
-**Registry:** existe e está em uso — é o Container Registry embutido do Gitea (`git.clube67.com`), não um serviço separado. Ver `REGISTRY.md`.
-103
View File
@@ -1,103 +0,0 @@
# Deploy ScoreOdonto — Como um commit vira produção (estado-ALVO)
> Este documento responde **uma** pergunta: **"como um commit vira produção?"**
> Descreve o **fluxo-alvo** com **Gitea como motor de CI/CD**. O que já existe hoje está marcado como ✅; o que ainda será construído está marcado como 🔜.
> Para a realidade da infra ver `arquitetura.md`. Para o procedimento manual executável **hoje** ver `DEPLOY-PRODUCAO.md`.
---
## 1. Princípios (inegociáveis)
1. **Gitea (VPS3) é a fonte única da verdade** e o motor de CI/CD.
2. **Produção (VPS1) nunca builda e nunca recebe código-fonte** — só consome **imagens versionadas** prontas.
3. **Build acontece fora da produção** (na CI / na VPS4).
4. **Deploy é um ato deliberado por TAG**, nunca automático em todo push (a `main` é usada para desenvolvimento e o banco é compartilhado — promover sem querer é perigoso).
5. **Rollback é trocar a imagem** por uma tag anterior — sem rebuild, sem recompilar, sem copiar arquivo.
6. **Toda imagem é rastreável**: carrega versão semântica + commit + data de build.
---
## 2. O fluxo-alvo, em uma frase
> **Push** abastece o desenvolvimento e (🔜) **produz uma imagem versionada no registry**;
> **criar uma tag `vX.Y.Z`** é o que **promove** essa imagem para produção.
```
git push ──► Gitea (VPS3) ──► CI: build da imagem ──► Registry privado (VPS3)
(na main) (versão+commit+data) imagem :vX.Y.Z
criar tag vX.Y.Z ────┘
VPS1: docker compose pull + up -d
(troca o container pela imagem da tag)
Produção rodando vX.Y.Z
```
---
## 3. As duas metades do fluxo
### Metade A — Integração contínua (push → imagem) ✅ FEITO
1. Desenvolvedor trabalha na VPS4 e valida em `https://dev.scoreodonto.com`.
2. `git push origin main` (ou tag `v*`) para o Gitea (VPS3).
3. A **CI do Gitea** (Gitea Actions) builda backend+frontend via `docker compose build`, injetando **versão, commit curto e data de build** (build args → `VITE_*`/`process.env`).
4. As imagens são enviadas ao **Container Registry do Gitea** com tags `:<versão>`, `:<commit-curto>` e `:latest`.
5. **Nada vai para produção aqui.** Esta metade só produz e guarda o artefato.
> **Implementado em 16/jun/2026.** Workflow: `.gitea/workflows/build.yml`. Runner: `vps4-builder` (act_runner 0.6.1, **modo host** com Docker, serviço systemd `act_runner` na VPS4, label `self-hosted`). Login no registry usa o secret `REGISTRY_TOKEN` se presente, senão a credencial do host. Validado: push na main → build → `scoreodonto-{backend,frontend}:v1.0.13`/`:<commit>`/`:latest` publicados.
### Metade B — Entrega por tag (tag → produção) ✅ FEITO
1. Quando uma versão está aprovada, cria-se uma **tag semântica**: `git tag v1.0.15 && git push origin v1.0.15`.
2. A CI builda e publica a imagem `:v1.0.15` no registry **e** o job `deploy` (só em tags `v*`) promove para a VPS1.
3. Na VPS1, o `deploy-prod.sh <tag>` faz `docker compose pull` + `up -d --no-build` apontando para a imagem da tag.
4. A versão visível (rodapé/endpoint `/api/version`) confirma a tag no ar com `environment: PROD`.
> **Implementado em 16/jun/2026.** `docker-compose.prod.yml` (imagens do registry, sem build) + `deploy-prod.sh <tag>` (na VPS1) + job `deploy` no workflow (SSH VPS4→VPS1, só em tags). **Push na main NÃO deploya — só tags.** Rollback: `deploy-prod.sh <tag-anterior>`. Validado: v1.0.13 promovida e confirmada (`V1.0.13 · fc0d65f · PROD`).
---
## 4. Versionamento visível (pré-requisito da confiança) ✅ FEITO
> Implementado em 16/jun/2026 (Etapa 3). O build carimba versão/commit/data/ambiente via build args (`GIT_COMMIT`, `BUILD_TIME`, `APP_ENV`, `APP_VERSION`) → `import.meta.env.VITE_*` no frontend e `process.env` no backend. Visível no **rodapé do Sidebar**, no endpoint **`GET /api/version`** e na **linha de boot** `[VERSION] …` dos logs.
Cada build carimba e **exibe**:
```
ScoreOdonto
v1.0.15
commit: 7a9b3c1
build: 2026-06-16 14:32 UTC
ambiente: PRODUÇÃO
```
Visível no **rodapé** da aplicação, em um **endpoint de diagnóstico** (ex.: `GET /api/version`) e na **linha de boot** dos logs. Hoje a versão fica escondida no bundle `index-<hash>.js` (ver `arquitetura.md` §7) — esta é a primeira melhoria de maior valor.
---
## 5. Rollback 🔜
- Listar as tags/imagens no registry da VPS3.
- Na VPS1, apontar o compose para a tag anterior (`:v1.0.14`) e `docker compose up -d`.
- Recuperação em minutos, **sem rebuild**. O banco é aditivo (ver `arquitetura.md` §5), então voltar o código é seguro; restaurar dump só em caso extremo.
---
## 6. Estado de hoje (honesto) ✅/❌
-`git push` para o Gitea **funciona** (guarda o código).
-**Não há build automático**, **não há registry**, **não há deploy automático**. O antigo listener (porta 9002) foi deletado e não há webhook do scoreodonto.com no Gitea.
- ➡️ Enquanto a CI acima não existe, a promoção para produção é **manual** e está documentada passo a passo em **`DEPLOY-PRODUCAO.md`** (build na VPS4 → enviar imagem para a VPS1 → `up -d`).
---
## 7. Roadmap para sair do "hoje" e chegar no "alvo"
| Etapa | Entrega | Status |
|-------|---------|--------|
| 3 | Versionamento visível (rodapé + endpoint + log) | ✅ |
| 4 | Registry Docker privado na VPS3 | ✅ (Gitea Container Registry — ver `REGISTRY.md`) |
| 5 | Pipeline de build no Gitea (push → imagem → registry), **sem deploy** | ✅ (`.gitea/workflows/build.yml` + runner `vps4-builder`) |
| 6 | Deploy por TAG (`vX.Y.Z` → produção na VPS1) + rollback por imagem | ✅ (`deploy-prod.sh` + job `deploy` em tags) |
Quando as Etapas 36 estiverem prontas, este documento deixa de ter marcações 🔜 e passa a descrever o fluxo corrente.
-5
View File
@@ -1,5 +0,0 @@
// LEGADO (Passo 4): a APLICAÇÃO não usa mais este valor — a versão exibida vem do backend
// (/api/version) em runtime via useAppVersion(). Permanece apenas porque o PIPELINE ainda o lê
// (update-version.js + .gitea/workflows/build.yml). Será eliminado no Passo 5, quando a versão
// passar a nascer da TAG git. Não referenciar no código do frontend.
export const APP_VERSION = 'V1.0.15';
-24
View File
@@ -1,24 +0,0 @@
#!/usr/bin/env node
// Increments the patch number in frontend/constants.ts before every build.
// Called via: node ../update-version.js (from inside frontend/)
const fs = require('fs');
const path = require('path');
const constantsPath = path.join(__dirname, 'frontend', 'constants.ts');
const content = fs.readFileSync(constantsPath, 'utf8');
const match = content.match(/APP_VERSION\s*=\s*['"]V(\d+)\.(\d+)\.(\d+)['"]/);
if (!match) {
console.error('[update-version] Could not parse APP_VERSION in constants.ts');
process.exit(1);
}
const [, major, minor, patch] = match;
const next = `V${major}.${minor}.${parseInt(patch, 10) + 1}`;
const updated = content.replace(
/APP_VERSION\s*=\s*['"]V\d+\.\d+\.\d+['"]/,
`APP_VERSION = '${next}'`
);
fs.writeFileSync(constantsPath, updated, 'utf8');
console.log(`[update-version] ${match[0].match(/V\d+\.\d+\.\d+/)[0]}${next}`);