7.0 KiB
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 | feat/operator-identity → main. Enthält PR #1 mit. Das ist der einzige, der gemergt werden muss. |
| PR #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 Guardoperator, eigene Anmeldeseite (App\Livewire\Auth\OperatorLogin) mit eigenem 2FA-Ablauf. Fortify bleibt beim Portal. - Alle 17 Spatie-Berechtigungen und 6 Rollen sind von
webnachoperatorumgezogen, nicht verdoppelt.Userhat keine Rollen mehr. - Registrieren-Link und 404 sind an der Ursache weg:
RestrictAdminHost::SHAREDführt nur nochlivewire/*,upundbroadcasting/auth./registerist auf dem Konsolen-Host keine Route mehr. - Serverstandort: nur noch
EU(deckt sich mitdocs/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-setupund Aussperr-Schutz. - R21 in
CLAUDE.md, erzwungen durchtests/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
- PR #2 mergen.
SECRETS_KEYerzeugen und in.enveintragen:head -c 32 /dev/urandom | base64Ohne den speichert die Zugangsdaten-Seite gar nichts — auch Stripe, DNS und Monitoring nicht. Er ist auf keinem Server gesetzt.- Update über Konsole → Einstellungen → Update anfordern (VPN-only, nicht per SSH). Die Migration läuft dabei mit.
- Postfach-Passwörter eintragen, Testversand je Postfach.
- Erst dann
MAIL_SCHEME=smtp(aktuell steht dorttls, was Symfony ablehnt) undMAIL_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.manageexistiert). - Passwort-Zurücksetzen für Operatoren (
passwords.operatorsist konfiguriert, aber unverdrahtet — mit 2FA-Pflicht ist der einzige Weg zurück die Kommandozeile). RequireOperatorTwoFactorfehlt in LivewiresaddPersistentMiddleware(): eine bereits offene Konsolenseite arbeitet nach dem Umlegen des Schalters weiter.IconLayoutTestprüft R18 nur für<x-ui.nav-item>, nicht für<x-ui.button>.Secrets::test()hat keinenisUsable()-Schutz — bei kaputtemSECRETS_KEYwirft 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()ruftAuth::shouldUse()und macht den genannten Guard zum Standard. Damit verschwindet jeder Unterschied zwischenauth()->user()undauth()->guard('x')->user()— das hat zwei echte Fehler verdeckt. Wer eine Guard-Unterscheidung beweisen will, meldet mitAuth::guard('x')->login()an.- Pest:
toThrow(SomeInterface::class)degradiert still zu einem String-Vergleich auf der Fehlermeldung, weilclass_exists()für Interfacesfalseist. Nie gegen Interface-Namen prüfen. Siehe Nutzer-Memorypest-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 mitclass="size-4".- Es gibt kein
layouts.guest. Auth-Seiten benutzenlayouts.portal. usershat keineuuid-Spalte,CustomerundOperatorschon.- Ein Kunde hängt über
customers.user_idam Benutzer, nicht überusers.customer_id.