diff --git a/backend/data/stackpulse.db-shm b/backend/data/stackpulse.db-shm deleted file mode 100644 index fe9ac28..0000000 Binary files a/backend/data/stackpulse.db-shm and /dev/null differ diff --git a/backend/data/stackpulse.db-wal b/backend/data/stackpulse.db-wal deleted file mode 100644 index e69de29..0000000 diff --git a/backend/db/migrate.js b/backend/db/migrate.js index 405120b..e564390 100644 --- a/backend/db/migrate.js +++ b/backend/db/migrate.js @@ -8,12 +8,24 @@ CREATE TABLE IF NOT EXISTS redeploy_logs ( stack_name TEXT NOT NULL, status TEXT NOT NULL, message TEXT, - endpoint INTEGER + endpoint INTEGER, + redeploy_type TEXT ); `; db.exec(createRedeployLogsTable); +try { + const columns = db.prepare('PRAGMA table_info(redeploy_logs)').all(); + const hasRedeployType = columns.some((column) => column.name === 'redeploy_type'); + if (!hasRedeployType) { + db.exec('ALTER TABLE redeploy_logs ADD COLUMN redeploy_type TEXT'); + console.log('ℹ️ redeploy_type column hinzugefügt'); + } +} catch (err) { + console.error('⚠️ Konnte redeploy_type Spalte nicht prüfen/erstellen:', err.message); +} + console.log('✅ redeploy_logs table ready'); db.close(); diff --git a/backend/db/redeployLogs.js b/backend/db/redeployLogs.js index 907b240..12ba865 100644 --- a/backend/db/redeployLogs.js +++ b/backend/db/redeployLogs.js @@ -57,6 +57,15 @@ export function buildLogFilter(queryParams = {}) { }); } + const redeployTypes = valueToArray(queryParams.redeployTypes ?? queryParams.redeployType); + if (redeployTypes.length) { + const placeholders = redeployTypes.map((_, idx) => `@redeployType${idx}`); + filters.push(`redeploy_type IN (${placeholders.join(', ')})`); + redeployTypes.forEach((entry, idx) => { + params[`redeployType${idx}`] = entry; + }); + } + const messageQuery = singleValue(queryParams.message); if (messageQuery && String(messageQuery).trim()) { filters.push('message LIKE @message'); @@ -82,18 +91,19 @@ export function buildLogFilter(queryParams = {}) { } const insertRedeployLogStmt = db.prepare(` - INSERT INTO redeploy_logs (stack_id, stack_name, status, message, endpoint) - VALUES (@stackId, @stackName, @status, @message, @endpoint) + INSERT INTO redeploy_logs (stack_id, stack_name, status, message, endpoint, redeploy_type) + VALUES (@stackId, @stackName, @status, @message, @endpoint, @redeployType) `); -export function logRedeployEvent({ stackId, stackName, status, message = null, endpoint = null }) { +export function logRedeployEvent({ stackId, stackName, status, message = null, endpoint = null, redeployType = null }) { try { insertRedeployLogStmt.run({ stackId: String(stackId), stackName: stackName ?? 'Unknown', status, message, - endpoint + endpoint, + redeployType: redeployType ?? null }); } catch (err) { console.error('❌ Fehler beim Speichern des Redeploy-Logs:', err.message); @@ -116,7 +126,7 @@ export function deleteLogsByFilters(queryParams = {}) { export function exportLogsByFilters(queryParams = {}, format = 'txt') { const { whereClause, params } = buildLogFilter(queryParams); const rows = db.prepare(` - SELECT id, timestamp, stack_id AS stackId, stack_name AS stackName, status, message, endpoint + SELECT id, timestamp, stack_id AS stackId, stack_name AS stackName, status, message, endpoint, redeploy_type AS redeployType FROM redeploy_logs ${whereClause} ORDER BY datetime(timestamp) DESC @@ -126,7 +136,7 @@ export function exportLogsByFilters(queryParams = {}, format = 'txt') { if (format === 'sql') { const statements = rows.map((row) => { - const columns = ['id', 'timestamp', 'stack_id', 'stack_name', 'status', 'message', 'endpoint']; + const columns = ['id', 'timestamp', 'stack_id', 'stack_name', 'status', 'message', 'endpoint', 'redeploy_type']; const values = [ row.id, row.timestamp, @@ -134,7 +144,8 @@ export function exportLogsByFilters(queryParams = {}, format = 'txt') { row.stackName, row.status, row.message, - row.endpoint + row.endpoint, + row.redeployType ].map((value) => { if (value === null || value === undefined) return 'NULL'; return `'${String(value).replace(/'/g, "''")}'`; @@ -156,7 +167,8 @@ export function exportLogsByFilters(queryParams = {}, format = 'txt') { `Stack: ${row.stackName ?? 'Unbekannt'} (ID: ${row.stackId})`, `Status: ${row.status}`, `Endpoint: ${row.endpoint ?? '-'}`, - `Nachricht: ${row.message ?? '-'}` + `Nachricht: ${row.message ?? '-'}`, + `Redeploy: ${row.redeployType ?? '-'}` ]; return parts.join(' | '); }); diff --git a/backend/index.js b/backend/index.js index ff6edc7..8de7754 100644 --- a/backend/index.js +++ b/backend/index.js @@ -117,7 +117,8 @@ app.get('/api/logs', (req, res) => { stack_name AS stackName, status, message, - endpoint + endpoint, + redeploy_type AS redeployType FROM redeploy_logs ${whereClause} ORDER BY datetime(timestamp) DESC @@ -224,7 +225,8 @@ app.put('/api/stacks/:id/redeploy', async (req, res) => { stackName: stack.Name, status: 'started', message: 'Redeploy gestartet', - endpoint: stack.EndpointId + endpoint: stack.EndpointId, + redeployType: 'Einzeln' }); if (stack.Type === 1) { @@ -262,7 +264,8 @@ app.put('/api/stacks/:id/redeploy', async (req, res) => { stackName: stack.Name, status: 'success', message: 'Redeploy erfolgreich abgeschlossen', - endpoint: stack.EndpointId + endpoint: stack.EndpointId, + redeployType: 'Einzeln' }); console.log(`✅ PUT /api/stacks/${id}/redeploy: Redeploy erfolgreich abgeschlossen`); res.json({ success: true, message: 'Stack redeployed' }); @@ -274,7 +277,8 @@ app.put('/api/stacks/:id/redeploy', async (req, res) => { stackName: stack?.Name || `Stack ${id}`, status: 'error', message: errorMessage, - endpoint: stack?.EndpointId || ENDPOINT_ID + endpoint: stack?.EndpointId || ENDPOINT_ID, + redeployType: 'Einzeln' }); console.error(`❌ Fehler beim Redeploy von Stack ${id}:`, errorMessage); res.status(500).json({ error: errorMessage }); @@ -292,6 +296,17 @@ app.put('/api/stacks/redeploy-all', async (req, res) => { console.log("📦 Redeploy ALL für folgende Stacks:"); filteredStacks.forEach(s => console.log(` - ${s.Name}`)); + const stackSummaryList = filteredStacks.map((stack) => `${stack.Name} (${stack.Id})`); + const stackSummary = stackSummaryList.length ? stackSummaryList.join(', ') : 'keine Stacks'; + logRedeployEvent({ + stackId: '---', + stackName: '---', + status: 'started', + message: `Redeploy ALL gestartet für: ${stackSummary}`, + endpoint: ENDPOINT_ID, + redeployType: 'Alle' + }); + filteredStacks.forEach(async (stack) => { try { broadcastRedeployStatus(stack.Id, true); @@ -299,8 +314,9 @@ app.put('/api/stacks/redeploy-all', async (req, res) => { stackId: stack.Id, stackName: stack.Name, status: 'started', - message: 'Redeploy über Redeploy ALL gestartet', - endpoint: stack.EndpointId + message: 'Redeploy ALL gestartet', + endpoint: stack.EndpointId, + redeployType: 'Alle' }); if (stack.Type === 1) { @@ -323,8 +339,9 @@ app.put('/api/stacks/redeploy-all', async (req, res) => { stackId: stack.Id, stackName: stack.Name, status: 'success', - message: 'Redeploy über Redeploy ALL abgeschlossen', - endpoint: stack.EndpointId + message: 'Redeploy ALL abgeschlossen', + endpoint: stack.EndpointId, + redeployType: 'Alle' }); console.log(`✅ Redeploy abgeschlossen: ${stack.Name}`); } catch (err) { @@ -335,7 +352,8 @@ app.put('/api/stacks/redeploy-all', async (req, res) => { stackName: stack.Name, status: 'error', message: errorMessage, - endpoint: stack.EndpointId + endpoint: stack.EndpointId, + redeployType: 'Alle' }); console.error(`❌ Fehler beim Redeploy von Stack ${stack.Name}:`, errorMessage); } @@ -344,10 +362,121 @@ app.put('/api/stacks/redeploy-all', async (req, res) => { res.json({ success: true, message: 'Redeploy ALL gestartet' }); } catch (err) { console.error(`❌ Fehler beim Redeploy ALL:`, err.message); + logRedeployEvent({ + stackId: '---', + stackName: '---', + status: 'error', + message: err.message, + endpoint: ENDPOINT_ID, + redeployType: 'Alle' + }); res.status(500).json({ error: err.message }); } }); +app.put('/api/stacks/redeploy-selection', async (req, res) => { + const { stackIds } = req.body || {}; + console.log(`🚀 PUT /api/stacks/redeploy-selection: Redeploy Auswahl gestartet (${Array.isArray(stackIds) ? stackIds.length : 0} Stacks)`); + + if (!Array.isArray(stackIds) || !stackIds.length) { + return res.status(400).json({ error: 'stackIds (array) erforderlich' }); + } + + const normalizedIds = stackIds.map((id) => String(id)); + + try { + const stacksRes = await axiosInstance.get('/api/stacks'); + const endpointStacks = stacksRes.data.filter(stack => stack.EndpointId === ENDPOINT_ID); + const selectedStacks = endpointStacks.filter((stack) => normalizedIds.includes(String(stack.Id))); + + if (!selectedStacks.length) { + return res.status(400).json({ error: 'Keine gültigen Stacks für Redeploy Auswahl gefunden' }); + } + + const missingIds = normalizedIds.filter((id) => !selectedStacks.some((stack) => String(stack.Id) === id)); + if (missingIds.length) { + return res.status(400).json({ error: `Ungültige Stack-IDs: ${missingIds.join(', ')}` }); + } + + const stackSummaryList = selectedStacks.map((stack) => `${stack.Name} (${stack.Id})`); + const stackSummary = stackSummaryList.length ? stackSummaryList.join(', ') : 'keine Stacks'; + logRedeployEvent({ + stackId: '---', + stackName: '---', + status: 'started', + message: `Redeploy Auswahl gestartet für: ${stackSummary}`, + endpoint: ENDPOINT_ID, + redeployType: 'Auswahl' + }); + + selectedStacks.forEach(async (stack) => { + try { + broadcastRedeployStatus(stack.Id, true); + logRedeployEvent({ + stackId: stack.Id, + stackName: stack.Name, + status: 'started', + message: 'Redeploy Auswahl gestartet', + endpoint: stack.EndpointId, + redeployType: 'Auswahl' + }); + + if (stack.Type === 1) { + console.log(`🔄 [Redeploy Auswahl] Git Stack "${stack.Name}" (${stack.Id})`); + await axiosInstance.put(`/api/stacks/${stack.Id}/git/redeploy?endpointId=${stack.EndpointId}`); + } else if (stack.Type === 2) { + console.log(`🔄 [Redeploy Auswahl] Compose Stack "${stack.Name}" (${stack.Id})`); + const fileRes = await axiosInstance.get(`/api/stacks/${stack.Id}/file`); + const stackFileContent = fileRes.data?.StackFileContent; + if (stackFileContent) { + await axiosInstance.put(`/api/stacks/${stack.Id}`, + { StackFileContent: stackFileContent, Prune: false, PullImage: true }, + { params: { endpointId: stack.EndpointId } } + ); + } + } + + broadcastRedeployStatus(stack.Id, false); + logRedeployEvent({ + stackId: stack.Id, + stackName: stack.Name, + status: 'success', + message: 'Redeploy Auswahl erfolgreich abgeschlossen', + endpoint: stack.EndpointId, + redeployType: 'Auswahl' + }); + console.log(`✅ Redeploy Auswahl abgeschlossen: ${stack.Name}`); + } catch (err) { + broadcastRedeployStatus(stack.Id, false); + const errorMessage = err.response?.data?.message || err.message; + logRedeployEvent({ + stackId: stack.Id, + stackName: stack.Name, + status: 'error', + message: errorMessage, + endpoint: stack.EndpointId, + redeployType: 'Auswahl' + }); + console.error(`❌ Fehler beim Redeploy Auswahl für Stack ${stack.Name}:`, errorMessage); + } + }); + + res.json({ success: true, message: 'Redeploy Auswahl gestartet' }); + } catch (err) { + const errorMessage = err.response?.data?.message || err.message; + console.error(`❌ Fehler beim Redeploy Auswahl:`, errorMessage); + logRedeployEvent({ + stackId: '---', + stackName: '---', + status: 'error', + message: errorMessage, + endpoint: ENDPOINT_ID, + redeployType: 'Auswahl' + }); + res.status(500).json({ error: errorMessage }); + } +}); + server.listen(PORT, '0.0.0.0', () => { console.log(`🚀 Backend läuft auf Port ${PORT}`); }); diff --git a/backend/public/assets/images/stackpulse.png b/backend/public/assets/images/stackpulse.png new file mode 100644 index 0000000..1df7f91 Binary files /dev/null and b/backend/public/assets/images/stackpulse.png differ diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index c6db119..5b757af 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -2,6 +2,7 @@ import React from "react"; import { NavLink, Route, Routes } from "react-router-dom"; import Stacks from "./Stacks.jsx"; import Logs from "./Logs.jsx"; +import logo from "./assets/images/stackpulse.png"; const navLinkBase = "px-4 py-2 rounded-md font-medium transition-colors duration-150"; @@ -14,12 +15,12 @@ export default function App() {
-
-
-

StackPulse

-

Verwalte deine Docker Stacks

+
+
+ v0.2 + StackPulse
-