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

7.0 KiB
Raw Blame History

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 feat/operator-identitymain. Enthält PR #1 mit. Das ist der einzige, der gemergt werden muss.
PR #1 feat/mailboxesmain. 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.