Compare commits

...
3 Commits
Author SHA1 Message Date
florian.wilde 8a594ee4a4 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
2026-08-22 23:03:53 +00:00
Florian Wilde 0128bf71e7 Seitenleiste bleibt stehen, Benutzermenue wieder erreichbar
Am Schreibtisch war "Mein Konto" praktisch nicht zu finden - und damit
auch nicht Passwortwechsel, zweiter Faktor und Passkey-Verwaltung.

Die Seitenleiste hatte keine feste Hoehe und wuchs mit der Seite mit. Ihr
Fuss mit dem Benutzermenue landete deshalb am Ende der SEITE statt am
unteren Bildschirmrand: auf /heute bei 1632 Pixel in einem 900 Pixel
hohen Fenster. Nachgemessen, nicht geraten - im Browser auf fuenf Seiten.

Der Fehler sah nach nichts aus. Kein Element fehlte, nichts war kaputt,
die Seite sah richtig aus. Man musste nur ganz nach unten scrollen.

Behoben mit lg:sticky, lg:top-0, lg:h-dvh und lg:self-start. Das
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.

Am Handy war es nie ein Problem - dort sitzt das Menue in der ohnehin
festen Kopfzeile.

Neu: e2e/benutzermenue.spec.ts fragt nicht, ob das Menue im DOM ist,
sondern ob es im sichtbaren Bereich steht - auf fuenf Seiten, an drei
Scrollpositionen, auf einem flachen Laptopfenster und am Handy. Die
Position in der Mitte ist Absicht: Am Seitenende wird eine mitwachsende
Leiste zufaellig doch sichtbar, der Test waere gruen und der Fehler
trotzdem da. Beide Sichtbarkeitstests sind gegen den kaputten Zustand
gegengeprueft und schlagen dort an.

Refs #1
2026-08-23 01:00:22 +02:00
Florian Wilde 38f4d8d91f fix: Skripte lagen ohne Ausfuehrungsrecht im Repository
Die Anleitung sagt "./scripts/update.sh" - auf Linux kam dort
"Permission denied". Die drei .sh-Dateien lagen mit Modus 644 statt 755
im Index.

Ursache ist der Entwicklungsrechner: Unter Windows steht
core.filemode=false, git nimmt ein fehlendes Ausfuehrungsrecht dort gar
nicht wahr. Gemeldet hat es der Betrieb, nicht der Bau.

Wer von 1.1.0 oder aelter kommt, braucht einmalig "bash
scripts/update.sh" - das alte Skript ist ja noch das nicht ausfuehrbare.
Steht jetzt in README und Installationsanleitung.

