'use client' /** * Mac Mini Control Admin Page * * Headless Mac Mini Server Management * - Power Controls (Wake-on-LAN, Restart, Shutdown) * - Docker Container Management * - Ollama LLM Model Management * - System Status Monitoring */ import AdminLayout from '@/components/admin/AdminLayout' import { useEffect, useState, useCallback, useRef } from 'react' interface MacMiniStatus { online: boolean ping: boolean ssh: boolean docker: boolean ollama: boolean internet: boolean // Neuer Status: Hat Mac Mini Internet-Zugang? ip: string uptime?: string cpu_load?: string memory?: string containers?: ContainerInfo[] models?: ModelInfo[] error?: string } // Aktionen die Internet benötigen const INTERNET_REQUIRED_ACTIONS = [ { action: 'LLM Modelle herunterladen', description: 'Ollama pull benötigt Verbindung zu ollama.com' }, { action: 'Docker Base Images pullen', description: 'Neue Images von Docker Hub/GHCR' }, { action: 'npm/pip/go Packages', description: 'Beim ersten Build oder neuen Dependencies' }, { action: 'Git Pull/Push', description: 'Code-Synchronisation mit Remote-Repository' }, ] interface ContainerInfo { name: string status: string ports?: string } interface ModelInfo { name: string size: string modified: string } interface DownloadProgress { model: string status: string completed: number total: number percent: number } // Modell-Informationen für Beschreibungen und Empfehlungen interface ModelDescription { name: string category: 'vision' | 'text' | 'code' | 'embedding' size: string description: string useCases: string[] recommended?: boolean } const MODEL_DATABASE: Record = { // Vision-Modelle (Handschrifterkennung) 'llama3.2-vision:11b': { name: 'Llama 3.2 Vision 11B', category: 'vision', size: '7.8 GB', description: 'Metas multimodales Vision-Modell. Kann Bilder und PDFs analysieren, Text aus Handschrift extrahieren.', useCases: ['Handschrifterkennung', 'Bild-Analyse', 'Dokumentenverarbeitung', 'OCR-Aufgaben'], recommended: true }, 'llama3.2-vision:90b': { name: 'Llama 3.2 Vision 90B', category: 'vision', size: '55 GB', description: 'Größte Version von Llama Vision. Beste Qualität für komplexe Bildanalyse.', useCases: ['Komplexe Handschrift', 'Detaillierte Bild-Analyse', 'Mathematische Formeln'], }, 'minicpm-v': { name: 'MiniCPM-V', category: 'vision', size: '5.5 GB', description: 'Kompaktes Vision-Modell mit gutem Preis-Leistungs-Verhältnis für OCR.', useCases: ['Schnelle OCR', 'Einfache Handschrift', 'Tabellen-Erkennung'], recommended: true }, 'llava:13b': { name: 'LLaVA 13B', category: 'vision', size: '8 GB', description: 'Large Language-and-Vision Assistant. Gut für Bild-zu-Text Aufgaben.', useCases: ['Bildbeschreibung', 'Handschrift', 'Diagramm-Analyse'], }, 'llava:34b': { name: 'LLaVA 34B', category: 'vision', size: '20 GB', description: 'Größere LLaVA-Version mit besserer Genauigkeit.', useCases: ['Komplexe Dokumente', 'Wissenschaftliche Notation', 'Detailanalyse'], }, 'bakllava': { name: 'BakLLaVA', category: 'vision', size: '4.7 GB', description: 'Verbesserte LLaVA-Variante mit Mistral-Basis.', useCases: ['Schnelle Bildanalyse', 'Handschrift', 'Formular-Verarbeitung'], }, // Text-Modelle (Klausurkorrektur) 'qwen2.5:14b': { name: 'Qwen 2.5 14B', category: 'text', size: '9 GB', description: 'Alibabas neuestes Sprachmodell. Exzellent für deutsche Texte und Bewertungsaufgaben.', useCases: ['Klausurkorrektur', 'Aufsatzbewertung', 'Feedback-Generierung', 'Grammatikprüfung'], recommended: true }, 'qwen2.5:7b': { name: 'Qwen 2.5 7B', category: 'text', size: '4.7 GB', description: 'Kleinere Qwen-Version, schneller bei ähnlicher Qualität.', useCases: ['Schnelle Korrektur', 'Einfache Bewertungen', 'Rechtschreibprüfung'], }, 'qwen2.5:32b': { name: 'Qwen 2.5 32B', category: 'text', size: '19 GB', description: 'Große Qwen-Version für komplexe Bewertungsaufgaben.', useCases: ['Detaillierte Analyse', 'Abitur-Klausuren', 'Komplexe Argumentation'], }, 'llama3.1:8b': { name: 'Llama 3.1 8B', category: 'text', size: '4.7 GB', description: 'Metas schnelles Textmodell. Gute Balance aus Geschwindigkeit und Qualität.', useCases: ['Allgemeine Korrektur', 'Schnelles Feedback', 'Zusammenfassungen'], }, 'llama3.1:70b': { name: 'Llama 3.1 70B', category: 'text', size: '40 GB', description: 'Großes Llama-Modell für anspruchsvolle Aufgaben.', useCases: ['Komplexe Klausuren', 'Tiefgehende Analyse', 'Wissenschaftliche Texte'], }, 'mistral': { name: 'Mistral 7B', category: 'text', size: '4.1 GB', description: 'Effizientes europäisches Modell mit guter deutscher Sprachunterstützung.', useCases: ['Deutsche Texte', 'Schnelle Verarbeitung', 'Allgemeine Korrektur'], }, 'mixtral:8x7b': { name: 'Mixtral 8x7B', category: 'text', size: '26 GB', description: 'Mixture-of-Experts Modell. Kombiniert Geschwindigkeit mit hoher Qualität.', useCases: ['Komplexe Korrektur', 'Multi-Aspekt-Bewertung', 'Wissenschaftliche Arbeiten'], }, 'gemma2:9b': { name: 'Gemma 2 9B', category: 'text', size: '5.5 GB', description: 'Googles kompaktes Modell. Gut für Instruktionen und Bewertungen.', useCases: ['Strukturierte Bewertung', 'Feedback', 'Zusammenfassungen'], }, 'phi3': { name: 'Phi-3', category: 'text', size: '2.3 GB', description: 'Microsofts kleines aber leistungsfähiges Modell.', useCases: ['Schnelle Checks', 'Einfache Korrektur', 'Ressourcenschonend'], }, } // Empfohlene Modelle für spezifische Anwendungsfälle const RECOMMENDED_MODELS = { handwriting: [ { model: 'llama3.2-vision:11b', reason: 'Beste Balance aus Qualität und Geschwindigkeit für Handschrift' }, { model: 'minicpm-v', reason: 'Schnell und ressourcenschonend für einfache Handschrift' }, { model: 'llava:13b', reason: 'Gute Alternative mit bewährter Vision-Architektur' }, ], grading: [ { model: 'qwen2.5:14b', reason: 'Beste Qualität für deutsche Klausurkorrektur' }, { model: 'llama3.1:8b', reason: 'Schnell für einfache Bewertungen' }, { model: 'mistral', reason: 'Europäisches Modell mit guter Sprachqualität' }, ] } export default function MacMiniControlPage() { const [status, setStatus] = useState(null) const [loading, setLoading] = useState(true) const [actionLoading, setActionLoading] = useState(null) const [error, setError] = useState(null) const [message, setMessage] = useState(null) const [downloadProgress, setDownloadProgress] = useState(null) const [modelInput, setModelInput] = useState('') const [selectedModel, setSelectedModel] = useState(null) const [showRecommendations, setShowRecommendations] = useState(false) const eventSourceRef = useRef(null) // Get model info from database const getModelInfo = (modelName: string): ModelDescription | null => { // Try exact match first if (MODEL_DATABASE[modelName]) return MODEL_DATABASE[modelName] // Try base name (without tag) const baseName = modelName.split(':')[0] const matchingKey = Object.keys(MODEL_DATABASE).find(key => key.startsWith(baseName) || key === baseName ) return matchingKey ? MODEL_DATABASE[matchingKey] : null } // Check if model is installed const isModelInstalled = (modelName: string): boolean => { if (!status?.models) return false return status.models.some(m => m.name === modelName || m.name.startsWith(modelName.split(':')[0]) ) } // API Endpoint (Mac Mini Backend or local proxy) const API_BASE = 'http://192.168.178.100:8000/api/mac-mini' // Fetch status const fetchStatus = useCallback(async () => { setLoading(true) setError(null) try { const response = await fetch(`${API_BASE}/status`) const data = await response.json() if (!response.ok) { throw new Error(data.detail || `HTTP ${response.status}`) } setStatus(data) } catch (err) { setError(err instanceof Error ? err.message : 'Verbindungsfehler') setStatus({ online: false, ping: false, ssh: false, docker: false, ollama: false, internet: false, ip: '192.168.178.100', error: 'Verbindung fehlgeschlagen' }) } finally { setLoading(false) } }, []) // Initial load useEffect(() => { fetchStatus() }, [fetchStatus]) // Auto-refresh every 30 seconds useEffect(() => { const interval = setInterval(fetchStatus, 30000) return () => clearInterval(interval) }, [fetchStatus]) // Wake on LAN const wakeOnLan = async () => { setActionLoading('wake') setError(null) setMessage(null) try { const response = await fetch(`${API_BASE}/wake`, { method: 'POST' }) const data = await response.json() if (!response.ok) { throw new Error(data.detail || 'Wake-on-LAN fehlgeschlagen') } setMessage('Wake-on-LAN Paket gesendet') setTimeout(fetchStatus, 5000) setTimeout(fetchStatus, 15000) } catch (err) { setError(err instanceof Error ? err.message : 'Fehler beim Aufwecken') } finally { setActionLoading(null) } } // Restart const restart = async () => { if (!confirm('Mac Mini wirklich neu starten?')) return setActionLoading('restart') setError(null) setMessage(null) try { const response = await fetch(`${API_BASE}/restart`, { method: 'POST' }) const data = await response.json() if (!response.ok) { throw new Error(data.detail || 'Neustart fehlgeschlagen') } setMessage('Neustart eingeleitet') setTimeout(fetchStatus, 30000) } catch (err) { setError(err instanceof Error ? err.message : 'Fehler beim Neustart') } finally { setActionLoading(null) } } // Shutdown const shutdown = async () => { if (!confirm('Mac Mini wirklich herunterfahren?')) return setActionLoading('shutdown') setError(null) setMessage(null) try { const response = await fetch(`${API_BASE}/shutdown`, { method: 'POST' }) const data = await response.json() if (!response.ok) { throw new Error(data.detail || 'Shutdown fehlgeschlagen') } setMessage('Shutdown eingeleitet') setTimeout(fetchStatus, 10000) } catch (err) { setError(err instanceof Error ? err.message : 'Fehler beim Herunterfahren') } finally { setActionLoading(null) } } // Docker Up const dockerUp = async () => { setActionLoading('docker-up') setError(null) setMessage(null) try { const response = await fetch(`${API_BASE}/docker/up`, { method: 'POST' }) const data = await response.json() if (!response.ok) { throw new Error(data.detail || 'Docker Start fehlgeschlagen') } setMessage('Docker Container werden gestartet...') setTimeout(fetchStatus, 5000) } catch (err) { setError(err instanceof Error ? err.message : 'Fehler beim Docker Start') } finally { setActionLoading(null) } } // Docker Down const dockerDown = async () => { if (!confirm('Docker Container wirklich stoppen?')) return setActionLoading('docker-down') setError(null) setMessage(null) try { const response = await fetch(`${API_BASE}/docker/down`, { method: 'POST' }) const data = await response.json() if (!response.ok) { throw new Error(data.detail || 'Docker Stop fehlgeschlagen') } setMessage('Docker Container werden gestoppt...') setTimeout(fetchStatus, 5000) } catch (err) { setError(err instanceof Error ? err.message : 'Fehler beim Docker Stop') } finally { setActionLoading(null) } } // Pull Model with SSE Progress const pullModel = async () => { if (!modelInput.trim()) return setActionLoading('pull') setError(null) setMessage(null) setDownloadProgress({ model: modelInput, status: 'starting', completed: 0, total: 0, percent: 0 }) try { // Close any existing EventSource if (eventSourceRef.current) { eventSourceRef.current.close() } // Use fetch with streaming for progress const response = await fetch(`${API_BASE}/ollama/pull`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: modelInput }) }) if (!response.ok) { const data = await response.json() throw new Error(data.detail || 'Model Pull fehlgeschlagen') } const reader = response.body?.getReader() const decoder = new TextDecoder() if (reader) { while (true) { const { done, value } = await reader.read() if (done) break const text = decoder.decode(value) const lines = text.split('\n').filter(line => line.trim()) for (const line of lines) { try { const data = JSON.parse(line) if (data.status === 'downloading' && data.total) { setDownloadProgress({ model: modelInput, status: data.status, completed: data.completed || 0, total: data.total, percent: Math.round((data.completed || 0) / data.total * 100) }) } else if (data.status === 'success') { setMessage(`Modell ${modelInput} erfolgreich heruntergeladen`) setDownloadProgress(null) setModelInput('') fetchStatus() } else if (data.error) { throw new Error(data.error) } } catch (e) { // Skip parsing errors for incomplete chunks } } } } } catch (err) { setError(err instanceof Error ? err.message : 'Fehler beim Model Download') setDownloadProgress(null) } finally { setActionLoading(null) } } // Format bytes const formatBytes = (bytes: number) => { if (bytes === 0) return '0 B' const k = 1024 const sizes = ['B', 'KB', 'MB', 'GB', 'TB'] const i = Math.floor(Math.log(bytes) / Math.log(k)) return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i] } // Status badge styling const getStatusBadge = (online: boolean) => { return online ? 'px-3 py-1 rounded-full text-sm font-semibold bg-green-100 text-green-800' : 'px-3 py-1 rounded-full text-sm font-semibold bg-red-100 text-red-800' } const getServiceStatus = (ok: boolean) => { return ok ? 'flex items-center gap-2 text-green-600' : 'flex items-center gap-2 text-red-500' } return ( {/* Power Controls */}
🖥️

Mac Mini Headless

IP: {status?.ip || '192.168.178.100'}

{loading ? 'Laden...' : status?.online ? 'Online' : 'Offline'}
{/* Power Buttons */}
{message && {message}} {error && {error}}
{/* Service Status Grid */}
Ping
{status?.ping ? 'Erreichbar' : 'Nicht erreichbar'}
SSH
{status?.ssh ? 'Verbunden' : 'Getrennt'}
Docker
{status?.docker ? 'Aktiv' : 'Inaktiv'}
Ollama
{status?.ollama ? 'Bereit' : 'Nicht bereit'}
Uptime
{status?.uptime || '-'}
{/* Internet Status Banner */}
{status?.internet ? '🌐' : '📴'}

Internet: {status?.internet ? 'Verbunden' : 'Offline (Normalbetrieb)'}

{status?.internet ? 'Mac Mini hat Internet-Zugang. LLM-Downloads und Updates möglich.' : 'Mac Mini arbeitet offline. Für bestimmte Aktionen muss Internet aktiviert werden.'}

{status?.internet ? 'Online' : 'Offline'}
{/* Internet Required Actions - nur anzeigen wenn offline */} {!status?.internet && (

⚠️ Diese Aktionen benötigen Internet:

{INTERNET_REQUIRED_ACTIONS.map((item, idx) => (
{item.action} – {item.description}
))}

💡 Tipp: Internet am Router/Switch nur bei Bedarf für den Mac Mini aktivieren.

)}
{/* Docker Section */}

🐳 Docker Container

{status?.containers && status.containers.length > 0 ? (
{status.containers.map((container, idx) => (
{container.name}
{container.ports && ( {container.ports} )} {container.status}
))}
) : (

{status?.online ? 'Keine Container gefunden' : 'Server nicht erreichbar'}

)}
{/* Ollama Section */}

🤖 Ollama LLM Modelle

{/* Installed Models */} {status?.models && status.models.length > 0 ? (
{status.models.map((model, idx) => { const modelInfo = getModelInfo(model.name) return (
{model.name} {modelInfo && ( )} {modelInfo?.category === 'vision' && ( Vision )}
{model.size} {model.modified}
) })}
) : (

{status?.ollama ? 'Keine Modelle installiert' : 'Ollama nicht erreichbar'}

)} {/* Model Info Modal */} {selectedModel && (
setSelectedModel(null)}>
e.stopPropagation()}> {(() => { const info = getModelInfo(selectedModel) if (!info) return

Keine Informationen verfügbar

return ( <>

{info.name}

{info.category === 'vision' ? '👁️ Vision' : info.category === 'text' ? '📝 Text' : info.category} {info.size}

{info.description}

Geeignet für:

{info.useCases.map((useCase, i) => ( {useCase} ))}
) })()}
)} {/* Download New Model */}

