From 1e68ccd4d0f1722c22b34285431bf9f490bff155 Mon Sep 17 00:00:00 2001 From: BreakPilot Dev Date: Sun, 8 Feb 2026 23:01:59 -0800 Subject: [PATCH] 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 --- .../infrastructure/night-mode/page.tsx | 561 ++++++++++++++++++ 1 file changed, 561 insertions(+) create mode 100644 admin-v2/app/(admin)/infrastructure/night-mode/page.tsx diff --git a/admin-v2/app/(admin)/infrastructure/night-mode/page.tsx b/admin-v2/app/(admin)/infrastructure/night-mode/page.tsx new file mode 100644 index 0000000..16b89a9 --- /dev/null +++ b/admin-v2/app/(admin)/infrastructure/night-mode/page.tsx @@ -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 +} + +interface ServicesInfo { + all_services: string[] + excluded_services: string[] + status: Record +} + +export default function NightModePage() { + const [status, setStatus] = useState(null) + const [services, setServices] = useState(null) + const [loading, setLoading] = useState(true) + const [actionLoading, setActionLoading] = useState(null) + const [error, setError] = useState(null) + const [successMessage, setSuccessMessage] = useState(null) + + // Lokale Konfiguration fuer Bearbeitung + const [editMode, setEditMode] = useState(false) + const [editConfig, setEditConfig] = useState(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 ( +
+
+

Nachtabschaltung

+

Automatisches Stoppen und Starten von Docker-Services nach Zeitplan.

+
+ + {/* Status Messages */} + {error && ( +
+ + + + {error} +
+ )} + {successMessage && ( +
+ + + + {successMessage} +
+ )} + + {loading ? ( +
+
+
+ ) : ( + <> + {/* Haupt-Steuerung */} +
+
+ {/* Toggle */} +
+
+ +
+
+

+ Nachtmodus: {editConfig?.enabled ? 'Aktiv' : 'Inaktiv'} +

+

+ {editConfig?.enabled + ? `Abschaltung um ${editConfig.shutdown_time}, Start um ${editConfig.startup_time}` + : 'Zeitgesteuerte Abschaltung ist deaktiviert'} +

+
+
+ + {/* Manuelle Aktionen */} +
+ + +
+
+
+ + {/* Status-Karten */} +
+
+
+
+ + + +
+
+
{status?.current_time || '--:--'}
+
Aktuelle Zeit
+
+
+
+ +
+
+
+ {status?.next_action === 'shutdown' ? ( + + + + ) : ( + + + + )} +
+
+
+ {status?.next_action_time || '--:--'} +
+
+ {status?.next_action === 'shutdown' ? 'Naechste Abschaltung' : 'Naechster Start'} +
+
+
+
+ +
+
+
+ + + +
+
+
+ {status?.time_until_next_action || '-'} +
+
Countdown
+
+
+
+ +
+
+
+ + + +
+
+
+ {runningCount} + / + {stoppedCount} +
+
Aktiv / Gestoppt
+
+
+
+
+ + {/* Zeitkonfiguration */} +
+
+

Zeitkonfiguration

+ {editMode ? ( +
+ + +
+ ) : ( + + )} +
+ +
+
+ + {editMode ? ( + 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" + /> + ) : ( +
+ {editConfig?.shutdown_time || '22:00'} +
+ )} +
+ +
+ + {editMode ? ( + 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" + /> + ) : ( +
+ {editConfig?.startup_time || '06:00'} +
+ )} +
+
+ + {/* Letzte Aktion */} + {status?.config.last_action && ( +
+
+ Letzte Aktion:{' '} + + {status.config.last_action === 'startup' ? 'Gestartet' : 'Abgeschaltet'} + + {status.config.last_action_time && ( + + am {new Date(status.config.last_action_time).toLocaleString('de-DE')} + + )} +
+
+ )} +
+ + {/* Service-Liste */} +
+
+

Services

+

+ Gruen = wird verwaltet, Grau = ausgeschlossen (laeuft immer) +

+
+ +
+
+ {services?.all_services.map(service => { + const serviceStatus = status?.services_status[service] || 'unknown' + const isExcluded = services.excluded_services.includes(service) + + return ( +
+
+ {isExcluded ? ( + + + + ) : ( + + + + )} + + {service} + +
+ + {serviceStatus} + +
+ ) + })} + + {/* Auch excluded Services anzeigen, die nicht in all_services sind */} + {services?.excluded_services + .filter(s => !services.all_services.includes(s)) + .map(service => ( +
+
+ + + + + {service} + +
+ + excluded + +
+ ))} +
+
+
+ + {/* Info Box */} +
+
+ + + +
+

Hinweise zur Nachtabschaltung

+
    +
  • Der night-scheduler und nginx bleiben immer aktiv
  • +
  • Services werden mit docker compose stop angehalten (Daten bleiben erhalten)
  • +
  • Bei manuellem Start/Stop wird die letzte Aktion gespeichert
  • +
  • Der Scheduler prueft jede Minute, ob eine Aktion faellig ist
  • +
+
+
+
+ + )} +
+ ) +}