From 18838b5273f7631c24485b619bb30ab8fcd00c5c Mon Sep 17 00:00:00 2001 From: BreakPilot Dev Date: Mon, 9 Feb 2026 00:31:09 -0800 Subject: [PATCH] feat(dashboard): Add Night Mode widget to dashboard Add a compact Night Mode widget to the main dashboard that allows: - Quick toggle of night mode on/off - View countdown to next scheduled action - Manual start/stop of all services - See count of running vs stopped services The widget links to the full night-mode settings page for detailed configuration. Co-Authored-By: Claude Opus 4.5 --- admin-v2/app/(admin)/dashboard/page.tsx | 15 +- .../components/dashboard/NightModeWidget.tsx | 231 ++++++++++++++++++ 2 files changed, 243 insertions(+), 3 deletions(-) create mode 100644 admin-v2/components/dashboard/NightModeWidget.tsx diff --git a/admin-v2/app/(admin)/dashboard/page.tsx b/admin-v2/app/(admin)/dashboard/page.tsx index 188f2e8..dbd3b99 100644 --- a/admin-v2/app/(admin)/dashboard/page.tsx +++ b/admin-v2/app/(admin)/dashboard/page.tsx @@ -6,6 +6,7 @@ import { getStoredRole, isCategoryVisibleForRole, RoleId } from '@/lib/roles' import { CategoryCard } from '@/components/common/ModuleCard' import { InfoNote } from '@/components/common/InfoBox' import { ServiceStatus } from '@/components/common/ServiceStatus' +import { NightModeWidget } from '@/components/dashboard/NightModeWidget' import Link from 'next/link' interface Stats { @@ -111,7 +112,18 @@ export default function DashboardPage() { ))} + {/* Infrastructure & System Status */} +

Infrastruktur

+
+ {/* Night Mode Widget */} + + + {/* System Status */} + +
+ {/* Recent Activity */} +

Aktivitaet

{/* Recent DSR */}
@@ -127,9 +139,6 @@ export default function DashboardPage() {

- - {/* System Status */} - {/* Info Box */} diff --git a/admin-v2/components/dashboard/NightModeWidget.tsx b/admin-v2/components/dashboard/NightModeWidget.tsx new file mode 100644 index 0000000..6de4459 --- /dev/null +++ b/admin-v2/components/dashboard/NightModeWidget.tsx @@ -0,0 +1,231 @@ +'use client' + +import { useEffect, useState, useCallback } from 'react' +import Link from 'next/link' + +interface NightModeConfig { + enabled: boolean + shutdown_time: string + startup_time: string + last_action: string | null + last_action_time: string | null +} + +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 +} + +export function NightModeWidget() { + const [status, setStatus] = useState(null) + const [loading, setLoading] = useState(true) + const [actionLoading, setActionLoading] = useState(null) + const [error, setError] = useState(null) + + const fetchData = useCallback(async () => { + try { + const response = await fetch('/api/admin/night-mode') + if (response.ok) { + setStatus(await response.json()) + setError(null) + } else { + setError('Nicht erreichbar') + } + } catch { + setError('Verbindung fehlgeschlagen') + } finally { + setLoading(false) + } + }, []) + + useEffect(() => { + fetchData() + const interval = setInterval(fetchData, 30000) + return () => clearInterval(interval) + }, [fetchData]) + + const toggleEnabled = async () => { + if (!status) return + setActionLoading('toggle') + + try { + const response = await fetch('/api/admin/night-mode', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ ...status.config, enabled: !status.config.enabled }), + }) + if (response.ok) { + fetchData() + } + } catch { + // ignore + } finally { + setActionLoading(null) + } + } + + const executeAction = async (action: 'start' | 'stop') => { + setActionLoading(action) + try { + const response = await fetch('/api/admin/night-mode/execute', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ action }), + }) + if (response.ok) { + fetchData() + } + } catch { + // ignore + } finally { + setActionLoading(null) + } + } + + const runningCount = Object.values(status?.services_status || {}).filter( + s => s.toLowerCase() === 'running' || s.toLowerCase().includes('up') + ).length + const totalCount = Object.keys(status?.services_status || {}).length + + if (loading) { + return ( +
+
+
+
+
+
+
+
+
+ ) + } + + if (error) { + return ( +
+
+
+
+ + + +
+
+

Nachtabschaltung

+

{error}

+
+
+ + Details + +
+
+ ) + } + + return ( +
+ {/* Header */} +
+
+
+ + + +
+
+

Nachtabschaltung

+

+ {status?.config.enabled + ? `${status.config.shutdown_time} - ${status.config.startup_time}` + : 'Deaktiviert'} +

+
+
+ + Einstellungen + +
+ + {/* Content */} +
+ {/* Status Row */} +
+
+ {/* Toggle */} + + + {/* Countdown */} + {status?.config.enabled && status.time_until_next_action && ( +
+ + {status.next_action === 'shutdown' ? '⏸' : '▶'} {status.time_until_next_action} + +
+ )} +
+ + {/* Service Count */} +
+ {runningCount} + /{totalCount} + aktiv +
+
+ + {/* Action Buttons */} +
+ + +
+
+
+ ) +}