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
+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>