Update
This commit is contained in:
+279
-97
@@ -45,7 +45,23 @@ const axiosInstance = axios.create({
|
|||||||
baseURL: process.env.PORTAINER_URL,
|
baseURL: process.env.PORTAINER_URL,
|
||||||
});
|
});
|
||||||
|
|
||||||
const redeployingStacks = {};
|
const REDEPLOY_PHASES = {
|
||||||
|
QUEUED: 'queued',
|
||||||
|
STARTED: 'started',
|
||||||
|
SUCCESS: 'success',
|
||||||
|
ERROR: 'error',
|
||||||
|
INFO: 'info'
|
||||||
|
};
|
||||||
|
|
||||||
|
const redeployingStacks = new Map();
|
||||||
|
|
||||||
|
const isActiveRedeployPhase = (phase) => phase === REDEPLOY_PHASES.QUEUED || phase === REDEPLOY_PHASES.STARTED;
|
||||||
|
|
||||||
|
const resolveRedeployPhase = (phase, message) => {
|
||||||
|
if (phase) return phase;
|
||||||
|
if (message) return REDEPLOY_PHASES.INFO;
|
||||||
|
return REDEPLOY_PHASES.SUCCESS;
|
||||||
|
};
|
||||||
|
|
||||||
const server = http.createServer(app);
|
const server = http.createServer(app);
|
||||||
const io = new Server(server, {
|
const io = new Server(server, {
|
||||||
@@ -57,23 +73,35 @@ io.on("connection", (socket) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const broadcastRedeployStatus = ({ stackId, stackName, phase, message }) => {
|
const broadcastRedeployStatus = ({ stackId, stackName, phase, message }) => {
|
||||||
const normalizedPhase = phase || (message ? 'info' : undefined);
|
if (!stackId) return;
|
||||||
const isRedeploying = normalizedPhase === 'started';
|
|
||||||
redeployingStacks[stackId] = Boolean(isRedeploying);
|
const resolvedPhase = resolveRedeployPhase(phase, message);
|
||||||
|
const isRedeploying = isActiveRedeployPhase(resolvedPhase);
|
||||||
|
|
||||||
|
if (isRedeploying) {
|
||||||
|
redeployingStacks.set(String(stackId), {
|
||||||
|
phase: resolvedPhase,
|
||||||
|
stackName: stackName || null,
|
||||||
|
message: message || null,
|
||||||
|
updatedAt: Date.now()
|
||||||
|
});
|
||||||
|
} else if (resolvedPhase === REDEPLOY_PHASES.SUCCESS || resolvedPhase === REDEPLOY_PHASES.ERROR || resolvedPhase === 'idle') {
|
||||||
|
redeployingStacks.delete(String(stackId));
|
||||||
|
}
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
stackId,
|
stackId,
|
||||||
stackName,
|
stackName,
|
||||||
phase: normalizedPhase,
|
phase: resolvedPhase,
|
||||||
message,
|
message,
|
||||||
isRedeploying
|
isRedeploying,
|
||||||
|
redeployPhase: resolvedPhase
|
||||||
};
|
};
|
||||||
|
|
||||||
io.emit("redeployStatus", payload);
|
io.emit("redeployStatus", payload);
|
||||||
|
|
||||||
const label = stackName ? `${stackName} (${stackId})` : `Stack ${stackId}`;
|
const label = stackName ? `${stackName} (${stackId})` : `Stack ${stackId}`;
|
||||||
const phaseLabel = normalizedPhase ?? (isRedeploying ? 'started' : 'success');
|
console.log(`🔄 [RedeployStatus] ${label} -> ${resolvedPhase}${message ? `: ${message}` : ""}`);
|
||||||
console.log(`🔄 [RedeployStatus] ${label} -> ${phaseLabel}${message ? `: ${message}` : ""}`);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const REDEPLOY_TYPES = {
|
const REDEPLOY_TYPES = {
|
||||||
@@ -962,6 +990,162 @@ const filterOutdatedStacks = async (stacks = []) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getRedeployMessages = (type) => {
|
||||||
|
switch (type) {
|
||||||
|
case REDEPLOY_TYPES.ALL:
|
||||||
|
return { started: 'Redeploy ALL gestartet', success: 'Redeploy ALL abgeschlossen' };
|
||||||
|
case REDEPLOY_TYPES.SELECTION:
|
||||||
|
return { started: 'Redeploy Auswahl gestartet', success: 'Redeploy Auswahl abgeschlossen' };
|
||||||
|
case REDEPLOY_TYPES.MAINTENANCE:
|
||||||
|
return { started: 'Redeploy (Wartung) gestartet', success: 'Redeploy (Wartung) abgeschlossen' };
|
||||||
|
default:
|
||||||
|
return { started: 'Redeploy gestartet', success: 'Redeploy erfolgreich abgeschlossen' };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const shouldFallbackToStackFile = (message) => {
|
||||||
|
if (!message) return false;
|
||||||
|
const normalized = String(message).toLowerCase();
|
||||||
|
return normalized.includes('not created from git') || normalized.includes('no git configuration');
|
||||||
|
};
|
||||||
|
|
||||||
|
const redeployStackById = async (stackId, redeployType) => {
|
||||||
|
let stack;
|
||||||
|
const messages = getRedeployMessages(redeployType);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const stackRes = await axiosInstance.get(`/api/stacks/${stackId}`);
|
||||||
|
stack = stackRes.data;
|
||||||
|
|
||||||
|
if (stack.EndpointId !== ENDPOINT_ID) {
|
||||||
|
throw new Error(`Stack gehört nicht zum Endpoint ${ENDPOINT_ID}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetId = stack.Id || stackId;
|
||||||
|
const targetName = stack.Name || `Stack ${stackId}`;
|
||||||
|
|
||||||
|
broadcastRedeployStatus({
|
||||||
|
stackId: targetId,
|
||||||
|
stackName: targetName,
|
||||||
|
phase: REDEPLOY_PHASES.STARTED
|
||||||
|
});
|
||||||
|
|
||||||
|
logRedeployEvent({
|
||||||
|
stackId: targetId,
|
||||||
|
stackName: targetName,
|
||||||
|
status: 'started',
|
||||||
|
message: messages.started,
|
||||||
|
endpoint: stack.EndpointId,
|
||||||
|
redeployType
|
||||||
|
});
|
||||||
|
|
||||||
|
const redeployViaStackFile = async () => {
|
||||||
|
const fileRes = await axiosInstance.get(`/api/stacks/${stack.Id}/file`);
|
||||||
|
const stackFileContent = fileRes.data?.StackFileContent;
|
||||||
|
if (!stackFileContent) {
|
||||||
|
throw new Error('Stack file konnte nicht geladen werden');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stack.Type === 2) {
|
||||||
|
const services = fileRes.data?.Config?.services || {};
|
||||||
|
for (const serviceName in services) {
|
||||||
|
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 (err) {
|
||||||
|
console.error(`❌ Fehler beim Pulling von Image "${imageName}":`, err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatePayload = {
|
||||||
|
StackFileContent: stackFileContent,
|
||||||
|
Prune: false,
|
||||||
|
PullImage: true,
|
||||||
|
Env: stack.Env || []
|
||||||
|
};
|
||||||
|
|
||||||
|
const swarmId = stack.SwarmId || stack.SwarmID || fileRes.data?.SwarmID;
|
||||||
|
if (swarmId) {
|
||||||
|
updatePayload.SwarmID = swarmId;
|
||||||
|
}
|
||||||
|
|
||||||
|
await axiosInstance.put(
|
||||||
|
`/api/stacks/${stack.Id}`,
|
||||||
|
updatePayload,
|
||||||
|
{ params: { endpointId: stack.EndpointId } }
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isGitStack = Boolean(stack.GitConfig?.RepositoryURL);
|
||||||
|
let gitRedeploySucceeded = false;
|
||||||
|
|
||||||
|
if (isGitStack) {
|
||||||
|
try {
|
||||||
|
console.log(`🔄 [Redeploy] Git Stack "${stack.Name}" (${stack.Id}) wird redeployed`);
|
||||||
|
await axiosInstance.put(`/api/stacks/${stack.Id}/git/redeploy?endpointId=${stack.EndpointId}`);
|
||||||
|
gitRedeploySucceeded = true;
|
||||||
|
} catch (err) {
|
||||||
|
const gitErrorMessage = err.response?.data?.message || err.message;
|
||||||
|
if (shouldFallbackToStackFile(gitErrorMessage)) {
|
||||||
|
console.warn(`⚠️ Git Redeploy nicht möglich für Stack "${stack.Name}" (${stack.Id}): ${gitErrorMessage}. Fallback auf Stack-Datei.`);
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!gitRedeploySucceeded) {
|
||||||
|
console.log(`🔄 [Redeploy] Stack "${stack.Name}" (${stack.Id}) wird über Stack-Datei redeployed`);
|
||||||
|
await redeployViaStackFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
broadcastRedeployStatus({
|
||||||
|
stackId: targetId,
|
||||||
|
stackName: targetName,
|
||||||
|
phase: REDEPLOY_PHASES.SUCCESS
|
||||||
|
});
|
||||||
|
|
||||||
|
logRedeployEvent({
|
||||||
|
stackId: targetId,
|
||||||
|
stackName: targetName,
|
||||||
|
status: 'success',
|
||||||
|
message: messages.success,
|
||||||
|
endpoint: stack.EndpointId,
|
||||||
|
redeployType
|
||||||
|
});
|
||||||
|
|
||||||
|
return stack;
|
||||||
|
} catch (err) {
|
||||||
|
const errorMessage = err.response?.data?.message || err.message;
|
||||||
|
const fallbackId = stack?.Id || stackId;
|
||||||
|
const fallbackName = stack?.Name || `Stack ${stackId}`;
|
||||||
|
|
||||||
|
broadcastRedeployStatus({
|
||||||
|
stackId: fallbackId,
|
||||||
|
stackName: fallbackName,
|
||||||
|
phase: REDEPLOY_PHASES.ERROR,
|
||||||
|
message: errorMessage
|
||||||
|
});
|
||||||
|
|
||||||
|
logRedeployEvent({
|
||||||
|
stackId: fallbackId,
|
||||||
|
stackName: fallbackName,
|
||||||
|
status: 'error',
|
||||||
|
message: errorMessage,
|
||||||
|
endpoint: stack?.EndpointId || ENDPOINT_ID,
|
||||||
|
redeployType
|
||||||
|
});
|
||||||
|
|
||||||
|
console.error(`❌ Fehler beim Redeploy von Stack ${fallbackName}:`, errorMessage);
|
||||||
|
throw new Error(errorMessage);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// --- API Endpoints ---
|
// --- API Endpoints ---
|
||||||
|
|
||||||
// Stacks abrufen
|
// Stacks abrufen
|
||||||
@@ -983,19 +1167,27 @@ app.get('/api/stacks', maintenanceGuard, async (req, res) => {
|
|||||||
`/api/stacks/${stack.Id}/images_status?refresh=true`
|
`/api/stacks/${stack.Id}/images_status?refresh=true`
|
||||||
);
|
);
|
||||||
const statusEmoji = statusRes.data.Status === 'outdated' ? '⚠️' : '✅';
|
const statusEmoji = statusRes.data.Status === 'outdated' ? '⚠️' : '✅';
|
||||||
|
const redeployMeta = redeployingStacks.get(String(stack.Id));
|
||||||
|
const redeployPhase = redeployMeta?.phase || null;
|
||||||
return {
|
return {
|
||||||
...stack,
|
...stack,
|
||||||
updateStatus: statusEmoji,
|
updateStatus: statusEmoji,
|
||||||
redeploying: redeployingStacks[stack.Id] || false,
|
redeploying: redeployPhase === REDEPLOY_PHASES.STARTED || redeployPhase === REDEPLOY_PHASES.QUEUED,
|
||||||
|
redeployPhase,
|
||||||
|
redeployQueued: redeployPhase === REDEPLOY_PHASES.QUEUED,
|
||||||
redeployDisabled: SELF_STACK_ID ? String(stack.Id) === SELF_STACK_ID : false,
|
redeployDisabled: SELF_STACK_ID ? String(stack.Id) === SELF_STACK_ID : false,
|
||||||
duplicateName: duplicateNameSet.has(stack.Name)
|
duplicateName: duplicateNameSet.has(stack.Name)
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`❌ Fehler beim Abrufen des Status für Stack ${stack.Id}:`, err.message);
|
console.error(`❌ Fehler beim Abrufen des Status für Stack ${stack.Id}:`, err.message);
|
||||||
|
const redeployMeta = redeployingStacks.get(String(stack.Id));
|
||||||
|
const redeployPhase = redeployMeta?.phase || null;
|
||||||
return {
|
return {
|
||||||
...stack,
|
...stack,
|
||||||
updateStatus: '❌',
|
updateStatus: '❌',
|
||||||
redeploying: redeployingStacks[stack.Id] || false,
|
redeploying: redeployPhase === REDEPLOY_PHASES.STARTED || redeployPhase === REDEPLOY_PHASES.QUEUED,
|
||||||
|
redeployPhase,
|
||||||
|
redeployQueued: redeployPhase === REDEPLOY_PHASES.QUEUED,
|
||||||
redeployDisabled: SELF_STACK_ID ? String(stack.Id) === SELF_STACK_ID : false,
|
redeployDisabled: SELF_STACK_ID ? String(stack.Id) === SELF_STACK_ID : false,
|
||||||
duplicateName: duplicateNameSet.has(stack.Name)
|
duplicateName: duplicateNameSet.has(stack.Name)
|
||||||
};
|
};
|
||||||
@@ -1484,94 +1676,16 @@ app.get('/api/logs/export', (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Einzel-Redeploy
|
// Einzel-Redeploy
|
||||||
app.put('/api/stacks/:id/redeploy', maintenanceGuard, async (req, res) => {
|
app.put('/api/stacks/:id/redeploy', async (req, res) => {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
console.log(`🔄 PUT /api/stacks/${id}/redeploy: Redeploy gestartet`);
|
console.log(`🔄 PUT /api/stacks/${id}/redeploy: Redeploy gestartet`);
|
||||||
|
|
||||||
let stack;
|
|
||||||
try {
|
try {
|
||||||
const stackRes = await axiosInstance.get(`/api/stacks/${id}`);
|
await redeployStackById(id, REDEPLOY_TYPES.SINGLE);
|
||||||
stack = stackRes.data;
|
|
||||||
|
|
||||||
if (stack.EndpointId !== ENDPOINT_ID) {
|
|
||||||
throw new Error(`Stack gehört nicht zum Endpoint ${ENDPOINT_ID}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
broadcastRedeployStatus({
|
|
||||||
stackId: stack.Id || id,
|
|
||||||
stackName: stack.Name,
|
|
||||||
phase: 'started'
|
|
||||||
});
|
|
||||||
|
|
||||||
logRedeployEvent({
|
|
||||||
stackId: stack.Id || id,
|
|
||||||
stackName: stack.Name,
|
|
||||||
status: 'started',
|
|
||||||
message: 'Redeploy gestartet',
|
|
||||||
endpoint: stack.EndpointId,
|
|
||||||
redeployType: REDEPLOY_TYPES.SINGLE
|
|
||||||
});
|
|
||||||
|
|
||||||
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(`🖼️ Pulling image "${imageName}" für Service "${serviceName}"`);
|
|
||||||
await axiosInstance.post(
|
|
||||||
`/api/endpoints/${stack.EndpointId}/docker/images/create?fromImage=${encodeURIComponent(imageName)}`
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
console.error(`❌ 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({
|
|
||||||
stackId: stack.Id || id,
|
|
||||||
stackName: stack.Name,
|
|
||||||
phase: 'success'
|
|
||||||
});
|
|
||||||
logRedeployEvent({
|
|
||||||
stackId: stack.Id || id,
|
|
||||||
stackName: stack.Name,
|
|
||||||
status: 'success',
|
|
||||||
message: 'Redeploy erfolgreich abgeschlossen',
|
|
||||||
endpoint: stack.EndpointId,
|
|
||||||
redeployType: REDEPLOY_TYPES.SINGLE
|
|
||||||
});
|
|
||||||
console.log(`✅ PUT /api/stacks/${id}/redeploy: Redeploy erfolgreich abgeschlossen`);
|
console.log(`✅ PUT /api/stacks/${id}/redeploy: Redeploy erfolgreich abgeschlossen`);
|
||||||
res.json({ success: true, message: 'Stack redeployed' });
|
res.json({ success: true, message: 'Stack redeployed' });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const errorMessage = err.response?.data?.message || err.message;
|
const errorMessage = err.message || 'Unbekannter Fehler beim Redeploy';
|
||||||
broadcastRedeployStatus({
|
|
||||||
stackId: stack?.Id || id,
|
|
||||||
stackName: stack?.Name,
|
|
||||||
phase: 'error',
|
|
||||||
message: errorMessage
|
|
||||||
});
|
|
||||||
logRedeployEvent({
|
|
||||||
stackId: stack?.Id || id,
|
|
||||||
stackName: stack?.Name || `Stack ${id}`,
|
|
||||||
status: 'error',
|
|
||||||
message: errorMessage,
|
|
||||||
endpoint: stack?.EndpointId || ENDPOINT_ID,
|
|
||||||
redeployType: REDEPLOY_TYPES.SINGLE
|
|
||||||
});
|
|
||||||
console.error(`❌ Fehler beim Redeploy von Stack ${id}:`, errorMessage);
|
console.error(`❌ Fehler beim Redeploy von Stack ${id}:`, errorMessage);
|
||||||
res.status(500).json({ error: errorMessage });
|
res.status(500).json({ error: errorMessage });
|
||||||
}
|
}
|
||||||
@@ -1612,9 +1726,17 @@ app.put('/api/stacks/redeploy-all', maintenanceGuard, async (req, res) => {
|
|||||||
return res.json({ success: true, message: 'Keine veralteten Stacks gefunden' });
|
return res.json({ success: true, message: 'Keine veralteten Stacks gefunden' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
eligibleStacks.forEach((stack) => {
|
||||||
|
broadcastRedeployStatus({
|
||||||
|
stackId: stack.Id,
|
||||||
|
stackName: stack.Name,
|
||||||
|
phase: REDEPLOY_PHASES.QUEUED
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
for (const stack of eligibleStacks) {
|
for (const stack of eligibleStacks) {
|
||||||
try {
|
try {
|
||||||
await axiosInstance.put(`/api/stacks/${stack.Id}/git/redeploy?endpointId=${stack.EndpointId}`);
|
await redeployStackById(stack.Id, REDEPLOY_TYPES.ALL);
|
||||||
console.log(`✅ Redeploy ALL -> Stack ${stack.Name} (${stack.Id}) erfolgreich`);
|
console.log(`✅ Redeploy ALL -> Stack ${stack.Name} (${stack.Id}) erfolgreich`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`❌ Redeploy ALL -> Stack ${stack.Name} (${stack.Id}) fehlgeschlagen:`, err.message);
|
console.error(`❌ Redeploy ALL -> Stack ${stack.Name} (${stack.Id}) fehlgeschlagen:`, err.message);
|
||||||
@@ -1649,15 +1771,75 @@ app.put('/api/stacks/redeploy-all', maintenanceGuard, async (req, res) => {
|
|||||||
// Redeploy selection
|
// Redeploy selection
|
||||||
app.put('/api/stacks/redeploy-selection', maintenanceGuard, async (req, res) => {
|
app.put('/api/stacks/redeploy-selection', maintenanceGuard, async (req, res) => {
|
||||||
const { stackIds } = req.body || {};
|
const { stackIds } = req.body || {};
|
||||||
console.log(`🚀 PUT /api/stacks/redeploy-selection: Redeploy Auswahl gestartet (${Array.isArray(stackIds) ? stackIds.length : 0} Stacks)`);
|
const totalCount = Array.isArray(stackIds) ? stackIds.length : 0;
|
||||||
|
console.log(`🚀 PUT /api/stacks/redeploy-selection: Redeploy Auswahl gestartet (${totalCount} Stacks)`);
|
||||||
|
|
||||||
if (!Array.isArray(stackIds) || stackIds.length === 0) {
|
if (!Array.isArray(stackIds) || stackIds.length === 0) {
|
||||||
return res.status(400).json({ error: 'stackIds muss eine nicht leere Array sein' });
|
return res.status(400).json({ error: 'stackIds muss eine nicht leere Array sein' });
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for (const id of stackIds) {
|
const normalizedIds = stackIds.map((id) => String(id));
|
||||||
await axiosInstance.put(`/api/stacks/${id}/git/redeploy?endpointId=${ENDPOINT_ID}`);
|
const { filteredStacks } = await loadStackCollections();
|
||||||
|
const stacksById = new Map(filteredStacks.map((stack) => [String(stack.Id), stack]));
|
||||||
|
|
||||||
|
const missingIds = normalizedIds.filter((id) => !stacksById.has(id));
|
||||||
|
if (missingIds.length) {
|
||||||
|
return res.status(400).json({ error: `Ungültige Stack-IDs: ${missingIds.join(', ')}` });
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedStacks = normalizedIds.map((id) => stacksById.get(id)).filter(Boolean);
|
||||||
|
if (!selectedStacks.length) {
|
||||||
|
return res.status(400).json({ error: 'Keine gültigen Stacks für Redeploy Auswahl gefunden' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { eligibleStacks, skippedStacks } = await filterOutdatedStacks(selectedStacks);
|
||||||
|
|
||||||
|
if (skippedStacks.length) {
|
||||||
|
skippedStacks.forEach((stack) => {
|
||||||
|
console.log(`⏭️ Redeploy Auswahl übersprungen (aktuell): ${stack.Name} (${stack.Id})`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const summaryList = eligibleStacks.map((stack) => `${stack.Name} (${stack.Id})`);
|
||||||
|
const summaryText = summaryList.length ? summaryList.join(', ') : 'keine Stacks';
|
||||||
|
|
||||||
|
logRedeployEvent({
|
||||||
|
stackId: stackIds.join(','),
|
||||||
|
stackName: `Auswahl (${stackIds.length})`,
|
||||||
|
status: 'started',
|
||||||
|
message: `Redeploy Auswahl gestartet für: ${summaryText}`,
|
||||||
|
endpoint: ENDPOINT_ID,
|
||||||
|
redeployType: REDEPLOY_TYPES.SELECTION
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!eligibleStacks.length) {
|
||||||
|
logRedeployEvent({
|
||||||
|
stackId: stackIds.join(','),
|
||||||
|
stackName: `Auswahl (${stackIds.length})`,
|
||||||
|
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' });
|
||||||
|
}
|
||||||
|
|
||||||
|
eligibleStacks.forEach((stack) => {
|
||||||
|
broadcastRedeployStatus({
|
||||||
|
stackId: stack.Id,
|
||||||
|
stackName: stack.Name,
|
||||||
|
phase: REDEPLOY_PHASES.QUEUED
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const stack of eligibleStacks) {
|
||||||
|
try {
|
||||||
|
await redeployStackById(stack.Id, REDEPLOY_TYPES.SELECTION);
|
||||||
|
console.log(`✅ Redeploy Auswahl -> Stack ${stack.Name} (${stack.Id}) erfolgreich`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`❌ Redeploy Auswahl -> Stack ${stack.Name} (${stack.Id}) fehlgeschlagen:`, err.message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logRedeployEvent({
|
logRedeployEvent({
|
||||||
@@ -1673,8 +1855,8 @@ app.put('/api/stacks/redeploy-selection', maintenanceGuard, async (req, res) =>
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message = err.response?.data?.message || err.message;
|
const message = err.response?.data?.message || err.message;
|
||||||
logRedeployEvent({
|
logRedeployEvent({
|
||||||
stackId: stackIds.join(','),
|
stackId: Array.isArray(stackIds) ? stackIds.join(',') : String(stackIds ?? ''),
|
||||||
stackName: `Auswahl (${stackIds.length})`,
|
stackName: `Auswahl (${Array.isArray(stackIds) ? stackIds.length : 0})`,
|
||||||
status: 'error',
|
status: 'error',
|
||||||
message,
|
message,
|
||||||
endpoint: ENDPOINT_ID,
|
endpoint: ENDPOINT_ID,
|
||||||
|
|||||||
+170
-50
@@ -20,6 +20,14 @@ const STACKS_REFRESH_INTERVAL = 30 * 1000;
|
|||||||
let stacksCache = { data: null, timestamp: 0 };
|
let stacksCache = { data: null, timestamp: 0 };
|
||||||
let stacksCachePromise = null;
|
let stacksCachePromise = null;
|
||||||
|
|
||||||
|
const REDEPLOY_PHASES = {
|
||||||
|
QUEUED: 'queued',
|
||||||
|
STARTED: 'started',
|
||||||
|
SUCCESS: 'success',
|
||||||
|
ERROR: 'error',
|
||||||
|
INFO: 'info'
|
||||||
|
};
|
||||||
|
|
||||||
const isCacheFresh = () => Boolean(stacksCache.data) && (Date.now() - stacksCache.timestamp < STACKS_CACHE_DURATION);
|
const isCacheFresh = () => Boolean(stacksCache.data) && (Date.now() - stacksCache.timestamp < STACKS_CACHE_DURATION);
|
||||||
const updateStacksCache = (data) => {
|
const updateStacksCache = (data) => {
|
||||||
stacksCache = { data, timestamp: Date.now() };
|
stacksCache = { data, timestamp: Date.now() };
|
||||||
@@ -78,9 +86,20 @@ export default function Stacks() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let effectivePhase = stack.redeployPhase ?? previous?.redeployPhase ?? null;
|
||||||
|
if (!effectivePhase && (stack.redeploying || previous?.redeploying)) {
|
||||||
|
effectivePhase = REDEPLOY_PHASES.STARTED;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isQueued = effectivePhase === REDEPLOY_PHASES.QUEUED;
|
||||||
|
const isRunning = effectivePhase === REDEPLOY_PHASES.STARTED;
|
||||||
|
const isBusy = isQueued || isRunning;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...stack,
|
...stack,
|
||||||
redeploying: previous?.redeploying || stack.redeploying || false,
|
redeployPhase: effectivePhase,
|
||||||
|
redeployQueued: isQueued,
|
||||||
|
redeploying: isBusy,
|
||||||
redeployDisabled: stack.redeployDisabled ?? previous?.redeployDisabled ?? false,
|
redeployDisabled: stack.redeployDisabled ?? previous?.redeployDisabled ?? false,
|
||||||
duplicateName: stack.duplicateName ?? previous?.duplicateName ?? false
|
duplicateName: stack.duplicateName ?? previous?.duplicateName ?? false
|
||||||
};
|
};
|
||||||
@@ -98,33 +117,45 @@ export default function Stacks() {
|
|||||||
const { stackId } = payload;
|
const { stackId } = payload;
|
||||||
if (!stackId) return;
|
if (!stackId) return;
|
||||||
|
|
||||||
const hasBooleanStatus = typeof payload.status === 'boolean';
|
const resolvedPhaseRaw = payload.redeployPhase ?? payload.phase;
|
||||||
const isRedeploying = typeof payload.isRedeploying === 'boolean'
|
let resolvedPhase = resolvedPhaseRaw;
|
||||||
? payload.isRedeploying
|
if (!resolvedPhase) {
|
||||||
: (hasBooleanStatus ? payload.status : undefined);
|
if (payload.isRedeploying === true) {
|
||||||
const resolvedPhase = payload.phase ?? (hasBooleanStatus
|
resolvedPhase = REDEPLOY_PHASES.STARTED;
|
||||||
? (payload.status ? 'started' : 'success')
|
} else if (payload.isRedeploying === false) {
|
||||||
: undefined);
|
resolvedPhase = REDEPLOY_PHASES.SUCCESS;
|
||||||
|
}
|
||||||
|
} else if (resolvedPhaseRaw === 'started' || resolvedPhaseRaw === 'running') {
|
||||||
|
resolvedPhase = REDEPLOY_PHASES.STARTED;
|
||||||
|
} else if (resolvedPhaseRaw === 'queued') {
|
||||||
|
resolvedPhase = REDEPLOY_PHASES.QUEUED;
|
||||||
|
} else if (resolvedPhaseRaw === 'success') {
|
||||||
|
resolvedPhase = REDEPLOY_PHASES.SUCCESS;
|
||||||
|
} else if (resolvedPhaseRaw === 'error') {
|
||||||
|
resolvedPhase = REDEPLOY_PHASES.ERROR;
|
||||||
|
} else if (resolvedPhaseRaw === 'info') {
|
||||||
|
resolvedPhase = REDEPLOY_PHASES.INFO;
|
||||||
|
}
|
||||||
|
|
||||||
const stackSnapshot = stacksByIdRef.current.get(stackId);
|
const stackSnapshot = stacksByIdRef.current.get(stackId);
|
||||||
const stackName = payload.stackName ?? stackSnapshot?.Name;
|
const stackName = payload.stackName ?? stackSnapshot?.Name;
|
||||||
const stackLabel = stackName ? `${stackName} (ID: ${stackId})` : `Stack ${stackId}`;
|
const stackLabel = stackName ? `${stackName} (ID: ${stackId})` : `Stack ${stackId}`;
|
||||||
|
|
||||||
console.log(`🔄 Stack ${stackId} Redeploy Update: ${resolvedPhase ?? (isRedeploying ? 'running' : 'finished')}`);
|
console.log(`🔄 Stack ${stackId} Redeploy Update: ${resolvedPhase ?? 'unbekannt'}`);
|
||||||
|
|
||||||
if (resolvedPhase === 'started') {
|
if (resolvedPhase === REDEPLOY_PHASES.STARTED) {
|
||||||
showToast({
|
showToast({
|
||||||
variant: 'info',
|
variant: 'info',
|
||||||
title: 'Redeploy gestartet',
|
title: 'Redeploy gestartet',
|
||||||
description: stackLabel
|
description: stackLabel
|
||||||
});
|
});
|
||||||
} else if (resolvedPhase === 'success') {
|
} else if (resolvedPhase === REDEPLOY_PHASES.SUCCESS) {
|
||||||
showToast({
|
showToast({
|
||||||
variant: 'success',
|
variant: 'success',
|
||||||
title: 'Redeploy abgeschlossen',
|
title: 'Redeploy abgeschlossen',
|
||||||
description: stackLabel
|
description: stackLabel
|
||||||
});
|
});
|
||||||
} else if (resolvedPhase === 'error') {
|
} else if (resolvedPhase === REDEPLOY_PHASES.ERROR) {
|
||||||
const detail = payload.message ? ` – ${payload.message}` : '';
|
const detail = payload.message ? ` – ${payload.message}` : '';
|
||||||
showToast({
|
showToast({
|
||||||
variant: 'error',
|
variant: 'error',
|
||||||
@@ -137,26 +168,45 @@ export default function Stacks() {
|
|||||||
prev.map(stack => {
|
prev.map(stack => {
|
||||||
if (stack.Id !== stackId) return stack;
|
if (stack.Id !== stackId) return stack;
|
||||||
|
|
||||||
if (resolvedPhase === 'started' || isRedeploying === true) {
|
const nextPhase = resolvedPhase ?? stack.redeployPhase ?? null;
|
||||||
return { ...stack, redeploying: true };
|
const isQueued = nextPhase === REDEPLOY_PHASES.QUEUED;
|
||||||
|
const isRunning = nextPhase === REDEPLOY_PHASES.STARTED;
|
||||||
|
const isSuccess = nextPhase === REDEPLOY_PHASES.SUCCESS;
|
||||||
|
const isError = nextPhase === REDEPLOY_PHASES.ERROR;
|
||||||
|
const isBusy = isQueued || isRunning;
|
||||||
|
|
||||||
|
const updated = {
|
||||||
|
...stack,
|
||||||
|
redeployPhase: nextPhase,
|
||||||
|
redeployQueued: isQueued,
|
||||||
|
redeploying: isBusy
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isSuccess) {
|
||||||
|
updated.updateStatus = '✅';
|
||||||
|
updated.redeploying = false;
|
||||||
|
updated.redeployQueued = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resolvedPhase === 'success') {
|
if (isError) {
|
||||||
return { ...stack, redeploying: false, updateStatus: '✅' };
|
updated.redeploying = false;
|
||||||
|
updated.redeployQueued = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resolvedPhase === 'error' || isRedeploying === false) {
|
if (!isBusy && !isSuccess && !isError && resolvedPhase === undefined) {
|
||||||
return { ...stack, redeploying: false };
|
updated.redeploying = false;
|
||||||
|
updated.redeployQueued = false;
|
||||||
|
updated.redeployPhase = stack.redeployPhase ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return stack;
|
return updated;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const shouldRefresh =
|
const shouldRefresh =
|
||||||
resolvedPhase === 'success' ||
|
resolvedPhase === REDEPLOY_PHASES.SUCCESS ||
|
||||||
resolvedPhase === 'error' ||
|
resolvedPhase === REDEPLOY_PHASES.ERROR ||
|
||||||
(hasBooleanStatus && payload.status === false);
|
(payload.isRedeploying === false && !resolvedPhase);
|
||||||
|
|
||||||
if (shouldRefresh) {
|
if (shouldRefresh) {
|
||||||
try {
|
try {
|
||||||
@@ -259,7 +309,12 @@ export default function Stacks() {
|
|||||||
setSelectedStackIds(prev => {
|
setSelectedStackIds(prev => {
|
||||||
const filtered = prev.filter(id => {
|
const filtered = prev.filter(id => {
|
||||||
const match = stacks.find(stack => stack.Id === id);
|
const match = stacks.find(stack => stack.Id === id);
|
||||||
return match && match.updateStatus !== '✅' && !match.redeployDisabled;
|
if (!match) return false;
|
||||||
|
if (match.updateStatus === '✅') return false;
|
||||||
|
if (match.redeployDisabled) return false;
|
||||||
|
const phase = match.redeployPhase;
|
||||||
|
if (phase === REDEPLOY_PHASES.STARTED || phase === REDEPLOY_PHASES.QUEUED) return false;
|
||||||
|
return true;
|
||||||
});
|
});
|
||||||
return filtered.length === prev.length ? prev : filtered;
|
return filtered.length === prev.length ? prev : filtered;
|
||||||
});
|
});
|
||||||
@@ -286,7 +341,13 @@ export default function Stacks() {
|
|||||||
}, [stacks, statusFilter, normalizedSearch]);
|
}, [stacks, statusFilter, normalizedSearch]);
|
||||||
|
|
||||||
const eligibleFilteredStacks = useMemo(
|
const eligibleFilteredStacks = useMemo(
|
||||||
() => filteredStacks.filter((stack) => stack.updateStatus !== '✅' && !stack.redeployDisabled),
|
() => filteredStacks.filter((stack) => {
|
||||||
|
if (stack.updateStatus === '✅') return false;
|
||||||
|
if (stack.redeployDisabled) return false;
|
||||||
|
const phase = stack.redeployPhase;
|
||||||
|
if (phase === REDEPLOY_PHASES.STARTED || phase === REDEPLOY_PHASES.QUEUED) return false;
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
[filteredStacks]
|
[filteredStacks]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -312,7 +373,13 @@ export default function Stacks() {
|
|||||||
const visiblePageStackIds = useMemo(() => new Set(paginatedStacks.map((stack) => stack.Id)), [paginatedStacks]);
|
const visiblePageStackIds = useMemo(() => new Set(paginatedStacks.map((stack) => stack.Id)), [paginatedStacks]);
|
||||||
|
|
||||||
const eligiblePageStacks = useMemo(
|
const eligiblePageStacks = useMemo(
|
||||||
() => paginatedStacks.filter((stack) => stack.updateStatus !== '✅' && !stack.redeployDisabled),
|
() => paginatedStacks.filter((stack) => {
|
||||||
|
if (stack.updateStatus === '✅') return false;
|
||||||
|
if (stack.redeployDisabled) return false;
|
||||||
|
const phase = stack.redeployPhase;
|
||||||
|
if (phase === REDEPLOY_PHASES.STARTED || phase === REDEPLOY_PHASES.QUEUED) return false;
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
[paginatedStacks]
|
[paginatedStacks]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -498,10 +565,21 @@ export default function Stacks() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleRedeploy = async (stackId) => {
|
const handleRedeploy = async (stackId) => {
|
||||||
|
const snapshot = stacksByIdRef.current.get(stackId);
|
||||||
|
const phase = snapshot?.redeployPhase;
|
||||||
|
if (phase === REDEPLOY_PHASES.STARTED || phase === REDEPLOY_PHASES.QUEUED) return;
|
||||||
|
|
||||||
setSelectedStackIds((prev) => prev.filter((id) => id !== stackId));
|
setSelectedStackIds((prev) => prev.filter((id) => id !== stackId));
|
||||||
setStacks((prev) =>
|
setStacks((prev) =>
|
||||||
prev.map((stack) =>
|
prev.map((stack) =>
|
||||||
stack.Id === stackId ? { ...stack, redeploying: true } : stack
|
stack.Id === stackId
|
||||||
|
? {
|
||||||
|
...stack,
|
||||||
|
redeployPhase: REDEPLOY_PHASES.STARTED,
|
||||||
|
redeploying: true,
|
||||||
|
redeployQueued: false
|
||||||
|
}
|
||||||
|
: stack
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -512,13 +590,20 @@ export default function Stacks() {
|
|||||||
console.error("❌ Fehler beim Redeploy:", err);
|
console.error("❌ Fehler beim Redeploy:", err);
|
||||||
setStacks((prev) =>
|
setStacks((prev) =>
|
||||||
prev.map((stack) =>
|
prev.map((stack) =>
|
||||||
stack.Id === stackId ? { ...stack, redeploying: false } : stack
|
stack.Id === stackId
|
||||||
|
? {
|
||||||
|
...stack,
|
||||||
|
redeployPhase: null,
|
||||||
|
redeploying: false,
|
||||||
|
redeployQueued: false
|
||||||
|
}
|
||||||
|
: stack
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!err.response) {
|
if (!err.response) {
|
||||||
const snapshot = stacksByIdRef.current.get(stackId);
|
const current = stacksByIdRef.current.get(stackId);
|
||||||
const stackLabel = snapshot?.Name ? `${snapshot.Name} (ID: ${stackId})` : `Stack ${stackId}`;
|
const stackLabel = current?.Name ? `${current.Name} (ID: ${stackId})` : `Stack ${stackId}`;
|
||||||
const errorText = err.message || 'Unbekannter Fehler';
|
const errorText = err.message || 'Unbekannter Fehler';
|
||||||
showToast({
|
showToast({
|
||||||
variant: 'error',
|
variant: 'error',
|
||||||
@@ -537,7 +622,12 @@ export default function Stacks() {
|
|||||||
setStacks(prev =>
|
setStacks(prev =>
|
||||||
prev.map(stack =>
|
prev.map(stack =>
|
||||||
targetIds.has(stack.Id)
|
targetIds.has(stack.Id)
|
||||||
? { ...stack, redeploying: true }
|
? {
|
||||||
|
...stack,
|
||||||
|
redeployPhase: REDEPLOY_PHASES.QUEUED,
|
||||||
|
redeploying: true,
|
||||||
|
redeployQueued: true
|
||||||
|
}
|
||||||
: stack
|
: stack
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -555,7 +645,12 @@ export default function Stacks() {
|
|||||||
setStacks(prev =>
|
setStacks(prev =>
|
||||||
prev.map(stack =>
|
prev.map(stack =>
|
||||||
targetIds.has(stack.Id)
|
targetIds.has(stack.Id)
|
||||||
? { ...stack, redeploying: false }
|
? {
|
||||||
|
...stack,
|
||||||
|
redeployPhase: null,
|
||||||
|
redeploying: false,
|
||||||
|
redeployQueued: false
|
||||||
|
}
|
||||||
: stack
|
: stack
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -574,7 +669,12 @@ export default function Stacks() {
|
|||||||
|
|
||||||
const eligibleIds = selectedStackIds.filter((id) => {
|
const eligibleIds = selectedStackIds.filter((id) => {
|
||||||
const stack = stacks.find((entry) => entry.Id === id);
|
const stack = stacks.find((entry) => entry.Id === id);
|
||||||
return stack && stack.updateStatus !== '✅' && !stack.redeployDisabled;
|
if (!stack) return false;
|
||||||
|
if (stack.updateStatus === '✅') return false;
|
||||||
|
if (stack.redeployDisabled) return false;
|
||||||
|
const phase = stack.redeployPhase;
|
||||||
|
if (phase === REDEPLOY_PHASES.STARTED || phase === REDEPLOY_PHASES.QUEUED) return false;
|
||||||
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!eligibleIds.length) {
|
if (!eligibleIds.length) {
|
||||||
@@ -587,7 +687,12 @@ export default function Stacks() {
|
|||||||
setStacks(prev =>
|
setStacks(prev =>
|
||||||
prev.map(stack =>
|
prev.map(stack =>
|
||||||
eligibleSet.has(stack.Id)
|
eligibleSet.has(stack.Id)
|
||||||
? { ...stack, redeploying: true }
|
? {
|
||||||
|
...stack,
|
||||||
|
redeployPhase: REDEPLOY_PHASES.QUEUED,
|
||||||
|
redeploying: true,
|
||||||
|
redeployQueued: true
|
||||||
|
}
|
||||||
: stack
|
: stack
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -601,7 +706,12 @@ export default function Stacks() {
|
|||||||
setStacks(prev =>
|
setStacks(prev =>
|
||||||
prev.map(stack =>
|
prev.map(stack =>
|
||||||
eligibleSet.has(stack.Id)
|
eligibleSet.has(stack.Id)
|
||||||
? { ...stack, redeploying: false }
|
? {
|
||||||
|
...stack,
|
||||||
|
redeployPhase: null,
|
||||||
|
redeploying: false,
|
||||||
|
redeployQueued: false
|
||||||
|
}
|
||||||
: stack
|
: stack
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -624,9 +734,13 @@ export default function Stacks() {
|
|||||||
const bulkActionDisabled = maintenanceLocked || (hasSelection
|
const bulkActionDisabled = maintenanceLocked || (hasSelection
|
||||||
? selectionPromptVisible || selectedStackIds.length === 0 || selectedStackIds.every(id => {
|
? selectionPromptVisible || selectedStackIds.length === 0 || selectedStackIds.every(id => {
|
||||||
const targetStack = stacks.find(stack => stack.Id === id);
|
const targetStack = stacks.find(stack => stack.Id === id);
|
||||||
return !targetStack || targetStack.redeploying || targetStack.updateStatus === '✅' || targetStack.redeployDisabled;
|
if (!targetStack) return true;
|
||||||
|
if (targetStack.updateStatus === '✅') return true;
|
||||||
|
if (targetStack.redeployDisabled) return true;
|
||||||
|
const phase = targetStack.redeployPhase;
|
||||||
|
return phase === REDEPLOY_PHASES.STARTED || phase === REDEPLOY_PHASES.QUEUED;
|
||||||
})
|
})
|
||||||
: !hasOutdatedStacks || eligiblePageStacks.every(stack => stack.redeploying));
|
: !hasOutdatedStacks);
|
||||||
|
|
||||||
const handleBulkRedeploy = () => {
|
const handleBulkRedeploy = () => {
|
||||||
if (maintenanceLocked) {
|
if (maintenanceLocked) {
|
||||||
@@ -807,19 +921,22 @@ export default function Stacks() {
|
|||||||
|
|
||||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
|
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||||
{paginatedStacks.map(stack => {
|
{paginatedStacks.map(stack => {
|
||||||
const isRedeploying = stack.redeploying;
|
const phase = stack.redeployPhase;
|
||||||
|
const isQueued = phase === REDEPLOY_PHASES.QUEUED;
|
||||||
|
const isRunning = phase === REDEPLOY_PHASES.STARTED;
|
||||||
|
const isBusy = isQueued || isRunning;
|
||||||
const isSelected = selectedStackIds.includes(stack.Id);
|
const isSelected = selectedStackIds.includes(stack.Id);
|
||||||
const isCurrent = stack.updateStatus === '✅';
|
const isCurrent = stack.updateStatus === '✅';
|
||||||
const isSelfStack = Boolean(stack.redeployDisabled);
|
const isSelfStack = Boolean(stack.redeployDisabled);
|
||||||
const isSelectable = !isRedeploying && !isCurrent && !isSelfStack && !maintenanceLocked;
|
const isSelectable = !isBusy && !isCurrent && !isSelfStack && !maintenanceLocked;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={stack.Id}
|
key={stack.Id}
|
||||||
className={`flex justify-between items-center p-5 rounded-xl shadow-lg transition border
|
className={`flex justify-between items-center p-5 rounded-xl shadow-lg transition border
|
||||||
${isSelected ? 'border-purple-500 ring-1 ring-purple-500/40' : 'border-transparent'}
|
${isSelected ? 'border-purple-500 ring-1 ring-purple-500/40' : 'border-transparent'}
|
||||||
${isRedeploying ? 'bg-gray-700 cursor-wait' : 'bg-gray-800 hover:bg-gray-700'}
|
${isBusy ? 'bg-gray-700 cursor-wait' : 'bg-gray-800 hover:bg-gray-700'}
|
||||||
${!isSelectable && !isRedeploying ? 'opacity-75' : ''}`}
|
${!isSelectable && !isBusy ? 'opacity-75' : ''}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
<input
|
<input
|
||||||
@@ -844,11 +961,16 @@ export default function Stacks() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col items-end gap-1 text-sm">
|
<div className="flex flex-col items-end gap-1 text-sm">
|
||||||
{isRedeploying ? (
|
{isRunning ? (
|
||||||
<>
|
<>
|
||||||
<span className="text-xs uppercase tracking-wide text-orange-300">Redeploy</span>
|
<span className="text-xs uppercase tracking-wide text-orange-300">Redeploy</span>
|
||||||
<span className="text-orange-200">läuft…</span>
|
<span className="text-orange-200">läuft…</span>
|
||||||
</>
|
</>
|
||||||
|
) : isQueued ? (
|
||||||
|
<>
|
||||||
|
<span className="text-xs uppercase tracking-wide text-orange-300">Redeploy</span>
|
||||||
|
<span className="text-orange-200">in Warteliste…</span>
|
||||||
|
</>
|
||||||
) : isSelfStack ? (
|
) : isSelfStack ? (
|
||||||
<>
|
<>
|
||||||
<span className="text-xs uppercase tracking-wide text-gray-400">System</span>
|
<span className="text-xs uppercase tracking-wide text-gray-400">System</span>
|
||||||
@@ -860,15 +982,13 @@ export default function Stacks() {
|
|||||||
<span className="text-green-300">Aktuell</span>
|
<span className="text-green-300">Aktuell</span>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<button
|
||||||
<button
|
onClick={() => handleRedeploy(stack.Id)}
|
||||||
onClick={() => handleRedeploy(stack.Id)}
|
disabled={isBusy || maintenanceLocked}
|
||||||
disabled={isRedeploying || maintenanceLocked}
|
className="px-5 py-2 rounded-lg font-medium transition bg-blue-500 hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
className="px-5 py-2 rounded-lg font-medium transition bg-blue-500 hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed"
|
>
|
||||||
>
|
Redeploy
|
||||||
Redeploy
|
</button>
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user