diff --git a/backend/index.js b/backend/index.js index 1a26dfc..c9f6f97 100644 --- a/backend/index.js +++ b/backend/index.js @@ -48,7 +48,7 @@ const server = http.createServer(app); const io = new Server(server, { cors: { origin: "*" } }); io.on("connection", (socket) => { - console.log("Client verbunden:", socket.id); + console.log("🔌 Client verbunden:", socket.id); }); const broadcastRedeployStatus = (stackId, status) => { @@ -59,12 +59,13 @@ const broadcastRedeployStatus = (stackId, status) => { // --- API Endpoints --- app.get('/api/stacks', async (req, res) => { try { + console.log("📦 Stacks werden von Portainer abgefragt …"); const stacksRes = await axiosInstance.get('/api/stacks'); - + // Filter nach Endpoint-ID const filteredStacks = stacksRes.data.filter(stack => stack.EndpointId === ENDPOINT_ID); - // Deduplication nach Name: nur einmal pro Name + // Deduplication nach Name const uniqueStacksMap = {}; filteredStacks.forEach(stack => { if (!uniqueStacksMap[stack.Name]) { @@ -90,13 +91,17 @@ app.get('/api/stacks', async (req, res) => { stacksWithStatus.sort((a, b) => a.Name.localeCompare(b.Name)); res.json(stacksWithStatus); } catch (err) { + console.error("❌ Fehler beim Laden der Stacks:", err.message); res.status(500).json({ error: err.message }); } }); app.put('/api/stacks/:id/redeploy', async (req, res) => { const { id } = req.params; + const startTime = Date.now(); + try { + console.log(`🟢 Redeploy gestartet für Stack ${id} …`); broadcastRedeployStatus(id, true); const stackRes = await axiosInstance.get(`/api/stacks/${id}`); @@ -108,8 +113,11 @@ app.put('/api/stacks/:id/redeploy', async (req, res) => { } if (stack.Type === 1) { + console.log(`📂 Git-Stack erkannt (${stack.Name}) → Git-Redeploy`); await axiosInstance.put(`/api/stacks/${id}/git/redeploy?endpointId=${stack.EndpointId}`); } else if (stack.Type === 2) { + console.log(`🐳 Docker-Compose-Stack erkannt (${stack.Name}) → Images pullen & neu deployen`); + const fileRes = await axiosInstance.get(`/api/stacks/${id}/file`); const stackFileContent = fileRes.data?.StackFileContent; if (!stackFileContent) throw new Error("Stack file konnte nicht geladen werden"); @@ -119,21 +127,29 @@ app.put('/api/stacks/:id/redeploy', async (req, res) => { const imageName = services[serviceName].image; if (!imageName) continue; try { + console.log(`⬇️ Pulling image: ${imageName}`); await axiosInstance.post( `/api/endpoints/${stack.EndpointId}/docker/images/create?fromImage=${encodeURIComponent(imageName)}` ); - } catch {} + } catch (err) { + console.warn(`⚠️ Konnte Image ${imageName} nicht pullen:`, err.message); + } } - await axiosInstance.put(`/api/stacks/${id}`, + await axiosInstance.put( + `/api/stacks/${id}`, { StackFileContent: stackFileContent, Prune: false, PullImage: true }, { params: { endpointId: stack.EndpointId } } ); } + const duration = ((Date.now() - startTime) / 1000).toFixed(1); + console.log(`✅ Redeploy erfolgreich abgeschlossen für Stack ${id} (${stack.Name}) in ${duration}s`); + broadcastRedeployStatus(id, false); res.json({ success: true, message: 'Stack redeployed' }); } catch (err) { + console.error(`❌ Redeploy-Fehler für Stack ${id}:`, err.message); broadcastRedeployStatus(id, false); res.status(500).json({ error: err.message }); } @@ -141,5 +157,5 @@ app.put('/api/stacks/:id/redeploy', async (req, res) => { // Server starten server.listen(PORT, '0.0.0.0', () => { - console.log(`Backend läuft auf Port ${PORT}`); + console.log(`🚀 Backend läuft auf Port ${PORT}`); }); diff --git a/backend/index.js.bak b/backend/index.js.bak new file mode 100644 index 0000000..1a26dfc --- /dev/null +++ b/backend/index.js.bak @@ -0,0 +1,145 @@ +import express from 'express'; +import dotenv from 'dotenv'; +import https from 'https'; +import axios from 'axios'; +import http from 'http'; +import { Server } from 'socket.io'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +dotenv.config(); + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const app = express(); +app.use(express.json()); + +// Statische Frontend-Dateien ausliefern +app.use(express.static(path.join(__dirname, 'public'))); + +// SPA-Fallback für React-Router +app.get('*', (req, res, next) => { + if (req.path.startsWith('/api')) return next(); + res.sendFile(path.join(__dirname, 'public', 'index.html')); +}); + +// Backend-Port fix +const PORT = 4001; + +// Endpoint-ID aus der env +const ENDPOINT_ID = Number(process.env.PORTAINER_ENDPOINT_ID); + +// HTTPS Agent für Self-Signed-Zertifikate +const agent = new https.Agent({ rejectUnauthorized: false }); + +// Axios-Instance für Portainer +const axiosInstance = axios.create({ + httpsAgent: agent, + headers: { "X-API-Key": process.env.PORTAINER_API_KEY }, + baseURL: process.env.PORTAINER_URL, +}); + +// In-Memory Redeploy-Status +const redeployingStacks = {}; + +// HTTP Server + Socket.IO +const server = http.createServer(app); +const io = new Server(server, { cors: { origin: "*" } }); + +io.on("connection", (socket) => { + console.log("Client verbunden:", socket.id); +}); + +const broadcastRedeployStatus = (stackId, status) => { + redeployingStacks[stackId] = status; + io.emit("redeployStatus", { stackId, status }); +}; + +// --- API Endpoints --- +app.get('/api/stacks', async (req, res) => { + try { + const stacksRes = await axiosInstance.get('/api/stacks'); + + // Filter nach Endpoint-ID + const filteredStacks = stacksRes.data.filter(stack => stack.EndpointId === ENDPOINT_ID); + + // Deduplication nach Name: nur einmal pro Name + const uniqueStacksMap = {}; + filteredStacks.forEach(stack => { + if (!uniqueStacksMap[stack.Name]) { + uniqueStacksMap[stack.Name] = stack; + } + }); + const uniqueStacks = Object.values(uniqueStacksMap); + + const stacksWithStatus = await Promise.all( + uniqueStacks.map(async (stack) => { + try { + const statusRes = await axiosInstance.get( + `/api/stacks/${stack.Id}/images_status?refresh=true` + ); + const statusEmoji = statusRes.data.Status === 'outdated' ? '⚠️' : '✅'; + return { ...stack, updateStatus: statusEmoji, redeploying: redeployingStacks[stack.Id] || false }; + } catch { + return { ...stack, updateStatus: '❌', redeploying: redeployingStacks[stack.Id] || false }; + } + }) + ); + + stacksWithStatus.sort((a, b) => a.Name.localeCompare(b.Name)); + res.json(stacksWithStatus); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +app.put('/api/stacks/:id/redeploy', async (req, res) => { + const { id } = req.params; + try { + broadcastRedeployStatus(id, true); + + const stackRes = await axiosInstance.get(`/api/stacks/${id}`); + const stack = stackRes.data; + + // Prüfen, ob Stack zum konfigurierten Endpoint gehört + if (stack.EndpointId !== ENDPOINT_ID) { + throw new Error(`Stack gehört nicht zum Endpoint ${ENDPOINT_ID}`); + } + + if (stack.Type === 1) { + await axiosInstance.put(`/api/stacks/${id}/git/redeploy?endpointId=${stack.EndpointId}`); + } else if (stack.Type === 2) { + const fileRes = await axiosInstance.get(`/api/stacks/${id}/file`); + const stackFileContent = fileRes.data?.StackFileContent; + if (!stackFileContent) throw new Error("Stack file konnte nicht geladen werden"); + + const services = fileRes.data?.Config?.services || {}; + for (const serviceName in services) { + const imageName = services[serviceName].image; + if (!imageName) continue; + try { + await axiosInstance.post( + `/api/endpoints/${stack.EndpointId}/docker/images/create?fromImage=${encodeURIComponent(imageName)}` + ); + } catch {} + } + + await axiosInstance.put(`/api/stacks/${id}`, + { StackFileContent: stackFileContent, Prune: false, PullImage: true }, + { params: { endpointId: stack.EndpointId } } + ); + } + + broadcastRedeployStatus(id, false); + res.json({ success: true, message: 'Stack redeployed' }); + } catch (err) { + broadcastRedeployStatus(id, false); + res.status(500).json({ error: err.message }); + } +}); + +// Server starten +server.listen(PORT, '0.0.0.0', () => { + console.log(`Backend läuft auf Port ${PORT}`); +}); diff --git a/frontend/src/Stacks.jsx b/frontend/src/Stacks.jsx index 8b7bf27..6c930a4 100644 --- a/frontend/src/Stacks.jsx +++ b/frontend/src/Stacks.jsx @@ -6,35 +6,30 @@ export default function Stacks() { const [stacks, setStacks] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); - // Map: { [stackId]: boolean } - const [redeploying, setRedeploying] = useState({}); + const [redeploying, setRedeploying] = useState({}); // { [stackId]: boolean } // ------------------------- // WebSocket: connect + events // ------------------------- useEffect(() => { - // wenn Frontend und Backend auf verschiedenen Hosts/Ports laufen: - // const socket = io("http://localhost:4001"); - const socket = io(); // gleiche Origin (Express static) -> passt normalerweise - socket.on("connect", () => console.log("Socket connected:", socket.id)); + const socket = io(); // gleiche Origin + socket.on("connect", () => console.log("🔌 Socket connected:", socket.id)); socket.on("redeployStatus", async ({ stackId, status }) => { - // sofort den lokalen map-state setzen (optimistisch) setRedeploying(prev => ({ ...prev, [stackId]: status })); - // Wenn der Redeploy zu false geht, holen wir die aktuellen Stacks (aktualisieren UI) if (!status) { try { const res = await axios.get("/api/stacks"); const sortedStacks = res.data.sort((a, b) => a.Name.localeCompare(b.Name)); setStacks(sortedStacks); - // Map aus API-Daten aufbauen (wichtig für F5 / Konsistenz) + // Map aus API-Daten aufbauen const map = {}; sortedStacks.forEach(s => { map[s.Id] = !!s.redeploying; }); setRedeploying(map); } catch (err) { - console.error("Fehler beim Aktualisieren des Status nach Redeploy:", err); + console.error("Fehler beim Aktualisieren der Stacks:", err); } } }); @@ -46,7 +41,7 @@ export default function Stacks() { }, []); // ------------------------- - // Initiale Stacks laden (auch setzt redeploying map) + // Initiale Stacks laden // ------------------------- const fetchStacks = async () => { setLoading(true); @@ -55,7 +50,7 @@ export default function Stacks() { const sortedStacks = res.data.sort((a, b) => a.Name.localeCompare(b.Name)); setStacks(sortedStacks); - // Wichtig: redeploying-Map aus API-Daten setzen, damit F5 den echten Zustand zeigt + // Map aus API-Daten setzen const map = {}; sortedStacks.forEach(s => { map[s.Id] = !!s.redeploying; }); setRedeploying(map); @@ -75,15 +70,13 @@ export default function Stacks() { // Redeploy Trigger // ------------------------- const handleRedeploy = async (stackId) => { - // sofort UI-Feedback setRedeploying(prev => ({ ...prev, [stackId]: true })); try { await axios.put(`/api/stacks/${stackId}/redeploy`); - // kein sofortiges setRedeploying(false) – Backend sendet das finale Event + // Backend sendet Event → UI wird dann automatisch zurückgesetzt } catch (err) { console.error("❌ Fehler beim Redeploy:", err); - // Fehlerfall: wieder auf false setzen damit UI nicht hängen bleibt setRedeploying(prev => ({ ...prev, [stackId]: false })); } }; @@ -102,9 +95,8 @@ export default function Stacks() { return (
"gedämpfter" Container (sichtbar deaktiviert) className={`flex justify-between items-center p-5 rounded-xl shadow-lg transition - ${isRedeploying ? "bg-gray-700 opacity-60" : "bg-gray-800 hover:bg-gray-700"}`} + ${isRedeploying ? "bg-gray-700 opacity-60 cursor-not-allowed" : "bg-gray-800 hover:bg-gray-700"}`} aria-disabled={isRedeploying} >
@@ -119,7 +111,6 @@ export default function Stacks() {
- {/* Button ist jetzt immer sichtbar */} - )} + {/* Button ist jetzt immer sichtbar */} + ); })} + {stacks.length === 0 &&

Keine Stacks gefunden.

} );