From ae3a3f93e80ef05aa0be2597cbe8a1713aa987c0 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 29 Oct 2025 15:47:07 +0000 Subject: [PATCH] y --- frontend_old/index.html | 13 - frontend_old/package.json | 33 - frontend_old/postcss.config.js | 6 - frontend_old/src/App.jsx | 65 -- frontend_old/src/Logs.jsx | 935 --------------- frontend_old/src/Maintenance.jsx | 1037 ---------------- frontend_old/src/Stacks.jsx | 1038 ----------------- frontend_old/src/assets/images/stackpulse.png | Bin 75913 -> 0 bytes frontend_old/src/components/ToastProvider.jsx | 121 -- .../src/context/MaintenanceContext.jsx | 153 --- frontend_old/src/index.css | 18 - frontend_old/src/main.jsx | 19 - frontend_old/tailwind.config.js | 13 - frontend_old/vite.config.js | 21 - 14 files changed, 3472 deletions(-) delete mode 100644 frontend_old/index.html delete mode 100644 frontend_old/package.json delete mode 100644 frontend_old/postcss.config.js delete mode 100644 frontend_old/src/App.jsx delete mode 100644 frontend_old/src/Logs.jsx delete mode 100644 frontend_old/src/Maintenance.jsx delete mode 100644 frontend_old/src/Stacks.jsx delete mode 100644 frontend_old/src/assets/images/stackpulse.png delete mode 100644 frontend_old/src/components/ToastProvider.jsx delete mode 100644 frontend_old/src/context/MaintenanceContext.jsx delete mode 100644 frontend_old/src/index.css delete mode 100644 frontend_old/src/main.jsx delete mode 100644 frontend_old/tailwind.config.js delete mode 100644 frontend_old/vite.config.js 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 @@ - - - - - - StackPulse - - - -
- - - diff --git a/frontend_old/package.json b/frontend_old/package.json deleted file mode 100644 index 1316001..0000000 --- a/frontend_old/package.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "name": "stackpulse-frontend", - "version": "0.1.0", - "type": "module", - "scripts": { - "dev": "vite", - "build": "vite build", - "preview": "vite preview" - }, - "dependencies": { - "axios": "1.7.5", - "socket.io-client": "4.8.1", - "@heroicons/react": "2.0.18", - "@material-tailwind/react": "2.1.4", - "apexcharts": "3.44.0", - "prop-types": "15.8.1", - "react": "18.2.0", - "react-apexcharts": "1.4.1", - "react-dom": "18.2.0", - "react-router-dom": "6.17.0" - }, - "devDependencies": { - "@types/react": "18.2.31", - "@types/react-dom": "18.2.14", - "@vitejs/plugin-react": "4.1.0", - "autoprefixer": "10.4.16", - "postcss": "8.4.31", - "prettier": "3.0.3", - "prettier-plugin-tailwindcss": "0.5.6", - "tailwindcss": "3.3.4", - "vite": "4.5.0" - } -} \ No newline at end of file diff --git a/frontend_old/postcss.config.js b/frontend_old/postcss.config.js deleted file mode 100644 index 33ad091..0000000 --- a/frontend_old/postcss.config.js +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -} diff --git a/frontend_old/src/App.jsx b/frontend_old/src/App.jsx deleted file mode 100644 index 5f9c323..0000000 --- a/frontend_old/src/App.jsx +++ /dev/null @@ -1,65 +0,0 @@ -import React from "react"; -import { NavLink, Route, Routes } from "react-router-dom"; -import Stacks from "./Stacks.jsx"; -import Logs from "./Logs.jsx"; -import Maintenance from "./Maintenance.jsx"; -import logo from "./assets/images/stackpulse.png"; -import { useMaintenance } from "./context/MaintenanceContext.jsx"; - -const navLinkBase = - "px-4 py-2 rounded-md font-medium transition-colors duration-150"; - -const getNavClass = ({ isActive }) => - `${navLinkBase} ${isActive ? "bg-purple-600 text-white" : "text-gray-300 hover:bg-gray-700"}`; - -export default function App() { - const { maintenance, update } = useMaintenance(); - const maintenanceActive = Boolean(maintenance?.active); - const maintenanceLabel = maintenance?.message || (update?.running ? "Portainer-Update läuft" : "Wartungsmodus aktiv"); - - return ( -
-
-
-
-
- v0.3 - {maintenanceActive && ( - - - {maintenanceLabel} - - )} - StackPulse -
- -
-
-
-
- - } /> - } /> - } /> - } /> - -
-
- ); -} diff --git a/frontend_old/src/Logs.jsx b/frontend_old/src/Logs.jsx deleted file mode 100644 index 96a4ade..0000000 --- a/frontend_old/src/Logs.jsx +++ /dev/null @@ -1,935 +0,0 @@ -import React, { useCallback, useEffect, useMemo, useState } from "react"; -import axios from "axios"; - -const STATUS_COLORS = { - success: "text-green-400", - warning: "text-yellow-400", - error: "text-red-400", - started: "text-blue-300" -}; - -const formatTimestamp = (value) => { - if (!value) return "-"; - const date = new Date(value); - if (Number.isNaN(date.getTime())) return value; - return date.toLocaleString("de-DE", { - year: "numeric", - month: "2-digit", - day: "2-digit", - hour: "2-digit", - minute: "2-digit", - second: "2-digit" - }); -}; - -const normalizeDateParam = (value) => { - if (!value) return undefined; - return value.replace("T", " "); -}; - -const FILTER_STORAGE_KEY = "redeployLogFilters"; -const ALL_OPTION_VALUE = "__all__"; -const ALL_OPTION_LABEL = "- Alle -"; -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 REDEPLOY_TYPE_LABELS = { - Einzeln: "Einzeln", - Alle: "Alle", - Auswahl: "Auswahl", - single: "Einzeln", - all: "Alle", - selection: "Auswahl" -}; - -const hasActiveFilters = (filters) => Boolean( - (filters.stacks && filters.stacks.length) || - (filters.statuses && filters.statuses.length) || - (filters.endpoints && filters.endpoints.length) || - (filters.redeployTypes && filters.redeployTypes.length) || - (filters.message && filters.message.trim()) || - (filters.from && filters.from.trim()) || - (filters.to && filters.to.trim()) -); - -export default function Logs() { - const [logs, setLogs] = useState([]); - const [totalLogs, setTotalLogs] = useState(0); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(""); - const [actionLoading, setActionLoading] = useState(false); - - const [stackOptions, setStackOptions] = useState([]); - const [statusOptions, setStatusOptions] = useState([]); - const [endpointOptions, setEndpointOptions] = useState([]); - const [redeployTypeOptions, setRedeployTypeOptions] = useState([]); - - const [selectedStacks, setSelectedStacks] = useState([]); - const [selectedStatuses, setSelectedStatuses] = useState([]); - const [selectedEndpoints, setSelectedEndpoints] = useState([]); - const [selectedRedeployTypes, setSelectedRedeployTypes] = useState([]); - const [messageQuery, setMessageQuery] = useState(""); - const [fromDate, setFromDate] = useState(""); - const [toDate, setToDate] = useState(""); - - const [filtersOpen, setFiltersOpen] = useState(false); - const [filtersReady, setFiltersReady] = useState(false); - const [optionsInitialized, setOptionsInitialized] = useState(false); - const [refreshSignal, setRefreshSignal] = useState(0); - - const [perPage, setPerPage] = useState(PER_PAGE_DEFAULT); - const [page, setPage] = useState(1); - - const updateFilterOptions = useCallback((payload) => { - const logsPayload = Array.isArray(payload) ? payload : payload?.items ?? []; - - const stackMap = new Map(); - const statusSet = new Set(); - const endpointSet = new Set(); - const redeployTypeSet = new Set(); - - logsPayload.forEach((log) => { - if (log.stackId) { - const value = String(log.stackId); - const label = log.stackName || `Stack ${value}`; - stackMap.set(value, label); - } - - if (log.status) { - statusSet.add(log.status); - } - - if (log.endpoint !== null && log.endpoint !== undefined && log.endpoint !== "") { - endpointSet.add(String(log.endpoint)); - } - - if (log.redeployType) { - redeployTypeSet.add(log.redeployType); - } - }); - - setStackOptions(Array.from(stackMap.entries()) - .map(([value, label]) => ({ value, label })) - .sort((a, b) => a.label.localeCompare(b.label))); - - setStatusOptions(Array.from(statusSet).sort()); - - setEndpointOptions(Array.from(endpointSet) - .sort((a, b) => a.localeCompare(b, undefined, { numeric: true }))); - - setRedeployTypeOptions(Array.from(redeployTypeSet).sort()); - setOptionsInitialized(true); - }, []); - - const stackLabelMap = useMemo(() => { - const map = new Map(); - stackOptions.forEach((option) => { - map.set(option.value, option.label); - }); - return map; - }, [stackOptions]); - - useEffect(() => { - if (!optionsInitialized) return; - setSelectedStacks((prev) => { - const valid = prev.filter((value) => stackOptions.some((option) => option.value === value)); - return valid.length === prev.length ? prev : valid; - }); - }, [optionsInitialized, stackOptions]); - - useEffect(() => { - if (!optionsInitialized) return; - setSelectedStatuses((prev) => { - const valid = prev.filter((value) => statusOptions.includes(value)); - return valid.length === prev.length ? prev : valid; - }); - }, [optionsInitialized, statusOptions]); - - useEffect(() => { - if (!optionsInitialized) return; - setSelectedEndpoints((prev) => { - const valid = prev.filter((value) => endpointOptions.includes(value)); - return valid.length === prev.length ? prev : valid; - }); - }, [optionsInitialized, endpointOptions]); - - useEffect(() => { - if (!optionsInitialized) return; - setSelectedRedeployTypes((prev) => { - const valid = prev.filter((value) => redeployTypeOptions.includes(value)); - return valid.length === prev.length ? prev : valid; - }); - }, [optionsInitialized, redeployTypeOptions]); - - useEffect(() => { - if (typeof window === "undefined") { - setFiltersReady(true); - return; - } - - try { - const storedValue = window.localStorage.getItem(FILTER_STORAGE_KEY); - if (storedValue) { - const parsed = JSON.parse(storedValue); - const storedFilters = parsed?.filters ?? parsed ?? {}; - const storedPagination = parsed?.pagination ?? {}; - - setSelectedStacks(storedFilters.stacks || []); - setSelectedStatuses(storedFilters.statuses || []); - setSelectedEndpoints(storedFilters.endpoints || []); - setSelectedRedeployTypes(storedFilters.redeployTypes || []); - setMessageQuery(storedFilters.message || ""); - setFromDate(storedFilters.from || ""); - setToDate(storedFilters.to || ""); - setFiltersOpen(hasActiveFilters(storedFilters)); - - const rawPerPage = storedPagination.perPage; - if (rawPerPage !== undefined) { - const parsedPerPage = String(rawPerPage); - if (VALID_PER_PAGE_VALUES.has(parsedPerPage)) { - setPerPage(parsedPerPage); - } - } - - const rawPage = storedPagination.page; - if (typeof rawPage === 'number' && rawPage > 0) { - setPage(rawPage); - } - } - } catch (storageError) { - console.error("❌ Fehler beim Laden der gespeicherten Log-Filter:", storageError); - } finally { - setFiltersReady(true); - } - }, []); - - const buildFilterParams = useCallback(() => { - const params = {}; - - if (selectedStacks.length) { - params.stackIds = selectedStacks.join(","); - } - - if (selectedStatuses.length) { - params.statuses = selectedStatuses.join(","); - } - - if (selectedEndpoints.length) { - params.endpoints = selectedEndpoints.join(","); - } - - if (selectedRedeployTypes.length) { - params.redeployTypes = selectedRedeployTypes.join(","); - } - - if (messageQuery.trim()) { - params.message = messageQuery.trim(); - } - - const fromParam = normalizeDateParam(fromDate); - if (fromParam) { - params.from = fromParam; - } - - const toParam = normalizeDateParam(toDate); - if (toParam) { - params.to = toParam; - } - - return params; - }, [selectedStacks, selectedStatuses, selectedEndpoints, selectedRedeployTypes, messageQuery, fromDate, toDate]); - - useEffect(() => { - if (!filtersReady) return; - - let cancelled = false; - axios.get("/api/logs", { params: { perPage: 'all', page: 1 } }) - .then((response) => { - if (cancelled) return; - updateFilterOptions(response.data); - }) - .catch((err) => { - if (cancelled) return; - console.error("⚠️ Konnte Filteroptionen nicht aktualisieren:", err); - }); - - return () => { - cancelled = true; - }; - }, [filtersReady, updateFilterOptions, refreshSignal]); - - const currentFilters = useMemo(() => ({ - stacks: selectedStacks, - statuses: selectedStatuses, - endpoints: selectedEndpoints, - redeployTypes: selectedRedeployTypes, - message: messageQuery, - from: fromDate, - to: toDate - }), [selectedStacks, selectedStatuses, selectedEndpoints, selectedRedeployTypes, messageQuery, fromDate, toDate]); - - useEffect(() => { - if (!filtersReady) return; - - const params = { ...buildFilterParams() }; - if (perPage === 'all') { - params.perPage = 'all'; - params.page = 1; - } else { - params.perPage = perPage; - params.page = page; - } - - let cancelled = false; - setLoading(true); - setError(""); - - axios.get("/api/logs", { params }) - .then((response) => { - if (cancelled) return; - const data = response.data || {}; - const items = Array.isArray(data) ? data : data.items ?? []; - const total = Array.isArray(data) ? items.length : data.total ?? items.length; - - setLogs(items); - setTotalLogs(total); - - if (perPage === 'all') { - if (page !== 1) setPage(1); - } else { - const totalPages = Math.max(1, Math.ceil((total || 0) / Number(perPage))); - const nextPage = Math.min(Math.max(page, 1), totalPages); - if (nextPage !== page) { - setPage(nextPage); - } - } - }) - .catch((err) => { - if (cancelled) return; - console.error("❌ Fehler beim Laden der Logs:", err); - setError("Fehler beim Laden der Redeploy-Logs"); - }) - .finally(() => { - if (cancelled) return; - setLoading(false); - }); - - return () => { - cancelled = true; - }; - }, [filtersReady, buildFilterParams, updateFilterOptions, refreshSignal, perPage, page]); - - useEffect(() => { - if (!filtersReady) return; - if (typeof window === "undefined") return; - - try { - window.localStorage.setItem( - FILTER_STORAGE_KEY, - JSON.stringify({ - filters: currentFilters, - pagination: { - perPage, - page - } - }) - ); - } catch (storageError) { - console.error("⚠️ Konnte Filter nicht speichern:", storageError); - } - }, [filtersReady, currentFilters, perPage, page]); - - const handleMultiSelectChange = (setter) => (event) => { - const values = Array.from(event.target.selectedOptions).map((option) => option.value); - if (values.includes(ALL_OPTION_VALUE)) { - setter([]); - } else { - setter(values); - } - setPage(1); - }; - - const handleOptionMouseDown = (event, currentValues, setter) => { - event.preventDefault(); - event.stopPropagation(); - - const { value } = event.target; - if (value === ALL_OPTION_VALUE) { - if (currentValues.length) { - setter([]); - setPage(1); - } - return; - } - - const nextValues = currentValues.includes(value) - ? currentValues.filter((entry) => entry !== value) - : [...currentValues, value]; - - setter(nextValues); - setPage(1); - }; - - const handleResetFilters = () => { - setSelectedStacks([]); - setSelectedStatuses([]); - setSelectedEndpoints([]); - setSelectedRedeployTypes([]); - setMessageQuery(""); - setFromDate(""); - setToDate(""); - setFiltersOpen(false); - setPage(1); - }; - - const handleToggleFilters = () => { - setFiltersOpen((prev) => !prev); - }; - - const handleRefresh = () => { - setRefreshSignal((prev) => prev + 1); - }; - - const handlePerPageChange = (event) => { - const value = event.target.value; - if (!VALID_PER_PAGE_VALUES.has(value)) return; - setPerPage(value); - setPage(1); - }; - - const handlePageChange = (nextPage) => { - if (perPage === 'all') return; - if (nextPage < 1) return; - const totalPages = Math.max(1, Math.ceil((totalLogs || 0) / Number(perPage))); - if (nextPage > totalPages) return; - setPage(nextPage); - }; - - const stackSelectOptions = useMemo(() => { - const entries = stackOptions.filter((option) => option.value !== ALL_OPTION_VALUE); - return [ - { value: ALL_OPTION_VALUE, label: ALL_OPTION_LABEL }, - ...entries - ]; - }, [stackOptions]); - - const statusSelectOptions = useMemo(() => { - const entries = statusOptions - .filter((status) => status !== ALL_OPTION_VALUE) - .map((status) => ({ value: status, label: status })); - return [ - { value: ALL_OPTION_VALUE, label: ALL_OPTION_LABEL }, - ...entries - ]; - }, [statusOptions]); - - const endpointSelectOptions = useMemo(() => { - const entries = endpointOptions - .filter((endpoint) => endpoint !== ALL_OPTION_VALUE) - .map((endpoint) => ({ value: endpoint, label: endpoint })); - return [ - { value: ALL_OPTION_VALUE, label: ALL_OPTION_LABEL }, - ...entries - ]; - }, [endpointOptions]); - - const redeployTypeSelectOptions = useMemo(() => { - const entries = redeployTypeOptions - .filter((type) => type !== ALL_OPTION_VALUE) - .map((type) => ({ value: type, label: REDEPLOY_TYPE_LABELS[type] ?? type })); - return [ - { value: ALL_OPTION_VALUE, label: ALL_OPTION_LABEL }, - ...entries - ]; - }, [redeployTypeOptions]); - - const activeFilterCount = useMemo(() => { - let count = 0; - if (selectedStacks.length) count += selectedStacks.length; - if (selectedStatuses.length) count += selectedStatuses.length; - if (selectedEndpoints.length) count += selectedEndpoints.length; - if (selectedRedeployTypes.length) count += selectedRedeployTypes.length; - if (messageQuery.trim()) count += 1; - if (fromDate) count += 1; - if (toDate) count += 1; - return count; - }, [selectedStacks, selectedStatuses, selectedEndpoints, messageQuery, fromDate, toDate]); - - const totalPages = useMemo(() => { - if (perPage === 'all') return 1; - const numeric = Number(perPage) || Number(PER_PAGE_DEFAULT); - return Math.max(1, Math.ceil((totalLogs || 0) / numeric)); - }, [perPage, totalLogs]); - - const rangeStart = useMemo(() => { - if (totalLogs === 0) return 0; - if (perPage === 'all') return 1; - return (page - 1) * Number(perPage) + 1; - }, [totalLogs, perPage, page]); - - const rangeEnd = useMemo(() => { - if (totalLogs === 0) return 0; - if (perPage === 'all') return totalLogs; - return Math.min(totalLogs, (page - 1) * Number(perPage) + logs.length); - }, [totalLogs, perPage, page, logs.length]); - - const handleDeleteLog = async (id) => { - if (!window.confirm("Diesen Log-Eintrag dauerhaft löschen?")) return; - setActionLoading(true); - setError(""); - try { - await axios.delete(`/api/logs/${id}`); - setRefreshSignal((prev) => prev + 1); - } catch (err) { - console.error("❌ Fehler beim Löschen des Logs:", err); - setError("Fehler beim Löschen des Redeploy-Logs"); - } finally { - setActionLoading(false); - } - }; - - const handleDeleteFiltered = async () => { - if (!logs.length) return; - if (!window.confirm("Alle angezeigten Logs (entsprechend Filter) löschen?")) return; - setActionLoading(true); - setError(""); - try { - const params = { - ...buildFilterParams(), - ids: logs.map((log) => log.id).join(',') - }; - await axios.delete("/api/logs", { params }); - setRefreshSignal((prev) => prev + 1); - } catch (err) { - console.error("❌ Fehler beim Löschen der gefilterten Logs:", err); - setError("Fehler beim Löschen der gefilterten Logs"); - } finally { - setActionLoading(false); - } - }; - - const handleExport = async (format) => { - setActionLoading(true); - setError(""); - try { - const response = await axios.get("/api/logs/export", { - params: { ...buildFilterParams(), format }, - responseType: "blob" - }); - - const contentType = response.headers["content-type"] || (format === "sql" ? "application/sql" : "text/plain"); - const disposition = response.headers["content-disposition"] || ""; - const match = disposition.match(/filename="?([^";]+)"?/i); - const filename = match?.[1] || `redeploy-logs.${format}`; - - const blob = new Blob([response.data], { type: contentType }); - const url = window.URL.createObjectURL(blob); - const link = document.createElement("a"); - link.href = url; - link.download = filename; - document.body.appendChild(link); - link.click(); - link.remove(); - window.URL.revokeObjectURL(url); - } catch (err) { - console.error("❌ Fehler beim Export der Logs:", err); - setError("Fehler beim Export der Redeploy-Logs"); - } finally { - setActionLoading(false); - } - }; - - return ( -
-
-

Redeploy-Logs

-
- - - - {loading && Aktualisiere…} - -
-
- - {error && ( -
- {error} -
- )} - -
- - - {filtersOpen && ( -
-
-
- - -
- {selectedStacks.length === 0 ? ( - - Alle Stacks - - ) : ( -
- {selectedStacks.map((stackId) => ( - - {stackLabelMap.get(stackId) ?? `Stack ${stackId}`} - - ))} -
- )} -
-
- -
- - -
- {selectedStatuses.length === 0 ? ( - - Alle Status - - ) : ( -
- {selectedStatuses.map((status) => ( - - {status} - - ))} -
- )} -
-
- -
- - -
- {selectedEndpoints.length === 0 ? ( - - Alle Endpoints - - ) : ( -
- {selectedEndpoints.map((endpoint) => ( - - {endpoint} - - ))} -
- )} -
-
- -
- - -
- {selectedRedeployTypes.length === 0 ? ( - - Alle Typen - - ) : ( -
- {selectedRedeployTypes.map((type) => ( - - {REDEPLOY_TYPE_LABELS[type] ?? type} - - ))} -
- )} -
-
- -
- - { - 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" - /> -
- -
- - { - 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" - /> -
- -
- - { - 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" - /> -
-
- -
- -
-
- )} -
- -
- Einträge pro Seite - -
- -
- - - - - - - - - - - - - - {logs.length === 0 && !loading && ( - - - - )} - {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 ( - - - - - - - - - - ); - })} - -
ZeitpunktStackArtStatusNachrichtEndpointAktionen
- Keine Logs vorhanden. -
- {formatTimestamp(log.timestamp)} - -
- {stackDisplayName} - {showStackId && ( - ID: {log.stackId} - )} -
-
- {redeployTypeLabel} - - {log.status} - - {log.message || "-"} - - {log.endpoint ?? "-"} - - -
-
- -
- - {totalLogs === 0 - ? "Keine Einträge" - : perPage === 'all' - ? `Zeige alle ${totalLogs} Einträge` - : `Zeige ${rangeStart.toLocaleString()} – ${rangeEnd.toLocaleString()} von ${totalLogs.toLocaleString()} Einträgen`} - - {perPage !== 'all' && ( -
- - - Seite {page} / {totalPages} - - -
- )} -
-
- ); -} 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" - })} -

- )} -
-
- -
-
- - {!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

-
-
- -
- - -
-
-
- - -
- handleSshDraftChange('password', event.target.value)} - disabled={sshSaving || sshTesting || sshDeleting || updateRunning} - className="w-full rounded-md border border-gray-700 bg-gray-950/70 px-3 py-2 text-sm text-gray-100 focus:border-purple-500 focus:outline-none" - placeholder="Passwort für den SSH-Benutzer" - autoComplete="off" - spellCheck={false} - /> - {sshPasswordStored && !sshDraft.password && ( - - Ein Passwort ist gespeichert. Neuer Inhalt ersetzt es oder lösche die Konfiguration unten. - - )} -
-