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/backend/frontend/modules/mac_mini.py
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

809 lines
21 KiB
Python

"""
BreakPilot Studio - Mac Mini Control Module
Funktionen:
- Fernsteuerung des Mac Mini Servers
- Status-Uebersicht (Ping, SSH, Docker, Ollama)
- Docker Container Management
- Ollama Modell-Downloads mit Fortschrittsanzeige
- Power Management (Wake-on-LAN, Restart, Shutdown)
"""
class MacMiniControlModule:
"""Modul fuer die Mac Mini Server-Steuerung."""
@staticmethod
def get_css() -> str:
"""CSS fuer das Mac Mini Control Modul."""
return """
/* =============================================
MAC MINI CONTROL MODULE
============================================= */
.panel-mac-mini {
display: flex;
flex-direction: column;
height: 100%;
background: var(--bp-bg);
overflow-y: auto;
}
.mac-mini-header {
padding: 32px 40px 24px;
background: var(--bp-surface);
border-bottom: 1px solid var(--bp-border);
display: flex;
justify-content: space-between;
align-items: center;
}
.mac-mini-header h1 {
font-size: 28px;
font-weight: 700;
color: var(--bp-text);
margin: 0;
display: flex;
align-items: center;
gap: 12px;
}
.mac-mini-status-badge {
padding: 6px 16px;
border-radius: 20px;
font-size: 14px;
font-weight: 600;
}
.mac-mini-status-badge.online {
background: rgba(34, 197, 94, 0.2);
color: #22c55e;
border: 1px solid #22c55e;
}
.mac-mini-status-badge.offline {
background: rgba(239, 68, 68, 0.2);
color: #ef4444;
border: 1px solid #ef4444;
}
.mac-mini-status-badge.checking {
background: rgba(251, 191, 36, 0.2);
color: #fbbf24;
border: 1px solid #fbbf24;
}
.mac-mini-content {
padding: 32px 40px;
flex: 1;
}
.mac-mini-controls {
display: flex;
gap: 12px;
margin-bottom: 24px;
flex-wrap: wrap;
}
.mac-mini-controls .btn {
padding: 10px 20px;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
border: none;
transition: all 0.2s;
}
.mac-mini-controls .btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
}
.mac-mini-controls .btn-wake {
background: #22c55e;
color: white;
}
.mac-mini-controls .btn-restart {
background: #f59e0b;
color: white;
}
.mac-mini-controls .btn-shutdown {
background: #ef4444;
color: white;
}
.mac-mini-controls .btn-refresh {
background: var(--bp-surface-elevated);
color: var(--bp-text);
border: 1px solid var(--bp-border);
}
.mac-mini-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 20px;
margin-bottom: 24px;
}
.mac-mini-card {
background: var(--bp-surface);
border: 1px solid var(--bp-border);
border-radius: 12px;
padding: 20px;
}
.mac-mini-card h3 {
color: var(--bp-text);
font-size: 16px;
margin: 0 0 16px 0;
display: flex;
align-items: center;
gap: 8px;
}
.mac-mini-card-row {
display: flex;
justify-content: space-between;
padding: 10px 0;
border-bottom: 1px solid var(--bp-border);
}
.mac-mini-card-row:last-child {
border-bottom: none;
}
.mac-mini-card-label {
color: var(--bp-text-muted);
}
.mac-mini-card-value {
font-weight: 500;
}
.mac-mini-card-value.ok {
color: #22c55e;
}
.mac-mini-card-value.error {
color: #ef4444;
}
/* Docker Containers */
.mac-mini-container-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.mac-mini-container-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background: var(--bp-surface-elevated);
border-radius: 8px;
border: 1px solid var(--bp-border);
}
.mac-mini-container-name {
color: var(--bp-text);
font-size: 14px;
font-family: monospace;
}
.mac-mini-container-status {
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
}
.mac-mini-container-status.healthy {
background: rgba(34, 197, 94, 0.2);
color: #22c55e;
}
.mac-mini-container-status.running {
background: rgba(59, 130, 246, 0.2);
color: #3b82f6;
}
.mac-mini-container-status.stopped {
background: rgba(239, 68, 68, 0.2);
color: #ef4444;
}
/* Ollama Models */
.mac-mini-model-list {
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 20px;
}
.mac-mini-model-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
background: var(--bp-surface-elevated);
border-radius: 8px;
border: 1px solid var(--bp-border);
}
.mac-mini-model-info {
display: flex;
flex-direction: column;
gap: 4px;
}
.mac-mini-model-name {
color: var(--bp-text);
font-size: 16px;
font-weight: 600;
}
.mac-mini-model-details {
color: var(--bp-text-muted);
font-size: 13px;
}
.mac-mini-model-size {
color: var(--bp-primary);
font-size: 14px;
font-weight: 600;
}
/* Download Progress */
.mac-mini-download {
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid var(--bp-border);
}
.mac-mini-download h4 {
color: var(--bp-text);
margin: 0 0 12px 0;
}
.mac-mini-download-input {
display: flex;
gap: 12px;
margin-bottom: 16px;
}
.mac-mini-download-input input {
flex: 1;
padding: 12px 16px;
border-radius: 8px;
border: 1px solid var(--bp-border);
background: var(--bp-surface-elevated);
color: var(--bp-text);
font-size: 14px;
}
.mac-mini-download-input input:focus {
outline: none;
border-color: var(--bp-primary);
}
.mac-mini-progress {
display: none;
}
.mac-mini-progress.active {
display: block;
}
.mac-mini-progress-header {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
}
.mac-mini-progress-name {
color: var(--bp-text);
font-weight: 600;
}
.mac-mini-progress-stats {
color: var(--bp-text-muted);
font-size: 13px;
}
.mac-mini-progress-bar-container {
height: 24px;
background: var(--bp-surface-elevated);
border-radius: 12px;
overflow: hidden;
position: relative;
}
.mac-mini-progress-bar {
height: 100%;
background: linear-gradient(90deg, var(--bp-primary), #991b1b);
border-radius: 12px;
transition: width 0.3s ease;
width: 0%;
}
.mac-mini-progress-text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
font-size: 12px;
font-weight: 600;
text-shadow: 0 1px 2px rgba(0,0,0,0.5);
}
.mac-mini-progress-log {
margin-top: 16px;
padding: 16px;
background: #0a0a0a;
border-radius: 8px;
font-family: monospace;
font-size: 12px;
color: #22c55e;
max-height: 200px;
overflow-y: auto;
white-space: pre-wrap;
}
/* Docker Controls */
.mac-mini-docker-controls {
margin-top: 16px;
display: flex;
gap: 8px;
}
.mac-mini-docker-controls .btn {
flex: 1;
padding: 10px 16px;
border-radius: 8px;
font-weight: 500;
cursor: pointer;
background: var(--bp-surface-elevated);
color: var(--bp-text);
border: 1px solid var(--bp-border);
transition: all 0.2s;
}
.mac-mini-docker-controls .btn:hover {
background: var(--bp-surface);
border-color: var(--bp-primary);
}
"""
@staticmethod
def get_html() -> str:
"""HTML fuer das Mac Mini Control Modul."""
return """
<!-- Mac Mini Control Panel -->
<div id="panel-mac-mini" class="panel-mac-mini" style="display: none;">
<!-- Header -->
<div class="mac-mini-header">
<h1>🖥️ Mac Mini Control</h1>
<span class="mac-mini-status-badge checking" id="mac-mini-status-badge">Prüfe...</span>
</div>
<!-- Content -->
<div class="mac-mini-content">
<!-- Power Controls -->
<div class="mac-mini-controls">
<button class="btn btn-wake" onclick="macMiniWake()" id="btn-mac-mini-wake">⚡ Wake on LAN</button>
<button class="btn btn-restart" onclick="macMiniRestart()" id="btn-mac-mini-restart">🔄 Neustart</button>
<button class="btn btn-shutdown" onclick="macMiniShutdown()" id="btn-mac-mini-shutdown">⏻ Herunterfahren</button>
<button class="btn btn-refresh" onclick="macMiniRefresh()">🔍 Status aktualisieren</button>
</div>
<!-- Status Grid -->
<div class="mac-mini-grid">
<!-- Connection -->
<div class="mac-mini-card">
<h3>🌐 Verbindung</h3>
<div class="mac-mini-card-row">
<span class="mac-mini-card-label">IP-Adresse</span>
<span class="mac-mini-card-value" id="mac-mini-ip">192.168.178.100</span>
</div>
<div class="mac-mini-card-row">
<span class="mac-mini-card-label">SSH</span>
<span class="mac-mini-card-value" id="mac-mini-ssh">--</span>
</div>
<div class="mac-mini-card-row">
<span class="mac-mini-card-label">Ping</span>
<span class="mac-mini-card-value" id="mac-mini-ping">--</span>
</div>
</div>
<!-- Services -->
<div class="mac-mini-card">
<h3>⚙️ Services</h3>
<div class="mac-mini-card-row">
<span class="mac-mini-card-label">Backend API</span>
<span class="mac-mini-card-value" id="mac-mini-backend">--</span>
</div>
<div class="mac-mini-card-row">
<span class="mac-mini-card-label">Ollama</span>
<span class="mac-mini-card-value" id="mac-mini-ollama">--</span>
</div>
<div class="mac-mini-card-row">
<span class="mac-mini-card-label">Docker</span>
<span class="mac-mini-card-value" id="mac-mini-docker">--</span>
</div>
</div>
<!-- System -->
<div class="mac-mini-card">
<h3>💻 System</h3>
<div class="mac-mini-card-row">
<span class="mac-mini-card-label">Uptime</span>
<span class="mac-mini-card-value" id="mac-mini-uptime">--</span>
</div>
<div class="mac-mini-card-row">
<span class="mac-mini-card-label">CPU Load</span>
<span class="mac-mini-card-value" id="mac-mini-cpu">--</span>
</div>
<div class="mac-mini-card-row">
<span class="mac-mini-card-label">Memory</span>
<span class="mac-mini-card-value" id="mac-mini-memory">--</span>
</div>
</div>
</div>
<!-- Docker Containers -->
<div class="mac-mini-card" style="margin-bottom: 24px;">
<h3>🐳 Docker Container</h3>
<div class="mac-mini-container-list" id="mac-mini-containers">
<div style="color: var(--bp-text-muted); text-align: center; padding: 20px;">
Lade Container-Status...
</div>
</div>
<div class="mac-mini-docker-controls">
<button class="btn" onclick="macMiniDockerUp()">▶️ Container starten</button>
<button class="btn" onclick="macMiniDockerDown()">⏹️ Container stoppen</button>
</div>
</div>
<!-- Ollama Models -->
<div class="mac-mini-card">
<h3>🤖 Ollama LLM Modelle</h3>
<div class="mac-mini-model-list" id="mac-mini-models">
<div style="color: var(--bp-text-muted); text-align: center; padding: 20px;">
Lade Modelle...
</div>
</div>
<div class="mac-mini-download">
<h4>📥 Neues Modell herunterladen</h4>
<div class="mac-mini-download-input">
<input type="text" id="mac-mini-model-input" placeholder="Modellname (z.B. llama3.2-vision:11b, mistral, qwen2.5:7b)">
<button class="btn btn-refresh" onclick="macMiniPullModel()" id="btn-mac-mini-pull">Herunterladen</button>
</div>
<div class="mac-mini-progress" id="mac-mini-progress">
<div class="mac-mini-progress-header">
<span class="mac-mini-progress-name" id="mac-mini-progress-name">--</span>
<span class="mac-mini-progress-stats" id="mac-mini-progress-stats">-- / --</span>
</div>
<div class="mac-mini-progress-bar-container">
<div class="mac-mini-progress-bar" id="mac-mini-progress-bar"></div>
<span class="mac-mini-progress-text" id="mac-mini-progress-text">0%</span>
</div>
<div class="mac-mini-progress-log" id="mac-mini-progress-log">Starte Download...</div>
</div>
</div>
</div>
</div>
</div>
"""
@staticmethod
def get_js() -> str:
"""JavaScript fuer das Mac Mini Control Modul."""
return """
// =============================================
// MAC MINI CONTROL MODULE
// =============================================
let macMiniModuleState = {
ip: '192.168.178.100',
isOnline: false,
downloadInProgress: false,
pollInterval: null
};
function loadMacMiniModule() {
console.log('Loading Mac Mini Control Module...');
macMiniRefresh();
startMacMiniPolling();
}
function unloadMacMiniModule() {
stopMacMiniPolling();
}
function startMacMiniPolling() {
stopMacMiniPolling();
macMiniModuleState.pollInterval = setInterval(macMiniRefresh, 30000);
}
function stopMacMiniPolling() {
if (macMiniModuleState.pollInterval) {
clearInterval(macMiniModuleState.pollInterval);
macMiniModuleState.pollInterval = null;
}
}
async function macMiniRefresh() {
const statusBadge = document.getElementById('mac-mini-status-badge');
if (!statusBadge) return;
statusBadge.className = 'mac-mini-status-badge checking';
statusBadge.textContent = 'Prüfe...';
try {
const response = await fetch('/api/mac-mini/status');
const data = await response.json();
macMiniModuleState.isOnline = data.online;
macMiniModuleState.ip = data.ip || macMiniModuleState.ip;
// Update status badge
if (data.online) {
statusBadge.className = 'mac-mini-status-badge online';
statusBadge.textContent = 'Online';
} else {
statusBadge.className = 'mac-mini-status-badge offline';
statusBadge.textContent = 'Offline';
}
// Update IP
setMacMiniValue('mac-mini-ip', macMiniModuleState.ip);
// Update connection
setMacMiniStatus('mac-mini-ssh', data.ssh ? 'Verbunden' : 'Nicht verfügbar', data.ssh);
setMacMiniStatus('mac-mini-ping', data.ping ? 'OK' : 'Timeout', data.ping);
// Update services
setMacMiniStatus('mac-mini-backend', data.backend ? 'Läuft' : 'Offline', data.backend);
setMacMiniStatus('mac-mini-ollama', data.ollama ? 'Läuft' : 'Offline', data.ollama);
setMacMiniStatus('mac-mini-docker', data.docker ? 'Läuft' : 'Offline', data.docker);
// Update system
setMacMiniValue('mac-mini-uptime', data.uptime || '--');
setMacMiniValue('mac-mini-cpu', data.cpu_load || '--');
setMacMiniValue('mac-mini-memory', data.memory || '--');
// Update containers
renderMacMiniContainers(data.containers || []);
// Update models
renderMacMiniModels(data.models || []);
// Enable/disable buttons
const btnWake = document.getElementById('btn-mac-mini-wake');
const btnRestart = document.getElementById('btn-mac-mini-restart');
const btnShutdown = document.getElementById('btn-mac-mini-shutdown');
if (btnWake) btnWake.disabled = data.online;
if (btnRestart) btnRestart.disabled = !data.online;
if (btnShutdown) btnShutdown.disabled = !data.online;
} catch (error) {
console.error('Mac Mini status error:', error);
statusBadge.className = 'mac-mini-status-badge offline';
statusBadge.textContent = 'Fehler';
}
}
function setMacMiniValue(id, value) {
const el = document.getElementById(id);
if (el) el.textContent = value;
}
function setMacMiniStatus(id, text, isOk) {
const el = document.getElementById(id);
if (el) {
el.textContent = text;
el.className = 'mac-mini-card-value ' + (isOk ? 'ok' : 'error');
}
}
function renderMacMiniContainers(containers) {
const list = document.getElementById('mac-mini-containers');
if (!list) return;
if (containers.length === 0) {
list.innerHTML = '<div style="color: var(--bp-text-muted); text-align: center; padding: 20px;">Keine Container gefunden</div>';
return;
}
list.innerHTML = containers.map(c => {
const isHealthy = c.status.includes('healthy');
const isRunning = c.status.includes('Up');
const statusClass = isHealthy ? 'healthy' : (isRunning ? 'running' : 'stopped');
return `
<div class="mac-mini-container-item">
<span class="mac-mini-container-name">${c.name}</span>
<span class="mac-mini-container-status ${statusClass}">${c.status}</span>
</div>
`;
}).join('');
}
function renderMacMiniModels(models) {
const list = document.getElementById('mac-mini-models');
if (!list) return;
if (models.length === 0) {
list.innerHTML = '<div style="color: var(--bp-text-muted); text-align: center; padding: 20px;">Keine Modelle installiert</div>';
return;
}
list.innerHTML = models.map(m => {
const sizeGB = (m.size / (1024 * 1024 * 1024)).toFixed(1);
const details = m.details || {};
return `
<div class="mac-mini-model-item">
<div class="mac-mini-model-info">
<span class="mac-mini-model-name">${m.name}</span>
<span class="mac-mini-model-details">${details.parameter_size || ''} | ${details.quantization_level || ''}</span>
</div>
<span class="mac-mini-model-size">${sizeGB} GB</span>
</div>
`;
}).join('');
}
// Power Controls
async function macMiniWake() {
if (!confirm('Mac Mini per Wake-on-LAN aufwecken?')) return;
try {
const response = await fetch('/api/mac-mini/wake', { method: 'POST' });
const data = await response.json();
alert(data.message || 'Wake-on-LAN Paket gesendet');
setTimeout(macMiniRefresh, 5000);
} catch (error) {
alert('Fehler: ' + error.message);
}
}
async function macMiniRestart() {
if (!confirm('Mac Mini wirklich neu starten?')) return;
try {
const response = await fetch('/api/mac-mini/restart', { method: 'POST' });
const data = await response.json();
alert(data.message || 'Neustart ausgelöst');
setTimeout(macMiniRefresh, 60000);
} catch (error) {
alert('Fehler: ' + error.message);
}
}
async function macMiniShutdown() {
if (!confirm('Mac Mini wirklich herunterfahren?')) return;
try {
const response = await fetch('/api/mac-mini/shutdown', { method: 'POST' });
const data = await response.json();
alert(data.message || 'Shutdown ausgelöst');
setTimeout(macMiniRefresh, 10000);
} catch (error) {
alert('Fehler: ' + error.message);
}
}
// Docker Controls
async function macMiniDockerUp() {
try {
const response = await fetch('/api/mac-mini/docker/up', { method: 'POST' });
const data = await response.json();
alert(data.message || 'Container werden gestartet...');
setTimeout(macMiniRefresh, 10000);
} catch (error) {
alert('Fehler: ' + error.message);
}
}
async function macMiniDockerDown() {
if (!confirm('Alle Container stoppen?')) return;
try {
const response = await fetch('/api/mac-mini/docker/down', { method: 'POST' });
const data = await response.json();
alert(data.message || 'Container werden gestoppt...');
setTimeout(macMiniRefresh, 5000);
} catch (error) {
alert('Fehler: ' + error.message);
}
}
// Ollama Model Download
async function macMiniPullModel() {
const input = document.getElementById('mac-mini-model-input');
const modelName = input ? input.value.trim() : '';
if (!modelName) {
alert('Bitte Modellnamen eingeben');
return;
}
if (macMiniModuleState.downloadInProgress) {
alert('Download läuft bereits');
return;
}
macMiniModuleState.downloadInProgress = true;
const btnPull = document.getElementById('btn-mac-mini-pull');
if (btnPull) btnPull.disabled = true;
const progressDiv = document.getElementById('mac-mini-progress');
const progressBar = document.getElementById('mac-mini-progress-bar');
const progressText = document.getElementById('mac-mini-progress-text');
const progressStats = document.getElementById('mac-mini-progress-stats');
const progressLog = document.getElementById('mac-mini-progress-log');
const progressName = document.getElementById('mac-mini-progress-name');
if (progressDiv) progressDiv.classList.add('active');
if (progressName) progressName.textContent = modelName;
if (progressLog) progressLog.textContent = 'Starte Download...\\n';
try {
const response = await fetch('/api/mac-mini/ollama/pull', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ model: modelName })
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const text = decoder.decode(value);
const lines = text.split('\\n').filter(l => l.trim());
for (const line of lines) {
try {
const data = JSON.parse(line);
if (data.status && progressLog) {
progressLog.textContent += data.status + '\\n';
progressLog.scrollTop = progressLog.scrollHeight;
}
if (data.total && data.completed) {
const percent = Math.round((data.completed / data.total) * 100);
const completedMB = (data.completed / (1024 * 1024)).toFixed(1);
const totalMB = (data.total / (1024 * 1024)).toFixed(1);
if (progressBar) progressBar.style.width = percent + '%';
if (progressText) progressText.textContent = percent + '%';
if (progressStats) progressStats.textContent = completedMB + ' MB / ' + totalMB + ' MB';
}
if (data.status === 'success' && progressLog) {
progressLog.textContent += '\\n✅ Download abgeschlossen!\\n';
if (progressBar) progressBar.style.width = '100%';
if (progressText) progressText.textContent = '100%';
}
} catch (e) {
if (progressLog) progressLog.textContent += line + '\\n';
}
}
}
setTimeout(macMiniRefresh, 2000);
} catch (error) {
if (progressLog) progressLog.textContent += '\\n❌ Fehler: ' + error.message + '\\n';
} finally {
macMiniModuleState.downloadInProgress = false;
if (btnPull) btnPull.disabled = false;
}
}
"""