added last fixes for dev branch prepartion

This commit is contained in:
2025-11-27 23:50:59 +01:00
parent 145dfd3d7c
commit 688b277b5d
13 changed files with 1051 additions and 158 deletions

View File

@@ -1,11 +1,78 @@
import { useState, useEffect } from 'react'
import { useState, useEffect, useRef } from 'react'
import { useAuth } from '../contexts/AuthContext'
// Custom Dropdown Component
const CustomDropdown = ({ label, value, onChange, options, placeholder = "Auswählen" }) => {
const [isOpen, setIsOpen] = useState(false)
const dropdownRef = useRef(null)
useEffect(() => {
const handleClickOutside = (event) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
setIsOpen(false)
}
}
document.addEventListener('mousedown', handleClickOutside)
return () => document.removeEventListener('mousedown', handleClickOutside)
}, [])
const selectedOption = options.find(opt => opt.value === value) || { label: placeholder }
return (
<div className="relative" ref={dropdownRef}>
<label className="block text-xs font-medium text-slate-400 uppercase tracking-wider mb-3">
{label}
</label>
<button
type="button"
onClick={() => setIsOpen(!isOpen)}
className="w-full px-4 py-3 bg-slate-700/50 hover:bg-slate-700/70 active:bg-slate-700 text-white rounded-lg border border-slate-600/50 focus:outline-none focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500/50 transition-all duration-200 text-sm flex items-center justify-between"
>
<span className={!value ? 'text-slate-400' : 'text-white'}>{selectedOption.label}</span>
<svg
className={`w-4 h-4 text-slate-400 transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 9l-7 7-7-7" />
</svg>
</button>
{isOpen && (
<div className="absolute z-50 w-full mt-2 bg-slate-800/95 backdrop-blur-sm border border-slate-600/50 rounded-lg shadow-2xl overflow-hidden">
<div className="max-h-60 overflow-y-auto custom-scrollbar">
{options.map((option) => (
<button
key={option.value}
type="button"
onClick={() => {
onChange(option.value)
setIsOpen(false)
}}
className={`w-full px-4 py-2.5 text-left text-sm transition-all duration-150 ${
value === option.value
? 'bg-blue-600/20 text-blue-300 border-l-2 border-blue-500 font-medium'
: 'text-slate-300 hover:bg-slate-700/50 hover:text-white'
}`}
>
{option.label}
</button>
))}
</div>
</div>
)}
</div>
)
}
const AuditLogs = () => {
const { authFetch } = useAuth()
const [logs, setLogs] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState('')
const [isRefreshing, setIsRefreshing] = useState(false)
const [lastUpdate, setLastUpdate] = useState(null)
const [filters, setFilters] = useState({
action: '',
resourceType: '',
@@ -23,6 +90,7 @@ const AuditLogs = () => {
try {
if (!silent) {
setLoading(true)
setIsRefreshing(true)
}
setError('')
@@ -41,9 +109,8 @@ const AuditLogs = () => {
}
const data = await response.json()
console.log('Audit-Logs Response:', data)
console.log('Anzahl Logs:', data.logs?.length || 0)
setLogs(data.logs || [])
setLastUpdate(new Date())
setPagination({
...pagination,
total: data.total || 0,
@@ -57,6 +124,7 @@ const AuditLogs = () => {
} finally {
if (!silent) {
setLoading(false)
setIsRefreshing(false)
}
}
}
@@ -72,7 +140,7 @@ const AuditLogs = () => {
}, 5000) // Aktualisiere alle 5 Sekunden
return () => clearInterval(interval)
}, [filters.action, filters.resourceType, filters.userId, pagination.offset])
}, [filters.action, filters.resourceType, filters.userId, pagination.offset, authFetch])
const handleFilterChange = (key, value) => {
setFilters({ ...filters, [key]: value })
@@ -188,6 +256,18 @@ const AuditLogs = () => {
}
}
const formatLastUpdate = () => {
if (!lastUpdate) return ''
const now = new Date()
const diff = Math.floor((now - lastUpdate) / 1000)
if (diff < 5) return 'Gerade eben'
if (diff < 60) return `Vor ${diff} Sekunden`
const minutes = Math.floor(diff / 60)
return `Vor ${minutes} Minute${minutes > 1 ? 'n' : ''}`
}
const hasActiveFilters = filters.action || filters.resourceType || filters.userId
return (
<div className="min-h-screen bg-gradient-to-r from-slate-700 to-slate-900 p-6">
<div className="max-w-7xl mx-auto">
@@ -196,55 +276,80 @@ const AuditLogs = () => {
<h1 className="text-3xl font-bold text-white mb-2">Audit Log</h1>
<p className="text-slate-300">Übersicht aller Systemaktivitäten und Änderungen</p>
</div>
<div className="flex items-center gap-2 text-sm text-slate-400">
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
<span>Live-Aktualisierung aktiv</span>
<div className="flex items-center gap-3">
{isRefreshing && (
<div className="flex items-center gap-2 text-blue-400">
<svg className="animate-spin h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<span className="text-sm">Aktualisiere...</span>
</div>
)}
<div className="flex items-center gap-2 px-3 py-1.5 bg-green-500/20 border border-green-500/50 rounded-full">
<div className="w-2 h-2 bg-green-400 rounded-full animate-pulse"></div>
<span className="text-sm text-green-300 font-medium">Live</span>
</div>
{lastUpdate && (
<div className="text-sm text-slate-400">
{formatLastUpdate()}
</div>
)}
</div>
</div>
{/* Filter */}
<div className="bg-slate-800/50 border border-slate-600/50 rounded-lg p-4 mb-6">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium text-slate-200 mb-2">
Aktion
</label>
<select
value={filters.action}
onChange={(e) => handleFilterChange('action', e.target.value)}
className="w-full px-4 py-2 bg-slate-700/50 border border-slate-600 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
<div className="bg-slate-800/80 backdrop-blur-sm rounded-lg shadow-xl border border-slate-600/50 p-6 mb-6">
<div className="flex items-center justify-between mb-6">
<h2 className="text-lg font-semibold text-white">Filter</h2>
{hasActiveFilters && (
<button
onClick={() => {
setFilters({ action: '', resourceType: '', userId: '' })
setPagination({ ...pagination, offset: 0 })
}}
className="flex items-center gap-2 px-3 py-1.5 bg-slate-700/50 hover:bg-slate-700 text-slate-300 rounded-lg transition-colors duration-200 text-sm"
>
<option value="">Alle Aktionen</option>
<option value="CREATE">Erstellt</option>
<option value="UPDATE">Aktualisiert</option>
<option value="DELETE">Gelöscht</option>
<option value="UPLOAD">Hochgeladen</option>
<option value="SIGN">Signiert</option>
<option value="ENABLE">Aktiviert</option>
<option value="DISABLE">Deaktiviert</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-slate-200 mb-2">
Ressourcentyp
</label>
<select
value={filters.resourceType}
onChange={(e) => handleFilterChange('resourceType', e.target.value)}
className="w-full px-4 py-2 bg-slate-700/50 border border-slate-600 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="">Alle Typen</option>
<option value="user">Benutzer</option>
<option value="space">Space</option>
<option value="fqdn">FQDN</option>
<option value="csr">CSR</option>
<option value="provider">Provider</option>
<option value="certificate">Zertifikat</option>
<option value="permission_group">Berechtigungsgruppen</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-slate-200 mb-2">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />
</svg>
Zurücksetzen
</button>
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<CustomDropdown
label="Aktion"
value={filters.action}
onChange={(value) => handleFilterChange('action', value)}
options={[
{ value: '', label: 'Alle Aktionen' },
{ value: 'CREATE', label: 'Erstellt' },
{ value: 'UPDATE', label: 'Aktualisiert' },
{ value: 'DELETE', label: 'Gelöscht' },
{ value: 'UPLOAD', label: 'Hochgeladen' },
{ value: 'SIGN', label: 'Signiert' },
{ value: 'ENABLE', label: 'Aktiviert' },
{ value: 'DISABLE', label: 'Deaktiviert' }
]}
/>
<CustomDropdown
label="Ressourcentyp"
value={filters.resourceType}
onChange={(value) => handleFilterChange('resourceType', value)}
options={[
{ value: '', label: 'Alle Typen' },
{ value: 'user', label: 'Benutzer' },
{ value: 'space', label: 'Space' },
{ value: 'fqdn', label: 'FQDN' },
{ value: 'csr', label: 'CSR' },
{ value: 'provider', label: 'Provider' },
{ value: 'certificate', label: 'Zertifikat' },
{ value: 'permission_group', label: 'Berechtigungsgruppen' }
]}
/>
<div className="relative">
<label className="block text-xs font-medium text-slate-400 uppercase tracking-wider mb-3">
Benutzer-ID
</label>
<input
@@ -252,7 +357,7 @@ const AuditLogs = () => {
value={filters.userId}
onChange={(e) => handleFilterChange('userId', e.target.value)}
placeholder="Benutzer-ID filtern"
className="w-full px-4 py-2 bg-slate-700/50 border border-slate-600 rounded-lg text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
className="w-full px-4 py-3 bg-slate-700/50 hover:bg-slate-700/70 text-white rounded-lg border border-slate-600/50 focus:outline-none focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500/50 transition-all duration-200 text-sm placeholder-slate-400"
/>
</div>
</div>

View File

@@ -1,38 +1,166 @@
import { useState, useEffect } from 'react'
import { useState, useEffect, useMemo, useRef } from 'react'
import { useAuth } from '../contexts/AuthContext'
// Custom Dropdown Component
const CustomDropdown = ({ label, value, onChange, options, placeholder = "Auswählen" }) => {
const [isOpen, setIsOpen] = useState(false)
const dropdownRef = useRef(null)
useEffect(() => {
const handleClickOutside = (event) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
setIsOpen(false)
}
}
document.addEventListener('mousedown', handleClickOutside)
return () => document.removeEventListener('mousedown', handleClickOutside)
}, [])
const selectedOption = options.find(opt => opt.value === value) || { label: placeholder }
return (
<div className="relative" ref={dropdownRef}>
<label className="block text-xs font-medium text-slate-400 uppercase tracking-wider mb-3">
{label}
</label>
<button
type="button"
onClick={() => setIsOpen(!isOpen)}
className="w-full px-4 py-3 bg-slate-700/50 hover:bg-slate-700/70 active:bg-slate-700 text-white rounded-lg border border-slate-600/50 focus:outline-none focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500/50 transition-all duration-200 text-sm flex items-center justify-between"
>
<span className={value === 'all' ? 'text-slate-400' : 'text-white'}>{selectedOption.label}</span>
<svg
className={`w-4 h-4 text-slate-400 transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 9l-7 7-7-7" />
</svg>
</button>
{isOpen && (
<div className="absolute z-50 w-full mt-2 bg-slate-800/95 backdrop-blur-sm border border-slate-600/50 rounded-lg shadow-2xl overflow-hidden">
<div className="max-h-60 overflow-y-auto custom-scrollbar">
{options.map((option) => (
<button
key={option.value}
type="button"
onClick={() => {
onChange(option.value)
setIsOpen(false)
}}
className={`w-full px-4 py-2.5 text-left text-sm transition-all duration-150 ${
value === option.value
? 'bg-blue-600/20 text-blue-300 border-l-2 border-blue-500 font-medium'
: 'text-slate-300 hover:bg-slate-700/50 hover:text-white'
}`}
>
{option.label}
</button>
))}
</div>
</div>
)}
</div>
)
}
const RenewalQueue = () => {
const { authFetch } = useAuth()
const [queue, setQueue] = useState([])
const [loading, setLoading] = useState(true)
const [statusFilter, setStatusFilter] = useState('all')
const [spaceFilter, setSpaceFilter] = useState('all')
const [isRefreshing, setIsRefreshing] = useState(false)
const [lastUpdate, setLastUpdate] = useState(null)
const fetchQueue = async () => {
const fetchQueue = async (silent = false) => {
try {
if (!silent) {
setIsRefreshing(true)
}
const response = await authFetch('/api/renewal-queue')
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const data = await response.json()
setQueue(data.queue || [])
setLastUpdate(new Date())
setLoading(false)
} catch (err) {
console.error('Error fetching renewal queue:', err)
setLoading(false)
} finally {
if (!silent) {
setIsRefreshing(false)
}
}
}
useEffect(() => {
fetchQueue()
const interval = setInterval(fetchQueue, 30000) // Refresh every 30 seconds
// Live-Refresh alle 5 Sekunden für Echtzeit-Ansicht
const interval = setInterval(() => fetchQueue(true), 5000)
return () => clearInterval(interval)
}, [authFetch])
// Extrahiere eindeutige Spaces für Filter
const uniqueSpaces = useMemo(() => {
const spaces = new Set()
queue.forEach(item => {
if (item.spaceName) {
spaces.add(item.spaceName)
}
})
return Array.from(spaces).sort()
}, [queue])
// Filtere und sortiere Queue-Einträge
const filteredAndSortedQueue = useMemo(() => {
let filtered = queue
// Filter nach Status
if (statusFilter !== 'all') {
filtered = filtered.filter(item => item.status === statusFilter)
}
// Filter nach Space
if (spaceFilter !== 'all') {
filtered = filtered.filter(item => item.spaceName === spaceFilter)
}
// Sortiere nach scheduled_at (nächste oben)
filtered.sort((a, b) => {
const dateA = new Date(a.scheduledAt)
const dateB = new Date(b.scheduledAt)
return dateA - dateB
})
return filtered
}, [queue, statusFilter, spaceFilter])
// Teile in "Ausstehend" und "Erledigt"
const pendingQueue = useMemo(() => {
return filteredAndSortedQueue.filter(item =>
item.status === 'pending' || item.status === 'processing'
)
}, [filteredAndSortedQueue])
const completedQueue = useMemo(() => {
return filteredAndSortedQueue.filter(item =>
item.status === 'completed' || item.status === 'failed'
)
}, [filteredAndSortedQueue])
const getStatusColor = (status) => {
switch (status) {
case 'pending':
return 'bg-yellow-500/20 text-yellow-400 border-yellow-500/50'
case 'processing':
return 'bg-blue-500/20 text-blue-400 border-blue-500/50'
case 'success':
return 'bg-green-500/20 text-green-400 border-green-500/50'
case 'completed':
return 'bg-green-500/20 text-green-400 border-green-500/50'
case 'failed':
@@ -86,13 +214,92 @@ const RenewalQueue = () => {
}
}
const renderQueueTable = (items, title) => {
if (items.length === 0) {
return null
}
return (
<div className="mb-8">
<h2 className="text-2xl font-bold text-white mb-4">{title}</h2>
<div className="bg-slate-800/80 backdrop-blur-sm rounded-lg shadow-xl border border-slate-600/50 overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-slate-700/50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-slate-300 uppercase tracking-wider">FQDN</th>
<th className="px-6 py-3 text-left text-xs font-medium text-slate-300 uppercase tracking-wider">Space</th>
<th className="px-6 py-3 text-left text-xs font-medium text-slate-300 uppercase tracking-wider">Geplant für</th>
<th className="px-6 py-3 text-left text-xs font-medium text-slate-300 uppercase tracking-wider">Status</th>
<th className="px-6 py-3 text-left text-xs font-medium text-slate-300 uppercase tracking-wider">Verarbeitet</th>
<th className="px-6 py-3 text-left text-xs font-medium text-slate-300 uppercase tracking-wider">Fehler</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-700/50">
{items.map((item) => (
<tr key={item.id} className="hover:bg-slate-700/30 transition-colors">
<td className="px-6 py-4 whitespace-nowrap text-sm text-white font-medium">{item.fqdn || '-'}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-slate-300">{item.spaceName || '-'}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-slate-300">{formatDate(item.scheduledAt)}</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`px-3 py-1 rounded-full text-xs font-semibold border ${getStatusColor(item.status)}`}>
{item.status}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-slate-300">{formatDate(item.processedAt)}</td>
<td className="px-6 py-4 text-sm text-red-400">{item.errorMessage || '-'}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
)
}
const formatLastUpdate = () => {
if (!lastUpdate) return ''
const now = new Date()
const diff = Math.floor((now - lastUpdate) / 1000)
if (diff < 5) return 'Gerade eben'
if (diff < 60) return `Vor ${diff} Sekunden`
const minutes = Math.floor(diff / 60)
return `Vor ${minutes} Minute${minutes > 1 ? 'n' : ''}`
}
return (
<div className="p-8 min-h-full bg-gradient-to-r from-slate-700 to-slate-900">
<div className="max-w-10xl mx-auto">
<h1 className="text-4xl font-bold text-white mb-4">Renewal Queue</h1>
<p className="text-lg text-slate-200 mb-8">
Übersicht über geplante und laufende Zertifikatserneuerungen
</p>
<div className="flex items-center justify-between mb-4">
<div>
<h1 className="text-4xl font-bold text-white mb-2">Renewal Queue</h1>
<p className="text-lg text-slate-200">
Übersicht über geplante und laufende Zertifikatserneuerungen
</p>
</div>
<div className="flex items-center gap-3">
{isRefreshing && (
<div className="flex items-center gap-2 text-blue-400">
<svg className="animate-spin h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<span className="text-sm">Aktualisiere...</span>
</div>
)}
<div className="flex items-center gap-2 px-3 py-1.5 bg-green-500/20 border border-green-500/50 rounded-full">
<div className="w-2 h-2 bg-green-400 rounded-full animate-pulse"></div>
<span className="text-sm text-green-300 font-medium">Live</span>
</div>
{lastUpdate && (
<div className="text-sm text-slate-400">
{formatLastUpdate()}
</div>
)}
</div>
</div>
{loading ? (
<div className="text-center py-12">
@@ -100,44 +307,68 @@ const RenewalQueue = () => {
<p className="text-slate-300 mt-4">Lade Queue...</p>
</div>
) : (
<div className="bg-slate-800/80 backdrop-blur-sm rounded-lg shadow-xl border border-slate-600/50 overflow-hidden">
{queue.length === 0 ? (
<div className="p-12 text-center">
<p className="text-slate-400 text-lg">Keine Einträge in der Renewal Queue</p>
<>
{/* Filter */}
<div className="bg-slate-800/80 backdrop-blur-sm rounded-lg shadow-xl border border-slate-600/50 p-6 mb-6">
<div className="flex items-center justify-between mb-6">
<h2 className="text-lg font-semibold text-white">Filter</h2>
{(statusFilter !== 'all' || spaceFilter !== 'all') && (
<button
onClick={() => {
setStatusFilter('all')
setSpaceFilter('all')
}}
className="flex items-center gap-2 px-3 py-1.5 bg-slate-700/50 hover:bg-slate-700 text-slate-300 rounded-lg transition-colors duration-200 text-sm"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />
</svg>
Zurücksetzen
</button>
)}
</div>
) : (
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-slate-700/50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-slate-300 uppercase tracking-wider">FQDN</th>
<th className="px-6 py-3 text-left text-xs font-medium text-slate-300 uppercase tracking-wider">Space</th>
<th className="px-6 py-3 text-left text-xs font-medium text-slate-300 uppercase tracking-wider">Geplant für</th>
<th className="px-6 py-3 text-left text-xs font-medium text-slate-300 uppercase tracking-wider">Status</th>
<th className="px-6 py-3 text-left text-xs font-medium text-slate-300 uppercase tracking-wider">Verarbeitet</th>
<th className="px-6 py-3 text-left text-xs font-medium text-slate-300 uppercase tracking-wider">Fehler</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-700/50">
{queue.map((item) => (
<tr key={item.id} className="hover:bg-slate-700/30 transition-colors">
<td className="px-6 py-4 whitespace-nowrap text-sm text-white font-medium">{item.fqdn || '-'}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-slate-300">{item.spaceName || '-'}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-slate-300">{formatDate(item.scheduledAt)}</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`px-3 py-1 rounded-full text-xs font-semibold border ${getStatusColor(item.status)}`}>
{item.status}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-slate-300">{formatDate(item.processedAt)}</td>
<td className="px-6 py-4 text-sm text-red-400">{item.errorMessage || '-'}</td>
</tr>
))}
</tbody>
</table>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<CustomDropdown
label="Status"
value={statusFilter}
onChange={setStatusFilter}
options={[
{ value: 'all', label: 'Alle Status' },
{ value: 'pending', label: 'Pending' },
{ value: 'processing', label: 'Processing' },
{ value: 'completed', label: 'Completed' },
{ value: 'failed', label: 'Failed' }
]}
/>
<CustomDropdown
label="Space"
value={spaceFilter}
onChange={setSpaceFilter}
options={[
{ value: 'all', label: 'Alle Spaces' },
...uniqueSpaces.map(space => ({ value: space, label: space }))
]}
/>
</div>
</div>
{/* Ausstehende Tasks */}
{renderQueueTable(pendingQueue, `Ausstehend (${pendingQueue.length})`)}
{/* Erledigte Tasks */}
{renderQueueTable(completedQueue, `Erledigt (${completedQueue.length})`)}
{/* Keine Einträge */}
{filteredAndSortedQueue.length === 0 && (
<div className="bg-slate-800/80 backdrop-blur-sm rounded-lg shadow-xl border border-slate-600/50 p-12 text-center">
<p className="text-slate-400 text-lg">
{queue.length === 0
? 'Keine Einträge in der Renewal Queue'
: 'Keine Einträge entsprechen den gewählten Filtern'}
</p>
</div>
)}
</div>
</>
)}
</div>
</div>