feat(deploys): implement versions admin panel, automatic plugin compilation & production loader paths
This commit is contained in:
@@ -9,9 +9,9 @@ export interface LoadedPlugin {
|
||||
dir: string
|
||||
}
|
||||
|
||||
// Em produção: __dirname = backend/dist/core → 3 níveis acima = raiz do projeto
|
||||
// Em dev com tsx: __dirname = backend/src/core → 3 níveis acima = raiz do projeto
|
||||
const PLUGINS_DIR = path.resolve(__dirname, '../../../plugins')
|
||||
const PLUGINS_DIR = process.env.NODE_ENV === 'production'
|
||||
? '/app/plugins'
|
||||
: path.resolve(__dirname, '../../../plugins')
|
||||
|
||||
/**
|
||||
* 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' })
|
||||
}
|
||||
})
|
||||
|
||||
// ── 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: 'registration', label: 'Registro', icon: Users },
|
||||
{ id: 'system', label: 'Sistema', icon: Wrench },
|
||||
{ id: 'deploys', label: 'Versões', icon: RefreshCw },
|
||||
] as const
|
||||
|
||||
type Tab = typeof TABS[number]['id']
|
||||
@@ -167,6 +168,81 @@ export default function AdminSettings() {
|
||||
const [engine, setEngine] = useState<'infinite' | 'official'>('infinite')
|
||||
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(() => {
|
||||
if (!loading && user?.role !== 'admin') router.replace('/inbox')
|
||||
}, [loading, user, router])
|
||||
@@ -269,7 +345,7 @@ export default function AdminSettings() {
|
||||
|
||||
<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 }}>
|
||||
<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 ── */}
|
||||
{tab === 'branding' && (
|
||||
@@ -568,6 +644,147 @@ export default function AdminSettings() {
|
||||
</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>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
|
||||
@@ -9,7 +9,11 @@
|
||||
"resolveJsonModule": true,
|
||||
"sourceMap": true,
|
||||
"moduleResolution": "node",
|
||||
"noEmitOnError": false
|
||||
"noEmitOnError": false,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"*": ["*", "../backend/node_modules/*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"./**/*.ts"
|
||||
|
||||
Reference in New Issue
Block a user