From cc7d80e569731610dd50d03bf566eadd9a20a8f6 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 30 Sep 2025 09:08:36 +0000 Subject: [PATCH] Delete/Export/Pageination --- backend/data/stackpulse.db-shm | Bin 32768 -> 32768 bytes backend/data/stackpulse.db-wal | Bin 0 -> 8272 bytes backend/db/redeployLogs.js | 149 ++++++++++ backend/index.js | 151 +++++----- frontend/src/Logs.jsx | 487 +++++++++++++++++++++++++-------- 5 files changed, 598 insertions(+), 189 deletions(-) diff --git a/backend/data/stackpulse.db-shm b/backend/data/stackpulse.db-shm index fe9ac2845eca6fe6da8a63cd096d9cf9e24ece10..50e066a7fea23e8d411259b916f9392eb600e852 100644 GIT binary patch delta 155 zcmZo@U}|V!s+V}A%K!pQK+MR%AOND7fq3?f5|=W$y1?aggZ6I}lw#RYa!GzN=VD$` h)dP(J1CY7@kpNV9Vm&7-NQ!A=W4GR1Pit^n&UkRTHfGsDEiFJ+ufR+KZ~<=@A^ z$e+W&p93UD!DtAKhQMeDjE2By2#kinXb6mkz-S1JhQMeDjE2By2#kgR#UT*D#KB-J zC@&qzXk=hys%v1WYh { + if (!value && value !== 0) return []; + const base = Array.isArray(value) ? value : [value]; + return base + .flatMap((entry) => String(entry).split(',')) + .map((entry) => entry.trim()) + .filter(Boolean); +}; + +const singleValue = (value) => { + if (value === undefined || value === null) return undefined; + return Array.isArray(value) ? value[0] : value; +}; + +export function buildLogFilter(queryParams = {}) { + const filters = []; + const params = {}; + + const ids = valueToArray(queryParams.ids ?? queryParams.id) + .map((entry) => Number(entry)) + .filter((value) => !Number.isNaN(value)); + if (ids.length) { + const placeholders = ids.map((_, idx) => `@id${idx}`); + filters.push(`id IN (${placeholders.join(', ')})`); + ids.forEach((value, idx) => { + params[`id${idx}`] = value; + }); + } + + const stackIds = valueToArray(queryParams.stackIds ?? queryParams.stackId); + if (stackIds.length) { + const placeholders = stackIds.map((_, idx) => `@stackId${idx}`); + filters.push(`stack_id IN (${placeholders.join(', ')})`); + stackIds.forEach((stack, idx) => { + params[`stackId${idx}`] = stack; + }); + } + + const statuses = valueToArray(queryParams.statuses ?? queryParams.status); + if (statuses.length) { + const placeholders = statuses.map((_, idx) => `@status${idx}`); + filters.push(`status IN (${placeholders.join(', ')})`); + statuses.forEach((entry, idx) => { + params[`status${idx}`] = entry; + }); + } + + const endpoints = valueToArray(queryParams.endpoints ?? queryParams.endpoint); + if (endpoints.length) { + const placeholders = endpoints.map((_, idx) => `@endpoint${idx}`); + filters.push(`endpoint IN (${placeholders.join(', ')})`); + endpoints.forEach((entry, idx) => { + const numeric = Number(entry); + params[`endpoint${idx}`] = Number.isNaN(numeric) ? entry : numeric; + }); + } + + const messageQuery = singleValue(queryParams.message); + if (messageQuery && String(messageQuery).trim()) { + filters.push('message LIKE @message'); + params.message = `%${String(messageQuery).trim()}%`; + } + + const from = singleValue(queryParams.from); + if (from) { + filters.push('timestamp >= @from'); + params.from = from; + } + + const to = singleValue(queryParams.to); + if (to) { + filters.push('timestamp <= @to'); + params.to = to; + } + + return { + whereClause: filters.length ? `WHERE ${filters.join(' AND ')}` : '', + params, + }; +} + const insertRedeployLogStmt = db.prepare(` INSERT INTO redeploy_logs (stack_id, stack_name, status, message, endpoint) VALUES (@stackId, @stackName, @status, @message, @endpoint) @@ -18,3 +99,71 @@ export function logRedeployEvent({ stackId, stackName, status, message = null, e console.error('❌ Fehler beim Speichern des Redeploy-Logs:', err.message); } } + +export function deleteLogById(id) { + const stmt = db.prepare('DELETE FROM redeploy_logs WHERE id = ?'); + const info = stmt.run(id); + return info.changes || 0; +} + +export function deleteLogsByFilters(queryParams = {}) { + const { whereClause, params } = buildLogFilter(queryParams); + const stmt = db.prepare(`DELETE FROM redeploy_logs ${whereClause}`); + const info = stmt.run(params); + return info.changes || 0; +} + +export function exportLogsByFilters(queryParams = {}, format = 'txt') { + const { whereClause, params } = buildLogFilter(queryParams); + const rows = db.prepare(` + SELECT id, timestamp, stack_id AS stackId, stack_name AS stackName, status, message, endpoint + FROM redeploy_logs + ${whereClause} + ORDER BY datetime(timestamp) DESC + `).all(params); + + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + + if (format === 'sql') { + const statements = rows.map((row) => { + const columns = ['id', 'timestamp', 'stack_id', 'stack_name', 'status', 'message', 'endpoint']; + const values = [ + row.id, + row.timestamp, + row.stackId, + row.stackName, + row.status, + row.message, + row.endpoint + ].map((value) => { + if (value === null || value === undefined) return 'NULL'; + return `'${String(value).replace(/'/g, "''")}'`; + }); + return `INSERT INTO redeploy_logs (${columns.join(', ')}) VALUES (${values.join(', ')});`; + }); + + return { + filename: `redeploy-logs-${timestamp}.sql`, + contentType: 'application/sql; charset=utf-8', + content: statements.join('\n') + }; + } + + const lines = rows.map((row) => { + const parts = [ + `[${row.id}]`, + row.timestamp, + `Stack: ${row.stackName ?? 'Unbekannt'} (ID: ${row.stackId})`, + `Status: ${row.status}`, + `Endpoint: ${row.endpoint ?? '-'}`, + `Nachricht: ${row.message ?? '-'}` + ]; + return parts.join(' | '); + }); + + return { + filename: `redeploy-logs-${timestamp}.txt`, + contentType: 'text/plain; charset=utf-8', + content: lines.join('\n') + }; +} diff --git a/backend/index.js b/backend/index.js index ef4d3b1..ff6edc7 100644 --- a/backend/index.js +++ b/backend/index.js @@ -7,7 +7,13 @@ import { Server } from 'socket.io'; import path from 'path'; import { fileURLToPath } from 'url'; import { db } from './db/index.js'; -import { logRedeployEvent } from './db/redeployLogs.js'; +import { + logRedeployEvent, + buildLogFilter, + deleteLogById, + deleteLogsByFilters, + exportLogsByFilters +} from './db/redeployLogs.js'; dotenv.config(); @@ -95,74 +101,15 @@ app.get('/api/stacks', async (req, res) => { // Redeploy-Logs abrufen app.get('/api/logs', (req, res) => { - const limit = Math.min(parseInt(req.query.limit, 10) || 100, 500); - const offset = Math.max(parseInt(req.query.offset, 10) || 0, 0); + const perPageParam = req.query.perPage ?? req.query.limit; + const perPage = perPageParam === 'all' ? 'all' : Math.min(parseInt(perPageParam, 10) || 50, 500); + const page = Math.max(parseInt(req.query.page, 10) || 1, 1); - const valueToArray = (value) => { - if (!value && value !== 0) return []; - const base = Array.isArray(value) ? value : [value]; - return base - .flatMap((entry) => String(entry).split(',')) - .map((entry) => entry.trim()) - .filter(Boolean); - }; + const limit = perPage === 'all' ? undefined : perPage; + const offset = perPage === 'all' ? 0 : (page - 1) * perPage; - const singleValue = (value) => { - if (value === undefined || value === null) return undefined; - return Array.isArray(value) ? value[0] : value; - }; - - const filters = []; - const params = { limit, offset }; - - const stackIds = valueToArray(req.query.stackIds ?? req.query.stackId); - if (stackIds.length) { - const placeholders = stackIds.map((_, idx) => `@stackId${idx}`); - filters.push(`stack_id IN (${placeholders.join(', ')})`); - stackIds.forEach((stack, idx) => { - params[`stackId${idx}`] = stack; - }); - } - - const statuses = valueToArray(req.query.statuses ?? req.query.status); - if (statuses.length) { - const placeholders = statuses.map((_, idx) => `@status${idx}`); - filters.push(`status IN (${placeholders.join(', ')})`); - statuses.forEach((entry, idx) => { - params[`status${idx}`] = entry; - }); - } - - const endpoints = valueToArray(req.query.endpoints ?? req.query.endpoint); - if (endpoints.length) { - const placeholders = endpoints.map((_, idx) => `@endpoint${idx}`); - filters.push(`endpoint IN (${placeholders.join(', ')})`); - endpoints.forEach((entry, idx) => { - const numeric = Number(entry); - params[`endpoint${idx}`] = Number.isNaN(numeric) ? entry : numeric; - }); - } - - const messageQuery = singleValue(req.query.message); - if (messageQuery && String(messageQuery).trim()) { - filters.push('message LIKE @message'); - params.message = `%${String(messageQuery).trim()}%`; - } - - const from = singleValue(req.query.from); - if (from) { - filters.push('timestamp >= @from'); - params.from = from; - } - - const to = singleValue(req.query.to); - if (to) { - filters.push('timestamp <= @to'); - params.to = to; - } - - const whereClause = filters.length ? `WHERE ${filters.join(' AND ')}` : ''; - const query = ` + const { whereClause, params } = buildLogFilter(req.query); + const baseQuery = ` SELECT id, timestamp, @@ -174,13 +121,34 @@ app.get('/api/logs', (req, res) => { FROM redeploy_logs ${whereClause} ORDER BY datetime(timestamp) DESC - LIMIT @limit OFFSET @offset `; + const countQuery = ` + SELECT COUNT(*) as total + FROM redeploy_logs + ${whereClause} + `; + + const query = limit !== undefined + ? `${baseQuery} LIMIT @limit OFFSET @offset` + : baseQuery; + + if (limit !== undefined) { + params.limit = limit; + params.offset = offset; + } + try { const stmt = db.prepare(query); const logs = stmt.all(params); - res.json(logs); + const total = db.prepare(countQuery).get(params)?.total ?? logs.length; + + res.json({ + total, + items: logs, + page, + perPage: perPage === 'all' ? 'all' : limit + }); } catch (err) { console.error('❌ Fehler beim Abrufen der Redeploy-Logs:', err.message); if (err.message.includes('no such table')) { @@ -190,6 +158,51 @@ app.get('/api/logs', (req, res) => { } }); +app.delete('/api/logs/:id', (req, res) => { + const id = Number(req.params.id); + if (Number.isNaN(id)) { + return res.status(400).json({ error: 'Ungültige ID' }); + } + + try { + const changes = deleteLogById(id); + if (!changes) { + return res.status(404).json({ error: 'Eintrag nicht gefunden' }); + } + res.json({ success: true, deleted: changes }); + } catch (err) { + console.error('❌ Fehler beim Löschen des Redeploy-Logs:', err.message); + res.status(500).json({ error: 'Fehler beim Löschen des Redeploy-Logs' }); + } +}); + +app.delete('/api/logs', (req, res) => { + try { + const deleted = deleteLogsByFilters(req.query); + res.json({ success: true, deleted }); + } catch (err) { + console.error('❌ Fehler beim Löschen der Redeploy-Logs:', err.message); + res.status(500).json({ error: 'Fehler beim Löschen der Redeploy-Logs' }); + } +}); + +app.get('/api/logs/export', (req, res) => { + const format = (req.query.format || 'txt').toLowerCase(); + if (!['txt', 'sql'].includes(format)) { + return res.status(400).json({ error: 'Ungültiges Export-Format' }); + } + + try { + const payload = exportLogsByFilters(req.query, format); + res.setHeader('Content-Type', payload.contentType); + res.setHeader('Content-Disposition', `attachment; filename="${payload.filename}"`); + res.send(payload.content); + } catch (err) { + console.error('❌ Fehler beim Export der Redeploy-Logs:', err.message); + res.status(500).json({ error: 'Fehler beim Export der Redeploy-Logs' }); + } +}); + // Einzel-Redeploy app.put('/api/stacks/:id/redeploy', async (req, res) => { const { id } = req.params; diff --git a/frontend/src/Logs.jsx b/frontend/src/Logs.jsx index 5cdd203..24e389a 100644 --- a/frontend/src/Logs.jsx +++ b/frontend/src/Logs.jsx @@ -30,6 +30,15 @@ const normalizeDateParam = (value) => { 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 hasActiveFilters = (filters) => Boolean( (filters.stacks && filters.stacks.length) || @@ -42,8 +51,10 @@ const hasActiveFilters = (filters) => Boolean( 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([]); @@ -60,7 +71,12 @@ export default function Logs() { const [filtersReady, setFiltersReady] = useState(false); const [refreshSignal, setRefreshSignal] = useState(0); - const updateFilterOptions = useCallback((logsPayload) => { + const [perPage, setPerPage] = useState(PER_PAGE_DEFAULT); + const [page, setPage] = useState(1); + + const updateFilterOptions = useCallback((payload) => { + const logsPayload = Array.isArray(payload) ? payload : payload?.items ?? []; + setStackOptions((prev) => { const map = new Map(prev.map((entry) => [entry.value, entry.label])); logsPayload.forEach((log) => { @@ -93,6 +109,14 @@ export default function Logs() { }); }, []); + const stackLabelMap = useMemo(() => { + const map = new Map(); + stackOptions.forEach((option) => { + map.set(option.value, option.label); + }); + return map; + }, [stackOptions]); + useEffect(() => { if (typeof window === "undefined") { setFiltersReady(true); @@ -105,6 +129,7 @@ export default function Logs() { const parsed = JSON.parse(storedValue); const storedFilters = parsed?.filters ?? parsed ?? {}; const storedOptions = parsed?.options ?? {}; + const storedPagination = parsed?.pagination ?? {}; setSelectedStacks(storedFilters.stacks || []); setSelectedStatuses(storedFilters.statuses || []); @@ -125,6 +150,19 @@ export default function Logs() { if (Array.isArray(storedOptions.endpoints) && storedOptions.endpoints.length) { setEndpointOptions(storedOptions.endpoints); } + + 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); @@ -133,11 +171,43 @@ export default function Logs() { } }, []); + 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 (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, messageQuery, fromDate, toDate]); + useEffect(() => { if (!filtersReady) return; let cancelled = false; - axios.get("/api/logs", { params: { limit: 500 } }) + axios.get("/api/logs", { params: { ...buildFilterParams(), perPage: 'all', page: 1 } }) .then((response) => { if (cancelled) return; updateFilterOptions(response.data); @@ -150,7 +220,7 @@ export default function Logs() { return () => { cancelled = true; }; - }, [filtersReady, updateFilterOptions, refreshSignal]); + }, [filtersReady, buildFilterParams, updateFilterOptions, refreshSignal]); const currentFilters = useMemo(() => ({ stacks: selectedStacks, @@ -161,13 +231,126 @@ export default function Logs() { to: toDate }), [selectedStacks, selectedStatuses, selectedEndpoints, messageQuery, fromDate, toDate]); - const stackLabelMap = useMemo(() => { - const map = new Map(); - stackOptions.forEach((option) => { - map.set(option.value, option.label); - }); - return map; - }, [stackOptions]); + 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); + updateFilterOptions(response.data); + + 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, + options: { + stacks: stackOptions, + statuses: statusOptions, + endpoints: endpointOptions, + }, + pagination: { + perPage, + page + } + }) + ); + } catch (storageError) { + console.error("⚠️ Konnte Filter nicht speichern:", storageError); + } + }, [filtersReady, currentFilters, stackOptions, statusOptions, endpointOptions, 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 handleResetFilters = () => { + setSelectedStacks([]); + setSelectedStatuses([]); + setSelectedEndpoints([]); + 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); @@ -197,110 +380,6 @@ export default function Logs() { ]; }, [endpointOptions]); - useEffect(() => { - if (!filtersReady) return; - - const params = { limit: 200 }; - - if (currentFilters.stacks.length) { - params.stackIds = currentFilters.stacks.join(","); - } - - if (currentFilters.statuses.length) { - params.statuses = currentFilters.statuses.join(","); - } - - if (currentFilters.endpoints.length) { - params.endpoints = currentFilters.endpoints.join(","); - } - - if (currentFilters.message.trim()) { - params.message = currentFilters.message.trim(); - } - - const fromParam = normalizeDateParam(currentFilters.from); - if (fromParam) { - params.from = fromParam; - } - - const toParam = normalizeDateParam(currentFilters.to); - if (toParam) { - params.to = toParam; - } - - let cancelled = false; - setLoading(true); - setError(""); - - axios.get("/api/logs", { params }) - .then((response) => { - if (cancelled) return; - setLogs(response.data); - updateFilterOptions(response.data); - }) - .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, currentFilters, updateFilterOptions, refreshSignal]); - - useEffect(() => { - if (!filtersReady) return; - if (typeof window === "undefined") return; - - try { - window.localStorage.setItem( - FILTER_STORAGE_KEY, - JSON.stringify({ - filters: currentFilters, - options: { - stacks: stackOptions, - statuses: statusOptions, - endpoints: endpointOptions, - } - }) - ); - } catch (storageError) { - console.error("⚠️ Konnte Filter nicht speichern:", storageError); - } - }, [filtersReady, currentFilters, stackOptions, statusOptions, endpointOptions]); - - const handleMultiSelectChange = (setter) => (event) => { - const values = Array.from(event.target.selectedOptions).map((option) => option.value); - if (values.includes(ALL_OPTION_VALUE)) { - setter([]); - return; - } - setter(values); - }; - - const handleResetFilters = () => { - setSelectedStacks([]); - setSelectedStatuses([]); - setSelectedEndpoints([]); - setMessageQuery(""); - setFromDate(""); - setToDate(""); - setFiltersOpen(false); - }; - - const handleToggleFilters = () => { - setFiltersOpen((prev) => !prev); - }; - - const handleRefresh = () => { - setRefreshSignal((prev) => prev + 1); - }; - const activeFilterCount = useMemo(() => { let count = 0; if (selectedStacks.length) count += selectedStacks.length; @@ -312,15 +391,133 @@ export default function Logs() { 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

-
+
+
+ Einträge pro Seite + +
+ + + {loading && Aktualisiere…} @@ -472,7 +669,10 @@ export default function Logs() { setMessageQuery(event.target.value)} + onChange={(event) => { + 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" /> @@ -483,7 +683,10 @@ export default function Logs() { setFromDate(event.target.value)} + onChange={(event) => { + 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" />
@@ -493,7 +696,10 @@ export default function Logs() { setToDate(event.target.value)} + onChange={(event) => { + 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" />
@@ -520,12 +726,13 @@ export default function Logs() { Status Nachricht Endpoint + Aktionen {logs.length === 0 && !loading && ( - + Keine Logs vorhanden. @@ -552,12 +759,52 @@ export default function Logs() { {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} + + +
+ )} +
); }