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>
877 lines
23 KiB
Python
877 lines
23 KiB
Python
"""
|
|
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 """
|
|
<div class="mac-mini-dashboard" id="mac-mini-dashboard">
|
|
<!-- Header -->
|
|
<div class="mac-mini-header">
|
|
<h1>
|
|
<span style="font-size: 32px;">🖥️</span>
|
|
Mac Mini Control
|
|
</h1>
|
|
<span class="mac-mini-status-badge checking" id="mac-mini-overall-status">
|
|
Prüfe...
|
|
</span>
|
|
</div>
|
|
|
|
<!-- Power Controls -->
|
|
<div class="power-controls">
|
|
<button class="power-btn wake" onclick="macMiniWake()" id="btn-wake">
|
|
⚡ Wake on LAN
|
|
</button>
|
|
<button class="power-btn restart" onclick="macMiniRestart()" id="btn-restart">
|
|
🔄 Neustart
|
|
</button>
|
|
<button class="power-btn shutdown" onclick="macMiniShutdown()" id="btn-shutdown">
|
|
⏻ Herunterfahren
|
|
</button>
|
|
<button class="power-btn refresh" onclick="macMiniRefreshStatus()">
|
|
🔍 Status aktualisieren
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Status Grid -->
|
|
<div class="status-grid">
|
|
<!-- Connection Status -->
|
|
<div class="status-card">
|
|
<h3>🌐 Verbindung</h3>
|
|
<div class="status-item">
|
|
<span class="status-item-name">IP-Adresse</span>
|
|
<span class="status-item-value" id="mac-mini-ip">192.168.178.163</span>
|
|
</div>
|
|
<div class="status-item">
|
|
<span class="status-item-name">SSH</span>
|
|
<span class="status-item-value" id="status-ssh">--</span>
|
|
</div>
|
|
<div class="status-item">
|
|
<span class="status-item-name">Ping</span>
|
|
<span class="status-item-value" id="status-ping">--</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Services Status -->
|
|
<div class="status-card">
|
|
<h3>⚙️ Services</h3>
|
|
<div class="status-item">
|
|
<span class="status-item-name">Backend API</span>
|
|
<span class="status-item-value" id="status-backend">--</span>
|
|
</div>
|
|
<div class="status-item">
|
|
<span class="status-item-name">Ollama</span>
|
|
<span class="status-item-value" id="status-ollama">--</span>
|
|
</div>
|
|
<div class="status-item">
|
|
<span class="status-item-name">Docker</span>
|
|
<span class="status-item-value" id="status-docker">--</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- System Info -->
|
|
<div class="status-card">
|
|
<h3>💻 System</h3>
|
|
<div class="status-item">
|
|
<span class="status-item-name">Uptime</span>
|
|
<span class="status-item-value" id="status-uptime">--</span>
|
|
</div>
|
|
<div class="status-item">
|
|
<span class="status-item-name">CPU Load</span>
|
|
<span class="status-item-value" id="status-cpu">--</span>
|
|
</div>
|
|
<div class="status-item">
|
|
<span class="status-item-name">Memory</span>
|
|
<span class="status-item-value" id="status-memory">--</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Docker Containers -->
|
|
<div class="status-card" style="margin-bottom: 24px;">
|
|
<h3>🐳 Docker Container</h3>
|
|
<div class="container-list" id="docker-container-list">
|
|
<div style="color: var(--bp-text-muted); text-align: center; padding: 20px;">
|
|
Lade Container-Status...
|
|
</div>
|
|
</div>
|
|
<div style="margin-top: 16px; display: flex; gap: 8px;">
|
|
<button class="power-btn refresh" onclick="macMiniDockerUp()" style="flex: 1;">
|
|
▶️ Container starten
|
|
</button>
|
|
<button class="power-btn refresh" onclick="macMiniDockerDown()" style="flex: 1;">
|
|
⏹️ Container stoppen
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Ollama Section -->
|
|
<div class="ollama-section">
|
|
<h3>🤖 Ollama LLM Modelle</h3>
|
|
|
|
<div class="model-list" id="ollama-model-list">
|
|
<div style="color: var(--bp-text-muted); text-align: center; padding: 20px;">
|
|
Lade Modelle...
|
|
</div>
|
|
</div>
|
|
|
|
<div class="download-section">
|
|
<h4 style="color: var(--bp-text); margin: 0 0 12px 0;">📥 Neues Modell herunterladen</h4>
|
|
<div class="download-input-row">
|
|
<input type="text" class="download-input" id="model-download-input"
|
|
placeholder="Modellname (z.B. llama3.2, mistral, qwen2.5:7b)">
|
|
<button class="download-btn" onclick="macMiniPullModel()" id="btn-pull-model">
|
|
Herunterladen
|
|
</button>
|
|
</div>
|
|
|
|
<div class="download-progress" id="download-progress">
|
|
<div class="progress-header">
|
|
<span class="progress-model" id="download-model-name">--</span>
|
|
<span class="progress-stats" id="download-stats">-- / --</span>
|
|
</div>
|
|
<div class="progress-bar-container">
|
|
<div class="progress-bar-fill" id="download-progress-bar" style="width: 0%"></div>
|
|
<span class="progress-text" id="download-progress-text">0%</span>
|
|
</div>
|
|
<div class="log-output" id="download-log"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
"""
|
|
|
|
@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 = '<div style="color: var(--bp-text-muted); text-align: center; padding: 20px;">Keine Container gefunden</div>';
|
|
return;
|
|
}
|
|
|
|
list.innerHTML = containers.map(c => {
|
|
const statusClass = c.status.includes('healthy') ? 'healthy' :
|
|
c.status.includes('Up') ? 'running' : 'stopped';
|
|
return `
|
|
<div class="container-item">
|
|
<span class="container-name">${c.name}</span>
|
|
<span class="container-status ${statusClass}">${c.status}</span>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
}
|
|
|
|
function updateOllamaModels(models) {
|
|
const list = document.getElementById('ollama-model-list');
|
|
|
|
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);
|
|
return `
|
|
<div class="model-item">
|
|
<div class="model-info">
|
|
<span class="model-name">${m.name}</span>
|
|
<span class="model-details">${m.details?.parameter_size || ''} | ${m.details?.quantization_level || ''}</span>
|
|
</div>
|
|
<span class="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(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"""
|
|
<style>{cls.get_css()}</style>
|
|
{cls.get_html()}
|
|
<script>{cls.get_js()}</script>
|
|
"""
|