From 9db80682067c15c69643874193c8e0317b759c9b Mon Sep 17 00:00:00 2001 From: root Date: Wed, 24 Sep 2025 13:25:25 +0000 Subject: [PATCH 1/8] Final commit --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index deed335..01f3642 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules/ dist/ .env +scripts/docker-release.sh From 1b9fa894056b233b85ac75a96c3c4d3d431fd793 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 24 Sep 2025 15:05:45 +0000 Subject: [PATCH 2/8] Update --- scripts/switch-branch.sh | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/scripts/switch-branch.sh b/scripts/switch-branch.sh index c025b64..0249a19 100755 --- a/scripts/switch-branch.sh +++ b/scripts/switch-branch.sh @@ -1,5 +1,19 @@ #!/bin/bash +# =============================== +# Dateien sichern, die nicht gepusht werden +# =============================== +UNVERSIONED_FILES=("scripts/docker-release.sh") +for f in "${UNVERSIONED_FILES[@]}"; do + if [[ -f $f ]]; then + mkdir -p /tmp/git_safe_backup + cp "$f" "/tmp/git_safe_backup/$(basename "$f")" + fi +done + +# =============================== +# Alle Branches sammeln +# =============================== # Alle lokalen Branches holen, führende Sternchen und Leerzeichen entfernen LOCAL_BRANCHES=$(git branch | sed 's/* //' | sed 's/^[[:space:]]*//') @@ -58,4 +72,16 @@ fi git reset --hard origin/$SELECTED_BRANCH git clean -fd +# =============================== +# Gesicherte unversionierte Dateien zurückkopieren +# =============================== +for f in "${UNVERSIONED_FILES[@]}"; do + if [[ -f "/tmp/git_safe_backup/$(basename "$f")" ]]; then + mkdir -p "$(dirname "$f")" + mv "/tmp/git_safe_backup/$(basename "$f")" "$f" + fi +done +rm -rf /tmp/git_safe_backup + echo "Branch '$SELECTED_BRANCH' ist nun aktiv. Arbeitsverzeichnis entspricht exakt dem Remote-Stand." +echo "Unversionierte Dateien wurden wiederhergestellt." From 2008ccc7338d502459af6fb84d22ee41c6fd873a Mon Sep 17 00:00:00 2001 From: root Date: Wed, 24 Sep 2025 15:09:02 +0000 Subject: [PATCH 3/8] Update --- .dockerignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.dockerignore b/.dockerignore index 00fa812..2691cf2 100644 --- a/.dockerignore +++ b/.dockerignore @@ -8,6 +8,7 @@ npm-debug.log .idea frontend/build frontend/dist +backend/.env backend/node_modules frontend/node_modules From 6cf882da14d42f58d7ff33cac0d07df2c3f9b5a4 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 24 Sep 2025 15:46:21 +0000 Subject: [PATCH 4/8] Update --- backend/index.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/backend/index.js b/backend/index.js index dcc837e..1a26dfc 100644 --- a/backend/index.js +++ b/backend/index.js @@ -64,8 +64,17 @@ app.get('/api/stacks', async (req, res) => { // 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( - filteredStacks.map(async (stack) => { + uniqueStacks.map(async (stack) => { try { const statusRes = await axiosInstance.get( `/api/stacks/${stack.Id}/images_status?refresh=true` From cb205f09d20485c54e042a2095431fde809dcd01 Mon Sep 17 00:00:00 2001 From: Mboehmlaender <41646409+Mboehmlaender@users.noreply.github.com> Date: Thu, 25 Sep 2025 11:26:06 +0200 Subject: [PATCH 5/8] Update index.js --- backend/index.js | 48 +++++++++++++++++++++++++----------------------- 1 file changed, 25 insertions(+), 23 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}`); }); From 126daea89c66e73dcad0d792e9034a3f62ef96d2 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 25 Sep 2025 09:51:18 +0000 Subject: [PATCH 6/8] Update --- .env.example | 4 -- .gitignore | 2 +- backend/.env.example | 4 ++ backend/index.js.bak | 154 ------------------------------------------- 4 files changed, 5 insertions(+), 159 deletions(-) delete mode 100644 .env.example create mode 100644 backend/.env.example delete mode 100644 backend/index.js.bak diff --git a/.env.example b/.env.example deleted file mode 100644 index 1c11102..0000000 --- a/.env.example +++ /dev/null @@ -1,4 +0,0 @@ -PORT=4000 -NODE_ENV=production -PORTAINER_URL=https://your-portainer.example.com -PORTAINER_API_KEY=your_api_key_here diff --git a/.gitignore b/.gitignore index deed335..1ae8931 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ node_modules/ dist/ -.env +backend/.env diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..89d93ec --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,4 @@ +PORTAINER_URL=your_portainer_url_here +PORTAINER_API_KEY=your_api_key_here +PORTAINER_ENDPOINT_ID=Enpoint_ID_from_Portainer + diff --git a/backend/index.js.bak b/backend/index.js.bak deleted file mode 100644 index ee67420..0000000 --- a/backend/index.js.bak +++ /dev/null @@ -1,154 +0,0 @@ -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 Dateien -app.use(express.static(path.join(__dirname, 'public'))); - -// SPA-Fallback -app.get('*', (req, res, next) => { - if (req.path.startsWith('/api')) return next(); - res.sendFile(path.join(__dirname, 'public', 'index.html')); -}); - -const PORT = 4001; -const ENDPOINT_ID = Number(process.env.PORTAINER_ENDPOINT_ID); - -// HTTPS Agent -const agent = new https.Agent({ rejectUnauthorized: false }); - -// Axios für Portainer -const axiosInstance = axios.create({ - httpsAgent: agent, - headers: { "X-API-Key": process.env.PORTAINER_API_KEY }, - baseURL: process.env.PORTAINER_URL, -}); - -const redeployingStacks = {}; - -// HTTP Server + Socket.IO -const server = http.createServer(app); -const io = new Server(server, { cors: { origin: "*" } }); - -io.on("connection", (socket) => { - 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 --- - -// 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); - - 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 (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(`❌ [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; - console.log(`🔄 [API] PUT /api/stacks/${id}/redeploy: Redeploy gestartet`); - - try { - broadcastRedeployStatus(id, true); - - const stackRes = await axiosInstance.get(`/api/stacks/${id}`); - const stack = stackRes.data; - - 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"); - - const services = fileRes.data?.Config?.services || {}; - for (const serviceName in services) { - const imageName = services[serviceName].image; - if (!imageName) continue; - try { - 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.error(`❌ [Redeploy] Fehler beim Pulling von Image "${imageName}":`, err.message); - } - } - - await axiosInstance.put(`/api/stacks/${id}`, - { StackFileContent: stackFileContent, Prune: false, PullImage: true }, - { params: { endpointId: stack.EndpointId } } - ); - } - - broadcastRedeployStatus(id, false); - console.log(`✅ [API] PUT /api/stacks/${id}/redeploy: Redeploy erfolgreich abgeschlossen`); - res.json({ success: true, message: 'Stack redeployed' }); - } catch (err) { - 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(`🚀 [Server] Backend läuft auf Port ${PORT}`); -}); From 876acfa763e0efc67feeb877c75d289284c38bcb Mon Sep 17 00:00:00 2001 From: root Date: Mon, 29 Sep 2025 08:15:31 +0000 Subject: [PATCH 7/8] Update --- README.md | 46 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 5befeae..988ae4e 100644 --- a/README.md +++ b/README.md @@ -10,14 +10,46 @@ Ziel: --- -## 🚀 Features (0.1 Roadmap) +## 🚀 Features & Roadmap + +
+ ✅ v0.1.0 – Initial Release + +- Projektstruktur mit Frontend & Backend +- Lokales Startskript (`scripts/start-dev.sh`) +- Frontend zeigt Stacks an (über Backend) +- API-Verbindung zu Portainer +- Stack Redeploy +- Bereitstellung eines Docker Images über GHCR + +
+ +
+ 🟡 v0.2.0 – In Entwicklung + +### Backend +- [ ] Logging der Redeploy-Aktionen in SQLite speichern +- [ ] API-Endpunkte für Log-Abfragen + +### Frontend +- [ ] Anzeige der Logs (inkl. Statusfarben) +- [ ] UI-Komponenten für Log-Details + +### Features +- [ ] Selektive Auswahl: einzelne Stacks oder Services neu deployen + +
+ +
+ 🔮 Geplante Features (v0.3+) + +- Notifications (z. B. via Webhooks oder Mail) +- Authentifizierung & Benutzerverwaltung +- Monitoring (Status, CPU/RAM) +- Verbesserte UI/UX + +
-- [x] Projektstruktur mit Frontend & Backend -- [x] Lokales Startskript (`scripts/start-dev.sh`) -- [x] Frontend zeigt Stacks an (über Backend) -- [x] API-Verbindung zu Portainer -- [x] Stack Redeploy -- [x] Docker Image im ghcr zur Verfügung stellen --- From ba0905e2b92ce9f5b47de589ddd7c8a9d137f124 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 29 Sep 2025 08:19:15 +0000 Subject: [PATCH 8/8] Update --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 988ae4e..6d71400 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ Ziel: 🟡 v0.2.0 – In Entwicklung ### Backend +- [ ] Anbindung einer SQLite-Datenbank - [ ] Logging der Redeploy-Aktionen in SQLite speichern - [ ] API-Endpunkte für Log-Abfragen