From 9fc74eedc3f9c6679304f52b3b3f4391c8525ffb Mon Sep 17 00:00:00 2001 From: nexxo Date: Tue, 28 Jul 2026 16:13:08 +0200 Subject: [PATCH] Hand off with the effort rule at the top, not the feature list --- .../2026-07-28-operator-identity-handoff.md | 151 ++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 docs/handoffs/2026-07-28-operator-identity-handoff.md diff --git a/docs/handoffs/2026-07-28-operator-identity-handoff.md b/docs/handoffs/2026-07-28-operator-identity-handoff.md new file mode 100644 index 0000000..6d67628 --- /dev/null +++ b/docs/handoffs/2026-07-28-operator-identity-handoff.md @@ -0,0 +1,151 @@ +# Handoff — CluPilot (Stand 2026-07-28) + +**Für:** frische Claude-Code-Session +**Zuerst lesen:** dieses Dokument, dann `CLAUDE.md` (**R22 zuerst**, dann R18–R21). +**Vorgänger:** `docs/handoffs/2026-07-27-portal-design-handoff.md` (Umgebung, Stack, Befehle — gilt unverändert). + +--- + +## 0. Das Wichtigste zuerst: R22 + +Die letzte Sitzung hat für eine Tabellenumstellung **zehn Codex-Runden und rund +zwölf Stunden** gebraucht und einen erheblichen Teil des Wochenkontingents des +Nutzers verbraucht. Die Funde waren echt, die Priorisierung war falsch. + +**R22 in `CLAUDE.md` ist die Konsequenz und bindend.** Kurzfassung: + +- Eine Prüfrunde, eine Fix-Runde, ein Re-Review über den Fix-Diff. Danach parken. +- Codex/R15: höchstens **zwei Runden ohne P1**, Rest als Folgepunkt in den MR. +- **Nie Dateien prüfen, die seit dem letzten grünen Testlauf unverändert sind.** +- Mutationstests nur an der Grenze zu Datenverlust, Rechteausweitung, Preisgabe. +- Aufgaben ohne Entwurfsentscheidung bekommen keinen eigenen Review. +- Unabhängige Analysen parallel starten, nicht nacheinander. + +Der Nutzer wartet auf ein Ergebnis, nicht auf ein Verfahren. + +--- + +## 1. Stand + +**Zwei Merge Requests offen, beide gegen `main`:** + +| | | +|---|---| +| [PR #2](https://git.bave.dev/boban/CluPilotCloud/pulls/2) | `feat/operator-identity` → `main`. **Enthält PR #1 mit.** Das ist der einzige, der gemergt werden muss. | +| [PR #1](https://git.bave.dev/boban/CluPilotCloud/pulls/1) | `feat/mailboxes` → `main`. Durch #2 überflüssig, kann geschlossen werden. | + +**885 Tests grün.** Zweig `feat/operator-identity` ist gepusht, Arbeitsbaum sauber. + +**`main` hat den Stand noch nicht** — PR #2 muss gemergt werden, bevor der +Live-Server über Konsole → Einstellungen → Update aktualisiert werden kann. + +--- + +## 2. Was fertig ist + +### 2.1 Betreiber-Identität (die ursprüngliche Meldung) + +Der Nutzer meldete: Admin-Login wird mit dem App-Login geteilt, zeigt +„Registrieren", das führt in eine 404, und der Serverstandort-Text ist zu lang. + +- **Eigene Tabelle `operators`**, eigener Guard `operator`, eigene Anmeldeseite + (`App\Livewire\Auth\OperatorLogin`) mit eigenem 2FA-Ablauf. Fortify bleibt + beim Portal. +- Alle 17 Spatie-Berechtigungen und 6 Rollen sind von `web` nach `operator` + **umgezogen**, nicht verdoppelt. `User` hat keine Rollen mehr. +- **Registrieren-Link und 404 sind an der Ursache weg**: `RestrictAdminHost::SHARED` + führt nur noch `livewire/*`, `up` und `broadcasting/auth`. `/register` ist auf + dem Konsolen-Host keine Route mehr. +- Serverstandort: nur noch `EU` (deckt sich mit `docs/design/tpl-home.html`). +- Impersonation über **signierte Einmal-URL** gegen den Portal-Host (Cookies + sind host-gebunden, ein `Auth::login()` über zwei Hosts konnte nie funktionieren). +- 2FA-Pflicht als Schalter des Inhabers, mit eigener Einrichtungsseite + `admin.two-factor-setup` und Aussperr-Schutz. +- **R21** in `CLAUDE.md`, erzwungen durch `tests/Feature/IdentitySeparationTest.php`. + +### 2.2 Postfächer (im selben MR) + +Adressen als Datensätze statt eines fest verdrahteten SMTP-Zugangs: Tabelle +`mailboxes`, kuratierte Zweckliste, `From`/`Reply-To`, Konsolenseite mit +`mail.manage`, Testversand, der bewusst am `MAIL_MAILER=log` vorbeigeht. + +--- + +## 3. Live-Gang — Reihenfolge nicht vertauschen + +1. **PR #2 mergen.** +2. **`SECRETS_KEY` erzeugen** und in `.env` eintragen: + `head -c 32 /dev/urandom | base64` + Ohne den speichert die Zugangsdaten-Seite **gar nichts** — auch Stripe, DNS + und Monitoring nicht. Er ist auf keinem Server gesetzt. +3. **Update über Konsole → Einstellungen → Update anfordern** (VPN-only, nicht per SSH). + Die Migration läuft dabei mit. +4. **Postfach-Passwörter eintragen, Testversand je Postfach.** +5. **Erst dann** `MAIL_SCHEME=smtp` (aktuell steht dort `tls`, was Symfony + ablehnt) und `MAIL_MAILER=smtp`. + +Andersherum bricht die Bereitstellung eines zahlenden Kunden ab: `CloudReady` +wird bewusst synchron verschickt, damit ein Mailfehler den Schritt wiederholt +statt das Passwort zu verlieren. + +### Was die Migration mit den Konten macht + +Passwort-Hash und 2FA werden **unverändert übernommen** — dasselbe Passwort wie +bisher. Vor jeder Änderung prüft sie vorab, ob jemand Betreiber *und* Kunde ist, +und bricht dann ab, ohne etwas angefasst zu haben, mit allen betroffenen Adressen. + +**Live hat betreibereigene VPN-Peers** (WireGuard auf zwei Geräten). `vpn_peers.user_id` +ist `ON DELETE RESTRICT` — die Migration schreibt die Werte um, bevor sie löscht. +Das war der Fund, der den Live-Gang gerettet hat; er ist getestet, aber auf dev +nie mit echten Zeilen durchgespielt worden. **Vor dem Live-Update eine Probe auf +dev mit echten VPN-Peer-Zeilen fahren.** + +Auf dev ist der Umzug bereits erfolgt: `operators` enthält +`admin@clupilot.local` und `boban.blaskovic@gmail.com`, `users` nur noch +Kundenkonten. + +--- + +## 4. Offene Punkte + +### 4.1 Als Folgepunkte geparkt (aus PR #2, bewusst nicht gebaut) + +- Mitarbeiterverwaltung als eigene Konsolen-Ansicht (`staff.manage` existiert). +- Passwort-Zurücksetzen für Operatoren (`passwords.operators` ist konfiguriert, + aber unverdrahtet — mit 2FA-Pflicht ist der einzige Weg zurück die Kommandozeile). +- `RequireOperatorTwoFactor` fehlt in Livewires `addPersistentMiddleware()`: eine + bereits offene Konsolenseite arbeitet nach dem Umlegen des Schalters weiter. +- `IconLayoutTest` prüft R18 nur für ``, nicht für ``. +- `Secrets::test()` hat keinen `isUsable()`-Schutz — bei kaputtem `SECRETS_KEY` + wirft der Testknopf ungefangen. Vorbestehend. + +### 4.2 Aus dem vorigen Handoff weiterhin offen + +- 22 Konsolen-Blades auf das Designsystem. +- Support-Warteschlange in der Konsole (**jetzt baubar**, die Absender existieren). +- Startseite ins Blade (`docs/design/tpl-home.html`). +- E-Mail-Vorlagen: Bestellbestätigung, Störungsmeldung. +- Verschoben gespeicherte Wartungsfenster (Entscheidung des Nutzers, siehe + Vorgänger-Handoff §3.4). + +--- + +## 5. Fallen, die diese Sitzung gekostet haben + +- **`actingAs()` ruft `Auth::shouldUse()`** und macht den genannten Guard zum + Standard. Damit verschwindet jeder Unterschied zwischen `auth()->user()` und + `auth()->guard('x')->user()` — das hat zwei echte Fehler verdeckt. Wer eine + Guard-Unterscheidung beweisen will, meldet mit `Auth::guard('x')->login()` an. +- **Pest: `toThrow(SomeInterface::class)`** degradiert still zu einem + String-Vergleich auf der Fehlermeldung, weil `class_exists()` für Interfaces + `false` ist. Nie gegen Interface-Namen prüfen. Siehe Nutzer-Memory + `pest-tothrow-interface-trap`. +- **SQLite verdeckt Migrationsfehler**, die MariaDB zeigt: Transaktionen um DDL, + doppelt gelöste Constraints, zusammenfallende Auto-Increment-Werte. Migrationen + gegen die echte dev-Datenbank prüfen. +- **`` hat keinen Icon-Slot** — ein `` wird still + verschluckt. Icon in den Standard-Slot mit `class="size-4"`. +- **Es gibt kein `layouts.guest`.** Auth-Seiten benutzen `layouts.portal`. +- **`users` hat keine `uuid`-Spalte**, `Customer` und `Operator` schon. +- **Ein Kunde hängt über `customers.user_id`** am Benutzer, nicht über + `users.customer_id`.