feat(ui): Add night-mode dashboard page
Add the missing page.tsx for the night-mode dashboard UI that was not included in the previous commit. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
561
admin-v2/app/(admin)/infrastructure/night-mode/page.tsx
Normal file
561
admin-v2/app/(admin)/infrastructure/night-mode/page.tsx
Normal file
@@ -0,0 +1,561 @@
|
||||
'use client'
|
||||
|
||||
/**
|
||||
* Night Mode - Dashboard-gesteuerte Nachtabschaltung
|
||||
*
|
||||
* Ermoeglicht das automatische Stoppen und Starten von Docker-Services
|
||||
* nach Zeitplan. Manuelles Starten/Stoppen ebenfalls moeglich.
|
||||
*/
|
||||
|
||||
import { useEffect, useState, useCallback } from 'react'
|
||||
|
||||
interface NightModeConfig {
|
||||
enabled: boolean
|
||||
shutdown_time: string
|
||||
startup_time: string
|
||||
last_action: string | null
|
||||
last_action_time: string | null
|
||||
excluded_services: string[]
|
||||
}
|
||||
|
||||
interface NightModeStatus {
|
||||
config: NightModeConfig
|
||||
current_time: string
|
||||
next_action: string | null
|
||||
next_action_time: string | null
|
||||
time_until_next_action: string | null
|
||||
services_status: Record<string, string>
|
||||
}
|
||||
|
||||
interface ServicesInfo {
|
||||
all_services: string[]
|
||||
excluded_services: string[]
|
||||
status: Record<string, string>
|
||||
}
|
||||
|
||||
export default function NightModePage() {
|
||||
const [status, setStatus] = useState<NightModeStatus | null>(null)
|
||||
const [services, setServices] = useState<ServicesInfo | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [actionLoading, setActionLoading] = useState<string | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [successMessage, setSuccessMessage] = useState<string | null>(null)
|
||||
|
||||
// Lokale Konfiguration fuer Bearbeitung
|
||||
const [editMode, setEditMode] = useState(false)
|
||||
const [editConfig, setEditConfig] = useState<NightModeConfig | null>(null)
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
setError(null)
|
||||
try {
|
||||
const [statusRes, servicesRes] = await Promise.all([
|
||||
fetch('/api/admin/night-mode'),
|
||||
fetch('/api/admin/night-mode/services'),
|
||||
])
|
||||
|
||||
if (statusRes.ok) {
|
||||
const data = await statusRes.json()
|
||||
setStatus(data)
|
||||
if (!editMode) {
|
||||
setEditConfig(data.config)
|
||||
}
|
||||
} else {
|
||||
const errData = await statusRes.json()
|
||||
setError(errData.error || 'Fehler beim Laden des Status')
|
||||
}
|
||||
|
||||
if (servicesRes.ok) {
|
||||
setServices(await servicesRes.json())
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Verbindung zum Night-Scheduler fehlgeschlagen')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [editMode])
|
||||
|
||||
useEffect(() => {
|
||||
fetchData()
|
||||
}, [fetchData])
|
||||
|
||||
// Auto-Refresh alle 30 Sekunden
|
||||
useEffect(() => {
|
||||
const interval = setInterval(fetchData, 30000)
|
||||
return () => clearInterval(interval)
|
||||
}, [fetchData])
|
||||
|
||||
const saveConfig = async () => {
|
||||
if (!editConfig) return
|
||||
|
||||
setActionLoading('save')
|
||||
setError(null)
|
||||
setSuccessMessage(null)
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/admin/night-mode', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(editConfig),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errData = await response.json()
|
||||
throw new Error(errData.error || 'Fehler beim Speichern')
|
||||
}
|
||||
|
||||
setEditMode(false)
|
||||
setSuccessMessage('Konfiguration gespeichert')
|
||||
setTimeout(() => setSuccessMessage(null), 3000)
|
||||
fetchData()
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Speichern fehlgeschlagen')
|
||||
} finally {
|
||||
setActionLoading(null)
|
||||
}
|
||||
}
|
||||
|
||||
const executeAction = async (action: 'start' | 'stop') => {
|
||||
setActionLoading(action)
|
||||
setError(null)
|
||||
setSuccessMessage(null)
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/admin/night-mode/execute', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ action }),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errData = await response.json()
|
||||
throw new Error(errData.error || `Fehler bei ${action}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
setSuccessMessage(data.message || `${action === 'start' ? 'Gestartet' : 'Gestoppt'}`)
|
||||
setTimeout(() => setSuccessMessage(null), 5000)
|
||||
fetchData()
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : `${action} fehlgeschlagen`)
|
||||
} finally {
|
||||
setActionLoading(null)
|
||||
}
|
||||
}
|
||||
|
||||
const toggleEnabled = async () => {
|
||||
if (!editConfig) return
|
||||
|
||||
const newConfig = { ...editConfig, enabled: !editConfig.enabled }
|
||||
setEditConfig(newConfig)
|
||||
|
||||
setActionLoading('toggle')
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/admin/night-mode', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(newConfig),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Fehler beim Umschalten')
|
||||
}
|
||||
|
||||
setSuccessMessage(newConfig.enabled ? 'Nachtmodus aktiviert' : 'Nachtmodus deaktiviert')
|
||||
setTimeout(() => setSuccessMessage(null), 3000)
|
||||
fetchData()
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Umschalten fehlgeschlagen')
|
||||
// Zuruecksetzen bei Fehler
|
||||
setEditConfig({ ...editConfig })
|
||||
} finally {
|
||||
setActionLoading(null)
|
||||
}
|
||||
}
|
||||
|
||||
const getServiceStatusColor = (state: string) => {
|
||||
const lower = state.toLowerCase()
|
||||
if (lower === 'running' || lower.includes('up')) {
|
||||
return 'bg-green-100 text-green-800'
|
||||
}
|
||||
if (lower === 'exited' || lower.includes('exit')) {
|
||||
return 'bg-slate-100 text-slate-600'
|
||||
}
|
||||
if (lower === 'paused' || lower.includes('pause')) {
|
||||
return 'bg-yellow-100 text-yellow-800'
|
||||
}
|
||||
return 'bg-slate-100 text-slate-600'
|
||||
}
|
||||
|
||||
const runningCount = Object.values(status?.services_status || {}).filter(
|
||||
s => s.toLowerCase() === 'running' || s.toLowerCase().includes('up')
|
||||
).length
|
||||
const stoppedCount = Object.values(status?.services_status || {}).filter(
|
||||
s => s.toLowerCase() === 'exited' || s.toLowerCase().includes('exit')
|
||||
).length
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-2xl font-bold text-slate-900">Nachtabschaltung</h1>
|
||||
<p className="text-slate-500 mt-1">Automatisches Stoppen und Starten von Docker-Services nach Zeitplan.</p>
|
||||
</div>
|
||||
|
||||
{/* Status Messages */}
|
||||
{error && (
|
||||
<div className="mb-6 p-4 bg-red-50 border border-red-200 rounded-lg text-red-700 flex items-center gap-3">
|
||||
<svg className="w-5 h-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
{successMessage && (
|
||||
<div className="mb-6 p-4 bg-green-50 border border-green-200 rounded-lg text-green-700 flex items-center gap-3">
|
||||
<svg className="w-5 h-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
{successMessage}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading ? (
|
||||
<div className="flex justify-center py-12">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-orange-600" />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Haupt-Steuerung */}
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-6 mb-6">
|
||||
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-6">
|
||||
{/* Toggle */}
|
||||
<div className="flex items-center gap-6">
|
||||
<div
|
||||
onClick={toggleEnabled}
|
||||
className={`relative inline-flex h-10 w-20 cursor-pointer items-center rounded-full transition-colors ${
|
||||
editConfig?.enabled ? 'bg-orange-600' : 'bg-slate-300'
|
||||
} ${actionLoading === 'toggle' ? 'opacity-50 cursor-wait' : ''}`}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-8 w-8 transform rounded-full bg-white shadow-lg transition-transform ${
|
||||
editConfig?.enabled ? 'translate-x-11' : 'translate-x-1'
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-slate-900">
|
||||
Nachtmodus: {editConfig?.enabled ? 'Aktiv' : 'Inaktiv'}
|
||||
</h2>
|
||||
<p className="text-sm text-slate-500">
|
||||
{editConfig?.enabled
|
||||
? `Abschaltung um ${editConfig.shutdown_time}, Start um ${editConfig.startup_time}`
|
||||
: 'Zeitgesteuerte Abschaltung ist deaktiviert'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Manuelle Aktionen */}
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={() => executeAction('stop')}
|
||||
disabled={actionLoading !== null}
|
||||
className="flex items-center gap-2 px-5 py-2.5 bg-red-600 text-white rounded-lg font-medium hover:bg-red-700 disabled:opacity-50 transition-colors"
|
||||
>
|
||||
{actionLoading === 'stop' ? (
|
||||
<span className="animate-spin">◠</span>
|
||||
) : (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 10a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4z" />
|
||||
</svg>
|
||||
)}
|
||||
Jetzt abschalten
|
||||
</button>
|
||||
<button
|
||||
onClick={() => executeAction('start')}
|
||||
disabled={actionLoading !== null}
|
||||
className="flex items-center gap-2 px-5 py-2.5 bg-green-600 text-white rounded-lg font-medium hover:bg-green-700 disabled:opacity-50 transition-colors"
|
||||
>
|
||||
{actionLoading === 'start' ? (
|
||||
<span className="animate-spin">◠</span>
|
||||
) : (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
)}
|
||||
Jetzt starten
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status-Karten */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-5">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 rounded-full bg-blue-100 flex items-center justify-center">
|
||||
<svg className="w-6 h-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-slate-900">{status?.current_time || '--:--'}</div>
|
||||
<div className="text-sm text-slate-500">Aktuelle Zeit</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-5">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`w-12 h-12 rounded-full flex items-center justify-center ${
|
||||
status?.next_action === 'shutdown' ? 'bg-red-100' : 'bg-green-100'
|
||||
}`}>
|
||||
{status?.next_action === 'shutdown' ? (
|
||||
<svg className="w-6 h-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg className="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-slate-900">
|
||||
{status?.next_action_time || '--:--'}
|
||||
</div>
|
||||
<div className="text-sm text-slate-500">
|
||||
{status?.next_action === 'shutdown' ? 'Naechste Abschaltung' : 'Naechster Start'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-5">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 rounded-full bg-orange-100 flex items-center justify-center">
|
||||
<svg className="w-6 h-6 text-orange-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-slate-900">
|
||||
{status?.time_until_next_action || '-'}
|
||||
</div>
|
||||
<div className="text-sm text-slate-500">Countdown</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-5">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 rounded-full bg-slate-100 flex items-center justify-center">
|
||||
<svg className="w-6 h-6 text-slate-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-lg font-bold text-green-600">{runningCount}</span>
|
||||
<span className="text-slate-400">/</span>
|
||||
<span className="text-lg font-bold text-slate-600">{stoppedCount}</span>
|
||||
</div>
|
||||
<div className="text-sm text-slate-500">Aktiv / Gestoppt</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Zeitkonfiguration */}
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-6 mb-6">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h3 className="text-lg font-semibold text-slate-900">Zeitkonfiguration</h3>
|
||||
{editMode ? (
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
setEditMode(false)
|
||||
setEditConfig(status?.config || null)
|
||||
}}
|
||||
className="px-4 py-2 text-sm border border-slate-300 rounded-lg hover:bg-slate-50 transition-colors"
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
<button
|
||||
onClick={saveConfig}
|
||||
disabled={actionLoading === 'save'}
|
||||
className="px-4 py-2 text-sm bg-orange-600 text-white rounded-lg hover:bg-orange-700 disabled:opacity-50 transition-colors"
|
||||
>
|
||||
{actionLoading === 'save' ? 'Speichern...' : 'Speichern'}
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => setEditMode(true)}
|
||||
className="px-4 py-2 text-sm border border-slate-300 rounded-lg hover:bg-slate-50 transition-colors"
|
||||
>
|
||||
Bearbeiten
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-2">
|
||||
<span className="flex items-center gap-2">
|
||||
<svg className="w-4 h-4 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
|
||||
</svg>
|
||||
Abschaltung um
|
||||
</span>
|
||||
</label>
|
||||
{editMode ? (
|
||||
<input
|
||||
type="time"
|
||||
value={editConfig?.shutdown_time || '22:00'}
|
||||
onChange={e => setEditConfig(prev => prev ? { ...prev, shutdown_time: e.target.value } : null)}
|
||||
className="w-full px-4 py-3 text-xl font-mono border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-500"
|
||||
/>
|
||||
) : (
|
||||
<div className="text-3xl font-mono font-bold text-slate-900 bg-slate-50 px-4 py-3 rounded-lg">
|
||||
{editConfig?.shutdown_time || '22:00'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-2">
|
||||
<span className="flex items-center gap-2">
|
||||
<svg className="w-4 h-4 text-yellow-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
|
||||
</svg>
|
||||
Start um
|
||||
</span>
|
||||
</label>
|
||||
{editMode ? (
|
||||
<input
|
||||
type="time"
|
||||
value={editConfig?.startup_time || '06:00'}
|
||||
onChange={e => setEditConfig(prev => prev ? { ...prev, startup_time: e.target.value } : null)}
|
||||
className="w-full px-4 py-3 text-xl font-mono border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-500"
|
||||
/>
|
||||
) : (
|
||||
<div className="text-3xl font-mono font-bold text-slate-900 bg-slate-50 px-4 py-3 rounded-lg">
|
||||
{editConfig?.startup_time || '06:00'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Letzte Aktion */}
|
||||
{status?.config.last_action && (
|
||||
<div className="mt-6 pt-6 border-t border-slate-200">
|
||||
<div className="text-sm text-slate-500">
|
||||
Letzte Aktion:{' '}
|
||||
<span className={`font-semibold ${status.config.last_action === 'startup' ? 'text-green-600' : 'text-red-600'}`}>
|
||||
{status.config.last_action === 'startup' ? 'Gestartet' : 'Abgeschaltet'}
|
||||
</span>
|
||||
{status.config.last_action_time && (
|
||||
<span className="ml-2">
|
||||
am {new Date(status.config.last_action_time).toLocaleString('de-DE')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Service-Liste */}
|
||||
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden">
|
||||
<div className="p-6 border-b border-slate-200">
|
||||
<h3 className="text-lg font-semibold text-slate-900">Services</h3>
|
||||
<p className="text-sm text-slate-500 mt-1">
|
||||
Gruen = wird verwaltet, Grau = ausgeschlossen (laeuft immer)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="p-6">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3">
|
||||
{services?.all_services.map(service => {
|
||||
const serviceStatus = status?.services_status[service] || 'unknown'
|
||||
const isExcluded = services.excluded_services.includes(service)
|
||||
|
||||
return (
|
||||
<div
|
||||
key={service}
|
||||
className={`flex items-center justify-between px-4 py-3 rounded-lg border ${
|
||||
isExcluded
|
||||
? 'bg-slate-50 border-slate-200'
|
||||
: 'bg-orange-50 border-orange-200'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{isExcluded ? (
|
||||
<svg className="w-4 h-4 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg className="w-4 h-4 text-orange-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
)}
|
||||
<span className={`text-sm font-medium ${isExcluded ? 'text-slate-500' : 'text-slate-900'}`}>
|
||||
{service}
|
||||
</span>
|
||||
</div>
|
||||
<span className={`px-2 py-0.5 rounded text-xs font-semibold ${getServiceStatusColor(serviceStatus)}`}>
|
||||
{serviceStatus}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
{/* Auch excluded Services anzeigen, die nicht in all_services sind */}
|
||||
{services?.excluded_services
|
||||
.filter(s => !services.all_services.includes(s))
|
||||
.map(service => (
|
||||
<div
|
||||
key={service}
|
||||
className="flex items-center justify-between px-4 py-3 rounded-lg border bg-slate-50 border-slate-200"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<svg className="w-4 h-4 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
||||
</svg>
|
||||
<span className="text-sm font-medium text-slate-500">
|
||||
{service}
|
||||
</span>
|
||||
</div>
|
||||
<span className="px-2 py-0.5 rounded text-xs font-semibold bg-slate-100 text-slate-600">
|
||||
excluded
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Info Box */}
|
||||
<div className="mt-6 bg-orange-50 border border-orange-200 rounded-xl p-4">
|
||||
<div className="flex gap-3">
|
||||
<svg className="w-5 h-5 text-orange-600 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<div>
|
||||
<h4 className="font-semibold text-orange-900">Hinweise zur Nachtabschaltung</h4>
|
||||
<ul className="text-sm text-orange-800 mt-1 space-y-1 list-disc list-inside">
|
||||
<li>Der <strong>night-scheduler</strong> und <strong>nginx</strong> bleiben immer aktiv</li>
|
||||
<li>Services werden mit <code>docker compose stop</code> angehalten (Daten bleiben erhalten)</li>
|
||||
<li>Bei manuellem Start/Stop wird die letzte Aktion gespeichert</li>
|
||||
<li>Der Scheduler prueft jede Minute, ob eine Aktion faellig ist</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user