import { useState, useEffect } from 'react' import { Server, RefreshCw, Copy, CheckCircle, Trash2, Download } from 'lucide-react' import { api } from '../lib/api' import { simdWarnung } from '../lib/encoder' import { useToast } from '../context/ToastContext' import { Card, CardHeader, CardTitle, CardContent } from './ui/Card' import { Button } from './ui/Button' import { Input } from './ui/Input' import { useStrom } from '../lib/useEventStream' interface WorkerInfo { name: string encoders: string[] last_seen?: string online?: boolean info?: { hostname?: string ip?: string makemkv?: string handbrake?: string cpu_modell?: string cpu_kerne?: string cpu_simd?: string handbrake_encoder?: string rippy_version?: string } } const ENCODER_LABELS: Record = { 'cpu-x264': 'CPU · H.264', 'cpu-x265': 'CPU · H.265', 'cpu-av1': 'CPU · AV1 (SVT)', '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 [serverVersion, setServerVersion] = useState('dev') const [kopiert, setKopiert] = useState(false) const [variante, setVariante] = useState<'linux' | 'windows'>('linux') // BEWUSST leer: kein window.location.hostname-Default. Der füllte früher // die Adresse, über die man das UI geöffnet hat (z. B. den eigenen PC hinter // einem Proxy/Forward) — und die ist NICHT zwingend die direkte LAN-IP der // Rippy-Maschine, die der Worker für Redis/Postgres braucht. Lieber bewusst // eintragen lassen als eine falsche Adresse vorgaukeln. const [rippyHost, setRippyHost] = useState('') const { toast } = useToast() const strom = useStrom() // Für die Befehle: leeres Feld → sichtbarer Platzhalter (Befehl ist dann // erkennbar unvollständig, statt still auf die falsche Adresse zu zeigen). const hostFuerBefehl = rippyHost || '' const siehtOeffentlichAus = rippyHost !== '' && !/^(\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 (r.data.server_version) setServerVersion(r.data.server_version) if (manuell) toast('success', 'Worker-Liste aktualisiert') }) .catch(() => { if (manuell) toast('error', 'Worker-Liste konnte nicht geladen werden') }) } // Die Worker-Liste kommt aus dem Live-Strom. /capabilities wird nur noch // EINMAL geholt — dort steht ausser der Liste die Server-Version, und die // aendert sich nur beim Deploy. Vorher lief der Abruf alle 15 s. useEffect(() => { laden() }, []) useEffect(() => { if (strom.workers.length || strom.verbunden) { setWorkers(strom.workers as WorkerInfo[]) } }, [strom.workers, strom.verbunden]) const installBefehl = variante === 'linux' ? [ `git clone rippy && cd rippy`, `RIPPY_HOST=${hostFuerBefehl} WORKER_NAME=mein-pc \\`, ` docker compose -f deploy/remote-transcode-worker.yml up -d --build`, ].join('\n') : [ // Standard-Ziel ist "C:\Program Files\Rippy Worker" - dorthin darf nur // ein Administrator schreiben, deshalb der Hinweis direkt im Befehl. // Die .exe fragt selbst per UAC; hier muss die Konsole erhöht sein. `# PowerShell als Administrator starten (Standard-Ziel: Programme)`, `iwr http://${hostFuerBefehl}/api/worker-setup/windows -OutFile install-rippy-worker.ps1`, `powershell -ExecutionPolicy Bypass -File .\\install-rippy-worker.ps1 \``, ` -RippyHost ${hostFuerBefehl} -WorkerName mein-pc`, `# Ohne Adminrechte: -InstallDir "$env:LOCALAPPDATA\\Rippy Worker" anhaengen`, ].join('\n') const kopieren = () => { navigator.clipboard.writeText(installBefehl).then(() => { setKopiert(true) setTimeout(() => setKopiert(false), 2000) }) } // Fertige Installer-.exe (Rippy-Icon, kein Konsolenfenster) direkt laden. // Generisch — die GUI fragt die Rippy-Adresse selbst ab. const exeHerunterladen = () => { const a = document.createElement('a') a.href = '/api/worker-setup/windows-exe' a.download = 'RippyWorkerSetup.exe' document.body.appendChild(a); a.click(); a.remove() toast('success', 'RippyWorkerSetup.exe wird heruntergeladen — doppelklicken zum Installieren') } 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.info?.rippy_version && (

Version {w.info.rippy_version} {w.info.rippy_version !== serverVersion ? `(Veraltet! Server hat ${serverVersion})` : ''}

)} {(w.info?.cpu_kerne || w.info?.cpu_simd) && (

{w.info?.cpu_kerne ? `${w.info.cpu_kerne} Kerne` : ''} {w.info?.cpu_kerne && w.info?.cpu_simd ? ' · ' : ''} {w.info?.cpu_simd ? `Vektorbefehle ${w.info.cpu_simd}` : ''}

)} {simdWarnung(w.info?.cpu_simd) && (

{simdWarnung(w.info?.cpu_simd)}

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

Die IP der Maschine, auf der Rippy selbst (Docker) läuft — meist deine Server-/NAS-VM. Nicht die IP des PCs, auf dem der Worker läuft, und auch dann diese direkte LAN-IP eintragen, wenn du dieses UI über eine andere Adresse (Domain/Proxy/Portweiterleitung) geöffnet hast. Der Worker spricht Redis/Postgres direkt an (Ports 6379/5432) — das klappt nur mit der echten LAN-IP im Heimnetz.

{siehtOeffentlichAus && (

⚠️ Das sieht nach einer externen Domain aus (Reverse-Proxy) — die geht nicht. Bitte die LAN-IP der Rippy-Maschine eintragen — die beginnt im Heimnetz fast immer mit 192.168., 10. oder 172.16.–172.31.

)}
{([['linux', '🐧 Linux (Docker)'], ['windows', '🪟 Windows (nativ, ohne Docker)']] as const).map(([wert, label]) => ( ))}
{/* Windows: fertige .exe als Haupt-Weg (Doppelklick, Icon, kein Konsolenfenster) */} {variante === 'windows' && (

Einfachster Weg: Installer (.exe) herunterladen, doppelklicken, im Fenster Adresse + Name eintragen, „Installieren". Kein Konsolenfenster. Braucht nur Python 3.10+ auf dem PC — kein Docker, kein git.

Im Installer-Fenster als Rippy-Adresse eintragen: {rippyHost.trim() || ''}

Windows warnt beim ersten Start evtl. („Windows hat Ihren PC geschützt", unbekannter Herausgeber) → „Weitere Informationen" → „Trotzdem ausführen". Die .exe holt Worker-Code und HandBrake nur von deiner Rippy-Maschine bzw. dem offiziellen HandBrake-Release.

)}

{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.' : 'Alternativ (für Profis) per PowerShell-Befehl:'}

{installBefehl}

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

) }