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:
361
studio-v2/lib/ActivityContext.tsx
Normal file
361
studio-v2/lib/ActivityContext.tsx
Normal file
@@ -0,0 +1,361 @@
|
||||
'use client'
|
||||
|
||||
import React, { createContext, useContext, useState, useCallback, useEffect, useRef } from 'react'
|
||||
|
||||
/**
|
||||
* Activity Tracking System
|
||||
*
|
||||
* Tracks user activities and calculates time savings compared to manual work.
|
||||
* Used for the "Zeitersparnis" dashboard tile and weekly reports.
|
||||
*/
|
||||
|
||||
// =============================================================================
|
||||
// TYPES
|
||||
// =============================================================================
|
||||
|
||||
export type ActivityType =
|
||||
| 'vocab_extraction'
|
||||
| 'klausur_correction'
|
||||
| 'worksheet_creation'
|
||||
| 'fairness_check'
|
||||
| 'gutachten_generation'
|
||||
|
||||
export interface ActivityMetadata {
|
||||
vocabCount?: number
|
||||
studentCount?: number
|
||||
criteriaCount?: number
|
||||
elementCount?: number
|
||||
pageCount?: number
|
||||
description?: string
|
||||
}
|
||||
|
||||
export interface ActivityLog {
|
||||
id: string
|
||||
type: ActivityType
|
||||
startedAt: string // ISO string for serialization
|
||||
completedAt: string
|
||||
actualSeconds: number
|
||||
estimatedManualSeconds: number
|
||||
savedSeconds: number
|
||||
metadata: ActivityMetadata
|
||||
}
|
||||
|
||||
export interface ActivityStats {
|
||||
todaySavedSeconds: number
|
||||
weekSavedSeconds: number
|
||||
monthSavedSeconds: number
|
||||
totalSavedSeconds: number
|
||||
activityCount: number
|
||||
lastActivity: ActivityLog | null
|
||||
}
|
||||
|
||||
interface ActiveActivity {
|
||||
type: ActivityType
|
||||
startedAt: Date
|
||||
metadata: ActivityMetadata
|
||||
}
|
||||
|
||||
interface ActivityContextType {
|
||||
// Start tracking an activity
|
||||
startActivity: (type: ActivityType, metadata?: ActivityMetadata) => void
|
||||
// Complete the current activity
|
||||
completeActivity: (additionalMetadata?: ActivityMetadata) => ActivityLog | null
|
||||
// Cancel without saving
|
||||
cancelActivity: () => void
|
||||
// Check if currently tracking
|
||||
isTracking: boolean
|
||||
currentActivity: ActiveActivity | null
|
||||
// Get statistics
|
||||
stats: ActivityStats
|
||||
// Get recent activities
|
||||
recentActivities: ActivityLog[]
|
||||
// Refresh stats from storage
|
||||
refreshStats: () => void
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// ESTIMATION FORMULAS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Calculate estimated manual time based on activity type and metadata
|
||||
*/
|
||||
function calculateManualEstimate(type: ActivityType, metadata: ActivityMetadata): number {
|
||||
switch (type) {
|
||||
case 'vocab_extraction': {
|
||||
// Typing: ~25 seconds per vocab (word + translation + example)
|
||||
// Layout: 15 minutes base
|
||||
const vocabCount = metadata.vocabCount || 20
|
||||
return (vocabCount * 25) + (15 * 60)
|
||||
}
|
||||
|
||||
case 'klausur_correction': {
|
||||
// Reading + grading + gutachten: ~45 minutes per student
|
||||
const studentCount = metadata.studentCount || 1
|
||||
return studentCount * 45 * 60
|
||||
}
|
||||
|
||||
case 'worksheet_creation': {
|
||||
// Design + formatting: ~30 seconds per element + 20 min base
|
||||
const elementCount = metadata.elementCount || 10
|
||||
return (elementCount * 30) + (20 * 60)
|
||||
}
|
||||
|
||||
case 'fairness_check': {
|
||||
// Manual comparison: ~5 minutes per student
|
||||
const studentCount = metadata.studentCount || 10
|
||||
return studentCount * 5 * 60
|
||||
}
|
||||
|
||||
case 'gutachten_generation': {
|
||||
// Writing gutachten manually: ~15 minutes per student
|
||||
const studentCount = metadata.studentCount || 1
|
||||
return studentCount * 15 * 60
|
||||
}
|
||||
|
||||
default:
|
||||
return 10 * 60 // 10 minutes default
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// STORAGE
|
||||
// =============================================================================
|
||||
|
||||
const STORAGE_KEY = 'breakpilot_activity_logs'
|
||||
|
||||
function loadActivities(): ActivityLog[] {
|
||||
if (typeof window === 'undefined') return []
|
||||
try {
|
||||
const stored = localStorage.getItem(STORAGE_KEY)
|
||||
return stored ? JSON.parse(stored) : []
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
function saveActivities(activities: ActivityLog[]): void {
|
||||
if (typeof window === 'undefined') return
|
||||
try {
|
||||
// Keep only last 1000 activities to prevent storage overflow
|
||||
const trimmed = activities.slice(-1000)
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(trimmed))
|
||||
} catch (e) {
|
||||
console.error('Failed to save activities:', e)
|
||||
}
|
||||
}
|
||||
|
||||
function addActivity(activity: ActivityLog): void {
|
||||
const activities = loadActivities()
|
||||
activities.push(activity)
|
||||
saveActivities(activities)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// STATISTICS CALCULATION
|
||||
// =============================================================================
|
||||
|
||||
function calculateStats(activities: ActivityLog[]): ActivityStats {
|
||||
const now = new Date()
|
||||
const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate())
|
||||
const weekStart = new Date(todayStart)
|
||||
weekStart.setDate(weekStart.getDate() - weekStart.getDay() + 1) // Monday
|
||||
const monthStart = new Date(now.getFullYear(), now.getMonth(), 1)
|
||||
|
||||
let todaySavedSeconds = 0
|
||||
let weekSavedSeconds = 0
|
||||
let monthSavedSeconds = 0
|
||||
let totalSavedSeconds = 0
|
||||
|
||||
for (const activity of activities) {
|
||||
const activityDate = new Date(activity.completedAt)
|
||||
totalSavedSeconds += activity.savedSeconds
|
||||
|
||||
if (activityDate >= monthStart) {
|
||||
monthSavedSeconds += activity.savedSeconds
|
||||
}
|
||||
if (activityDate >= weekStart) {
|
||||
weekSavedSeconds += activity.savedSeconds
|
||||
}
|
||||
if (activityDate >= todayStart) {
|
||||
todaySavedSeconds += activity.savedSeconds
|
||||
}
|
||||
}
|
||||
|
||||
const lastActivity = activities.length > 0 ? activities[activities.length - 1] : null
|
||||
|
||||
return {
|
||||
todaySavedSeconds,
|
||||
weekSavedSeconds,
|
||||
monthSavedSeconds,
|
||||
totalSavedSeconds,
|
||||
activityCount: activities.length,
|
||||
lastActivity,
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CONTEXT
|
||||
// =============================================================================
|
||||
|
||||
const ActivityContext = createContext<ActivityContextType | null>(null)
|
||||
|
||||
export function ActivityProvider({ children }: { children: React.ReactNode }) {
|
||||
const [currentActivity, setCurrentActivity] = useState<ActiveActivity | null>(null)
|
||||
const [stats, setStats] = useState<ActivityStats>({
|
||||
todaySavedSeconds: 0,
|
||||
weekSavedSeconds: 0,
|
||||
monthSavedSeconds: 0,
|
||||
totalSavedSeconds: 0,
|
||||
activityCount: 0,
|
||||
lastActivity: null,
|
||||
})
|
||||
const [recentActivities, setRecentActivities] = useState<ActivityLog[]>([])
|
||||
|
||||
// Load initial stats
|
||||
useEffect(() => {
|
||||
refreshStats()
|
||||
}, [])
|
||||
|
||||
const refreshStats = useCallback(() => {
|
||||
const activities = loadActivities()
|
||||
setStats(calculateStats(activities))
|
||||
setRecentActivities(activities.slice(-10).reverse())
|
||||
}, [])
|
||||
|
||||
const startActivity = useCallback((type: ActivityType, metadata: ActivityMetadata = {}) => {
|
||||
setCurrentActivity({
|
||||
type,
|
||||
startedAt: new Date(),
|
||||
metadata,
|
||||
})
|
||||
}, [])
|
||||
|
||||
const completeActivity = useCallback((additionalMetadata: ActivityMetadata = {}): ActivityLog | null => {
|
||||
if (!currentActivity) return null
|
||||
|
||||
const completedAt = new Date()
|
||||
const actualSeconds = Math.round((completedAt.getTime() - currentActivity.startedAt.getTime()) / 1000)
|
||||
const mergedMetadata = { ...currentActivity.metadata, ...additionalMetadata }
|
||||
const estimatedManualSeconds = calculateManualEstimate(currentActivity.type, mergedMetadata)
|
||||
const savedSeconds = Math.max(0, estimatedManualSeconds - actualSeconds)
|
||||
|
||||
const log: ActivityLog = {
|
||||
id: `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
|
||||
type: currentActivity.type,
|
||||
startedAt: currentActivity.startedAt.toISOString(),
|
||||
completedAt: completedAt.toISOString(),
|
||||
actualSeconds,
|
||||
estimatedManualSeconds,
|
||||
savedSeconds,
|
||||
metadata: mergedMetadata,
|
||||
}
|
||||
|
||||
addActivity(log)
|
||||
setCurrentActivity(null)
|
||||
refreshStats()
|
||||
|
||||
return log
|
||||
}, [currentActivity, refreshStats])
|
||||
|
||||
const cancelActivity = useCallback(() => {
|
||||
setCurrentActivity(null)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<ActivityContext.Provider
|
||||
value={{
|
||||
startActivity,
|
||||
completeActivity,
|
||||
cancelActivity,
|
||||
isTracking: currentActivity !== null,
|
||||
currentActivity,
|
||||
stats,
|
||||
recentActivities,
|
||||
refreshStats,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ActivityContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useActivity() {
|
||||
const context = useContext(ActivityContext)
|
||||
if (!context) {
|
||||
// Return a no-op version if not wrapped in provider
|
||||
return {
|
||||
startActivity: () => {},
|
||||
completeActivity: () => null,
|
||||
cancelActivity: () => {},
|
||||
isTracking: false,
|
||||
currentActivity: null,
|
||||
stats: {
|
||||
todaySavedSeconds: 0,
|
||||
weekSavedSeconds: 0,
|
||||
monthSavedSeconds: 0,
|
||||
totalSavedSeconds: 0,
|
||||
activityCount: 0,
|
||||
lastActivity: null,
|
||||
},
|
||||
recentActivities: [],
|
||||
refreshStats: () => {},
|
||||
}
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// UTILITY FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Format seconds as human-readable duration
|
||||
*/
|
||||
export function formatDuration(seconds: number): string {
|
||||
if (seconds < 60) {
|
||||
return `${seconds}s`
|
||||
}
|
||||
|
||||
const hours = Math.floor(seconds / 3600)
|
||||
const minutes = Math.floor((seconds % 3600) / 60)
|
||||
|
||||
if (hours > 0) {
|
||||
return minutes > 0 ? `${hours}h ${minutes}min` : `${hours}h`
|
||||
}
|
||||
|
||||
return `${minutes}min`
|
||||
}
|
||||
|
||||
/**
|
||||
* Format seconds as compact duration (for tiles)
|
||||
*/
|
||||
export function formatDurationCompact(seconds: number): { value: string; unit: string } {
|
||||
if (seconds < 60) {
|
||||
return { value: String(seconds), unit: 's' }
|
||||
}
|
||||
|
||||
const hours = Math.floor(seconds / 3600)
|
||||
const minutes = Math.floor((seconds % 3600) / 60)
|
||||
|
||||
if (hours > 0) {
|
||||
const decimal = Math.round((minutes / 60) * 10) / 10
|
||||
return { value: String(hours + (decimal > 0 ? decimal : 0)), unit: 'h' }
|
||||
}
|
||||
|
||||
return { value: String(minutes), unit: 'min' }
|
||||
}
|
||||
|
||||
/**
|
||||
* Get activity type display name
|
||||
*/
|
||||
export function getActivityTypeName(type: ActivityType): string {
|
||||
const names: Record<ActivityType, string> = {
|
||||
vocab_extraction: 'Vokabelextraktion',
|
||||
klausur_correction: 'Klausurkorrektur',
|
||||
worksheet_creation: 'Arbeitsblatt erstellt',
|
||||
fairness_check: 'Fairness-Check',
|
||||
gutachten_generation: 'Gutachten generiert',
|
||||
}
|
||||
return names[type] || type
|
||||
}
|
||||
Reference in New Issue
Block a user