""" Mac Mini Remote Control Module for BreakPilot Admin Panel. Features: - Power control (shutdown, restart, wake-on-LAN) - Service status monitoring - Docker container management - Ollama model downloads with progress """ class MacMiniControlModule: """Mac Mini Remote Control Panel.""" MAC_MINI_IP = "192.168.178.100" MAC_MINI_USER = "benjaminadmin" @staticmethod def get_css() -> str: return """ /* ============================================ Mac Mini Control Panel ============================================ */ .mac-mini-dashboard { padding: 24px; max-width: 1400px; margin: 0 auto; } .mac-mini-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; } .mac-mini-header h1 { color: var(--bp-text, #e5e7eb); font-size: 28px; 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; } /* Power Controls */ .power-controls { display: flex; gap: 12px; margin-bottom: 24px; } .power-btn { padding: 12px 24px; border-radius: 8px; border: none; font-size: 14px; font-weight: 600; cursor: pointer; display: flex; align-items: center; gap: 8px; transition: all 0.2s; } .power-btn:disabled { opacity: 0.5; cursor: not-allowed; } .power-btn.wake { background: #22c55e; color: white; } .power-btn.wake:hover:not(:disabled) { background: #16a34a; } .power-btn.restart { background: #f59e0b; color: white; } .power-btn.restart:hover:not(:disabled) { background: #d97706; } .power-btn.shutdown { background: #ef4444; color: white; } .power-btn.shutdown:hover:not(:disabled) { background: #dc2626; } .power-btn.refresh { background: var(--bp-surface, #1e293b); color: var(--bp-text, #e5e7eb); border: 1px solid var(--bp-border, #334155); } .power-btn.refresh:hover:not(:disabled) { background: var(--bp-surface-elevated, #334155); } /* Status Grid */ .status-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 16px; margin-bottom: 24px; } .status-card { background: var(--bp-surface-elevated, #1e293b); border: 1px solid var(--bp-border, #334155); border-radius: 12px; padding: 20px; } .status-card h3 { color: var(--bp-text, #e5e7eb); font-size: 16px; margin: 0 0 16px 0; display: flex; align-items: center; gap: 8px; } .status-item { display: flex; justify-content: space-between; align-items: center; padding: 10px 0; border-bottom: 1px solid var(--bp-border, #334155); } .status-item:last-child { border-bottom: none; } .status-item-name { color: var(--bp-text-muted, #9ca3af); font-size: 14px; } .status-item-value { font-size: 14px; font-weight: 500; } .status-item-value.ok { color: #22c55e; } .status-item-value.error { color: #ef4444; } .status-item-value.warning { color: #fbbf24; } /* Docker Containers */ .container-list { display: flex; flex-direction: column; gap: 8px; } .container-item { display: flex; justify-content: space-between; align-items: center; padding: 12px 16px; background: var(--bp-surface, #0f172a); border-radius: 8px; border: 1px solid var(--bp-border, #334155); } .container-name { color: var(--bp-text, #e5e7eb); font-size: 14px; font-family: monospace; } .container-status { padding: 4px 12px; border-radius: 12px; font-size: 12px; font-weight: 600; } .container-status.healthy { background: rgba(34, 197, 94, 0.2); color: #22c55e; } .container-status.running { background: rgba(59, 130, 246, 0.2); color: #3b82f6; } .container-status.stopped { background: rgba(239, 68, 68, 0.2); color: #ef4444; } /* Ollama Section */ .ollama-section { background: var(--bp-surface-elevated, #1e293b); border: 1px solid var(--bp-border, #334155); border-radius: 12px; padding: 20px; margin-bottom: 24px; } .ollama-section h3 { color: var(--bp-text, #e5e7eb); font-size: 18px; margin: 0 0 16px 0; display: flex; align-items: center; gap: 8px; } .model-list { display: flex; flex-direction: column; gap: 12px; margin-bottom: 20px; } .model-item { display: flex; justify-content: space-between; align-items: center; padding: 16px; background: var(--bp-surface, #0f172a); border-radius: 8px; border: 1px solid var(--bp-border, #334155); } .model-info { display: flex; flex-direction: column; gap: 4px; } .model-name { color: var(--bp-text, #e5e7eb); font-size: 16px; font-weight: 600; } .model-details { color: var(--bp-text-muted, #9ca3af); font-size: 13px; } .model-size { color: var(--bp-primary, #6C1B1B); font-size: 14px; font-weight: 600; } /* Download Section */ .download-section { margin-top: 20px; padding-top: 20px; border-top: 1px solid var(--bp-border, #334155); } .download-input-row { display: flex; gap: 12px; margin-bottom: 16px; } .download-input { flex: 1; padding: 12px 16px; background: var(--bp-surface, #0f172a); border: 1px solid var(--bp-border, #334155); border-radius: 8px; color: var(--bp-text, #e5e7eb); font-size: 14px; } .download-input::placeholder { color: var(--bp-text-muted, #9ca3af); } .download-btn { padding: 12px 24px; background: var(--bp-primary, #6C1B1B); color: white; border: none; border-radius: 8px; font-size: 14px; font-weight: 600; cursor: pointer; transition: all 0.2s; } .download-btn:hover:not(:disabled) { background: #7f1d1d; } .download-btn:disabled { opacity: 0.5; cursor: not-allowed; } /* Download Progress */ .download-progress { display: none; margin-top: 16px; } .download-progress.active { display: block; } .progress-header { display: flex; justify-content: space-between; margin-bottom: 8px; } .progress-model { color: var(--bp-text, #e5e7eb); font-weight: 600; } .progress-stats { color: var(--bp-text-muted, #9ca3af); font-size: 13px; } .progress-bar-container { height: 24px; background: var(--bp-surface, #0f172a); border-radius: 12px; overflow: hidden; position: relative; } .progress-bar-fill { height: 100%; background: linear-gradient(90deg, var(--bp-primary, #6C1B1B), #991b1b); border-radius: 12px; transition: width 0.3s ease; position: relative; } .progress-bar-fill::after { content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: linear-gradient( 90deg, transparent, rgba(255,255,255,0.1), transparent ); animation: shimmer 1.5s infinite; } @keyframes shimmer { 0% { transform: translateX(-100%); } 100% { transform: translateX(100%); } } .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); } /* Log Output */ .log-output { 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; word-break: break-all; } /* Responsive */ @media (max-width: 768px) { .power-controls { flex-wrap: wrap; } .power-btn { flex: 1; min-width: 120px; justify-content: center; } } """ @staticmethod def get_html() -> str: return """

🖥️ Mac Mini Control

Prüfe...

🌐 Verbindung

IP-Adresse 192.168.178.163
SSH --
Ping --

⚙️ Services

Backend API --
Ollama --
Docker --

💻 System

Uptime --
CPU Load --
Memory --

🐳 Docker Container

Lade Container-Status...

🤖 Ollama LLM Modelle

Lade Modelle...

📥 Neues Modell herunterladen

-- -- / --
0%
""" @staticmethod def get_js() -> str: return """ // Mac Mini Control State let macMiniState = { ip: '192.168.178.163', isOnline: false, downloadInProgress: false, pollInterval: null }; // Initialize Mac Mini Control function initMacMiniControl() { macMiniRefreshStatus(); // Auto-refresh every 30 seconds macMiniState.pollInterval = setInterval(macMiniRefreshStatus, 30000); } // Refresh all status async function macMiniRefreshStatus() { const statusBadge = document.getElementById('mac-mini-overall-status'); 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(); macMiniState.isOnline = data.online; macMiniState.ip = data.ip || macMiniState.ip; // Update overall status 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 document.getElementById('mac-mini-ip').textContent = macMiniState.ip; // Update connection status updateStatusValue('status-ssh', data.ssh ? 'Verbunden' : 'Nicht verfügbar', data.ssh); updateStatusValue('status-ping', data.ping ? 'OK' : 'Timeout', data.ping); // Update services updateStatusValue('status-backend', data.backend ? 'Läuft' : 'Offline', data.backend); updateStatusValue('status-ollama', data.ollama ? 'Läuft' : 'Offline', data.ollama); updateStatusValue('status-docker', data.docker ? 'Läuft' : 'Offline', data.docker); // Update system info document.getElementById('status-uptime').textContent = data.uptime || '--'; document.getElementById('status-cpu').textContent = data.cpu_load || '--'; document.getElementById('status-memory').textContent = data.memory || '--'; // Update Docker containers updateDockerContainers(data.containers || []); // Update Ollama models updateOllamaModels(data.models || []); // Enable/disable buttons based on status document.getElementById('btn-wake').disabled = data.online; document.getElementById('btn-restart').disabled = !data.online; document.getElementById('btn-shutdown').disabled = !data.online; } catch (error) { console.error('Error fetching Mac Mini status:', error); statusBadge.className = 'mac-mini-status-badge offline'; statusBadge.textContent = 'Fehler'; } } function updateStatusValue(elementId, text, isOk) { const el = document.getElementById(elementId); el.textContent = text; el.className = 'status-item-value ' + (isOk ? 'ok' : 'error'); } function updateDockerContainers(containers) { const list = document.getElementById('docker-container-list'); if (containers.length === 0) { list.innerHTML = '
Keine Container gefunden
'; return; } list.innerHTML = containers.map(c => { const statusClass = c.status.includes('healthy') ? 'healthy' : c.status.includes('Up') ? 'running' : 'stopped'; return `
${c.name} ${c.status}
`; }).join(''); } function updateOllamaModels(models) { const list = document.getElementById('ollama-model-list'); if (models.length === 0) { list.innerHTML = '
Keine Modelle installiert
'; return; } list.innerHTML = models.map(m => { const sizeGB = (m.size / (1024 * 1024 * 1024)).toFixed(1); return `
${m.name} ${m.details?.parameter_size || ''} | ${m.details?.quantization_level || ''}
${sizeGB} GB
`; }).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(macMiniRefreshStatus, 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(macMiniRefreshStatus, 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(macMiniRefreshStatus, 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(macMiniRefreshStatus, 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(macMiniRefreshStatus, 5000); } catch (error) { alert('Fehler: ' + error.message); } } // Ollama Model Download async function macMiniPullModel() { const input = document.getElementById('model-download-input'); const modelName = input.value.trim(); if (!modelName) { alert('Bitte Modellnamen eingeben'); return; } if (macMiniState.downloadInProgress) { alert('Download läuft bereits'); return; } macMiniState.downloadInProgress = true; document.getElementById('btn-pull-model').disabled = true; const progressDiv = document.getElementById('download-progress'); const progressBar = document.getElementById('download-progress-bar'); const progressText = document.getElementById('download-progress-text'); const downloadStats = document.getElementById('download-stats'); const downloadLog = document.getElementById('download-log'); const modelNameEl = document.getElementById('download-model-name'); progressDiv.classList.add('active'); modelNameEl.textContent = modelName; downloadLog.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) { downloadLog.textContent += data.status + '\\n'; downloadLog.scrollTop = downloadLog.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); progressBar.style.width = percent + '%'; progressText.textContent = percent + '%'; downloadStats.textContent = `${completedMB} MB / ${totalMB} MB`; } if (data.status === 'success') { downloadLog.textContent += '\\n✅ Download abgeschlossen!\\n'; progressBar.style.width = '100%'; progressText.textContent = '100%'; } } catch (e) { // Not JSON, just log it downloadLog.textContent += line + '\\n'; } } } // Refresh models list setTimeout(macMiniRefreshStatus, 2000); } catch (error) { downloadLog.textContent += '\\n❌ Fehler: ' + error.message + '\\n'; } finally { macMiniState.downloadInProgress = false; document.getElementById('btn-pull-model').disabled = false; } } // Cleanup on panel hide function cleanupMacMiniControl() { if (macMiniState.pollInterval) { clearInterval(macMiniState.pollInterval); macMiniState.pollInterval = null; } } """ @classmethod def render(cls) -> str: return f""" {cls.get_html()} """