feat(v5): eigenes Fenster ohne Windows-Rahmen + Layout skaliert mit
Ampel / ampel (push) Successful in 1m34s

Zwei Befunde des Commanders (01.09.2026):

1. "Momentan ist alles nach links gerueckt, nichts skaliert mit der
   fenstergroesse und es ist alles gequetscht."

   Ursache gefunden: <main> hatte WEDER Zentrierung NOCH Maximalbreite,
   die Karten darin aber einen harten Deckel von max-w-2xl (672 px). In
   einem 1280er Fenster klebte damit alles am linken Rand und rechts
   stand nichts. Behoben:
   * <main> und Kopfzeile teilen sich mx-auto max-w-[1600px] mit
     mitwachsendem Innenabstand — der Inhalt ist mittig und nutzt die
     Breite.
   * Der 672-px-Deckel der Einstellungs-Spalte ist weg. Drei
     Encoder-Felder stehen jetzt nebeneinander statt untereinander.
   * Die Sektionswahl der Einstellungen wird im schmalen Fenster zur
     Zeile ueber den Karten (lg:flex-row), statt eine 176-px-Spalte vom
     ohnehin knappen Platz abzuziehen.
   * Info-Kacheln der Laufwerks-Karte: zwei Spalten schmal, vier breit.
   * Startgroesse 1000x700 -> 1280x820, Mindestmass 720x480 -> 860x560.

2. "waere sogar richtig cool wenn du den rahmen wegbekommst"

   frame:false, und die Kopfzeile IST jetzt die Titelleiste:
   Minimieren / Maximieren / Schliessen sitzen oben rechts in
   Windows-Massen (46x32), gezogen wird an der Kopfzeile, Doppelklick
   maximiert. Groesse aendern bleibt moeglich — Electron setzt fuer
   rahmenlose Fenster unter Windows weiter WS_THICKFRAME (thickFrame,
   Standard true), die Kanten ziehen also wie gewohnt.

   Schliessen nimmt GENAU den Weg des alten Fensterkreuzes
   (fenster.close() -> der bestehende close-Horcher versteckt nur):
   Rippy lebt im Tray weiter, ein laufender Rip merkt davon nichts
   (Paragraph 4.1 Punkt 3).

   Die Ziehflaeche ist .ziehbar in stil.css; alles Bedienbare darin
   traegt .nicht-ziehbar, sonst verschluckt der Griff den Klick.
   fensterMaximiert im HauptStatus entscheidet ueber das Knopf-Symbol
   und wird auch bei Doppelklick/Tastenkuerzel nachgefuehrt (maximize-
   und unmaximize-Horcher).

Nachgemessen mit dem Smoke-Beweis: beweise/ui-513-uebersicht.png (kein
Rahmen, eigene Knoepfe, Inhalt ueber die volle Breite) und
beweise/ui-513-einstellungen.png (drei Encoder nebeneinander).

230 Tests gruen, Typpruefung sauber. Version 5.1.3.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-09-01 18:07:45 +02:00
co-authored by Claude Opus 5
parent 0c38754c09
commit 827910fd63
12 changed files with 182 additions and 32 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 295 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB

