User, Groups, Logs Update

This commit is contained in:
root
2025-10-29 15:38:25 +00:00
parent 4eea4b23bd
commit 36a6e2b66c
25 changed files with 3623 additions and 1724 deletions
+521 -129
View File
@@ -23,12 +23,12 @@ import {
verifyPassword
} from './auth/superuser.js';
import {
logRedeployEvent,
buildLogFilter,
deleteLogById,
deleteLogsByFilters,
exportLogsByFilters
} from './db/redeployLogs.js';
logEvent,
buildEventLogFilter,
deleteEventLogById,
deleteEventLogsByFilters,
exportEventLogsByFilters
} from './logging/eventLogs.js';
import { getSetting, setSetting, deleteSetting } from './db/settings.js';
import { activateMaintenanceMode, deactivateMaintenanceMode, getMaintenanceState, isMaintenanceModeActive } from './maintenance/state.js';
import {
@@ -45,8 +45,8 @@ import {
removeServer,
setServerApiKey
} from './setup/index.js';
import { listUsers, getUserById, updateUserGroups, createUser } from './users/index.js';
import { listGroups, createGroup } from './groups/index.js';
import { listUsers, getUserById, updateUserGroups, createUser, updateUserDetails, deleteUser, updateUserActiveStatus } from './users/index.js';
import { listGroups, createGroup, getGroupById, updateGroupDetails, deleteGroup } from './groups/index.js';
dotenv.config();
@@ -301,6 +301,53 @@ const REDEPLOY_TYPES = {
MAINTENANCE: 'Wartung'
};
const logStackEvent = ({
stackId,
stackName,
status,
message,
redeployType = null,
endpointId = null,
metadata = {}
}) => {
const metadataPayload = {
...metadata
};
if (redeployType) {
metadataPayload.redeployType = redeployType;
}
if (endpointId !== undefined && endpointId !== null) {
metadataPayload.endpointId = String(endpointId);
}
const hasMetadata = Object.keys(metadataPayload).length > 0;
logEvent({
category: 'stack',
eventType: redeployType ?? null,
action: 'redeploy',
status: status ?? null,
entityType: 'stack',
entityId: stackId !== undefined && stackId !== null ? String(stackId) : null,
entityName: stackName ?? null,
contextType: endpointId !== undefined && endpointId !== null ? 'endpoint' : null,
contextId: endpointId !== undefined && endpointId !== null ? String(endpointId) : null,
message: message ?? null,
metadata: hasMetadata ? metadataPayload : null
});
};
const parseJsonColumn = (value) => {
if (value === undefined || value === null || value === '') return null;
try {
return JSON.parse(value);
} catch {
return null;
}
};
const SELF_STACK_ID = process.env.SELF_STACK_ID ? String(process.env.SELF_STACK_ID) : null;
const PORTAINER_SCRIPT_SETTING_KEY = 'portainer_update_script';
const PORTAINER_SSH_CONFIG_KEY = 'portainer_ssh_config';
@@ -942,22 +989,40 @@ const performPortainerUpdate = async ({ script, scriptSource, targetVersion }) =
});
maintenanceActivated = true;
logRedeployEvent({
stackId: 'portainer',
stackName: 'Portainer',
status: 'started',
logEvent({
category: 'wartung',
eventType: 'portainer-update',
action: 'aktualisieren',
status: 'gestartet',
entityType: 'service',
entityId: 'portainer',
entityName: 'Portainer',
contextType: 'endpoint',
contextId: String(endpointId),
message: `Portainer Update gestartet (Ziel: ${targetVersion ?? 'unbekannt'})`,
endpoint: endpointId,
redeployType: REDEPLOY_TYPES.MAINTENANCE
metadata: {
targetVersion: targetVersion ?? null,
scriptSource
},
source: 'system'
});
logRedeployEvent({
stackId: 'maintenance',
stackName: 'StackPulse Wartung',
status: 'started',
logEvent({
category: 'wartung',
eventType: 'Wartungsmodus',
action: 'aktivieren',
status: 'gestartet',
entityType: 'system',
entityId: 'wartung',
entityName: 'StackPulse Wartung',
contextType: 'endpoint',
contextId: String(endpointId),
message: 'Wartungsmodus aktiviert (Portainer Update)',
endpoint: endpointId,
redeployType: REDEPLOY_TYPES.MAINTENANCE
metadata: {
reason: 'portainer-update',
scriptSource
},
source: 'system'
});
updatePortainerState({
@@ -981,13 +1046,21 @@ const performPortainerUpdate = async ({ script, scriptSource, targetVersion }) =
const statusAfter = await fetchPortainerStatusSummary().catch(() => null);
const finalVersion = statusAfter?.currentVersion ?? null;
logRedeployEvent({
stackId: 'portainer',
stackName: 'Portainer',
status: 'success',
logEvent({
category: 'wartung',
eventType: 'portainer-update',
action: 'aktualisieren',
status: 'erfolgreich',
entityType: 'service',
entityId: 'portainer',
entityName: 'Portainer',
contextType: 'endpoint',
contextId: String(endpointId),
message: `Portainer Update abgeschlossen (Version: ${finalVersion ?? 'unbekannt'})`,
endpoint: endpointId,
redeployType: REDEPLOY_TYPES.MAINTENANCE
metadata: {
resultVersion: finalVersion ?? null
},
source: 'system'
});
updatePortainerState({
@@ -1005,24 +1078,37 @@ const performPortainerUpdate = async ({ script, scriptSource, targetVersion }) =
if (maintenanceActivated || maintenanceState?.active) {
deactivateMaintenanceMode({ message: 'Portainer Update abgeschlossen' });
maintenanceActivated = false;
logRedeployEvent({
stackId: 'maintenance',
stackName: 'StackPulse Wartung',
status: 'success',
logEvent({
category: 'wartung',
eventType: 'Wartungsmodus',
action: 'deaktivieren',
status: 'erfolgreich',
entityType: 'system',
entityId: 'wartung',
entityName: 'StackPulse Wartung',
contextType: 'endpoint',
contextId: String(endpointId),
message: 'Wartungsmodus deaktiviert',
endpoint: endpointId,
redeployType: REDEPLOY_TYPES.MAINTENANCE
metadata: {
reason: 'portainer-update'
},
source: 'system'
});
}
} catch (err) {
const message = err?.message || 'Portainer Update fehlgeschlagen';
logRedeployEvent({
stackId: 'portainer',
stackName: 'Portainer',
status: 'error',
logEvent({
category: 'wartung',
eventType: 'portainer-update',
action: 'aktualisieren',
status: 'fehler',
entityType: 'service',
entityId: 'portainer',
entityName: 'Portainer',
contextType: 'endpoint',
contextId: String(endpointId),
message,
endpoint: endpointId,
redeployType: REDEPLOY_TYPES.MAINTENANCE
source: 'system'
});
updatePortainerState({
@@ -1038,13 +1124,21 @@ const performPortainerUpdate = async ({ script, scriptSource, targetVersion }) =
if (maintenanceActivated || isMaintenanceModeActive()) {
deactivateMaintenanceMode({ message: 'Portainer Update fehlgeschlagen' });
logRedeployEvent({
stackId: 'maintenance',
stackName: 'StackPulse Wartung',
status: 'error',
logEvent({
category: 'wartung',
eventType: 'Wartungsmodus',
action: 'deaktivieren',
status: 'fehler',
entityType: 'system',
entityId: 'wartung',
entityName: 'StackPulse Wartung',
contextType: 'endpoint',
contextId: String(endpointId),
message: 'Wartungsmodus deaktiviert (Fehler)',
endpoint: endpointId,
redeployType: REDEPLOY_TYPES.MAINTENANCE
metadata: {
reason: 'portainer-update'
},
source: 'system'
});
}
} finally {
@@ -1191,12 +1285,12 @@ const redeployStackById = async (stackId, redeployType) => {
phase: REDEPLOY_PHASES.STARTED
});
logRedeployEvent({
logStackEvent({
stackId: targetId,
stackName: targetName,
status: 'started',
message: messages.started,
endpoint: stack.EndpointId,
endpointId: stack.EndpointId,
redeployType
});
@@ -1271,12 +1365,12 @@ const redeployStackById = async (stackId, redeployType) => {
phase: REDEPLOY_PHASES.SUCCESS
});
logRedeployEvent({
logStackEvent({
stackId: targetId,
stackName: targetName,
status: 'success',
message: messages.success,
endpoint: stack.EndpointId,
endpointId: stack.EndpointId,
redeployType
});
@@ -1293,12 +1387,12 @@ const redeployStackById = async (stackId, redeployType) => {
message: errorMessage
});
logRedeployEvent({
logStackEvent({
stackId: fallbackId,
stackName: fallbackName,
status: 'error',
message: errorMessage,
endpoint: stack?.EndpointId || endpointId,
endpointId: stack?.EndpointId || endpointId,
redeployType
});
@@ -1621,10 +1715,14 @@ app.post('/api/auth/login', (req, res) => {
}
const user = findUserByIdentifier(identifier);
if (!user || !user.is_active) {
if (!user) {
return res.status(401).json({ error: 'INVALID_CREDENTIALS' });
}
if (!user.is_active) {
return res.status(403).json({ error: 'USER_INACTIVE' });
}
const passwordValid = verifyPassword(password, user.password_hash, user.password_salt);
if (!passwordValid) {
return res.status(401).json({ error: 'INVALID_CREDENTIALS' });
@@ -1731,6 +1829,63 @@ app.get('/api/users/:userId', (req, res) => {
}
});
app.put('/api/users/:userId', (req, res) => {
const { userId } = req.params;
const numericId = Number(userId);
if (!Number.isFinite(numericId) || numericId <= 0) {
return res.status(400).json({ error: 'INVALID_USER_ID' });
}
const { username, email, password, avatarColor, groupId, groupIds } = req.body ?? {};
try {
const updatedUser = updateUserDetails(numericId, {
username,
email,
password,
avatarColor,
groupId,
groupIds
});
res.json({ item: updatedUser });
} catch (error) {
if (error.code === 'INVALID_USER_ID') {
return res.status(400).json({ error: 'INVALID_USER_ID' });
}
if (error.code === 'USER_NOT_FOUND') {
return res.status(404).json({ error: 'USER_NOT_FOUND' });
}
if (error.code === 'USERNAME_REQUIRED') {
return res.status(400).json({ error: 'USERNAME_REQUIRED' });
}
if (error.code === 'INVALID_EMAIL') {
return res.status(400).json({ error: 'INVALID_EMAIL' });
}
if (error.code === 'USERNAME_TAKEN') {
return res.status(409).json({ error: 'USERNAME_TAKEN' });
}
if (error.code === 'EMAIL_TAKEN') {
return res.status(409).json({ error: 'EMAIL_TAKEN' });
}
if (error.code === 'INVALID_PASSWORD' || error.code === 'PASSWORD_TOO_SHORT') {
return res.status(400).json({ error: error.code });
}
if (error.code === 'INVALID_AVATAR_COLOR') {
return res.status(400).json({ error: 'INVALID_AVATAR_COLOR' });
}
if (error.code === 'GROUP_NOT_FOUND') {
return res.status(400).json({ error: 'GROUP_NOT_FOUND', details: error.missingGroupIds || [] });
}
if (error.code === 'GROUP_SUPERUSER_PROTECTED') {
return res.status(403).json({ error: 'GROUP_SUPERUSER_PROTECTED' });
}
console.error(`⚠️ [Users] Aktualisierung der Benutzerdaten fehlgeschlagen (${userId}):`, error);
res.status(500).json({ error: 'USER_UPDATE_FAILED' });
}
});
app.put('/api/users/:userId/groups', (req, res) => {
const { userId } = req.params;
const numericId = Number(userId);
@@ -1758,6 +1913,61 @@ app.put('/api/users/:userId/groups', (req, res) => {
}
});
app.delete('/api/users/:userId', (req, res) => {
const { userId } = req.params;
const numericId = Number(userId);
if (!Number.isFinite(numericId) || numericId <= 0) {
return res.status(400).json({ error: 'INVALID_USER_ID' });
}
try {
deleteUser(numericId);
res.json({ success: true });
} catch (error) {
if (error.code === 'INVALID_USER_ID') {
return res.status(400).json({ error: 'INVALID_USER_ID' });
}
if (error.code === 'USER_NOT_FOUND') {
return res.status(404).json({ error: 'USER_NOT_FOUND' });
}
if (error.code === 'USER_SUPERUSER_PROTECTED') {
return res.status(403).json({ error: 'USER_SUPERUSER_PROTECTED' });
}
console.error(`⚠️ [Users] Löschen fehlgeschlagen (${userId}):`, error);
res.status(500).json({ error: 'USER_DELETE_FAILED' });
}
});
app.put('/api/users/:userId/active', (req, res) => {
const { userId } = req.params;
const numericId = Number(userId);
const { isActive } = req.body ?? {};
if (!Number.isFinite(numericId) || numericId <= 0) {
return res.status(400).json({ error: 'INVALID_USER_ID' });
}
try {
const updatedUser = updateUserActiveStatus(numericId, Boolean(isActive));
res.json({ item: updatedUser });
} catch (error) {
if (error.code === 'INVALID_USER_ID') {
return res.status(400).json({ error: 'INVALID_USER_ID' });
}
if (error.code === 'USER_NOT_FOUND') {
return res.status(404).json({ error: 'USER_NOT_FOUND' });
}
if (error.code === 'USER_SUPERUSER_PROTECTED') {
return res.status(403).json({ error: 'USER_SUPERUSER_PROTECTED' });
}
console.error(`⚠️ [Users] Aktualisierung des Aktivstatus fehlgeschlagen (${userId}):`, error);
res.status(500).json({ error: 'USER_STATUS_UPDATE_FAILED' });
}
});
app.get('/api/groups', (req, res) => {
try {
const groups = listGroups();
@@ -1768,6 +1978,26 @@ app.get('/api/groups', (req, res) => {
}
});
app.get('/api/groups/:groupId', (req, res) => {
const { groupId } = req.params;
const numericId = Number(groupId);
if (!Number.isFinite(numericId) || numericId <= 0) {
return res.status(400).json({ error: 'INVALID_GROUP_ID' });
}
try {
const group = getGroupById(numericId);
if (!group) {
return res.status(404).json({ error: 'GROUP_NOT_FOUND' });
}
res.json({ item: group });
} catch (error) {
console.error(`⚠️ [Groups] Abruf der Gruppendetails fehlgeschlagen (${groupId}):`, error);
res.status(500).json({ error: 'GROUP_FETCH_FAILED' });
}
});
app.post('/api/groups', (req, res) => {
const { name, description } = req.body ?? {};
try {
@@ -1785,6 +2015,74 @@ app.post('/api/groups', (req, res) => {
}
});
app.put('/api/groups/:groupId', (req, res) => {
const { groupId } = req.params;
const numericId = Number(groupId);
if (!Number.isFinite(numericId) || numericId <= 0) {
return res.status(400).json({ error: 'INVALID_GROUP_ID' });
}
const { name, description, avatarColor } = req.body ?? {};
try {
const updatedGroup = updateGroupDetails(numericId, { name, description, avatarColor });
res.json({ item: updatedGroup });
} catch (error) {
if (error.code === 'INVALID_GROUP_ID') {
return res.status(400).json({ error: 'INVALID_GROUP_ID' });
}
if (error.code === 'GROUP_NOT_FOUND') {
return res.status(404).json({ error: 'GROUP_NOT_FOUND' });
}
if (error.code === 'GROUP_NAME_REQUIRED') {
return res.status(400).json({ error: 'GROUP_NAME_REQUIRED' });
}
if (error.code === 'GROUP_NAME_TAKEN') {
return res.status(409).json({ error: 'GROUP_NAME_TAKEN' });
}
if (error.code === 'INVALID_AVATAR_COLOR') {
return res.status(400).json({ error: 'INVALID_AVATAR_COLOR' });
}
if (error.code === 'GROUP_SUPERUSER_PROTECTED') {
return res.status(403).json({ error: 'GROUP_SUPERUSER_PROTECTED' });
}
console.error(`⚠️ [Groups] Aktualisierung der Gruppendaten fehlgeschlagen (${groupId}):`, error);
res.status(500).json({ error: 'GROUP_UPDATE_FAILED' });
}
});
app.delete('/api/groups/:groupId', (req, res) => {
const { groupId } = req.params;
const numericId = Number(groupId);
if (!Number.isFinite(numericId) || numericId <= 0) {
return res.status(400).json({ error: 'INVALID_GROUP_ID' });
}
try {
deleteGroup(numericId);
res.json({ success: true });
} catch (error) {
if (error.code === 'INVALID_GROUP_ID') {
return res.status(400).json({ error: 'INVALID_GROUP_ID' });
}
if (error.code === 'GROUP_NOT_FOUND') {
return res.status(404).json({ error: 'GROUP_NOT_FOUND' });
}
if (error.code === 'GROUP_HAS_MEMBERS') {
return res.status(409).json({ error: 'GROUP_HAS_MEMBERS', memberCount: error.memberCount || 0 });
}
if (error.code === 'GROUP_SUPERUSER_PROTECTED') {
return res.status(403).json({ error: 'GROUP_SUPERUSER_PROTECTED' });
}
console.error(`⚠️ [Groups] Löschen der Gruppe fehlgeschlagen (${groupId}):`, error);
res.status(500).json({ error: 'GROUP_DELETE_FAILED' });
}
});
// Superuser Setup
app.get('/api/auth/superuser/status', (req, res) => {
const exists = hasSuperuser();
@@ -2198,13 +2496,16 @@ app.post('/api/maintenance/duplicates/cleanup', maintenanceGuard, async (req, re
return res.status(400).json({ error: 'Keine passenden Duplikate gefunden' });
}
logRedeployEvent({
logStackEvent({
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
endpointId: target.canonical.EndpointId,
redeployType: REDEPLOY_TYPES.MAINTENANCE,
metadata: {
duplicateIds: duplicateIds
}
});
const results = [];
@@ -2238,13 +2539,16 @@ app.post('/api/maintenance/duplicates/cleanup', maintenanceGuard, async (req, re
if (errors.length) {
const failedIds = errors.map((entry) => entry.id).join(', ');
logRedeployEvent({
logStackEvent({
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
endpointId: target.canonical.EndpointId,
redeployType: REDEPLOY_TYPES.MAINTENANCE,
metadata: {
duplicateIds: duplicateIds
}
});
return res.status(500).json({
@@ -2258,13 +2562,16 @@ app.post('/api/maintenance/duplicates/cleanup', maintenanceGuard, async (req, re
});
}
logRedeployEvent({
logStackEvent({
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
endpointId: target.canonical.EndpointId,
redeployType: REDEPLOY_TYPES.MAINTENANCE,
metadata: {
removedDuplicates: results.filter((entry) => entry.status === 'deleted').map((entry) => entry.id)
}
});
res.json({
@@ -2284,7 +2591,7 @@ app.post('/api/maintenance/duplicates/cleanup', maintenanceGuard, async (req, re
}
});
// Redeploy-Logs abrufen
// Event-Logs abrufen
app.get('/api/logs', (req, res) => {
const perPageParam = req.query.perPage ?? req.query.limit;
const perPage = perPageParam === 'all' ? 'all' : Math.min(parseInt(perPageParam, 10) || 50, 500);
@@ -2293,25 +2600,36 @@ app.get('/api/logs', (req, res) => {
const limit = perPage === 'all' ? undefined : perPage;
const offset = perPage === 'all' ? 0 : (page - 1) * perPage;
const { whereClause, params } = buildLogFilter(req.query);
const { whereClause, params } = buildEventLogFilter(req.query);
const baseQuery = `
SELECT
id,
timestamp,
stack_id AS stackId,
stack_name AS stackName,
category,
event_type AS eventType,
action,
status,
severity,
entity_type AS entityType,
entity_id AS entityId,
entity_name AS entityName,
actor_type AS actorType,
actor_id AS actorId,
actor_name AS actorName,
source,
context_type AS contextType,
context_id AS contextId,
context_label AS contextLabel,
message,
endpoint,
redeploy_type AS redeployType
FROM redeploy_logs
metadata
FROM event_logs
${whereClause}
ORDER BY datetime(timestamp) DESC
`;
const countQuery = `
SELECT COUNT(*) as total
FROM redeploy_logs
FROM event_logs
${whereClause}
`;
@@ -2326,7 +2644,25 @@ app.get('/api/logs', (req, res) => {
try {
const stmt = db.prepare(query);
const logs = stmt.all(params);
const rawLogs = stmt.all(params);
const logs = rawLogs.map((row) => {
const metadata = parseJsonColumn(row.metadata);
const legacyStack = row.entityType === 'stack' ? (row.entityId ?? null) : null;
const legacyStackName = row.entityType === 'stack' ? (row.entityName ?? row.entityId ?? null) : null;
const legacyEndpoint = row.contextType === 'endpoint' ? (row.contextId ?? null) : null;
return {
...row,
entityId: row.entityId ?? null,
actorId: row.actorId ?? null,
contextId: row.contextId ?? null,
metadata,
stackId: legacyStack,
stackName: legacyStackName,
redeployType: row.eventType ?? null,
endpoint: legacyEndpoint
};
});
const total = db.prepare(countQuery).get(params)?.total ?? logs.length;
res.json({
@@ -2336,11 +2672,8 @@ app.get('/api/logs', (req, res) => {
perPage: perPage === 'all' ? 'all' : limit
});
} catch (err) {
console.error('❌ Fehler beim Abrufen der Redeploy-Logs:', err.message);
if (err.message.includes('no such table')) {
return res.status(500).json({ error: 'redeploy_logs table nicht gefunden. Bitte Migration ausführen.' });
}
res.status(500).json({ error: 'Fehler beim Abrufen der Redeploy-Logs' });
console.error('❌ Fehler beim Abrufen der Event-Logs:', err.message);
res.status(500).json({ error: 'Fehler beim Abrufen der Logs' });
}
});
@@ -2351,24 +2684,24 @@ app.delete('/api/logs/:id', (req, res) => {
}
try {
const changes = deleteLogById(id);
const changes = deleteEventLogById(id);
if (!changes) {
return res.status(404).json({ error: 'Eintrag nicht gefunden' });
}
res.json({ success: true, deleted: changes });
} catch (err) {
console.error('❌ Fehler beim Löschen des Redeploy-Logs:', err.message);
res.status(500).json({ error: 'Fehler beim Löschen des Redeploy-Logs' });
console.error('❌ Fehler beim Löschen des Event-Logs:', err.message);
res.status(500).json({ error: 'Fehler beim Löschen des Logs' });
}
});
app.delete('/api/logs', (req, res) => {
try {
const deleted = deleteLogsByFilters(req.query);
const deleted = deleteEventLogsByFilters(req.query);
res.json({ success: true, deleted });
} catch (err) {
console.error('❌ Fehler beim Löschen der Redeploy-Logs:', err.message);
res.status(500).json({ error: 'Fehler beim Löschen der Redeploy-Logs' });
console.error('❌ Fehler beim Löschen der Event-Logs:', err.message);
res.status(500).json({ error: 'Fehler beim Löschen der Logs' });
}
});
@@ -2379,13 +2712,13 @@ app.get('/api/logs/export', (req, res) => {
}
try {
const payload = exportLogsByFilters(req.query, format);
const payload = exportEventLogsByFilters(req.query, format);
res.setHeader('Content-Type', payload.contentType);
res.setHeader('Content-Disposition', `attachment; filename="${payload.filename}"`);
res.send(payload.content);
} catch (err) {
console.error('❌ Fehler beim Export der Redeploy-Logs:', err.message);
res.status(500).json({ error: 'Fehler beim Export der Redeploy-Logs' });
console.error('❌ Fehler beim Export der Event-Logs:', err.message);
res.status(500).json({ error: 'Fehler beim Export der Logs' });
}
});
@@ -2409,8 +2742,10 @@ app.put('/api/stacks/:id/redeploy', async (req, res) => {
app.put('/api/stacks/redeploy-all', maintenanceGuard, async (req, res) => {
console.log(`🚀 PUT /api/stacks/redeploy-all: Redeploy ALL gestartet`);
let endpointId;
try {
const endpointId = requireActiveEndpointId();
endpointId = requireActiveEndpointId();
const stacksRes = await axiosInstance.get('/api/stacks');
const filteredStacks = stacksRes.data.filter(stack => String(stack.EndpointId) === String(endpointId));
@@ -2427,13 +2762,21 @@ app.put('/api/stacks/redeploy-all', maintenanceGuard, async (req, res) => {
const stackSummaryList = eligibleStacks.map((stack) => `${stack.Name} (${stack.Id})`);
const stackSummary = stackSummaryList.length ? stackSummaryList.join(', ') : 'keine Stacks';
logRedeployEvent({
stackId: '---',
stackName: '---',
status: 'started',
message: `Redeploy ALL gestartet für: ${stackSummary}`,
endpoint: endpointId,
redeployType: REDEPLOY_TYPES.ALL
logEvent({
category: 'stack',
eventType: REDEPLOY_TYPES.ALL,
action: 'redeploy-alle',
status: 'gestartet',
entityType: 'bulk-operation',
entityId: 'redeploy-alle',
entityName: 'Redeploy ALLE',
contextType: 'endpoint',
contextId: String(endpointId),
message: `Redeploy ALLE gestartet für: ${stackSummary}`,
metadata: {
stacks: stackSummaryList
},
source: 'system'
});
if (!eligibleStacks.length) {
@@ -2458,27 +2801,40 @@ app.put('/api/stacks/redeploy-all', maintenanceGuard, async (req, res) => {
}
}
logRedeployEvent({
stackId: '---',
stackName: '---',
status: 'success',
message: 'Redeploy ALL abgeschlossen',
endpoint: endpointId,
redeployType: REDEPLOY_TYPES.ALL
logEvent({
category: 'stack',
eventType: REDEPLOY_TYPES.ALL,
action: 'redeploy-alle',
status: 'erfolgreich',
entityType: 'bulk-operation',
entityId: 'redeploy-alle',
entityName: 'Redeploy ALLE',
contextType: 'endpoint',
contextId: String(endpointId),
message: 'Redeploy ALLE abgeschlossen',
metadata: {
processedStacks: stackSummaryList
},
source: 'system'
});
res.json({ success: true, message: 'Redeploy ALL abgeschlossen' });
} catch (err) {
const message = err.response?.data?.message || err.message;
logRedeployEvent({
stackId: '---',
stackName: '---',
logEvent({
category: 'stack',
eventType: REDEPLOY_TYPES.ALL,
action: 'redeploy-alle',
status: 'error',
entityType: 'bulk-operation',
entityId: 'redeploy-alle',
entityName: 'Redeploy ALLE',
contextType: endpointId !== undefined && endpointId !== null ? 'endpoint' : null,
contextId: endpointId !== undefined && endpointId !== null ? String(endpointId) : null,
message,
endpoint: endpointId,
redeployType: REDEPLOY_TYPES.ALL
source: 'system'
});
console.error('❌ Fehler bei Redeploy ALL:', message);
console.error('❌ Fehler bei Redeploy ALLE:', message);
res.status(500).json({ error: message });
}
});
@@ -2493,8 +2849,10 @@ app.put('/api/stacks/redeploy-selection', maintenanceGuard, async (req, res) =>
return res.status(400).json({ error: 'stackIds muss eine nicht leere Array sein' });
}
let endpointId;
try {
const endpointId = requireActiveEndpointId();
endpointId = requireActiveEndpointId();
const normalizedIds = stackIds.map((id) => String(id));
const { filteredStacks } = await loadStackCollections();
const stacksById = new Map(filteredStacks.map((stack) => [String(stack.Id), stack]));
@@ -2520,23 +2878,40 @@ app.put('/api/stacks/redeploy-selection', maintenanceGuard, async (req, res) =>
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',
logEvent({
category: 'stack',
eventType: REDEPLOY_TYPES.SELECTION,
action: 'redeploy-auswahl',
status: 'gestartet',
entityType: 'bulk-operation',
entityId: 'redeploy-auswahl',
entityName: `Redeploy Auswahl (${stackIds.length})`,
contextType: 'endpoint',
contextId: String(endpointId),
message: `Redeploy Auswahl gestartet für: ${summaryText}`,
endpoint: endpointId,
redeployType: REDEPLOY_TYPES.SELECTION
metadata: {
requestedStackIds: normalizedIds
},
source: 'system'
});
if (!eligibleStacks.length) {
logRedeployEvent({
stackId: stackIds.join(','),
stackName: `Auswahl (${stackIds.length})`,
status: 'success',
logEvent({
category: 'stack',
eventType: REDEPLOY_TYPES.SELECTION,
action: 'redeploy-auswahl',
status: 'erfolgreich',
entityType: 'bulk-operation',
entityId: 'redeploy-auswahl',
entityName: `Redeploy Auswahl (${stackIds.length})`,
contextType: 'endpoint',
contextId: String(endpointId),
message: 'Redeploy Auswahl übersprungen: keine veralteten Stacks',
endpoint: endpointId,
redeployType: REDEPLOY_TYPES.SELECTION
metadata: {
requestedStackIds: normalizedIds,
skipped: true
},
source: 'system'
});
return res.json({ success: true, message: 'Keine veralteten Stacks in der Auswahl' });
}
@@ -2558,25 +2933,42 @@ app.put('/api/stacks/redeploy-selection', maintenanceGuard, async (req, res) =>
}
}
logRedeployEvent({
stackId: stackIds.join(','),
stackName: `Auswahl (${stackIds.length})`,
status: 'success',
logEvent({
category: 'stack',
eventType: REDEPLOY_TYPES.SELECTION,
action: 'redeploy-auswahl',
status: 'erfolgreich',
entityType: 'bulk-operation',
entityId: 'redeploy-auswahl',
entityName: `Redeploy Auswahl (${stackIds.length})`,
contextType: 'endpoint',
contextId: String(endpointId),
message: 'Redeploy Auswahl abgeschlossen',
endpoint: endpointId,
redeployType: REDEPLOY_TYPES.SELECTION
metadata: {
processedStackIds: eligibleStacks.map((stack) => String(stack.Id))
},
source: 'system'
});
res.json({ success: true, message: 'Redeploy Auswahl abgeschlossen' });
} catch (err) {
const message = err.response?.data?.message || err.message;
logRedeployEvent({
stackId: Array.isArray(stackIds) ? stackIds.join(',') : String(stackIds ?? ''),
stackName: `Auswahl (${Array.isArray(stackIds) ? stackIds.length : 0})`,
status: 'error',
const normalized = Array.isArray(stackIds) ? stackIds.map((id) => String(id)) : [];
logEvent({
category: 'stack',
eventType: REDEPLOY_TYPES.SELECTION,
action: 'redeploy-auswahl',
status: 'fehler',
entityType: 'bulk-operation',
entityId: 'redeploy-auswahl',
entityName: `Redeploy Auswahl (${Array.isArray(stackIds) ? stackIds.length : 0})`,
contextType: endpointId !== undefined && endpointId !== null ? 'endpoint' : null,
contextId: endpointId !== undefined && endpointId !== null ? String(endpointId) : null,
message,
endpoint: endpointId,
redeployType: REDEPLOY_TYPES.SELECTION
metadata: {
requestedStackIds: normalized
},
source: 'system'
});
console.error('❌ Fehler bei Redeploy Auswahl:', message);
res.status(500).json({ error: message });