diff --git a/README.md b/README.md index f7882c6..9f6dd9e 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,9 @@ Ziel: - [x] Projektstruktur mit Frontend & Backend - [x] Lokales Startskript (`scripts/start-dev.sh`) -- [ ] Frontend zeigt Stacks an (über Backend) -- [ ] API-Verbindung zu Portainer +- [x] Frontend zeigt Stacks an (über Backend) +- [x] API-Verbindung zu Portainer +- [x] Stack Redeploy - [ ] Docker-Image bauen & per Compose deployen --- diff --git a/backend/index.js b/backend/index.js index ab992b8..a0f0b65 100644 --- a/backend/index.js +++ b/backend/index.js @@ -1,6 +1,9 @@ import express from 'express'; -import axios from 'axios'; import dotenv from 'dotenv'; +import https from 'https'; +import axios from 'axios'; +import http from 'http'; +import { Server } from 'socket.io'; dotenv.config(); @@ -8,38 +11,68 @@ const app = express(); app.use(express.json()); const PORT = process.env.PORT || 4000; -const PORTAINER_URL = process.env.PORTAINER_URL; -const PORTAINER_API_KEY = process.env.PORTAINER_API_KEY; -console.log("PORTAINER_API_KEY:", PORTAINER_API_KEY); +// HTTPS Agent für Self-Signed-Zertifikate +const agent = new https.Agent({ rejectUnauthorized: false }); + +// Axios-Instance für alle Portainer-Requests +const axiosInstance = axios.create({ + httpsAgent: agent, + headers: { "X-API-Key": process.env.PORTAINER_API_KEY }, + baseURL: process.env.PORTAINER_URL, +}); + +// In-Memory Store für Redeploy-Status +const redeployingStacks = {}; // { [stackId]: true/false } + +// HTTP Server + Socket.IO +const server = http.createServer(app); +const io = new Server(server, { + cors: { origin: "*" } // ggf. auf Frontend-URL anpassen +}); + +// Socket.IO Verbindung +io.on("connection", (socket) => { + console.log("Client verbunden:", socket.id); +}); + +// Hilfsfunktion zum Broadcasten des Redeploy-Status +const broadcastRedeployStatus = (stackId, status) => { + redeployingStacks[stackId] = status; + io.emit("redeployStatus", { stackId, status }); + console.log(`Stack ${stackId} redeploying: ${status}`); +}; // Root-Endpoint app.get('/', (req, res) => { + console.log("Root Endpoint aufgerufen"); res.send('StackPulse Backend läuft. Nutze /api/stacks für die Daten.'); }); -// Alle Stacks abrufen und Status prüfen +// Alle Stacks abrufen app.get('/api/stacks', async (req, res) => { try { - const stacksRes = await axios.get(`${PORTAINER_URL}/api/stacks`, { - headers: { 'X-API-Key': PORTAINER_API_KEY } - }); + const stacksRes = await axiosInstance.get('/api/stacks'); const stacksWithStatus = await Promise.all( stacksRes.data.map(async (stack) => { try { - const statusRes = await axios.get( - `${PORTAINER_URL}/api/stacks/${stack.Id}/images_status?refresh=true`, - { headers: { 'X-API-Key': PORTAINER_API_KEY } } - ); + const statusRes = await axiosInstance.get(`/api/stacks/${stack.Id}/images_status?refresh=true`); + let statusEmoji = '✅'; // up-to-date + if (statusRes.data.Status === 'outdated') statusEmoji = '⚠️'; // outdated - let statusEmoji = '✅'; - if (statusRes.data.Status === 'outdated') statusEmoji = '⚠️'; - - return { ...stack, updateStatus: statusEmoji }; + return { + ...stack, + updateStatus: statusEmoji, + redeploying: redeployingStacks[stack.Id] || false, + }; } catch (err) { console.error(`Fehler beim Abrufen Remote Digest für Stack ${stack.Id}:`, err.message); - return { ...stack, updateStatus: '❌' }; + return { + ...stack, + updateStatus: '❌', // Fehler + redeploying: redeployingStacks[stack.Id] || false, + }; } }) ); @@ -53,69 +86,59 @@ app.get('/api/stacks', async (req, res) => { } }); -// Redeploy eines Stacks (alle Typen) +// Redeploy eines Stacks app.put('/api/stacks/:id/redeploy', async (req, res) => { const { id } = req.params; try { - const stackRes = await axios.get(`${PORTAINER_URL}/api/stacks/${id}`, { - headers: { 'X-API-Key': PORTAINER_API_KEY } - }); + // Status auf "redeploying" setzen & an alle Clients senden + broadcastRedeployStatus(id, true); + + const stackRes = await axiosInstance.get(`/api/stacks/${id}`); const stack = stackRes.data; if (stack.Type === 1) { - await axios.put( - `${PORTAINER_URL}/api/stacks/${id}/git/redeploy?endpointId=${stack.EndpointId}`, - {}, - { headers: { 'X-API-Key': PORTAINER_API_KEY } } - ); - return res.json({ success: true, message: 'Git Stack redeployed' }); - + await axiosInstance.put(`/api/stacks/${id}/git/redeploy?endpointId=${stack.EndpointId}`); } else if (stack.Type === 2) { - const fileRes = await axios.get(`${PORTAINER_URL}/api/stacks/${id}/file`, { - headers: { 'X-API-Key': PORTAINER_API_KEY } - }); - + const fileRes = await axiosInstance.get(`/api/stacks/${id}/file`); const stackFileContent = fileRes.data?.StackFileContent; - if (!stackFileContent) return res.status(500).json({ error: 'Stack file konnte nicht geladen werden' }); + + 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 axios.post( - `${PORTAINER_URL}/api/endpoints/${stack.EndpointId}/docker/images/create?fromImage=${encodeURIComponent(imageName)}`, - {}, - { headers: { 'X-API-Key': PORTAINER_API_KEY } } + await axiosInstance.post( + `/api/endpoints/${stack.EndpointId}/docker/images/create?fromImage=${encodeURIComponent(imageName)}` ); } catch (pullErr) { console.error(`Fehler beim Pull von ${imageName}:`, pullErr.message); } } - await axios.put( - `${PORTAINER_URL}/api/stacks/${id}?endpointId=${stack.EndpointId}`, - { - StackFileContent: stackFileContent, - Prune: false, - PullImage: true - }, - { headers: { 'X-API-Key': PORTAINER_API_KEY } } + await axiosInstance.put( + `/api/stacks/${id}`, + { StackFileContent: stackFileContent, Prune: false, PullImage: true }, + { params: { endpointId: stack.EndpointId } } ); - - return res.json({ success: true, message: 'Compose/Local Stack redeployed' }); - - } else { - return res.status(400).json({ error: 'Unbekannter oder noch nicht unterstützter Stack-Typ' }); } + + // Redeploy beendet, Status an alle Clients senden + broadcastRedeployStatus(id, false); + + res.json({ success: true, message: 'Stack redeployed' }); } catch (err) { - console.error('Redeploy fehlgeschlagen:', err.response?.data || err.message); - res.status(err.response?.status || 500).json({ success: false, error: err.message }); + // Fehler → Status zurücksetzen & an Clients senden + broadcastRedeployStatus(id, false); + console.error(`Fehler beim Redeploy von Stack ${id}:`, err.message); + if (err.response) res.status(err.response.status).json(err.response.data); + else res.status(500).json({ error: err.message }); } }); - -app.listen(PORT, '0.0.0.0', () => { - console.log(`Backend läuft auf http://0.0.0.0:${PORT}`); +// Server starten +server.listen(PORT, '0.0.0.0', () => { + console.log(`Backend läuft auf Port ${PORT}`); }); diff --git a/backend/index.js.bak b/backend/index.js.bak deleted file mode 100644 index cc8fa82..0000000 --- a/backend/index.js.bak +++ /dev/null @@ -1,33 +0,0 @@ -import express from 'express'; -import axios from 'axios'; -import dotenv from 'dotenv'; - -dotenv.config(); - -const app = express(); -const PORT = process.env.PORT || 4000; -const PORTAINER_URL = process.env.PORTAINER_URL; -const PORTAINER_API_KEY = process.env.PORTAINER_API_KEY; -console.log("PORTAINER_API_KEY:", PORTAINER_API_KEY); - -// Root-Endpoint -app.get('/', (req, res) => { - res.send('StackPulse Backend läuft. Nutze /stacks für die Daten.'); -}); - -app.get('/api/stacks', async (req, res) => { - try { - const url = `${PORTAINER_URL}/api/stacks`; - console.log("Request URL:", url); - const response = await axios.get(url,{headers: { 'X-API-Key': PORTAINER_API_KEY} - }); - res.json(response.data); - } catch (err) { - console.error('Fehler:', err.message); if (err.response) { console.error('Status:', err.response.status); console.error('Data:', err.response.data); res.status(err.response.status).json(err.response.data); - } else { - res.status(500).json({ error: err.message }); - } - } -}); -app.listen(PORT, '0.0.0.0', () => { console.log(`Backend läuft auf http://0.0.0.0:${PORT}`); -}); diff --git a/backend/index.js.save b/backend/index.js.save deleted file mode 100644 index 5f6ed18..0000000 --- a/backend/index.js.save +++ /dev/null @@ -1,23 +0,0 @@ -import express from 'express'; -import axios from 'axios'; -import dotenv from 'dotenv'; - -dotenv.config(); - -const app = express(); -const PORT = process.env.PORT || 4000; // <- Port geändert -const PORTAINER_URL = process.env.PORTAINER_URL; -const PORTAINER_TOKEN = process.env.PORTAINER_TOKEN; - -app.get('/stacks', async (req, res) => { - try { const response = await axios.get(`${PORTAINER_URL}/stacks`, { headers: { - 'Authorization': `Bearer ${PORTAINER_TOKEN}` } - }); - res.json(response.data); - } catch (err) { - console.error(err); res.status(500).json({ error: 'Fehler beim Abrufen der Stacks' }); - } -}); -app.listen(PORT, '0.0.0.0', () => { console.log(`Backend läuft auf - http://0.0.0.0:${PORT}`); -}); diff --git a/backend/package-lock.json b/backend/package-lock.json index dd9cb28..6008df4 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -11,7 +11,8 @@ "axios": "^1.7.0", "dockerode": "^4.0.7", "dotenv": "^16.0.0", - "express": "^4.18.0" + "express": "^4.18.0", + "socket.io": "^4.8.1" } }, "node_modules/@balena/dockerignore": { @@ -111,6 +112,19 @@ "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==" + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/node": { "version": "24.3.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz", @@ -200,6 +214,14 @@ } ] }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, "node_modules/bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", @@ -385,6 +407,18 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/cpu-features": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz", @@ -534,6 +568,62 @@ "once": "^1.4.0" } }, + "node_modules/engine.io": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz", + "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -973,6 +1063,14 @@ "node": ">= 0.6" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -1269,6 +1367,107 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/socket.io": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", + "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.17.1" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-adapter/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, "node_modules/split-ca": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz", @@ -1449,6 +1648,26 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/backend/package.json b/backend/package.json index e00997c..959770e 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1 +1 @@ -{"name":"backend","version":"1.0.0","type":"module","scripts":{"start":"node index.js"},"dependencies":{"axios":"^1.7.0","dockerode":"^4.0.7","dotenv":"^16.0.0","express":"^4.18.0"}} \ No newline at end of file +{"name":"backend","version":"1.0.0","type":"module","scripts":{"start":"node index.js"},"dependencies":{"axios":"^1.7.0","dockerode":"^4.0.7","dotenv":"^16.0.0","express":"^4.18.0","socket.io":"^4.8.1"}} \ No newline at end of file diff --git a/frontend/index.html b/frontend/index.html index 6591fa7..bf8396c 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,6 +4,7 @@ StackPulse +
diff --git a/frontend/index.html.bak b/frontend/index.html.bak new file mode 100644 index 0000000..6591fa7 --- /dev/null +++ b/frontend/index.html.bak @@ -0,0 +1,12 @@ + + + + + + StackPulse + + +
+ + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 407a0d9..c8f96c0 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -10,7 +10,8 @@ "dependencies": { "axios": "^1.7.5", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "socket.io-client": "^4.8.1" }, "devDependencies": { "@vitejs/plugin-react": "^4.0.0", @@ -763,6 +764,11 @@ "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", "dev": true }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1211,6 +1217,42 @@ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "dev": true }, + "node_modules/engine.io-client": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz", + "integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-client/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -1794,8 +1836,7 @@ "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/mz": { "version": "2.7.0", @@ -2287,6 +2328,64 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/socket.io-client": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz", + "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-client/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -2699,6 +2798,34 @@ "node": ">=8" } }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index eee30fb..75934ec 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,7 +9,8 @@ "dependencies": { "axios": "^1.7.5", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "socket.io-client": "^4.8.1" }, "devDependencies": { "@vitejs/plugin-react": "^4.0.0", diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 272839b..3dd713a 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,13 +1,16 @@ import React from "react"; -import Stacks from "./Stacks"; +import Stacks from "./Stacks.jsx"; -function App() { +export default function App() { return ( -
-

StackPulse

- +
+
+

StackPulse

+

Verwalte deine Docker Stacks

+
+
+ +
); } - -export default App; diff --git a/frontend/src/App.jsx.bak b/frontend/src/App.jsx.bak new file mode 100644 index 0000000..4484844 --- /dev/null +++ b/frontend/src/App.jsx.bak @@ -0,0 +1,11 @@ +import React from "react"; +import Stacks from "./Stacks.jsx"; + +export default function App() { + return ( +
+

StackPulse

+ +
+ ); +} diff --git a/frontend/src/Stacks.jsx b/frontend/src/Stacks.jsx index d57d35e..9560ebd 100644 --- a/frontend/src/Stacks.jsx +++ b/frontend/src/Stacks.jsx @@ -1,11 +1,37 @@ import React, { useEffect, useState } from "react"; import axios from "axios"; +import { io } from "socket.io-client"; 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"); + + socket.on("redeployStatus", async ({ stackId, status }) => { + setRedeploying(prev => ({ ...prev, [stackId]: status })); + + // Wenn Redeploy beendet, Stack-Status neu laden + if (!status) { + try { + const res = await axios.get("/api/stacks"); + const sortedStacks = res.data.sort((a, b) => a.Name.localeCompare(b.Name)); + setStacks(sortedStacks); + } catch (err) { + console.error("Fehler beim Aktualisieren des Status nach Redeploy:", err); + } + } + }); + + return () => socket.disconnect(); + }, []); + + // Stacks initial laden const fetchStacks = async () => { try { const res = await axios.get("/api/stacks"); @@ -23,50 +49,61 @@ export default function Stacks() { fetchStacks(); }, []); + // Redeploy eines Stacks const handleRedeploy = async (stackId) => { - try { - const confirmRedeploy = window.confirm("Willst du den Stack wirklich redeployen?"); - if (!confirmRedeploy) return; + setRedeploying(prev => ({ ...prev, [stackId]: true })); - const res = await axios.put(`/api/stacks/${stackId}/redeploy`); - if (res.data.success) { - alert("✅ Stack erfolgreich redeployed!"); - fetchStacks(); // Optional: Stacks neu laden - } else { - alert("⚠️ Redeploy fehlgeschlagen: " + res.data.error); - } + try { + await axios.put(`/api/stacks/${stackId}/redeploy`); } catch (err) { console.error("❌ Fehler beim Redeploy:", err); - alert("⚠️ Redeploy fehlgeschlagen: " + err.message); + setRedeploying(prev => ({ ...prev, [stackId]: false })); } }; - if (loading) return

