Merge feature/Logging into dev

This commit is contained in:
root
2025-10-01 11:32:33 +00:00
3 changed files with 82 additions and 24 deletions
+36 -6
View File
@@ -62,6 +62,8 @@ const REDEPLOY_TYPES = {
SELECTION: 'Auswahl' SELECTION: 'Auswahl'
}; };
const SELF_STACK_ID = process.env.SELF_STACK_ID ? String(process.env.SELF_STACK_ID) : null;
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`);
@@ -76,7 +78,9 @@ const filterOutdatedStacks = async (stacks = []) => {
const results = await Promise.all( const results = await Promise.all(
stacks.map(async (stack) => ({ stacks.map(async (stack) => ({
stack, stack,
outdated: await isStackOutdated(stack.Id) outdated: SELF_STACK_ID && String(stack.Id) === SELF_STACK_ID
? false
: await isStackOutdated(stack.Id)
})) }))
); );
@@ -95,11 +99,31 @@ app.get('/api/stacks', async (req, res) => {
const stacksRes = await axiosInstance.get('/api/stacks'); const stacksRes = await axiosInstance.get('/api/stacks');
const filteredStacks = stacksRes.data.filter(stack => stack.EndpointId === ENDPOINT_ID); const filteredStacks = stacksRes.data.filter(stack => stack.EndpointId === ENDPOINT_ID);
const uniqueStacksMap = {}; const stacksByName = new Map();
const duplicateNames = new Set();
filteredStacks.forEach(stack => { filteredStacks.forEach(stack => {
if (!uniqueStacksMap[stack.Name]) uniqueStacksMap[stack.Name] = stack; const name = stack.Name;
const isSelf = SELF_STACK_ID && String(stack.Id) === SELF_STACK_ID;
const existingEntry = stacksByName.get(name);
if (!existingEntry) {
stacksByName.set(name, { stack, isSelf });
return;
}
duplicateNames.add(name);
if (!existingEntry.isSelf && isSelf) {
stacksByName.set(name, { stack, isSelf });
}
}); });
const uniqueStacks = Object.values(uniqueStacksMap);
const uniqueStacks = Array.from(stacksByName.values()).map(entry => entry.stack);
if (duplicateNames.size) {
console.warn(`⚠️ [API] GET /api/stacks: Doppelte Stack-Namen erkannt: ${Array.from(duplicateNames).join(', ')}`);
}
const stacksWithStatus = await Promise.all( const stacksWithStatus = await Promise.all(
uniqueStacks.map(async (stack) => { uniqueStacks.map(async (stack) => {
@@ -111,11 +135,17 @@ app.get('/api/stacks', async (req, res) => {
return { return {
...stack, ...stack,
updateStatus: statusEmoji, updateStatus: statusEmoji,
redeploying: redeployingStacks[stack.Id] || false redeploying: redeployingStacks[stack.Id] || false,
redeployDisabled: SELF_STACK_ID ? String(stack.Id) === SELF_STACK_ID : false
}; };
} 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);
return { ...stack, updateStatus: '❌', redeploying: redeployingStacks[stack.Id] || false }; return {
...stack,
updateStatus: '❌',
redeploying: redeployingStacks[stack.Id] || false,
redeployDisabled: SELF_STACK_ID ? String(stack.Id) === SELF_STACK_ID : false
};
} }
}) })
); );
+1
View File
@@ -12,6 +12,7 @@ services:
PORTAINER_URL: "Your_Portainer_Server_Address" PORTAINER_URL: "Your_Portainer_Server_Address"
PORTAINER_API_KEY: "Your_Portainer_API_Key" PORTAINER_API_KEY: "Your_Portainer_API_Key"
PORTAINER_ENDPOINT_ID: "Your_Portainer_Endpoint_ID" PORTAINER_ENDPOINT_ID: "Your_Portainer_Endpoint_ID"
SELF_STACK_ID: "Stackpulse ID"
restart: unless-stopped restart: unless-stopped
volumes: volumes:
+45 -18
View File
@@ -8,6 +8,20 @@ export default function Stacks() {
const [error, setError] = useState(""); const [error, setError] = useState("");
const [selectedStackIds, setSelectedStackIds] = useState([]); const [selectedStackIds, setSelectedStackIds] = useState([]);
const mergeStackState = (previousStacks, incomingStacks) => {
const prevMap = new Map(previousStacks.map((stack) => [stack.Id, stack]));
const sortedIncoming = [...incomingStacks].sort((a, b) => a.Name.localeCompare(b.Name));
return sortedIncoming.map((stack) => {
const previous = prevMap.get(stack.Id);
return {
...stack,
redeploying: previous?.redeploying || stack.redeploying || false,
redeployDisabled: stack.redeployDisabled ?? previous?.redeployDisabled ?? false
};
});
};
useEffect(() => { useEffect(() => {
const socket = io("/", { const socket = io("/", {
path: "/socket.io", path: "/socket.io",
@@ -18,22 +32,25 @@ export default function Stacks() {
socket.on("redeployStatus", async ({ stackId, status }) => { socket.on("redeployStatus", async ({ stackId, status }) => {
console.log(`🔄 Stack ${stackId} Redeploy Status: ${status ? "running" : "finished"}`); console.log(`🔄 Stack ${stackId} Redeploy Status: ${status ? "running" : "finished"}`);
setStacks(prev =>
prev.map(stack => {
if (stack.Id !== stackId) return stack;
return {
...stack,
redeploying: status,
updateStatus: status ? stack.updateStatus : '✅'
};
})
);
if (!status) { if (!status) {
// Status nach Redeploy neu vom Server holen // Status nach Redeploy neu vom Server holen
try { try {
const res = await axios.get("/api/stacks"); const res = await axios.get("/api/stacks");
const sortedStacks = [...res.data].sort((a, b) => a.Name.localeCompare(b.Name)); setStacks(prev => mergeStackState(prev, res.data));
setStacks(sortedStacks.map(stack => ({ ...stack, redeploying: stack.redeploying || false })));
} catch (err) { } catch (err) {
console.error("Fehler beim Aktualisieren nach Redeploy:", err); console.error("Fehler beim Aktualisieren nach Redeploy:", err);
} }
} else {
// UI direkt auf redeploying setzen
setStacks(prev =>
prev.map(stack =>
stack.Id === stackId ? { ...stack, redeploying: true } : stack
)
);
} }
}); });
@@ -44,8 +61,7 @@ export default function Stacks() {
setLoading(true); setLoading(true);
try { try {
const res = await axios.get("/api/stacks"); const res = await axios.get("/api/stacks");
const sortedStacks = [...res.data].sort((a, b) => a.Name.localeCompare(b.Name)); setStacks(prev => mergeStackState(prev, res.data));
setStacks(sortedStacks.map(stack => ({ ...stack, redeploying: stack.redeploying || false })));
} catch (err) { } catch (err) {
console.error("❌ Fehler beim Abrufen der Stacks:", err); console.error("❌ Fehler beim Abrufen der Stacks:", err);
setError("Fehler beim Laden der Stacks"); setError("Fehler beim Laden der Stacks");
@@ -62,7 +78,7 @@ 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 !== '✅'; return match && match.updateStatus !== '✅' && !match.redeployDisabled;
}); });
return filtered.length === prev.length ? prev : filtered; return filtered.length === prev.length ? prev : filtered;
}); });
@@ -78,6 +94,9 @@ export default function Stacks() {
}; };
const handleRedeploy = async (stackId) => { const handleRedeploy = async (stackId) => {
const targetStack = stacks.find((stack) => stack.Id === stackId);
if (targetStack?.redeployDisabled) 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) =>
@@ -99,7 +118,7 @@ export default function Stacks() {
}; };
const handleRedeployAll = async () => { const handleRedeployAll = async () => {
const outdatedStacks = stacks.filter((stack) => stack.updateStatus !== '✅'); const outdatedStacks = stacks.filter((stack) => stack.updateStatus !== '✅' && !stack.redeployDisabled);
if (!outdatedStacks.length) return; if (!outdatedStacks.length) return;
const outdatedIds = new Set(outdatedStacks.map((stack) => stack.Id)); const outdatedIds = new Set(outdatedStacks.map((stack) => stack.Id));
@@ -133,7 +152,7 @@ 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 !== '✅'; return stack && stack.updateStatus !== '✅' && !stack.redeployDisabled;
}); });
if (!eligibleIds.length) { if (!eligibleIds.length) {
@@ -168,7 +187,7 @@ export default function Stacks() {
}; };
const hasSelection = selectedStackIds.length > 0; const hasSelection = selectedStackIds.length > 0;
const hasOutdatedStacks = stacks.some((stack) => stack.updateStatus !== '✅'); const hasOutdatedStacks = stacks.some((stack) => stack.updateStatus !== '✅' && !stack.redeployDisabled);
const bulkButtonLabel = hasSelection const bulkButtonLabel = hasSelection
? `Redeploy Auswahl (${selectedStackIds.length})` ? `Redeploy Auswahl (${selectedStackIds.length})`
: 'Redeploy All'; : 'Redeploy All';
@@ -176,9 +195,9 @@ export default function Stacks() {
const bulkActionDisabled = hasSelection const bulkActionDisabled = hasSelection
? selectedStackIds.length === 0 || selectedStackIds.every(id => { ? 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 === '✅'; return !targetStack || targetStack.redeploying || targetStack.updateStatus === '✅' || targetStack.redeployDisabled;
}) })
: !hasOutdatedStacks || stacks.every(stack => stack.updateStatus !== '✅' || stack.redeploying); : !hasOutdatedStacks || stacks.every(stack => stack.updateStatus !== '✅' || stack.redeploying || stack.redeployDisabled);
const handleBulkRedeploy = () => { const handleBulkRedeploy = () => {
if (hasSelection) { if (hasSelection) {
@@ -208,7 +227,8 @@ export default function Stacks() {
const isRedeploying = stack.redeploying; const isRedeploying = stack.redeploying;
const isSelected = selectedStackIds.includes(stack.Id); const isSelected = selectedStackIds.includes(stack.Id);
const isCurrent = stack.updateStatus === '✅'; const isCurrent = stack.updateStatus === '✅';
const isSelectable = !isRedeploying && !isCurrent; const isSelfStack = Boolean(stack.redeployDisabled);
const isSelectable = !isRedeploying && !isCurrent && !isSelfStack;
return ( return (
<div <div
@@ -243,6 +263,11 @@ export default function Stacks() {
<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>
</> </>
) : isSelfStack ? (
<>
<span className="text-xs uppercase tracking-wide text-gray-400">System</span>
<span className="text-gray-300">Redeploy deaktiviert</span>
</>
) : isCurrent ? ( ) : isCurrent ? (
<> <>
<span className="text-xs uppercase tracking-wide text-gray-400">Status</span> <span className="text-xs uppercase tracking-wide text-gray-400">Status</span>
@@ -250,6 +275,8 @@ export default function Stacks() {
</> </>
) : ( ) : (
<> <>
<span className="text-xs uppercase tracking-wide text-gray-400">Status</span>
<span className="text-amber-300">Veraltet</span>
<button <button
onClick={() => handleRedeploy(stack.Id)} onClick={() => handleRedeploy(stack.Id)}
disabled={isRedeploying} disabled={isRedeploying}