feat(version): versão em runtime via /api/version (Passos 1/3/4)

- buildInfo.ts: hook useAppVersion() consome GET /api/version (fonte única = backend),
  com fallback ao carimbo do bundle; deixa de depender do APP_VERSION manual.
- Versão exibida em Configurações ("Sobre o sistema") + telas públicas (Login/WaitingInvite);
  removida do rodapé do Sidebar.
- compose.prod/deploy-prod.sh: injetam APP_VERSION (=tag) e APP_ENV=PROD em runtime,
  desacoplando a versão semântica do build (commit/build seguem da imagem).
- constants.ts marcado legado + bump V1.0.15 (transitório até o Passo 5; evita colisão de tag).
- DEPLOY-PRODUCAO.md reescrito com status /🧪/🎯 + nota de fase transitória.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
VPS 4 Builder
2026-06-17 05:19:27 +02:00
parent 6e81718725
commit a991f1fe2c
9 changed files with 187 additions and 164 deletions
+2 -1
View File
@@ -24,8 +24,9 @@ FILES="-f docker-compose.yml -f docker-compose.prod.yml"
echo "──────────────────────────────────────────────────────────────"
echo " Deploy ScoreOdonto → PRODUÇÃO tag: $TAG"
echo " Versão exibida (/api/version) será injetada em runtime = $TAG (commit/build vêm da imagem)"
echo "──────────────────────────────────────────────────────────────"
echo "⚠️ Recomendado: backup do banco antes (dev/prod compartilham o banco):"
echo "⚠️ Recomendado: backup do banco de PRODUÇÃO antes (migrações rodam no boot do backend):"
echo " ssh deploy@10.99.0.3 'pg_dump -Fc scoreodonto -f ~/backups/scoreodonto_\$(date +%Y%m%d_%H%M).dump'"
echo
+15 -4
View File
@@ -1,14 +1,25 @@
# Override de PRODUÇÃO (VPS1): roda IMAGENS VERSIONADAS do registry em vez de buildar.
# A produção não builda — só consome a imagem pronta publicada pela CI (Etapa 5).
# A produção não builda — só consome a imagem pronta publicada pela CI.
#
# Uso (na VPS1), via deploy-prod.sh <tag>, que faz:
# SCOREODONTO_TAG=v1.0.13 docker compose -f docker-compose.yml -f docker-compose.prod.yml pull ...
# Uso (na VPS1), via deploy-prod.sh <tag>, que exporta SCOREODONTO_TAG e faz:
# docker compose -f docker-compose.yml -f docker-compose.prod.yml pull ...
# docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --no-build ...
#
# O --no-build garante que o build: do compose base seja ignorado (usa a imagem puxada).
#
# VERSÃO EM RUNTIME (Passo 3): a versão semântica EXIBIDA (/api/version → footer/Configurações)
# vem da TAG promovida, injetada via `environment` abaixo — NÃO do build. `commit` e `build`
# continuam vindo do ENV carimbado na imagem (imutáveis, propriedade do artefato). Assim a
# MESMA imagem pode ser promovida sob qualquer tag sem rebuild — pré-requisito do
# "build once, promote the same artifact". O frontend não precisa de versão aqui: é estático
# e lê tudo de /api/version (backend) em runtime.
services:
scoreodonto-backend:
image: git.clube67.com/ruicesar/scoreodonto-backend:${SCOREODONTO_TAG:-latest}
# Mescla com o environment do compose base (por chave). Sobrescreve o APP_VERSION/APP_ENV
# que vieram embutidos na imagem pelo build, fixando a versão = tag e o ambiente = PROD.
environment:
- APP_VERSION=${SCOREODONTO_TAG:-dev}
- APP_ENV=PROD
scoreodonto-frontend:
image: git.clube67.com/ruicesar/scoreodonto-frontend:${SCOREODONTO_TAG:-latest}
+107 -135
View File
@@ -1,170 +1,142 @@
# DEPLOY-PRODUCAO — Runbook operacional
# Deploy & Versionamento — Runbook ScoreOdonto
> ✅ **FLUXO CANÔNICO HOJE: deploy por TAG (CI/CD).** Para subir uma versão à produção:
> ```
> # na VPS4, com a versão validada em dev:
> node update-version.js && git add -A && git commit -m "..." && git push origin main
> git tag v1.0.XX && git push origin v1.0.XX # ← a TAG dispara build + deploy automático na VPS1
> ```
> A CI builda a imagem versionada, publica no registry e promove para a VPS1 (`deploy-prod.sh`). Confirme em `https://scoreodonto.com/api/version`.
> **Rollback:** na VPS1, `./deploy-prod.sh <tag-anterior>`. Detalhes em `deploy.md` e `REGISTRY.md`.
> 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?"*.
>
> ⬇️ O passo a passo manual abaixo continua válido como **fallback** (deploy sem a CI).
---
> 🛑 Procedimento manual (fallback), passo a passo, sem adivinhação.
> Reflete o mecanismo de build local. A realidade da infra está em `arquitetura.md`; o fluxo por tag em `deploy.md`.
> **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**.
>
> ⚠️ **Regra de ouro:** antes de qualquer build/deploy/push, confirme com o operador **DEV ou PRODUÇÃO**.
> `dev.scoreodonto.com` = VPS4 (descartável, mas **mesmo banco** da prod). `scoreodonto.com` = VPS1 (real).
> **Nunca** `git push --force` na `main`. **Dev e prod compartilham o banco** (VPS3) — o backup é a única rede de segurança.
> **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).
---
## Mapa rápido (quem é quem)
## 1. O modelo
| Onde | Host | Papel | Socket Docker |
|------|------|-------|----------------|
| VPS4 | `10.99.0.4` | dev + builder (código aqui) | `/var/run/docker.sock` (root) |
| VPS1 | `10.99.0.1` | produção | `unix:///run/user/1000/docker.sock` (rootless) |
| VPS3 | `10.99.0.3` | Postgres + Gitea | — |
> **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.**
- Repo: `ssh://git@10.99.0.3/ruicesar/scoreodonto.com.git` (Gitea).
- Diretório do repo nas duas máquinas: `/home/deploy/stack/scoreodonto.com`.
- Project Compose: `scoreodontocom` (containers `scoreodontocom-*`).
```
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)
```
---
## FASE 0 — Pré-voo (na VPS4)
## 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`) | ✅ | 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) |
---
## 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.14", "commit": "081985c",
"build": "2026-06-16 08:06 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.
---
## 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
# 1. Sanidade do backend
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. Validar a mudança em DEV (já no ar em https://dev.scoreodonto.com)
# Conferir o bundle servido (deve mudar a cada rebuild):
curl -s https://dev.scoreodonto.com/ | grep -oE 'index-[A-Za-z0-9_-]+\.js' | head -1
```
# 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)
```bash
# 3. BACKUP DO BANCO (rede de segurança única — dev/prod compartilham o banco)
# Rodar na VPS3. Usar as credenciais do ambiente/segredo, nunca em texto puro.
ssh deploy@10.99.0.3 'pg_dump -Fc scoreodonto -f /home/deploy/backups/scoreodonto_$(date +%Y%m%d_%H%M).dump'
```
---
## FASE 1 — Versão (na VPS4) ⚠️ passo manual
O bump **não é automático** (`npm run build` é só `vite build`). É preciso incrementar **à mão**:
```bash
cd /home/deploy/stack/scoreodonto.com
node update-version.js # incrementa o patch em frontend/constants.ts (ex.: V1.0.12 → V1.0.13)
grep APP_VERSION frontend/constants.ts
```
> Hoje a versão **não fica visível no navegador** (está no bundle JS). Até a Etapa 3 (versionamento visível), anote a versão que está promovendo.
---
## FASE 2 — Commit & push para o Gitea (na VPS4)
> Só execute com autorização explícita do operador (é o repo de produção).
```bash
cd /home/deploy/stack/scoreodonto.com
git add -A
git commit -m "feat: <descrição> (bump vX.Y.Z)"
# 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
```
> ❗ Push **não** dispara deploy automático hoje (não há listener/webhook ativo). A promoção é a FASE 3.
**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.
---
## FASE 3 — Promover para produção (na VPS1)
## 6. Rollback ✅
Reverter o **código** é seguro (migrações só adicionam). Por imagem, **sem rebuild**:
```bash
ssh deploy@10.99.0.1
export DOCKER_HOST=unix:///run/user/1000/docker.sock # docker rootless da VPS1
cd /home/deploy/stack/scoreodonto.com
# 1. Trazer o código aprovado
git pull origin main
# 2. Carimbo de versão visível (lido pelos build args do compose) — em PROD use APP_ENV=PROD
export APP_VERSION=$(grep -oE 'V[0-9]+\.[0-9]+\.[0-9]+' frontend/constants.ts | head -1)
export GIT_COMMIT=$(git rev-parse --short HEAD)
export BUILD_TIME=$(date -u +'%Y-%m-%d %H:%M UTC')
export APP_ENV=PROD
# 3. Buildar as imagens (frontend faz vite build; backend npm install)
docker compose build scoreodonto-frontend scoreodonto-backend
# 4. Subir (troca os containers pelas imagens novas)
docker compose up -d scoreodonto-frontend scoreodonto-backend nginx
/home/deploy/stack/scoreodonto.com/deploy-prod.sh v1.0.13 # tag anterior → pull + up --no-build
```
> Os 4 `export` carimbam versão/commit/data/ambiente na imagem. Sem eles, o build ainda funciona mas mostra `dev`/`unknown` no rodapé. Em **DEV** (VPS4) use `APP_ENV=DEV`.
O backend leva ~1030s no boot (reconecta Postgres/Dragonfly e roda as migrações idempotentes).
Restaurar o dump do banco só em caso extremo (`pg_restore --clean`).
---
## FASE 4 — Verificação pós-deploy (na VPS1)
## 7. Passo 5 — "tag promove, não builda" 🎯
```bash
export DOCKER_HOST=unix:///run/user/1000/docker.sock
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.
# Containers no ar
docker ps --format '{{.Names}} | {{.Status}}' | grep score
# Backend: esperar a linha de migração + carimbo de versão
docker logs scoreodontocom-scoreodonto-backend-1 --tail 40 | grep -iE 'migration|VERSION'
# alvo: [MIGRATION] all migrations OK
# [VERSION] ScoreOdonto API vX.Y.Z · commit ... · build ... · PROD
# Confirmar a versão no ar SEM acessar container (a forma definitiva):
curl -s https://scoreodonto.com/api/version
# {"name":"ScoreOdonto API","version":"vX.Y.Z","commit":"...","build":"...","environment":"PROD"}
# E visualmente: a versão aparece no rodapé do menu lateral (Sidebar).
```
**Smoke tests mínimos:** abrir `https://scoreodonto.com`, logar, e exercitar o que mudou (ex.: lançamento financeiro, agenda, salas). Conferir ausência de erro 500 no `docker logs`.
Racional completo em `deploy.md` e na memória `project-cicd-tag-promove-nao-rebuilda`.
---
## FASE 5 — Rollback (se algo quebrar)
As migrações são **aditivas** → reverter o **código** é seguro (colunas/tabelas extras ficam inertes).
```bash
ssh deploy@10.99.0.1
export DOCKER_HOST=unix:///run/user/1000/docker.sock
cd /home/deploy/stack/scoreodonto.com
git log --oneline -5 # achar o commit bom anterior
git checkout <commit-anterior> # ou: git reset --hard <commit-anterior>
docker compose build scoreodonto-frontend scoreodonto-backend
docker compose up -d scoreodonto-frontend scoreodonto-backend
```
> Hoje o rollback **rebuilda** a versão anterior (as imagens são `:latest`, não há tag por versão). Quando a **Etapa 6** entregar deploy por tag + registry, o rollback vira só `docker compose pull` da tag anterior — sem rebuild. Restaurar o dump do banco (FASE 0) só em caso extremo:
> `pg_restore -d "postgresql://scoreodonto_user:<senha>@10.99.0.3:5432/scoreodonto" --clean <arquivo>.dump`
---
## Resumo em uma tela
```
VPS4: validar em dev → node --check → backup do banco (VPS3)
→ node update-version.js → commit → git push origin main (com OK do operador)
VPS1: git pull → docker compose build → docker compose up -d
→ verificar logs ([MIGRATION] all migrations OK) + smoke test
Rollback: git checkout <commit-bom> → build → up -d
```
> **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.
+24 -6
View File
@@ -1,17 +1,35 @@
import { APP_VERSION } from './constants.ts';
import { useState, useEffect } from 'react';
// Metadados de build. commit/build/ambiente são carimbados no build via
// variáveis VITE_* (ARGs do Dockerfile do frontend). Em dev sem build args,
// caem nos defaults abaixo.
// Carimbo do BUILD (bundle). É só FALLBACK enquanto o /api/version não responde.
// A FONTE OFICIAL da versão é o backend, em runtime, via useAppVersion().
// Não depende mais de APP_VERSION manual: version cai em VITE_APP_VERSION (se o
// pipeline injetar) ou 'dev'. commit/build/ambiente seguem carimbados no build (VITE_*).
const meta = (import.meta as any).env || {};
export const BUILD_INFO = {
name: 'ScoreOdonto',
version: APP_VERSION,
version: meta.VITE_APP_VERSION || 'dev',
commit: meta.VITE_GIT_COMMIT || 'dev',
build: meta.VITE_BUILD_TIME || '',
environment: String(meta.VITE_APP_ENV || 'DEV').toUpperCase(),
};
// Rótulo curto: "V1.0.13 · 7a9b3c1 · DEV"
// Rótulo curto: "v1.0.15 · 7a9b3c1 · DEV"
export const versionLabel = `${BUILD_INFO.version} · ${BUILD_INFO.commit} · ${BUILD_INFO.environment}`;
// Fonte ÚNICA da versão exibida: o backend em runtime (GET /api/version, público).
// Cache em módulo p/ não refazer o fetch a cada tela. Fallback = BUILD_INFO (bundle).
let _verCache: any = null;
export function useAppVersion() {
const [info, setInfo] = useState<any>(_verCache || BUILD_INFO);
useEffect(() => {
if (_verCache) { setInfo(_verCache); return; }
let alive = true;
fetch('/api/version')
.then(r => (r.ok ? r.json() : null))
.then(v => { if (alive && v && v.version) { _verCache = { ...BUILD_INFO, ...v }; setInfo(_verCache); } })
.catch(() => {});
return () => { alive = false; };
}, []);
return info;
}
-10
View File
@@ -7,7 +7,6 @@ import {
import { ToothIcon } from './ToothIcon.tsx';
import { HybridBackend } from '../services/backend.ts';
import { getActivePlugins } from '../views/plugins/pluginRegistry.ts';
import { BUILD_INFO } from '../buildInfo.ts';
interface SidebarProps {
activeTab: string;
@@ -284,15 +283,6 @@ export const Sidebar: React.FC<SidebarProps> = ({ activeTab, setActiveTab }) =>
<LogOut size={16} />
SAIR DO SISTEMA
</button>
<div
className="px-4 pt-1 text-[8px] leading-tight text-gray-300 font-mono select-text"
title={`Build: ${BUILD_INFO.build || 'n/a'}`}
>
<span className="font-bold text-gray-400">{BUILD_INFO.version}</span>
{' · '}{BUILD_INFO.commit}
{' · '}<span className={BUILD_INFO.environment === 'PRODUÇÃO' || BUILD_INFO.environment === 'PROD' ? 'text-emerald-400' : 'text-amber-400'}>{BUILD_INFO.environment}</span>
</div>
</div>
</div>
);
+5 -1
View File
@@ -1 +1,5 @@
export const APP_VERSION = 'V1.0.14';
// 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';
+27 -2
View File
@@ -5,7 +5,7 @@ import { GoogleConnectButton } from '../components/GoogleConnectButton.tsx';
import { useToast } from '../contexts/ToastContext.tsx';
import { useConfirm } from '../contexts/ConfirmContext.tsx';
import { ChangePasswordModal } from '../components/ChangePasswordModal.tsx';
import { APP_VERSION } from '../constants.ts';
import { useAppVersion } from '../buildInfo.ts';
import { buscarCep } from '../services/viacep.ts';
const FIELD_CLS = 'w-full border border-gray-200 rounded-xl px-3 py-2.5 text-sm font-medium text-gray-800 outline-none focus:border-teal-500 focus:ring-2 focus:ring-teal-100 transition-all';
@@ -352,6 +352,8 @@ export const ConfiguracoesView: React.FC = () => {
const toast = useToast();
const confirm = useConfirm();
const [showChangePw, setShowChangePw] = useState(false);
// Versão/commit/ambiente vêm do backend em RUNTIME (fonte única). Fallback: carimbo do bundle.
const ver = useAppVersion();
const authHeader = { 'Authorization': `Bearer ${localStorage.getItem('SCOREODONTO_AUTH_TOKEN')}`, 'Content-Type': 'application/json' };
@@ -417,7 +419,7 @@ export const ConfiguracoesView: React.FC = () => {
<p className="text-sm text-gray-400 font-medium mt-1">Gerencie seu perfil e integrações</p>
</div>
<span className="bg-gray-100 text-gray-400 text-[11px] font-mono font-bold px-3 py-1 rounded-full border border-gray-200 mb-1">
{APP_VERSION}
{ver.version}
</span>
</div>
@@ -567,6 +569,29 @@ export const ConfiguracoesView: React.FC = () => {
</div>
</div>
{/* Sobre o sistema — versão/commit/ambiente vindos do backend em runtime */}
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm overflow-hidden">
<div className="px-6 py-4 border-b border-gray-50">
<h2 className="text-xs font-black text-gray-500 uppercase tracking-widest">Sobre o sistema</h2>
</div>
<div className="p-6">
<dl className="grid grid-cols-[auto,1fr] gap-x-6 gap-y-2 text-sm font-mono">
<dt className="text-gray-400 font-bold uppercase text-[11px] tracking-widest self-center">Versão</dt>
<dd className="text-gray-900 font-bold">{ver.version}</dd>
<dt className="text-gray-400 font-bold uppercase text-[11px] tracking-widest self-center">Commit</dt>
<dd className="text-gray-700">{ver.commit}</dd>
<dt className="text-gray-400 font-bold uppercase text-[11px] tracking-widest self-center">Build</dt>
<dd className="text-gray-700">{ver.build || '—'}</dd>
<dt className="text-gray-400 font-bold uppercase text-[11px] tracking-widest self-center">Ambiente</dt>
<dd>
<span className={`font-black ${ver.environment === 'PRODUÇÃO' || ver.environment === 'PROD' ? 'text-emerald-600' : 'text-amber-600'}`}>
{ver.environment}
</span>
</dd>
</dl>
</div>
</div>
{showChangePw && <ChangePasswordModal onClose={() => setShowChangePw(false)} onDone={() => setShowChangePw(false)} />}
</div>
);
+4 -3
View File
@@ -5,7 +5,7 @@ import {
UserSearch, DoorOpen, ScanLine, Sparkles, MapPin, Building2, Users, FlaskConical
} from 'lucide-react';
import { HybridBackend } from '../services/backend.ts';
import { APP_VERSION } from '../constants.ts';
import { useAppVersion } from '../buildInfo.ts';
import { OnboardingChat } from './OnboardingChat.tsx';
interface LoginViewProps {
@@ -57,6 +57,7 @@ const InputCls = 'w-full pl-10 pr-4 py-3.5 bg-gray-50 border border-gray-200 rou
// ── Component ─────────────────────────────────────────────────────────────────
export const LoginView: React.FC<LoginViewProps> = ({ onLoginSuccess }) => {
const ver = useAppVersion(); // versão oficial via /api/version (público), fallback bundle
const [tab, setTab] = useState<'login' | 'cadastro'>('login');
const [authMode, setAuthMode] = useState<'login' | 'forgot' | 'reset'>('login');
@@ -215,7 +216,7 @@ export const LoginView: React.FC<LoginViewProps> = ({ onLoginSuccess }) => {
{/* Footer */}
<div className="relative z-10 flex items-center gap-2 text-teal-300/50 text-[10px] font-black uppercase tracking-widest">
<ShieldCheck size={12} /> Dados criptografados · LGPD · {APP_VERSION}
<ShieldCheck size={12} /> Dados criptografados · LGPD · {ver.version}
</div>
</div>
@@ -335,7 +336,7 @@ export const LoginView: React.FC<LoginViewProps> = ({ onLoginSuccess }) => {
</div>
<div className="px-8 py-4 bg-gray-50 border-t border-gray-100 flex items-center justify-center gap-2">
<ShieldCheck size={12} className="text-gray-400" />
<span className="text-[10px] font-black text-gray-400 uppercase tracking-widest">Ambiente Seguro · {APP_VERSION}</span>
<span className="text-[10px] font-black text-gray-400 uppercase tracking-widest">Ambiente Seguro · {ver.version}</span>
</div>
</div>
)}
+3 -2
View File
@@ -1,7 +1,7 @@
import React, { useState } from 'react';
import { Mail, LogOut, Stethoscope, Clock, CheckCircle2, ArrowRight, MapPin, Users, Wrench, ChevronDown, ChevronUp, Loader2 } from 'lucide-react';
import { HybridBackend } from '../services/backend.ts';
import { APP_VERSION } from '../constants.ts';
import { useAppVersion } from '../buildInfo.ts';
const UF_LIST = ['AC','AL','AM','AP','BA','CE','DF','ES','GO','MA','MG','MS','MT','PA','PB','PE','PI','PR','RJ','RN','RO','RR','RS','SC','SE','SP','TO'];
@@ -13,6 +13,7 @@ const ROLE_CONFIG: Record<string, { label: string; icon: React.ElementType; colo
};
export const WaitingInviteView: React.FC = () => {
const ver = useAppVersion();
const userData = localStorage.getItem('SCOREODONTO_USER_DATA');
const user = userData ? JSON.parse(userData) : null;
const role = user?.role ?? 'dentista';
@@ -229,7 +230,7 @@ export const WaitingInviteView: React.FC = () => {
</div>
<div className="px-8 py-4 bg-gray-50 border-t border-gray-100 flex items-center justify-center">
<span className="text-[10px] font-black text-gray-400 uppercase tracking-widest">{APP_VERSION}</span>
<span className="text-[10px] font-black text-gray-400 uppercase tracking-widest">{ver.version}</span>
</div>
</div>
</div>