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:
+24
-6
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user