Zusaetzlich setzt update.sh nach dem Umschalten selbst ein chmod +x auf
scripts/*.sh. Das faengt den Fall ab, dass jemand Dateien von Hand
kopiert und das Recht dabei verliert - genau so kam man bisher an dieses
Skript, bevor es in der laufenden Fassung existierte.

Neu: tests/skripte.test.ts prueft nicht Verhalten, sondern Verpackung -
Ausfuehrungsrecht im Index, Shebang, scripts/ im Docker-Abbild, die
Sicherungsskripte in der Compose-Datei. Es ist der zweite Fehler dieser
Art (vorher fehlte scripts/ ganz im Abbild, 1.0.1); beide Male war der
Code richtig und kein Test sah hin, wie die Dateien beim Server
ankommen. Gegenprobe gemacht: mit zurueckgesetztem Recht schlaegt er
fehl und nennt die Datei.

Nebenbei aufgefallen: scripts/version.mjs datierte in UTC. Diese Fassung
waere um 00:08 deutscher Zeit auf den Vortag datiert worden. Rechnet
jetzt in Europe/Berlin.
2026-08-23 00:13:05 +02:00
13 changed files with 349 additions and 16 deletions
+85 -1
View File
@@ -26,6 +26,88 @@ Wiederherstellung*.
_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
### Behoben
- **`./scripts/update.sh` endete in „Permission denied".** Die
Shell-Skripte lagen ohne Ausführungsrecht im Repository (Modus 644 statt
755). Die Anleitung sagt `./scripts/update.sh` — auf Linux ging genau
das nicht.
Ursache ist der Entwicklungsrechner: Unter Windows steht
`core.filemode=false`, dort merkt git von einem fehlenden
Ausführungsrecht nichts. Aufgefallen ist es erst auf dem Server.
**Wer von 1.1.0 oder älter kommt**, braucht einmalig den Umweg — das
alte Skript ist ja noch das nicht ausführbare:
```bash
bash scripts/update.sh
```
Danach stimmt das Recht, und der Punktschrägstrich funktioniert.
- **`update.sh` setzt das Recht nach dem Umschalten selbst.** Wer Dateien
von Hand kopiert hat — etwa um an das Skript zu kommen, bevor es in der
laufenden Fassung existierte —, verliert es dabei. Ein `chmod +x` nach
dem Umschalten fängt das ab.
- **Das Versionsskript datierte in UTC.** Eine Veröffentlichung nach
22 Uhr deutscher Sommerzeit bekam im Changelog das Datum des Vortages —
diese Fassung hier war der erste Fall. Es rechnet jetzt in
`Europe/Berlin`, wie alles andere in dieser Anwendung auch.
### Geprüft
- Vier neue Prüfungen (`tests/skripte.test.ts`) sehen sich nicht das
Programm an, sondern die **Verpackung**: Liegt jedes `.sh` ausführbar im
Index? Hat es eine Shebang-Zeile? Kommt `scripts/` ins Docker-Abbild?
Sind die Sicherungsskripte in der Compose-Datei eingehängt?
Das ist der zweite Fehler dieser Art — vorher fehlte `scripts/` ganz im
Abbild (1.0.1). Beide Male war der Code richtig und kein Test schlug an,
weil keiner hinsah, wie die Dateien beim Server ankommen. Jetzt tut es
einer. Die Gegenprobe ist gemacht: Mit zurückgesetztem Recht schlägt er
fehl und nennt die Datei.
## [1.1.0] – 2026-08-22
### Hinzugefügt
@@ -466,7 +548,9 @@ 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.0...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.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.4]: https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/compare/v1.0.3...v1.0.4
+13 -3
View File
@@ -180,6 +180,16 @@ existiert.
./scripts/update.sh
```
> **Kommst du von 1.1.0 oder älter?** Dort lag `update.sh` ohne
> Ausführungsrecht im Repository — `./scripts/update.sh` endet dann in
> „Permission denied". Einmalig hilft:
>
> ```bash
> bash scripts/update.sh
> ```
>
> Ab 1.1.1 ist das Recht gesetzt und der Punktschrägstrich funktioniert.
Das Skript sichert zuerst, zeigt den Changelog zwischen altem und neuem
Stand, fragt nach, schaltet um, baut neu und führt danach die beiden
SQL-Skripte aus. Schlägt die Sicherung fehl, wird nicht aktualisiert.
@@ -570,11 +580,11 @@ Fehler, die während des Baus aufgefallen sind.
| | | 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` |
| Seiten | 79 | `find src/app -name page.tsx \| wc -l` |
| Unit-Tests (Vitest) | 496 | `npm test` |
| End-to-End-Tests (Playwright) | 37 | `npm run test:e2e` |
| Unit-Tests (Vitest) | 500 | `npm test` |
| 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` |
Die Befehle stehen dabei, weil die Zahlen sonst altern, ohne dass es
+16 -6
View File
@@ -131,14 +131,14 @@ cd /opt/betreuungs-cockpit
# Auf die letzte veröffentlichte Fassung wechseln,
# nicht auf den laufenden Entwicklungsstand
git checkout v1.1.0
git checkout v1.1.2
```
### Kontrolle
```bash
ls docker-compose.yml Dockerfile .env.example # alle drei da
git describe --tags # v1.1.0
git describe --tags # v1.1.2
```
---
@@ -299,7 +299,7 @@ betreuungs-cockpit-sicherung-1 Up
```bash
# Antwortet die Anwendung? (Port anpassen, falls du APP_PORT gesetzt hast)
curl -s http://127.0.0.1:3000/api/gesundheit
# {"status":"bereit","version":"1.1.0","commit":"..."}
# {"status":"bereit","version":"1.1.2","commit":"..."}
```
Kommt hier nichts, sieh in die Logs:
@@ -649,7 +649,7 @@ Erst **vom Proxy-Server aus** — kommt er überhaupt an?
```bash
curl -s http://10.0.0.5:3000/api/gesundheit
# {"status":"bereit","version":"1.1.0",...}
# {"status":"bereit","version":"1.1.2",...}
```
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.
- Unter **Fristen** stehen Einträge mit Paragraphenbezug.
- Unter **Protokoll** siehst du deine eigene Anmeldung.
- Unten links in der Seitenleiste steht `Version 1.1.0 · <commit>`.
- Unten links in der Seitenleiste steht `Version 1.1.2 · <commit>`.
---
@@ -843,6 +843,16 @@ cd /opt/betreuungs-cockpit
./scripts/update.sh
```
> **Kommst du von 1.1.0 oder älter?** Dort lag `update.sh` ohne
> Ausführungsrecht im Repository — `./scripts/update.sh` endet dann in
> „Permission denied". Einmalig hilft:
>
> ```bash
> bash scripts/update.sh
> ```
>
> Ab 1.1.1 ist das Recht gesetzt und der Punktschrägstrich funktioniert.
Das war es. Das Skript nimmt dir die Schritte ab, die man von Hand
vergisst:
@@ -903,7 +913,7 @@ git fetch --tags
git log --oneline HEAD..origin/main
# 3. Umschalten
git checkout v1.1.0
git checkout v1.1.2
# 4. Neu bauen und starten
GIT_COMMIT=$(git rev-parse --short HEAD) BUILD_ZEIT=$(date -Iseconds) docker compose up -d --build
+1 -1
View File
@@ -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
als wichtiger herausstellt, rutscht nach oben.
Stand: 22.08.2026 · Aktuelle Fassung: **1.1.0** ([CHANGELOG](../CHANGELOG.md))
Stand: 23.08.2026 · Aktuelle Fassung: **1.1.2** ([CHANGELOG](../CHANGELOG.md))
---
+117
View File
@@ -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
View File
@@ -1,6 +1,6 @@
{
"name": "betreuungs-cockpit",
"version": "1.1.0",
"version": "1.1.2",
"private": true,
"description": "Arbeits- und Dokumentationswerkzeug fuer die ehrenamtliche rechtliche Betreuung",
"engines": {
Regular → Executable
View File
Regular → Executable
+7
View File
@@ -288,6 +288,13 @@ fi
gut "$(git describe --tags --always)"
# Das Ausfuehrbarkeitsbit kommt normalerweise aus dem Repository mit. Wer
# Dateien von Hand kopiert hat - etwa um an dieses Skript zu kommen, bevor
# es in der laufenden Fassung existierte -, hat es verloren. Dann waere der
# naechste Aufruf ein "Permission denied", und man sucht an der falschen
# Stelle.
chmod +x scripts/*.sh 2>/dev/null || true
schritt "Bauen und starten"
printf ' Das dauert beim ersten Mal einige Minuten.\n\n'
+6 -1
View File
@@ -97,7 +97,12 @@ schreiben('src/lib/version.ts', ersetzt)
// --- 3. CHANGELOG.md --------------------------------------------------------
const heute = new Date().toISOString().slice(0, 10)
// Deutsche Zeit, nicht UTC. Sonst bekommt eine Veroeffentlichung nach
// 22 Uhr (Sommerzeit) das Datum des Vortages - dieselbe Zeitzonenfalle,
// derentwegen die Fristen im Schema @db.Date sind.
const heute = new Intl.DateTimeFormat('sv-SE', {
timeZone: 'Europe/Berlin',
}).format(new Date())
let changelog = lesen('CHANGELOG.md')
const UNVEROEFFENTLICHT = '## [Unveröffentlicht]'
Regular → Executable
View File
+15 -2
View File
@@ -56,8 +56,21 @@ export function AppHuelle({
Zum Inhalt springen
</a>
{/* --- Seitenleiste am Schreibtisch --------------------------------- */}
<aside className="no-print hidden w-72 shrink-0 border-r bg-card lg:flex lg:flex-col">
{/* --- Seitenleiste am Schreibtisch ---------------------------------
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">
<Link href="/heute" className="flex items-center gap-2 font-semibold">
<span
+1 -1
View File
@@ -17,7 +17,7 @@
*/
/** Muss zur "version" in der package.json passen. */
export const VERSION = '1.1.0'
export const VERSION = '1.1.2'
/**
* Kurzer Git-Stand, beim Bauen hineingereicht:
+87
View File
@@ -0,0 +1,87 @@
import { execFileSync } from 'node:child_process'
import { readFileSync } from 'node:fs'
import { describe, expect, it } from 'vitest'
/**
* Die Betriebsskripte müssen im Repository so liegen, dass sie auf dem
* Server auch laufen.
*
* Anlass sind zwei Fehler, die beide erst im Betrieb aufgefallen sind und
* beide nichts mit dem Programm zu tun hatten, sondern mit der Verpackung:
*
* 1. `scripts/` fehlte im Docker-Abbild — `node scripts/totp-geheimnis.mjs`
* lief ins Leere (behoben in 1.0.1).
* 2. Die `.sh`-Dateien lagen mit Modus 644 im Repository. Die Anleitung
* sagt `./scripts/update.sh`, auf Linux kam „Permission denied".
*
* Beide Male hat kein Test angeschlagen, weil beide Male der Code richtig
* war. Diese Datei prüft deshalb nicht Verhalten, sondern Verpackung.
*
* Der Ausführbarkeitsbit wird aus dem Git-Index gelesen, nicht vom
* Dateisystem: Unter Windows steht `core.filemode=false`, dort sagt das
* Dateisystem nichts aus. Maßgeblich ist, was ausgecheckt wird.
*/
function gitVerfuegbar(): boolean {
try {
execFileSync('git', ['rev-parse', '--git-dir'], { stdio: 'pipe' })
return true
} catch {
return false
}
}
/** Liefert [Modus, Pfad] je Datei, so wie sie im Index steht. */
function eintraegeImIndex(muster: string): [string, string][] {
const ausgabe = execFileSync('git', ['ls-files', '-s', muster], { encoding: 'utf8' })
return ausgabe
.split('\n')
.filter((zeile) => zeile.trim() !== '')
.map((zeile) => {
const [angaben, pfad] = zeile.split('\t')
return [angaben!.split(' ')[0]!, pfad!]
})
}
const mitGit = gitVerfuegbar()
describe.skipIf(!mitGit)('Betriebsskripte', () => {
it('legt jedes Shell-Skript ausführbar ab', () => {
const skripte = eintraegeImIndex('scripts/*.sh')
// Wenn hier nichts steht, stimmt das Muster nicht mehr - das waere
// ein stiller Ausfall der Pruefung.
expect(skripte.length).toBeGreaterThan(0)
for (const [modus, pfad] of skripte) {
expect(modus, `${pfad} muss ausführbar sein (100755, ist ${modus})`).toBe('100755')
}
})
it('gibt jedem Shell-Skript eine Shebang-Zeile', () => {
for (const [, pfad] of eintraegeImIndex('scripts/*.sh')) {
const ersteZeile = readFileSync(pfad, 'utf8').split('\n')[0] ?? ''
expect(ersteZeile, `${pfad} braucht eine Shebang-Zeile`).toMatch(/^#!/)
}
})
it('nimmt scripts/ ins Docker-Abbild auf', () => {
// Der Fehler aus 1.0.1: Die Wartungsskripte lagen im Repository, aber
// nicht im Behaelter. Aufgefallen ist es erst, als sie gebraucht wurden.
const dockerfile = readFileSync('Dockerfile', 'utf8')
// Die Zeile traegt zusaetzliche Schalter (--from, --chown) - deshalb
// nicht auf eine feste Reihenfolge festlegen, sondern nur darauf, dass
// scripts/ ins Abbild kopiert wird.
expect(dockerfile).toMatch(/^COPY.*\/scripts\s+\.\/scripts\s*$/m)
})
it('bindet die Sicherungsskripte aus scripts/ in die Compose-Datei ein', () => {
// Sie werden als Volume in den Sicherungsdienst gehaengt. Ein
// umbenanntes Skript faellt sonst erst um 03:15 Uhr auf - im Log,
// das niemand liest.
const compose = readFileSync('docker-compose.yml', 'utf8')
for (const name of ['sicherung.sh', 'wiederherstellung.sh']) {
expect(compose, `${name} wird nicht eingebunden`).toContain(`./scripts/${name}:`)
}
})
})