CluPilotCloud/docs/handoffs/2026-07-28-operator-identit...

152 lines
7.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# Handoff — CluPilot (Stand 2026-07-28)
**Für:** frische Claude-Code-Session
**Zuerst lesen:** dieses Dokument, dann `CLAUDE.md` (**R22 zuerst**, dann R18R21).
**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 `<x-ui.nav-item>`, nicht für `<x-ui.button>`.
- `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.
- **`<x-ui.button>` hat keinen Icon-Slot** — ein `<x-slot:icon>` 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`.