feat(deploy): padroniza rx p/ Build Once->Promote->Deploy (CI registry, compose.prod, badge/health)
- .gitea/workflows/build.yml: push->build rx-scoreodonto:<sha> no registry; tag->promote+deploy - docker-compose.prod.yml: image do registry, APP_VERSION/GIT_COMMIT em runtime - deploy-prod.sh: VPS1 pull + up --no-build (antigo vira deploy-prod-legado-webhook.sh) - /api/health usa APP_VERSION (tag) com fallback version.txt; badge no canto - webhook 9003 aposentado (gatilho agora e CI/tag) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1 +1 @@
|
||||
2.1.76
|
||||
2.1.77
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
|
||||
import { AuthProvider } from './contexts/AuthContext';
|
||||
import { SocketProvider } from './contexts/SocketContext';
|
||||
@@ -23,8 +24,24 @@ import UsersPage from './pages/UsersPage';
|
||||
import SyncPage from './pages/SyncPage';
|
||||
import ApiDocsPage from './pages/ApiDocsPage';
|
||||
|
||||
// Versão fixa no canto inferior direito (padrão da stack — lê /api/health; não bloqueia cliques).
|
||||
function VersionBadge() {
|
||||
const [ver, setVer] = useState('');
|
||||
useEffect(() => {
|
||||
fetch('/api/health').then(r => (r.ok ? r.json() : null))
|
||||
.then(v => { if (v && v.version) setVer(v.version); }).catch(() => {});
|
||||
}, []);
|
||||
if (!ver) return null;
|
||||
return (
|
||||
<div style={{ position: 'fixed', bottom: 6, right: 8, zIndex: 40, pointerEvents: 'none', userSelect: 'none', fontSize: 10, fontFamily: 'monospace', color: 'rgba(120,120,120,0.7)' }}>
|
||||
v{ver}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<>
|
||||
<BrowserRouter future={{ v7_startTransition: true, v7_relativeSplatPath: true }}>
|
||||
<AuthProvider>
|
||||
<ToastProvider>
|
||||
@@ -145,5 +162,7 @@ export default function App() {
|
||||
</ToastProvider>
|
||||
</AuthProvider>
|
||||
</BrowserRouter>
|
||||
<VersionBadge />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,9 +4,13 @@ import react from '@vitejs/plugin-react'
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
// Em dev, faz proxy da API para o backend Express
|
||||
// Em dev, o vite serve o frontend e faz proxy da API para o backend (container rx_dev_api).
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
port: 5173,
|
||||
strictPort: true,
|
||||
cors: true,
|
||||
allowedHosts: ['dev.rx.scoreodonto.com'],
|
||||
hmr: {
|
||||
host: 'dev.rx.scoreodonto.com',
|
||||
clientPort: 443,
|
||||
@@ -15,7 +19,7 @@ export default defineConfig({
|
||||
},
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://127.0.0.1:3000',
|
||||
target: 'http://rx_dev_api:3000',
|
||||
configure: (proxy) => {
|
||||
proxy.on('proxyRes', (proxyRes) => {
|
||||
proxyRes.headers['access-control-allow-origin'] = '*';
|
||||
@@ -23,15 +27,14 @@ export default defineConfig({
|
||||
}
|
||||
},
|
||||
'/socket.io': {
|
||||
target: 'http://127.0.0.1:3000',
|
||||
target: 'http://rx_dev_api:3000',
|
||||
ws: true
|
||||
},
|
||||
'/uploads': 'http://127.0.0.1:3000',
|
||||
'/updates': 'http://127.0.0.1:3000'
|
||||
'/uploads': 'http://rx_dev_api:3000',
|
||||
'/updates': 'http://rx_dev_api:3000'
|
||||
}
|
||||
},
|
||||
build: {
|
||||
// O output vai para o diretório que o Express vai servir
|
||||
outDir: 'dist',
|
||||
emptyOutDir: true
|
||||
}
|
||||
|
||||
@@ -122,6 +122,40 @@ app.get('/api/redis/ping', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Health check padronizado (status do backend + banco + cache). Padrão da stack:
|
||||
// ver git.clube67.com/ruicesar/Documentos-Unicos → deploy/versionamento.md
|
||||
app.get('/api/health', async (req, res) => {
|
||||
const checks = {};
|
||||
try {
|
||||
const t = Date.now();
|
||||
await db.get('SELECT 1');
|
||||
checks.database = { status: 'ok', latency_ms: Date.now() - t };
|
||||
} catch {
|
||||
checks.database = { status: 'down' };
|
||||
}
|
||||
try {
|
||||
const client = redis.getClient();
|
||||
if (client) { const t = Date.now(); await client.ping(); checks.cache = { status: 'ok', latency_ms: Date.now() - t }; }
|
||||
else checks.cache = { status: 'disabled' };
|
||||
} catch {
|
||||
checks.cache = { status: 'down' };
|
||||
}
|
||||
// Em PROD a versão vem da TAG (APP_VERSION, injetada em runtime pelo deploy);
|
||||
// em DEV cai para version.txt (arquivo da imagem). Padrão "build once, promote".
|
||||
let version = (process.env.APP_VERSION && process.env.APP_VERSION !== 'dev') ? process.env.APP_VERSION : 'dev';
|
||||
if (version === 'dev') { try { version = (await fs.readFile(path.join(__dirname, 'version.txt'), 'utf8')).trim(); } catch {} }
|
||||
const dbOk = checks.database.status === 'ok';
|
||||
const status = !dbOk ? 'down' : (checks.cache.status === 'down' ? 'degraded' : 'ok');
|
||||
res.status(dbOk ? 200 : 503).json({
|
||||
status,
|
||||
version,
|
||||
commit: process.env.GIT_COMMIT || 'dev',
|
||||
environment: (process.env.APP_ENV || 'DEV').toUpperCase(),
|
||||
uptime_s: Math.round(process.uptime()),
|
||||
checks,
|
||||
});
|
||||
});
|
||||
|
||||
// Verificar se o nome do computador/cliente já existe
|
||||
app.get('/api/clients/check-name', async (req, res) => {
|
||||
try {
|
||||
|
||||
@@ -1 +1 @@
|
||||
2.1.76
|
||||
2.1.77
|
||||
|
||||
Reference in New Issue
Block a user