feat(deploys): implement versions admin panel, automatic plugin compilation & production loader paths

This commit is contained in:
VPS 4 Deploy Agent
2026-05-11 05:24:06 +02:00
parent 10c906b61d
commit f95ac82558
4 changed files with 243 additions and 5 deletions
@@ -9,9 +9,9 @@ export interface LoadedPlugin {
dir: string dir: string
} }
// Em produção: __dirname = backend/dist/core → 3 níveis acima = raiz do projeto const PLUGINS_DIR = process.env.NODE_ENV === 'production'
// Em dev com tsx: __dirname = backend/src/core → 3 níveis acima = raiz do projeto ? '/app/plugins'
const PLUGINS_DIR = path.resolve(__dirname, '../../../plugins') : path.resolve(__dirname, '../../../plugins')
/** /**
* Ordena plugins por dependências usando topological sort (Kahn's algorithm). * Ordena plugins por dependências usando topological sort (Kahn's algorithm).
@@ -373,3 +373,20 @@ adminRouter.post('/engine', async (req: Request, res: Response) => {
res.status(500).json({ error: 'Erro ao salvar a engine' }) res.status(500).json({ error: 'Erro ao salvar a engine' })
} }
}) })
// ── Rota para Disparo Manual do Deploy ──
adminRouter.post('/deploys/trigger', async (_req: Request, res: Response) => {
try {
const response = await fetch('http://10.99.0.4:9000/hooks/clube67-deploy-hook', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ trigger: 'admin_panel' })
})
if (!response.ok) throw new Error(`Status ${response.status}`)
res.json({ ok: true, message: 'Deploy automático iniciado na VPS 4.' })
} catch (err: any) {
console.error('[Admin] Falha ao disparar webhook de deploy:', err.message)
res.status(502).json({ error: 'Não foi possível se conectar ao servidor de build' })
}
})
@@ -33,6 +33,7 @@ const TABS = [
{ id: 'email', label: 'E-mail', icon: Mail }, { id: 'email', label: 'E-mail', icon: Mail },
{ id: 'registration', label: 'Registro', icon: Users }, { id: 'registration', label: 'Registro', icon: Users },
{ id: 'system', label: 'Sistema', icon: Wrench }, { id: 'system', label: 'Sistema', icon: Wrench },
{ id: 'deploys', label: 'Versões', icon: RefreshCw },
] as const ] as const
type Tab = typeof TABS[number]['id'] type Tab = typeof TABS[number]['id']
@@ -167,6 +168,81 @@ export default function AdminSettings() {
const [engine, setEngine] = useState<'infinite' | 'official'>('infinite') const [engine, setEngine] = useState<'infinite' | 'official'>('infinite')
const [switchingEngine, setSwitchingEngine] = useState(false) const [switchingEngine, setSwitchingEngine] = useState(false)
// ── Estados de Controle de Versões & Deploys ──
const [versionData, setVersionData] = useState<{
version: string;
buildCount: number;
lastDeploy: string;
lastCommit?: { hash: string; message: string; author: string };
} | null>(null)
const [deploysHistory, setDeploysHistory] = useState<any[]>([])
const [triggeringDeploy, setTriggeringDeploy] = useState(false)
const fetchDeployData = useCallback(async () => {
try {
const vRes = await fetch('/version.json?t=' + Date.now())
if (vRes.ok) {
const vData = await vRes.json()
setVersionData(vData)
}
} catch (e) {}
try {
const dRes = await fetch('/deploys.json?t=' + Date.now())
if (dRes.ok) {
const dData = await dRes.json()
setDeploysHistory(dData)
}
} catch (e) {}
}, [])
const triggerDeploy = async () => {
if (!window.confirm('Deseja realmente iniciar uma nova compilação e deploy em produção agora?\n\nIsso irá atualizar o repositório, reinstalar dependências, compilar o frontend e backend, e reiniciar os serviços.')) return
setTriggeringDeploy(true)
try {
await api.post('/api/admin/deploys/trigger')
alert('Deploy automático iniciado em segundo plano! A VPS 4 está compilando os arquivos. O painel monitorará o progresso automaticamente.')
let attempts = 0
const interval = setInterval(async () => {
attempts++
try {
const vRes = await fetch('/version.json?t=' + Date.now())
if (vRes.ok) {
const vData = await vRes.json()
if (!versionData || vData.buildCount !== versionData.buildCount) {
setVersionData(vData)
const dRes = await fetch('/deploys.json?t=' + Date.now())
if (dRes.ok) {
setDeploysHistory(await dRes.json())
}
setTriggeringDeploy(false)
clearInterval(interval)
alert(`Sucesso! Deploy finalizado. Versão atualizada para ${vData.version}.`)
return
}
}
} catch (e) {}
if (attempts >= 36) { // 3 minutos
clearInterval(interval)
setTriggeringDeploy(false)
alert('O tempo limite de monitoramento esgotou, mas o deploy pode ainda estar rodando. Por favor, recarregue a página em instantes.')
}
}, 5000)
} catch (err) {
alert('Erro ao tentar acionar o deploy automático.')
setTriggeringDeploy(false)
}
}
useEffect(() => {
if (user?.role === 'admin') {
fetchDeployData()
}
}, [user, fetchDeployData])
useEffect(() => { useEffect(() => {
if (!loading && user?.role !== 'admin') router.replace('/inbox') if (!loading && user?.role !== 'admin') router.replace('/inbox')
}, [loading, user, router]) }, [loading, user, router])
@@ -269,7 +345,7 @@ export default function AdminSettings() {
<AnimatePresence mode="wait"> <AnimatePresence mode="wait">
<motion.div key={tab} initial={{ opacity: 0, y: 8 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -8 }} transition={{ duration: 0.18 }}> <motion.div key={tab} initial={{ opacity: 0, y: 8 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -8 }} transition={{ duration: 0.18 }}>
<Card variant="glass" padding="p-8" className="rounded-3xl max-w-3xl"> <Card variant="glass" padding="p-8" className={`rounded-3xl transition-all duration-300 ${tab === 'deploys' ? 'max-w-5xl w-full' : 'max-w-3xl'}`}>
{/* ── BRANDING ── */} {/* ── BRANDING ── */}
{tab === 'branding' && ( {tab === 'branding' && (
@@ -568,6 +644,147 @@ export default function AdminSettings() {
</div> </div>
)} )}
{/* ── DEPLOYS ── */}
{tab === 'deploys' && (
<div className="space-y-8">
<div>
<SectionTitle>Ciclo de Deploy (GitOps) e Versões</SectionTitle>
<p className="text-xs text-slate-500 mt-1">
Visualize o histórico de compilação da plataforma e gerencie os deploys automáticos em produção diretamente do painel de administração.
</p>
</div>
{/* Grid das Versões */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Card Versão Atual */}
<div className="p-6 rounded-2xl bg-white/[0.02] border border-white/5 flex flex-col justify-between">
<div>
<span className="text-[10px] font-bold text-slate-500 uppercase tracking-widest">Versão Ativa</span>
<div className="flex items-baseline gap-2 mt-2">
<span className="text-4xl font-extrabold text-transparent bg-clip-text bg-gradient-to-r from-brand-400 to-brand-500">
{versionData?.version || 'v1.0.0'}
</span>
<span className="text-xs text-slate-400 font-semibold bg-white/5 px-2 py-0.5 rounded-lg border border-white/5">
build #{versionData?.buildCount || 0}
</span>
</div>
<div className="flex items-center gap-2 mt-4 text-xs text-slate-400">
<span className="relative flex h-2 w-2">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
<span className="relative inline-flex rounded-full h-2 w-2 bg-emerald-500"></span>
</span>
Online em Produção (VPS 1)
</div>
</div>
<div className="mt-6 pt-4 border-t border-white/5 text-xs text-slate-400">
Último deploy:{' '}
<span className="text-slate-300 font-semibold">
{versionData?.lastDeploy
? new Date(versionData.lastDeploy).toLocaleString('pt-BR')
: 'Pendente ou Manual'}
</span>
</div>
</div>
{/* Card Último Commit */}
<div className="p-6 rounded-2xl bg-white/[0.02] border border-white/5 flex flex-col justify-between">
<div>
<span className="text-[10px] font-bold text-slate-500 uppercase tracking-widest">Última Alteração Integrada</span>
<div className="flex items-center gap-2 mt-2">
<span className="text-sm font-bold text-slate-200 truncate">
{versionData?.lastCommit?.message || 'Sem dados de commit'}
</span>
</div>
<div className="text-xs text-slate-500 mt-1">
Por: <span className="text-slate-400 font-medium">{versionData?.lastCommit?.author || 'N/A'}</span>
</div>
</div>
<div className="mt-6 pt-4 border-t border-white/5 flex items-center justify-between text-xs">
<span className="text-slate-500">Hash do commit:</span>
<span className="font-mono text-brand-400 bg-brand-500/10 px-2 py-0.5 rounded-lg border border-brand-500/20">
{versionData?.lastCommit?.hash || 'N/A'}
</span>
</div>
</div>
</div>
{/* Bloco de Ação Manual */}
<div className="p-6 rounded-2xl bg-brand-500/5 border border-brand-500/10 flex flex-col md:flex-row items-center justify-between gap-4">
<div className="space-y-1">
<h4 className="text-sm font-bold text-slate-200">Compilação e Deploy Forçado</h4>
<p className="text-xs text-slate-500 max-w-xl">
Deseja puxar a última versão do repositório Git, reinstalar pacotes, rodar o build do Next.js e reiniciar os containers de produção agora?
</p>
</div>
<button
onClick={triggerDeploy}
disabled={triggeringDeploy}
className="flex items-center justify-center gap-2 px-6 py-3 rounded-xl bg-brand-600 hover:bg-brand-500 text-white text-sm font-bold shadow-lg shadow-brand-900/20 transition-all disabled:opacity-50 whitespace-nowrap min-w-[200px]"
>
{triggeringDeploy ? (
<>
<RefreshCw size={16} className="animate-spin" />
Compilando & Sincronizando...
</>
) : (
<>
<Send size={15} />
Compilar e Despachar
</>
)}
</button>
</div>
{/* Histórico Completo de Deploys */}
<div className="space-y-4">
<SectionTitle>Histórico de Push & Compilações</SectionTitle>
{deploysHistory.length === 0 ? (
<div className="text-center py-10 rounded-2xl border border-dashed border-white/5 text-xs text-slate-500">
Nenhum histórico de deploy registrado ainda.
</div>
) : (
<div className="overflow-x-auto rounded-2xl border border-white/5 bg-white/[0.01]">
<table className="w-full text-left text-xs text-slate-400">
<thead>
<tr className="bg-white/[0.02] border-b border-white/5 font-semibold text-slate-300">
<th className="px-5 py-3.5">Versão</th>
<th className="px-5 py-3.5">Autor</th>
<th className="px-5 py-3.5">Mensagem do Commit</th>
<th className="px-5 py-3.5">Data & Hora</th>
<th className="px-5 py-3.5 text-right">Status</th>
</tr>
</thead>
<tbody className="divide-y divide-white/5">
{deploysHistory.map((deploy, index) => (
<tr key={index} className="hover:bg-white/[0.01] transition-colors">
<td className="px-5 py-4 font-bold text-transparent bg-clip-text bg-gradient-to-r from-brand-300 to-brand-400 font-mono">
{deploy.version}
</td>
<td className="px-5 py-4 text-slate-200 font-semibold">{deploy.author}</td>
<td className="px-5 py-4 max-w-xs truncate text-slate-300">{deploy.commitMessage}</td>
<td className="px-5 py-4">
{new Date(deploy.timestamp).toLocaleString('pt-BR')}
</td>
<td className="px-5 py-4 text-right">
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-[10px] font-bold bg-emerald-500/10 text-emerald-400 border border-emerald-500/20">
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400" />
{deploy.status || 'Sucesso'}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</div>
)}
</Card> </Card>
</motion.div> </motion.div>
</AnimatePresence> </AnimatePresence>
+5 -1
View File
@@ -9,7 +9,11 @@
"resolveJsonModule": true, "resolveJsonModule": true,
"sourceMap": true, "sourceMap": true,
"moduleResolution": "node", "moduleResolution": "node",
"noEmitOnError": false "noEmitOnError": false,
"baseUrl": ".",
"paths": {
"*": ["*", "../backend/node_modules/*"]
}
}, },
"include": [ "include": [
"./**/*.ts" "./**/*.ts"