fix(abnahme): Healthcheck schweigt, zwei Konventionen praezisiert #41
@@ -5,6 +5,30 @@ Alle nennenswerten Änderungen an Laufzettel werden in dieser Datei dokumentiert
|
|||||||
Das Format basiert auf [Keep a Changelog](https://keepachangelog.com/de/1.1.0/),
|
Das Format basiert auf [Keep a Changelog](https://keepachangelog.com/de/1.1.0/),
|
||||||
und dieses Projekt folgt [Semantic Versioning](https://semver.org/lang/de/).
|
und dieses Projekt folgt [Semantic Versioning](https://semver.org/lang/de/).
|
||||||
|
|
||||||
|
## [Unveröffentlicht]
|
||||||
|
|
||||||
|
### Geändert
|
||||||
|
|
||||||
|
- Der Healthcheck `/api/gesundheit` nennt im Fehlerfall keinen Grund mehr. Der Pfad ist ohne
|
||||||
|
Anmeldung erreichbar, und eine Meldung von Prisma oder `pg` nennt Host, Port, Benutzernamen und
|
||||||
|
Datenbanknamen. Nach außen steht jetzt nur noch `{"status":"fehler"}` mit Status 503; die
|
||||||
|
Begründung geht ins Protokoll des Prozesses (#039)
|
||||||
|
- Die Konvention „nie auf rohe Hexwerte“ nennt ihre sechs begründeten Ausnahmen beim Namen —
|
||||||
|
Druckansichten, QR-Code und `themeColor` entscheiden nicht nach unserem Farbschema. Die Regel war
|
||||||
|
strenger formuliert als gemeint (#040)
|
||||||
|
- `CLAUDE.md` und `README.md` nennen den LDAP-Testcontainer als Teil des Tors vor dem Commit. Ohne
|
||||||
|
ihn überspringt die Suite **22 Tests** — den ganzen Verzeichnis-Abgleich —, sagt das auf stderr,
|
||||||
|
und der Lauf sieht trotzdem grün aus (#040)
|
||||||
|
- Im README fehlte `format:pruefen` in den Qualitätsschranken, obwohl `CLAUDE.md` es seit #010
|
||||||
|
verlangt (#040)
|
||||||
|
|
||||||
|
### Hinzugefügt
|
||||||
|
|
||||||
|
- `pnpm pruefe:doku` vergleicht die Schranken-Befehle in `CLAUDE.md` und `README.md` und verlangt von
|
||||||
|
beiden den LDAP-Container. Genau diese Abweichung war unbemerkt entstanden (#040)
|
||||||
|
- Fünf Tests für den Healthcheck, darunter einer, der belegt, dass die Begründung ins Protokoll
|
||||||
|
geschrieben und nicht bloß verschluckt wird (#039)
|
||||||
|
|
||||||
## [0.1.0] – 2026-09-26
|
## [0.1.0] – 2026-09-26
|
||||||
|
|
||||||
### Hinzugefügt
|
### Hinzugefügt
|
||||||
|
|||||||
@@ -176,9 +176,16 @@ speichern, neu laden, und nachsehen, ob es wirklich in der Datenbank steht.
|
|||||||
## Vor jedem Commit
|
## Vor jedem Commit
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
docker compose -f docker-compose.test.yml up -d --wait ldap
|
||||||
pnpm lint && pnpm typecheck && pnpm format:pruefen && pnpm test && pnpm pruefe:doku
|
pnpm lint && pnpm typecheck && pnpm format:pruefen && pnpm test && pnpm pruefe:doku
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Der LDAP-Container gehört in die erste Zeile.** Ohne ihn überspringt die Suite **22 Tests** — den
|
||||||
|
ganzen Verzeichnis-Abgleich (nachgezählt: 81 statt 59 in `tests/abgleich.test.ts` und
|
||||||
|
`tests/verzeichnis.test.ts`). Sie sagt das auf stderr, aber der Lauf sieht grün aus, und wer nur
|
||||||
|
diesem Abschnitt folgte, hat den Abgleich nie geprüft. In CI bricht sie stattdessen ab, absichtlich.
|
||||||
|
Aufgefallen bei der Abnahme zu #019.
|
||||||
|
|
||||||
`format:pruefen` gehört dazu, seit der Review in #010 darauf gestoßen ist: `lint-staged` formatiert
|
`format:pruefen` gehört dazu, seit der Review in #010 darauf gestoßen ist: `lint-staged` formatiert
|
||||||
beim Commit automatisch nach, und das Ergebnis wäre ein Commit mit Änderungen, die niemand gesehen
|
beim Commit automatisch nach, und das Ergebnis wäre ein Commit mit Änderungen, die niemand gesehen
|
||||||
hat. Lieber vorher sehen, was sich ändert.
|
hat. Lieber vorher sehen, was sich ändert.
|
||||||
@@ -198,6 +205,17 @@ Abschnitte mit zweistelliger Nummer (`01 · Vorbereitung`), die aus der Position
|
|||||||
kommt und nie gespeichert wird. Druckansichten und Etiketten immer hell.
|
kommt und nie gespeichert wird. Druckansichten und Etiketten immer hell.
|
||||||
**Am Handy nie horizontales Scrollen** — bei 390 px geprüft.
|
**Am Handy nie horizontales Scrollen** — bei 390 px geprüft.
|
||||||
|
|
||||||
|
**„Nie auf rohe Hexwerte" meint die Oberfläche.** Es gibt genau sechs begründete Ausnahmen, und sie
|
||||||
|
haben alle denselben Grund: dort entscheidet nicht unser Farbschema, sondern etwas außerhalb davon.
|
||||||
|
|
||||||
|
| Stelle | Wert | Warum kein Token |
|
||||||
|
|---|---|---|
|
||||||
|
| `src/app/drucken/lauf/[id]/page.tsx`, `src/app/drucken/etiketten/page.tsx` | `#ffffff` | Druck ist immer hell, unabhängig vom Erscheinungsbild der Anwendung |
|
||||||
|
| `src/lib/qr.ts` | `#000000ff` / `#ffffffff` | Ein QR-Code muss reines Schwarz auf reinem Weiß sein, sonst liest ihn kein Decoder |
|
||||||
|
| `src/app/layout.tsx` (`themeColor`), `src/app/manifest.ts` (zweimal) | `#0B0D10` | `themeColor` und das Web-App-Manifest vertragen keine CSS-Variablen |
|
||||||
|
|
||||||
|
Wer eine siebte hinzufügt, trägt sie hier ein — oder lässt es.
|
||||||
|
|
||||||
> Der folgende Block ist **nicht von uns**: `next dev` schreibt ihn bei jedem Start selbst in diese
|
> Der folgende Block ist **nicht von uns**: `next dev` schreibt ihn bei jedem Start selbst in diese
|
||||||
> Datei und stellt ihn wieder her, wenn man ihn löscht. Er steht deshalb hier, auf Englisch, und
|
> Datei und stellt ihn wieder her, wenn man ihn löscht. Er steht deshalb hier, auf Englisch, und
|
||||||
> wird mitcommittet, damit der Arbeitsbaum sauber bleibt.
|
> wird mitcommittet, damit der Arbeitsbaum sauber bleibt.
|
||||||
|
|||||||
@@ -45,11 +45,16 @@ openssl rand -base64 48
|
|||||||
### Qualitätsschranken
|
### Qualitätsschranken
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm lint && pnpm typecheck && pnpm test && pnpm pruefe:doku
|
docker compose -f docker-compose.test.yml up -d --wait ldap # sonst fehlen 22 Tests
|
||||||
|
pnpm lint && pnpm typecheck && pnpm format:pruefen && pnpm test && pnpm pruefe:doku
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Der LDAP-Container ist keine Zierde: ohne ihn überspringt die Suite den ganzen Verzeichnis-Abgleich
|
||||||
|
— sie sagt das auf stderr, aber der Lauf sieht grün aus. In CI bricht sie stattdessen ab.
|
||||||
|
|
||||||
Ein Pre-Commit-Hook erzwingt Lint und Format, ein Commit-Msg-Hook Conventional Commits.
|
Ein Pre-Commit-Hook erzwingt Lint und Format, ein Commit-Msg-Hook Conventional Commits.
|
||||||
Ende-zu-Ende-Tests: `pnpm test:e2e`.
|
Ende-zu-Ende-Tests: `pnpm test:e2e` (verlangt einen gebauten, laufenden Stand — siehe
|
||||||
|
`docs/anleitung/`).
|
||||||
|
|
||||||
## Betrieb mit Docker Compose
|
## Betrieb mit Docker Compose
|
||||||
|
|
||||||
|
|||||||
@@ -249,7 +249,7 @@ Datei nicht neu. Änderungen an `APP_VERSION` oder `GIT_SHA` sind Build-Argument
|
|||||||
|---|---|
|
|---|---|
|
||||||
| `APP_SECRET fehlt in der .env` beim `up` | Genau das. Der Stand bricht absichtlich ab, statt mit einem Standardwert zu starten. Gilt ebenso für `APP_URL` und `POSTGRES_PASSWORD`. |
|
| `APP_SECRET fehlt in der .env` beim `up` | Genau das. Der Stand bricht absichtlich ab, statt mit einem Standardwert zu starten. Gilt ebenso für `APP_URL` und `POSTGRES_PASSWORD`. |
|
||||||
| `web` startet nicht, `migrate` steht auf `exited (1)` | `docker compose logs migrate`. Meist erreicht die Migration die Datenbank nicht — oder du hast `POSTGRES_PASSWORD` nach dem ersten Start geändert. Das Passwort steckt dann noch im Volume. |
|
| `web` startet nicht, `migrate` steht auf `exited (1)` | `docker compose logs migrate`. Meist erreicht die Migration die Datenbank nicht — oder du hast `POSTGRES_PASSWORD` nach dem ersten Start geändert. Das Passwort steckt dann noch im Volume. |
|
||||||
| `/api/gesundheit` antwortet mit 503 | Die Anwendung läuft, die Datenbank antwortet nicht. `docker compose ps` — ist `postgres` gesund? |
|
| `/api/gesundheit` antwortet mit 503 | Die Anwendung läuft, die Datenbank antwortet nicht. `docker compose ps` — ist `postgres` gesund? Die Antwort nennt absichtlich keinen Grund, weil der Pfad ohne Anmeldung erreichbar ist; der Grund steht in `docker compose logs web`. |
|
||||||
| Der Seed bricht mittendrin ab | Bis zur Abnahme von #019 fehlte dem Einmal-Dienst `APP_SECRET`, und das Bind-Passwort der Beispiel-Verzeichnisquelle ließ sich nicht verschlüsseln — abgebrochen wurde erst, nachdem die Vorlagen schon standen. Steht dein `docker-compose.yml` auf dem aktuellen Stand, ist das behoben; ein erneuter Lauf holt den Rest nach. |
|
| Der Seed bricht mittendrin ab | Bis zur Abnahme von #019 fehlte dem Einmal-Dienst `APP_SECRET`, und das Bind-Passwort der Beispiel-Verzeichnisquelle ließ sich nicht verschlüsseln — abgebrochen wurde erst, nachdem die Vorlagen schon standen. Steht dein `docker-compose.yml` auf dem aktuellen Stand, ist das behoben; ein erneuter Lauf holt den Rest nach. |
|
||||||
| Nach dem Login wieder auf der Anmeldeseite | Das Cookie ist `Secure`, die Verbindung aber `http`. `APP_URL` auf `https://…` setzen und HTTPS über den Proxy anbieten. |
|
| Nach dem Login wieder auf der Anmeldeseite | Das Cookie ist `Secure`, die Verbindung aber `http`. `APP_URL` auf `https://…` setzen und HTTPS über den Proxy anbieten. |
|
||||||
| Die QR-Codes zeigen auf `localhost:3000` | `APP_URL` ist nicht oder falsch gesetzt. Die Oberfläche warnt unter **Admin** und in der Etikettenansicht davor. Aufkleber, die so gedruckt wurden, sind wertlos — bevor du weiterdruckst, `APP_URL` richtigstellen und `docker compose up -d`. |
|
| Die QR-Codes zeigen auf `localhost:3000` | `APP_URL` ist nicht oder falsch gesetzt. Die Oberfläche warnt unter **Admin** und in der Etikettenansicht davor. Aufkleber, die so gedruckt wurden, sind wertlos — bevor du weiterdruckst, `APP_URL` richtigstellen und `docker compose up -d`. |
|
||||||
|
|||||||
@@ -203,6 +203,11 @@ schlimmsten Fall, macht den Unterpfad aber nicht betriebsfähig.
|
|||||||
antwortet `{"status":"ok"}` oder mit Status 503. Das ist der richtige Pfad für die Überwachung des
|
antwortet `{"status":"ok"}` oder mit Status 503. Das ist der richtige Pfad für die Überwachung des
|
||||||
Proxys; `/` leitet ohne Cookie auf die Anmeldeseite um und sagt nichts über den Zustand.
|
Proxys; `/` leitet ohne Cookie auf die Anmeldeseite um und sagt nichts über den Zustand.
|
||||||
|
|
||||||
|
Im Fehlerfall sagt er nur `{"status":"fehler"}` und **nicht, warum** — der Pfad ist ohne Anmeldung
|
||||||
|
erreichbar, und eine Datenbankmeldung nennt Host, Port, Benutzernamen und Datenbanknamen. Die
|
||||||
|
Begründung steht im Protokoll des Prozesses: `docker compose logs web` beziehungsweise
|
||||||
|
`journalctl -u laufzettel -n 50`.
|
||||||
|
|
||||||
**Die Kamera braucht HTTPS.** Der QR-Scanner in der Anwendung benutzt die Kamera des Geräts, und
|
**Die Kamera braucht HTTPS.** Der QR-Scanner in der Anwendung benutzt die Kamera des Geräts, und
|
||||||
Browser geben sie nur über eine gesicherte Verbindung frei (`localhost` ausgenommen). Ohne TLS
|
Browser geben sie nur über eine gesicherte Verbindung frei (`localhost` ausgenommen). Ohne TLS
|
||||||
scannt am Handy niemand — das ist der praktische Grund, warum der Proxy nicht optional ist.
|
scannt am Handy niemand — das ist der praktische Grund, warum der Proxy nicht optional ist.
|
||||||
|
|||||||
+50
-1
@@ -22,11 +22,14 @@ als Unterpunkte mit Aufgabe und Dauer.
|
|||||||
| #017 | M12 – Seed der sieben Vorlagen | 1 h 30 min |
|
| #017 | M12 – Seed der sieben Vorlagen | 1 h 30 min |
|
||||||
| #018 | M13 – E2E-Tests, Anleitung, PDF | 2 h 15 min |
|
| #018 | M13 – E2E-Tests, Anleitung, PDF | 2 h 15 min |
|
||||||
| #019 | M14 – Betriebswege und Release v0.1.0 | 5 h 25 min |
|
| #019 | M14 – Betriebswege und Release v0.1.0 | 5 h 25 min |
|
||||||
| | **Summe** | **35 h 20 min** |
|
| #025 | Healthcheck gibt die rohe Datenbankfehlermeldung heraus | 25 min |
|
||||||
|
| #026 | Zwei Konventionen präzisieren | 20 min |
|
||||||
|
| | **Summe** | **36 h 05 min** |
|
||||||
|
|
||||||
| Meilenstein | Tatsächlich | Status |
|
| Meilenstein | Tatsächlich | Status |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| v0.1.0 | 35 h 20 min | veröffentlicht |
|
| v0.1.0 | 35 h 20 min | veröffentlicht |
|
||||||
|
| v0.2.0 | 45 min | in Arbeit |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -977,6 +980,52 @@ als Unterpunkte mit Aufgabe und Dauer.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 2026-09-26 · Sitzung 16 · 16:00–16:45
|
||||||
|
|
||||||
|
**Issues:** #025, #026 (beide aus der Abnahme zu #019)
|
||||||
|
**Version:** 0.2.0 (unveröffentlicht)
|
||||||
|
|
||||||
|
### Erledigt
|
||||||
|
|
||||||
|
- **`/api/gesundheit` verrät nichts mehr.** Der Pfad ist ohne Anmeldung erreichbar; eine Meldung von
|
||||||
|
Prisma oder `pg` nennt Host, Port, Benutzernamen und Datenbanknamen. Nach außen bleibt
|
||||||
|
`{"status":"fehler"}` mit 503, die Begründung geht ins Protokoll — dasselbe Muster, das
|
||||||
|
`src/server/actions/verzeichnis.ts` schon für unbekannte Ausnahmen benutzt
|
||||||
|
- **Fünf Tests** dafür, einer davon für die andere Hälfte der Zusage: dass die Begründung wirklich
|
||||||
|
**geschrieben** und nicht bloß verschluckt wird
|
||||||
|
- **Die Hexwert-Regel nennt ihre Ausnahmen.** Sechs Stellen, als Tabelle mit Begründung je Zeile
|
||||||
|
- **Der LDAP-Container steht jetzt im Tor**, in `CLAUDE.md` und im README — mit der gemessenen Zahl
|
||||||
|
- **`pnpm pruefe:doku` vergleicht die beiden Schranken-Blöcke** und verlangt von beiden den Container
|
||||||
|
|
||||||
|
### Entscheidungen
|
||||||
|
|
||||||
|
- **Die Regel wurde ergänzt, nicht der Code geändert.** Alle sechs Hexwerte haben denselben Grund:
|
||||||
|
dort entscheidet nicht unser Farbschema, sondern etwas außerhalb davon — Papier, ein QR-Decoder,
|
||||||
|
das Web-App-Manifest. Sie in eine Konstante zu ziehen hätte eine Abstraktion über drei
|
||||||
|
unverwandte Dinge gelegt und die Begründung versteckt. Die Tabelle in `CLAUDE.md` zeigt sie.
|
||||||
|
- **Der Hinweis auf den Container reicht nicht, er muss geprüft werden.** Zwei Dateien beschreiben
|
||||||
|
dasselbe Tor und waren auseinandergelaufen. Ein Satz in einer Konvention hält nur, solange ihn
|
||||||
|
jemand liest; die sechste Prüfung in `pruefe:doku` hält ihn, auch wenn niemand hinsieht.
|
||||||
|
|
||||||
|
### Probleme
|
||||||
|
|
||||||
|
- **Ich hätte „rund zwanzig Tests" in eine Konvention geschrieben.** Die Zahl stammte aus dem
|
||||||
|
Abnahmebericht (dort: 18), nicht aus einer Messung. Nachgezählt sind es **22** — mit Container 81,
|
||||||
|
ohne 59. Eine ungefähre Zahl in einer verbindlichen Datei ist derselbe Fehlertyp, den dieses
|
||||||
|
Projekt vierzehn Meilensteine lang gesammelt hat, nur kleiner.
|
||||||
|
- **Der Review hat eine Ausnahme übersehen.** Er nannte vier rohe Hexwerte, es sind sechs:
|
||||||
|
`src/lib/qr.ts` fehlte. Das Issue #026 hat die falsche Zahl von ihm übernommen; beim Abhaken wäre
|
||||||
|
ein richtiger Haken über falschem Text entstanden. Issue-Text korrigiert, dann abgehakt.
|
||||||
|
- **`docs/architektur.md` trägt die Gestaltungsregel nicht**, obwohl `CLAUDE.md` verlangt, dass
|
||||||
|
Änderungen dort mitgehen und das Issue es behauptete. Geprüft statt geglaubt.
|
||||||
|
|
||||||
|
### Nächster Schritt
|
||||||
|
|
||||||
|
- Phase 2 (#002) oder die vier verbliebenen v0.2.0-Issues: #020 Vorgesetzter, #021 stabiler
|
||||||
|
Schlüssel, #022 Schnellstart pflegen, #023 Protokoll-Ansicht. #024 wartet auf einen PVE-Host.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Phase 1 abgeschlossen: was aus dem Plan wurde
|
## Phase 1 abgeschlossen: was aus dem Plan wurde
|
||||||
|
|
||||||
**35 h 20 min** über vierzehn Meilensteine, geschätzt waren **148 h**. Der Faktor ist kein Verdienst,
|
**35 h 20 min** über vierzehn Meilensteine, geschätzt waren **148 h**. Der Faktor ist kein Verdienst,
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
# #025 Healthcheck gibt die rohe Datenbankfehlermeldung heraus
|
# #025 Healthcheck gibt die rohe Datenbankfehlermeldung heraus
|
||||||
|
|
||||||
**Status:** offen
|
**Status:** erledigt
|
||||||
**Im Tracker:** [#39](https://git.wilde-server.de/florian.wilde/Laufzettel/issues/39)
|
**Im Tracker:** [#39](https://git.wilde-server.de/florian.wilde/Laufzettel/issues/39)
|
||||||
**Meilenstein:** v0.2.0
|
**Meilenstein:** v0.2.0
|
||||||
**Labels:** phase-2, backend
|
**Labels:** phase-2, backend
|
||||||
**Geschätzt:** 1 h **Tatsächlich:** –
|
**Geschätzt:** 1 h **Tatsächlich:** 25 min
|
||||||
|
|
||||||
## Ziel
|
## Ziel
|
||||||
|
|
||||||
@@ -23,13 +23,13 @@ Aufgefallen beim Abnahme-Review zu [#019](019-betrieb-release.md).
|
|||||||
|
|
||||||
## Aufgaben
|
## Aufgaben
|
||||||
|
|
||||||
- [ ] Nach außen nur `{ "status": "fehler" }` mit Status 503
|
- [x] Nach außen nur `{ "status": "fehler" }` mit Status 503
|
||||||
- [ ] Die Begründung serverseitig protokollieren, damit sie beim Suchen nicht fehlt
|
- [x] Die Begründung serverseitig protokollieren, damit sie beim Suchen nicht fehlt
|
||||||
- [ ] Prüfen, ob `docs/betrieb/` irgendwo die Meldung als Diagnosehilfe nennt — dann auf das Log
|
- [x] Prüfen, ob `docs/betrieb/` irgendwo die Meldung als Diagnosehilfe nennt — dann auf das Log
|
||||||
verweisen
|
verweisen
|
||||||
|
|
||||||
## Akzeptanzkriterien
|
## Akzeptanzkriterien
|
||||||
|
|
||||||
- [ ] Test: bei nicht erreichbarer Datenbank enthält die Antwort weder Host noch Port noch
|
- [x] Test: bei nicht erreichbarer Datenbank enthält die Antwort weder Host noch Port noch
|
||||||
Benutzernamen
|
Benutzernamen
|
||||||
- [ ] Test: der Statuscode bleibt 503, damit Docker und der Reverse Proxy weiter richtig entscheiden
|
- [x] Test: der Statuscode bleibt 503, damit Docker und der Reverse Proxy weiter richtig entscheiden
|
||||||
|
|||||||
@@ -1,22 +1,27 @@
|
|||||||
# #026 Zwei Konventionen präzisieren, die strenger klingen als sie gemeint sind
|
# #026 Zwei Konventionen präzisieren, die strenger klingen als sie gemeint sind
|
||||||
|
|
||||||
**Status:** offen
|
**Status:** erledigt
|
||||||
**Im Tracker:** [#40](https://git.wilde-server.de/florian.wilde/Laufzettel/issues/40)
|
**Im Tracker:** [#40](https://git.wilde-server.de/florian.wilde/Laufzettel/issues/40)
|
||||||
**Meilenstein:** v0.2.0
|
**Meilenstein:** v0.2.0
|
||||||
**Labels:** phase-2, docs
|
**Labels:** phase-2, docs
|
||||||
**Geschätzt:** 1 h **Tatsächlich:** –
|
**Geschätzt:** 1 h **Tatsächlich:** 20 min
|
||||||
|
|
||||||
## Ziel
|
## Ziel
|
||||||
|
|
||||||
Zwei Sätze in `CLAUDE.md` (und gleichlautend in `docs/architektur.md`) beschreiben den Code nicht
|
Zwei Sätze in `CLAUDE.md` beschreiben den Code nicht genau. Beide fielen beim Abnahme-Review zu
|
||||||
genau. Beide fielen beim Abnahme-Review zu [#019](019-betrieb-release.md) auf.
|
[#019](019-betrieb-release.md) auf.
|
||||||
|
|
||||||
|
**Nachtrag aus der Umsetzung:** `docs/architektur.md` trägt die Gestaltungsregel gar nicht — sie
|
||||||
|
steht nur in `CLAUDE.md`. Und es sind **sechs** Ausnahmen, nicht vier: der Review hat `src/lib/qr.ts`
|
||||||
|
übersehen, wo der QR-Code reines Schwarz auf reinem Weiß braucht, damit ein Decoder ihn liest.
|
||||||
|
|
||||||
**1. Rohe Hexwerte.** Die Regel sagt „Komponenten greifen nur darauf zu, nie auf rohe Hexwerte“.
|
**1. Rohe Hexwerte.** Die Regel sagt „Komponenten greifen nur darauf zu, nie auf rohe Hexwerte“.
|
||||||
Tatsächlich stehen vier begründete Ausnahmen im Code: `#ffffff` in `drucken/lauf/[id]/page.tsx` und
|
Tatsächlich stehen sechs begründete Ausnahmen im Code: `#ffffff` in `drucken/lauf/[id]/page.tsx` und
|
||||||
`drucken/etiketten/page.tsx` (Druck ist immer hell, unabhängig vom Farbschema der Anwendung) sowie
|
`drucken/etiketten/page.tsx` (Druck ist immer hell, unabhängig vom Farbschema der Anwendung),
|
||||||
`#0B0D10` in `layout.tsx` und `manifest.ts` (`themeColor` und das Manifest vertragen keine
|
`#000000ff`/`#ffffffff` in `src/lib/qr.ts` (ein QR-Code muss reines Schwarz auf reinem Weiß sein)
|
||||||
CSS-Variablen). Die Regel ist strenger formuliert als gemeint — sie soll um diese Ausnahmen ergänzt
|
sowie `#0B0D10` in `layout.tsx` und zweimal in `manifest.ts` (`themeColor` und das Manifest
|
||||||
werden, nicht der Code geändert.
|
vertragen keine CSS-Variablen). Die Regel ist strenger formuliert als gemeint — sie soll um diese
|
||||||
|
Ausnahmen ergänzt werden, nicht der Code geändert.
|
||||||
|
|
||||||
**2. `pnpm test` ohne LDAP-Container.** „Vor jedem Commit“ in `CLAUDE.md` und „Qualitätsschranken“ im
|
**2. `pnpm test` ohne LDAP-Container.** „Vor jedem Commit“ in `CLAUDE.md` und „Qualitätsschranken“ im
|
||||||
README nennen `pnpm test`, aber nicht
|
README nennen `pnpm test`, aber nicht
|
||||||
@@ -24,14 +29,17 @@ README nennen `pnpm test`, aber nicht
|
|||||||
Tests gegen das echte Verzeichnis — sie sagt das auf stderr, aber der Lauf sieht grün aus. Wer nur
|
Tests gegen das echte Verzeichnis — sie sagt das auf stderr, aber der Lauf sieht grün aus. Wer nur
|
||||||
dem dokumentierten Weg folgt, prüft den Verzeichnis-Abgleich nie. Ein Satz in beiden Dateien.
|
dem dokumentierten Weg folgt, prüft den Verzeichnis-Abgleich nie. Ein Satz in beiden Dateien.
|
||||||
|
|
||||||
|
Nachgezählt sind es **22 Tests**: `tests/abgleich.test.ts` und `tests/verzeichnis.test.ts` ergeben
|
||||||
|
mit Container 81, ohne 59. Dabei fiel auf, dass im README zusätzlich `format:pruefen` fehlte.
|
||||||
|
|
||||||
## Aufgaben
|
## Aufgaben
|
||||||
|
|
||||||
- [ ] Ausnahme für rohe Hexwerte in `CLAUDE.md` und `docs/architektur.md` benennen, mit Begründung
|
- [x] Ausnahme für rohe Hexwerte in `CLAUDE.md` benennen, mit Begründung (in `docs/architektur.md` steht die Regel nicht)
|
||||||
- [ ] Den LDAP-Container in `CLAUDE.md` („Vor jedem Commit“) und `README.md` aufnehmen
|
- [x] Den LDAP-Container in `CLAUDE.md` („Vor jedem Commit“) und `README.md` aufnehmen
|
||||||
- [ ] Prüfen, ob sich der Hinweis maschinell absichern lässt — `pruefe:doku` könnte verlangen, dass
|
- [x] Prüfen, ob sich der Hinweis maschinell absichern lässt — `pruefe:doku` könnte verlangen, dass
|
||||||
jede in `CLAUDE.md` genannte Schranke auch den nötigen Vorlauf nennt
|
jede in `CLAUDE.md` genannte Schranke auch den nötigen Vorlauf nennt
|
||||||
|
|
||||||
## Akzeptanzkriterien
|
## Akzeptanzkriterien
|
||||||
|
|
||||||
- [ ] Die vier Hexwerte im Code stehen in Einklang mit der Regel, ohne den Code zu ändern
|
- [x] Die sechs Hexwerte im Code stehen in Einklang mit der Regel, ohne den Code zu ändern
|
||||||
- [ ] Wer `CLAUDE.md` von oben nach unten befolgt, fährt die Suite mit LDAP-Container
|
- [x] Wer `CLAUDE.md` von oben nach unten befolgt, fährt die Suite mit LDAP-Container
|
||||||
|
|||||||
@@ -29,6 +29,6 @@ Diese Dateien sind die Quelle; `pnpm issues:sync` spiegelt sie in den Gitea-Trac
|
|||||||
| #022 | [Schnellstart je Gerätetyp pflegen](022-schnellstart-pflegen.md) | offen | v0.2.0 | 3 h | – |
|
| #022 | [Schnellstart je Gerätetyp pflegen](022-schnellstart-pflegen.md) | offen | v0.2.0 | 3 h | – |
|
||||||
| #023 | [Protokoll ansehen](023-protokoll-ansicht.md) | offen | v0.2.0 | 4 h | – |
|
| #023 | [Protokoll ansehen](023-protokoll-ansicht.md) | offen | v0.2.0 | 4 h | – |
|
||||||
| #024 | [Proxmox-Einzeiler auf einem echten PVE-Host gegenprüfen](024-proxmox-echtlauf.md) | offen | v0.2.0 | 2 h | – |
|
| #024 | [Proxmox-Einzeiler auf einem echten PVE-Host gegenprüfen](024-proxmox-echtlauf.md) | offen | v0.2.0 | 2 h | – |
|
||||||
| #025 | [Healthcheck gibt die rohe Datenbankfehlermeldung heraus](025-gesundheit-fehlermeldung.md) | offen | v0.2.0 | 1 h | – |
|
| #025 | [Healthcheck gibt die rohe Datenbankfehlermeldung heraus](025-gesundheit-fehlermeldung.md) | erledigt | v0.2.0 | 1 h | 25 min |
|
||||||
| #026 | [Zwei Konventionen präzisieren, die strenger klingen als sie gemeint sind](026-konventionen-praezisieren.md) | offen | v0.2.0 | 1 h | – |
|
| #026 | [Zwei Konventionen präzisieren, die strenger klingen als sie gemeint sind](026-konventionen-praezisieren.md) | erledigt | v0.2.0 | 1 h | 20 min |
|
||||||
| | **Summe** | | | **152 h** | **35 h 20 min** |
|
| | **Summe** | | | **152 h** | **36 h 05 min** |
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
* 3. Geschaetzte und tatsaechliche Zeiten stimmen zwischen Datei und Uebersicht.
|
* 3. Geschaetzte und tatsaechliche Zeiten stimmen zwischen Datei und Uebersicht.
|
||||||
* 4. Alle in der Anleitung verlinkten Bilder sind vorhanden.
|
* 4. Alle in der Anleitung verlinkten Bilder sind vorhanden.
|
||||||
* 5. Kein Markdown verweist auf eine Datei, die es nicht gibt.
|
* 5. Kein Markdown verweist auf eine Datei, die es nicht gibt.
|
||||||
|
* 6. Die Schranken-Befehle in CLAUDE.md und README.md stimmen ueberein.
|
||||||
*/
|
*/
|
||||||
import { readFileSync, existsSync, readdirSync } from "node:fs";
|
import { readFileSync, existsSync, readdirSync } from "node:fs";
|
||||||
import { join, dirname, resolve } from "node:path";
|
import { join, dirname, resolve } from "node:path";
|
||||||
@@ -134,6 +135,56 @@ function feld(text, name) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- 6. Die Schranken in CLAUDE.md und README.md -------------------------
|
||||||
|
//
|
||||||
|
// Zwei Dateien beschreiben dasselbe Tor, und sie sind auseinandergelaufen: im
|
||||||
|
// README fehlte `format:pruefen`, und beide nannten den LDAP-Container nicht, ohne
|
||||||
|
// den die Suite 22 Tests still ueberspringt. Beides fiel erst bei der Abnahme zu
|
||||||
|
// #019 auf -- ein gruener Lauf sieht genauso aus. Geprueft wird deshalb der
|
||||||
|
// Befehlsblock selbst, nicht seine Umgebung (#040).
|
||||||
|
{
|
||||||
|
/** Der erste eingezaeunte Block nach einer Ueberschrift. */
|
||||||
|
const blockNach = (text, ueberschrift) => {
|
||||||
|
const ab = text.indexOf(ueberschrift);
|
||||||
|
if (ab < 0) return null;
|
||||||
|
const treffer = text.slice(ab).match(/```(?:bash|sh)?\n([\s\S]*?)```/);
|
||||||
|
return treffer?.[1] ?? null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const skripte = (block) => [...block.matchAll(/pnpm ([a-z:]+)/g)].map((m) => m[1]).sort();
|
||||||
|
|
||||||
|
const quellen = [
|
||||||
|
["CLAUDE.md", "## Vor jedem Commit"],
|
||||||
|
["README.md", "### Qualitätsschranken"],
|
||||||
|
];
|
||||||
|
|
||||||
|
const bloecke = quellen.map(([datei, ueberschrift]) => {
|
||||||
|
const block = blockNach(lies(datei), ueberschrift);
|
||||||
|
if (block === null) melde(`${datei}: kein Befehlsblock unter "${ueberschrift}" gefunden.`);
|
||||||
|
return [datei, block];
|
||||||
|
});
|
||||||
|
|
||||||
|
if (bloecke.every(([, block]) => block !== null)) {
|
||||||
|
const [[dateiA, blockA], [dateiB, blockB]] = bloecke;
|
||||||
|
const a = skripte(blockA);
|
||||||
|
const b = skripte(blockB);
|
||||||
|
if (a.join(" ") !== b.join(" ")) {
|
||||||
|
melde(
|
||||||
|
`Die Schranken laufen auseinander: ${dateiA} nennt [${a.join(", ")}], ` +
|
||||||
|
`${dateiB} nennt [${b.join(", ")}].`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for (const [datei, block] of bloecke) {
|
||||||
|
if (!/docker compose -f docker-compose\.test\.yml up .*ldap/.test(block)) {
|
||||||
|
melde(
|
||||||
|
`${datei}: der Schranken-Block nennt den LDAP-Container nicht. ` +
|
||||||
|
`Ohne ihn ueberspringt die Suite den Verzeichnis-Abgleich, und der Lauf sieht gruen aus.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Ergebnis --------------------------------------------------------------
|
// ---- Ergebnis --------------------------------------------------------------
|
||||||
if (befunde.length > 0) {
|
if (befunde.length > 0) {
|
||||||
console.error(`\nDokumentation und Code laufen auseinander (${befunde.length}):\n`);
|
console.error(`\nDokumentation und Code laufen auseinander (${befunde.length}):\n`);
|
||||||
|
|||||||
@@ -1,12 +1,26 @@
|
|||||||
import { prisma } from "@/lib/db";
|
import { prisma } from "@/lib/db";
|
||||||
|
|
||||||
/** Healthcheck fuer Docker und den Reverse Proxy. Prueft, ob die Datenbank antwortet. */
|
/**
|
||||||
|
* Healthcheck fuer Docker und den Reverse Proxy. Prueft, ob die Datenbank antwortet.
|
||||||
|
*
|
||||||
|
* Dieser Pfad ist absichtlich ohne Anmeldung erreichbar — er steht in der
|
||||||
|
* Allowlist von `src/proxy.ts`, damit ein Healthcheck kein Cookie braucht.
|
||||||
|
* Deshalb geht die **Begruendung** eines Fehlers nicht hinaus: eine Meldung von
|
||||||
|
* Prisma oder `pg` nennt typischerweise Host, Port, Benutzernamen und
|
||||||
|
* Datenbanknamen. Nach aussen genuegt, **ob** es geht; warum nicht, steht im
|
||||||
|
* Protokoll des Prozesses (`docker compose logs web`,
|
||||||
|
* `journalctl -u laufzettel`). Dasselbe Muster wie in
|
||||||
|
* `src/server/actions/verzeichnis.ts` (#039).
|
||||||
|
*/
|
||||||
export async function GET(): Promise<Response> {
|
export async function GET(): Promise<Response> {
|
||||||
try {
|
try {
|
||||||
await prisma.$queryRaw`SELECT 1`;
|
await prisma.$queryRaw`SELECT 1`;
|
||||||
return Response.json({ status: "ok" });
|
return Response.json({ status: "ok" });
|
||||||
} catch (fehler) {
|
} catch (fehler) {
|
||||||
const meldung = fehler instanceof Error ? fehler.message : "unbekannter Fehler";
|
console.error(
|
||||||
return Response.json({ status: "fehler", meldung }, { status: 503 });
|
"Healthcheck fehlgeschlagen:",
|
||||||
|
fehler instanceof Error ? `${fehler.name}: ${fehler.message}` : "unbekannter Fehler",
|
||||||
|
);
|
||||||
|
return Response.json({ status: "fehler" }, { status: 503 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Der Healthcheck (#039).
|
||||||
|
*
|
||||||
|
* Dieser Pfad ist der einzige, der ohne Anmeldung **und** mit Datenbankzugriff
|
||||||
|
* antwortet. Geprueft wird deshalb nicht, dass er funktioniert — das tun der
|
||||||
|
* Compose-Stand, `install.sh --test` und jeder e2e-Vorlauf —, sondern dass er im
|
||||||
|
* Fehlerfall **nichts verraet**. Die Datenbank wird dafuer ersetzt: eine echte
|
||||||
|
* Verbindung laesst sich nicht so kaputtmachen, dass die Meldung reproduzierbar
|
||||||
|
* Host, Port und Benutzernamen enthaelt.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const abfrage = vi.fn();
|
||||||
|
|
||||||
|
vi.mock("@/lib/db", () => ({
|
||||||
|
prisma: {
|
||||||
|
get $queryRaw() {
|
||||||
|
return abfrage;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* So sieht die Meldung wirklich aus, wenn `pg` nicht durchkommt — sie nennt
|
||||||
|
* alles, was ein Angreifer ueber die Datenbank wissen will.
|
||||||
|
*/
|
||||||
|
const ECHTE_PG_MELDUNG =
|
||||||
|
"Can't reach database server at `db-prod.firma.local:5432`. " +
|
||||||
|
"Authentication failed against database server, the provided database credentials for " +
|
||||||
|
"`laufzettel_prod` are not valid. Database `laufzettel_betrieb` does not exist.";
|
||||||
|
|
||||||
|
const GEHEIMNISSE = [
|
||||||
|
"db-prod.firma.local",
|
||||||
|
"5432",
|
||||||
|
"laufzettel_prod",
|
||||||
|
"laufzettel_betrieb",
|
||||||
|
"Authentication failed",
|
||||||
|
];
|
||||||
|
|
||||||
|
async function antwort() {
|
||||||
|
// Nach jedem Zuruecksetzen der Module neu geholt, damit der Mock greift.
|
||||||
|
const { GET } = await import("@/app/api/gesundheit/route");
|
||||||
|
return GET();
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("Healthcheck", () => {
|
||||||
|
let protokoll: ReturnType<typeof vi.spyOn>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.resetModules();
|
||||||
|
abfrage.mockReset();
|
||||||
|
protokoll = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
protokoll.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("antwortet mit ok, wenn die Datenbank antwortet", async () => {
|
||||||
|
abfrage.mockResolvedValue([{ "1": 1 }]);
|
||||||
|
const a = await antwort();
|
||||||
|
expect(a.status).toBe(200);
|
||||||
|
await expect(a.json()).resolves.toEqual({ status: "ok" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("gibt im Fehlerfall weder Host noch Port noch Benutzernamen heraus", async () => {
|
||||||
|
abfrage.mockRejectedValue(new Error(ECHTE_PG_MELDUNG));
|
||||||
|
const a = await antwort();
|
||||||
|
|
||||||
|
const text = await a.text();
|
||||||
|
for (const geheimnis of GEHEIMNISSE) {
|
||||||
|
expect(text).not.toContain(geheimnis);
|
||||||
|
}
|
||||||
|
expect(JSON.parse(text)).toEqual({ status: "fehler" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("behaelt 503, damit Docker und der Reverse Proxy richtig entscheiden", async () => {
|
||||||
|
abfrage.mockRejectedValue(new Error(ECHTE_PG_MELDUNG));
|
||||||
|
expect((await antwort()).status).toBe(503);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("schreibt die Begruendung ins Protokoll, statt sie zu verschlucken", async () => {
|
||||||
|
abfrage.mockRejectedValue(new Error(ECHTE_PG_MELDUNG));
|
||||||
|
await antwort();
|
||||||
|
|
||||||
|
expect(protokoll).toHaveBeenCalledTimes(1);
|
||||||
|
const geschrieben = protokoll.mock.calls[0]?.join(" ") ?? "";
|
||||||
|
expect(geschrieben).toContain("Healthcheck fehlgeschlagen");
|
||||||
|
// Ohne das waere die Meldung nur weg, nicht aufgehoben.
|
||||||
|
expect(geschrieben).toContain("db-prod.firma.local:5432");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("nennt auch eine Ausnahme ohne Error-Klasse, ohne sie auszugeben", async () => {
|
||||||
|
abfrage.mockRejectedValue("nur ein Text");
|
||||||
|
const a = await antwort();
|
||||||
|
|
||||||
|
expect(a.status).toBe(503);
|
||||||
|
await expect(a.json()).resolves.toEqual({ status: "fehler" });
|
||||||
|
expect(protokoll.mock.calls[0]?.join(" ")).toContain("unbekannter Fehler");
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user