Neues Modell herunterladen

setModelInput(e.target.value)} placeholder="z.B. llama3.2, mistral, qwen2.5:14b" className="flex-1 px-4 py-2 border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent" disabled={actionLoading === 'pull'} />
{/* Download Progress */} {downloadProgress && (
{downloadProgress.model} {formatBytes(downloadProgress.completed)} / {formatBytes(downloadProgress.total)}
{downloadProgress.percent}%
)} {/* Toggle Recommendations */}
{/* Recommendations Section */} {showRecommendations && (

📚 Empfohlene Modelle

{/* Handwriting Recognition */}
✍️ Handschrifterkennung (Vision-Modelle)
{RECOMMENDED_MODELS.handwriting.map((rec, idx) => { const info = MODEL_DATABASE[rec.model] const installed = isModelInstalled(rec.model) return (
{info?.name || rec.model} Vision {info?.recommended && ⭐ Empfohlen} {installed && ✓ Installiert}

{rec.reason}

Größe: {info?.size || 'unbekannt'}

{!installed && ( )}
) })}
{/* Grading / Text Analysis */}
📝 Klausurkorrektur (Text-Modelle)
{RECOMMENDED_MODELS.grading.map((rec, idx) => { const info = MODEL_DATABASE[rec.model] const installed = isModelInstalled(rec.model) return (
{info?.name || rec.model} Text {info?.recommended && ⭐ Empfohlen} {installed && ✓ Installiert}

{rec.reason}

Größe: {info?.size || 'unbekannt'}

{!installed && ( )}
) })}
{/* Info Box */}
💡
Tipp: Modell-Kombinationen

Für beste Ergebnisse bei Klausuren mit Handschrift kombiniere ein Vision-Modell (für OCR/Handschrifterkennung) mit einem Text-Modell (für Bewertung und Feedback). Beispiel: llama3.2-vision:11b + qwen2.5:14b

)}
{/* Info */}

Mac Mini Headless Server

Der Mac Mini läuft ohne Monitor im LAN (192.168.178.100). Er hostet Docker-Container für das Backend, Ollama für lokale LLM-Verarbeitung und weitere Services. Wake-on-LAN ermöglicht das Remote-Einschalten.

) }