fix(windows): die Datenbank lag AUSSERHALB der Installation — daher fehlte
Ampel / ampel (push) Failing after 54s
Ampel / ampel (push) Failing after 54s
der Ersteinrichtungs-Assistent
Der Befund des Commanders: "Ausserdem fehlt der 1st run wizzard wenn man es
installiert." Meine erste Reparatur (App.tsx las einen gescheiterten Abruf
als "erledigt") war richtig, aber nicht die Ursache. Nachgemessen:
Installation %LOCALAPPDATA%\Rippy wird deinstalliert
Datenbank %ProgramData%\Rippy BLEIBT LIEGEN
Eine "frische" Installation erbte damit die alte Datenbank samt
`setup.done = true` -- der Assistent erschien nie wieder. Gegengeprueft:
nach dem Aufraeumen meldet /api/setup wieder {"done": false}, und das
Fenster zeigt "Willkommen bei Rippy".
Der Grund fuer den alten Ort stand im Docstring: "Ein Programmordner ist
unter Windows fuer einen DIENST nicht zuverlaessig beschreibbar." Das stimmte
-- fuer den Windows-Dienst, den es nie gab. Rippy laeuft als der angemeldete
Benutzer (Autostart unter HKCU). Jetzt liegt alles, was Rippy gehoert, in
EINEM Ordner: Programm, UI, Werkzeuge, Protokoll, WebView2-Zwischenspeicher
und die Datenbank.
Dazu:
* `datenbank_umziehen()` holt eine vorhandene Datenbank vom alten Ort ab --
wer Rippy schon benutzt hat, behaelt Jobs und Einstellungen. Gibt es am
neuen Ort schon eine, bleibt sie unangetastet: Die BENUTZTE gewinnt.
* `deinstallieren()` raeumt den alten Ort mit weg, und `alte_orte()` fasst
nur einen Ordner an, der wirklich "Rippy" heisst.
* Der Ersteinrichtungs-Assistent spricht nicht mehr von "Encoding-Worker",
wenn es keine gibt -- dort steht jetzt "Kompression". Gegengeprueft: kein
"docker", kein "Container", kein "Worker" mehr im Text.
Ampel lokal: 709 gruen, ruff sauber.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
082e8b8b55
commit
8e65411189
@@ -1,6 +1,7 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { Disc, Cpu, Globe, Tv, CheckCircle, ArrowRight, HardDrive, AlertTriangle, KeyRound } from 'lucide-react'
|
import { Disc, Cpu, Globe, Tv, CheckCircle, ArrowRight, HardDrive, AlertTriangle, KeyRound } from 'lucide-react'
|
||||||
import { api } from '../lib/api'
|
import { api } from '../lib/api'
|
||||||
|
import { useBetrieb } from '../lib/useBetrieb'
|
||||||
import { MEDIA_SERVER_OPTIONEN } from '../lib/mediaServer'
|
import { MEDIA_SERVER_OPTIONEN } from '../lib/mediaServer'
|
||||||
import { PRESET_KEINE, schwacheEncoderCpu } from '../lib/encoder'
|
import { PRESET_KEINE, schwacheEncoderCpu } from '../lib/encoder'
|
||||||
import { Button } from './ui/Button'
|
import { Button } from './ui/Button'
|
||||||
@@ -74,6 +75,7 @@ export default function FirstRunWizard({ onDone }: { onDone: () => void }) {
|
|||||||
const [makemkvKey, setMakemkvKey] = useState('')
|
const [makemkvKey, setMakemkvKey] = useState('')
|
||||||
const [mediaServer, setMediaServer] = useState('jellyfin')
|
const [mediaServer, setMediaServer] = useState('jellyfin')
|
||||||
const [transcodeEnabled, setTranscodeEnabled] = useState(true)
|
const [transcodeEnabled, setTranscodeEnabled] = useState(true)
|
||||||
|
const betrieb = useBetrieb()
|
||||||
const [workers, setWorkers] = useState<WorkerInfo[]>([])
|
const [workers, setWorkers] = useState<WorkerInfo[]>([])
|
||||||
const [geraete, setGeraete] = useState<GeraetInfo[]>([])
|
const [geraete, setGeraete] = useState<GeraetInfo[]>([])
|
||||||
const [plaetze, setPlaetze] = useState<PlatzInfo[]>([])
|
const [plaetze, setPlaetze] = useState<PlatzInfo[]>([])
|
||||||
@@ -197,7 +199,11 @@ export default function FirstRunWizard({ onDone }: { onDone: () => void }) {
|
|||||||
schlechtText="Kein optisches Laufwerk gefunden. Rippy kann dann nur komprimieren, nicht rippen. Prüfe, ob das Laufwerk angeschlossen und (bei einer VM) durchgereicht ist."
|
schlechtText="Kein optisches Laufwerk gefunden. Rippy kann dann nur komprimieren, nicht rippen. Prüfe, ob das Laufwerk angeschlossen und (bei einer VM) durchgereicht ist."
|
||||||
/>
|
/>
|
||||||
<Zeile
|
<Zeile
|
||||||
titel="Encoding-Worker"
|
/* Auf einem Windows-PC gibt es keinen "Encoding-Worker" und
|
||||||
|
keinen Worker-Container -- Rippy komprimiert selbst. Der
|
||||||
|
Commander am 28.08.2026: "Das gilt fuer die ganze
|
||||||
|
standalone version fuer Windows." */
|
||||||
|
titel={betrieb.kann.externe_worker ? 'Encoding-Worker' : 'Kompression'}
|
||||||
geladen={geladen}
|
geladen={geladen}
|
||||||
gut={encoderWorker.length > 0}
|
gut={encoderWorker.length > 0}
|
||||||
gutText={encoderWorker.map(w => {
|
gutText={encoderWorker.map(w => {
|
||||||
@@ -207,7 +213,9 @@ export default function FirstRunWizard({ onDone }: { onDone: () => void }) {
|
|||||||
.filter(Boolean).join(', ')
|
.filter(Boolean).join(', ')
|
||||||
return zusatz ? `${w.name} (${zusatz})` : w.name
|
return zusatz ? `${w.name} (${zusatz})` : w.name
|
||||||
}).join(' · ')}
|
}).join(' · ')}
|
||||||
schlechtText="Noch kein Worker gemeldet. Beim ersten Start dauert das bis zu einer Minute — diese Anzeige aktualisiert sich selbst. Bleibt es dabei, läuft der Worker-Container nicht."
|
schlechtText={betrieb.kann.externe_worker
|
||||||
|
? 'Noch kein Worker gemeldet. Beim ersten Start dauert das bis zu einer Minute — diese Anzeige aktualisiert sich selbst. Bleibt es dabei, läuft der Worker-Container nicht.'
|
||||||
|
: 'Rippy meldet noch keine Encoder. Beim ersten Start dauert das bis zu einer Minute — diese Anzeige aktualisiert sich selbst.'}
|
||||||
/>
|
/>
|
||||||
<Zeile
|
<Zeile
|
||||||
titel="Freier Platz"
|
titel="Freier Platz"
|
||||||
@@ -351,7 +359,7 @@ export default function FirstRunWizard({ onDone }: { onDone: () => void }) {
|
|||||||
<span>Rippy hat deine Rechenleistung gemessen und passt die Wahl an.</span>
|
<span>Rippy hat deine Rechenleistung gemessen und passt die Wahl an.</span>
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs mt-2 text-amber-700/90 dark:text-amber-300/90">
|
<p className="text-xs mt-2 text-amber-700/90 dark:text-amber-300/90">
|
||||||
Keiner deiner Worker kann AVX2 — die Vektorbefehle, von denen H.265
|
{betrieb.kann.externe_worker ? 'Keiner deiner Worker' : 'Dieser Rechner'} kann kein AVX2 — die Vektorbefehle, von denen H.265
|
||||||
lebt. 4K in H.265 würde hier <strong>ein bis zwei Tage pro Film</strong>
|
lebt. 4K in H.265 würde hier <strong>ein bis zwei Tage pro Film</strong>
|
||||||
{' '}dauern. Deshalb wird eingestellt:
|
{' '}dauern. Deshalb wird eingestellt:
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
+39
-5
@@ -223,13 +223,47 @@ def datenbank_url(werte: dict, standard_pfad: str = None) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def standard_datenbankpfad() -> str:
|
def standard_datenbankpfad() -> str:
|
||||||
"""Wo die SQLite-Datei liegt, wenn niemand etwas anderes sagt.
|
r"""Wo die SQLite-Datei liegt, wenn niemand etwas anderes sagt.
|
||||||
|
|
||||||
Je Plattform dort, wo Dienste ihre Daten ablegen — nicht neben dem
|
## Warum unter Windows NICHT mehr %ProgramData% (28.08.2026)
|
||||||
Programm. Ein Programmordner ist unter Windows für einen Dienst nicht
|
|
||||||
zuverlässig beschreibbar, und unter Linux widerspräche es dem FHS.
|
Hier stand `%ProgramData%\Rippy\rippy.db`, mit der Begründung: „Ein
|
||||||
|
Programmordner ist unter Windows für einen DIENST nicht zuverlässig
|
||||||
|
beschreibbar." Das stimmte — für den Windows-Dienst, den es nie gab.
|
||||||
|
Rippy läuft als der angemeldete Benutzer (Autostart unter `HKCU`, siehe
|
||||||
|
`windows_app.py`), und der Grund ist damit hinfällig.
|
||||||
|
|
||||||
|
Geblieben war der Schaden. Der Commander meldete: **„Es fehlt der 1st run
|
||||||
|
wizzard wenn man es installiert."** Nachgemessen:
|
||||||
|
|
||||||
|
Installation %LOCALAPPDATA%\Rippy (wird deinstalliert)
|
||||||
|
Datenbank %ProgramData%\Rippy (bleibt liegen)
|
||||||
|
|
||||||
|
Eine „frische" Installation erbte also die alte Datenbank samt
|
||||||
|
`setup.done = true`, und der Ersteinrichtungs-Assistent erschien nie
|
||||||
|
wieder. Ein Deinstallieren, das den Zustand stehen lässt, ist kein
|
||||||
|
Deinstallieren.
|
||||||
|
|
||||||
|
Jetzt liegt alles, was Rippy gehört, in EINEM Ordner: Programm, UI,
|
||||||
|
Werkzeuge, Protokoll, WebView2-Zwischenspeicher — und die Datenbank.
|
||||||
|
|
||||||
|
Unter Linux bleibt `/var/lib/rippy`: Dort läuft Rippy wirklich als
|
||||||
|
Systemdienst, und der FHS sagt genau das.
|
||||||
"""
|
"""
|
||||||
if os.name == "nt":
|
if os.name == "nt":
|
||||||
basis = os.environ.get("PROGRAMDATA") or os.path.expanduser("~")
|
basis = os.environ.get("LOCALAPPDATA") or os.path.expanduser("~")
|
||||||
return os.path.join(basis, "Rippy", "rippy.db")
|
return os.path.join(basis, "Rippy", "rippy.db")
|
||||||
return "/var/lib/rippy/rippy.db"
|
return "/var/lib/rippy/rippy.db"
|
||||||
|
|
||||||
|
|
||||||
|
def alter_datenbankpfad() -> str:
|
||||||
|
r"""Der Ort VOR dem 28.08.2026 — nur noch zum Umziehen und Aufräumen.
|
||||||
|
|
||||||
|
Wer Rippy schon installiert hatte, hat seine Daten dort. Sie kommentarlos
|
||||||
|
liegen zu lassen wäre ein stiller Verlust; sie zu löschen wäre schlimmer.
|
||||||
|
`windows_app.datenbank_umziehen()` holt sie ab.
|
||||||
|
"""
|
||||||
|
if os.name != "nt":
|
||||||
|
return ""
|
||||||
|
basis = os.environ.get("PROGRAMDATA") or ""
|
||||||
|
return os.path.join(basis, "Rippy", "rippy.db") if basis else ""
|
||||||
|
|||||||
@@ -233,6 +233,80 @@ def test_die_testsuite_ruehrt_den_ECHTEN_eintrag_nicht_an(tmp_path, monkeypatch)
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Die Datenbank am richtigen Ort ──────────────────────────────────────
|
||||||
|
def test_datenbank_liegt_im_installationsordner():
|
||||||
|
"""DER Befund vom 28.08.2026: „Es fehlt der 1st run wizzard wenn man es
|
||||||
|
installiert."
|
||||||
|
|
||||||
|
Die Datenbank lag unter %ProgramData%\\Rippy, die Installation unter
|
||||||
|
%LOCALAPPDATA%\\Rippy. Beim Deinstallieren blieb sie liegen, und die
|
||||||
|
naechste Installation erbte `setup.done = true` — der
|
||||||
|
Ersteinrichtungs-Assistent erschien nie wieder.
|
||||||
|
|
||||||
|
Der Grund fuer den alten Ort war ein Windows-DIENST, den es nie gab.
|
||||||
|
"""
|
||||||
|
from rippy import config
|
||||||
|
|
||||||
|
pfad = config.standard_datenbankpfad()
|
||||||
|
if os.name != "nt":
|
||||||
|
assert pfad == "/var/lib/rippy/rippy.db"
|
||||||
|
return
|
||||||
|
assert "ProgramData" not in pfad, (
|
||||||
|
"Die Datenbank liegt wieder ausserhalb des Installationsordners — "
|
||||||
|
"dann ueberlebt sie jede Deinstallation."
|
||||||
|
)
|
||||||
|
assert pfad.lower().startswith(
|
||||||
|
(os.environ.get("LOCALAPPDATA") or "").lower())
|
||||||
|
|
||||||
|
|
||||||
|
@nur_windows
|
||||||
|
def test_alte_datenbank_wird_uebernommen(tmp_path, monkeypatch):
|
||||||
|
"""Wer Rippy schon benutzt hat, soll Jobs und Einstellungen behalten.
|
||||||
|
Die Datei wird verschoben, nicht ignoriert und nicht geloescht."""
|
||||||
|
from rippy import config
|
||||||
|
|
||||||
|
alt = tmp_path / "alt" / "Rippy" / "rippy.db"
|
||||||
|
alt.parent.mkdir(parents=True)
|
||||||
|
alt.write_bytes(b"alte-daten")
|
||||||
|
neu = tmp_path / "neu" / "Rippy" / "rippy.db"
|
||||||
|
|
||||||
|
monkeypatch.setattr(config, "alter_datenbankpfad", lambda: str(alt))
|
||||||
|
monkeypatch.setattr(config, "standard_datenbankpfad", lambda: str(neu))
|
||||||
|
|
||||||
|
assert windows_app.datenbank_umziehen() == str(neu)
|
||||||
|
assert neu.read_bytes() == b"alte-daten"
|
||||||
|
assert not alt.exists()
|
||||||
|
|
||||||
|
|
||||||
|
@nur_windows
|
||||||
|
def test_vorhandene_datenbank_wird_NICHT_ueberschrieben(tmp_path, monkeypatch):
|
||||||
|
"""Die BENUTZTE gewinnt, nicht die aeltere."""
|
||||||
|
from rippy import config
|
||||||
|
|
||||||
|
alt = tmp_path / "alt" / "rippy.db"
|
||||||
|
alt.parent.mkdir(parents=True)
|
||||||
|
alt.write_bytes(b"alt")
|
||||||
|
neu = tmp_path / "neu" / "rippy.db"
|
||||||
|
neu.parent.mkdir(parents=True)
|
||||||
|
neu.write_bytes(b"in-benutzung")
|
||||||
|
|
||||||
|
monkeypatch.setattr(config, "alter_datenbankpfad", lambda: str(alt))
|
||||||
|
monkeypatch.setattr(config, "standard_datenbankpfad", lambda: str(neu))
|
||||||
|
|
||||||
|
assert windows_app.datenbank_umziehen() == ""
|
||||||
|
assert neu.read_bytes() == b"in-benutzung"
|
||||||
|
|
||||||
|
|
||||||
|
def test_alte_orte_fassen_nur_rippy_an(monkeypatch):
|
||||||
|
"""Ein `%ProgramData%` ohne unseren Namen darin waere ein fremder Ordner."""
|
||||||
|
from rippy import config
|
||||||
|
|
||||||
|
monkeypatch.setattr(config, "alter_datenbankpfad",
|
||||||
|
lambda: "C:" + BACKSLASH + "ProgramData" + BACKSLASH
|
||||||
|
+ "Fremd" + BACKSLASH + "rippy.db")
|
||||||
|
assert windows_app.alte_orte() == []
|
||||||
|
|
||||||
|
|
||||||
# ── Rueckmeldung ohne Konsole ───────────────────────────────────────────
|
# ── Rueckmeldung ohne Konsole ───────────────────────────────────────────
|
||||||
def test_ohne_konsole_kommt_ein_fenster(monkeypatch):
|
def test_ohne_konsole_kommt_ein_fenster(monkeypatch):
|
||||||
"""Seit die EXE ohne Konsole gebaut wird (Wunsch des Commanders am
|
"""Seit die EXE ohne Konsole gebaut wird (Wunsch des Commanders am
|
||||||
|
|||||||
@@ -147,6 +147,7 @@ def installieren(ziel: str = None, autostart: bool = True,
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
ui_auspacken(ziel)
|
ui_auspacken(ziel)
|
||||||
|
datenbank_umziehen()
|
||||||
|
|
||||||
# Mitgelieferte Werkzeuge (HandBrakeCLI) neben das Programm legen. Dort
|
# Mitgelieferte Werkzeuge (HandBrakeCLI) neben das Programm legen. Dort
|
||||||
# sucht katalog.kandidaten() VOR allen Systempfaden.
|
# sucht katalog.kandidaten() VOR allen Systempfaden.
|
||||||
@@ -183,6 +184,55 @@ def installieren(ziel: str = None, autostart: bool = True,
|
|||||||
return ziel
|
return ziel
|
||||||
|
|
||||||
|
|
||||||
|
def alte_orte() -> list:
|
||||||
|
"""Ordner ausserhalb des Installationsordners, die Rippy einmal angelegt hat.
|
||||||
|
|
||||||
|
Reine Aufzaehlung, damit `deinstallieren()` pruefbar bleibt und niemand
|
||||||
|
versehentlich einen fremden Ordner in die Liste schreibt.
|
||||||
|
"""
|
||||||
|
from rippy import config
|
||||||
|
|
||||||
|
alt = config.alter_datenbankpfad()
|
||||||
|
if not alt:
|
||||||
|
return []
|
||||||
|
ordner = os.path.dirname(alt)
|
||||||
|
# Nur wenn er wirklich Rippy gehoert. Ein `%ProgramData%` ohne unseren
|
||||||
|
# Namen darin waere ein fremder Ordner, und den fasst niemand an.
|
||||||
|
if os.path.basename(ordner).lower() != "rippy" or not os.path.isdir(ordner):
|
||||||
|
return []
|
||||||
|
return [ordner]
|
||||||
|
|
||||||
|
|
||||||
|
def datenbank_umziehen() -> str:
|
||||||
|
r"""Holt eine Datenbank vom alten Ort ab. Gibt den Zielpfad zurück, sonst "".
|
||||||
|
|
||||||
|
Bis zum 28.08.2026 lag sie unter `%ProgramData%\Rippy\rippy.db` — also
|
||||||
|
AUSSERHALB des Installationsordners. Die Deinstallation räumte sie damit
|
||||||
|
nicht weg, und eine „frische" Installation erbte den alten Zustand samt
|
||||||
|
`setup.done = true`. Genau deshalb fehlte dem Commander der
|
||||||
|
Ersteinrichtungs-Assistent.
|
||||||
|
|
||||||
|
Wer Rippy schon benutzt hat, soll seine Jobs und Einstellungen behalten:
|
||||||
|
Die Datei wird verschoben, nicht ignoriert und nicht gelöscht. Gibt es am
|
||||||
|
neuen Ort schon eine, bleibt sie unangetastet — die neuere gewinnt nicht
|
||||||
|
automatisch, aber die BENUTZTE.
|
||||||
|
"""
|
||||||
|
from rippy import config
|
||||||
|
|
||||||
|
alt = config.alter_datenbankpfad()
|
||||||
|
neu = config.standard_datenbankpfad()
|
||||||
|
if not alt or not os.path.isfile(alt) or os.path.isfile(neu):
|
||||||
|
return ""
|
||||||
|
try:
|
||||||
|
os.makedirs(os.path.dirname(neu), exist_ok=True)
|
||||||
|
shutil.move(alt, neu)
|
||||||
|
except OSError as e:
|
||||||
|
melden(" Alte Datenbank konnte nicht umgezogen werden (%s)." % e)
|
||||||
|
return ""
|
||||||
|
melden(" Datenbank uebernommen: %s -> %s" % (alt, neu))
|
||||||
|
return neu
|
||||||
|
|
||||||
|
|
||||||
def ui_auspacken(ziel: str) -> str:
|
def ui_auspacken(ziel: str) -> str:
|
||||||
r"""Legt die Weboberfläche NEBEN das Programm. Gibt den Ordner zurück.
|
r"""Legt die Weboberfläche NEBEN das Programm. Gibt den Ordner zurück.
|
||||||
|
|
||||||
@@ -282,6 +332,19 @@ def deinstallieren(still: bool = False) -> None:
|
|||||||
|
|
||||||
fenster_modul.helfer_beenden(os.path.join(ordner, "fenster"))
|
fenster_modul.helfer_beenden(os.path.join(ordner, "fenster"))
|
||||||
|
|
||||||
|
# Der ALTE Datenbank-Ort (bis 28.08.2026 %ProgramData%\Rippy) liegt
|
||||||
|
# ausserhalb des Installationsordners. Blieb er stehen, erbte die naechste
|
||||||
|
# Installation den alten Zustand — genau daran fehlte dem Commander der
|
||||||
|
# Ersteinrichtungs-Assistent. Ein Deinstallieren, das Zustand stehen
|
||||||
|
# laesst, ist kein Deinstallieren.
|
||||||
|
for rest in alte_orte():
|
||||||
|
try:
|
||||||
|
shutil.rmtree(rest)
|
||||||
|
if not still:
|
||||||
|
melden(" Alter Datenordner entfernt: %s" % rest)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
laeuft_von_hier = os.path.abspath(_eigene_datei()).lower().startswith(
|
laeuft_von_hier = os.path.abspath(_eigene_datei()).lower().startswith(
|
||||||
os.path.abspath(ordner).lower())
|
os.path.abspath(ordner).lower())
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user