From d49605001e55bbdf7699dcaf9a8fa03ea9df19fc Mon Sep 17 00:00:00 2001 From: root Date: Thu, 25 Sep 2025 09:18:19 +0000 Subject: [PATCH] Update --- backend/index.js | 48 ++++++++--------- backend/index.js.bak | 71 ++++++++++++------------- frontend/src/Stacks.jsx | 63 ++++++++++++----------- frontend/src/Stacks.jsx.bak | 100 +++++++++++++----------------------- frontend/vite.config.js | 24 +++++---- 5 files changed, 139 insertions(+), 167 deletions(-) diff --git a/backend/index.js b/backend/index.js index 1a26dfc..398d5a3 100644 --- a/backend/index.js +++ b/backend/index.js @@ -15,61 +15,49 @@ 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); + console.log(`🔌 [Socket] Client verbunden: ${socket.id}`); }); const broadcastRedeployStatus = (stackId, status) => { redeployingStacks[stackId] = status; io.emit("redeployStatus", { stackId, status }); + console.log(`🔄 [RedeployStatus] Stack ${stackId} ist jetzt ${status ? "im Redeploy" : "fertig"}`); }; // --- API Endpoints --- + app.get('/api/stacks', async (req, res) => { + console.log("ℹ️ [API] GET /api/stacks: Abruf gestartet"); 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; - } + if (!uniqueStacksMap[stack.Name]) uniqueStacksMap[stack.Name] = stack; }); const uniqueStacks = Object.values(uniqueStacksMap); @@ -80,36 +68,46 @@ app.get('/api/stacks', async (req, res) => { `/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: statusEmoji, + redeploying: redeployingStacks[stack.Id] || false + }; + } catch (err) { + console.error(`❌ Fehler beim Abrufen des Status für Stack ${stack.Id}:`, err.message); return { ...stack, updateStatus: '❌', redeploying: redeployingStacks[stack.Id] || false }; } }) ); stacksWithStatus.sort((a, b) => a.Name.localeCompare(b.Name)); + console.log(`✅ GET /api/stacks: Abruf erfolgreich, ${stacksWithStatus.length} Stacks geladen`); res.json(stacksWithStatus); } catch (err) { + console.error(`❌ Fehler beim Abrufen der Stacks:`, err.message); res.status(500).json({ error: err.message }); } }); app.put('/api/stacks/:id/redeploy', async (req, res) => { const { id } = req.params; + console.log(`🔄 PUT /api/stacks/${id}/redeploy: Redeploy gestartet`); + 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) { + console.log(`🔄 [Redeploy] Git Stack "${stack.Name}" (${id}) wird redeployed`); await axiosInstance.put(`/api/stacks/${id}/git/redeploy?endpointId=${stack.EndpointId}`); } else if (stack.Type === 2) { + console.log(`🔄 [Redeploy] Compose Stack "${stack.Name}" (${id}) wird redeployed`); 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,10 +117,13 @@ app.put('/api/stacks/:id/redeploy', async (req, res) => { const imageName = services[serviceName].image; if (!imageName) continue; try { + console.log(`🖼️ Pulling image "${imageName}" für Service "${serviceName}"`); await axiosInstance.post( `/api/endpoints/${stack.EndpointId}/docker/images/create?fromImage=${encodeURIComponent(imageName)}` ); - } catch {} + } catch (err) { + console.error(`❌ Fehler beim Pulling von Image "${imageName}":`, err.message); + } } await axiosInstance.put(`/api/stacks/${id}`, @@ -132,14 +133,15 @@ app.put('/api/stacks/:id/redeploy', async (req, res) => { } broadcastRedeployStatus(id, false); + console.log(`✅ PUT /api/stacks/${id}/redeploy: Redeploy erfolgreich abgeschlossen`); res.json({ success: true, message: 'Stack redeployed' }); } catch (err) { broadcastRedeployStatus(id, false); + console.error(`❌ Fehler beim Redeploy von Stack ${id}:`, err.message); res.status(500).json({ error: err.message }); } }); -// 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 index c9f6f97..ee67420 100644 --- a/backend/index.js.bak +++ b/backend/index.js.bak @@ -15,32 +15,28 @@ const __dirname = path.dirname(__filename); const app = express(); app.use(express.json()); -// Statische Frontend-Dateien ausliefern +// Statische Dateien app.use(express.static(path.join(__dirname, 'public'))); -// SPA-Fallback für React-Router +// SPA-Fallback 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 +// HTTPS Agent const agent = new https.Agent({ rejectUnauthorized: false }); -// Axios-Instance für Portainer +// Axios 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 @@ -48,29 +44,27 @@ const server = http.createServer(app); const io = new Server(server, { cors: { origin: "*" } }); io.on("connection", (socket) => { - console.log("🔌 Client verbunden:", socket.id); + console.log(`🔌 [Socket] Client verbunden: ${socket.id}`); }); const broadcastRedeployStatus = (stackId, status) => { redeployingStacks[stackId] = status; io.emit("redeployStatus", { stackId, status }); + console.log(`🔄 [RedeployStatus] Stack ${stackId} ist jetzt ${status ? "im Redeploy" : "fertig"}`); }; // --- 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 +// Stacks abrufen +app.get('/api/stacks', async (req, res) => { + console.log("ℹ️ [API] GET /api/stacks: Abruf gestartet"); + try { + const stacksRes = await axiosInstance.get('/api/stacks'); const filteredStacks = stacksRes.data.filter(stack => stack.EndpointId === ENDPOINT_ID); - // Deduplication nach Name const uniqueStacksMap = {}; filteredStacks.forEach(stack => { - if (!uniqueStacksMap[stack.Name]) { - uniqueStacksMap[stack.Name] = stack; - } + if (!uniqueStacksMap[stack.Name]) uniqueStacksMap[stack.Name] = stack; }); const uniqueStacks = Object.values(uniqueStacksMap); @@ -81,43 +75,45 @@ app.get('/api/stacks', async (req, res) => { `/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: statusEmoji, + redeploying: redeployingStacks[stack.Id] || false + }; + } catch (err) { + console.error(`❌ [API] Fehler beim Abrufen Status Stack ${stack.Id}:`, err.message); return { ...stack, updateStatus: '❌', redeploying: redeployingStacks[stack.Id] || false }; } }) ); stacksWithStatus.sort((a, b) => a.Name.localeCompare(b.Name)); + console.log(`✅ [API] GET /api/stacks: Abruf erfolgreich, ${stacksWithStatus.length} Stacks geladen`); res.json(stacksWithStatus); } catch (err) { - console.error("❌ Fehler beim Laden der Stacks:", err.message); + console.error(`❌ [API] Fehler beim Abrufen der Stacks:`, err.message); res.status(500).json({ error: err.message }); } }); +// Stack redeployen app.put('/api/stacks/:id/redeploy', async (req, res) => { const { id } = req.params; - const startTime = Date.now(); + console.log(`🔄 [API] PUT /api/stacks/${id}/redeploy: Redeploy gestartet`); try { - console.log(`🟢 Redeploy gestartet für Stack ${id} …`); 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.EndpointId !== ENDPOINT_ID) throw new Error(`Stack gehört nicht zum Endpoint ${ENDPOINT_ID}`); if (stack.Type === 1) { - console.log(`📂 Git-Stack erkannt (${stack.Name}) → Git-Redeploy`); + console.log(`🔄 [Redeploy] Git Stack "${stack.Name}" (${id}) wird redeployed`); 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`); - + console.log(`🔄 [Redeploy] Compose Stack "${stack.Name}" (${id}) wird redeployed`); 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"); @@ -127,35 +123,32 @@ app.put('/api/stacks/:id/redeploy', async (req, res) => { const imageName = services[serviceName].image; if (!imageName) continue; try { - console.log(`⬇️ Pulling image: ${imageName}`); + console.log(`🖼️ [Redeploy] Pulling image "${imageName}" für Service "${serviceName}"`); await axiosInstance.post( `/api/endpoints/${stack.EndpointId}/docker/images/create?fromImage=${encodeURIComponent(imageName)}` ); } catch (err) { - console.warn(`⚠️ Konnte Image ${imageName} nicht pullen:`, err.message); + console.error(`❌ [Redeploy] Fehler beim Pulling von Image "${imageName}":`, 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); + console.log(`✅ [API] PUT /api/stacks/${id}/redeploy: Redeploy erfolgreich abgeschlossen`); res.json({ success: true, message: 'Stack redeployed' }); } catch (err) { - console.error(`❌ Redeploy-Fehler für Stack ${id}:`, err.message); broadcastRedeployStatus(id, false); + console.error(`❌ [API] Fehler beim Redeploy von Stack ${id}:`, err.message); res.status(500).json({ error: err.message }); } }); // Server starten server.listen(PORT, '0.0.0.0', () => { - console.log(`🚀 Backend läuft auf Port ${PORT}`); + console.log(`🚀 [Server] Backend läuft auf Port ${PORT}`); }); diff --git a/frontend/src/Stacks.jsx b/frontend/src/Stacks.jsx index 9560ebd..cc6c353 100644 --- a/frontend/src/Stacks.jsx +++ b/frontend/src/Stacks.jsx @@ -6,37 +6,40 @@ export default function Stacks() { const [stacks, setStacks] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); - const [redeploying, setRedeploying] = useState({}); - // WebSocket initialisieren useEffect(() => { - const socket = io("/"); - console.log("Socket connected"); + const socket = io("/", { transports: ["websocket"] }); + console.log("🔌 Socket connected"); socket.on("redeployStatus", async ({ stackId, status }) => { - setRedeploying(prev => ({ ...prev, [stackId]: status })); + console.log(`🔄 Stack ${stackId} Redeploy Status: ${status ? "running" : "finished"}`); - // Wenn Redeploy beendet, Stack-Status neu laden if (!status) { + // Status nach Redeploy neu vom Server holen try { const res = await axios.get("/api/stacks"); - const sortedStacks = res.data.sort((a, b) => a.Name.localeCompare(b.Name)); - setStacks(sortedStacks); + setStacks(res.data.sort((a, b) => a.Name.localeCompare(b.Name))); } catch (err) { - console.error("Fehler beim Aktualisieren des Status nach Redeploy:", err); + console.error("Fehler beim Aktualisieren nach Redeploy:", err); } + } else { + // UI direkt auf redeploying setzen + setStacks(prev => + prev.map(stack => + stack.Id === stackId ? { ...stack, redeploying: true } : stack + ) + ); } }); return () => socket.disconnect(); }, []); - // Stacks initial laden const fetchStacks = async () => { + setLoading(true); try { const res = await axios.get("/api/stacks"); - const sortedStacks = res.data.sort((a, b) => a.Name.localeCompare(b.Name)); - setStacks(sortedStacks); + setStacks(res.data.map(stack => ({ ...stack, redeploying: stack.redeploying || false }))); } catch (err) { console.error("❌ Fehler beim Abrufen der Stacks:", err); setError("Fehler beim Laden der Stacks"); @@ -49,15 +52,19 @@ export default function Stacks() { fetchStacks(); }, []); - // Redeploy eines Stacks const handleRedeploy = async (stackId) => { - setRedeploying(prev => ({ ...prev, [stackId]: true })); + setStacks(prev => + prev.map(stack => stack.Id === stackId ? { ...stack, redeploying: true } : stack) + ); try { await axios.put(`/api/stacks/${stackId}/redeploy`); + // Socket.IO Event aktualisiert Status automatisch } catch (err) { console.error("❌ Fehler beim Redeploy:", err); - setRedeploying(prev => ({ ...prev, [stackId]: false })); + setStacks(prev => + prev.map(stack => stack.Id === stackId ? { ...stack, redeploying: false } : stack) + ); } }; @@ -67,8 +74,7 @@ export default function Stacks() { return (
{stacks.map(stack => { - const isRedeploying = redeploying[stack.Id] || false; - const isUpToDate = stack.updateStatus === "✅"; + const isRedeploying = stack.redeploying; return (
- {/* Status Indicator */}
-
+ />

{stack.Name}

ID: {stack.Id}

- {!isUpToDate && ( - - )} +
); })} diff --git a/frontend/src/Stacks.jsx.bak b/frontend/src/Stacks.jsx.bak index 8b7bf27..e890c62 100644 --- a/frontend/src/Stacks.jsx.bak +++ b/frontend/src/Stacks.jsx.bak @@ -6,59 +6,36 @@ export default function Stacks() { const [stacks, setStacks] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); - // Map: { [stackId]: boolean } - const [redeploying, setRedeploying] = useState({}); - // ------------------------- - // WebSocket: connect + events - // ------------------------- + // Socket.IO initialisieren (Proxy leitet /socket.io an Backend) 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("/", { transports: ["websocket"] }); + console.log("🔌 Socket connected"); - 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) - const map = {}; - sortedStacks.forEach(s => { map[s.Id] = !!s.redeploying; }); - setRedeploying(map); - } catch (err) { - console.error("Fehler beim Aktualisieren des Status nach Redeploy:", err); - } - } + socket.on("redeployStatus", ({ stackId, status }) => { + console.log(`🔄 Stack ${stackId} Redeploy Status: ${status ? "running" : "finished"}`); + setStacks(prevStacks => + prevStacks.map(stack => + stack.Id === stackId + ? { ...stack, redeploying: status, updateStatus: status ? stack.updateStatus : "✅" } + : stack + ) + ); }); - return () => { - socket.off("redeployStatus"); - socket.disconnect(); - }; + return () => socket.disconnect(); }, []); - // ------------------------- - // Initiale Stacks laden (auch setzt redeploying map) - // ------------------------- + // Stacks initial laden const fetchStacks = async () => { setLoading(true); try { const res = await axios.get("/api/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 - const map = {}; - sortedStacks.forEach(s => { map[s.Id] = !!s.redeploying; }); - setRedeploying(map); + setStacks( + res.data + .sort((a, b) => a.Name.localeCompare(b.Name)) + .map(stack => ({ ...stack, redeploying: stack.redeploying || false })) + ); } catch (err) { console.error("❌ Fehler beim Abrufen der Stacks:", err); setError("Fehler beim Laden der Stacks"); @@ -71,43 +48,40 @@ export default function Stacks() { fetchStacks(); }, []); - // ------------------------- - // Redeploy Trigger - // ------------------------- + // Redeploy starten const handleRedeploy = async (stackId) => { - // sofort UI-Feedback - setRedeploying(prev => ({ ...prev, [stackId]: true })); + // Sofort UI auf redeploying setzen + setStacks(prev => + prev.map(stack => stack.Id === stackId ? { ...stack, redeploying: true } : stack) + ); try { await axios.put(`/api/stacks/${stackId}/redeploy`); - // kein sofortiges setRedeploying(false) – Backend sendet das finale Event + // Backend sendet Socket-Event → UI aktualisiert automatisch } catch (err) { console.error("❌ Fehler beim Redeploy:", err); - // Fehlerfall: wieder auf false setzen damit UI nicht hängen bleibt - setRedeploying(prev => ({ ...prev, [stackId]: false })); + setStacks(prev => + prev.map(stack => stack.Id === stackId ? { ...stack, redeploying: false } : stack) + ); } }; - // ------------------------- - // Render - // ------------------------- if (loading) return

