7 Commits

Author SHA1 Message Date
root 3ede88b4f2 Hotfix 2025-10-12 09:20:22 +00:00
root 8db6a51ebb fix Version 2025-10-12 09:11:46 +00:00
root 4cd55a8f15 Fix Queue 2025-10-12 09:08:41 +00:00
root 827c50f674 Sync README from dev 2025-10-09 08:58:45 +00:00
root f9e8c36433 Merge dev into master 2025-10-02 19:15:26 +00:00
root 6d4e340185 Merge dev into master 2025-10-02 19:14:05 +00:00
root a81fde24f6 Merge dev into master 2025-10-02 18:57:26 +00:00
4 changed files with 169 additions and 61 deletions
+6 -10
View File
@@ -26,7 +26,7 @@ Ziel:
</details> </details>
<details> <details open>
<summary>✅ v0.2 Release</summary> <summary>✅ v0.2 Release</summary>
### Backend ### Backend
@@ -50,23 +50,19 @@ Ziel:
<summary>🟡 v0.3 In Entwicklung</summary> <summary>🟡 v0.3 In Entwicklung</summary>
### Backend ### Backend
- [ ] Scheduler-Service mit Cron/Timer (Jobs erstellen, starten, stoppen, löschen)
- [ ] Speicherung der Scheduler-Jobs in SQLite (Stack-ID, Zeit, Status, History)
- [ ] API-Endpunkte für Scheduler-Verwaltung (CRUD + Statusabfrage)
- [ ] Automatische Datenbereinigung: Duplikate bei Stack-IDs erkennen & entfernen - [ ] Automatische Datenbereinigung: Duplikate bei Stack-IDs erkennen & entfernen
- [ ] Erweiterung der Logs - [ ] Erweiterung der Logs
- [ ] API für Filter & Suche (Stacks nach Name/Status abrufen) - [x] API für Filter & Suche (Stacks nach Name/Status abrufen)
- [ ] API für Sortierung der Stacks
### Frontend ### Frontend
- [ ] UI für Scheduler (Stack auswählen, Zeit festlegen, Übersicht der Jobs) - [x] Filter: Stacks nach Name oder Status durchsuchen
- [ ] Filter: Stacks nach Name oder Status durchsuchen - [ ] Sortierung: Stacks im Frontend sortieren
- [ ] Benachrichtigungen im UI: erfolgreicher/fehlgeschlagener Redeploy (Toast + Notification-Center) - [ ] Benachrichtigungen im UI: erfolgreicher/fehlgeschlagener Redeploy (Toast + Notification-Center)
- [ ] Anzeige & Verwaltung der Scheduler-Jobs (Tabelle mit Status, Pause/Resume/Delete)
- [ ] Visualisierung der Datenbereinigung (Konflikt/Auto-Fix Meldungen) - [ ] Visualisierung der Datenbereinigung (Konflikt/Auto-Fix Meldungen)
### Features ### Features
- [ ] Automatische Redeploys nach Zeitplan (einmalig oder wiederkehrend) - [ ] Frontend-Filter und Sortierungen für schnellere Navigation bei vielen Stacks
- [ ] Frontend-Filter für schnellere Navigation bei vielen Stacks
- [ ] Echtzeit-Feedback im UI (Notifications) - [ ] Echtzeit-Feedback im UI (Notifications)
- [ ] Datenkonsistenz sichern: keine doppelten Stack-IDs mehr - [ ] Datenkonsistenz sichern: keine doppelten Stack-IDs mehr
+50 -16
View File
@@ -39,6 +39,12 @@ const axiosInstance = axios.create({
baseURL: process.env.PORTAINER_URL, baseURL: process.env.PORTAINER_URL,
}); });
const REDEPLOY_STATUS = {
IDLE: 'idle',
QUEUED: 'queued',
RUNNING: 'running'
};
const redeployingStacks = {}; const redeployingStacks = {};
const server = http.createServer(app); const server = http.createServer(app);
@@ -51,9 +57,22 @@ io.on("connection", (socket) => {
}); });
const broadcastRedeployStatus = (stackId, status) => { const broadcastRedeployStatus = (stackId, status) => {
redeployingStacks[stackId] = status; if (!status || status === REDEPLOY_STATUS.IDLE) {
io.emit("redeployStatus", { stackId, status }); delete redeployingStacks[stackId];
console.log(`🔄 [RedeployStatus] Stack ${stackId} ist jetzt ${status ? "im Redeploy" : "fertig"}`); } else {
redeployingStacks[stackId] = status;
}
const payloadStatus = status || REDEPLOY_STATUS.IDLE;
io.emit("redeployStatus", { stackId, status: payloadStatus });
let statusText = 'fertig';
if (payloadStatus === REDEPLOY_STATUS.RUNNING) {
statusText = 'im Redeploy';
} else if (payloadStatus === REDEPLOY_STATUS.QUEUED) {
statusText = 'in der Warteschlange';
}
console.log(`🔄 [RedeployStatus] Stack ${stackId} ist jetzt ${statusText}`);
}; };
const REDEPLOY_TYPES = { const REDEPLOY_TYPES = {
@@ -127,6 +146,14 @@ app.get('/api/stacks', async (req, res) => {
const stacksWithStatus = await Promise.all( const stacksWithStatus = await Promise.all(
uniqueStacks.map(async (stack) => { uniqueStacks.map(async (stack) => {
const redeployState = redeployingStacks[stack.Id] || REDEPLOY_STATUS.IDLE;
const redeployCommon = {
redeployState,
redeploying: redeployState === REDEPLOY_STATUS.RUNNING,
redeployQueued: redeployState === REDEPLOY_STATUS.QUEUED,
redeployDisabled: SELF_STACK_ID ? String(stack.Id) === SELF_STACK_ID : false
};
try { try {
const statusRes = await axiosInstance.get( const statusRes = await axiosInstance.get(
`/api/stacks/${stack.Id}/images_status?refresh=true` `/api/stacks/${stack.Id}/images_status?refresh=true`
@@ -135,21 +162,20 @@ app.get('/api/stacks', async (req, res) => {
return { return {
...stack, ...stack,
updateStatus: statusEmoji, updateStatus: statusEmoji,
redeploying: redeployingStacks[stack.Id] || false, ...redeployCommon
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 { return {
...stack, ...stack,
updateStatus: '❌', updateStatus: '❌',
redeploying: redeployingStacks[stack.Id] || false, ...redeployCommon
redeployDisabled: SELF_STACK_ID ? String(stack.Id) === SELF_STACK_ID : false
}; };
} }
}) })
); );
stacksWithStatus.sort((a, b) => a.Name.localeCompare(b.Name)); stacksWithStatus.sort((a, b) => a.Name.localeCompare(b.Name));
console.log(`✅ GET /api/stacks: Abruf erfolgreich, ${stacksWithStatus.length} Stacks geladen`); console.log(`✅ GET /api/stacks: Abruf erfolgreich, ${stacksWithStatus.length} Stacks geladen`);
res.json(stacksWithStatus); res.json(stacksWithStatus);
@@ -271,7 +297,7 @@ app.put('/api/stacks/:id/redeploy', async (req, res) => {
let stack; let stack;
try { try {
broadcastRedeployStatus(id, true); broadcastRedeployStatus(id, REDEPLOY_STATUS.RUNNING);
const stackRes = await axiosInstance.get(`/api/stacks/${id}`); const stackRes = await axiosInstance.get(`/api/stacks/${id}`);
stack = stackRes.data; stack = stackRes.data;
@@ -318,7 +344,7 @@ app.put('/api/stacks/:id/redeploy', async (req, res) => {
); );
} }
broadcastRedeployStatus(id, false); broadcastRedeployStatus(id, REDEPLOY_STATUS.IDLE);
logRedeployEvent({ logRedeployEvent({
stackId: stack.Id || id, stackId: stack.Id || id,
stackName: stack.Name, stackName: stack.Name,
@@ -330,7 +356,7 @@ app.put('/api/stacks/:id/redeploy', async (req, res) => {
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) {
broadcastRedeployStatus(id, false); broadcastRedeployStatus(id, REDEPLOY_STATUS.IDLE);
const errorMessage = err.response?.data?.message || err.message; const errorMessage = err.response?.data?.message || err.message;
logRedeployEvent({ logRedeployEvent({
stackId: stack?.Id || id, stackId: stack?.Id || id,
@@ -387,9 +413,13 @@ app.put('/api/stacks/redeploy-all', async (req, res) => {
return res.json({ success: true, message: 'Keine veralteten Stacks für Redeploy ALL' }); return res.json({ success: true, message: 'Keine veralteten Stacks für Redeploy ALL' });
} }
eligibleStacks.forEach((stack) => {
broadcastRedeployStatus(stack.Id, REDEPLOY_STATUS.QUEUED);
});
for (const stack of eligibleStacks) { for (const stack of eligibleStacks) {
try { try {
broadcastRedeployStatus(stack.Id, true); broadcastRedeployStatus(stack.Id, REDEPLOY_STATUS.RUNNING);
logRedeployEvent({ logRedeployEvent({
stackId: stack.Id, stackId: stack.Id,
stackName: stack.Name, stackName: stack.Name,
@@ -414,7 +444,7 @@ app.put('/api/stacks/redeploy-all', async (req, res) => {
} }
} }
broadcastRedeployStatus(stack.Id, false); broadcastRedeployStatus(stack.Id, REDEPLOY_STATUS.IDLE);
logRedeployEvent({ logRedeployEvent({
stackId: stack.Id, stackId: stack.Id,
stackName: stack.Name, stackName: stack.Name,
@@ -425,7 +455,7 @@ app.put('/api/stacks/redeploy-all', async (req, res) => {
}); });
console.log(`✅ Redeploy abgeschlossen: ${stack.Name}`); console.log(`✅ Redeploy abgeschlossen: ${stack.Name}`);
} catch (err) { } catch (err) {
broadcastRedeployStatus(stack.Id, false); broadcastRedeployStatus(stack.Id, REDEPLOY_STATUS.IDLE);
const errorMessage = err.response?.data?.message || err.message; const errorMessage = err.response?.data?.message || err.message;
logRedeployEvent({ logRedeployEvent({
stackId: stack.Id, stackId: stack.Id,
@@ -509,9 +539,13 @@ app.put('/api/stacks/redeploy-selection', async (req, res) => {
return res.json({ success: true, message: 'Keine veralteten Stacks in der Auswahl' }); return res.json({ success: true, message: 'Keine veralteten Stacks in der Auswahl' });
} }
eligibleStacks.forEach((stack) => {
broadcastRedeployStatus(stack.Id, REDEPLOY_STATUS.QUEUED);
});
for (const stack of eligibleStacks) { for (const stack of eligibleStacks) {
try { try {
broadcastRedeployStatus(stack.Id, true); broadcastRedeployStatus(stack.Id, REDEPLOY_STATUS.RUNNING);
logRedeployEvent({ logRedeployEvent({
stackId: stack.Id, stackId: stack.Id,
stackName: stack.Name, stackName: stack.Name,
@@ -536,7 +570,7 @@ app.put('/api/stacks/redeploy-selection', async (req, res) => {
} }
} }
broadcastRedeployStatus(stack.Id, false); broadcastRedeployStatus(stack.Id, REDEPLOY_STATUS.IDLE);
logRedeployEvent({ logRedeployEvent({
stackId: stack.Id, stackId: stack.Id,
stackName: stack.Name, stackName: stack.Name,
@@ -547,7 +581,7 @@ app.put('/api/stacks/redeploy-selection', async (req, res) => {
}); });
console.log(`✅ Redeploy Auswahl abgeschlossen: ${stack.Name}`); console.log(`✅ Redeploy Auswahl abgeschlossen: ${stack.Name}`);
} catch (err) { } catch (err) {
broadcastRedeployStatus(stack.Id, false); broadcastRedeployStatus(stack.Id, REDEPLOY_STATUS.IDLE);
const errorMessage = err.response?.data?.message || err.message; const errorMessage = err.response?.data?.message || err.message;
logRedeployEvent({ logRedeployEvent({
stackId: stack.Id, stackId: stack.Id,
+1 -1
View File
@@ -17,7 +17,7 @@ export default function App() {
<div className="max-w-6xl mx-auto px-6 py-6"> <div className="max-w-6xl mx-auto px-6 py-6">
<div className="flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between"> <div className="flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
<div className="flex flex-col items-end gap-1"> <div className="flex flex-col items-end gap-1">
<span className="text-xs text-gray-500">v0.2</span> <span className="text-xs text-gray-500">v0.2.1</span>
<img src={logo} alt="StackPulse" className="h-10 w-auto" /> <img src={logo} alt="StackPulse" className="h-10 w-auto" />
</div> </div>
<nav className="flex gap-2 items-end"> <nav className="flex gap-2 items-end">
+109 -31
View File
@@ -2,6 +2,12 @@ import React, { useEffect, useState } from "react";
import axios from "axios"; import axios from "axios";
import { io } from "socket.io-client"; import { io } from "socket.io-client";
const REDEPLOY_STATE = {
IDLE: "idle",
QUEUED: "queued",
RUNNING: "running"
};
export default function Stacks() { export default function Stacks() {
const [stacks, setStacks] = useState([]); const [stacks, setStacks] = useState([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@@ -14,11 +20,16 @@ export default function Stacks() {
return sortedIncoming.map((stack) => { return sortedIncoming.map((stack) => {
const previous = prevMap.get(stack.Id); const previous = prevMap.get(stack.Id);
const redeployState = stack.redeployState
?? previous?.redeployState
?? (previous?.redeploying ? REDEPLOY_STATE.RUNNING : REDEPLOY_STATE.IDLE);
return { return {
...stack, ...stack,
redeploying: previous?.redeploying || stack.redeploying || false, redeployState,
redeployDisabled: stack.redeployDisabled ?? previous?.redeployDisabled ?? false, redeploying: redeployState === REDEPLOY_STATE.RUNNING,
duplicateName: stack.duplicateName ?? previous?.duplicateName ?? false redeployQueued: redeployState === REDEPLOY_STATE.QUEUED,
redeployDisabled: stack.redeployDisabled ?? previous?.redeployDisabled ?? false
}; };
}); });
}; };
@@ -31,21 +42,26 @@ export default function Stacks() {
console.log("🔌 Socket connected"); console.log("🔌 Socket connected");
socket.on("redeployStatus", async ({ stackId, status }) => { socket.on("redeployStatus", async ({ stackId, status }) => {
console.log(`🔄 Stack ${stackId} Redeploy Status: ${status ? "running" : "finished"}`); const nextState = status || REDEPLOY_STATE.IDLE;
console.log(`🔄 Stack ${stackId} Redeploy Status: ${nextState}`);
setStacks(prev => setStacks(prev =>
prev.map(stack => { prev.map(stack => {
if (stack.Id !== stackId) return stack; if (stack.Id !== stackId) return stack;
return { const updated = {
...stack, ...stack,
redeploying: status, redeployState: nextState,
updateStatus: status ? stack.updateStatus : '✅' redeploying: nextState === REDEPLOY_STATE.RUNNING,
redeployQueued: nextState === REDEPLOY_STATE.QUEUED
}; };
if (nextState === REDEPLOY_STATE.IDLE) {
updated.updateStatus = "✅";
}
return updated;
}) })
); );
if (!status) { if (nextState === REDEPLOY_STATE.IDLE) {
// Status nach Redeploy neu vom Server holen
try { try {
const res = await axios.get("/api/stacks"); const res = await axios.get("/api/stacks");
setStacks(prev => mergeStackState(prev, res.data)); setStacks(prev => mergeStackState(prev, res.data));
@@ -79,7 +95,10 @@ 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; return match
&& match.updateStatus !== "✅"
&& !match.redeployDisabled
&& (match.redeployState ?? REDEPLOY_STATE.IDLE) === REDEPLOY_STATE.IDLE;
}); });
return filtered.length === prev.length ? prev : filtered; return filtered.length === prev.length ? prev : filtered;
}); });
@@ -95,10 +114,20 @@ export default function Stacks() {
}; };
const handleRedeploy = async (stackId) => { const handleRedeploy = async (stackId) => {
const targetStack = stacks.find((stack) => stack.Id === stackId);
if (!targetStack || targetStack.redeployDisabled || targetStack.redeployState !== REDEPLOY_STATE.IDLE) 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,
redeployState: REDEPLOY_STATE.RUNNING,
redeploying: true,
redeployQueued: false
}
: stack
) )
); );
@@ -109,14 +138,25 @@ 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,
redeployState: REDEPLOY_STATE.IDLE,
redeploying: false,
redeployQueued: false
}
: stack
) )
); );
} }
}; };
const handleRedeployAll = async () => { const handleRedeployAll = async () => {
const outdatedStacks = stacks.filter((stack) => stack.updateStatus !== '✅' && !stack.redeployDisabled); const outdatedStacks = stacks.filter((stack) =>
stack.updateStatus !== "✅"
&& !stack.redeployDisabled
&& (stack.redeployState ?? REDEPLOY_STATE.IDLE) === REDEPLOY_STATE.IDLE
);
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));
@@ -124,7 +164,12 @@ export default function Stacks() {
setStacks(prev => setStacks(prev =>
prev.map(stack => prev.map(stack =>
outdatedIds.has(stack.Id) outdatedIds.has(stack.Id)
? { ...stack, redeploying: true } ? {
...stack,
redeployState: REDEPLOY_STATE.QUEUED,
redeploying: false,
redeployQueued: true
}
: stack : stack
) )
); );
@@ -138,7 +183,12 @@ export default function Stacks() {
setStacks(prev => setStacks(prev =>
prev.map(stack => prev.map(stack =>
outdatedIds.has(stack.Id) outdatedIds.has(stack.Id)
? { ...stack, redeploying: false } ? {
...stack,
redeployState: REDEPLOY_STATE.IDLE,
redeploying: false,
redeployQueued: false
}
: stack : stack
) )
); );
@@ -150,7 +200,10 @@ 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; return stack
&& stack.updateStatus !== "✅"
&& !stack.redeployDisabled
&& (stack.redeployState ?? REDEPLOY_STATE.IDLE) === REDEPLOY_STATE.IDLE;
}); });
if (!eligibleIds.length) { if (!eligibleIds.length) {
@@ -163,7 +216,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,
redeployState: REDEPLOY_STATE.QUEUED,
redeploying: false,
redeployQueued: true
}
: stack : stack
) )
); );
@@ -177,7 +235,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,
redeployState: REDEPLOY_STATE.IDLE,
redeploying: false,
redeployQueued: false
}
: stack : stack
) )
); );
@@ -185,17 +248,24 @@ export default function Stacks() {
}; };
const hasSelection = selectedStackIds.length > 0; const hasSelection = selectedStackIds.length > 0;
const hasOutdatedStacks = stacks.some((stack) => stack.updateStatus !== '✅' && !stack.redeployDisabled); const hasRedeployableStacks = stacks.some((stack) =>
stack.updateStatus !== "✅"
&& !stack.redeployDisabled
&& (stack.redeployState ?? REDEPLOY_STATE.IDLE) === REDEPLOY_STATE.IDLE
);
const bulkButtonLabel = hasSelection const bulkButtonLabel = hasSelection
? `Redeploy Auswahl (${selectedStackIds.length})` ? `Redeploy Auswahl (${selectedStackIds.length})`
: 'Redeploy All'; : "Redeploy All";
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 === '✅' || targetStack.redeployDisabled; return !targetStack
|| targetStack.updateStatus === "✅"
|| targetStack.redeployDisabled
|| (targetStack.redeployState ?? REDEPLOY_STATE.IDLE) !== REDEPLOY_STATE.IDLE;
}) })
: !hasOutdatedStacks || stacks.every(stack => stack.updateStatus !== '✅' || stack.redeploying || stack.redeployDisabled); : !hasRedeployableStacks;
const handleBulkRedeploy = () => { const handleBulkRedeploy = () => {
if (hasSelection) { if (hasSelection) {
@@ -222,19 +292,22 @@ export default function Stacks() {
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{stacks.map(stack => { {stacks.map(stack => {
const isRedeploying = stack.redeploying; const redeployState = stack.redeployState ?? REDEPLOY_STATE.IDLE;
const isRunning = redeployState === REDEPLOY_STATE.RUNNING;
const isQueued = redeployState === REDEPLOY_STATE.QUEUED;
const isBusy = isRunning || isQueued;
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; const isSelectable = !isBusy && !isCurrent && !isSelfStack;
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
@@ -245,8 +318,8 @@ export default function Stacks() {
disabled={!isSelectable} disabled={!isSelectable}
/> />
<div className={`w-12 h-12 flex items-center justify-center rounded-full <div className={`w-12 h-12 flex items-center justify-center rounded-full
${stack.updateStatus === '✅' ? 'bg-green-500' : ${stack.updateStatus === "✅" ? 'bg-green-500' :
stack.updateStatus === '⚠️' ? 'bg-yellow-500' : stack.updateStatus === "⚠️" ? 'bg-yellow-500' :
'bg-red-500'}`} 'bg-red-500'}`}
/> />
<div> <div>
@@ -259,11 +332,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>
@@ -278,7 +356,7 @@ export default function Stacks() {
<> <>
<button <button
onClick={() => handleRedeploy(stack.Id)} onClick={() => handleRedeploy(stack.Id)}
disabled={isRedeploying} disabled={isBusy}
className="px-5 py-2 rounded-lg font-medium transition bg-blue-500 hover:bg-blue-600" className="px-5 py-2 rounded-lg font-medium transition bg-blue-500 hover:bg-blue-600"
> >
Redeploy Redeploy