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/studio-v2/lib/AlertsContext.tsx
Benjamin Admin bfdaf63ba9 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

335 lines
11 KiB
TypeScript

'use client'
import { createContext, useContext, useState, useEffect, useCallback, ReactNode } from 'react'
// Types
export type AlertImportance = 'KRITISCH' | 'DRINGEND' | 'WICHTIG' | 'PRUEFEN' | 'INFO'
export interface AlertSource {
title: string
url: string
domain: string
}
export interface Alert {
id: string
title: string
summary: string // LLM-generierte Zusammenfassung
source: string
importance: AlertImportance
timestamp: Date
isRead: boolean
sources: AlertSource[]
topicId?: string
}
export interface Topic {
id: string
name: string
keywords: string[]
googleAlertUrl?: string
rssFeedUrl?: string
isActive: boolean
icon?: string
}
export interface AlertsSettings {
notificationFrequency: 'realtime' | 'hourly' | 'daily'
minImportance: AlertImportance
wizardCompleted: boolean
}
interface AlertsContextType {
alerts: Alert[]
unreadCount: number
isLoading: boolean
error: string | null
fetchAlerts: () => Promise<void>
markAsRead: (id: string) => void
markAllAsRead: () => void
topics: Topic[]
addTopic: (topic: Topic) => void
updateTopic: (id: string, updates: Partial<Topic>) => void
removeTopic: (id: string) => void
settings: AlertsSettings
updateSettings: (settings: Partial<AlertsSettings>) => void
}
const AlertsContext = createContext<AlertsContextType | null>(null)
// LocalStorage Keys
const ALERTS_KEY = 'bp_alerts'
const TOPICS_KEY = 'bp_alerts_topics'
const SETTINGS_KEY = 'bp_alerts_settings'
// Default settings
const defaultSettings: AlertsSettings = {
notificationFrequency: 'daily',
minImportance: 'INFO',
wizardCompleted: false
}
// Vordefinierte Themen fuer Lehrer
export const lehrerThemen: Omit<Topic, 'id'>[] = [
{ name: 'Bildungspolitik', icon: '📜', keywords: ['Kultusministerium', 'Schulreform', 'Lehrplan', 'Bildungsminister'], isActive: false },
{ name: 'Digitale Bildung', icon: '💻', keywords: ['iPad Schule', 'digitale Medien', 'E-Learning', 'Tablet Unterricht'], isActive: false },
{ name: 'Inklusion', icon: '🤝', keywords: ['Förderschule', 'Integration', 'barrierefreies Lernen', 'Inklusion Schule'], isActive: false },
{ name: 'Abitur & Prüfungen', icon: '📝', keywords: ['Abitur', 'Zentralabitur', 'Prüfungsordnung', 'Abiturprüfung'], isActive: false },
{ name: 'Lehrerberuf', icon: '👩‍🏫', keywords: ['Lehrkräftemangel', 'Besoldung', 'Quereinsteiger', 'Lehrergehalt'], isActive: false },
{ name: 'KI in der Schule', icon: '🤖', keywords: ['ChatGPT Schule', 'KI Unterricht', 'künstliche Intelligenz Bildung'], isActive: false },
]
// Mock-Alerts für Demo (werden später durch Backend ersetzt)
const mockAlerts: Alert[] = [
{
id: 'alert-1',
title: 'Niedersachsen plant Digitalisierungsoffensive an Schulen',
summary: 'Das Kultusministerium Niedersachsen kündigt ein 50-Millionen-Programm zur Digitalisierung an. Alle weiterführenden Schulen sollen bis 2027 mit interaktiven Displays ausgestattet werden. Fortbildungen für Lehrkräfte sind Teil des Pakets.',
source: 'Hannoversche Allgemeine',
importance: 'WICHTIG',
timestamp: new Date(Date.now() - 2 * 60 * 60 * 1000),
isRead: false,
sources: [
{ title: 'Digitalisierung in Niedersachsens Schulen', url: 'https://example.com/1', domain: 'haz.de' }
],
topicId: 'digitale-bildung'
},
{
id: 'alert-2',
title: 'Neue Richtlinien für ChatGPT im Unterricht veröffentlicht',
summary: 'Die KMK hat Empfehlungen zum Umgang mit KI-Tools im Unterricht herausgegeben. Lehrkräfte sollen KI als Werkzeug nutzen dürfen, Prüfungsleistungen müssen jedoch eigenständig erbracht werden.',
source: 'Der Spiegel',
importance: 'DRINGEND',
timestamp: new Date(Date.now() - 5 * 60 * 60 * 1000),
isRead: false,
sources: [
{ title: 'KMK-Richtlinien zu KI', url: 'https://example.com/2', domain: 'spiegel.de' }
],
topicId: 'ki-schule'
},
{
id: 'alert-3',
title: 'Lehrerverband fordert bessere Besoldung',
summary: 'Der Deutsche Lehrerverband kritisiert die aktuelle Besoldungssituation. Besonders Grundschullehrkräfte seien im Vergleich zu anderen Bundesländern benachteiligt.',
source: 'Zeit Online',
importance: 'PRUEFEN',
timestamp: new Date(Date.now() - 24 * 60 * 60 * 1000),
isRead: true,
sources: [
{ title: 'Lehrergehälter im Vergleich', url: 'https://example.com/3', domain: 'zeit.de' }
],
topicId: 'lehrerberuf'
}
]
export function AlertsProvider({ children }: { children: ReactNode }) {
const [alerts, setAlerts] = useState<Alert[]>([])
const [topics, setTopics] = useState<Topic[]>([])
const [settings, setSettings] = useState<AlertsSettings>(defaultSettings)
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [mounted, setMounted] = useState(false)
// Nach dem ersten Render: Daten aus localStorage laden
useEffect(() => {
const storedAlerts = localStorage.getItem(ALERTS_KEY)
const storedTopics = localStorage.getItem(TOPICS_KEY)
const storedSettings = localStorage.getItem(SETTINGS_KEY)
if (storedAlerts) {
try {
const parsed = JSON.parse(storedAlerts)
// Konvertiere timestamp strings zu Date objects
setAlerts(parsed.map((a: any) => ({ ...a, timestamp: new Date(a.timestamp) })))
} catch (e) {
console.error('Error parsing stored alerts:', e)
setAlerts(mockAlerts)
}
} else {
// Demo-Alerts laden
setAlerts(mockAlerts)
}
if (storedTopics) {
try {
setTopics(JSON.parse(storedTopics))
} catch (e) {
console.error('Error parsing stored topics:', e)
}
}
if (storedSettings) {
try {
setSettings({ ...defaultSettings, ...JSON.parse(storedSettings) })
} catch (e) {
console.error('Error parsing stored settings:', e)
}
}
setMounted(true)
}, [])
// Alerts in localStorage speichern
useEffect(() => {
if (mounted && alerts.length > 0) {
localStorage.setItem(ALERTS_KEY, JSON.stringify(alerts))
}
}, [alerts, mounted])
// Topics in localStorage speichern
useEffect(() => {
if (mounted) {
localStorage.setItem(TOPICS_KEY, JSON.stringify(topics))
}
}, [topics, mounted])
// Settings in localStorage speichern
useEffect(() => {
if (mounted) {
localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings))
}
}, [settings, mounted])
// Alerts vom Backend abrufen
const fetchAlerts = useCallback(async () => {
setIsLoading(true)
setError(null)
try {
// TODO: Backend-Integration
// const response = await fetch('http://macmini:8000/api/alerts/')
// const data = await response.json()
// setAlerts(data)
// Für jetzt: Mock-Daten verwenden
await new Promise(resolve => setTimeout(resolve, 500))
// Alerts sind bereits geladen
} catch (err) {
setError('Fehler beim Laden der Alerts')
console.error('Error fetching alerts:', err)
} finally {
setIsLoading(false)
}
}, [])
// Alert als gelesen markieren
const markAsRead = useCallback((id: string) => {
setAlerts(prev => prev.map(alert =>
alert.id === id ? { ...alert, isRead: true } : alert
))
}, [])
// Alle Alerts als gelesen markieren
const markAllAsRead = useCallback(() => {
setAlerts(prev => prev.map(alert => ({ ...alert, isRead: true })))
}, [])
// Topic hinzufügen
const addTopic = useCallback((topic: Topic) => {
setTopics(prev => [...prev, topic])
}, [])
// Topic aktualisieren
const updateTopic = useCallback((id: string, updates: Partial<Topic>) => {
setTopics(prev => prev.map(topic =>
topic.id === id ? { ...topic, ...updates } : topic
))
}, [])
// Topic entfernen
const removeTopic = useCallback((id: string) => {
setTopics(prev => prev.filter(topic => topic.id !== id))
}, [])
// Settings aktualisieren
const updateSettings = useCallback((updates: Partial<AlertsSettings>) => {
setSettings(prev => ({ ...prev, ...updates }))
}, [])
// Ungelesene Alerts zählen
const unreadCount = alerts.filter(a => !a.isRead).length
// Während SSR: Default-Werte anzeigen
if (!mounted) {
return (
<AlertsContext.Provider
value={{
alerts: [],
unreadCount: 0,
isLoading: false,
error: null,
fetchAlerts: async () => {},
markAsRead: () => {},
markAllAsRead: () => {},
topics: [],
addTopic: () => {},
updateTopic: () => {},
removeTopic: () => {},
settings: defaultSettings,
updateSettings: () => {},
}}
>
{children}
</AlertsContext.Provider>
)
}
return (
<AlertsContext.Provider
value={{
alerts,
unreadCount,
isLoading,
error,
fetchAlerts,
markAsRead,
markAllAsRead,
topics,
addTopic,
updateTopic,
removeTopic,
settings,
updateSettings,
}}
>
{children}
</AlertsContext.Provider>
)
}
// Hook fuer einfache Verwendung
export function useAlerts() {
const context = useContext(AlertsContext)
if (!context) {
throw new Error('useAlerts must be used within an AlertsProvider')
}
return context
}
// Hilfsfunktion: Importance-Farben
export function getImportanceColor(importance: AlertImportance, isDark: boolean): string {
const colors = {
'KRITISCH': isDark ? 'bg-red-500/20 text-red-300 border-red-500/30' : 'bg-red-100 text-red-700 border-red-200',
'DRINGEND': isDark ? 'bg-orange-500/20 text-orange-300 border-orange-500/30' : 'bg-orange-100 text-orange-700 border-orange-200',
'WICHTIG': isDark ? 'bg-yellow-500/20 text-yellow-300 border-yellow-500/30' : 'bg-yellow-100 text-yellow-700 border-yellow-200',
'PRUEFEN': isDark ? 'bg-blue-500/20 text-blue-300 border-blue-500/30' : 'bg-blue-100 text-blue-700 border-blue-200',
'INFO': isDark ? 'bg-slate-500/20 text-slate-300 border-slate-500/30' : 'bg-slate-100 text-slate-600 border-slate-200',
}
return colors[importance]
}
// Hilfsfunktion: Relative Zeitangabe
export function getRelativeTime(date: Date): string {
const now = new Date()
const diffMs = now.getTime() - date.getTime()
const diffMins = Math.floor(diffMs / (1000 * 60))
const diffHours = Math.floor(diffMs / (1000 * 60 * 60))
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24))
if (diffMins < 1) return 'Gerade eben'
if (diffMins < 60) return `vor ${diffMins} Min.`
if (diffHours < 24) return `vor ${diffHours} Std.`
if (diffDays === 1) return 'Gestern'
if (diffDays < 7) return `vor ${diffDays} Tagen`
return date.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' })
}