Merge #2: Seitenleiste bleibt stehen, Benutzermenue wieder erreichbar
Behebt Issue #1: "Mein Konto" war am Schreibtisch praktisch nicht zu finden, weil die Seitenleiste mit der Seite mitwuchs und ihr Fuss am Seitenende landete. Closes #1
This commit was merged in pull request #2.
This commit is contained in:
+39
-1
@@ -26,6 +26,43 @@ Wiederherstellung*.
|
|||||||
|
|
||||||
_Noch nichts._
|
_Noch nichts._
|
||||||
|
|
||||||
|
## [1.1.2] – 2026-08-23
|
||||||
|
|
||||||
|
### Behoben
|
||||||
|
|
||||||
|
- **„Mein Konto" war am Schreibtisch praktisch nicht zu finden**
|
||||||
|
([#1](https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/issues/1)).
|
||||||
|
Damit kam man dort auch nicht an den Passwortwechsel, den zweiten Faktor
|
||||||
|
und die frisch eingebaute Passkey-Verwaltung.
|
||||||
|
|
||||||
|
Ursache: Die Seitenleiste hatte keine feste Höhe und wuchs mit der Seite
|
||||||
|
mit. Ihr Fuß — und damit das Benutzermenü — landete deshalb am **Ende der
|
||||||
|
Seite** statt am unteren Bildschirmrand. Auf „Heute" waren das 1632 Pixel
|
||||||
|
bei 900 Pixel Fensterhöhe: Man musste bis ganz nach unten scrollen, um
|
||||||
|
an sein eigenes Konto zu kommen.
|
||||||
|
|
||||||
|
Der Fehler sah nach nichts aus. Kein Element fehlte, nichts war kaputt,
|
||||||
|
die Seite sah richtig aus — und genau deshalb sucht man an der falschen
|
||||||
|
Stelle.
|
||||||
|
|
||||||
|
Die Leiste bleibt jetzt stehen (`sticky`, Fensterhöhe). Kopf,
|
||||||
|
Betreutenauswahl und Fuß sind immer sichtbar; die Navigation dazwischen
|
||||||
|
scrollt bei Bedarf in sich selbst. Am Handy war es nie ein Problem —
|
||||||
|
dort sitzt das Menü in der ohnehin festen Kopfzeile.
|
||||||
|
|
||||||
|
### Geprüft
|
||||||
|
|
||||||
|
- Fünf End-to-End-Tests (`e2e/benutzermenue.spec.ts`) fragen nicht, ob das
|
||||||
|
Menü **im DOM** ist, sondern ob es **im sichtbaren Bereich** steht: auf
|
||||||
|
fünf verschiedenen Seiten, an drei Scrollpositionen, auf einem flachen
|
||||||
|
Laptopfenster (1280 × 620) und am Handy.
|
||||||
|
|
||||||
|
Die Scrollposition in der Mitte ist Absicht. Nur oben und unten zu messen
|
||||||
|
hätte nicht gereicht: Am Seitenende wird eine mitwachsende Leiste
|
||||||
|
zufällig doch sichtbar — der Test wäre grün gewesen und der Fehler
|
||||||
|
trotzdem da. Beide Sichtbarkeitstests sind gegen den kaputten Zustand
|
||||||
|
gegengeprüft und schlagen dort an.
|
||||||
|
|
||||||
## [1.1.1] – 2026-08-23
|
## [1.1.1] – 2026-08-23
|
||||||
|
|
||||||
### Behoben
|
### Behoben
|
||||||
@@ -511,7 +548,8 @@ Was davon wann angegangen wird, steht in [`docs/ROADMAP.md`](docs/ROADMAP.md).
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
[Unveröffentlicht]: https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/compare/v1.1.1...HEAD
|
[Unveröffentlicht]: https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/compare/v1.1.2...HEAD
|
||||||
|
[1.1.2]: https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/compare/v1.1.1...v1.1.2
|
||||||
[1.1.1]: https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/compare/v1.1.0...v1.1.1
|
[1.1.1]: https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/compare/v1.1.0...v1.1.1
|
||||||
[1.1.0]: https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/compare/v1.0.5...v1.1.0
|
[1.1.0]: https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/compare/v1.0.5...v1.1.0
|
||||||
[1.0.5]: https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/compare/v1.0.4...v1.0.5
|
[1.0.5]: https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/compare/v1.0.4...v1.0.5
|
||||||
|
|||||||
@@ -580,11 +580,11 @@ Fehler, die während des Baus aufgefallen sind.
|
|||||||
|
|
||||||
| | | nachzuzählen mit |
|
| | | nachzuzählen mit |
|
||||||
|---|---:|---|
|
|---|---:|---|
|
||||||
| Zeilen TypeScript/TSX | 72.080 | `git ls-files "*.ts" "*.tsx" \| xargs wc -l` |
|
| Zeilen TypeScript/TSX | 73.774 | `git ls-files "*.ts" "*.tsx" \| xargs wc -l` |
|
||||||
| Prisma-Modelle | 33 | `grep -c '^model ' prisma/schema.prisma` |
|
| Prisma-Modelle | 33 | `grep -c '^model ' prisma/schema.prisma` |
|
||||||
| Seiten | 79 | `find src/app -name page.tsx \| wc -l` |
|
| Seiten | 79 | `find src/app -name page.tsx \| wc -l` |
|
||||||
| Unit-Tests (Vitest) | 500 | `npm test` |
|
| Unit-Tests (Vitest) | 500 | `npm test` |
|
||||||
| End-to-End-Tests (Playwright) | 37 | `npm run test:e2e` |
|
| End-to-End-Tests (Playwright) | 45 | `npm run test:e2e` — 26 Tests über zwei Geräteprofile, geräteabhängige übersprungen |
|
||||||
| TypeScript-Fehler | 0 | `npm run typecheck` |
|
| TypeScript-Fehler | 0 | `npm run typecheck` |
|
||||||
|
|
||||||
Die Befehle stehen dabei, weil die Zahlen sonst altern, ohne dass es
|
Die Befehle stehen dabei, weil die Zahlen sonst altern, ohne dass es
|
||||||
|
|||||||
@@ -131,14 +131,14 @@ cd /opt/betreuungs-cockpit
|
|||||||
|
|
||||||
# Auf die letzte veröffentlichte Fassung wechseln,
|
# Auf die letzte veröffentlichte Fassung wechseln,
|
||||||
# nicht auf den laufenden Entwicklungsstand
|
# nicht auf den laufenden Entwicklungsstand
|
||||||
git checkout v1.1.1
|
git checkout v1.1.2
|
||||||
```
|
```
|
||||||
|
|
||||||
### Kontrolle
|
### Kontrolle
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ls docker-compose.yml Dockerfile .env.example # alle drei da
|
ls docker-compose.yml Dockerfile .env.example # alle drei da
|
||||||
git describe --tags # v1.1.1
|
git describe --tags # v1.1.2
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -299,7 +299,7 @@ betreuungs-cockpit-sicherung-1 Up
|
|||||||
```bash
|
```bash
|
||||||
# Antwortet die Anwendung? (Port anpassen, falls du APP_PORT gesetzt hast)
|
# Antwortet die Anwendung? (Port anpassen, falls du APP_PORT gesetzt hast)
|
||||||
curl -s http://127.0.0.1:3000/api/gesundheit
|
curl -s http://127.0.0.1:3000/api/gesundheit
|
||||||
# {"status":"bereit","version":"1.1.1","commit":"..."}
|
# {"status":"bereit","version":"1.1.2","commit":"..."}
|
||||||
```
|
```
|
||||||
|
|
||||||
Kommt hier nichts, sieh in die Logs:
|
Kommt hier nichts, sieh in die Logs:
|
||||||
@@ -649,7 +649,7 @@ Erst **vom Proxy-Server aus** — kommt er überhaupt an?
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -s http://10.0.0.5:3000/api/gesundheit
|
curl -s http://10.0.0.5:3000/api/gesundheit
|
||||||
# {"status":"bereit","version":"1.1.1",...}
|
# {"status":"bereit","version":"1.1.2",...}
|
||||||
```
|
```
|
||||||
|
|
||||||
Kommt hier nichts, liegt es an 7a: falsche `APP_BIND`, Firewall, oder der
|
Kommt hier nichts, liegt es an 7a: falsche `APP_BIND`, Firewall, oder der
|
||||||
@@ -700,7 +700,7 @@ Zum Schluss: **Fristen → Pflichtfristen ableiten**.
|
|||||||
- Du kommst mit E-Mail, Passwort und dem Code aus der App herein.
|
- Du kommst mit E-Mail, Passwort und dem Code aus der App herein.
|
||||||
- Unter **Fristen** stehen Einträge mit Paragraphenbezug.
|
- Unter **Fristen** stehen Einträge mit Paragraphenbezug.
|
||||||
- Unter **Protokoll** siehst du deine eigene Anmeldung.
|
- Unter **Protokoll** siehst du deine eigene Anmeldung.
|
||||||
- Unten links in der Seitenleiste steht `Version 1.1.1 · <commit>`.
|
- Unten links in der Seitenleiste steht `Version 1.1.2 · <commit>`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -913,7 +913,7 @@ git fetch --tags
|
|||||||
git log --oneline HEAD..origin/main
|
git log --oneline HEAD..origin/main
|
||||||
|
|
||||||
# 3. Umschalten
|
# 3. Umschalten
|
||||||
git checkout v1.1.1
|
git checkout v1.1.2
|
||||||
|
|
||||||
# 4. Neu bauen und starten
|
# 4. Neu bauen und starten
|
||||||
GIT_COMMIT=$(git rev-parse --short HEAD) BUILD_ZEIT=$(date -Iseconds) docker compose up -d --build
|
GIT_COMMIT=$(git rev-parse --short HEAD) BUILD_ZEIT=$(date -Iseconds) docker compose up -d --build
|
||||||
|
|||||||
+1
-1
@@ -9,7 +9,7 @@ Zeit spart. Er steht weit unten, wenn er nur schön wäre.
|
|||||||
Diese Datei ist eine Absichtserklärung, kein Vertrag. Was sich im Betrieb
|
Diese Datei ist eine Absichtserklärung, kein Vertrag. Was sich im Betrieb
|
||||||
als wichtiger herausstellt, rutscht nach oben.
|
als wichtiger herausstellt, rutscht nach oben.
|
||||||
|
|
||||||
Stand: 23.08.2026 · Aktuelle Fassung: **1.1.1** ([CHANGELOG](../CHANGELOG.md))
|
Stand: 23.08.2026 · Aktuelle Fassung: **1.1.2** ([CHANGELOG](../CHANGELOG.md))
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
import { expect, test } from '@playwright/test'
|
||||||
|
import type { Page } from '@playwright/test'
|
||||||
|
import { anmelden } from './hilfen'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Das Benutzermenü muss erreichbar sein, ohne danach zu suchen.
|
||||||
|
*
|
||||||
|
* Anlass ist Issue #1: Am Schreibtisch war „Mein Konto" praktisch nicht
|
||||||
|
* zu finden — und damit auch nicht der Passwortwechsel, der zweite Faktor
|
||||||
|
* und die Passkey-Verwaltung. Die Seitenleiste hatte keine feste Höhe und
|
||||||
|
* wuchs mit der Seite mit; ihr Fuß landete deshalb am Ende der Seite statt
|
||||||
|
* am unteren Bildschirmrand. Auf `/heute` waren das 1632 Pixel bei 900
|
||||||
|
* Pixel Fensterhöhe.
|
||||||
|
*
|
||||||
|
* Der Fehler sah nach nichts aus: Kein Element fehlte, nichts war kaputt,
|
||||||
|
* die Seite sah richtig aus. Man musste nur ans Seitenende scrollen — und
|
||||||
|
* darauf kommt niemand, der eine Seitenleiste vor sich hat.
|
||||||
|
*
|
||||||
|
* Geprüft wird deshalb nicht „ist das Element im DOM", sondern „steht es
|
||||||
|
* im sichtbaren Bereich".
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** Der Auslöser des Benutzermenüs in der Seitenleiste am Schreibtisch. */
|
||||||
|
function ausloeserInSeitenleiste(seite: Page) {
|
||||||
|
return seite.locator('aside button[aria-haspopup="menu"]')
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Steht das Element vollständig im Fenster, ohne zu scrollen? */
|
||||||
|
async function imSichtbarenBereich(seite: Page, auswahl: string): Promise<boolean> {
|
||||||
|
return seite.evaluate((sel) => {
|
||||||
|
const el = document.querySelector(sel)
|
||||||
|
if (!el) return false
|
||||||
|
const k = el.getBoundingClientRect()
|
||||||
|
return k.top >= 0 && k.bottom <= window.innerHeight && k.height > 0
|
||||||
|
}, auswahl)
|
||||||
|
}
|
||||||
|
|
||||||
|
test.describe('Benutzermenü am Schreibtisch', () => {
|
||||||
|
test.skip(({ isMobile }) => Boolean(isMobile), 'Am Handy sitzt es in der Kopfzeile.')
|
||||||
|
|
||||||
|
test('steht auch auf langen Seiten im Bild, ohne zu scrollen', async ({ page }) => {
|
||||||
|
await anmelden(page)
|
||||||
|
|
||||||
|
// Seiten, die laenger sind als das Fenster - genau dort trat der
|
||||||
|
// Fehler auf. /fristen ist als Gegenprobe kurz.
|
||||||
|
for (const ziel of ['/heute', '/vorgaenge', '/dokumente', '/finanzen', '/fristen']) {
|
||||||
|
await page.goto(ziel)
|
||||||
|
await page.waitForLoadState('networkidle')
|
||||||
|
|
||||||
|
const sichtbar = await imSichtbarenBereich(page, 'aside button[aria-haspopup="menu"]')
|
||||||
|
expect(sichtbar, `Benutzermenü nicht im Bild auf ${ziel}`).toBe(true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('bleibt an jeder Scrollposition stehen', async ({ page }) => {
|
||||||
|
await anmelden(page)
|
||||||
|
await page.goto('/heute')
|
||||||
|
await page.waitForLoadState('networkidle')
|
||||||
|
|
||||||
|
// Bewusst auch die Mitte pruefen. Nur oben und unten zu messen genuegt
|
||||||
|
// nicht: Eine mitwachsende Leiste ist am Seitenende zufaellig doch
|
||||||
|
// sichtbar - der Test waere dann gruen und der Fehler trotzdem da.
|
||||||
|
for (const anteil of [0, 0.5, 1]) {
|
||||||
|
await page.evaluate((a) => {
|
||||||
|
const hoehe = document.documentElement.scrollHeight - window.innerHeight
|
||||||
|
window.scrollTo(0, hoehe * a)
|
||||||
|
}, anteil)
|
||||||
|
await page.waitForTimeout(250)
|
||||||
|
|
||||||
|
const sichtbar = await imSichtbarenBereich(page, 'aside button[aria-haspopup="menu"]')
|
||||||
|
expect(sichtbar, `Benutzermenü nicht im Bild bei ${anteil * 100}% Scrollposition`).toBe(true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('führt in zwei Klicks nach „Mein Konto"', async ({ page }) => {
|
||||||
|
await anmelden(page)
|
||||||
|
await page.goto('/heute')
|
||||||
|
|
||||||
|
await ausloeserInSeitenleiste(page).click()
|
||||||
|
await page.getByRole('menuitem', { name: 'Mein Konto' }).click()
|
||||||
|
|
||||||
|
await page.waitForURL('**/konto')
|
||||||
|
// Und dort steht auch, worum es im Issue ging.
|
||||||
|
await expect(page.getByRole('heading', { name: 'Mein Konto' })).toBeVisible()
|
||||||
|
await expect(page.getByRole('heading', { name: 'Passwort ändern' })).toBeVisible()
|
||||||
|
await expect(page.getByRole('heading', { name: 'Passkeys' })).toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('bleibt auf einem flachen Fenster erreichbar', async ({ page }) => {
|
||||||
|
// Ein Laptop mit 1280x620 ist knapp: Kopf, Betreutenauswahl,
|
||||||
|
// Navigation und Fuss muessen zusammen hineinpassen. Die Navigation
|
||||||
|
// dazwischen darf scrollen, der Fuss nicht verschwinden.
|
||||||
|
await anmelden(page)
|
||||||
|
await page.setViewportSize({ width: 1280, height: 620 })
|
||||||
|
await page.goto('/heute')
|
||||||
|
await page.waitForLoadState('networkidle')
|
||||||
|
|
||||||
|
expect(await imSichtbarenBereich(page, 'aside button[aria-haspopup="menu"]')).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test.describe('Benutzermenü am Handy', () => {
|
||||||
|
test.skip(({ isMobile }) => !isMobile, 'Am Schreibtisch sitzt es in der Seitenleiste.')
|
||||||
|
|
||||||
|
test('steht in der Kopfzeile und bleibt beim Scrollen stehen', async ({ page }) => {
|
||||||
|
await anmelden(page)
|
||||||
|
await page.goto('/heute')
|
||||||
|
await page.waitForLoadState('networkidle')
|
||||||
|
|
||||||
|
expect(await imSichtbarenBereich(page, 'header button[aria-haspopup="menu"]')).toBe(true)
|
||||||
|
|
||||||
|
await page.evaluate(() => window.scrollTo(0, document.documentElement.scrollHeight))
|
||||||
|
await page.waitForTimeout(300)
|
||||||
|
|
||||||
|
expect(await imSichtbarenBereich(page, 'header button[aria-haspopup="menu"]')).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "betreuungs-cockpit",
|
"name": "betreuungs-cockpit",
|
||||||
"version": "1.1.1",
|
"version": "1.1.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Arbeits- und Dokumentationswerkzeug fuer die ehrenamtliche rechtliche Betreuung",
|
"description": "Arbeits- und Dokumentationswerkzeug fuer die ehrenamtliche rechtliche Betreuung",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
@@ -56,8 +56,21 @@ export function AppHuelle({
|
|||||||
Zum Inhalt springen
|
Zum Inhalt springen
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
{/* --- Seitenleiste am Schreibtisch --------------------------------- */}
|
{/* --- Seitenleiste am Schreibtisch ---------------------------------
|
||||||
<aside className="no-print hidden w-72 shrink-0 border-r bg-card lg:flex lg:flex-col">
|
|
||||||
|
sticky + h-dvh, nicht einfach mitwachsen lassen: Ohne feste Hoehe
|
||||||
|
wird die Leiste so hoch wie die laengste Seite. Der Fuss mit dem
|
||||||
|
Benutzermenue - und damit der einzige Weg zu "Mein Konto",
|
||||||
|
Passwort, zweitem Faktor und Passkeys - landet dann am Ende der
|
||||||
|
Seite statt am unteren Bildschirmrand. Auf /heute waren das 1632
|
||||||
|
Pixel bei 900 Pixel Fensterhoehe: unerreichbar, ohne dass etwas
|
||||||
|
kaputt aussah.
|
||||||
|
|
||||||
|
self-start gehoert dazu, weil ein Flex-Kind sonst auf die volle
|
||||||
|
Zeilenhoehe gestreckt wird und die feste Hoehe wieder verliert.
|
||||||
|
Die Navigation dazwischen hat bereits overflow-y-auto und scrollt
|
||||||
|
bei Bedarf in sich selbst. */}
|
||||||
|
<aside className="no-print hidden w-72 shrink-0 border-r bg-card lg:sticky lg:top-0 lg:flex lg:h-dvh lg:flex-col lg:self-start">
|
||||||
<div className="flex h-16 items-center border-b px-5">
|
<div className="flex h-16 items-center border-b px-5">
|
||||||
<Link href="/heute" className="flex items-center gap-2 font-semibold">
|
<Link href="/heute" className="flex items-center gap-2 font-semibold">
|
||||||
<span
|
<span
|
||||||
|
|||||||
+1
-1
@@ -17,7 +17,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/** Muss zur "version" in der package.json passen. */
|
/** Muss zur "version" in der package.json passen. */
|
||||||
export const VERSION = '1.1.1'
|
export const VERSION = '1.1.2'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Kurzer Git-Stand, beim Bauen hineingereicht:
|
* Kurzer Git-Stand, beim Bauen hineingereicht:
|
||||||
|
|||||||
Reference in New Issue
Block a user