feat(ui): die Oberflaeche weiss jetzt, worauf sie laeuft
Commander-Befund 28.08.2026, zum Windows-Fenster:
Worker erreichbar: 0 von 1
Kein Worker antwortet — Pruefen: docker compose ps
Container-Platte: unbekannt
Freigaben: keine eingehaengt
Kein Satz davon ergibt auf einem Windows-PC einen Sinn. Sein Urteil: "Du hast
ja quasi nur rippy genommen und die docker installation fuer Windows gebaut.
Das gilt fuer die ganze standalone version fuer Windows, auch fuer die
settings und die Anleitung usw."
## Die Ursache war nicht die Anzeige
Die naheliegende Reparatur waere ein `if (windows)` an dreissig Stellen
gewesen -- dieselbe Falle noch einmal, nur mit einer zweiten Sorte Vermutung.
Es fehlte etwas anderes: Das UI hat nie erfahren, worauf es laeuft. config.py
kennt das Profil seit V2-2, weitergegeben wurde es nie. Also hat das UI
angenommen.
Neu: GET /betrieb meldet FAEHIGKEITEN, keinen Modus-Namen.
externe_worker Gibt es andere Maschinen, die Jobs uebernehmen?
freigaben_einhaengen Kann Rippy Netzwerk-Freigaben selbst einhaengen?
container_pfade Sind Pfade wie /app/media ueberhaupt gemeint?
werkzeuge_verwalten Kann Rippy MakeMKV/HandBrake selbst beschaffen?
Ein Modus-Name wuerde das UI zwingen, aus einem Namen auf Verhalten zu
schliessen -- und das bricht beim naechsten Betriebsfall: Ein
Docker-All-in-One hat Container-Pfade, aber keinen zweiten Worker.
## Was sich sichtbar aendert (auf Windows nachgemessen)
Server-Status "Rippy arbeitet: auf diesem Rechner" statt Worker-Zaehler
"Platz fuer Rippy: 59.2 von 232 GB frei" statt "unbekannt"
Freigaben-Block faellt weg
Einstellungen kein Reiter "Worker"; Speicherziele BLEIBT (dort steht die
Ablage -- "wo will ich das hinspeichern" war die Frage),
aber mit Pfadfeld statt Container-Auswahl und ohne die
Maske zum Einhaengen
Anleitung kein docker, kein Encoding-Worker-Abschnitt, stattdessen
der echte Ordner (C:\Users\...\Videos\Rippy)
## Zwei Fehler, die dabei aufgefallen sind
* MEDIA_ROOT = "/app/media" war in main.py fest verdrahtet. shutil.disk_usage
warf unter Windows, die Liste blieb leer -- daher "unbekannt", obwohl auf
dem Laufwerk 59 GB frei waren. Eine Nichtauskunft, die wie eine Auskunft
aussieht. platz_orte() liefert die Orte jetzt je Betrieb, und ein noch
nicht angelegter Ordner faellt auf das naechste vorhandene Elternteil
zurueck.
* Der SSE-Schnappschuss enthielt den Server-Zustand NICHT, und der Waechter
schickt ihn nur alle 15 Sekunden. Nach jedem Neuladen stand deshalb bis zu
eine Viertelminute "unbekannt" da. Jetzt ist er im Schnappschuss, und das
UI uebernimmt ihn auch von dort.
Dazu: der Windows-Skip in test_api_smoke.py ist weg. Er stammte aus der Zeit
vor V2-4, als main.py fcntl brauchte; seit der Treiberwahl ueber den Port
laedt es auf beiden Plattformen (57 Routen, gemessen). Damit laufen 20 Tests
mehr auch lokal statt nur auf der Ampel.
Ampel lokal: 654 gruen, ruff sauber. Docker-Zweig durch Unit-Tests gedeckt,
am echten Container noch nicht gegengeprueft -- das kommt beim Deploy.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
a5e64f8e56
commit
66642b8d93
@@ -7,6 +7,7 @@ import AnleitungPage from './pages/Anleitung'
|
||||
import FirstRunWizard from './components/FirstRunWizard'
|
||||
import { api } from './lib/api'
|
||||
import { EventStreamProvider, useStrom } from './lib/useEventStream'
|
||||
import { BetriebProvider } from './lib/useBetrieb'
|
||||
|
||||
type Page = 'dashboard' | 'anleitung' | 'logs' | 'settings'
|
||||
|
||||
@@ -123,9 +124,14 @@ export default function App() {
|
||||
// Der Provider haelt GENAU EINE SSE-Verbindung fuer die ganze Anwendung.
|
||||
// Ein Hook je Komponente waere eine Verbindung je Komponente — also das
|
||||
// alte Problem in neuer Form.
|
||||
// BetriebProvider ganz aussen: Was Rippy anzeigen DARF, entscheidet sich
|
||||
// vor allem anderen. Ohne ihn zeigte das Windows-Fenster "Pruefen: docker
|
||||
// compose ps" — einen Rat, den dort niemand befolgen kann.
|
||||
return (
|
||||
<EventStreamProvider>
|
||||
<AppInhalt />
|
||||
</EventStreamProvider>
|
||||
<BetriebProvider>
|
||||
<EventStreamProvider>
|
||||
<AppInhalt />
|
||||
</EventStreamProvider>
|
||||
</BetriebProvider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
* Worauf läuft Rippy — und was kann dieser Betrieb überhaupt?
|
||||
*
|
||||
* ## Der Befund des Commanders (28.08.2026)
|
||||
*
|
||||
* Im Windows-Fenster stand auf der Server-Status-Kachel:
|
||||
*
|
||||
* Worker erreichbar: 0 von 1
|
||||
* Kein Worker antwortet — ohne ihn läuft kein Rip. Prüfen: docker compose ps
|
||||
* Container-Platte: unbekannt
|
||||
* Freigaben: keine eingehängt
|
||||
*
|
||||
* Kein Satz davon ergibt auf einem Windows-PC einen Sinn. Es gibt keinen
|
||||
* Container, kein `docker compose`, keinen zweiten Worker — Rippy rippt dort
|
||||
* selbst. Sein Urteil: „Du hast ja quasi nur rippy genommen und die docker
|
||||
* installation für Windows gebaut."
|
||||
*
|
||||
* ## Warum ein Provider und kein Abruf je Seite
|
||||
*
|
||||
* Dieselbe Begründung wie beim Ereignis-Strom nebenan: Vier Seiten, die
|
||||
* dasselbe abfragen, sind vier Abrufe — und vier Gelegenheiten, dass eine
|
||||
* davon einen anderen Stand hat als die anderen. Der Betrieb ändert sich zur
|
||||
* Laufzeit nicht; er wird EINMAL geholt.
|
||||
*
|
||||
* ## Warum die Vorgabe „Docker" ist
|
||||
*
|
||||
* Solange die Antwort noch unterwegs ist, muss irgendetwas gelten. Die
|
||||
* Docker-Annahme ist hier die richtige Vorgabe — sie zeigt MEHR, und ein kurz
|
||||
* zu viel angezeigter Bereich ist harmloser als ein Bedienelement, das für
|
||||
* einen Augenblick verschwindet und wieder auftaucht.
|
||||
*
|
||||
* Ein FEHLGESCHLAGENER Abruf ist etwas anderes als „noch unterwegs":
|
||||
* `geladen` bleibt dann false, und wer das wissen will, kann es abfragen. Ein
|
||||
* Verbindungsabriss ist keine Aussage über die Welt (siehe useEventStream).
|
||||
*/
|
||||
import { createContext, useContext, useEffect, useState, type ReactNode } from 'react'
|
||||
import { api } from './api'
|
||||
|
||||
export interface BetriebsFaehigkeiten {
|
||||
/** Gibt es andere Maschinen, die Jobs übernehmen? */
|
||||
externe_worker: boolean
|
||||
/** Kann Rippy Netzwerk-Freigaben selbst einhängen? */
|
||||
freigaben_einhaengen: boolean
|
||||
/** Sind Pfade wie /app/media überhaupt gemeint? */
|
||||
container_pfade: boolean
|
||||
/** Kann Rippy MakeMKV/HandBrake selbst beschaffen? */
|
||||
werkzeuge_verwalten: boolean
|
||||
}
|
||||
|
||||
export interface Betrieb {
|
||||
modus: 'standalone' | 'verteilt'
|
||||
plattform: 'windows' | 'linux' | 'macos'
|
||||
im_container: boolean
|
||||
kann: BetriebsFaehigkeiten
|
||||
/** Womit ein Ablage-Feld vorbelegt wird — je Betrieb ein anderer Ort. */
|
||||
ablage_vorgabe: string
|
||||
/** Der Befehl zum Nachsehen. LEER heißt: es gibt keinen, den der Nutzer
|
||||
* ausführen könnte — dann darf auch keiner dastehen. */
|
||||
hilfe_befehl: string
|
||||
/** Ist die Auskunft schon da? False heißt „noch unterwegs ODER nicht
|
||||
* erreichbar" — nicht „es gibt keinen Betrieb". */
|
||||
geladen: boolean
|
||||
}
|
||||
|
||||
const VORGABE: Betrieb = {
|
||||
modus: 'verteilt',
|
||||
plattform: 'linux',
|
||||
im_container: true,
|
||||
kann: {
|
||||
externe_worker: true,
|
||||
freigaben_einhaengen: true,
|
||||
container_pfade: true,
|
||||
werkzeuge_verwalten: false,
|
||||
},
|
||||
ablage_vorgabe: '/app/media',
|
||||
hilfe_befehl: 'docker compose -p rippy ps',
|
||||
geladen: false,
|
||||
}
|
||||
|
||||
const BetriebContext = createContext<Betrieb>(VORGABE)
|
||||
|
||||
export function BetriebProvider({ children }: { children: ReactNode }) {
|
||||
const [betrieb, setBetrieb] = useState<Betrieb>(VORGABE)
|
||||
|
||||
useEffect(() => {
|
||||
let abgemeldet = false
|
||||
api.get('/betrieb')
|
||||
.then(antwort => {
|
||||
const d = antwort.data
|
||||
// Nur übernehmen, was WIRKLICH ankommt. Ein halb gefülltes Objekt
|
||||
// hieße in JavaScript `undefined` — und `undefined` ist falsch, also
|
||||
// verschwände ein Bereich stillschweigend.
|
||||
if (abgemeldet || !d || !d.kann) return
|
||||
setBetrieb({
|
||||
modus: d.modus === 'verteilt' ? 'verteilt' : 'standalone',
|
||||
plattform: d.plattform || 'linux',
|
||||
im_container: !!d.im_container,
|
||||
kann: {
|
||||
externe_worker: !!d.kann.externe_worker,
|
||||
freigaben_einhaengen: !!d.kann.freigaben_einhaengen,
|
||||
container_pfade: !!d.kann.container_pfade,
|
||||
werkzeuge_verwalten: !!d.kann.werkzeuge_verwalten,
|
||||
},
|
||||
ablage_vorgabe: d.ablage_vorgabe || '',
|
||||
hilfe_befehl: d.hilfe_befehl || '',
|
||||
geladen: true,
|
||||
})
|
||||
})
|
||||
.catch(() => {
|
||||
// Nichts tun. Ein misslungener Abruf ist keine Aussage über den
|
||||
// Betrieb — die Vorgabe bleibt stehen, `geladen` bleibt false.
|
||||
})
|
||||
return () => { abgemeldet = true }
|
||||
}, [])
|
||||
|
||||
return <BetriebContext.Provider value={betrieb}>{children}</BetriebContext.Provider>
|
||||
}
|
||||
|
||||
export function useBetrieb(): Betrieb {
|
||||
return useContext(BetriebContext)
|
||||
}
|
||||
|
||||
/**
|
||||
* Kurzform für den häufigsten Fall: Läuft Rippy als eigenständige App
|
||||
* (Windows-Client oder Docker-All-in-One)?
|
||||
*
|
||||
* Bewusst NICHT „ist Windows": Ein Docker-All-in-One hat auch keinen zweiten
|
||||
* Worker. Wer nach der Plattform fragt, obwohl er die Fähigkeit meint, baut
|
||||
* die nächste falsche Annahme ein.
|
||||
*/
|
||||
export function useAlleinbetrieb(): boolean {
|
||||
return !useBetrieb().kann.externe_worker
|
||||
}
|
||||
@@ -123,6 +123,17 @@ export function EventStreamProvider({ children }: { children: ReactNode }) {
|
||||
devices: d.devices === undefined ? null : d.devices,
|
||||
workers: d.workers ?? [],
|
||||
logs: d.logs ?? [],
|
||||
// ⚠️ Server-Zustand MIT übernehmen (Befund 28.08.2026).
|
||||
//
|
||||
// Er stand hier nicht, obwohl der Schnappschuss ihn liefert — und
|
||||
// `system.status` kommt erst nach bis zu 15 Sekunden. Nach jedem
|
||||
// Neuladen stand deshalb „Platz für Rippy: unbekannt" auf dem
|
||||
// Bildschirm. Das sieht aus wie eine Auskunft und ist keine.
|
||||
//
|
||||
// `undefined` heißt weiterhin „nicht mitgeschickt": dann bleibt der
|
||||
// alte Stand, statt auf leer zu fallen.
|
||||
systemInfo: d.info !== undefined ? d.info : null,
|
||||
ablagen: d.ablagen !== undefined ? d.ablagen : null,
|
||||
verbunden: true,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,6 +2,7 @@ import { ReactNode } from 'react'
|
||||
import { Disc, Tv, HardDrive, Bell, Wrench, Cpu, Download, HelpCircle } from 'lucide-react'
|
||||
import { PageHeader } from '../components/ui/PageHeader'
|
||||
import { Card, CardHeader, CardTitle, CardContent } from '../components/ui/Card'
|
||||
import { useBetrieb } from '../lib/useBetrieb'
|
||||
|
||||
function Abschnitt({ icon: Icon, titel, children }: { icon: any, titel: string, children: ReactNode }) {
|
||||
return (
|
||||
@@ -21,6 +22,18 @@ function Abschnitt({ icon: Icon, titel, children }: { icon: any, titel: string,
|
||||
|
||||
export default function AnleitungPage() {
|
||||
const fett = "font-semibold text-slate-100"
|
||||
/*
|
||||
* Auch die Anleitung haengt am Betrieb.
|
||||
*
|
||||
* Commander-Befund 28.08.2026: „Das gilt fuer die ganze standalone version
|
||||
* fuer Windows, auch fuer die settings und die Anleitung usw."
|
||||
*
|
||||
* Eine Anleitung, die einem Windows-Nutzer erklaert, wie er Freigaben
|
||||
* einhaengt und weitere Encoding-Worker aufsetzt, beschreibt ein Programm,
|
||||
* das er nicht hat. Das ist schlimmer als gar keine Anleitung: Sie laesst
|
||||
* ihn glauben, er habe etwas falsch gemacht.
|
||||
*/
|
||||
const betrieb = useBetrieb()
|
||||
|
||||
return (
|
||||
<div className="space-y-6 max-w-6xl mx-auto">
|
||||
@@ -67,18 +80,28 @@ export default function AnleitungPage() {
|
||||
</p>
|
||||
</Abschnitt>
|
||||
|
||||
<Abschnitt icon={HardDrive} titel="NAS & Speicherziele">
|
||||
<Abschnitt icon={HardDrive} titel="Wohin die Filme kommen">
|
||||
{betrieb.kann.freigaben_einhaengen ? (
|
||||
<p>
|
||||
Einstellungen → Speicherziele: Rechner/NAS eintragen, <span className={fett}>Benutzer +
|
||||
Passwort</span> angeben (Windows und die meisten NAS lehnen Gast-Zugriffe ab!), „Freigaben
|
||||
auflisten", Freigabe wählen, „Einhängen & speichern". Danach taucht das Ziel im
|
||||
„Rippen starten"-Dialog auf und wird beim Start automatisch wieder verbunden.
|
||||
</p>
|
||||
) : (
|
||||
<p>
|
||||
Einstellungen → Speicherziele: Bei <span className={fett}>Ablage</span> steht der Ordner,
|
||||
in dem die fertigen Filme landen. Das darf ein normaler Ordner sein
|
||||
{betrieb.ablage_vorgabe ? <> (Vorgabe: <code>{betrieb.ablage_vorgabe}</code>)</> : null} oder
|
||||
ein Netzwerkpfad wie <code>\NAS\Filme</code> — Rippy schreibt einfach dorthin.
|
||||
Ein Einhängen wie unter Linux gibt es hier nicht und ist auch nicht nötig.
|
||||
</p>
|
||||
)}
|
||||
<p>
|
||||
Einstellungen → Speicherziele: Rechner/NAS eintragen, <span className={fett}>Benutzer +
|
||||
Passwort</span> angeben (Windows und die meisten NAS lehnen Gast-Zugriffe ab!), „Freigaben
|
||||
auflisten", Freigabe wählen, „Einhängen & speichern". Danach taucht das Ziel im
|
||||
„Rippen starten"-Dialog auf und wird beim Start automatisch wieder verbunden.
|
||||
</p>
|
||||
<p>
|
||||
<span className={fett}>4K-UHD-Tipp:</span> Rohdaten sind bis 100 GB groß. Wenn die
|
||||
Rippy-Platte knapp ist (Dashboard zeigt den freien Platz), das Arbeitsverzeichnis unter
|
||||
Einstellungen → Verarbeitung auf eine große Freigabe legen. Rippy prüft den Platz vor
|
||||
jedem Rip und bricht sonst mit Klartext ab.
|
||||
<span className={fett}>4K-UHD-Tipp:</span> Rohdaten sind bis 100 GB groß. Wenn der Platz
|
||||
knapp wird (das Dashboard zeigt ihn), das Arbeitsverzeichnis unter Einstellungen →
|
||||
Verarbeitung auf {betrieb.kann.freigaben_einhaengen ? 'eine große Freigabe' : 'ein großes Laufwerk'} legen.
|
||||
Rippy prüft den Platz vor jedem Rip und bricht sonst mit Klartext ab.
|
||||
</p>
|
||||
</Abschnitt>
|
||||
|
||||
@@ -104,6 +127,11 @@ export default function AnleitungPage() {
|
||||
</p>
|
||||
</Abschnitt>
|
||||
|
||||
{/* Nur im verteilten Betrieb. Einem Windows-Nutzer zu erklaeren,
|
||||
wie er weitere Encoding-Worker aufsetzt, beschreibt ein Programm,
|
||||
das er nicht hat — und laesst ihn glauben, er habe etwas falsch
|
||||
gemacht (Commander-Befund 28.08.2026). */}
|
||||
{betrieb.kann.externe_worker && (
|
||||
<Abschnitt icon={Cpu} titel="Weitere Maschinen als Encoding-Worker">
|
||||
<p>
|
||||
Die Kompression kann jede Maschine im Netz übernehmen — Einstellungen → Worker.
|
||||
@@ -126,6 +154,7 @@ export default function AnleitungPage() {
|
||||
nie eine externe Domain (die läuft über den Reverse-Proxy und blockt).
|
||||
</p>
|
||||
</Abschnitt>
|
||||
)}
|
||||
|
||||
<Abschnitt icon={Wrench} titel="MakeMKV-Beta-Key & System">
|
||||
<p>
|
||||
|
||||
@@ -11,6 +11,7 @@ import JobDetailModal from '../components/JobDetailModal'
|
||||
import LiveLogSection from '../components/LiveLogSection'
|
||||
import RetryDialog from '../components/RetryDialog'
|
||||
import { useStrom } from '../lib/useEventStream'
|
||||
import { useBetrieb } from '../lib/useBetrieb'
|
||||
|
||||
interface JobMeta {
|
||||
year?: number
|
||||
@@ -128,6 +129,9 @@ export default function Dashboard() {
|
||||
const [laufwerke, setLaufwerke] = useState<LaufwerkLive[]>([])
|
||||
const [ablagen, setAblagen] = useState<AblageZiel[]>([])
|
||||
const strom = useStrom()
|
||||
// Was hier ueberhaupt angezeigt werden DARF, haengt am Betrieb.
|
||||
// Ohne das stand im Windows-Fenster "Pruefen: docker compose ps".
|
||||
const betrieb = useBetrieb()
|
||||
const { toast } = useToast()
|
||||
|
||||
/*
|
||||
@@ -678,7 +682,30 @@ export default function Dashboard() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 2. Worker — die ECHTE Erreichbarkeit, plus was sie können */}
|
||||
{/* 2. Wer arbeitet hier — und das hängt am BETRIEB.
|
||||
⚠️ Commander-Befund 28.08.2026: Im Windows-Fenster stand
|
||||
„Worker erreichbar: 0 von 1 — Kein Worker antwortet.
|
||||
Prüfen: docker compose ps". Auf einem Windows-PC gibt es
|
||||
keinen zweiten Worker, auf den man warten könnte, und
|
||||
kein docker compose, das man prüfen könnte. Die Meldung
|
||||
war nicht nur unpassend, sie war falsch. */}
|
||||
{!betrieb.kann.externe_worker ? (
|
||||
<div>
|
||||
<div className="flex items-center justify-between text-xs font-mono mb-1">
|
||||
<span className="text-slate-400 flex items-center gap-1.5">
|
||||
<Cpu size={14} className="text-amber-400" /> Rippy arbeitet:
|
||||
</span>
|
||||
<span className="text-slate-200">
|
||||
auf diesem Rechner
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-[11px] font-mono text-slate-500">
|
||||
{aktiverJob
|
||||
? 'Rippen und Komprimieren laufen hier — kein zweiter Rechner nötig.'
|
||||
: 'Rippen und Komprimieren erledigt Rippy selbst.'}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<div className="flex items-center justify-between text-xs font-mono mb-1">
|
||||
<span className="text-slate-400 flex items-center gap-1.5">
|
||||
@@ -692,7 +719,9 @@ export default function Dashboard() {
|
||||
{workerOnline.length === 0 ? (
|
||||
<p className="text-[11px] font-mono text-rose-400">
|
||||
Kein Worker antwortet — ohne ihn läuft kein Rip.
|
||||
Prüfen: <span className="text-slate-300">docker compose ps</span>
|
||||
{betrieb.hilfe_befehl && (
|
||||
<> Prüfen: <span className="text-slate-300">{betrieb.hilfe_befehl}</span></>
|
||||
)}
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-0.5">
|
||||
@@ -713,6 +742,7 @@ export default function Dashboard() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 3. Platz — Container-Platte UND die eingehängten Freigaben.
|
||||
Vorher stand hier nur die Container-Platte; wer wissen
|
||||
@@ -721,7 +751,8 @@ export default function Dashboard() {
|
||||
<div>
|
||||
<div className="flex items-center justify-between text-xs font-mono mb-1">
|
||||
<span className="text-slate-400 flex items-center gap-1.5">
|
||||
<HardDrive size={14} className="text-amber-400" /> Container-Platte:
|
||||
<HardDrive size={14} className="text-amber-400" />
|
||||
{betrieb.kann.container_pfade ? 'Container-Platte:' : 'Platz für Rippy:'}
|
||||
</span>
|
||||
<span className={freiGb > 0 && freiGb < 60 ? 'text-amber-400 font-bold' : 'text-slate-200'}>
|
||||
{freiGb > 0 ? `${freiGb} von ${gesamtGb} GB frei` : 'unbekannt'}
|
||||
@@ -744,7 +775,16 @@ export default function Dashboard() {
|
||||
</div>
|
||||
|
||||
{/* Die eingehängten Freigaben. Sie sind der Ort, an dem die
|
||||
Rohdaten liegen und — bei externem Encoden — liegen MÜSSEN. */}
|
||||
Rohdaten liegen und — bei externem Encoden — liegen MÜSSEN.
|
||||
|
||||
⚠️ NUR im Container. Unter Windows hängt Rippy nichts ein:
|
||||
Dort gibt man einen Ordner oder einen UNC-Pfad an, fertig.
|
||||
Der Satz „Ohne Freigabe kann ein externer Encoder nichts
|
||||
tun — er sieht die Container-Platte nicht" stand am
|
||||
28.08.2026 im Windows-Fenster und ergab dort keinen Sinn:
|
||||
Es gibt weder einen externen Encoder noch eine
|
||||
Container-Platte. */}
|
||||
{betrieb.kann.freigaben_einhaengen && (
|
||||
<div>
|
||||
<div className="flex items-center justify-between text-xs font-mono mb-1">
|
||||
<span className="text-slate-400 flex items-center gap-1.5">
|
||||
@@ -778,6 +818,7 @@ export default function Dashboard() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { api } from '../lib/api'
|
||||
import { useToast } from '../context/ToastContext'
|
||||
import StorageMounts from '../components/StorageMounts'
|
||||
import WorkerVerwaltung from '../components/WorkerVerwaltung'
|
||||
import { useBetrieb } from '../lib/useBetrieb'
|
||||
import { PageHeader } from '../components/ui/PageHeader'
|
||||
import { Card, CardHeader, CardTitle, CardContent } from '../components/ui/Card'
|
||||
import { Button } from '../components/ui/Button'
|
||||
@@ -48,7 +49,10 @@ const defaultSettings: SettingsState = {
|
||||
tmdbApiKey: '',
|
||||
tvdbApiKey: '',
|
||||
omdbApiKey: '',
|
||||
outputDir: '/app/media',
|
||||
// Kein fester Container-Pfad: Auf Windows gibt es kein /app/media. Der
|
||||
// echte Vorgabewert kommt aus GET /betrieb (ablage_vorgabe) und wird
|
||||
// beim Laden gesetzt.
|
||||
outputDir: '',
|
||||
movieDir: 'movies',
|
||||
seriesDir: 'series',
|
||||
musicDir: 'music',
|
||||
@@ -187,6 +191,7 @@ function zeitLesbar(iso: string): string {
|
||||
|
||||
export default function SettingsPage() {
|
||||
const [settings, setSettings] = useState<SettingsState>(defaultSettings)
|
||||
const betrieb = useBetrieb()
|
||||
const [activeTab, setActiveTab] = useState<SettingsTab>('ripping')
|
||||
const [workers, setWorkers] = useState<WorkerInfo[]>([])
|
||||
const [systemInfo, setSystemInfo] = useState<SystemInfo | null>(null)
|
||||
@@ -485,7 +490,12 @@ export default function SettingsPage() {
|
||||
const istFreigabe = (pfad: string) =>
|
||||
ziele.some(z => z.path === pfad && z.is_mount)
|
||||
const ablageWarnung = (() => {
|
||||
const ablage = settings.outputDir || '/app/media'
|
||||
// ⚠️ Die ganze Warnung dreht sich um einen Encoder auf einem ANDEREN
|
||||
// Rechner, der die Container-Platte nicht sieht. Ohne externe Worker gibt
|
||||
// es weder den einen noch die andere — der Satz stand am 28.08.2026
|
||||
// trotzdem im Windows-Fenster und riet dort zu etwas, das es nicht gibt.
|
||||
if (!betrieb.kann.externe_worker) return ''
|
||||
const ablage = settings.outputDir || betrieb.ablage_vorgabe
|
||||
const arbeit = settings.workDir
|
||||
if (!arbeit) {
|
||||
return 'Das Arbeitsverzeichnis steht auf der Container-Platte. Ein Encoder auf '
|
||||
@@ -505,16 +515,45 @@ export default function SettingsPage() {
|
||||
return ''
|
||||
})()
|
||||
|
||||
/*
|
||||
* Die Reiter haengen am BETRIEB.
|
||||
*
|
||||
* Commander-Befund 28.08.2026: „Das gilt fuer die ganze standalone version
|
||||
* fuer Windows, auch fuer die settings und die Anleitung usw."
|
||||
*
|
||||
* Ein Reiter „Worker" fuehrt in einer Standalone-Installation zu einer
|
||||
* Maske, die Rechner in einem Netz verwaltet, das es nicht gibt. Ein Reiter
|
||||
* „Speicherziele" bietet dort das Einhaengen von Freigaben an — etwas, das
|
||||
* unter Windows niemand braucht (dort gibt man einen Ordner oder UNC-Pfad
|
||||
* an). Beides ist keine kosmetische Frage: Wer darauf klickt, bekommt
|
||||
* Bedienelemente, die ins Leere fuehren.
|
||||
*/
|
||||
const tabs: { id: SettingsTab; label: string; icon: any }[] = [
|
||||
{ id: 'ripping', label: 'Ripping', icon: Disc },
|
||||
{ id: 'verarbeitung', label: 'Verarbeitung', icon: Cpu },
|
||||
{ id: 'worker', label: 'Worker', icon: Cpu },
|
||||
...(betrieb.kann.externe_worker
|
||||
? [{ id: 'worker' as SettingsTab, label: 'Worker', icon: Cpu }]
|
||||
: []),
|
||||
// Speicherziele bleibt IMMER. Hier steht die Ablage — „wo will ich das
|
||||
// hinspeichern" war die ausdrueckliche Frage des Commanders. Nur das
|
||||
// EINHAENGEN von Freigaben faellt weg, wenn der Betrieb es nicht kann;
|
||||
// das steckt weiter unten in einer eigenen Bedingung.
|
||||
{ id: 'speicherziele', label: 'Speicherziele', icon: HardDrive },
|
||||
{ id: 'apis', label: 'APIs', icon: Globe },
|
||||
{ id: 'benachrichtigungen', label: 'Benachrichtigungen', icon: AlertCircle },
|
||||
{ id: 'system', label: 'System', icon: Wrench },
|
||||
]
|
||||
|
||||
/*
|
||||
* Ein Reiter, den es nicht mehr gibt, darf nicht ausgewaehlt bleiben.
|
||||
*
|
||||
* Der Betrieb kommt ERST NACH dem ersten Rendern an (ein Abruf). Wer in der
|
||||
* Zwischenzeit auf „Worker" steht — oder wer den Reiter beim Neuladen aus
|
||||
* dem Zustand mitbringt — saehe sonst eine leere Seite ohne jeden Hinweis.
|
||||
*/
|
||||
const sichtbareReiter = tabs.map(t => t.id)
|
||||
const aktiverReiter = sichtbareReiter.includes(activeTab) ? activeTab : 'ripping'
|
||||
|
||||
return (
|
||||
<div className="max-w-6xl mx-auto space-y-6">
|
||||
<PageHeader
|
||||
@@ -533,7 +572,7 @@ export default function SettingsPage() {
|
||||
{/* Navigation Tabs */}
|
||||
<div className="flex border-b border-slate-800 overflow-x-auto [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden bg-[#0c101d]">
|
||||
{tabs.map((tab) => {
|
||||
const isActive = activeTab === tab.id
|
||||
const isActive = aktiverReiter === tab.id
|
||||
return (
|
||||
<button
|
||||
key={tab.id}
|
||||
@@ -554,7 +593,7 @@ export default function SettingsPage() {
|
||||
{/* Tab Content */}
|
||||
<CardContent className="p-6 space-y-8">
|
||||
{/* Ripping Settings */}
|
||||
{activeTab === 'ripping' && (
|
||||
{aktiverReiter === 'ripping' && (
|
||||
<section className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold flex items-center gap-2 text-slate-900 dark:text-slate-100">
|
||||
@@ -697,7 +736,7 @@ export default function SettingsPage() {
|
||||
)}
|
||||
|
||||
{/* Verarbeitung */}
|
||||
{activeTab === 'verarbeitung' && (
|
||||
{aktiverReiter === 'verarbeitung' && (
|
||||
<section className="space-y-6">
|
||||
<h2 className="text-lg font-semibold flex items-center gap-2 text-slate-900 dark:text-slate-100">
|
||||
<Cpu size={20} className="text-amber-500" />
|
||||
@@ -906,7 +945,11 @@ export default function SettingsPage() {
|
||||
value={settings.workDir}
|
||||
onChange={(e) => handleChange('workDir', e.target.value)}
|
||||
>
|
||||
<option value="">Container-Platte (Standard) — klein, nur für DVD/Blu-ray</option>
|
||||
<option value="">
|
||||
{betrieb.kann.container_pfade
|
||||
? 'Container-Platte (Standard) — klein, nur für DVD/Blu-ray'
|
||||
: `Standard (${betrieb.ablage_vorgabe || 'neben der Ablage'})`}
|
||||
</option>
|
||||
{ziele.map(z => (
|
||||
<option key={z.path} value={z.path}>
|
||||
{z.name}{z.is_mount ? ' (Netzwerk-Freigabe)' : ''}
|
||||
@@ -918,9 +961,10 @@ export default function SettingsPage() {
|
||||
<strong className="text-slate-700 dark:text-slate-300">Der Standard</strong> — beim
|
||||
„Rippen starten" kannst du für jede Disc etwas anderes wählen. <strong className="text-slate-700 dark:text-slate-300">
|
||||
Läuft die Vollautomatik</strong>, fragt dich niemand: dann gilt genau dieser Wert.
|
||||
Wichtig für 4K-UHD: Rohdaten sind bis 100 GB groß und passen selten auf die
|
||||
Container-Platte. Am besten dieselbe Freigabe wie das Ziel — dann muss Rippy die
|
||||
Rohdatei am Ende nur umhängen statt sie zu kopieren.
|
||||
Wichtig für 4K-UHD: Rohdaten sind bis 100 GB groß.
|
||||
{betrieb.kann.container_pfade
|
||||
? ' Auf die Container-Platte passen die selten. Am besten dieselbe Freigabe wie das Ziel — dann muss Rippy die Rohdatei am Ende nur umhängen statt sie zu kopieren.'
|
||||
: ' Am besten dasselbe Laufwerk wie die Ablage — dann muss Rippy die Rohdatei am Ende nur umhängen statt sie über die Platte zu kopieren.'}
|
||||
{systemInfo?.plaetze?.length ? (
|
||||
<> Aktuell frei: {systemInfo.plaetze.map(p => `${p.name}: ${p.frei_gb} GB`).join(' · ')}</>
|
||||
) : null}
|
||||
@@ -930,7 +974,7 @@ export default function SettingsPage() {
|
||||
)}
|
||||
|
||||
{/* Worker */}
|
||||
{activeTab === 'worker' && (
|
||||
{aktiverReiter === 'worker' && (
|
||||
<section className="space-y-6">
|
||||
<h2 className="text-lg font-semibold flex items-center gap-2 text-slate-900 dark:text-slate-100">
|
||||
<Cpu size={20} className="text-amber-500" />
|
||||
@@ -941,7 +985,7 @@ export default function SettingsPage() {
|
||||
)}
|
||||
|
||||
{/* Speicherziele */}
|
||||
{activeTab === 'speicherziele' && (
|
||||
{aktiverReiter === 'speicherziele' && (
|
||||
<section className="space-y-6">
|
||||
<h2 className="text-lg font-semibold flex items-center gap-2 text-slate-900 dark:text-slate-100">
|
||||
<HardDrive size={20} className="text-amber-500" />
|
||||
@@ -961,6 +1005,19 @@ export default function SettingsPage() {
|
||||
<h3 className="font-semibold flex items-center gap-2 text-slate-900 dark:text-slate-100">
|
||||
<HardDrive size={16} className="text-amber-500" /> Ablage — wohin die fertigen Filme kommen
|
||||
</h3>
|
||||
{!betrieb.kann.container_pfade ? (
|
||||
/* Kein Auswahlfeld, sondern ein echtes Pfadfeld: Auf einem
|
||||
Windows-PC gibt es keine Liste von Container-Zielen, aus
|
||||
der man waehlen koennte — es gibt Laufwerke, Ordner und
|
||||
UNC-Pfade. Eine Auswahl mit einem Eintrag waere eine
|
||||
Bedienung, die nichts bedient. */
|
||||
<Input
|
||||
label="Ordner"
|
||||
value={settings.outputDir}
|
||||
placeholder={betrieb.ablage_vorgabe}
|
||||
onChange={(e) => handleChange('outputDir', e.target.value)}
|
||||
/>
|
||||
) : (
|
||||
<Select
|
||||
label="Ablage"
|
||||
value={settings.outputDir}
|
||||
@@ -974,6 +1031,7 @@ export default function SettingsPage() {
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400">
|
||||
Gilt für die Schnellwahl im Rip-Dialog <strong className="text-slate-700 dark:text-slate-300">und
|
||||
für die Vollautomatik</strong> — dort fragt niemand nach, also entscheidet dieser Wert.
|
||||
@@ -1005,12 +1063,15 @@ export default function SettingsPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<StorageMounts />
|
||||
{/* Freigaben EINHAENGEN kann nur der Container-Betrieb.
|
||||
Unter Windows gibt man einen Ordner oder einen UNC-Pfad an —
|
||||
eine Maske zum Einhaengen fuehrte dort ins Leere. */}
|
||||
{betrieb.kann.freigaben_einhaengen && <StorageMounts />}
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* APIs */}
|
||||
{activeTab === 'apis' && (
|
||||
{aktiverReiter === 'apis' && (
|
||||
<section className="space-y-6">
|
||||
<h2 className="text-lg font-semibold flex items-center gap-2 text-slate-900 dark:text-slate-100">
|
||||
<Globe size={20} className="text-amber-500" />
|
||||
@@ -1070,7 +1131,7 @@ export default function SettingsPage() {
|
||||
)}
|
||||
|
||||
{/* Benachrichtigungen */}
|
||||
{activeTab === 'benachrichtigungen' && (
|
||||
{aktiverReiter === 'benachrichtigungen' && (
|
||||
<section className="space-y-6">
|
||||
<h2 className="text-lg font-semibold flex items-center gap-2 text-slate-900 dark:text-slate-100">
|
||||
<AlertCircle size={20} className="text-amber-500" />
|
||||
@@ -1120,7 +1181,7 @@ export default function SettingsPage() {
|
||||
)}
|
||||
|
||||
{/* System */}
|
||||
{activeTab === 'system' && (
|
||||
{aktiverReiter === 'system' && (
|
||||
<section className="space-y-6">
|
||||
<h2 className="text-lg font-semibold flex items-center gap-2 text-slate-900 dark:text-slate-100">
|
||||
<Wrench size={20} className="text-amber-500" />
|
||||
|
||||
Reference in New Issue
Block a user