diff --git a/.gitignore b/.gitignore index 3498314..805f1ca 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,4 @@ pflegepilot_v3/backend/.venv*/ pflegepilot_v3/frontend/node_modules/ pflegepilot_v3/frontend/.next/ pflegepilot_v3/.env +pflegepilot_v3/secrets/ diff --git a/CLAUDE.md b/CLAUDE.md index 02d0bda..ced86ab 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,6 +2,16 @@ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. +## Naming: pflegepilot_v3's product is now branded "MeinPflegeFuchs" (renamed 2026-07-24) + +The `pflegepilot_v3` product was renamed from "Pflegepilot" to **"MeinPflegeFuchs"** (domain: meinpflegefuchs.de, ordered via Strato) — there turned out to be several unrelated existing apps/services already using "Pflegepilot" or very close variants, discovered when checking candidate replacement names for trademark/confusion risk (a lesson worth repeating: the German Pflege-app naming space is extremely crowded — a `"Pflege" + descriptive word` pattern is very likely to already be taken by a real competing service; "MeinPflegeFuchs" was chosen specifically because it's a mascot-style name rather than a literal descriptor). + +- **Every user-visible string, generated-document footer, email subject, page title/metadata, and the two legal texts under `rechtstexte/` were updated** to "MeinPflegeFuchs" — see the `changelog_seed.py` entry (version 1.1.0) for the user-facing announcement of the rename. +- **The `pflegepilot_v3` directory name, package/module names, DB names, and NAS deployment path (`/Dockers/pflege`) were deliberately left unchanged** — renaming those would touch git history, every absolute path referenced throughout this file and past deployment scripts, and the NAS's manually-maintained (non-git) copy, for zero user-facing benefit. Don't be confused that the folder still says "pflegepilot" while the product itself is called MeinPflegeFuchs — that's intentional, not a leftover. +- Only scope was `pflegepilot_v3` — `pflegepilot_alpha_v2` (the old, separately-running Flask app) still legitimately calls itself Pflegepilot and was not touched. +- Historical references to "Pflegepilot" — e.g. the changelog's 1.0.0 entry, or anything below in this file describing what was true *at the time it was written* — are accurate records of the past and were deliberately left as-is rather than rewritten; only forward-looking/current-state documentation and all actual product code got renamed. When adding new documentation to this file, use "MeinPflegeFuchs", not "Pflegepilot". +- `einstellungen.firmenname` (sidebar branding / PDF-footer company name) is a separate, operator-configured field for the *legal entity* name (currently "Daniel Barac Onlinedienste" in production) — unrelated to the product brand name and intentionally not touched by this rename. + ## Repository layout This is not a single project but a working directory with one active app plus reference/planning material: @@ -129,6 +139,48 @@ Added 2026-07-24 per customer feedback: when a patient moves, both the Krankenka Added 2026-07-24: until then there was no way to fix a typo made at patient creation — only Pflegegrad (`PATCH .../pflegegrad`) and Notizen (`POST .../notizen`, audit-trailed via `notizen_verlauf`) had dedicated update routes/UI; every other field (name, address, contact, insurance) was write-once via the "Neuer Patient" wizard. `PUT /api/patienten/{pid}` (`PatientUpdate` schema, generic `setattr` loop in `routers/patienten.py`) already supported updating all of these — it just had no frontend form. `app/(app)/patienten/[id]/bearbeiten/page.tsx` is that form: a single-page (not multi-step, unlike the creation wizard) edit form prefilled from the current patient, "Bearbeiten" link added next to "Löschen" on the patient detail page. Deliberately excludes `notizen` (keeps going through its own audited flow) and `patient_status`/`pflegegrad`/`foto_pfad` (each has its own dedicated update path already). +## Passwort-vergessen per SMTP (pflegepilot_v3 only, 2026-07-24) — deliberate exception to "no SMTP" + +The operator noticed self-registered accounts had no email address at all, so a "Passwort vergessen" flow had no way to verify identity. Fixed by adding `nutzer.email` (unique, mandatory on new self-registrations, optional/backfillable on admin-created accounts) plus a standard token-based reset flow. + +**This was, until 2026-07-28, the only place in `pflegepilot_v3` where the server sends email of its own accord.** (The "Email: mailto hand-off, not SMTP" section further below describes `pflegepilot_alpha_v2`'s design, not v3's — v3's own Musterbrief/Widerspruch/Ummeldung/Vollmacht generators don't send email at all, mailto or otherwise, they only offer a PDF download.) Two more proactive-email use cases were added on 2026-07-28 with operator sign-off; see "Proaktiver Server-Mailversand erweitert" below for the current, complete list and the reasoning for each. Don't add a *fourth* proactive-email use case without the same kind of explicit check-in — this isn't meant to become the default pattern for every notification idea. + +- `app/email_util.py: send_email()` is the one place SMTP is touched. Configured via `SMTP_HOST`/`SMTP_PORT`/`SMTP_USER`/`SMTP_PASSWORD`/`SMTP_FROM`/`SMTP_USE_SSL`/`SMTP_USE_TLS` env vars (`app/config.py`), all optional. **As long as `SMTP_HOST` is unset, it only logs a warning and returns — never raises.** This was deliberate, not a stopgap to remove later: `POST /api/auth/passwort-vergessen` must always return the same generic response regardless of whether the email exists or whether sending succeeds (anti-enumeration), so `send_email()` swallowing its own failures is exactly the right shape for that caller, not a bug. +- `docker-compose.yml`'s `api.environment` block only passes through explicitly listed keys — adding a new `Settings` field in `config.py` is not enough by itself, the corresponding `${VAR}` line must also be added to `environment:` (this bit locally the first time: the `.env` value was set correctly but never reached the container until `docker-compose.yml` itself was updated). `SMTP_USE_SSL`/`smtp_use_ssl` was added alongside `smtp_use_tls` (STARTTLS) because Strato (the operator's SMTP provider) offers both port 465 (implicit SSL, `smtplib.SMTP_SSL`) and 587 (STARTTLS on a plaintext-then-upgraded connection) — these are different connection modes, not interchangeable, and the operator's provider notes specified SSL/465. **Live and verified 2026-07-24** against the operator's real Strato mailbox (`support@db-onlinedienste.de`, port 465/SSL) — both a direct `send_email()` call and the actual `/api/auth/passwort-vergessen` endpoint were tested end-to-end with no errors logged. +- **The real SMTP password lives in `pflegepilot_v3/secrets/SMTP-Secrets.txt`** (gitignored — this folder was untracked-but-NOT-ignored when the operator first created it, meaning a stray `git add -A` would have committed a plaintext password; added to `.gitignore` in the same pass this feature shipped). Never read this file's contents into a chat response, a commit, or any other persisted location — when working with it, treat the value as write-only: read it, pipe it directly into `.env` via file transfer (not inline shell arguments — special characters in the actual password broke naive shell-quoting attempts elsewhere in this project's history), never echo or log it. +- Reset tokens: `secrets.token_urlsafe(32)`, only the SHA-256 hash is stored (`nutzer.reset_token_hash`/`reset_token_laeuft_ab`), 1-hour validity, single-use (cleared on successful reset). Same rate-limiting pattern as `login`/`registrieren` (`is_rate_limited`, keyed by IP). +- The reset-link landing page (`/passwort-zuruecksetzen?token=...`) had to go in `(marketing)/`, not `(app)/` — the `(app)` layout shows a blocking `AuthOverlay` for anyone not logged in, which would swallow the page before the reset form ever rendered. `(marketing)/` has no such gate, which is exactly why Impressum/Datenschutz/AGB already live there. +- **Existing accounts created before this change have no email** and can't use password-reset until one is set — either the account holder re-registers, or an admin sets `email` via `PUT /api/nutzer/{uid}` (schema-ready; no dedicated admin edit-account modal exists yet in pflegepilot_v3's admin dashboard to do this through the UI — only the quick-create form and sperren/entsperren/löschen/Freischalten/Rechnung actions exist there today). Building that modal is a natural next step but wasn't part of this task. + +## Berechtigungsbestätigung je Patient (pflegepilot_v3 only, 2026-07-24) + +Operationalizes what the ToS (Ziffer 5) and Datenschutzerklärung (Ziffer 7) already say in prose — that the Nutzer, not Pflegepilot, is responsible for having a legal basis (Einwilligung, Pflege-/Fürsorgebeziehung, or rechtliche Vertretung) to enter another person's data — as an actual captured declaration, same pattern as the Widerrufsrecht consent at registration (§ 356 BGB) added earlier the same day. + +- **Per patient, not once per account** (deliberate choice, confirmed with the operator): each care recipient is a distinct person who may have a distinct authorization basis (e.g. Vorsorgevollmacht for one parent but not the other), so the checkbox appears on Step 6 ("Übersicht") of the "Neuer Patient" wizard every time, not just for the first patient created. +- Enforced server-side (`POST /api/patienten` rejects with 400 if `berechtigung_bestaetigt` isn't `true`), not just a disabled button — same reasoning as the registration consent: a client-only gate is trivially bypassed by calling the API directly. +- `patient.berechtigung_bestaetigt_am` (migration `f552607a045e`) stores the timestamp as the evidentiary record, mirroring `nutzer.widerrufsverzicht_bestaetigt_am`. Not exposed via `PatientOut` (kept DB-only, same restraint as the Nutzer field) — query directly if ever needed. +- `PatientCreate.berechtigung_bestaetigt` is excluded from the `model_dump()` spread into the `Patient(...)` constructor in `routers/patienten.py` (it isn't a column — only `_am` is) — don't add fields to `PatientCreate` that aren't 1:1 `Patient` columns without checking this exclusion still holds. + +## Incident 2026-07-24: German-comma price value crashed the entire admin dashboard + +`app/einstellungen.py: abo_preise()` used to do a bare `decimal.Decimal(get_setting(...))`. The admin saved the "Aufpreis je zusätzlichem Patienten" field in Backend-Einstellungen with a German-formatted value (`"4,99"` — comma, not dot) — completely natural input for a German user, but `decimal.Decimal("4,99")` raises `InvalidOperation`. Since `abo_preise()` is called from `GET /api/nutzer` (admin's Nutzerliste, to compute `abo_preis_aktuell` per account), this took down the **entire** admin account list with a 500 — reported by the operator as "the accounts are gone," when in fact no data was lost at all, only the listing endpoint crashed. + +Fixed in two ways, not just one: `_preis_decimal()` normalizes `,` → `.` before parsing, *and* falls back to the compiled-in default on any other `InvalidOperation` rather than propagating the exception — a plain textarea/text-input settings field will eventually get a bad value again (typo, copy-paste artifact, whatever), and a single bad price string must never be able to crash every endpoint that happens to call `abo_preise()` (currently: admin Nutzerliste, `/api/abo/status`, `/api/einstellungen/public`, invoice generation). The admin Einstellungen form (`app/(app)/admin/einstellungen/page.tsx`) also got a small inline hint under both price fields ("bitte mit Punkt, nicht Komma") — cheap prevention, but the defensive parsing is the real fix; don't remove it even if the hint seems to make it redundant. + +**General lesson for this settings store**: any numeric `einstellungen` value that gets `decimal.Decimal()`/`int()`/etc.-parsed at read time is one bad save away from taking down every caller, since `get_setting()` returns whatever string is stored with no validation at write time. If you add another numeric settings key, parse it as defensively as `_preis_decimal()` does — don't assume the stored string is well-formed just because the admin UI is "just" a plain text input. + +## Legal-page links inside the logged-in app shell (pflegepilot_v3 only, 2026-07-24) + +Gap found by the operator: `pflegepilot_alpha_v2` already has a `.sb-legal` sidebar link row for Impressum/Datenschutz/AGB (see that section above), but `pflegepilot_v3`'s `(app)/layout.tsx` never got the equivalent — once logged in, there was no way to reach any of the three legal pages at all (they only existed via the `(marketing)/` footer, which a logged-in user viewing `/dashboard` etc. never sees). Fixed with a small link row at the bottom of the sidebar `