diff --git a/clube67/newwhats.local/backend/src/core/plugin-loader.ts b/clube67/newwhats.local/backend/src/core/plugin-loader.ts index b5938bc..6a9883a 100644 --- a/clube67/newwhats.local/backend/src/core/plugin-loader.ts +++ b/clube67/newwhats.local/backend/src/core/plugin-loader.ts @@ -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). diff --git a/clube67/newwhats.local/backend/src/modules/auth/admin.routes.ts b/clube67/newwhats.local/backend/src/modules/auth/admin.routes.ts index b98b9a1..22d09b2 100644 --- a/clube67/newwhats.local/backend/src/modules/auth/admin.routes.ts +++ b/clube67/newwhats.local/backend/src/modules/auth/admin.routes.ts @@ -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' }) + } +}) + diff --git a/clube67/newwhats.local/frontend/pages/admin/settings.tsx b/clube67/newwhats.local/frontend/pages/admin/settings.tsx index 2eb9b99..a17bcf2 100644 --- a/clube67/newwhats.local/frontend/pages/admin/settings.tsx +++ b/clube67/newwhats.local/frontend/pages/admin/settings.tsx @@ -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([]) + 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() { - + {/* ── BRANDING ── */} {tab === 'branding' && ( @@ -568,6 +644,147 @@ export default function AdminSettings() { )} + {/* ── DEPLOYS ── */} + {tab === 'deploys' && ( +
+
+ Ciclo de Deploy (GitOps) e Versões +

+ Visualize o histórico de compilação da plataforma e gerencie os deploys automáticos em produção diretamente do painel de administração. +

+
+ + {/* Grid das Versões */} +
+ {/* Card Versão Atual */} +
+
+ Versão Ativa +
+ + {versionData?.version || 'v1.0.0'} + + + build #{versionData?.buildCount || 0} + +
+
+ + + + + Online em Produção (VPS 1) +
+
+ +
+ Último deploy:{' '} + + {versionData?.lastDeploy + ? new Date(versionData.lastDeploy).toLocaleString('pt-BR') + : 'Pendente ou Manual'} + +
+
+ + {/* Card Último Commit */} +
+
+ Última Alteração Integrada +
+ + {versionData?.lastCommit?.message || 'Sem dados de commit'} + +
+
+ Por: {versionData?.lastCommit?.author || 'N/A'} +
+
+ +
+ Hash do commit: + + {versionData?.lastCommit?.hash || 'N/A'} + +
+
+
+ + {/* Bloco de Ação Manual */} +
+
+

Compilação e Deploy Forçado

+

+ 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? +

+
+ +
+ + {/* Histórico Completo de Deploys */} +
+ Histórico de Push & Compilações + + {deploysHistory.length === 0 ? ( +
+ Nenhum histórico de deploy registrado ainda. +
+ ) : ( +
+ + + + + + + + + + + + {deploysHistory.map((deploy, index) => ( + + + + + + + + ))} + +
VersãoAutorMensagem do CommitData & HoraStatus
+ {deploy.version} + {deploy.author}{deploy.commitMessage} + {new Date(deploy.timestamp).toLocaleString('pt-BR')} + + + + {deploy.status || 'Sucesso'} + +
+
+ )} +
+
+ )} + +
diff --git a/clube67/newwhats.local/plugins/tsconfig.json b/clube67/newwhats.local/plugins/tsconfig.json index a1b0dc8..a4ace93 100644 --- a/clube67/newwhats.local/plugins/tsconfig.json +++ b/clube67/newwhats.local/plugins/tsconfig.json @@ -9,7 +9,11 @@ "resolveJsonModule": true, "sourceMap": true, "moduleResolution": "node", - "noEmitOnError": false + "noEmitOnError": false, + "baseUrl": ".", + "paths": { + "*": ["*", "../backend/node_modules/*"] + } }, "include": [ "./**/*.ts"