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
|
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>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user