fix(windows): vier Befunde aus dem ersten echten Installationslauf

Alle vom Commander gemeldet, alle nachgestellt.

## 1. Der Installer brach ab: No module named 'db'

    Fehlgeschlagen: ModuleNotFoundError: No module named 'db'

Zu Recht: **docker/api/db.py gibt es nicht.** Seit der Zusammenlegung in V2-1
ist der Store `rippy.store`; main.py schreibt seitdem
`from rippy import store as db`. Ich habe mir aus einem ALIAS ein Modul
zusammengereimt und importiert -- genau die erfundene Schnittstelle, vor der
AGENTS.md Regel D warnt, nur diesmal im eigenen Code. Der Store wird jetzt
direkt benutzt; der Umweg ueber den API-Suchpfad war ohnehin ueberfluessig.

## 2. Der Ersteinrichtungs-Assistent fehlte nach der Installation

In App.tsx stand:

    .catch(() => setSetupDone(true))  // API/Setup nicht erreichbar -> nicht blockieren

Das Fenster geht unmittelbar nach dem Setup auf, der Dienst braucht ein bis
zwei Sekunden bis zur ersten Antwort -- der erste Abruf scheitert also fast
immer. Aus "ich konnte nicht nachsehen" wurde "ist schon erledigt", und der
Assistent war fuer immer weg.

Dasselbe Prinzip steht nebenan in useEventStream.tsx: Ein Verbindungsabriss
ist KEINE Aussage ueber die Welt. Jetzt wird nachgefragt, bis der Server
ANTWORTET (zwei Minuten lang alle zwei Sekunden), und solange steht
"Rippy startet ..." im Fenster statt einer leeren Flaeche.

## 3. + 4. Als Administrator keine Netzlaufwerke, Netzlaufwerk = "keine Rechte"

Beides Windows-Verhalten, kein Rippy-Fehler -- aber Rippy hat ihn
hineinlaufen lassen. Auf dem Rechner nachgemessen:

    X:, Y:, Z:                 Netzlaufwerke
    EnableLinkedConnections    nicht gesetzt (Standard)
    C:\Program Files (x86)     ohne Administrator nicht beschreibbar

Ein erhoehter Prozess bekommt ein anderes Zugriffstoken; eingebundene
Netzlaufwerke haengen am Token der Sitzung und existieren dort schlicht
nicht. Daraus wird ein Teufelskreis:

    Program Files    verlangt Administrator
    Administrator    versteckt die Netzlaufwerke
    Netzlaufwerk     wirkt dann wie "keine Schreibrechte"

Rippy braucht ueberhaupt keine Administratorrechte (Vorgabeordner unter
%LOCALAPPDATA%, Autostart unter HKCU). Der Assistent sagt das jetzt:

* eine eigene Pruefung "Rechte", die bei erhoehtem Start warnt und die
  unsichtbaren Laufwerke beim Namen nennt
* ein fehlender Laufwerksbuchstabe wird als solcher gemeldet, nicht als
  fehlendes Schreibrecht ("Laufwerk Q: ist hier nicht vorhanden")
* ein Ziel unter "Programme" erklaert den Kreis und verweist auf den
  Vorgabeordner

## Dazu: zwei Schreibfallen in AGENTS.md

Deutsche Anfuehrungszeichen in doppelt gequoteten Zeichenketten (vier Mal an
einem Tag) und Bash-Heredocs, die Backslashes halbieren (fuenf Mal, zuletzt
beim Schreiben genau dieses Absatzes). Ein Test dafuer waere Rauschen gewesen
-- Python faengt beides bereits beim Import, nur mit verwirrender Meldung.
Die Regel gehoert in die Konventionen, nicht in eine Pruefung.

