refactor(admin): split dsr/new, compliance-hub, iace/monitoring, cookie-banner pages

Extract components and hooks from 4 oversized pages (518–508 LOC each) to bring
each page.tsx under 300 LOC (hard cap 500). Zero behavior changes.

- dsr/new: TypeSelector, SourceSelector → _components/; useNewDSRForm → _hooks/
- compliance-hub: QuickActions, StatsRow, DomainChart, MappingsAndFindings,
  RegulationsTable → _components/; useComplianceHub → _hooks/
- iace/[projectId]/monitoring: Badges, EventForm, ResolveModal, TimelineEvent →
  _components/; useMonitoring → _hooks/
- cookie-banner: BannerPreview, CategoryCard → _components/; useCookieBanner → _hooks/

Result: page.tsx LOC: dsr/new=259, compliance-hub=95, monitoring=157, cookie-banner=212

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Sharang Parnerkar
2026-04-16 13:22:01 +02:00
parent 519ffdc8dc
commit e04816cfe5
20 changed files with 1514 additions and 1378 deletions

View File

@@ -0,0 +1,71 @@
'use client'
const EVENT_TYPE_CONFIG: Record<string, { label: string; color: string; bgColor: string; icon: string }> = {
incident: {
label: 'Vorfall',
color: 'text-red-700',
bgColor: 'bg-red-100',
icon: '🚨',
},
update: {
label: 'Update',
color: 'text-blue-700',
bgColor: 'bg-blue-100',
icon: '🔄',
},
drift_alert: {
label: 'Drift-Warnung',
color: 'text-orange-700',
bgColor: 'bg-orange-100',
icon: '📉',
},
regulation_change: {
label: 'Regulierungsaenderung',
color: 'text-purple-700',
bgColor: 'bg-purple-100',
icon: '📜',
},
}
const SEVERITY_CONFIG: Record<string, { label: string; color: string }> = {
low: { label: 'Niedrig', color: 'bg-green-100 text-green-700' },
medium: { label: 'Mittel', color: 'bg-yellow-100 text-yellow-700' },
high: { label: 'Hoch', color: 'bg-orange-100 text-orange-700' },
critical: { label: 'Kritisch', color: 'bg-red-100 text-red-700' },
}
const STATUS_CONFIG: Record<string, { label: string; color: string }> = {
open: { label: 'Offen', color: 'bg-red-100 text-red-700' },
investigating: { label: 'In Untersuchung', color: 'bg-yellow-100 text-yellow-700' },
resolved: { label: 'Geloest', color: 'bg-green-100 text-green-700' },
closed: { label: 'Geschlossen', color: 'bg-gray-100 text-gray-700' },
}
export { EVENT_TYPE_CONFIG }
export function EventTypeBadge({ type }: { type: string }) {
const config = EVENT_TYPE_CONFIG[type] || EVENT_TYPE_CONFIG.incident
return (
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${config.bgColor} ${config.color}`}>
{config.icon} {config.label}
</span>
)
}
export function SeverityBadge({ severity }: { severity: string }) {
const config = SEVERITY_CONFIG[severity] || SEVERITY_CONFIG.low
return (
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${config.color}`}>
{config.label}
</span>
)
}
export function StatusBadge({ status }: { status: string }) {
const config = STATUS_CONFIG[status] || STATUS_CONFIG.open
return (
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${config.color}`}>
{config.label}
</span>
)
}

View File

@@ -0,0 +1,93 @@
'use client'
import { useState } from 'react'
import { EventFormData } from '../_hooks/useMonitoring'
export function EventForm({
onSubmit,
onCancel,
}: {
onSubmit: (data: EventFormData) => void
onCancel: () => void
}) {
const [formData, setFormData] = useState<EventFormData>({
event_type: 'incident',
title: '',
description: '',
severity: 'medium',
})
return (
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">Neues Monitoring-Ereignis</h3>
<div className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Titel *</label>
<input
type="text"
value={formData.title}
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
placeholder="z.B. KI-Modell Drift erkannt"
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent dark:bg-gray-700 dark:border-gray-600 dark:text-white"
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Typ</label>
<select
value={formData.event_type}
onChange={(e) => setFormData({ ...formData, event_type: e.target.value })}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent dark:bg-gray-700 dark:border-gray-600 dark:text-white"
>
<option value="incident">Vorfall</option>
<option value="update">Update</option>
<option value="drift_alert">Drift-Warnung</option>
<option value="regulation_change">Regulierungsaenderung</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Schwere</label>
<select
value={formData.severity}
onChange={(e) => setFormData({ ...formData, severity: e.target.value })}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent dark:bg-gray-700 dark:border-gray-600 dark:text-white"
>
<option value="low">Niedrig</option>
<option value="medium">Mittel</option>
<option value="high">Hoch</option>
<option value="critical">Kritisch</option>
</select>
</div>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Beschreibung</label>
<textarea
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
rows={3}
placeholder="Beschreiben Sie das Ereignis..."
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent dark:bg-gray-700 dark:border-gray-600 dark:text-white"
/>
</div>
</div>
<div className="mt-4 flex items-center gap-3">
<button
onClick={() => onSubmit(formData)}
disabled={!formData.title}
className={`px-6 py-2 rounded-lg font-medium transition-colors ${
formData.title
? 'bg-purple-600 text-white hover:bg-purple-700'
: 'bg-gray-200 text-gray-400 cursor-not-allowed'
}`}
>
Ereignis erfassen
</button>
<button onClick={onCancel} className="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded-lg transition-colors">
Abbrechen
</button>
</div>
</div>
)
}

View File

@@ -0,0 +1,56 @@
'use client'
import { useState } from 'react'
import { MonitoringEvent } from '../_hooks/useMonitoring'
export function ResolveModal({
event,
onSubmit,
onClose,
}: {
event: MonitoringEvent
onSubmit: (id: string, notes: string) => void
onClose: () => void
}) {
const [notes, setNotes] = useState('')
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white dark:bg-gray-800 rounded-xl w-full max-w-lg p-6">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Ereignis loesen: {event.title}
</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Loesung / Massnahmen
</label>
<textarea
value={notes}
onChange={(e) => setNotes(e.target.value)}
rows={4}
placeholder="Beschreiben Sie die durchgefuehrten Massnahmen..."
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent dark:bg-gray-700 dark:border-gray-600 dark:text-white"
/>
</div>
</div>
<div className="mt-6 flex items-center gap-3">
<button
onClick={() => onSubmit(event.id, notes)}
disabled={!notes}
className={`px-6 py-2 rounded-lg font-medium transition-colors ${
notes
? 'bg-green-600 text-white hover:bg-green-700'
: 'bg-gray-200 text-gray-400 cursor-not-allowed'
}`}
>
Als geloest markieren
</button>
<button onClick={onClose} className="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded-lg transition-colors">
Abbrechen
</button>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,77 @@
'use client'
import { MonitoringEvent } from '../_hooks/useMonitoring'
import { EventTypeBadge, SeverityBadge, StatusBadge, EVENT_TYPE_CONFIG } from './Badges'
export function TimelineEvent({
event,
onResolve,
}: {
event: MonitoringEvent
onResolve: (event: MonitoringEvent) => void
}) {
const typeConfig = EVENT_TYPE_CONFIG[event.event_type] || EVENT_TYPE_CONFIG.incident
const lineColor = event.status === 'resolved' || event.status === 'closed' ? 'bg-green-300' : 'bg-gray-300'
return (
<div className="relative flex gap-4 pb-8 last:pb-0">
{/* Timeline line */}
<div className="flex flex-col items-center">
<div className={`w-10 h-10 rounded-full flex items-center justify-center text-lg ${typeConfig.bgColor} flex-shrink-0`}>
{typeConfig.icon}
</div>
<div className={`w-0.5 flex-1 ${lineColor} mt-2`} />
</div>
{/* Content */}
<div className="flex-1 bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-4 -mt-1">
<div className="flex items-start justify-between mb-2">
<div>
<h4 className="text-sm font-semibold text-gray-900 dark:text-white">{event.title}</h4>
<div className="flex items-center gap-2 mt-1">
<EventTypeBadge type={event.event_type} />
<SeverityBadge severity={event.severity} />
<StatusBadge status={event.status} />
</div>
</div>
<span className="text-xs text-gray-400 flex-shrink-0">
{new Date(event.created_at).toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
})}
</span>
</div>
{event.description && (
<p className="text-sm text-gray-600 dark:text-gray-400 mt-2">{event.description}</p>
)}
{event.resolution_notes && (
<div className="mt-3 p-3 bg-green-50 dark:bg-green-900/20 rounded-lg border border-green-200 dark:border-green-800">
<div className="text-xs font-medium text-green-700 dark:text-green-400 mb-1">Loesung:</div>
<p className="text-sm text-green-800 dark:text-green-300">{event.resolution_notes}</p>
{event.resolved_at && (
<div className="text-xs text-green-600 mt-1">
Geloest am {new Date(event.resolved_at).toLocaleDateString('de-DE')}
</div>
)}
</div>
)}
{(event.status === 'open' || event.status === 'investigating') && (
<div className="mt-3">
<button
onClick={() => onResolve(event)}
className="text-sm px-3 py-1.5 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors"
>
Loesen
</button>
</div>
)}
</div>
</div>
)
}

View File

@@ -0,0 +1,99 @@
'use client'
import { useState, useEffect } from 'react'
export interface MonitoringEvent {
id: string
event_type: 'incident' | 'update' | 'drift_alert' | 'regulation_change'
title: string
description: string
severity: 'low' | 'medium' | 'high' | 'critical'
status: 'open' | 'investigating' | 'resolved' | 'closed'
created_at: string
resolved_at: string | null
resolved_by: string | null
resolution_notes: string | null
}
export interface EventFormData {
event_type: string
title: string
description: string
severity: string
}
export function useMonitoring(projectId: string) {
const [events, setEvents] = useState<MonitoringEvent[]>([])
const [loading, setLoading] = useState(true)
const [showForm, setShowForm] = useState(false)
const [resolvingEvent, setResolvingEvent] = useState<MonitoringEvent | null>(null)
const [filterType, setFilterType] = useState('')
const [filterStatus, setFilterStatus] = useState('')
useEffect(() => {
fetchEvents()
}, [projectId])
async function fetchEvents() {
try {
const res = await fetch(`/api/sdk/v1/iace/projects/${projectId}/monitoring`)
if (res.ok) {
const json = await res.json()
setEvents(json.events || json || [])
}
} catch (err) {
console.error('Failed to fetch monitoring events:', err)
} finally {
setLoading(false)
}
}
async function handleSubmit(data: EventFormData) {
try {
const res = await fetch(`/api/sdk/v1/iace/projects/${projectId}/monitoring`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
})
if (res.ok) {
setShowForm(false)
await fetchEvents()
}
} catch (err) {
console.error('Failed to add event:', err)
}
}
async function handleResolve(id: string, notes: string) {
try {
const res = await fetch(`/api/sdk/v1/iace/projects/${projectId}/monitoring/${id}/resolve`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ resolution_notes: notes }),
})
if (res.ok) {
setResolvingEvent(null)
await fetchEvents()
}
} catch (err) {
console.error('Failed to resolve event:', err)
}
}
const filteredEvents = events.filter((e) => {
const matchType = !filterType || e.event_type === filterType
const matchStatus = !filterStatus || e.status === filterStatus
return matchType && matchStatus
})
const openCount = events.filter((e) => e.status === 'open' || e.status === 'investigating').length
const resolvedCount = events.filter((e) => e.status === 'resolved' || e.status === 'closed').length
return {
events, loading, showForm, resolvingEvent,
filterType, filterStatus, filteredEvents,
openCount, resolvedCount,
setShowForm, setResolvingEvent, setFilterType, setFilterStatus,
handleSubmit, handleResolve,
}
}

View File

@@ -1,378 +1,23 @@
'use client'
import React, { useState, useEffect } from 'react'
import React from 'react'
import { useParams } from 'next/navigation'
interface MonitoringEvent {
id: string
event_type: 'incident' | 'update' | 'drift_alert' | 'regulation_change'
title: string
description: string
severity: 'low' | 'medium' | 'high' | 'critical'
status: 'open' | 'investigating' | 'resolved' | 'closed'
created_at: string
resolved_at: string | null
resolved_by: string | null
resolution_notes: string | null
}
const EVENT_TYPE_CONFIG: Record<string, { label: string; color: string; bgColor: string; icon: string }> = {
incident: {
label: 'Vorfall',
color: 'text-red-700',
bgColor: 'bg-red-100',
icon: '🚨',
},
update: {
label: 'Update',
color: 'text-blue-700',
bgColor: 'bg-blue-100',
icon: '🔄',
},
drift_alert: {
label: 'Drift-Warnung',
color: 'text-orange-700',
bgColor: 'bg-orange-100',
icon: '📉',
},
regulation_change: {
label: 'Regulierungsaenderung',
color: 'text-purple-700',
bgColor: 'bg-purple-100',
icon: '📜',
},
}
const SEVERITY_CONFIG: Record<string, { label: string; color: string }> = {
low: { label: 'Niedrig', color: 'bg-green-100 text-green-700' },
medium: { label: 'Mittel', color: 'bg-yellow-100 text-yellow-700' },
high: { label: 'Hoch', color: 'bg-orange-100 text-orange-700' },
critical: { label: 'Kritisch', color: 'bg-red-100 text-red-700' },
}
const STATUS_CONFIG: Record<string, { label: string; color: string }> = {
open: { label: 'Offen', color: 'bg-red-100 text-red-700' },
investigating: { label: 'In Untersuchung', color: 'bg-yellow-100 text-yellow-700' },
resolved: { label: 'Geloest', color: 'bg-green-100 text-green-700' },
closed: { label: 'Geschlossen', color: 'bg-gray-100 text-gray-700' },
}
function EventTypeBadge({ type }: { type: string }) {
const config = EVENT_TYPE_CONFIG[type] || EVENT_TYPE_CONFIG.incident
return (
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${config.bgColor} ${config.color}`}>
{config.icon} {config.label}
</span>
)
}
function SeverityBadge({ severity }: { severity: string }) {
const config = SEVERITY_CONFIG[severity] || SEVERITY_CONFIG.low
return (
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${config.color}`}>
{config.label}
</span>
)
}
function StatusBadge({ status }: { status: string }) {
const config = STATUS_CONFIG[status] || STATUS_CONFIG.open
return (
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${config.color}`}>
{config.label}
</span>
)
}
interface EventFormData {
event_type: string
title: string
description: string
severity: string
}
function EventForm({
onSubmit,
onCancel,
}: {
onSubmit: (data: EventFormData) => void
onCancel: () => void
}) {
const [formData, setFormData] = useState<EventFormData>({
event_type: 'incident',
title: '',
description: '',
severity: 'medium',
})
return (
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">Neues Monitoring-Ereignis</h3>
<div className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Titel *</label>
<input
type="text"
value={formData.title}
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
placeholder="z.B. KI-Modell Drift erkannt"
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent dark:bg-gray-700 dark:border-gray-600 dark:text-white"
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Typ</label>
<select
value={formData.event_type}
onChange={(e) => setFormData({ ...formData, event_type: e.target.value })}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent dark:bg-gray-700 dark:border-gray-600 dark:text-white"
>
<option value="incident">Vorfall</option>
<option value="update">Update</option>
<option value="drift_alert">Drift-Warnung</option>
<option value="regulation_change">Regulierungsaenderung</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Schwere</label>
<select
value={formData.severity}
onChange={(e) => setFormData({ ...formData, severity: e.target.value })}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent dark:bg-gray-700 dark:border-gray-600 dark:text-white"
>
<option value="low">Niedrig</option>
<option value="medium">Mittel</option>
<option value="high">Hoch</option>
<option value="critical">Kritisch</option>
</select>
</div>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Beschreibung</label>
<textarea
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
rows={3}
placeholder="Beschreiben Sie das Ereignis..."
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent dark:bg-gray-700 dark:border-gray-600 dark:text-white"
/>
</div>
</div>
<div className="mt-4 flex items-center gap-3">
<button
onClick={() => onSubmit(formData)}
disabled={!formData.title}
className={`px-6 py-2 rounded-lg font-medium transition-colors ${
formData.title
? 'bg-purple-600 text-white hover:bg-purple-700'
: 'bg-gray-200 text-gray-400 cursor-not-allowed'
}`}
>
Ereignis erfassen
</button>
<button onClick={onCancel} className="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded-lg transition-colors">
Abbrechen
</button>
</div>
</div>
)
}
function ResolveModal({
event,
onSubmit,
onClose,
}: {
event: MonitoringEvent
onSubmit: (id: string, notes: string) => void
onClose: () => void
}) {
const [notes, setNotes] = useState('')
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white dark:bg-gray-800 rounded-xl w-full max-w-lg p-6">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Ereignis loesen: {event.title}
</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Loesung / Massnahmen
</label>
<textarea
value={notes}
onChange={(e) => setNotes(e.target.value)}
rows={4}
placeholder="Beschreiben Sie die durchgefuehrten Massnahmen..."
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent dark:bg-gray-700 dark:border-gray-600 dark:text-white"
/>
</div>
</div>
<div className="mt-6 flex items-center gap-3">
<button
onClick={() => onSubmit(event.id, notes)}
disabled={!notes}
className={`px-6 py-2 rounded-lg font-medium transition-colors ${
notes
? 'bg-green-600 text-white hover:bg-green-700'
: 'bg-gray-200 text-gray-400 cursor-not-allowed'
}`}
>
Als geloest markieren
</button>
<button onClick={onClose} className="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded-lg transition-colors">
Abbrechen
</button>
</div>
</div>
</div>
)
}
function TimelineEvent({
event,
onResolve,
}: {
event: MonitoringEvent
onResolve: (event: MonitoringEvent) => void
}) {
const typeConfig = EVENT_TYPE_CONFIG[event.event_type] || EVENT_TYPE_CONFIG.incident
const lineColor = event.status === 'resolved' || event.status === 'closed' ? 'bg-green-300' : 'bg-gray-300'
return (
<div className="relative flex gap-4 pb-8 last:pb-0">
{/* Timeline line */}
<div className="flex flex-col items-center">
<div className={`w-10 h-10 rounded-full flex items-center justify-center text-lg ${typeConfig.bgColor} flex-shrink-0`}>
{typeConfig.icon}
</div>
<div className={`w-0.5 flex-1 ${lineColor} mt-2`} />
</div>
{/* Content */}
<div className="flex-1 bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-4 -mt-1">
<div className="flex items-start justify-between mb-2">
<div>
<h4 className="text-sm font-semibold text-gray-900 dark:text-white">{event.title}</h4>
<div className="flex items-center gap-2 mt-1">
<EventTypeBadge type={event.event_type} />
<SeverityBadge severity={event.severity} />
<StatusBadge status={event.status} />
</div>
</div>
<span className="text-xs text-gray-400 flex-shrink-0">
{new Date(event.created_at).toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
})}
</span>
</div>
{event.description && (
<p className="text-sm text-gray-600 dark:text-gray-400 mt-2">{event.description}</p>
)}
{event.resolution_notes && (
<div className="mt-3 p-3 bg-green-50 dark:bg-green-900/20 rounded-lg border border-green-200 dark:border-green-800">
<div className="text-xs font-medium text-green-700 dark:text-green-400 mb-1">Loesung:</div>
<p className="text-sm text-green-800 dark:text-green-300">{event.resolution_notes}</p>
{event.resolved_at && (
<div className="text-xs text-green-600 mt-1">
Geloest am {new Date(event.resolved_at).toLocaleDateString('de-DE')}
</div>
)}
</div>
)}
{(event.status === 'open' || event.status === 'investigating') && (
<div className="mt-3">
<button
onClick={() => onResolve(event)}
className="text-sm px-3 py-1.5 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors"
>
Loesen
</button>
</div>
)}
</div>
</div>
)
}
import { useMonitoring } from './_hooks/useMonitoring'
import { EventForm } from './_components/EventForm'
import { ResolveModal } from './_components/ResolveModal'
import { TimelineEvent } from './_components/TimelineEvent'
export default function MonitoringPage() {
const params = useParams()
const projectId = params.projectId as string
const [events, setEvents] = useState<MonitoringEvent[]>([])
const [loading, setLoading] = useState(true)
const [showForm, setShowForm] = useState(false)
const [resolvingEvent, setResolvingEvent] = useState<MonitoringEvent | null>(null)
const [filterType, setFilterType] = useState('')
const [filterStatus, setFilterStatus] = useState('')
useEffect(() => {
fetchEvents()
}, [projectId])
async function fetchEvents() {
try {
const res = await fetch(`/api/sdk/v1/iace/projects/${projectId}/monitoring`)
if (res.ok) {
const json = await res.json()
setEvents(json.events || json || [])
}
} catch (err) {
console.error('Failed to fetch monitoring events:', err)
} finally {
setLoading(false)
}
}
async function handleSubmit(data: EventFormData) {
try {
const res = await fetch(`/api/sdk/v1/iace/projects/${projectId}/monitoring`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
})
if (res.ok) {
setShowForm(false)
await fetchEvents()
}
} catch (err) {
console.error('Failed to add event:', err)
}
}
async function handleResolve(id: string, notes: string) {
try {
const res = await fetch(`/api/sdk/v1/iace/projects/${projectId}/monitoring/${id}/resolve`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ resolution_notes: notes }),
})
if (res.ok) {
setResolvingEvent(null)
await fetchEvents()
}
} catch (err) {
console.error('Failed to resolve event:', err)
}
}
const filteredEvents = events.filter((e) => {
const matchType = !filterType || e.event_type === filterType
const matchStatus = !filterStatus || e.status === filterStatus
return matchType && matchStatus
})
const openCount = events.filter((e) => e.status === 'open' || e.status === 'investigating').length
const resolvedCount = events.filter((e) => e.status === 'resolved' || e.status === 'closed').length
const {
events, loading, showForm, resolvingEvent,
filterType, filterStatus, filteredEvents,
openCount, resolvedCount,
setShowForm, setResolvingEvent, setFilterType, setFilterStatus,
handleSubmit, handleResolve,
} = useMonitoring(projectId)
if (loading) {
return (