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
-
+
);
}
-
-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 (
-
- {stacks.map((stack) => (
- -
-
-
{stack.updateStatus}
-
-
{stack.Name}
-
ID: {stack.Id}
-
-
-