Ampel lokal: 703 gruen, ruff sauber.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-08-28 14:36:56 +02:00
co-authored by Claude Opus 5
parent 8eca982e68
commit 6dfff5c99d
5 changed files with 263 additions and 20 deletions
+23
View File
@@ -29,6 +29,29 @@ rot heißt: nicht deployen). Nie freihändig per SSH auf der VM bauen.
Bibliotheks-APIs: `--help`/Doku prüfen und die Fundstelle im Commit nennen.
(Vorgefallen: erfundene Celery-Methode `self.send_task`, erfundene abcde-Flags.)
### Zwei Schreibfallen, die am 28.08.2026 mehrfach gekostet haben
**1. Deutsche Anführungszeichen in doppelt gequoteten Zeichenketten.**
Das öffnende `„` ist U+201E und harmlos. Das schließende ist das **gerade
ASCII-Zeichen** `"` — es beendet die Python-Zeichenkette. Der Fehler erscheint
dann am Gedankenstrich dahinter, also an einer Stelle, an der nichts falsch
ist. An einem Tag vier Mal passiert.
falsch: "einen Ordner unter „Benutzer" wählen"
richtig: 'einen Ordner unter „Benutzer" wählen'
**Regel: Steht ein `„` in der Zeichenkette, gehört sie in EINFACHE
Anführungszeichen.**
**2. Bash-Heredocs mit Windows-Pfaden.** `<<'EOF'` sollte literal sein,
halbiert in dieser Umgebung aber Backslashes — aus `C:\\Users` wird `C:\Users`,
und das ist eine ungültige Escape-Sequenz. An einem Tag fünf Mal passiert,
zuletzt beim Schreiben genau dieses Absatzes.
**Regel: Dateien mit Windows-Pfaden oder Umlauten mit dem Write-Werkzeug
schreiben, nicht per Heredoc.** Wo es doch sein muss: Backslashes über
`chr(92)` zusammensetzen.
## Workflow
1. **Read:** KONZEPT.md + ROADMAP.md lesen. Verstehen, welche Etappe dran ist.
+54 -4
View File
@@ -51,10 +51,45 @@ function AppInhalt() {
// ihn statt des Dashboards. null = /setup noch nicht geprüft (kein Aufblitzen).
const [setupDone, setSetupDone] = useState<boolean | null>(null)
/*
* ⚠️ Ein fehlgeschlagener Abruf ist KEINE Aussage über die Einrichtung.
*
* Hier stand `.catch(() => setSetupDone(true))` mit der Begründung
* „API/Setup nicht erreichbar → nicht blockieren". Die Folge hat der
* Commander am 28.08.2026 gemeldet: **Nach einer frischen Installation
* fehlte der Ersteinrichtungs-Assistent.**
*
* Und zwar zwangsläufig: Das Fenster geht unmittelbar nach dem Setup auf,
* der Dienst braucht ein bis zwei Sekunden bis zur ersten Antwort — der
* erste Abruf scheitert also fast immer. Aus „ich konnte nicht nachsehen"
* wurde „ist schon erledigt", und der Assistent war für immer weg.
*
* Dasselbe Prinzip steht nebenan in `useEventStream.tsx`: Wer nichts Neues
* weiß, behält, was er wusste — und behauptet nichts.
*
* Deshalb: nachfragen, bis der Server ANTWORTET. Erst eine Antwort
* entscheidet.
*/
useEffect(() => {
api.get('/setup')
.then((r) => setSetupDone(Boolean(r.data?.done)))
.catch(() => setSetupDone(true)) // API/Setup nicht erreichbar → nicht blockieren
let abgemeldet = false
let versuche = 0
const fragen = () => {
api.get('/setup')
.then((r) => { if (!abgemeldet) setSetupDone(Boolean(r.data?.done)) })
.catch(() => {
if (abgemeldet) return
versuche += 1
// Zwei Minuten lang alle zwei Sekunden. Antwortet der Server bis
// dahin nicht, hat der Nutzer ein anderes Problem als den
// Assistenten — dann zeigen wir die Oberfläche, damit er die Logs
// sehen kann.
if (versuche < 60) setTimeout(fragen, 2000)
else setSetupDone(true)
})
}
fragen()
return () => { abgemeldet = true }
}, [])
const navItems: { id: Page; label: string; icon: any }[] = [
@@ -64,7 +99,22 @@ function AppInhalt() {
{ id: 'settings', label: 'Einstellungen', icon: SettingsIcon },
]
if (setupDone === null) return null // kurzer Moment, bis /setup geantwortet hat
// Solange /setup noch nicht geantwortet hat: sagen, dass gewartet wird.
// Ein leeres Fenster sieht aus wie ein Absturz — und nach einer frischen
// Installation ist genau das der erste Eindruck.
if (setupDone === null) {
return (
<div className="min-h-screen flex items-center justify-center bg-[#0b0f19]">
<div className="text-center">
<div className="text-amber-400 text-3xl mb-3"></div>
<p className="text-slate-300 text-sm">Rippy startet </p>
<p className="text-slate-500 text-xs mt-1 font-mono">
warte auf den Dienst
</p>
</div>
</div>
)
}
if (!setupDone) return <FirstRunWizard onDone={() => setSetupDone(true)} />
return (
+113 -4
View File
@@ -124,12 +124,93 @@ def pruefe_platz(ziel: str, frei_mb=None) -> dict:
"Platz schaffen oder einen anderen Zielordner wählen.")
def pruefe_schreibrecht(ziel: str, probe=None) -> dict:
def laeuft_erhoeht(erhoeht=None) -> bool:
"""Läuft dieser Prozess mit Administratorrechten?
Wichtig für die Diagnose, nicht als Anforderung: **Rippy braucht keine.**
Erhöht gestartet handelt es sich sogar Nachteile ein — siehe unten.
"""
if erhoeht is not None:
return bool(erhoeht)
if os.name != "nt":
return False
import ctypes
try:
return bool(ctypes.WinDLL("shell32").IsUserAnAdmin())
except (AttributeError, OSError):
return False
def netzlaufwerke(pruefer=None) -> list:
"""Die eingebundenen Netzlaufwerke, wie DIESER Prozess sie sieht."""
if pruefer is not None:
return list(pruefer)
if os.name != "nt":
return []
import ctypes
import string
DRIVE_REMOTE = 4
try:
k = ctypes.WinDLL("kernel32")
maske = k.GetLogicalDrives()
return [b + ":" for i, b in enumerate(string.ascii_uppercase)
if (maske & (1 << i))
and k.GetDriveTypeW(b + ":\\") == DRIVE_REMOTE]
except (AttributeError, OSError):
return []
def pruefe_administrator(erhoeht=None, laufwerke=None) -> dict:
r"""Als Administrator gestartet? Dann fehlen die Netzlaufwerke.
## Der Befund des Commanders (28.08.2026)
> „Wenn man das Tool als Admin startet, kann man im explorer kein
> netzlaufwerk wählen. Wenn man im explorer ein via windows eingebundenes
> netzlaufwerk einbindet dann kommt der fehler das man nicht installieren
> kann, weil die schreibrechte fehlen."
Beides ist Windows-Verhalten, kein Rippy-Fehler — aber Rippy hat ihn
hineinlaufen lassen. Ein erhöhter Prozess bekommt ein ANDERES
Zugriffstoken, und eingebundene Netzlaufwerke hängen am Token der
Sitzung. `X:` existiert dort schlicht nicht. (Auf diesem Rechner
nachgesehen: `EnableLinkedConnections` ist nicht gesetzt — der Standard.)
Der Teufelskreis, in den das führt:
C:\Program Files\… verlangt Administrator
Administrator versteckt die Netzlaufwerke
Netzlaufwerk wirkt dann wie „keine Schreibrechte"
**Rippy braucht überhaupt keine Administratorrechte.** Der Vorgabeordner
liegt unter `%LOCALAPPDATA%`, der Autostart unter `HKCU`.
"""
if not laeuft_erhoeht(erhoeht):
return _eintrag("Rechte", OK, "normal gestartet (so ist es richtig)")
gefunden = netzlaufwerke(laufwerke)
text = "als Administrator gestartet"
return _eintrag(
"Rechte", WARNUNG, text,
"Rippy braucht keine Administratorrechte — und erhöht sieht es die "
"eingebundenen Netzlaufwerke NICHT" +
(" (%s fehlen hier)" % ", ".join(gefunden) if gefunden else "") +
". Für ein Netzlaufwerk den UNC-Pfad eintippen "
"(\\\\Server\\Freigabe) oder Rippy ohne Administrator starten.")
def pruefe_schreibrecht(ziel: str, probe=None, erhoeht=None) -> dict:
"""Darf Rippy dorthin schreiben? Wirklich probiert, nicht geraten.
Ein `os.access(..., W_OK)` lügt unter Windows: Es beantwortet die Frage
nach dem Schreibrecht anhand des Nur-Lesen-Flags, nicht anhand der
Zugriffsrechte. Also wird eine Datei angelegt und wieder gelöscht.
nach dem Nur-Lesen-Flag, nicht nach den Zugriffsrechten. Also wird eine
Datei angelegt und wieder gelöscht.
Und der Grund wird BENANNT. „Schreibrechte fehlen" war die Meldung, die
der Commander bei einem Netzlaufwerk bekam — richtig im Wortlaut,
unbrauchbar als Auskunft: Das Laufwerk war für den erhöhten Prozess gar
nicht da.
"""
if probe is not None:
geklappt, grund = probe
@@ -137,11 +218,38 @@ def pruefe_schreibrecht(ziel: str, probe=None) -> dict:
geklappt, grund = _schreibprobe(ziel)
if geklappt:
return _eintrag("Schreibrecht", OK, ziel)
laufwerk = os.path.splitdrive(ziel or "")[0]
if laufwerk and len(laufwerk) == 2 and not os.path.isdir(laufwerk + "\\"):
# Ein Laufwerksbuchstabe, den es in DIESEM Prozess nicht gibt.
zusatz = (" Als Administrator gestartete Programme sehen die "
"eingebundenen Netzlaufwerke der Sitzung nicht."
if laeuft_erhoeht(erhoeht) else "")
return _eintrag(
"Schreibrecht", FEHLER,
"%s — Laufwerk %s ist hier nicht vorhanden" % (ziel, laufwerk),
("Statt des Laufwerksbuchstabens den UNC-Pfad eintragen "
"(\\\\Server\\Freigabe)." + zusatz))
if _unter_programme(ziel):
return _eintrag(
"Schreibrecht", FEHLER, "%s (%s)" % (ziel, grund),
'Der Ordner „Programme" verlangt Administratorrechte — und die '
'verstecken wiederum die Netzlaufwerke. Rippy braucht sie nicht: '
'Der Vorgabeordner unter „Benutzer" funktioniert ohne.')
return _eintrag("Schreibrecht", FEHLER, "%s (%s)" % (ziel, grund),
'Einen Ordner unterhalb von „Benutzer" wählen — dort '
'braucht Rippy keine Administratorrechte.')
def _unter_programme(ziel: str) -> bool:
"""Liegt das Ziel unter „Programme"? (reine Funktion)"""
unten = (ziel or "").lower().replace("/", "\\")
return ("\\program files" in unten or unten.startswith("program files")
or "\\programme\\" in unten)
def _schreibprobe(ziel: str):
import tempfile
@@ -227,7 +335,8 @@ def pruefe_werkzeuge(bestand=None) -> list:
# ── Alles zusammen ──────────────────────────────────────────────────────
def alle_pruefungen(ziel: str, port: int = STANDARD_PORT) -> list:
"""Der vollständige Voraussetzungs-Bericht, in Anzeigereihenfolge."""
return ([pruefe_windows(), pruefe_webview(), pruefe_laufwerk()]
return ([pruefe_windows(), pruefe_webview(), pruefe_administrator(),
pruefe_laufwerk()]
+ pruefe_werkzeuge()
+ [pruefe_platz(ziel), pruefe_schreibrecht(ziel), pruefe_port(port)])
+20 -12
View File
@@ -145,23 +145,31 @@ class Bruecke:
def _ablage_speichern(pfad: str) -> None:
"""Die gewählte Ablage in die Einstellungen schreiben.
Über die laufende Datenbank, nicht über eine Konfigurationsdatei: Das UI
liest `outputDir` aus den Einstellungen, und genau dort muss der Wert
landen, damit die Wahl im Assistenten überhaupt etwas bewirkt.
## Der Fehler, der das Setup abgebrochen hat (28.08.2026)
Hier stand `import db as api_db`, nachdem `daemon._api_pfad()` in den
Suchpfad gehängt worden war. Beim Commander brach die Installation ab mit:
Fehlgeschlagen: ModuleNotFoundError: No module named 'db'
Und zwar zu Recht: **`docker/api/db.py` gibt es nicht.** Seit der
Zusammenlegung in V2-1 ist der Store `rippy.store`; `main.py` schreibt
seitdem `from rippy import store as db`. Ich habe ein Modul importiert,
das ich mir aus einem Alias zusammengereimt habe — genau die Sorte
erfundener Schnittstelle, vor der AGENTS.md Regel D warnt, nur diesmal
im eigenen Code.
Der Store wird direkt benutzt. Ein Umweg über den Suchpfad der API war
ohnehin überflüssig.
"""
from rippy import config, daemon, store
from rippy import config, store
werte = config.laden()
store.verbinden(config.datenbank_url(werte))
import sys
sys.path.insert(0, daemon._api_pfad())
import db as api_db # noqa: E402
api_db.init_db()
aktuell = api_db.get_settings() or {}
store.init_db()
aktuell = store.get_settings() or {}
aktuell["outputDir"] = pfad
api_db.save_settings(aktuell)
store.save_settings(aktuell)
def starten() -> dict:
+53
View File
@@ -90,6 +90,57 @@ def test_schreibprobe_am_echten_ordner(tmp_path):
assert list(tmp_path.iterdir()) == []
# ── Administrator und Netzlaufwerke ─────────────────────────────────────
def test_normal_gestartet_ist_der_richtige_zustand():
"""Rippy braucht KEINE Administratorrechte — Vorgabeordner unter
%LOCALAPPDATA%, Autostart unter HKCU."""
befund = e.pruefe_administrator(erhoeht=False)
assert befund["zustand"] == e.OK
def test_als_administrator_wird_vor_den_fehlenden_netzlaufwerken_gewarnt():
"""DER Befund des Commanders (28.08.2026): „Wenn man das Tool als Admin
startet, kann man im explorer kein netzlaufwerk waehlen."
Ein erhoehter Prozess bekommt ein anderes Zugriffstoken; eingebundene
Netzlaufwerke haengen am Token der Sitzung. Windows-Verhalten, kein
Rippy-Fehler — aber Rippy hat ihn hineinlaufen lassen."""
befund = e.pruefe_administrator(erhoeht=True, laufwerke=["X:", "Y:", "Z:"])
assert befund["zustand"] == e.WARNUNG
assert "X:" in befund["abhilfe"]
assert "UNC" in befund["abhilfe"] or "Server" in befund["abhilfe"]
def test_fehlendes_laufwerk_wird_beim_namen_genannt():
"""„Schreibrechte fehlen" war richtig im Wortlaut und unbrauchbar als
Auskunft: Das Laufwerk war fuer den erhoehten Prozess gar nicht da."""
befund = e.pruefe_schreibrecht(
"Q:" + chr(92) + "Rippy", probe=(False, "Pfad nicht gefunden"),
erhoeht=True)
assert befund["zustand"] == e.FEHLER
assert "Q:" in befund["text"]
assert "nicht vorhanden" in befund["text"]
assert "Administrator" in befund["abhilfe"]
def test_programme_ordner_erklaert_den_teufelskreis():
"""Program Files verlangt Administrator, Administrator versteckt die
Netzlaufwerke — und Rippy braucht beides nicht."""
befund = e.pruefe_schreibrecht(
"C:" + chr(92) + "Program Files (x86)" + chr(92) + "Rippy",
probe=(False, "Zugriff verweigert"))
assert befund["zustand"] == e.FEHLER
assert "Administratorrechte" in befund["abhilfe"]
assert "Vorgabeordner" in befund["abhilfe"]
def test_programme_wird_erkannt():
B = chr(92)
assert e._unter_programme("C:" + B + "Program Files" + B + "Rippy") is True
assert e._unter_programme("C:" + B + "Program Files (x86)" + B + "R") is True
assert e._unter_programme("C:" + B + "Users" + B + "X" + B + "Rippy") is False
def test_belegter_port_ist_nur_eine_warnung():
"""Sehr oft ist es ein bereits laufendes Rippy — und dann ist eine
Neuinstallation genau richtig."""
@@ -164,6 +215,8 @@ def test_alles_gruen_sagt_es_auch_so():
def test_jede_pruefung_hat_dieselbe_form():
"""Ein fehlendes Feld waere in der Oberflaeche ein `undefined`."""
for befund in ([e.pruefe_windows(22631), e.pruefe_webview(version=""),
e.pruefe_administrator(erhoeht=True, laufwerke=["X:"]),
e.pruefe_administrator(erhoeht=False),
e.pruefe_laufwerk(laufwerke=[]),
e.pruefe_platz("C:\\x", frei_mb=9999),
e.pruefe_port(7788, belegt=False)]