Hand off with the effort rule at the top, not the feature list
tests / pest (pull_request) Failing after 7m21s Details
tests / assets (pull_request) Successful in 23s Details
tests / release (pull_request) Has been skipped Details
tests / pest (push) Failing after 7m18s Details
tests / assets (push) Successful in 27s Details
tests / release (push) Has been skipped Details

feat/operator-identity
nexxo 2026-07-28 16:13:08 +02:00
parent 75cb45bcbc
commit 9fc74eedc3
1 changed files with 151 additions and 0 deletions

View File

@ -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 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`.