import { useState, useEffect } from 'react' import { Server, RefreshCw, Copy, CheckCircle, Trash2 } from 'lucide-react' import { api } from '../lib/api' import { useToast } from '../context/ToastContext' import { Card, CardHeader, CardTitle, CardContent } from './ui/Card' import { Button } from './ui/Button' import { Input } from './ui/Input' interface WorkerInfo { name: string encoders: string[] last_seen?: string online?: boolean info?: { hostname?: string ip?: string makemkv?: string handbrake?: string } } const ENCODER_LABELS: Record = { 'cpu-x264': 'CPU · H.264', 'cpu-x265': 'CPU · H.265', 'vaapi': 'GPU · VAAPI (AMD/Intel)', 'nvenc': 'GPU · NVENC (NVIDIA)', } function relativeZeit(iso?: string): string { if (!iso) return 'nie' const sekunden = Math.floor((Date.now() - new Date(iso).getTime()) / 1000) if (sekunden < 90) return 'gerade eben' if (sekunden < 3600) return `vor ${Math.floor(sekunden / 60)} min` return `vor ${Math.floor(sekunden / 3600)} h` } export default function WorkerVerwaltung() { const [workers, setWorkers] = useState([]) const [kopiert, setKopiert] = useState(false) const [variante, setVariante] = useState<'linux' | 'windows'>('linux') const [rippyHost, setRippyHost] = useState(window.location.hostname) const { toast } = useToast() const siehtOeffentlichAus = !/^(\d{1,3}\.){3}\d{1,3}$/.test(rippyHost) && !rippyHost.endsWith('.local') && rippyHost !== 'localhost' && rippyHost.includes('.') const laden = (manuell = false) => { api.get('/capabilities') .then(r => { setWorkers(r.data.workers || []) if (manuell) toast('success', 'Worker-Liste aktualisiert') }) .catch(() => { if (manuell) toast('error', 'Worker-Liste konnte nicht geladen werden') }) } useEffect(() => { laden() const interval = setInterval(laden, 15000) return () => clearInterval(interval) }, []) const installBefehl = variante === 'linux' ? [ `git clone rippy && cd rippy`, `RIPPY_HOST=${rippyHost} WORKER_NAME=mein-pc \\`, ` docker compose -f deploy/remote-transcode-worker.yml up -d --build`, ].join('\n') : [ `iwr http://${rippyHost}/api/worker-setup/windows -OutFile install-rippy-worker.ps1`, `powershell -ExecutionPolicy Bypass -File .\\install-rippy-worker.ps1 \``, ` -RippyHost ${rippyHost} -WorkerName mein-pc`, ].join('\n') const kopieren = () => { navigator.clipboard.writeText(installBefehl).then(() => { setKopiert(true) setTimeout(() => setKopiert(false), 2000) }) } return (
{/* Verbundene Worker */} Verbundene Worker {workers.length === 0 ? (

Noch kein Worker gemeldet.

) : (
{workers.map(w => (

{w.name}

{w.online ? 'online' : 'offline'} · Lebenszeichen {relativeZeit(w.last_seen)} {w.info?.ip ? ` · IP ${w.info.ip}` : ''} {w.info?.hostname && w.info.hostname !== w.name ? ` · ID ${w.info.hostname}` : ''}

{w.encoders.map(e => ( {ENCODER_LABELS[e] || e} ))}
{!w.online && ( )}
))}
)}
{/* Neue Maschine anbinden */} Maschine als Encoding-Worker anbinden
setRippyHost(e.target.value.trim())} placeholder="192.168.178.162" /> {siehtOeffentlichAus && (

⚠️ Das sieht nach einer externen Domain aus (Reverse-Proxy). Worker verbinden sich direkt mit Redis/Postgres (Ports 6379/5432) — das geht nur im Heimnetz. Hier die LAN-IP eintragen, z. B. 192.168.178.162.

)}
{([['linux', '🐧 Linux (Docker)'], ['windows', '🪟 Windows (nativ, ohne Docker)']] as const).map(([wert, label]) => ( ))}

{variante === 'linux' ? 'Auf der Linux-Maschine (mit Docker) ausführen — WORKER_NAME frei wählen, unter dem Namen taucht sie oben auf und übernimmt Kompressions-Jobs. GPU wird erkannt und angezeigt.' : 'In einer PowerShell auf dem Windows-PC ausführen (braucht nur Python 3.10+ — kein Docker, kein git). Der Installer holt Worker-Code und HandBrake automatisch.'}

{installBefehl}

Beide Varianten bedienen nur die Kompressions-Queue — gerippt wird immer hier, wo das Laufwerk hängt.

) }