diff --git a/backend/index.js b/backend/index.js index 9a30bab..9814dce 100644 --- a/backend/index.js +++ b/backend/index.js @@ -8,9 +8,6 @@ import { Server } from 'socket.io'; import path from 'path'; import { fileURLToPath } from 'url'; import { db } from './db/index.js'; -import { getSetting, setSetting, deleteSetting } from './db/settings.js'; -import { activateMaintenanceMode, deactivateMaintenanceMode, getMaintenanceState, isMaintenanceModeActive } from './maintenance/state.js'; -import { spawn } from 'child_process'; import { logRedeployEvent, buildLogFilter, @@ -608,531 +605,6 @@ const loadStackCollections = async () => { }; }; -const PORTAINER_SCRIPT_SETTING_KEY = 'portainer_update_script'; - -const DEFAULT_PORTAINER_UPDATE_SCRIPT = [ - 'docker stop portainer', - 'docker rm portainer', - 'docker pull portainer/portainer-ee:lts', - 'docker run -d -p 8000:8000 -p 9443:9443 --name=portainer --restart=always -v /var/run/docker.sock:/var/run/docker.sock' -].join('\n'); - -let portainerUpdateState = { - running: false, - status: 'idle', - stage: 'idle', - startedAt: null, - finishedAt: null, - targetVersion: null, - resultVersion: null, - scriptSource: null, - message: null, - error: null, - logs: [] -}; - -const addUpdateLog = (message, level = 'info') => { - const entry = { - timestamp: new Date().toISOString(), - level, - message - }; - portainerUpdateState = { - ...portainerUpdateState, - logs: [...(portainerUpdateState.logs || []).slice(-50), entry] - }; - console.log(`🛠️ [PortainerUpdate:${level}] ${message}`); -}; - -const updatePortainerState = (partial = {}) => { - portainerUpdateState = { - ...portainerUpdateState, - ...partial - }; - return portainerUpdateState; -}; - -const getPortainerUpdateStatus = () => ({ - ...portainerUpdateState -}); - -const getCustomPortainerScript = () => { - const row = getSetting(PORTAINER_SCRIPT_SETTING_KEY); - if (!row) return null; - const value = typeof row.value === 'string' ? row.value.trim() : ''; - if (!value) return null; - return { - script: row.value, - updatedAt: row.updated_at || null - }; -}; - -const saveCustomPortainerScript = (script) => { - const normalized = String(script ?? '').replace(/\r?\n/g, '\n').trim(); - if (!normalized) { - deleteSetting(PORTAINER_SCRIPT_SETTING_KEY); - return null; - } - setSetting(PORTAINER_SCRIPT_SETTING_KEY, normalized); - return normalized; -}; - -const getEffectivePortainerScript = () => { - const custom = getCustomPortainerScript(); - if (custom) { - return { - script: custom.script, - source: 'custom', - updatedAt: custom.updatedAt - }; - } - return { - script: DEFAULT_PORTAINER_UPDATE_SCRIPT, - source: 'default', - updatedAt: null - }; -}; - -const detectPortainerContainer = async () => { - try { - const containersRes = await axiosInstance.get(`/api/endpoints/${ENDPOINT_ID}/docker/containers/json`, { - params: { all: true } - }); - const containers = Array.isArray(containersRes.data) ? containersRes.data : []; - - const normalizeName = (value) => (typeof value === 'string' ? value.replace(/^\//, '').toLowerCase() : ''); - const isPortainerContainer = (container = {}) => { - const names = Array.isArray(container.Names) - ? container.Names.map(normalizeName) - : []; - const image = String(container.Image ?? '').toLowerCase(); - const labels = container.Labels || {}; - - if (labels['io.portainer.container']) return true; - if (labels['io.portainer.role'] === 'instance') return true; - if (names.includes('portainer') || names.includes('portainer_ce')) return true; - if (image.includes('portainer/portainer')) return true; - if (image.includes('portainer-ce')) return true; - return false; - }; - - const matchedContainer = containers.find((entry) => isPortainerContainer(entry)); - if (!matchedContainer) { - return { summary: null, error: 'Portainer-Container nicht gefunden' }; - } - - const inspectRes = await axiosInstance.get(`/api/endpoints/${ENDPOINT_ID}/docker/containers/${matchedContainer.Id}/json`); - const inspect = inspectRes.data ?? {}; - - const trimName = (value) => (typeof value === 'string' ? value.replace(/^\//, '') : value); - const toArray = (value) => { - if (!value) return []; - if (Array.isArray(value)) return value.map((item) => String(item)); - return [String(value)]; - }; - - const summary = { - id: inspect.Id ?? matchedContainer.Id ?? null, - name: trimName(inspect.Name) ?? (matchedContainer.Names?.[0] ? trimName(matchedContainer.Names[0]) : null), - image: inspect.Config?.Image ?? matchedContainer.Image ?? null, - created: inspect.Created ?? null, - entrypoint: toArray(inspect.Config?.Entrypoint), - command: toArray(inspect.Config?.Cmd), - args: toArray(inspect.Args), - env: Array.isArray(inspect.Config?.Env) ? inspect.Config.Env : [], - binds: Array.isArray(inspect.HostConfig?.Binds) ? inspect.HostConfig.Binds : [], - mounts: Array.isArray(inspect.Mounts) - ? inspect.Mounts.map((mount) => ({ - type: mount.Type ?? null, - source: mount.Source ?? null, - destination: mount.Destination ?? null, - mode: mount.Mode ?? null, - rw: typeof mount.RW === 'boolean' ? mount.RW : null - })) - : [], - labels: inspect.Config?.Labels ?? matchedContainer.Labels ?? {}, - ports: inspect.HostConfig?.PortBindings ?? null, - networks: inspect.NetworkSettings?.Networks ?? null, - restartPolicy: inspect.HostConfig?.RestartPolicy ?? null - }; - - return { summary, error: null }; - } catch (err) { - const message = err.response?.data?.message || err.message; - return { summary: null, error: message }; - } -}; - -const waitForPortainerAvailability = async ({ timeoutMs = 5 * 60 * 1000, intervalMs = 5000 } = {}) => { - const start = Date.now(); - while (Date.now() - start < timeoutMs) { - try { - await axiosInstance.get('/api/status'); - return true; - } catch (err) { - await new Promise((resolve) => setTimeout(resolve, intervalMs)); - } - } - return false; -}; - -const fetchPortainerStatusSummary = async () => { - const statusRes = await axiosInstance.get('/api/status'); - const statusData = statusRes.data ?? {}; - - const currentVersion = statusData.Version - ?? statusData.ServerVersion - ?? statusData.Server?.Version - ?? statusData.ServerInfo?.Version - ?? statusData.ServerVersionNumber - ?? null; - const edition = statusData.Edition ?? statusData.Server?.Edition ?? null; - const build = statusData.BuildNumber ?? statusData.Server?.Build ?? null; - - const errors = {}; - let latestVersion = null; - - const { summary: containerSummary, error: containerError } = await detectPortainerContainer(); - if (containerError) { - errors.container = containerError; - } - - try { - const githubRes = await axios.get('https://api.github.com/repos/portainer/portainer/releases/latest', { - headers: { - 'Accept': 'application/vnd.github+json', - 'User-Agent': 'StackPulse-Maintenance' - }, - timeout: 5000 - }); - latestVersion = githubRes.data?.tag_name ?? githubRes.data?.tagName ?? null; - } catch (err) { - const message = err.response?.data?.message || err.message; - console.warn(`⚠️ [Maintenance] Konnte Portainer-Latest-Version nicht ermitteln: ${message}`); - errors.latestVersion = message; - } - - if (!currentVersion) { - errors.currentVersion = 'Portainer-Version konnte nicht ermittelt werden'; - } - - const normalizedCurrent = normalizeVersion(currentVersion); - const normalizedLatest = normalizeVersion(latestVersion); - const portainerFlag = typeof statusData.UpdateAvailable === 'boolean' - ? statusData.UpdateAvailable - : null; - - let updateAvailable = null; - if (normalizedCurrent && normalizedLatest) { - updateAvailable = compareSemver(normalizedCurrent, normalizedLatest) < 0; - } else if (portainerFlag !== null) { - updateAvailable = portainerFlag; - } - - const responsePayload = { - currentVersion, - latestVersion, - normalized: { - current: normalizedCurrent, - latest: normalizedLatest - }, - updateAvailable, - portainerFlag, - edition: edition ?? null, - build: build ?? null, - fetchedAt: new Date().toISOString(), - container: containerSummary - }; - - if (Object.keys(errors).length) { - responsePayload.errors = errors; - } - - return responsePayload; -}; - -const maintenanceGuard = (req, res, next) => { - if (!isMaintenanceModeActive()) { - return next(); - } - - return res.status(423).json({ - error: 'Wartungsmodus aktiv', - maintenance: getMaintenanceState(), - update: getPortainerUpdateStatus() - }); -}; - -const logScriptOutput = (data, level) => { - if (!data) return; - const text = data.toString(); - text.split(/\r?\n/) - .map((line) => line.trim()) - .filter(Boolean) - .forEach((line) => addUpdateLog(line, level)); -}; - -const executePortainerUpdateScript = async (script) => { - const normalized = String(script ?? '').replace(/\r?\n/g, '\n').trim(); - if (!normalized) { - addUpdateLog('Kein Update-Skript definiert. Vorgang wird übersprungen.', 'warning'); - return; - } - - return new Promise((resolve, reject) => { - const child = spawn('bash', ['-lc', `set -e -${normalized}`], { - env: process.env, - stdio: ['ignore', 'pipe', 'pipe'] - }); - - child.stdout.on('data', (chunk) => logScriptOutput(chunk, 'stdout')); - child.stderr.on('data', (chunk) => logScriptOutput(chunk, 'stderr')); - child.on('error', (err) => reject(err)); - child.on('close', (code) => { - if (code === 0) { - resolve(); - } else { - reject(new Error(`Update-Skript beendet mit Exit-Code ${code}`)); - } - }); - }); -}; - -let currentPortainerUpdatePromise = null; - -const performPortainerUpdate = async ({ script, scriptSource, targetVersion }) => { - let maintenanceActivated = false; - - try { - addUpdateLog(`Portainer-Update gestartet (Quelle: ${scriptSource})`, 'info'); - updatePortainerState({ - status: 'running', - stage: 'activating-maintenance', - message: 'Wartungsmodus wird aktiviert' - }); - - const maintenanceDetails = { - type: 'portainer-update', - targetVersion: targetVersion ?? null, - scriptSource - }; - - const maintenanceState = activateMaintenanceMode({ - message: 'Portainer Update läuft', - extra: maintenanceDetails - }); - maintenanceActivated = true; - - logRedeployEvent({ - stackId: 'portainer', - stackName: 'Portainer', - status: 'started', - message: `Portainer Update gestartet (Ziel: ${targetVersion ?? 'unbekannt'})`, - endpoint: ENDPOINT_ID, - redeployType: REDEPLOY_TYPES.MAINTENANCE - }); - - logRedeployEvent({ - stackId: 'maintenance', - stackName: 'StackPulse Wartung', - status: 'started', - message: 'Wartungsmodus aktiviert (Portainer Update)', - endpoint: ENDPOINT_ID, - redeployType: REDEPLOY_TYPES.MAINTENANCE - }); - - updatePortainerState({ - stage: 'executing-script', - message: 'Update-Skript wird ausgeführt' - }); - await executePortainerUpdateScript(script); - - addUpdateLog('Update-Skript erfolgreich abgeschlossen', 'info'); - updatePortainerState({ - stage: 'waiting', - message: 'Warte auf Portainer-Verfügbarkeit' - }); - - const available = await waitForPortainerAvailability({ timeoutMs: 5 * 60 * 1000, intervalMs: 5000 }); - if (!available) { - throw new Error('Portainer blieb nach dem Update unerreichbar'); - } - - addUpdateLog('Portainer antwortet wieder. Ermittle Version…', 'info'); - const statusAfter = await fetchPortainerStatusSummary().catch(() => null); - const finalVersion = statusAfter?.currentVersion ?? null; - - logRedeployEvent({ - stackId: 'portainer', - stackName: 'Portainer', - status: 'success', - message: `Portainer Update abgeschlossen (Version: ${finalVersion ?? 'unbekannt'})`, - endpoint: ENDPOINT_ID, - redeployType: REDEPLOY_TYPES.MAINTENANCE - }); - - updatePortainerState({ - running: false, - status: 'success', - stage: 'completed', - finishedAt: new Date().toISOString(), - message: 'Portainer Update abgeschlossen', - error: null, - resultVersion: finalVersion - }); - - addUpdateLog('Portainer Update erfolgreich abgeschlossen', 'success'); - - if (maintenanceActivated || maintenanceState?.active) { - deactivateMaintenanceMode({ message: 'Portainer Update abgeschlossen' }); - maintenanceActivated = false; - logRedeployEvent({ - stackId: 'maintenance', - stackName: 'StackPulse Wartung', - status: 'success', - message: 'Wartungsmodus deaktiviert', - endpoint: ENDPOINT_ID, - redeployType: REDEPLOY_TYPES.MAINTENANCE - }); - } - } catch (err) { - const message = err?.message || 'Portainer Update fehlgeschlagen'; - logRedeployEvent({ - stackId: 'portainer', - stackName: 'Portainer', - status: 'error', - message, - endpoint: ENDPOINT_ID, - redeployType: REDEPLOY_TYPES.MAINTENANCE - }); - - updatePortainerState({ - running: false, - status: 'error', - stage: 'failed', - finishedAt: new Date().toISOString(), - message, - error: message - }); - - addUpdateLog(message, 'error'); - - if (maintenanceActivated || isMaintenanceModeActive()) { - deactivateMaintenanceMode({ message: 'Portainer Update fehlgeschlagen' }); - logRedeployEvent({ - stackId: 'maintenance', - stackName: 'StackPulse Wartung', - status: 'error', - message: 'Wartungsmodus deaktiviert (Fehler)', - endpoint: ENDPOINT_ID, - redeployType: REDEPLOY_TYPES.MAINTENANCE - }); - } - } finally { - currentPortainerUpdatePromise = null; - } -}; - -const fetchPortainerStacks = async () => { - const stacksRes = await axiosInstance.get('/api/stacks'); - return stacksRes.data.filter((stack) => stack.EndpointId === ENDPOINT_ID); -}; - -const buildStackCollections = (stacks = []) => { - const collections = new Map(); - - stacks.forEach((stack) => { - const name = stack.Name || 'Unbenannt'; - const isSelf = SELF_STACK_ID && String(stack.Id) === SELF_STACK_ID; - const entry = collections.get(name); - - if (!entry) { - collections.set(name, { - canonical: stack, - isSelf, - members: [stack] - }); - return; - } - - entry.members.push(stack); - - if (!entry.isSelf && isSelf) { - entry.canonical = stack; - entry.isSelf = true; - } - }); - - const canonicalStacks = []; - const duplicates = []; - - collections.forEach((entry, name) => { - canonicalStacks.push(entry.canonical); - - if (entry.members.length > 1) { - const seenIds = new Set(); - const duplicateEntries = entry.members.filter((member) => { - const id = String(member.Id); - if (id === String(entry.canonical.Id)) { - return false; - } - if (seenIds.has(id)) { - return false; - } - seenIds.add(id); - return true; - }); - - if (duplicateEntries.length > 0) { - duplicates.push({ - name, - canonical: entry.canonical, - members: duplicateEntries - }); - } - } - }); - - return { canonicalStacks, duplicates }; -}; - -const loadStackCollections = async () => { - const filteredStacks = await fetchPortainerStacks(); - return { - filteredStacks, - ...buildStackCollections(filteredStacks) - }; -}; - -const normalizeVersion = (value) => { - if (!value) return null; - return String(value).trim().replace(/^v/i, ''); -}; - -const semverParts = (value) => { - const normalized = normalizeVersion(value); - if (!normalized) return null; - return normalized.split(/[.-]/).map((segment) => { - const numericPart = segment.replace(/[^0-9].*$/, ''); - const parsed = Number.parseInt(numericPart, 10); - return Number.isNaN(parsed) ? 0 : parsed; - }); -}; - -const compareSemver = (a, b) => { - const partsA = semverParts(a) || []; - const partsB = semverParts(b) || []; - const length = Math.max(partsA.length, partsB.length); - for (let index = 0; index < length; index += 1) { - const valueA = partsA[index] ?? 0; - const valueB = partsB[index] ?? 0; - if (valueA > valueB) return 1; - if (valueA < valueB) return -1; - } - return 0; -}; - const isStackOutdated = async (stackId) => { try { const statusRes = await axiosInstance.get(`/api/stacks/${stackId}/images_status?refresh=true`); @@ -1772,360 +1244,7 @@ app.put('/api/stacks/redeploy-all', maintenanceGuard, async (req, res) => { redeployType: REDEPLOY_TYPES.ALL }); -<<<<<<< HEAD -app.get('/api/maintenance/portainer-status', async (req, res) => { - console.log("🧭 [Maintenance] GET /api/maintenance/portainer-status: Prüfung gestartet"); - try { - const statusRes = await axiosInstance.get('/api/status'); - const statusData = statusRes.data ?? {}; - - const currentVersion = statusData.Version - ?? statusData.ServerVersion - ?? statusData.Server?.Version - ?? statusData.ServerInfo?.Version - ?? statusData.ServerVersionNumber - ?? null; - const edition = statusData.Edition ?? statusData.Server?.Edition ?? null; - const build = statusData.BuildNumber ?? statusData.Server?.Build ?? null; - - const errors = {}; - let latestVersion = null; - - try { - const githubRes = await axios.get('https://api.github.com/repos/portainer/portainer/releases/latest', { - headers: { - 'Accept': 'application/vnd.github+json', - 'User-Agent': 'StackPulse-Maintenance' - }, - timeout: 5000 - }); - latestVersion = githubRes.data?.tag_name ?? githubRes.data?.tagName ?? null; - } catch (err) { - const message = err.response?.data?.message || err.message; - console.warn(`⚠️ [Maintenance] Konnte Portainer-Latest-Version nicht ermitteln: ${message}`); - errors.latestVersion = message; - } - - if (!currentVersion) { - errors.currentVersion = 'Portainer-Version konnte nicht ermittelt werden'; - } - - const normalizedCurrent = normalizeVersion(currentVersion); - const normalizedLatest = normalizeVersion(latestVersion); - const portainerFlag = typeof statusData.UpdateAvailable === 'boolean' - ? statusData.UpdateAvailable - : null; - - let updateAvailable = null; - if (normalizedCurrent && normalizedLatest) { - updateAvailable = compareSemver(normalizedCurrent, normalizedLatest) < 0; - } else if (portainerFlag !== null) { - updateAvailable = portainerFlag; - } - - const responsePayload = { - currentVersion, - latestVersion, - normalized: { - current: normalizedCurrent, - latest: normalizedLatest - }, - updateAvailable, - portainerFlag, - edition: edition ?? null, - build: build ?? null, - fetchedAt: new Date().toISOString() - }; - - if (Object.keys(errors).length) { - responsePayload.errors = errors; - } - - res.json(responsePayload); - } catch (err) { - const message = err.response?.data?.message || err.message || 'Unbekannter Fehler'; - console.error(`❌ [Maintenance] Fehler beim Prüfen des Portainer-Status: ${message}`); - res.status(500).json({ error: message }); - } -}); - -app.get('/api/maintenance/duplicates', async (req, res) => { - console.log("🧹 [Maintenance] GET /api/maintenance/duplicates: Abruf gestartet"); - try { - const { duplicates } = await loadStackCollections(); - - const payload = duplicates - .sort((a, b) => a.name.localeCompare(b.name)) - .map((entry) => ({ - name: entry.name, - canonical: { - Id: entry.canonical.Id, - Name: entry.canonical.Name, - EndpointId: entry.canonical.EndpointId, - Type: entry.canonical.Type, - Created: entry.canonical.Created - }, - duplicates: entry.members.map((stack) => ({ - Id: stack.Id, - Name: stack.Name, - EndpointId: stack.EndpointId, - Type: stack.Type, - Created: stack.Created - })) - })); - - res.json({ - total: payload.length, - items: payload - }); - } catch (err) { - console.error(`❌ [Maintenance] Fehler beim Abrufen der Duplikate:`, err.message); - res.status(500).json({ error: 'Fehler beim Abrufen der doppelten Stacks' }); - } -}); - -app.post('/api/maintenance/duplicates/cleanup', async (req, res) => { - const canonicalId = req.body?.canonicalId; - const duplicateIdsInput = Array.isArray(req.body?.duplicateIds) ? req.body.duplicateIds : []; - const duplicateIds = duplicateIdsInput - .map((value) => String(value).trim()) - .filter((value) => value.length > 0); - - if (!canonicalId) { - return res.status(400).json({ error: 'canonicalId ist erforderlich' }); - } - - if (!duplicateIds.length) { - return res.status(400).json({ error: 'duplicateIds ist erforderlich' }); - } - - const canonicalIdStr = String(canonicalId); - console.log(`🧹 [Maintenance] Bereinigung angefordert für Stack ${canonicalIdStr}. Ziel-IDs: ${duplicateIds.join(', ')}`); - - try { - const { duplicates } = await loadStackCollections(); - const target = duplicates.find((entry) => String(entry.canonical.Id) === canonicalIdStr); - - if (!target) { - return res.status(404).json({ error: 'Kein doppelter Stack für diese ID gefunden' }); - } - - const duplicatesToDelete = target.members.filter((stack) => duplicateIds.includes(String(stack.Id))); - - if (!duplicatesToDelete.length) { - return res.status(400).json({ error: 'Keine passenden Duplikate gefunden' }); - } - - logRedeployEvent({ - stackId: target.canonical.Id, - stackName: target.canonical.Name, - status: 'started', - message: `Bereinigung doppelter Stacks gestartet (${duplicatesToDelete.length} Einträge)`, - endpoint: target.canonical.EndpointId, - redeployType: REDEPLOY_TYPES.MAINTENANCE - }); - - const results = []; - const errors = []; - - for (const stack of duplicatesToDelete) { - try { - await axiosInstance.delete(`/api/stacks/${stack.Id}`, { - params: { endpointId: stack.EndpointId } - }); - console.log(`🧹 [Maintenance] Stack entfernt: ${stack.Name} (${stack.Id})`); - results.push({ - id: stack.Id, - name: stack.Name, - endpointId: stack.EndpointId, - status: 'deleted' - }); - } catch (err) { - const message = err.response?.data?.message || err.message; - console.error(`❌ [Maintenance] Fehler beim Entfernen von Stack ${stack.Id}:`, message); - errors.push({ id: stack.Id, message }); - results.push({ - id: stack.Id, - name: stack.Name, - endpointId: stack.EndpointId, - status: 'error', - message - }); - } - } - - if (errors.length) { - const failedIds = errors.map((entry) => entry.id).join(', '); - logRedeployEvent({ - stackId: target.canonical.Id, - stackName: target.canonical.Name, - status: 'error', - message: `Bereinigung fehlgeschlagen für IDs: ${failedIds}`, - endpoint: target.canonical.EndpointId, - redeployType: REDEPLOY_TYPES.MAINTENANCE - }); - - return res.status(500).json({ - success: false, - canonical: { - id: target.canonical.Id, - name: target.canonical.Name, - endpointId: target.canonical.EndpointId - }, - results - }); - } - - logRedeployEvent({ - stackId: target.canonical.Id, - stackName: target.canonical.Name, - status: 'success', - message: `Bereinigung abgeschlossen. Entfernte IDs: ${results.map((entry) => entry.id).join(', ')}`, - endpoint: target.canonical.EndpointId, - redeployType: REDEPLOY_TYPES.MAINTENANCE - }); - - res.json({ - success: true, - canonical: { - id: target.canonical.Id, - name: target.canonical.Name, - endpointId: target.canonical.EndpointId - }, - removed: results.length, - results - }); - } catch (err) { - const message = err.response?.data?.message || err.message; - console.error(`❌ [Maintenance] Fehler bei der Bereinigung:`, message); - res.status(500).json({ error: message || 'Fehler bei der Bereinigung' }); - } -}); - -app.put('/api/stacks/redeploy-selection', maintenanceGuard, 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 { eligibleStacks, skippedStacks } = await filterOutdatedStacks(selectedStacks); - - if (skippedStacks.length) { - skippedStacks.forEach((stack) => { - console.log(`⏭️ Übersprungen (aktuell): ${stack.Name} (${stack.Id})`); - }); - } - - const stackSummaryList = eligibleStacks.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: REDEPLOY_TYPES.SELECTION - }); - - if (!eligibleStacks.length) { - logRedeployEvent({ - stackId: '---', - stackName: '---', - status: 'success', - message: 'Redeploy Auswahl übersprungen: keine veralteten Stacks', - endpoint: ENDPOINT_ID, - redeployType: REDEPLOY_TYPES.SELECTION - }); - return res.json({ success: true, message: 'Keine veralteten Stacks in der Auswahl' }); - } - - for (const stack of eligibleStacks) { - try { - broadcastRedeployStatus({ - stackId: stack.Id, - stackName: stack.Name, - phase: 'started' - }); - logRedeployEvent({ - stackId: stack.Id, - stackName: stack.Name, - status: 'started', - message: 'Redeploy Auswahl gestartet', - endpoint: stack.EndpointId, - redeployType: REDEPLOY_TYPES.SELECTION - }); - - 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({ - stackId: stack.Id, - stackName: stack.Name, - phase: 'success' - }); - logRedeployEvent({ - stackId: stack.Id, - stackName: stack.Name, - status: 'success', - message: 'Redeploy Auswahl erfolgreich abgeschlossen', - endpoint: stack.EndpointId, - redeployType: REDEPLOY_TYPES.SELECTION - }); - console.log(`✅ Redeploy Auswahl abgeschlossen: ${stack.Name}`); - } catch (err) { - const errorMessage = err.response?.data?.message || err.message; - broadcastRedeployStatus({ - stackId: stack.Id, - stackName: stack.Name, - phase: 'error', - message: errorMessage - }); - logRedeployEvent({ - stackId: stack.Id, - stackName: stack.Name, - status: 'error', - message: errorMessage, - endpoint: stack.EndpointId, - redeployType: REDEPLOY_TYPES.SELECTION - }); - console.error(`❌ Fehler beim Redeploy Auswahl für Stack ${stack.Name}:`, errorMessage); - } - } - - res.json({ success: true, message: 'Redeploy Auswahl gestartet' }); -======= res.json({ success: true, message: 'Redeploy ALL abgeschlossen' }); ->>>>>>> feature/v03-notifications } catch (err) { const message = err.response?.data?.message || err.message; logRedeployEvent({ diff --git a/backend/maintenance/state.js b/backend/maintenance/state.js index 16500de..54bc0d0 100644 --- a/backend/maintenance/state.js +++ b/backend/maintenance/state.js @@ -53,16 +53,6 @@ export function activateMaintenanceMode({ message = null, extra = null } = {}) { }); } -<<<<<<< HEAD -export function updateMaintenanceState(partial = {}) { - return persistState({ - ...maintenanceState, - ...partial - }); -} - -======= ->>>>>>> feature/v03-notifications export function deactivateMaintenanceMode({ message = null } = {}) { return persistState({ active: false, diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 2d124c3..a911f20 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -44,6 +44,7 @@ export default function App() { )} + Logs diff --git a/frontend/src/Logs.jsx b/frontend/src/Logs.jsx index 1488dee..b86bd83 100644 --- a/frontend/src/Logs.jsx +++ b/frontend/src/Logs.jsx @@ -44,8 +44,6 @@ const REDEPLOY_TYPE_LABELS = { Einzeln: "Einzeln", Alle: "Alle", Auswahl: "Auswahl", - Wartung: "Wartung", - maintenance: "Wartung", single: "Einzeln", all: "Alle", selection: "Auswahl"