diff --git a/frontend_old/index.html b/frontend_old/index.html
deleted file mode 100644
index bf8396c..0000000
--- a/frontend_old/index.html
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
-
-
Redeploy-Logs
-
- handleExport('txt')}
- disabled={actionLoading || loading}
- className="rounded-md border border-gray-600 px-3 py-1.5 text-sm text-gray-200 transition hover:bg-gray-700 disabled:opacity-60"
- >
- Export TXT
-
- handleExport('sql')}
- disabled={actionLoading || loading}
- className="rounded-md border border-gray-600 px-3 py-1.5 text-sm text-gray-200 transition hover:bg-gray-700 disabled:opacity-60"
- >
- Export SQL
-
-
- Angezeigte löschen
-
- {loading && Aktualisiere… }
-
- Aktualisieren
-
-
-
-
- {error && (
-
- {error}
-
- )}
-
-
-
-
- Filter {activeFilterCount > 0 && (
-
- {activeFilterCount} aktiv
-
- )}
-
-
- {filtersOpen ? "Ausblenden" : "Anzeigen"}
-
-
-
- {filtersOpen && (
-
-
-
-
Stack
-
- {stackSelectOptions.map(({ value, label }) => (
- handleOptionMouseDown(event, selectedStacks, setSelectedStacks)}
- className={`bg-gray-900 text-gray-200 ${value === ALL_OPTION_VALUE ? 'font-semibold text-gray-100' : ''}`}
- >
- {label}
-
- ))}
-
-
- {selectedStacks.length === 0 ? (
-
- Alle Stacks
-
- ) : (
-
- {selectedStacks.map((stackId) => (
-
- {stackLabelMap.get(stackId) ?? `Stack ${stackId}`}
-
- ))}
-
- )}
-
-
-
-
-
Status
-
- {statusSelectOptions.map(({ value, label }) => (
- handleOptionMouseDown(event, selectedStatuses, setSelectedStatuses)}
- className={`bg-gray-900 text-gray-200 ${value === ALL_OPTION_VALUE ? 'font-semibold text-gray-100' : ''}`}
- >
- {label}
-
- ))}
-
-
- {selectedStatuses.length === 0 ? (
-
- Alle Status
-
- ) : (
-
- {selectedStatuses.map((status) => (
-
- {status}
-
- ))}
-
- )}
-
-
-
-
-
Endpoint
-
- {endpointSelectOptions.map(({ value, label }) => (
- handleOptionMouseDown(event, selectedEndpoints, setSelectedEndpoints)}
- className={`bg-gray-900 text-gray-200 ${value === ALL_OPTION_VALUE ? 'font-semibold text-gray-100' : ''}`}
- >
- {label}
-
- ))}
-
-
- {selectedEndpoints.length === 0 ? (
-
- Alle Endpoints
-
- ) : (
-
- {selectedEndpoints.map((endpoint) => (
-
- {endpoint}
-
- ))}
-
- )}
-
-
-
-
-
Redeploy-Typ
-
- {redeployTypeSelectOptions.map(({ value, label }) => (
- handleOptionMouseDown(event, selectedRedeployTypes, setSelectedRedeployTypes)}
- className={`bg-gray-900 text-gray-200 ${value === ALL_OPTION_VALUE ? 'font-semibold text-gray-100' : ''}`}
- >
- {label}
-
- ))}
-
-
- {selectedRedeployTypes.length === 0 ? (
-
- Alle Typen
-
- ) : (
-
- {selectedRedeployTypes.map((type) => (
-
- {REDEPLOY_TYPE_LABELS[type] ?? type}
-
- ))}
-
- )}
-
-
-
-
- Nachricht (Freitext)
- {
- setMessageQuery(event.target.value);
- setPage(1);
- }}
- placeholder="Textsuche in Log-Nachrichten..."
- className="w-full rounded-md border border-gray-700 bg-gray-900/70 px-3 py-2 text-gray-200 focus:outline-none focus:ring-2 focus:ring-purple-500"
- />
-
-
-
- Von
- {
- setFromDate(event.target.value);
- setPage(1);
- }}
- className="w-full rounded-md border border-gray-700 bg-gray-900/70 px-3 py-2 text-gray-200 focus:outline-none focus:ring-2 focus:ring-purple-500"
- />
-
-
-
- Bis
- {
- setToDate(event.target.value);
- setPage(1);
- }}
- className="w-full rounded-md border border-gray-700 bg-gray-900/70 px-3 py-2 text-gray-200 focus:outline-none focus:ring-2 focus:ring-purple-500"
- />
-
-
-
-
-
- Zurücksetzen
-
-
-
- )}
-
-
-
- Einträge pro Seite
-
- {PER_PAGE_OPTIONS.map(({ value, label }) => (
- {label}
- ))}
-
-
-
-
-
-
-
- Zeitpunkt
- Stack
- Art
- Status
- Nachricht
- Endpoint
- Aktionen
-
-
-
- {logs.length === 0 && !loading && (
-
-
- Keine Logs vorhanden.
-
-
- )}
- {logs.map((log) => {
- const statusClass = STATUS_COLORS[log.status] || "text-blue-300";
- const stackDisplayName = log.stackName || "Unbekannt";
- const showStackId = stackDisplayName !== '---' && log.stackId !== undefined && log.stackId !== null;
- const redeployTypeLabel = log.redeployType
- ? (REDEPLOY_TYPE_LABELS[log.redeployType] ?? log.redeployType)
- : '---';
- return (
-
-
- {formatTimestamp(log.timestamp)}
-
-
-
- {stackDisplayName}
- {showStackId && (
- ID: {log.stackId}
- )}
-
-
-
- {redeployTypeLabel}
-
-
- {log.status}
-
-
- {log.message || "-"}
-
-
- {log.endpoint ?? "-"}
-
-
- handleDeleteLog(log.id)}
- disabled={actionLoading}
- className="rounded-md border border-red-600 px-3 py-1 text-xs text-red-300 transition hover:bg-red-600/20 disabled:opacity-60"
- >
- Löschen
-
-
-
- );
- })}
-
-
-
-
-
-
- {totalLogs === 0
- ? "Keine Einträge"
- : perPage === 'all'
- ? `Zeige alle ${totalLogs} Einträge`
- : `Zeige ${rangeStart.toLocaleString()} – ${rangeEnd.toLocaleString()} von ${totalLogs.toLocaleString()} Einträgen`}
-
- {perPage !== 'all' && (
-
- handlePageChange(page - 1)}
- disabled={page <= 1 || actionLoading}
- className="rounded-md border border-gray-600 px-3 py-1 text-gray-200 transition hover:bg-gray-700 disabled:opacity-60"
- >
- Zurück
-
-
- Seite {page} / {totalPages}
-
- handlePageChange(page + 1)}
- disabled={page >= totalPages || actionLoading}
- className="rounded-md border border-gray-600 px-3 py-1 text-gray-200 transition hover:bg-gray-700 disabled:opacity-60"
- >
- Weiter
-
-
- )}
-
-
- );
-}
diff --git a/frontend_old/src/Maintenance.jsx b/frontend_old/src/Maintenance.jsx
deleted file mode 100644
index 86b5953..0000000
--- a/frontend_old/src/Maintenance.jsx
+++ /dev/null
@@ -1,1037 +0,0 @@
-import React, { useCallback, useEffect, useMemo, useState } from "react";
-import axios from "axios";
-import { useToast } from "./components/ToastProvider.jsx";
-import { useMaintenance } from "./context/MaintenanceContext.jsx";
-
-const UPDATE_STATUS_LABELS = {
- idle: "Bereit",
- running: "Läuft",
- success: "Erfolgreich",
- error: "Fehlgeschlagen"
-};
-
-const UPDATE_STATUS_STYLES = {
- idle: "bg-gray-700 text-gray-200",
- running: "bg-sky-600 text-white",
- success: "bg-green-600 text-white",
- error: "bg-red-600 text-white"
-};
-
-const UPDATE_STAGE_LABELS = {
- initializing: "Vorbereitung",
- "activating-maintenance": "Wartungsmodus aktivieren",
- "executing-script": "Skript wird ausgeführt",
- waiting: "Warte auf Portainer",
- completed: "Abgeschlossen",
- failed: "Fehlgeschlagen"
-};
-
-const LOG_LEVEL_STYLES = {
- info: "text-sky-300",
- success: "text-green-300",
- warning: "text-amber-300",
- error: "text-red-300",
- stdout: "text-gray-300",
- stderr: "text-orange-300",
- debug: "text-slate-400"
-};
-
-const formatLogTimestamp = (value) => {
- if (!value) return "-";
- const date = new Date(value);
- if (Number.isNaN(date.getTime())) return value;
- return date.toLocaleString("de-DE", {
- hour: "2-digit",
- minute: "2-digit",
- second: "2-digit"
- });
-};
-
-const formatCreatedAt = (value) => {
- if (!value && value !== 0) return "-";
-
- const normalizeToDate = (input) => {
- if (input instanceof Date) return input;
-
- if (typeof input === "number") {
- const epoch = input > 1e12 ? input : input * 1000;
- return new Date(epoch);
- }
-
- if (typeof input === "string") {
- const numeric = Number(input);
- if (!Number.isNaN(numeric)) {
- const epoch = numeric > 1e12 ? numeric : numeric * 1000;
- return new Date(epoch);
- }
- return new Date(input);
- }
-
- return null;
- };
-
- const date = normalizeToDate(value);
- if (!date || Number.isNaN(date.getTime())) {
- return String(value);
- }
-
- return date.toLocaleString("de-DE", {
- year: "numeric",
- month: "2-digit",
- day: "2-digit",
- hour: "2-digit",
- minute: "2-digit",
- second: "2-digit"
- });
-};
-
-const resolveStackType = (type) => {
- if (type === 1) return "Git";
- if (type === 2) return "Compose";
- return type ?? "-";
-};
-
-const createEmptySshDraft = () => ({
- host: '',
- port: '22',
- username: '',
- password: '',
- extraSshArgs: ''
-});
-
-export default function Maintenance() {
- const { showToast } = useToast();
- const {
- maintenance: maintenanceMeta,
- update: updateState,
- script: scriptConfig,
- ssh: sshConfig,
- loading: maintenanceLoading,
- error: maintenanceError,
- triggerUpdate,
- saveScript,
- resetScript,
- saveSshConfig,
- deleteSshConfig,
- testSshConnection
- } = useMaintenance();
-
- const maintenanceActive = Boolean(maintenanceMeta?.active);
- const maintenanceMessage = maintenanceMeta?.message;
- const updateRunning = Boolean(updateState?.running);
-
- const [scriptDraft, setScriptDraft] = useState("");
- const [scriptSaving, setScriptSaving] = useState(false);
- const [updateActionLoading, setUpdateActionLoading] = useState(false);
- const [updateActionError, setUpdateActionError] = useState("");
-
- const [sshDraft, setSshDraft] = useState(() => createEmptySshDraft());
- const [sshPasswordStored, setSshPasswordStored] = useState(false);
- const [showPassword, setShowPassword] = useState(false);
- const [sshSaving, setSshSaving] = useState(false);
- const [sshTesting, setSshTesting] = useState(false);
- const [sshDeleting, setSshDeleting] = useState(false);
- const [sshTestResult, setSshTestResult] = useState(null);
-
- useEffect(() => {
- if (!scriptConfig) return;
- const nextValue = scriptConfig.custom ?? scriptConfig.default ?? scriptConfig.effective ?? "";
- setScriptDraft(nextValue);
- }, [scriptConfig]);
-
- useEffect(() => {
- if (!sshConfig) {
- setSshDraft(createEmptySshDraft());
- setSshPasswordStored(false);
- setShowPassword(false);
- setSshTestResult(null);
- return;
- }
- setSshDraft({
- host: sshConfig.host ?? '',
- port: String(sshConfig.port ?? '22'),
- username: sshConfig.username ?? '',
- password: '',
- extraSshArgs: Array.isArray(sshConfig.extraSshArgs) ? sshConfig.extraSshArgs.join('\n') : ''
- });
- setSshPasswordStored(Boolean(sshConfig.passwordStored));
- setShowPassword(false);
- setSshTestResult(null);
- }, [sshConfig]);
-
- const scriptBaseline = useMemo(() => {
- if (!scriptConfig) return "";
- if (scriptConfig.source === "custom" && typeof scriptConfig.custom === "string") {
- return scriptConfig.custom;
- }
- return scriptConfig.default ?? scriptConfig.effective ?? "";
- }, [scriptConfig]);
-
- const scriptIsDirty = scriptConfig ? scriptDraft !== scriptBaseline : false;
- const scriptSourceLabel = scriptConfig?.source === "custom" ? "Benutzerdefiniert" : "Standard";
-
- const [duplicates, setDuplicates] = useState([]);
- const [duplicatesLoading, setDuplicatesLoading] = useState(true);
- const [duplicatesError, setDuplicatesError] = useState("");
- const [duplicatesRefreshing, setDuplicatesRefreshing] = useState(false);
- const [activeCleanupId, setActiveCleanupId] = useState(null);
- const [duplicatesUpdatedAt, setDuplicatesUpdatedAt] = useState(null);
-
- const [portainerStatus, setPortainerStatus] = useState(null);
- const [portainerLoading, setPortainerLoading] = useState(true);
- const [portainerError, setPortainerError] = useState("");
- const [portainerRefreshing, setPortainerRefreshing] = useState(false);
- const [portainerUpdatedAt, setPortainerUpdatedAt] = useState(null);
-
- const fetchPortainerStatus = useCallback(async ({ silent = false } = {}) => {
- if (silent) {
- setPortainerRefreshing(true);
- } else {
- setPortainerLoading(true);
- }
- setPortainerError("");
-
- try {
- const response = await axios.get("/api/maintenance/portainer-status");
- const payload = response.data ?? {};
- setPortainerStatus(payload);
- setPortainerUpdatedAt(new Date());
- } catch (err) {
- const message = err.response?.data?.error || err.message || "Fehler beim Prüfen des Portainer-Status";
- setPortainerError(message);
- } finally {
- if (silent) {
- setPortainerRefreshing(false);
- } else {
- setPortainerLoading(false);
- }
- }
- }, []);
-
- const fetchDuplicates = useCallback(async ({ silent = false } = {}) => {
- if (maintenanceActive || updateRunning) {
- setDuplicates([]);
- setDuplicatesError("Wartungsmodus aktiv – Duplikat-Verwaltung ist vorübergehend deaktiviert.");
- setDuplicatesLoading(false);
- setDuplicatesRefreshing(false);
- return;
- }
-
- if (silent) {
- setDuplicatesRefreshing(true);
- } else {
- setDuplicatesLoading(true);
- }
- setDuplicatesError("");
-
- try {
- const response = await axios.get("/api/maintenance/duplicates");
- const payload = response.data;
- const items = Array.isArray(payload) ? payload : payload?.items ?? [];
- setDuplicates(items);
- setDuplicatesUpdatedAt(new Date());
- } catch (err) {
- if (err.response?.status === 423) {
- setDuplicates([]);
- setDuplicatesError("Wartungsmodus aktiv – Duplikat-Verwaltung ist vorübergehend deaktiviert.");
- } else {
- const message = err.response?.data?.error || err.message || "Fehler beim Laden der Wartungsdaten";
- setDuplicatesError(message);
- }
- } finally {
- if (silent) {
- setDuplicatesRefreshing(false);
- } else {
- setDuplicatesLoading(false);
- }
- }
- }, [maintenanceActive, updateRunning]);
-
- useEffect(() => {
- fetchPortainerStatus();
- }, [fetchPortainerStatus]);
-
- useEffect(() => {
- fetchDuplicates();
- }, [fetchDuplicates]);
-
- const totals = useMemo(() => {
- const groups = Array.isArray(duplicates) ? duplicates.length : 0;
- const duplicateCount = Array.isArray(duplicates)
- ? duplicates.reduce((sum, entry) => sum + ((entry?.duplicates?.length) || 0), 0)
- : 0;
- return { groups, duplicateCount };
- }, [duplicates]);
-
- const handleSshDraftChange = useCallback((field, value) => {
- setSshDraft((prev) => ({ ...prev, [field]: value }));
- if (field === 'password') {
- setSshPasswordStored(false);
- }
- }, []);
-
- const normalizedSshDraft = useMemo(() => {
- const normalized = {
- host: sshDraft.host.trim(),
- port: Number.parseInt(sshDraft.port, 10) || 22,
- username: sshDraft.username.trim(),
- extraSshArgs: (sshDraft.extraSshArgs || '')
- .split(/\r?\n/)
- .map((entry) => entry.trim())
- .filter(Boolean)
- };
-
- const rawPassword = sshDraft.password ?? '';
- if (rawPassword) {
- normalized.password = rawPassword;
- } else if (!sshPasswordStored) {
- normalized.password = '';
- }
-
- return normalized;
- }, [sshDraft, sshPasswordStored]);
-
- const handleScriptSave = useCallback(async () => {
- if (!scriptConfig) return;
- try {
- setScriptSaving(true);
- await saveScript(scriptDraft);
- showToast({
- variant: "success",
- title: "Skript gespeichert",
- description: "Das benutzerdefinierte Portainer-Update-Skript wurde aktualisiert."
- });
- } catch (err) {
- const message = err.response?.data?.error || err.message || "Skript konnte nicht gespeichert werden";
- showToast({ variant: "error", title: "Speichern fehlgeschlagen", description: message });
- } finally {
- setScriptSaving(false);
- }
- }, [saveScript, scriptDraft, scriptConfig, showToast]);
-
- const handleScriptReset = useCallback(async () => {
- try {
- setScriptSaving(true);
- await resetScript();
- showToast({
- variant: "info",
- title: "Standardskript wiederhergestellt",
- description: "Es wird wieder das Standard-Update-Skript verwendet."
- });
- } catch (err) {
- const message = err.response?.data?.error || err.message || "Standardskript konnte nicht wiederhergestellt werden";
- showToast({ variant: "error", title: "Zurücksetzen fehlgeschlagen", description: message });
- } finally {
- setScriptSaving(false);
- }
- }, [resetScript, showToast]);
-
- const handleSshSaveConfig = useCallback(async () => {
- try {
- setSshSaving(true);
- await saveSshConfig(normalizedSshDraft);
- setSshTestResult(null);
- setShowPassword(false);
- showToast({
- variant: "success",
- title: "SSH-Konfiguration gespeichert",
- description: "Verbindungseinstellungen wurden aktualisiert."
- });
- } catch (err) {
- const message = err.response?.data?.error || err.message || "SSH-Konfiguration konnte nicht gespeichert werden";
- showToast({ variant: "error", title: "Speichern fehlgeschlagen", description: message });
- } finally {
- setSshSaving(false);
- }
- }, [normalizedSshDraft, saveSshConfig, showToast]);
-
- const handleSshTestConnection = useCallback(async () => {
- try {
- setSshTesting(true);
- const result = await testSshConnection(normalizedSshDraft);
- setSshTestResult({ success: true, timestamp: new Date(), details: result?.result });
- showToast({
- variant: "success",
- title: "SSH-Verbindung erfolgreich",
- description: "Verbindung zum Portainer-Host wurde hergestellt."
- });
- } catch (err) {
- const message = err.response?.data?.error || err.message || "SSH-Verbindung fehlgeschlagen";
- setSshTestResult({ success: false, timestamp: new Date(), error: message });
- showToast({ variant: "error", title: "SSH-Test fehlgeschlagen", description: message });
- } finally {
- setSshTesting(false);
- }
- }, [normalizedSshDraft, testSshConnection, showToast]);
-
- const handleSshDeleteConfig = useCallback(async () => {
- try {
- setSshDeleting(true);
- await deleteSshConfig();
- setSshDraft(createEmptySshDraft());
- setSshPasswordStored(false);
- setShowPassword(false);
- setSshTestResult(null);
- showToast({
- variant: "info",
- title: "SSH-Konfiguration gelöscht",
- description: "Die Verbindungseinstellungen wurden zurückgesetzt."
- });
- } catch (err) {
- const message = err.response?.data?.error || err.message || "SSH-Konfiguration konnte nicht gelöscht werden";
- showToast({ variant: "error", title: "Löschen fehlgeschlagen", description: message });
- } finally {
- setSshDeleting(false);
- }
- }, [deleteSshConfig, showToast]);
-
- const handleTriggerUpdate = useCallback(async () => {
- if (maintenanceActive || updateRunning) {
- showToast({
- variant: "warning",
- title: "Update nicht möglich",
- description: "Während eines aktiven Wartungsmodus kann kein weiteres Update gestartet werden."
- });
- return;
- }
-
- const targetVersion = portainerStatus?.latestVersion ?? portainerStatus?.currentVersion ?? "unbekannt";
- if (typeof window !== "undefined") {
- const confirmed = window.confirm(
- `Portainer-Update starten?\nZielversion: ${targetVersion}.\nWährend des Updates befindet sich StackPulse im Wartungsmodus.\nFortfahren?`
- );
- if (!confirmed) {
- return;
- }
- }
-
- try {
- setUpdateActionError("");
- setUpdateActionLoading(true);
- await triggerUpdate();
- showToast({
- variant: "info",
- title: "Update gestartet",
- description: "Das Portainer-Update wurde gestartet."
- });
- } catch (err) {
- const message = err.response?.data?.error || err.message || "Portainer-Update konnte nicht gestartet werden";
- setUpdateActionError(message);
- showToast({ variant: "error", title: "Update fehlgeschlagen", description: message });
- } finally {
- setUpdateActionLoading(false);
- }
- }, [maintenanceActive, updateRunning, portainerStatus, triggerUpdate, showToast]);
-
- const handleCleanup = useCallback(async (entry) => {
- if (!entry) return;
- if (maintenanceActive || updateRunning) {
- showToast({
- variant: "warning",
- title: "Aktion nicht möglich",
- description: "Während des Wartungsmodus sind Bereinigungen deaktiviert."
- });
- return;
- }
-
- const canonicalId = entry.canonical?.Id;
- if (!canonicalId) return;
-
- const duplicateIds = (entry.duplicates || []).map((dup) => dup.Id).filter(Boolean);
- if (!duplicateIds.length) return;
-
- const canonicalName = entry.canonical?.Name || entry.name || `Stack ${canonicalId}`;
-
- if (typeof window !== "undefined") {
- const confirmation = window.confirm(
- `Bereinigung für "${canonicalName}" starten?\n` +
- `Es werden ${duplicateIds.length} Duplikate entfernt: ${duplicateIds.join(", ")}`
- );
- if (!confirmation) {
- return;
- }
- }
-
- setActiveCleanupId(String(canonicalId));
-
- try {
- const response = await axios.post("/api/maintenance/duplicates/cleanup", {
- canonicalId,
- duplicateIds
- });
-
- const payload = response.data ?? {};
- if (payload.success === false) {
- throw new Error(payload.error || "Bereinigung fehlgeschlagen");
- }
-
- const removedIds = Array.isArray(payload.results)
- ? payload.results.filter((result) => result.status === "deleted").map((result) => result.id)
- : duplicateIds;
-
- showToast({
- variant: "success",
- title: "Bereinigung abgeschlossen",
- description: `${canonicalName} – entfernte IDs: ${removedIds.join(", ")}`
- });
- await fetchDuplicates({ silent: true });
- } catch (err) {
- const message = err.response?.data?.error || err.message || "Bereinigung fehlgeschlagen";
- showToast({
- variant: "error",
- title: "Bereinigung fehlgeschlagen",
- description: message
- });
- } finally {
- setActiveCleanupId(null);
- }
- }, [fetchDuplicates, maintenanceActive, showToast, updateRunning]);
-
- const isDuplicatesDisabled = maintenanceActive || updateRunning;
- const updateStatusKey = updateState?.status ?? (updateRunning ? "running" : "idle");
- const updateStatusLabel = UPDATE_STATUS_LABELS[updateStatusKey] ?? updateStatusKey;
- const updateBadgeClass = UPDATE_STATUS_STYLES[updateStatusKey] ?? UPDATE_STATUS_STYLES.idle;
- const updateStageLabel = updateState?.stage
- ? UPDATE_STAGE_LABELS[updateState.stage] ?? updateState.stage
- : "–";
- const updateLogs = updateState?.logs ?? [];
- const updateTargetVersion = updateState?.targetVersion ?? "-";
- const updateResultVersion = updateState?.resultVersion ?? "-";
- const updateStartedAt = updateState?.startedAt ? formatCreatedAt(updateState.startedAt) : "-";
- const updateFinishedAt = updateState?.finishedAt ? formatCreatedAt(updateState.finishedAt) : "-";
- const updateStatusMessage = updateState?.message ?? (updateRunning ? "Update läuft…" : "");
-
- const disableUpdateButton = updateRunning || scriptSaving || maintenanceLoading;
-
- return (
-
-
-
Wartung
-
- Werkzeuge für wiederkehrende Wartungsaufgaben. Entfernt derzeit doppelte Stack-Einträge und verwaltet Portainer-Updates.
-
-
-
- {(maintenanceActive || updateRunning) && (
-
-
-
- Wartungsmodus aktiv{maintenanceMessage ? ` – ${maintenanceMessage}` : updateRunning ? " – Portainer-Update läuft" : ""}.
-
- {updateRunning && (
-
- Phase: {updateStageLabel}
-
- )}
-
-
- )}
-
- {maintenanceError && (
-
- {maintenanceError}
-
- )}
- {/* Beginn Portainer Update */}
-
-
-
-
Portainer Status
-
- {portainerLoading
- ? "Status wird geprüft…"
- : portainerError
- ? portainerError
- : updateRunning
- ? "Portainer wird aktuell aktualisiert."
- : (portainerStatus?.updateAvailable === true
- ? "Es ist ein Update für Portainer verfügbar."
- : "Portainer ist auf dem aktuellen Stand.")}
-
- {portainerUpdatedAt && !portainerLoading && (
-
- Stand: {portainerUpdatedAt.toLocaleString("de-DE", {
- hour: "2-digit",
- minute: "2-digit",
- second: "2-digit"
- })}
-
- )}
-
-
- fetchPortainerStatus({ silent: false })}
- disabled={portainerLoading || portainerRefreshing}
- className="rounded-lg border border-purple-500 px-4 py-2 text-sm font-medium text-purple-200 transition hover:bg-purple-500/20 disabled:opacity-50"
- >
- Status aktualisieren
-
-
-
-
- {!portainerLoading && !portainerError && portainerStatus && (
-
-
-
- Installierte Version
- {portainerStatus.currentVersion ?? "Unbekannt"}
-
-
- Neueste Version
- {portainerStatus.latestVersion ?? "Unbekannt"}
-
-
- Status
-
- {portainerStatus.updateAvailable === true
- ? "Update verfügbar"
- : portainerStatus.updateAvailable === false
- ? "Aktuell"
- : "Unbekannt"}
-
-
- {portainerStatus.edition && (
-
- Edition
- {portainerStatus.edition}
-
- )}
- {portainerStatus.build && (
-
- Build
- {portainerStatus.build}
-
- )}
- {portainerStatus.errors?.latestVersion && (
-
- Neueste Version konnte nicht ermittelt werden: {portainerStatus.errors.latestVersion}
-
- )}
- {portainerStatus.errors?.container && (
-
- Container-Details konnten nicht ermittelt werden: {portainerStatus.errors.container}
-
- )}
-
-
- {/* {portainerStatus.container && (
-
-
Startparameter
-
- {(portainerStatus.container.name || portainerStatus.container.image) && (
-
-
Container
-
- {portainerStatus.container.name ?? "Unbekannt"}
- {portainerStatus.container.image && (
- {portainerStatus.container.image}
- )}
-
-
- )}
- {portainerStatus.container.id && (
-
- Container-ID
-
- {portainerStatus.container.id.slice(0, 12)}
-
-
- )}
-
- Startkommando
-
- {[...(portainerStatus.container.entrypoint || []), ...(portainerStatus.container.command || [])].join(" ") || "Unbekannt"}
-
-
- {Array.isArray(portainerStatus.container.args) && portainerStatus.container.args.length > 0 && (
-
- Argumente
-
- {portainerStatus.container.args.join(" ")}
-
-
- )}
- {Array.isArray(portainerStatus.container.env) && portainerStatus.container.env.length > 0 && (
-
-
Umgebungsvariablen
-
- {portainerStatus.container.env.map((entry, index) => (
-
- {entry}
-
- ))}
-
-
- )}
- {Array.isArray(portainerStatus.container.binds) && portainerStatus.container.binds.length > 0 && (
-
-
Bind-Mounts
-
- {portainerStatus.container.binds.map((bind, index) => (
-
- {bind}
-
- ))}
-
-
- )}
-
-
- )} */}
-
- )}
-
-
-
-
-
SSH-Verbindung
-
-
-
-
- {sshSaving ? 'Speichern…' : 'SSH-Konfiguration speichern'}
-
-
- {sshTesting ? 'Test läuft…' : 'Verbindung testen'}
-
-
- {sshDeleting ? 'Löschen…' : 'SSH-Einstellungen löschen'}
-
-
- {sshTestResult && (
-
- {sshTestResult.success ? 'SSH-Verbindung erfolgreich.' : `SSH-Verbindung fehlgeschlagen: ${sshTestResult.error}`}
-
- )}
-
-
-
-
-
Update-Skript
- Quelle: {scriptSourceLabel}
-
-
-
-
-
-
Update-Status
-
- {updateStatusLabel}
-
-
-
-
- Phase
- {updateStageLabel}
-
-
- Ziel-Version
- {updateTargetVersion}
-
-
- Installierte Version
- {updateResultVersion}
-
-
- Gestartet
- {updateStartedAt}
-
-
- Beendet
- {updateFinishedAt}
-
- {updateStatusMessage && (
-
- {updateStatusMessage}
-
- )}
- {updateState?.error && (
-
- {updateState.error}
-
- )}
- {updateActionError && (
-
- {updateActionError}
-
- )}
-
-
- {updateLogs.length === 0 ? (
-
Noch keine Protokolle vorhanden.
- ) : (
- updateLogs.map((entry, index) => (
-
- [{formatLogTimestamp(entry.timestamp)}] {" "}
-
- {entry.message}
-
-
- ))
- )}
-
-
- {updateActionLoading ? "Update wird gestartet…" : "Portainer aktualisieren"}
-
-
-
-
- {/* Ende Portainer Update */}
-
-
-
-
-
Doppelte Stacks
-
- {isDuplicatesDisabled
- ? "Wartungsmodus aktiv – Duplikat-Verwaltung ist vorübergehend deaktiviert."
- : duplicatesLoading
- ? "Analyse läuft…"
- : totals.groups === 0
- ? "Keine Duplikate gefunden"
- : `${totals.groups} Stack-Namen mit insgesamt ${totals.duplicateCount} Duplikaten gefunden`}
-
- {duplicatesUpdatedAt && !duplicatesLoading && !isDuplicatesDisabled && (
-
- Stand: {duplicatesUpdatedAt.toLocaleString("de-DE", {
- hour: "2-digit",
- minute: "2-digit",
- second: "2-digit"
- })}
-
- )}
-
-
- fetchDuplicates({ silent: false })}
- disabled={isDuplicatesDisabled || duplicatesLoading || duplicatesRefreshing || activeCleanupId !== null}
- className="rounded-lg border border-purple-500 px-4 py-2 text-sm font-medium text-purple-200 transition hover:bg-purple-500/20 disabled:opacity-50"
- >
- Aktualisieren
-
-
-
-
-
- Das Entfernen von deppelten Stacks ist noch nicht getestet. Bitte nicht in Produktivumgebungen einsetzen.
-
- {duplicatesError && !isDuplicatesDisabled && (
-
- {duplicatesError}
-
- )}
-
- {duplicatesLoading ? (
-
- Daten werden geladen…
-
- ) : totals.groups === 0 || isDuplicatesDisabled ? (
-
- {isDuplicatesDisabled
- ? "Wartungsmodus aktiv – Duplikat-Verwaltung ist vorübergehend deaktiviert."
- : "Es wurden keine doppelten Stacks gefunden."}
-
- ) : (
-
- {duplicates.map((entry) => {
- const canonicalId = entry?.canonical?.Id;
- const duplicatesForEntry = entry?.duplicates || [];
- const isProcessing = activeCleanupId === String(canonicalId);
-
- return (
-
-
-
-
-
{entry.name}
-
- {duplicatesForEntry.length} Duplikat{duplicatesForEntry.length === 1 ? "" : "e"}
-
-
-
- Behaltener Stack: ID {canonicalId} (Endpoint {entry?.canonical?.EndpointId ?? "-"})
-
-
- Typ: {resolveStackType(entry?.canonical?.Type)} • Erstellt: {formatCreatedAt(entry?.canonical?.Created)}
-
-
-
-
handleCleanup(entry)}
- disabled={isDuplicatesDisabled || isProcessing || duplicatesRefreshing || duplicatesLoading}
- className="self-start rounded-lg bg-red-500 px-4 py-2 text-sm font-semibold text-white transition hover:bg-red-600 disabled:opacity-50"
- >
- {isProcessing ? "Bereinigung läuft…" : `Bereinigen (${duplicatesForEntry.length})`}
-
-
-
-
- {duplicatesForEntry.map((duplicate) => (
-
-
- ID: {duplicate.Id}
- Endpoint: {duplicate.EndpointId ?? "-"}
- Typ: {resolveStackType(duplicate.Type)}
- Erstellt: {formatCreatedAt(duplicate.Created)}
-
-
- ))}
-
-
- );
- })}
-
- )}
-
- );
-}
diff --git a/frontend_old/src/Stacks.jsx b/frontend_old/src/Stacks.jsx
deleted file mode 100644
index 6368be5..0000000
--- a/frontend_old/src/Stacks.jsx
+++ /dev/null
@@ -1,1038 +0,0 @@
-import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
-import axios from "axios";
-import { io } from "socket.io-client";
-import { useToast } from "./components/ToastProvider.jsx";
-import { useMaintenance } from "./context/MaintenanceContext.jsx";
-
-const SELECTION_PROMPT_STORAGE_KEY = "stackSelectionPreference";
-
-const PER_PAGE_DEFAULT = "50";
-const PER_PAGE_OPTIONS = [
- { value: "10", label: "10" },
- { value: "25", label: "25" },
- { value: "50", label: "50" },
- { value: "100", label: "100" },
- { value: "all", label: "Alle" }
-];
-const VALID_PER_PAGE_VALUES = new Set(PER_PAGE_OPTIONS.map((option) => option.value));
-const STACKS_CACHE_DURATION = 30 * 1000;
-const STACKS_REFRESH_INTERVAL = 30 * 1000;
-let stacksCache = { data: null, timestamp: 0 };
-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 updateStacksCache = (data) => {
- stacksCache = { data, timestamp: Date.now() };
-};
-
-const prepareInitialStacks = (data) => {
- if (!Array.isArray(data)) return [];
- return [...data].sort((a, b) => (a?.Name || '').localeCompare(b?.Name || ''));
-};
-
-
-export default function Stacks() {
- const [stacks, setStacks] = useState(() => prepareInitialStacks(stacksCache.data));
- const [loading, setLoading] = useState(() => !stacksCache.data);
- const [error, setError] = useState("");
- const [selectedStackIds, setSelectedStackIds] = useState([]);
- const [statusFilter, setStatusFilter] = useState("all");
- const [searchQuery, setSearchQuery] = useState("");
- const [selectionPromptVisible, setSelectionPromptVisible] = useState(false);
- const [rememberSelectionChoice, setRememberSelectionChoice] = useState(false);
- const [selectionPreferenceStored, setSelectionPreferenceStored] = useState(false);
- const [perPage, setPerPage] = useState(PER_PAGE_DEFAULT);
- const [page, setPage] = useState(1);
-
- const { showToast } = useToast();
-
- const { maintenance, update } = useMaintenance();
- const maintenanceActive = Boolean(maintenance?.active);
- const maintenanceMessage = maintenance?.message;
- const maintenanceLocked = maintenanceActive || Boolean(update?.running);
- const maintenanceBanner = maintenanceLocked ? (maintenanceMessage || (update?.running ? "Portainer-Update läuft" : "Wartungsmodus aktiv")) : "";
-
- const stacksByIdRef = useRef(new Map());
-
- useEffect(() => {
- if (maintenanceLocked) {
- setLoading(false);
- setStacks([]);
- setSelectedStackIds([]);
- }
- }, [maintenanceLocked]);
-
- const mergeStackState = useCallback((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);
-
- if (previous && previous.updateStatus === '✅' && stack.updateStatus === '⚠️') {
- const stackLabel = stack.Name ? `${stack.Name} (ID: ${stack.Id})` : `Stack ${stack.Id}`;
- showToast({
- variant: 'warning',
- title: 'Aktualisierung gefunden',
- description: stackLabel
- });
- }
-
- 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 {
- ...stack,
- redeployPhase: effectivePhase,
- redeployQueued: isQueued,
- redeploying: isBusy,
- redeployDisabled: stack.redeployDisabled ?? previous?.redeployDisabled ?? false,
- duplicateName: stack.duplicateName ?? previous?.duplicateName ?? false
- };
- });
- }, [showToast]);
-
- useEffect(() => {
- const socket = io("/", {
- path: "/socket.io",
- transports: ["websocket"]
- });
- console.log("🔌 Socket connected");
-
- socket.on("redeployStatus", async (payload = {}) => {
- const { stackId } = payload;
- if (!stackId) return;
-
- const resolvedPhaseRaw = payload.redeployPhase ?? payload.phase;
- let resolvedPhase = resolvedPhaseRaw;
- if (!resolvedPhase) {
- if (payload.isRedeploying === true) {
- resolvedPhase = REDEPLOY_PHASES.STARTED;
- } else if (payload.isRedeploying === false) {
- 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 stackName = payload.stackName ?? stackSnapshot?.Name;
- const stackLabel = stackName ? `${stackName} (ID: ${stackId})` : `Stack ${stackId}`;
-
- console.log(`🔄 Stack ${stackId} Redeploy Update: ${resolvedPhase ?? 'unbekannt'}`);
-
- if (resolvedPhase === REDEPLOY_PHASES.STARTED) {
- showToast({
- variant: 'info',
- title: 'Redeploy gestartet',
- description: stackLabel
- });
- } else if (resolvedPhase === REDEPLOY_PHASES.SUCCESS) {
- showToast({
- variant: 'success',
- title: 'Redeploy abgeschlossen',
- description: stackLabel
- });
- } else if (resolvedPhase === REDEPLOY_PHASES.ERROR) {
- const detail = payload.message ? ` – ${payload.message}` : '';
- showToast({
- variant: 'error',
- title: 'Redeploy fehlgeschlagen',
- description: `${stackLabel}${detail}`
- });
- }
-
- setStacks(prev =>
- prev.map(stack => {
- if (stack.Id !== stackId) return stack;
-
- const nextPhase = resolvedPhase ?? stack.redeployPhase ?? null;
- 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 (isError) {
- updated.redeploying = false;
- updated.redeployQueued = false;
- }
-
- if (!isBusy && !isSuccess && !isError && resolvedPhase === undefined) {
- updated.redeploying = false;
- updated.redeployQueued = false;
- updated.redeployPhase = stack.redeployPhase ?? null;
- }
-
- return updated;
- })
- );
-
- const shouldRefresh =
- resolvedPhase === REDEPLOY_PHASES.SUCCESS ||
- resolvedPhase === REDEPLOY_PHASES.ERROR ||
- (payload.isRedeploying === false && !resolvedPhase);
-
- if (shouldRefresh) {
- try {
- const res = await axios.get('/api/stacks');
- updateStacksCache(res.data);
- setStacks(prev => mergeStackState(prev, res.data));
- } catch (err) {
- console.error('Fehler beim Aktualisieren nach Redeploy:', err);
- }
- }
- });
-
- return () => socket.disconnect();
- }, []);
-
- const fetchStacks = useCallback(async ({ force = false, silent = false } = {}) => {
- if (maintenanceActive) {
- if (!silent) {
- setLoading(false);
- }
- return;
- }
-
- const hadCache = Boolean(stacksCache.data);
-
- if (!force && hadCache) {
- setStacks(prev => mergeStackState(prev, stacksCache.data));
- if (!silent) {
- setLoading(false);
- if (isCacheFresh()) {
- return;
- }
- }
- }
-
- const shouldShowSpinner = !silent && (!hadCache || force);
- if (shouldShowSpinner) {
- setLoading(true);
- }
-
- try {
- if (force || !stacksCachePromise) {
- stacksCachePromise = axios.get("/api/stacks")
- .then((res) => {
- updateStacksCache(res.data);
- return res.data;
- })
- .finally(() => {
- stacksCachePromise = null;
- });
- }
-
- const data = await stacksCachePromise;
- setStacks(prev => mergeStackState(prev, data));
- setError("");
- } catch (err) {
- console.error("❌ Fehler beim Abrufen der Stacks:", err);
- if (!hadCache && !silent) {
- setError("Fehler beim Laden der Stacks");
- }
- } finally {
- if (shouldShowSpinner) {
- setLoading(false);
- }
- }
- }, [maintenanceActive, mergeStackState]);
-
- useEffect(() => {
- fetchStacks();
- }, [fetchStacks]);
-
- useEffect(() => {
- if (typeof document === 'undefined' || maintenanceActive) return undefined;
-
- const intervalId = setInterval(() => {
- if (!document.hidden) {
- fetchStacks({ force: true, silent: true });
- }
- }, STACKS_REFRESH_INTERVAL);
-
- return () => clearInterval(intervalId);
- }, [fetchStacks, maintenanceActive]);
-
- useEffect(() => {
- if (typeof document === 'undefined' || maintenanceActive) return undefined;
-
- const handleVisibility = () => {
- if (!document.hidden) {
- fetchStacks({ force: true, silent: true });
- }
- };
-
- document.addEventListener('visibilitychange', handleVisibility);
- return () => {
- document.removeEventListener('visibilitychange', handleVisibility);
- };
- }, [fetchStacks, maintenanceActive]);
-
- useEffect(() => {
- setSelectedStackIds(prev => {
- const filtered = prev.filter(id => {
- const match = stacks.find(stack => stack.Id === id);
- 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;
- });
- }, [stacks]);
-
- useEffect(() => {
- stacksByIdRef.current = new Map(stacks.map((stack) => [stack.Id, stack]));
- }, [stacks]);
-
- const normalizedSearch = searchQuery.trim().toLowerCase();
-
- const filteredStacks = useMemo(() => {
- return stacks.filter((stack) => {
- if (statusFilter === "current" && stack.updateStatus !== '✅') return false;
- if (statusFilter === "outdated" && stack.updateStatus === '✅') return false;
-
- if (normalizedSearch) {
- const identifier = `${stack.Name ?? ""} ${stack.Id ?? ""}`.toLowerCase();
- if (!identifier.includes(normalizedSearch)) return false;
- }
-
- return true;
- });
- }, [stacks, statusFilter, normalizedSearch]);
-
- const eligibleFilteredStacks = useMemo(
- () => 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]
- );
-
- const stacksById = useMemo(() => {
- const map = new Map();
- stacks.forEach((stack) => {
- map.set(stack.Id, stack);
- });
- return map;
- }, [stacks]);
-
- const filteredStackIdSet = useMemo(() => new Set(filteredStacks.map((stack) => stack.Id)), [filteredStacks]);
-
- const paginatedStacks = useMemo(() => {
- if (perPage === 'all') {
- return filteredStacks;
- }
- const perPageNumber = Number(perPage);
- const start = (page - 1) * perPageNumber;
- return filteredStacks.slice(start, start + perPageNumber);
- }, [filteredStacks, perPage, page]);
-
- const visiblePageStackIds = useMemo(() => new Set(paginatedStacks.map((stack) => stack.Id)), [paginatedStacks]);
-
- const eligiblePageStacks = useMemo(
- () => 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]
- );
-
- const selectionPreferenceRef = useRef({ action: 'keep', remember: false });
- const previousFiltersRef = useRef({ status: statusFilter, search: normalizedSearch });
- const didMountRef = useRef(false);
-
- useEffect(() => {
- if (typeof window === 'undefined') return;
- try {
- const storedValue = window.sessionStorage.getItem(SELECTION_PROMPT_STORAGE_KEY);
- if (!storedValue) return;
- const parsed = JSON.parse(storedValue);
- if (parsed && (parsed.action === 'keep' || parsed.action === 'clear')) {
- selectionPreferenceRef.current = { action: parsed.action, remember: true };
- setSelectionPreferenceStored(true);
- }
- } catch (err) {
- console.error('⚠️ Konnte gespeicherte Auswahl-Einstellung nicht laden:', err);
- selectionPreferenceRef.current = { action: 'keep', remember: false };
- }
- }, []);
-
- useEffect(() => {
- const normalized = normalizedSearch;
- if (!didMountRef.current) {
- didMountRef.current = true;
- previousFiltersRef.current = { status: statusFilter, search: normalized };
- return;
- }
-
- const prev = previousFiltersRef.current;
- const filtersChanged = prev.status !== statusFilter || prev.search !== normalized;
-
- if (!filtersChanged) {
- if (statusFilter === 'all' && normalized.length === 0 && selectionPromptVisible) {
- setSelectionPromptVisible(false);
- }
- return;
- }
-
- previousFiltersRef.current = { status: statusFilter, search: normalized };
-
- if (selectedStackIds.length === 0) {
- setSelectionPromptVisible(false);
- return;
- }
-
- const hasFilters = statusFilter !== 'all' || normalized.length > 0;
- if (!hasFilters) {
- setSelectionPromptVisible(false);
- return;
- }
-
- const storedPreference = selectionPreferenceRef.current;
- if (storedPreference?.remember) {
- if (storedPreference.action === 'clear') {
- setSelectedStackIds([]);
- }
- setSelectionPromptVisible(false);
- return;
- }
-
- setRememberSelectionChoice(Boolean(storedPreference?.remember));
- setSelectionPromptVisible(true);
- }, [statusFilter, normalizedSearch, selectedStackIds.length, selectionPromptVisible]);
-
- useEffect(() => {
- if (selectedStackIds.length === 0 && selectionPromptVisible) {
- setSelectionPromptVisible(false);
- }
- }, [selectedStackIds.length, selectionPromptVisible]);
-
- const hasActiveFilters = statusFilter !== "all" || normalizedSearch.length > 0;
-
- useEffect(() => {
- setPage(1);
- }, [statusFilter, normalizedSearch]);
-
- useEffect(() => {
- if (perPage === 'all') {
- if (page !== 1) setPage(1);
- return;
- }
-
- const perPageNumber = Number(perPage);
- const totalPagesCalc = Math.max(1, Math.ceil(filteredStacks.length / perPageNumber));
- if (page > totalPagesCalc) {
- setPage(totalPagesCalc);
- }
- }, [filteredStacks.length, perPage, page]);
-
- const totalItems = filteredStacks.length;
- const perPageNumber = perPage === 'all' ? (totalItems || 1) : Number(perPage);
- const totalPages = perPage === 'all' ? 1 : Math.max(1, Math.ceil(totalItems / perPageNumber));
- const pageStart = totalItems === 0 ? 0 : (perPage === 'all' ? 1 : ((page - 1) * perPageNumber + 1));
- const pageEnd = perPage === 'all' ? totalItems : Math.min(totalItems, page * perPageNumber);
-
- const toggleStackSelection = (stackId, disabled) => {
- if (maintenanceLocked || disabled) return;
- setSelectedStackIds(prev =>
- prev.includes(stackId)
- ? prev.filter(id => id !== stackId)
- : [...prev, stackId]
- );
- };
-
- const handlePerPageChange = (event) => {
- const value = event.target.value;
- if (!VALID_PER_PAGE_VALUES.has(value)) return;
- setPerPage(value);
- setPage(1);
- };
-
- const handlePrevPage = () => {
- setPage((prev) => Math.max(1, prev - 1));
- };
-
- const handleNextPage = () => {
- if (perPage === 'all') return;
- setPage((prev) => {
- const perPageNumberLocal = Number(perPage);
- const totalPagesCalc = Math.max(1, Math.ceil(filteredStacks.length / perPageNumberLocal));
- return Math.min(totalPagesCalc, prev + 1);
- });
- };
-
- const applySelectionPreference = (action) => {
- const remember = rememberSelectionChoice;
- if (typeof window !== 'undefined') {
- try {
- if (remember) {
- const payload = { action, remember: true };
- window.sessionStorage.setItem(SELECTION_PROMPT_STORAGE_KEY, JSON.stringify(payload));
- selectionPreferenceRef.current = payload;
- } else {
- window.sessionStorage.removeItem(SELECTION_PROMPT_STORAGE_KEY);
- selectionPreferenceRef.current = { action, remember: false };
- }
- } catch (err) {
- console.error('⚠️ Konnte Auswahl-Einstellung nicht speichern:', err);
- selectionPreferenceRef.current = { action, remember: false };
- }
- } else {
- selectionPreferenceRef.current = { action, remember: false };
- }
-
- setSelectionPreferenceStored(remember);
- setRememberSelectionChoice(remember);
-
- if (action === 'clear') {
- setSelectedStackIds([]);
- }
-
- setSelectionPromptVisible(false);
- };
-
- const clearStoredSelectionPreference = () => {
- if (typeof window !== 'undefined') {
- try {
- window.sessionStorage.removeItem(SELECTION_PROMPT_STORAGE_KEY);
- } catch (err) {
- console.error('⚠️ Konnte gespeicherte Auswahl-Einstellung nicht löschen:', err);
- }
- }
-
- selectionPreferenceRef.current = { action: 'keep', remember: false };
- setSelectionPreferenceStored(false);
- setRememberSelectionChoice(false);
-
- if (hasActiveFilters && selectedStackIds.length > 0) {
- setSelectionPromptVisible(true);
- }
- };
-
- const clearSelection = () => {
- setSelectedStackIds([]);
- setSelectionPromptVisible(false);
- };
-
- const handleChipRemove = (stackId) => {
- setSelectedStackIds((prev) => prev.filter((id) => id !== 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));
- setStacks((prev) =>
- prev.map((stack) =>
- stack.Id === stackId
- ? {
- ...stack,
- redeployPhase: REDEPLOY_PHASES.STARTED,
- redeploying: true,
- redeployQueued: false
- }
- : stack
- )
- );
-
- try {
- await axios.put(`/api/stacks/${stackId}/redeploy`);
- // Statusupdates kommen über Socket.IO
- } catch (err) {
- console.error("❌ Fehler beim Redeploy:", err);
- setStacks((prev) =>
- prev.map((stack) =>
- stack.Id === stackId
- ? {
- ...stack,
- redeployPhase: null,
- redeploying: false,
- redeployQueued: false
- }
- : stack
- )
- );
-
- if (!err.response) {
- const current = stacksByIdRef.current.get(stackId);
- const stackLabel = current?.Name ? `${current.Name} (ID: ${stackId})` : `Stack ${stackId}`;
- const errorText = err.message || 'Unbekannter Fehler';
- showToast({
- variant: 'error',
- title: 'Redeploy fehlgeschlagen',
- description: `${stackLabel} – ${errorText}`
- });
- }
- }
- };
-
- const handleRedeployAll = async () => {
- if (!eligiblePageStacks.length) return;
-
- const targetIds = new Set(eligiblePageStacks.map((stack) => stack.Id));
-
- setStacks(prev =>
- prev.map(stack =>
- targetIds.has(stack.Id)
- ? {
- ...stack,
- redeployPhase: REDEPLOY_PHASES.QUEUED,
- redeploying: true,
- redeployQueued: true
- }
- : stack
- )
- );
-
- try {
- if (targetIds.size === eligibleFilteredStacks.length) {
- await axios.put("/api/stacks/redeploy-all");
- } else {
- await axios.put("/api/stacks/redeploy-selection", { stackIds: Array.from(targetIds) });
- }
- setSelectedStackIds((prev) => prev.filter((id) => !targetIds.has(id)));
- // Statusupdates kommen über Socket.IO
- } catch (err) {
- console.error("❌ Fehler beim Redeploy ALL:", err);
- setStacks(prev =>
- prev.map(stack =>
- targetIds.has(stack.Id)
- ? {
- ...stack,
- redeployPhase: null,
- redeploying: false,
- redeployQueued: false
- }
- : stack
- )
- );
-
- const errorText = err.response?.data?.error || err.message || 'Unbekannter Fehler';
- showToast({
- variant: 'error',
- title: 'Redeploy ALL fehlgeschlagen',
- description: errorText
- });
- }
- };
-
- const handleRedeploySelection = async () => {
- if (!selectedStackIds.length) return;
-
- const eligibleIds = selectedStackIds.filter((id) => {
- const stack = stacks.find((entry) => entry.Id === id);
- 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) {
- setSelectedStackIds([]);
- return;
- }
-
- const eligibleSet = new Set(eligibleIds);
-
- setStacks(prev =>
- prev.map(stack =>
- eligibleSet.has(stack.Id)
- ? {
- ...stack,
- redeployPhase: REDEPLOY_PHASES.QUEUED,
- redeploying: true,
- redeployQueued: true
- }
- : stack
- )
- );
-
- try {
- await axios.put("/api/stacks/redeploy-selection", { stackIds: eligibleIds });
- setSelectedStackIds((prev) => prev.filter((id) => !eligibleSet.has(id)));
- // Statusupdates kommen über Socket.IO
- } catch (err) {
- console.error("❌ Fehler beim Redeploy Auswahl:", err);
- setStacks(prev =>
- prev.map(stack =>
- eligibleSet.has(stack.Id)
- ? {
- ...stack,
- redeployPhase: null,
- redeploying: false,
- redeployQueued: false
- }
- : stack
- )
- );
-
- const errorText = err.response?.data?.error || err.message || 'Unbekannter Fehler';
- showToast({
- variant: 'error',
- title: 'Redeploy Auswahl fehlgeschlagen',
- description: errorText
- });
- }
- };
-
- const hasSelection = selectedStackIds.length > 0;
- const hasOutdatedStacks = eligiblePageStacks.length > 0;
- const bulkButtonLabel = hasSelection
- ? `Redeploy Auswahl (${selectedStackIds.length})`
- : 'Redeploy All';
-
- const bulkActionDisabled = maintenanceLocked || (hasSelection
- ? selectionPromptVisible || selectedStackIds.length === 0 || selectedStackIds.every(id => {
- const targetStack = stacks.find(stack => stack.Id === id);
- 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);
-
- const handleBulkRedeploy = () => {
- if (maintenanceLocked) {
- showToast({
- variant: 'warning',
- title: 'Wartungsmodus aktiv',
- description: 'Redeploy-Aktionen sind während des Wartungsmodus deaktiviert.'
- });
- return;
- }
-
- if (hasSelection) {
- handleRedeploySelection();
- } else {
- handleRedeployAll();
- }
- };
-
- if (loading) return Lade Stacks...
;
- if (error) return
- {maintenanceLocked && (
-
- {maintenanceBanner}
-
- )}
-
-
-
-
Status
-
- setStatusFilter('all')}
- className={`px-4 py-2 text-sm font-medium transition ${statusFilter === 'all' ? 'bg-purple-600 text-white' : 'text-gray-300 hover:text-white'} border-r border-gray-700 last:border-r-0`}
- >
- Alle
-
- setStatusFilter('current')}
- className={`px-4 py-2 text-sm font-medium transition ${statusFilter === 'current' ? 'bg-purple-600 text-white' : 'text-gray-300 hover:text-white'} border-r border-gray-700 last:border-r-0`}
- >
- Aktuell
-
- setStatusFilter('outdated')}
- className={`px-4 py-2 text-sm font-medium transition ${statusFilter === 'outdated' ? 'bg-purple-600 text-white' : 'text-gray-300 hover:text-white'}`}
- >
- Veraltet
-
-
-
-
- Suche
- setSearchQuery(event.target.value)}
- placeholder="Name oder ID"
- className="w-full rounded-lg border border-gray-700 bg-gray-900 px-3 py-2 text-sm text-gray-100 placeholder-gray-500 focus:border-purple-500 focus:outline-none focus:ring-1 focus:ring-purple-500"
- />
-
-
-
- {selectionPreferenceStored && (
-
- Gespeicherte Entscheidung löschen
-
- )}
-
- {bulkButtonLabel}
-
-
- Einträge pro Seite
-
- {PER_PAGE_OPTIONS.map((option) => (
-
- {option.label}
-
- ))}
-
-
-
-
-
- {selectedStackIds.length > 0 && (
-
-
- Auswahl:
- {selectedStackIds.map((id) => {
- const stack = stacksById.get(id);
- const name = stack?.Name || `Stack ${id}`;
- const isFiltered = filteredStackIdSet.has(id);
- const isVisibleOnPage = visiblePageStackIds.has(id);
- return (
- handleChipRemove(id)}
- className="inline-flex items-center gap-2 rounded-full border border-purple-500/60 bg-purple-500/10 px-3 py-1 text-purple-100 transition hover:border-purple-400 hover:bg-purple-500/20"
- >
- {name}
- {!isFiltered && (
- Ausgefiltert
- )}
- {isFiltered && !isVisibleOnPage && (
- Andere Seite
- )}
- x
-
- );
- })}
-
-
- Auswahl aufheben
-
-
- )}
-
- {selectionPromptVisible && (
-
-
-
- {selectedStackIds.length === 1
- ? '1 Stack ist weiterhin ausgewählt.'
- : `${selectedStackIds.length} Stacks sind weiterhin ausgewählt.`}
- {' '}Soll die Auswahl entfernt werden?
-
-
- setRememberSelectionChoice(event.target.checked)}
- className="h-4 w-4 rounded border border-amber-400 bg-amber-950 text-amber-400 focus:ring-amber-300"
- />
- Einstellung merken (bis Browser geschlossen wird)
-
-
-
- applySelectionPreference('clear')}
- className="rounded-md bg-amber-500 px-4 py-2 text-sm font-medium text-amber-950 transition hover:bg-amber-400"
- >
- Auswahl entfernen
-
- applySelectionPreference('keep')}
- className="rounded-md border border-amber-400 px-4 py-2 text-sm font-medium transition hover:bg-amber-400/20"
- >
- Auswahl behalten
-
-
-
- )}
-
-
- {paginatedStacks.map(stack => {
- 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 isCurrent = stack.updateStatus === '✅';
- const isSelfStack = Boolean(stack.redeployDisabled);
- const isSelectable = !isBusy && !isCurrent && !isSelfStack && !maintenanceLocked;
-
- return (
-
-
-
toggleStackSelection(stack.Id, !isSelectable)}
- className={`h-5 w-5 text-purple-500 focus:ring-purple-400 border-gray-600 bg-gray-900 rounded ${!isSelectable ? 'opacity-40 cursor-not-allowed' : ''}`}
- disabled={!isSelectable}
- />
-
-
-
{stack.Name}
-
ID: {stack.Id}
- {stack.duplicateName && (
-
⚠️ Doppelter Name erkannt
- )}
-
-
-
-
- {isRunning ? (
- <>
- Redeploy
- läuft…
- >
- ) : isQueued ? (
- <>
- Redeploy
- in Warteliste…
- >
- ) : isSelfStack ? (
- <>
- System
- Redeploy deaktiviert
- >
- ) : isCurrent ? (
- <>
- Status
- Aktuell
- >
- ) : (
- handleRedeploy(stack.Id)}
- disabled={isBusy || 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"
- >
- Redeploy
-
- )}
-
-
- );
- })}
- {filteredStacks.length === 0 && (
-
- {hasActiveFilters ? 'Keine Stacks für die gesetzten Filter.' : 'Keine Stacks gefunden.'}
-
- )}
-
-
-
-
- {totalItems === 0
- ? 'Keine Stacks verfügbar'
- : perPage === 'all'
- ? `Zeige alle ${totalItems} Stacks`
- : `Zeige ${pageStart}-${pageEnd} von ${totalItems} Stacks`}
-
- {perPage !== 'all' && totalItems > 0 && (
-
-
- Zurück
-
-
- Seite {Math.min(page, totalPages)} / {totalPages}
-
- = totalPages}
- className="rounded-md border border-gray-600 px-3 py-1 text-sm text-gray-200 transition hover:bg-gray-700 disabled:opacity-60"
- >
- Weiter
-
-
- )}
-
-
- );
-}
diff --git a/frontend_old/src/assets/images/stackpulse.png b/frontend_old/src/assets/images/stackpulse.png
deleted file mode 100644
index 1df7f91..0000000
Binary files a/frontend_old/src/assets/images/stackpulse.png and /dev/null differ
diff --git a/frontend_old/src/components/ToastProvider.jsx b/frontend_old/src/components/ToastProvider.jsx
deleted file mode 100644
index f6ceeab..0000000
--- a/frontend_old/src/components/ToastProvider.jsx
+++ /dev/null
@@ -1,121 +0,0 @@
-import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
-
-const ToastContext = createContext(null);
-
-const VARIANT_STYLES = {
- success: {
- container: "border-green-500/80 bg-green-950/90 text-green-100",
- accent: "bg-green-400"
- },
- warning: {
- container: "border-yellow-500/80 bg-yellow-950/90 text-yellow-100",
- accent: "bg-yellow-400"
- },
- error: {
- container: "border-red-500/80 bg-red-950/90 text-red-100",
- accent: "bg-red-400"
- },
- info: {
- container: "border-sky-500/60 bg-sky-950/90 text-sky-100",
- accent: "bg-sky-400"
- }
-};
-
-const DEFAULT_DURATION = 6000;
-
-export default function ToastProvider({ children }) {
- const [toasts, setToasts] = useState([]);
- const timeoutsRef = useRef(new Map());
-
- const dismissToast = useCallback((id) => {
- setToasts((prev) => prev.filter((toast) => toast.id !== id));
-
- const handle = timeoutsRef.current.get(id);
- if (handle && typeof window !== "undefined") {
- window.clearTimeout(handle);
- }
- timeoutsRef.current.delete(id);
- }, []);
-
- const showToast = useCallback(({ id, title, description, variant = "info", duration = DEFAULT_DURATION } = {}) => {
- const toastId = id ?? `${Date.now()}-${Math.random().toString(16).slice(2)}`;
- const normalizedVariant = VARIANT_STYLES[variant] ? variant : "info";
-
- setToasts((prev) => {
- const withoutExisting = prev.filter((toast) => toast.id !== toastId);
- return [
- ...withoutExisting,
- {
- id: toastId,
- title,
- description,
- variant: normalizedVariant
- }
- ];
- });
-
- if (duration !== null && duration !== Infinity && typeof window !== "undefined") {
- const timeoutHandle = window.setTimeout(() => {
- dismissToast(toastId);
- }, duration);
-
- const existingHandle = timeoutsRef.current.get(toastId);
- if (existingHandle) {
- window.clearTimeout(existingHandle);
- }
- timeoutsRef.current.set(toastId, timeoutHandle);
- }
-
- return toastId;
- }, [dismissToast]);
-
- useEffect(() => {
- return () => {
- if (typeof window === "undefined") return;
- timeoutsRef.current.forEach((handle) => window.clearTimeout(handle));
- timeoutsRef.current.clear();
- };
- }, []);
-
- const contextValue = useMemo(() => ({ showToast, dismissToast }), [showToast, dismissToast]);
-
- return (
-