+20 -12
View File
@@ -1,17 +1,25 @@
Version 5.1.2 Version 5.1.3
Neu: Rippy sagt Bescheid, wenn ein Update da ist. Das Fenster gehört jetzt Rippy.
Bisher hat es sich still im Hintergrund aktualisiert — jetzt steht eine Der graue Windows-Rahmen ist weg. Minimieren, Maximieren und Schließen
Karte oben in der Übersicht, mit Versionsnummer und dieser Liste hier. sitzen oben rechts im Programm selbst, gezogen wird an der Kopfzeile —
In den Einstellungen gibt es dazu den Knopf "Jetzt auf Updates prüfen". Doppelklick darauf maximiert. Die Kanten lassen sich weiter ziehen wie
Wer nicht warten will, startet über die Karte sofort neu. gewohnt. Schließen versteckt Rippy weiterhin nur im Infobereich, ein
laufender Rip merkt davon nichts.
Nichts klebt mehr links.
Die Seite ist jetzt mittig und wächst mit dem Fenster mit, statt auf
halber Breite abzubrechen. Im schmalen Fenster rutschen die Kacheln
untereinander, statt sich zu quetschen; die Einstellungen legen ihre
Abschnittswahl dann als Zeile über die Karten.
Aus 5.1.2: Rippy sagt Bescheid, wenn ein Update da ist.
Diese Karte hier ist der Beweis — mit Versionsnummer und dieser Liste.
In den Einstellungen gibt es den Knopf "Jetzt auf Updates prüfen".
Aus 5.1.1: Serien-Discs werden richtig erkannt. Aus 5.1.1: Serien-Discs werden richtig erkannt.
Disc 2 von "Spartacus: Gods of the Arena" wurde als der Film "Spartacus" Disc 2 von "Spartacus: Gods of the Arena" galt als der Film "Spartacus"
von 1960 erkannt. Rippy fragt jetzt die Disc selbst: Ihr von 1960. Rippy fragt jetzt die Disc selbst: Ihr Inhaltsverzeichnis
Inhaltsverzeichnis nennt die Folgen, und daran erkennt Rippy eine Serie, nennt die Folgen. Disc-Nummer und Folgenzahl stehen neben dem Titel.
statt sie zu raten. Disc-Nummer und Folgenzahl stehen neben dem Titel.
Und wenn Rippy sich unsicher ist, sagt es das jetzt, statt sich sicher
zu irren.
Unverändert: Updates warten immer, bis kein Rip mehr läuft. Unverändert: Updates warten immer, bis kein Rip mehr läuft.
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "rippy", "name": "rippy",
"version": "5.1.2", "version": "5.1.3",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "rippy", "name": "rippy",
"version": "5.1.2", "version": "5.1.3",
"license": "UNLICENSED", "license": "UNLICENSED",
"dependencies": { "dependencies": {
"electron-updater": "^6.8.9", "electron-updater": "^6.8.9",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "rippy", "name": "rippy",
"productName": "Rippy", "productName": "Rippy",
"version": "5.1.2", "version": "5.1.3",
"description": "Rippy v5 — Disc-Ripping als eigenständiges Windows-Programm (KONZEPT-WINDOWS.md)", "description": "Rippy v5 — Disc-Ripping als eigenständiges Windows-Programm (KONZEPT-WINDOWS.md)",
"main": "out/haupt/index.js", "main": "out/haupt/index.js",
"author": "KrBrZ", "author": "KrBrZ",
+5
View File
@@ -56,6 +56,11 @@ contextBridge.exposeInMainWorld('rippy', {
autostartSetzen(an: boolean): Promise<boolean> { autostartSetzen(an: boolean): Promise<boolean> {
return ipcRenderer.invoke('autostart-setzen', an === true) as Promise<boolean> return ipcRenderer.invoke('autostart-setzen', an === true) as Promise<boolean>
}, },
/** Eigene Titelleiste (§ 4.2): minimieren | maximieren | schliessen.
* „schliessen" versteckt nur — Rippy lebt im Tray weiter. */
fensterSteuern(was: 'minimieren' | 'maximieren' | 'schliessen'): Promise<boolean> {
return ipcRenderer.invoke('fenster-steuern', String(was)) as Promise<boolean>
},
/** „Jetzt prüfen" (§ 6.8) — das Ergebnis kommt über den Haupt-Status. */ /** „Jetzt prüfen" (§ 6.8) — das Ergebnis kommt über den Haupt-Status. */
updatePruefen(): Promise<unknown> { updatePruefen(): Promise<unknown> {
return ipcRenderer.invoke('update-pruefen') as Promise<unknown> return ipcRenderer.invoke('update-pruefen') as Promise<unknown>
+97 -13
View File
@@ -83,6 +83,60 @@ const PHASEN_MONO: Record<RipStatus['phase'], string> = {
const LAUFENDE_PHASEN = ['liest-info', 'rippt', 'komprimiert', 'legt-ab'] const LAUFENDE_PHASEN = ['liest-info', 'rippt', 'komprimiert', 'legt-ab']
// ── Eigene Fensterknöpfe (§ 4.2, seit 5.1.3) ────────────────────────────
// Bewusst schlicht und in Windows-Maßen (46 × 32) — wer sie sucht, findet
// sie dort, wo er sie erwartet. Sie sitzen in der Ziehfläche und müssen
// deshalb `nicht-ziehbar` sein, sonst verschluckt der Fenstergriff den Klick.
const IconMinimieren = (): React.JSX.Element => (
<svg width="10" height="10" viewBox="0 0 10 10" aria-hidden="true">
<path d="M0 5h10" stroke="currentColor" strokeWidth="1.1" />
</svg>
)
const IconMaximieren = (): React.JSX.Element => (
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" aria-hidden="true">
<rect x="0.6" y="0.6" width="8.8" height="8.8" stroke="currentColor" strokeWidth="1.1" />
</svg>
)
const IconWiederherstellen = (): React.JSX.Element => (
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" aria-hidden="true">
<rect x="0.6" y="2.6" width="6.8" height="6.8" stroke="currentColor" strokeWidth="1.1" />
<path d="M2.7 2.6V0.6h6.7v6.7h-2" stroke="currentColor" strokeWidth="1.1" />
</svg>
)
const IconSchliessen = (): React.JSX.Element => (
<svg width="10" height="10" viewBox="0 0 10 10" aria-hidden="true">
<path d="M0.6 0.6l8.8 8.8M9.4 0.6l-8.8 8.8" stroke="currentColor" strokeWidth="1.1" />
</svg>
)
function FensterKnopf({
titel,
aufKlick,
rot = false,
children,
}: {
titel: string
aufKlick: () => void
rot?: boolean
children: React.ReactNode
}) {
return (
<button
onClick={aufKlick}
title={titel}
aria-label={titel}
className={`flex h-8 w-[46px] items-center justify-center text-dunst transition-colors ${
rot ? 'hover:bg-rot hover:text-white' : 'hover:bg-white/10 hover:text-schnee'
}`}
>
{children}
</button>
)
}
function sicherheitsWort(confidence: number): string { function sicherheitsWort(confidence: number): string {
if (confidence >= 0.9) return 'sehr wahrscheinlich' if (confidence >= 0.9) return 'sehr wahrscheinlich'
if (confidence >= 0.8) return 'wahrscheinlich' if (confidence >= 0.8) return 'wahrscheinlich'
@@ -427,7 +481,9 @@ function LaufwerkKachel({
</p> </p>
)} )}
<div className="mt-3 grid max-w-2xl grid-cols-4 gap-2.5"> {/* Zwei Kacheln nebeneinander im schmalen Fenster, vier im
breiten — vorher immer vier, also im schmalen gequetscht. */}
<div className="mt-3 grid max-w-3xl grid-cols-2 gap-2.5 sm:grid-cols-4">
<InfoKachel label="MEDIUM" wert={TYP_NAMEN[geraet.typ]} /> <InfoKachel label="MEDIUM" wert={TYP_NAMEN[geraet.typ]} />
<InfoKachel label="GRÖSSE" wert={geraet.groesseBytes > 0 ? gb(geraet.groesseBytes) : '—'} /> <InfoKachel label="GRÖSSE" wert={geraet.groesseBytes > 0 ? gb(geraet.groesseBytes) : '—'} />
<InfoKachel <InfoKachel
@@ -807,9 +863,12 @@ function Einstellungen({ verbindung, haupt }: { verbindung: Verbindung; haupt: H
if (ordner !== null) setzen('ablage', ordner) if (ordner !== null) setzen('ablage', ordner)
} }
// Schmales Fenster: die Sektionswahl legt sich als Zeile über die Karten.
// Ab lg steht sie wieder als mitlaufende Spalte daneben (Commander-Fund
// 01.09.2026: „alles gequetscht").
return ( return (
<div className="flex gap-8"> <div className="flex flex-col gap-5 lg:flex-row lg:gap-8">
<nav className="sticky top-4 flex h-fit w-44 flex-none flex-col gap-1"> <nav className="flex flex-row flex-wrap gap-1 lg:sticky lg:top-4 lg:h-fit lg:w-44 lg:flex-none lg:flex-col">
{SEKTIONEN.map(([id, name]) => ( {SEKTIONEN.map(([id, name]) => (
<button <button
key={id} key={id}
@@ -825,7 +884,9 @@ function Einstellungen({ verbindung, haupt }: { verbindung: Verbindung; haupt: H
))} ))}
</nav> </nav>
<div className="min-w-0 max-w-2xl flex-1 space-y-5"> {/* Kein max-w mehr: Der 672-px-Deckel war der Grund, warum in einem
breiten Fenster alles links klebte und rechts nichts stand. */}
<div className="min-w-0 flex-1 space-y-5">
<SektionsKarte <SektionsKarte
id="ablage" id="ablage"
titel="ABLAGE" titel="ABLAGE"
@@ -1395,30 +1456,53 @@ export default function App() {
<div className="buehnenlicht pointer-events-none fixed inset-0" /> <div className="buehnenlicht pointer-events-none fixed inset-0" />
<div className="filmkorn pointer-events-none fixed inset-0" /> <div className="filmkorn pointer-events-none fixed inset-0" />
<header className="relative border-b border-white/8 px-7 py-3.5"> {/* Die Kopfzeile IST die Titelleiste: Sie ist die Ziehfläche des
<div className="flex items-center justify-between gap-3"> rahmenlosen Fensters (§ 4.2). Alles Bedienbare darin trägt
<div className="flex items-center gap-3.5"> `nicht-ziehbar`, sonst schluckt der Griff den Klick. */}
<IconDisc groesse={30} farbe="oklch(0.83 0.11 75)" /> <header className="ziehbar relative flex-none border-b border-white/8">
<h1 className="font-display text-[25px] font-bold tracking-tight">Rippy</h1> <div className="mx-auto flex w-full max-w-[1600px] items-center gap-3 py-2 pl-5 pr-0 sm:pl-7">
<span className="rounded-md border border-white/12 px-2 py-0.5 font-mono text-[10px] text-nebel"> <div className="flex min-w-0 items-center gap-3">
<IconDisc groesse={28} farbe="oklch(0.83 0.11 75)" />
<h1 className="font-display text-[22px] font-bold tracking-tight sm:text-[25px]">Rippy</h1>
<span className="hidden rounded-md border border-white/12 px-2 py-0.5 font-mono text-[10px] text-nebel md:inline">
v{haupt?.version ?? '…'} v{haupt?.version ?? '…'}
</span> </span>
{einRipLaeuft && ( {einRipLaeuft && (
<span className="inline-flex items-center gap-2 rounded-md border border-rec/45 bg-rec/12 px-2.5 py-0.5 font-mono text-[10px] tracking-[0.15em] text-[oklch(0.83_0.12_50)]"> <span className="inline-flex flex-none items-center gap-2 rounded-md border border-rec/45 bg-rec/12 px-2.5 py-0.5 font-mono text-[10px] tracking-[0.15em] text-[oklch(0.83_0.12_50)]">
<span className="inline-block h-1.5 w-1.5 animate-pulse rounded-full bg-rec shadow-[0_0_9px_oklch(0.72_0.17_45/0.9)]" /> <span className="inline-block h-1.5 w-1.5 animate-pulse rounded-full bg-rec shadow-[0_0_9px_oklch(0.72_0.17_45/0.9)]" />
REC REC
</span> </span>
)} )}
</div> </div>
<nav className="flex gap-1.5"> <nav className="nicht-ziehbar ml-auto flex flex-wrap justify-end gap-1.5">
{knopf('uebersicht', 'Übersicht')} {knopf('uebersicht', 'Übersicht')}
{knopf('einstellungen', 'Einstellungen')} {knopf('einstellungen', 'Einstellungen')}
{knopf('bibliothek', `Bibliothek${verbindung.bibliothek.length > 0 ? ` (${verbindung.bibliothek.length})` : ''}`)} {knopf('bibliothek', `Bibliothek${verbindung.bibliothek.length > 0 ? ` (${verbindung.bibliothek.length})` : ''}`)}
</nav> </nav>
<div className="nicht-ziehbar ml-1 flex flex-none items-center self-start">
<FensterKnopf titel="Minimieren" aufKlick={() => void window.rippy.fensterSteuern('minimieren')}>
<IconMinimieren />
</FensterKnopf>
<FensterKnopf
titel={haupt?.fensterMaximiert === true ? 'Wiederherstellen' : 'Maximieren'}
aufKlick={() => void window.rippy.fensterSteuern('maximieren')}
>
{haupt?.fensterMaximiert === true ? <IconWiederherstellen /> : <IconMaximieren />}
</FensterKnopf>
{/* Schließen versteckt nur — Rippy lebt im Tray weiter, ein
laufender Rip merkt davon nichts (§ 4.1 Punkt 3). */}
<FensterKnopf
titel="In den Infobereich (Rippy läuft weiter)"
rot
aufKlick={() => void window.rippy.fensterSteuern('schliessen')}
>
<IconSchliessen />
</FensterKnopf>
</div>
</div> </div>
</header> </header>
<main className="relative flex-1 space-y-7 p-7"> <main className="relative mx-auto w-full max-w-[1600px] flex-1 space-y-6 px-5 py-6 sm:px-7 sm:py-7">
{setupOffen && <ErstEinrichtung verbindung={verbindung} />} {setupOffen && <ErstEinrichtung verbindung={verbindung} />}
{!setupOffen && bereich === 'uebersicht' && ( {!setupOffen && bereich === 'uebersicht' && (
<> <>
+1
View File
@@ -11,6 +11,7 @@ declare global {
autostartSetzen(an: boolean): Promise<boolean> autostartSetzen(an: boolean): Promise<boolean>
updatePruefen(): Promise<unknown> updatePruefen(): Promise<unknown>
updateInstallieren(): Promise<{ ok: boolean; grund: string }> updateInstallieren(): Promise<{ ok: boolean; grund: string }>
fensterSteuern(was: 'minimieren' | 'maximieren' | 'schliessen'): Promise<boolean>
} }
} }
} }
+12
View File
@@ -59,6 +59,18 @@
box-shadow: 0 0 8px oklch(0.8 0.12 70 / 0.45); box-shadow: 0 0 8px oklch(0.8 0.12 70 / 0.45);
} }
/* Eigene Titelleiste (§ 4.2): Das Fenster hat seit 5.1.3 keinen
Windows-Rahmen mehr (frame:false) — gezogen wird an der Kopfzeile.
`drag` macht eine Fläche zum Fenstergriff; ALLES Bedienbare darin
braucht `nicht-ziehbar`, sonst verschluckt der Griff den Klick.
Doppelklick auf den Griff maximiert, das macht Windows von selbst. */
.ziehbar {
-webkit-app-region: drag;
}
.nicht-ziehbar {
-webkit-app-region: no-drag;
}
/* Dezente Scrollbalken — helle Standardbalken zerschneiden die Bühne. */ /* Dezente Scrollbalken — helle Standardbalken zerschneiden die Bühne. */
::-webkit-scrollbar { width: 10px; height: 10px; } ::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-thumb { background: oklch(1 0 0 / 0.12); border-radius: 999px; border: 2px solid transparent; background-clip: content-box; } ::-webkit-scrollbar-thumb { background: oklch(1 0 0 / 0.12); border-radius: 999px; border: 2px solid transparent; background-clip: content-box; }
@@ -191,6 +191,9 @@ export interface HauptStatus {
autostart: boolean autostart: boolean
/** Selbst-Update (§ 6.8). */ /** Selbst-Update (§ 6.8). */
update: UpdateStand update: UpdateStand
/** Für die eigene Titelleiste (§ 4.2, seit 5.1.3): Ist das Fenster
* gerade maximiert? Entscheidet nur, welches Symbol der Knopf zeigt. */
fensterMaximiert: boolean
} }
/** Der Stand des Selbst-Updates (§ 6.8) — EINE Definition, die sich Haupt /** Der Stand des Selbst-Updates (§ 6.8) — EINE Definition, die sich Haupt
+13 -4
View File
@@ -7,12 +7,21 @@ import { join } from 'node:path'
export function fensterErstellen(): BrowserWindow { export function fensterErstellen(): BrowserWindow {
const fenster = new BrowserWindow({ const fenster = new BrowserWindow({
width: 1000, width: 1280,
height: 700, height: 820,
minWidth: 720, minWidth: 860,
minHeight: 480, minHeight: 560,
show: true, show: true,
autoHideMenuBar: true, autoHideMenuBar: true,
// Eigene Titelleiste (Commander-Wunsch 01.09.2026): Der Windows-Rahmen
// ist weg, Minimieren/Maximieren/Schließen sitzen im UI und gezogen
// wird an der Kopfzeile (.ziehbar in fenster/stil.css).
//
// Wichtig: GRÖSSE ÄNDERN bleibt möglich. Electron setzt für rahmenlose
// Fenster unter Windows weiterhin WS_THICKFRAME (Option `thickFrame`,
// Standard true) — die Kanten lassen sich also weiter ziehen, und
// Doppelklick auf die Ziehfläche maximiert wie gewohnt.
frame: false,
// Dunkler Grund SOFORT — sonst blitzt beim Start ein weißes Rechteck, // Dunkler Grund SOFORT — sonst blitzt beim Start ein weißes Rechteck,
// bevor React lädt. Das warme Bühnen-Schwarz des Kino-Mixes // bevor React lädt. Das warme Bühnen-Schwarz des Kino-Mixes
// (oklch(0.13 0.012 60), siehe fenster/stil.css). // (oklch(0.13 0.012 60), siehe fenster/stil.css).
+28
View File
@@ -176,6 +176,9 @@ function start(): void {
kern: kernVerwaltung.zustand(), kern: kernVerwaltung.zustand(),
autostart: autostartAktiv(), autostart: autostartAktiv(),
update: updateStand(), update: updateStand(),
// Für die eigene Titelleiste: Das Knopf-Symbol muss wissen, ob das
// Fenster gerade maximiert ist.
fensterMaximiert: fenster !== null && !fenster.isDestroyed() && fenster.isMaximized(),
} }
fenster.webContents.send('haupt-status', status) fenster.webContents.send('haupt-status', status)
} }
@@ -189,6 +192,11 @@ function start(): void {
statusSenden() statusSenden()
smokePruefen() smokePruefen()
}) })
// Die eigene Titelleiste zeichnet ihr Maximieren-Symbol nach diesem
// Zustand — auch wenn per Doppelklick oder Windows-Tastenkürzel
// maximiert wird, nicht nur über unseren Knopf.
fenster.on('maximize', () => statusSenden())
fenster.on('unmaximize', () => statusSenden())
// § 4.1 Punkt 3: Fenster zu, Rip läuft weiter — Rippy lebt im Tray. // § 4.1 Punkt 3: Fenster zu, Rip läuft weiter — Rippy lebt im Tray.
fenster.on('close', (ereignis) => { fenster.on('close', (ereignis) => {
if (beendenGewollt) return if (beendenGewollt) return
@@ -223,6 +231,26 @@ function start(): void {
} }
}) })
// Eigene Titelleiste (§ 4.2): Das Fenster hat keinen Windows-Rahmen
// mehr, die drei Knöpfe sitzen im UI und melden sich hier.
ipcMain.handle('fenster-steuern', (_ereignis, was: unknown) => {
if (fenster === null || fenster.isDestroyed()) return false
if (was === 'minimieren') {
fenster.minimize()
} else if (was === 'maximieren') {
if (fenster.isMaximized()) fenster.unmaximize()
else fenster.maximize()
} else if (was === 'schliessen') {
// Genau der Weg des alten Fensterkreuzes: Der close-Horcher weiter
// unten versteckt nur — Rippy lebt im Tray weiter (§ 4.1 Punkt 3),
// ein laufender Rip darf davon nichts merken.
fenster.close()
} else {
return false
}
return fenster.isDestroyed() ? false : fenster.isMaximized()
})
// Der Knopf „Jetzt prüfen" (§ 6.8). Der Stand meldet sich über // Der Knopf „Jetzt prüfen" (§ 6.8). Der Stand meldet sich über
// beiAenderung von selbst ans Fenster — hier gibt es nichts zurück. // beiAenderung von selbst ans Fenster — hier gibt es nichts zurück.
ipcMain.handle('update-pruefen', () => { ipcMain.handle('update-pruefen', () => {