Lade Stacks...

; - if (error) return

{error}

; + if (loading) return

Lade Stacks...

; + if (error) return

{error}

; return ( - +
+ {/* Status Indicator */} +
+
+
+

{stack.Name}

+

ID: {stack.Id}

+
+
+ + {!isUpToDate && ( + + )} +
+ ); + })} + {stacks.length === 0 &&

Keine Stacks gefunden.

} + ); } diff --git a/frontend/src/Stacks.jsx.bak b/frontend/src/Stacks.jsx.bak index 0d00787..ddd32be 100644 --- a/frontend/src/Stacks.jsx.bak +++ b/frontend/src/Stacks.jsx.bak @@ -1,47 +1,111 @@ import React, { useEffect, useState } from "react"; import axios from "axios"; +import { io } from "socket.io-client"; export default function Stacks() { const [stacks, setStacks] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); + const [redeploying, setRedeploying] = useState({}); // { stackId: true/false } + + // WebSocket initialisieren + useEffect(() => { + const socket = io("/"); + console.log("Socket connected"); + + socket.on("redeployStatus", async ({ stackId, status }) => { + setRedeploying(prev => ({ ...prev, [stackId]: status })); + + // Wenn Redeploy beendet, Stack-Status neu laden + if (!status) { + try { + const res = await axios.get("/api/stacks"); + const sortedStacks = res.data.sort((a, b) => a.Name.localeCompare(b.Name)); + setStacks(sortedStacks); + } catch (err) { + console.error("Fehler beim Aktualisieren des Status nach Redeploy:", err); + } + } + }); + + return () => socket.disconnect(); + }, []); + + // Stacks initial laden + const fetchStacks = async () => { + try { + const res = await axios.get("/api/stacks"); + const sortedStacks = res.data.sort((a, b) => a.Name.localeCompare(b.Name)); + setStacks(sortedStacks); + } catch (err) { + console.error("❌ Fehler beim Abrufen der Stacks:", err); + setError("Fehler beim Laden der Stacks"); + } finally { + setLoading(false); + } + }; useEffect(() => { - const fetchStacks = async () => { - try { - const res = await axios.get("/api/stacks"); // Vite-Proxy auf Backend - const sortedStacks = res.data.sort((a, b) => a.Name.localeCompare(b.Name)); - setStacks(sortedStacks); - } catch (err) { - console.error("❌ Fehler beim Abrufen der Stacks:", err); - setError("Fehler beim Laden der Stacks"); - } finally { - setLoading(false); - } - }; - fetchStacks(); }, []); + // Redeploy eines Stacks + const handleRedeploy = async (stackId) => { + // Button sofort auf Redeploying setzen + setRedeploying(prev => ({ ...prev, [stackId]: true })); + + try { + await axios.put(`/api/stacks/${stackId}/redeploy`); + } catch (err) { + console.error("❌ Fehler beim Redeploy:", err); + } + + // Stack-Status immer neu laden + try { + const res = await axios.get("/api/stacks"); + const sortedStacks = res.data.sort((a, b) => a.Name.localeCompare(b.Name)); + setStacks(sortedStacks); + } catch (err) { + console.error("Fehler beim Aktualisieren nach Redeploy:", err); + } finally { + // Erst nach Datenaktualisierung Button zurücksetzen + setRedeploying(prev => ({ ...prev, [stackId]: false })); + } + }; + if (loading) return

