Take all changes from b2 into b1

This commit is contained in:
root
2025-10-10 10:36:42 +00:00
parent 9313c01637
commit 016cb6671a
4 changed files with 1 additions and 893 deletions
-881
View File
@@ -8,9 +8,6 @@ import { Server } from 'socket.io';
import path from 'path'; import path from 'path';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import { db } from './db/index.js'; 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 { import {
logRedeployEvent, logRedeployEvent,
buildLogFilter, 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) => { const isStackOutdated = async (stackId) => {
try { try {
const statusRes = await axiosInstance.get(`/api/stacks/${stackId}/images_status?refresh=true`); 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 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' }); res.json({ success: true, message: 'Redeploy ALL abgeschlossen' });
>>>>>>> feature/v03-notifications
} catch (err) { } catch (err) {
const message = err.response?.data?.message || err.message; const message = err.response?.data?.message || err.message;
logRedeployEvent({ logRedeployEvent({
-10
View File
@@ -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 } = {}) { export function deactivateMaintenanceMode({ message = null } = {}) {
return persistState({ return persistState({
active: false, active: false,
+1
View File
@@ -44,6 +44,7 @@ export default function App() {
)} )}
</span> </span>
</NavLink> </NavLink>
<NavLink to="/logs" className={getNavClass}> <NavLink to="/logs" className={getNavClass}>
Logs Logs
</NavLink> </NavLink>
-2
View File
@@ -44,8 +44,6 @@ const REDEPLOY_TYPE_LABELS = {
Einzeln: "Einzeln", Einzeln: "Einzeln",
Alle: "Alle", Alle: "Alle",
Auswahl: "Auswahl", Auswahl: "Auswahl",
Wartung: "Wartung",
maintenance: "Wartung",
single: "Einzeln", single: "Einzeln",
all: "Alle", all: "Alle",
selection: "Auswahl" selection: "Auswahl"