Lade Stacks...

; if (error) return

{error}

; return (
{stacks.map(stack => { - const isRedeploying = Boolean(redeploying[stack.Id]); + const isRedeploying = stack.redeploying; 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"}`} - aria-disabled={isRedeploying} + ${isRedeploying ? "bg-gray-700 cursor-not-allowed" : "bg-gray-800 hover:bg-gray-700"}`} >
+ {/* Status Indicator */}
- {/* Button ist jetzt immer sichtbar */} + {/* Redeploy Button */}
); })} - {stacks.length === 0 &&

Keine Stacks gefunden.

}
); diff --git a/frontend/vite.config.js b/frontend/vite.config.js index aa3ae76..d33ca44 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -1,17 +1,21 @@ -import { defineConfig } from "vite"; -import react from "@vitejs/plugin-react"; +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; export default defineConfig({ plugins: [react()], server: { - host: true, - port: 5173, + host: true, // Hört auf allen Netzwerk-Interfaces, kein localhost notwendig + port: 5173, // optional, Standardport für Vite proxy: { - "/api": { - target: "http://127.0.0.1:4001", // dein Backend - changeOrigin: true, + '/api': { + target: 'http://127.0.0.1:4001', // Backend lokal auf Port 4001 + changeOrigin: true }, - }, - allowedHosts: "all", - }, + '/socket.io': { + target: 'http://127.0.0.1:4001', // WebSocket-Verbindungen über Proxy weiterleiten + ws: true, + changeOrigin: true + } + } + } });