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>
This commit is contained in:
334
studio-v2/lib/AlertsContext.tsx
Normal file
334
studio-v2/lib/AlertsContext.tsx
Normal file
@@ -0,0 +1,334 @@
|
||||
'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' })
|
||||
}
|
||||
Reference in New Issue
Block a user