This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/admin-v2/components/dashboard/NightModeWidget.tsx
Benjamin Admin 21a844cb8a fix: Restore all files lost during destructive rebase
A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.

This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).

Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 09:51:32 +01:00

232 lines
8.6 KiB
TypeScript

'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<string, string>
}
export function NightModeWidget() {
const [status, setStatus] = useState<NightModeStatus | null>(null)
const [loading, setLoading] = useState(true)
const [actionLoading, setActionLoading] = useState<string | null>(null)
const [error, setError] = useState<string | null>(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 (
<div className="bg-white rounded-xl border border-slate-200 shadow-sm p-4">
<div className="animate-pulse flex items-center gap-3">
<div className="w-10 h-10 bg-slate-200 rounded-full" />
<div className="flex-1">
<div className="h-4 bg-slate-200 rounded w-24 mb-2" />
<div className="h-3 bg-slate-200 rounded w-32" />
</div>
</div>
</div>
)
}
if (error) {
return (
<div className="bg-white rounded-xl border border-slate-200 shadow-sm p-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-slate-100 rounded-full flex items-center justify-center">
<svg className="w-5 h-5 text-slate-400" 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>
</div>
<div>
<h3 className="font-medium text-slate-900">Nachtabschaltung</h3>
<p className="text-sm text-red-500">{error}</p>
</div>
</div>
<Link href="/infrastructure/night-mode" className="text-sm text-primary-600 hover:text-primary-700">
Details
</Link>
</div>
</div>
)
}
return (
<div className="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden">
{/* Header */}
<div className="px-4 py-3 border-b border-slate-200 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${
status?.config.enabled ? 'bg-orange-100' : 'bg-slate-100'
}`}>
<svg className={`w-5 h-5 ${status?.config.enabled ? 'text-orange-600' : 'text-slate-400'}`} 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>
</div>
<div>
<h3 className="font-semibold text-slate-900">Nachtabschaltung</h3>
<p className="text-sm text-slate-500">
{status?.config.enabled
? `${status.config.shutdown_time} - ${status.config.startup_time}`
: 'Deaktiviert'}
</p>
</div>
</div>
<Link href="/infrastructure/night-mode" className="text-sm text-primary-600 hover:text-primary-700">
Einstellungen
</Link>
</div>
{/* Content */}
<div className="p-4">
{/* Status Row */}
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-4">
{/* Toggle */}
<button
onClick={toggleEnabled}
disabled={actionLoading !== null}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
status?.config.enabled ? 'bg-orange-600' : 'bg-slate-300'
} ${actionLoading === 'toggle' ? 'opacity-50' : ''}`}
>
<span className={`inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform ${
status?.config.enabled ? 'translate-x-6' : 'translate-x-1'
}`} />
</button>
{/* Countdown */}
{status?.config.enabled && status.time_until_next_action && (
<div className="flex items-center gap-2">
<span className={`text-sm font-medium ${
status.next_action === 'shutdown' ? 'text-red-600' : 'text-green-600'
}`}>
{status.next_action === 'shutdown' ? '⏸' : '▶'} {status.time_until_next_action}
</span>
</div>
)}
</div>
{/* Service Count */}
<div className="text-sm text-slate-500">
<span className="font-semibold text-green-600">{runningCount}</span>
<span className="text-slate-400">/{totalCount}</span>
<span className="ml-1">aktiv</span>
</div>
</div>
{/* Action Buttons */}
<div className="flex gap-2">
<button
onClick={() => executeAction('stop')}
disabled={actionLoading !== null}
className="flex-1 flex items-center justify-center gap-2 px-3 py-2 bg-red-50 text-red-700 rounded-lg text-sm font-medium hover:bg-red-100 disabled:opacity-50 transition-colors"
>
{actionLoading === 'stop' ? (
<span className="animate-spin text-xs"></span>
) : (
<svg className="w-4 h-4" 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 10h6v4H9z" />
</svg>
)}
Stoppen
</button>
<button
onClick={() => executeAction('start')}
disabled={actionLoading !== null}
className="flex-1 flex items-center justify-center gap-2 px-3 py-2 bg-green-50 text-green-700 rounded-lg text-sm font-medium hover:bg-green-100 disabled:opacity-50 transition-colors"
>
{actionLoading === 'start' ? (
<span className="animate-spin text-xs"></span>
) : (
<svg className="w-4 h-4" 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>
)}
Starten
</button>
</div>
</div>
</div>
)
}