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