Lade Stacks...

; if (error) return

{error}

; return ( ); diff --git a/frontend/src/Stacks.jsx.bak_working b/frontend/src/Stacks.jsx.bak_working new file mode 100644 index 0000000..ddd32be --- /dev/null +++ b/frontend/src/Stacks.jsx.bak_working @@ -0,0 +1,112 @@ +import React, { useEffect, useState } from "react"; +import axios from "axios"; +import { io } from "socket.io-client"; + +export default function Stacks() { + const [stacks, setStacks] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(""); + const [redeploying, setRedeploying] = useState({}); // { stackId: true/false } + + // WebSocket initialisieren + useEffect(() => { + const socket = io("/"); + console.log("Socket connected"); + + socket.on("redeployStatus", async ({ stackId, status }) => { + setRedeploying(prev => ({ ...prev, [stackId]: status })); + + // Wenn Redeploy beendet, Stack-Status neu laden + if (!status) { + try { + const res = await axios.get("/api/stacks"); + const sortedStacks = res.data.sort((a, b) => a.Name.localeCompare(b.Name)); + setStacks(sortedStacks); + } catch (err) { + console.error("Fehler beim Aktualisieren des Status nach Redeploy:", err); + } + } + }); + + return () => socket.disconnect(); + }, []); + + // Stacks initial laden + const fetchStacks = async () => { + try { + const res = await axios.get("/api/stacks"); + const sortedStacks = res.data.sort((a, b) => a.Name.localeCompare(b.Name)); + setStacks(sortedStacks); + } catch (err) { + console.error("❌ Fehler beim Abrufen der Stacks:", err); + setError("Fehler beim Laden der Stacks"); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchStacks(); + }, []); + + // Redeploy eines Stacks + const handleRedeploy = async (stackId) => { + // Button sofort auf Redeploying setzen + setRedeploying(prev => ({ ...prev, [stackId]: true })); + + try { + await axios.put(`/api/stacks/${stackId}/redeploy`); + } catch (err) { + console.error("❌ Fehler beim Redeploy:", err); + } + + // Stack-Status immer neu laden + try { + const res = await axios.get("/api/stacks"); + const sortedStacks = res.data.sort((a, b) => a.Name.localeCompare(b.Name)); + setStacks(sortedStacks); + } catch (err) { + console.error("Fehler beim Aktualisieren nach Redeploy:", err); + } finally { + // Erst nach Datenaktualisierung Button zurücksetzen + setRedeploying(prev => ({ ...prev, [stackId]: false })); + } + }; + + if (loading) return

Lade Stacks...

; + if (error) return

{error}

; + + return ( + + ); +} diff --git a/frontend/src/index.css b/frontend/src/index.css index b5c61c9..78bbd55 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1,3 +1,18 @@ @tailwind base; @tailwind components; @tailwind utilities; + +/* Custom tweaks für GitHub-Next-Style */ +body { + font-family: 'Inter', sans-serif; + color: white; +} + +button { + outline: none; + border: none; +} + +button:disabled { + opacity: 0.8; +} diff --git a/frontend/src/index.css.bak b/frontend/src/index.css.bak new file mode 100644 index 0000000..b5c61c9 --- /dev/null +++ b/frontend/src/index.css.bak @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx index 4bfdbd6..b194025 100644 --- a/frontend/src/main.jsx +++ b/frontend/src/main.jsx @@ -1,7 +1,7 @@ import React from "react"; import ReactDOM from "react-dom/client"; import App from "./App.jsx"; -import './index.css' // <-- Tailwind CSS hier einbinden +import './index.css'; // Tailwind CSS ReactDOM.createRoot(document.getElementById("root")).render( diff --git a/frontend/src/main.jsx.bak b/frontend/src/main.jsx.bak new file mode 100644 index 0000000..b194025 --- /dev/null +++ b/frontend/src/main.jsx.bak @@ -0,0 +1,10 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import App from "./App.jsx"; +import './index.css'; // Tailwind CSS + +ReactDOM.createRoot(document.getElementById("root")).render( + + + +); diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index 3718949..d09d153 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -1,10 +1,13 @@ +/** @type {import('tailwindcss').Config} */ export default { content: [ "./index.html", - "./src/**/*.{js,ts,jsx,tsx}", + "./src/**/*.{js,jsx,ts,tsx}", ], theme: { extend: {}, }, + darkMode: 'class', // Aktiviert Dark Mode via class="dark" plugins: [], } + diff --git a/frontend/tailwind.config.js.bak b/frontend/tailwind.config.js.bak new file mode 100644 index 0000000..3718949 --- /dev/null +++ b/frontend/tailwind.config.js.bak @@ -0,0 +1,10 @@ +export default { + content: [ + "./index.html", + "./src/**/*.{js,ts,jsx,tsx}", + ], + theme: { + extend: {}, + }, + plugins: [], +} diff --git a/frontend/vite.config.js b/frontend/vite.config.js index 2f36c57..bc8b97d 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -8,10 +8,15 @@ export default defineConfig({ port: 5173, proxy: { "/api": { - target: "http://localhost:4000", // dein Backend + target: "http://127.0.0.1:3300", // dein Backend changeOrigin: true, }, }, + allowedHosts: [ + "10.10.10.23", // dein Dev-Rechner + "stackpulse.d-razz.de", // der Host, den du brauchst + "localhost", + ], }, }); diff --git a/index.js.bak b/index.js.bak new file mode 100644 index 0000000..a0f0b65 --- /dev/null +++ b/index.js.bak @@ -0,0 +1,144 @@ +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'; + +dotenv.config(); + +const app = express(); +app.use(express.json()); + +const PORT = process.env.PORT || 4000; + +// HTTPS Agent für Self-Signed-Zertifikate +const agent = new https.Agent({ rejectUnauthorized: false }); + +// Axios-Instance für alle Portainer-Requests +const axiosInstance = axios.create({ + httpsAgent: agent, + headers: { "X-API-Key": process.env.PORTAINER_API_KEY }, + baseURL: process.env.PORTAINER_URL, +}); + +// In-Memory Store für Redeploy-Status +const redeployingStacks = {}; // { [stackId]: true/false } + +// HTTP Server + Socket.IO +const server = http.createServer(app); +const io = new Server(server, { + cors: { origin: "*" } // ggf. auf Frontend-URL anpassen +}); + +// Socket.IO Verbindung +io.on("connection", (socket) => { + console.log("Client verbunden:", socket.id); +}); + +// Hilfsfunktion zum Broadcasten des Redeploy-Status +const broadcastRedeployStatus = (stackId, status) => { + redeployingStacks[stackId] = status; + io.emit("redeployStatus", { stackId, status }); + console.log(`Stack ${stackId} redeploying: ${status}`); +}; + +// Root-Endpoint +app.get('/', (req, res) => { + console.log("Root Endpoint aufgerufen"); + res.send('StackPulse Backend läuft. Nutze /api/stacks für die Daten.'); +}); + +// Alle Stacks abrufen +app.get('/api/stacks', async (req, res) => { + try { + const stacksRes = await axiosInstance.get('/api/stacks'); + + const stacksWithStatus = await Promise.all( + stacksRes.data.map(async (stack) => { + try { + const statusRes = await axiosInstance.get(`/api/stacks/${stack.Id}/images_status?refresh=true`); + let statusEmoji = '✅'; // up-to-date + if (statusRes.data.Status === 'outdated') statusEmoji = '⚠️'; // outdated + + return { + ...stack, + updateStatus: statusEmoji, + redeploying: redeployingStacks[stack.Id] || false, + }; + } catch (err) { + console.error(`Fehler beim Abrufen Remote Digest für Stack ${stack.Id}:`, err.message); + return { + ...stack, + updateStatus: '❌', // Fehler + redeploying: redeployingStacks[stack.Id] || false, + }; + } + }) + ); + + stacksWithStatus.sort((a, b) => a.Name.localeCompare(b.Name)); + res.json(stacksWithStatus); + } catch (err) { + console.error('Fehler beim Abrufen der Stacks:', err.message); + if (err.response) res.status(err.response.status).json(err.response.data); + else res.status(500).json({ error: err.message }); + } +}); + +// Redeploy eines Stacks +app.put('/api/stacks/:id/redeploy', async (req, res) => { + const { id } = req.params; + + try { + // Status auf "redeploying" setzen & an alle Clients senden + broadcastRedeployStatus(id, true); + + const stackRes = await axiosInstance.get(`/api/stacks/${id}`); + const stack = stackRes.data; + + 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 (pullErr) { + console.error(`Fehler beim Pull von ${imageName}:`, pullErr.message); + } + } + + await axiosInstance.put( + `/api/stacks/${id}`, + { StackFileContent: stackFileContent, Prune: false, PullImage: true }, + { params: { endpointId: stack.EndpointId } } + ); + } + + // Redeploy beendet, Status an alle Clients senden + broadcastRedeployStatus(id, false); + + res.json({ success: true, message: 'Stack redeployed' }); + } catch (err) { + // Fehler → Status zurücksetzen & an Clients senden + broadcastRedeployStatus(id, false); + console.error(`Fehler beim Redeploy von Stack ${id}:`, err.message); + if (err.response) res.status(err.response.status).json(err.response.data); + else 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/scripts/start-dev.sh b/scripts/start-dev.sh index 0694270..9e4fe42 100755 --- a/scripts/start-dev.sh +++ b/scripts/start-dev.sh @@ -18,7 +18,7 @@ cd .. echo "" echo "✅ StackPulse läuft lokal:" echo "Frontend: http://localhost:5173" -echo "Backend: http://localhost:4000" +echo "Backend: http://localhost:3300" echo "Beenden mit STRG+C" wait $BACK_PID $FRONT_PID