13 KiB
Handoff — CluPilot (Stand 2026-07-27)
Für: frische Claude-Code-Session
Art: Zustands-/Fortsetzungs-Handoff
Zuerst lesen: dieses Dokument komplett, dann CLAUDE.md (R18–R20) und git log --oneline -15.
Vorgänger: docs/handoffs/2026-07-25-clupilot-state-handoff.md (Umgebung, Stack, Befehle, R1–R17 — gilt unverändert weiter).
0. TL;DR
Seit dem letzten Handoff: 34 Commits, Zweig feat/portal-design ist in main gemerged und gepusht. 637 Pest-Tests grün.
Inhaltlich waren das drei Blöcke:
- Ein Designsystem für alle drei Oberflächen, gebaut gegen eine vom Nutzer freigegebene Vorlage — und diesmal gemessen statt geglaubt.
- Erfundene Daten raus, echte Messreihen rein. Speicher, Datenvolumen und Verfügbarkeit werden jetzt tatsächlich erhoben; ein Demo-Kunde liegt als echte Datenbankzeilen vor.
- Drei gemeldete Fehlerklassen abgestellt und als Regeln festgeschrieben: Icon-Layout (R18), Zeitzone (R19), Bearbeiten im Modal (R20).
Nächster Block: die zwölf Konsolen-Ansichten auf das Designsystem umstellen, die Support-Anfragen in der Konsole beantwortbar machen, die Startseite ins Blade übertragen.
1. Was fertig ist
1.1 Designsystem
- Ein Token-Satz in
resources/css/portal-tokens.css(hell: ground#f6f6f8, card#fff, line#e9e9ee, ink#17171c, accent#f97316, Radien 9/11/16/22).admin-tokens.cssträgt nur noch Dichte (13.5px Basis, engere Radien), keine eigenen Farben. - Ein Shell für beide Layouts:
resources/views/components/shell/{head,nav,topbar}.blade.php. Navigation und Brotkrume kommen aus einer Quelle,app/Support/Navigation.php— auf Route-Namen gematcht, weil sich der Pfad der Konsole zwischen host-gebundenem und Fallback-Modus ändert, ihre Route-Namen aber nicht. - Komponenten:
ui/metric,ui/ring,ui/spark,ui/button,ui/badge,ui/icon,ui/nav-item. - Die Kleinkapitälchen-Beschriftung ist eine CSS-Klasse
.lbl(11.5px,.07em). Vorher stand die Zahl an drei Stellen einzeln — so ist sie von der Vorlage weggedriftet.
Wie das Angleichen verifiziert wurde (wichtig, weil es mehrfach schiefging): Vorlage und Umsetzung wurden im Browser gerendert und mit getComputedStyle gemessen, dann Spalte gegen Spalte verglichen. Das hat sofort einen Streit entschieden — eine Prüfung des Vorlagen-Quelltexts behauptete 20px Rasterabstand, gerendert sind es 14px. Codex-Urteil zum Schluss: „Yes. Side by side, a person would call them the same design.", einzige Abweichung 1px.
1.2 Echte Daten statt Attrappen
instance_metrics(Migrationen2026_07_27_150000+_160000): eine Zeile je Instanz und Tag,disk_used_bytes/disk_total_bytesnullable, dazurx_bytes,tx_bytes,checks_total,checks_ok.CollectInstanceTrafficliest den Plattenstand per Proxmox-guestExec(df),SyncMonitoringStatuszählt Prüfungen nur, wenn eine echte Antwort kam.- Regel dahinter: eine Messung, die nicht genommen werden konnte, ist keine Messung von null. Fehlende Tage bleiben fehlend, eine ungeprüfte Instanz liefert
nullstatt 100 %. DemoCustomerSeeder: ein vollständiger Kunde als echte Zeilen (Instanz, Abo, 6 Plätze über vier Rollen, Backup, Periodenverkehr, 30 Tage Messreihe mit Absicht-Zittern und einem Tag auf 286/288). Entfernen ist eine Löschung. Das Passwort kommt ausDEMO_PASSWORDoder wird zufällig erzeugt und einmalig ausgegeben — auf einem öffentlich erreichbaren Login liegt kein Passwort im Quelltext.- Entfernt wurden: erfundene Support-Tickets, Dashboard-Fixtures, der Rechenzentrumsname (nur noch das Land), eine erfundene SLA, ein hartkodiertes „B", Laufzeit-Versionen, die keinen Kunden interessieren.
1.3 Kundenportal
- Dashboard mit vier Karten (Speicher/Benutzer/Datenvolumen/Verfügbarkeit) auf Vorlagenmaß, Ring 62px, Sparkline aus echter Reihe.
- Downgrade über
App\Services\Billing\DowngradeCheck— geprüft wird gegen gemessenen Speicher, nicht gegen das verkaufte Kontingent, und die Blockade wird in den Zahlen des Kunden benannt („Sie haben 31 Benutzer, Start erlaubt 10"). - Benutzer-Tabelle: Bearbeiten (Modal), Sperren/Entsperren, Entfernen. Aktionsspalte immer sichtbar.
- Support-Seite auf echte Daten:
support_requests(Migration2026_07_27_180000), Anfrage-Modal, eigene Historie, FAQ mit Sprungzielen ins Panel.
1.4 Betrieb
- VPN funktioniert (iPhone + Laptop bestätigt). Der Bereitschafts-Test fragte vorher
https://127.0.0.1/, während das Gateway nur auf10.66.0.1:443lauscht — dadurch bliebVPN_READYfalsch und die App hieltDNS = 10.66.0.1zurück. Jetzt eigener Health-Port, 60-s-Fenster. - iOS-Zoom beim Fokussieren abgestellt (
@media (pointer:coarse)→ Felder 16px). Bewusst nicht über die Viewport-Sperre, die das Pinch-Zoom mit töten würde. - Update-Knopf und Update-Beobachter, siehe 2.3.
1.5 Regeln (in CLAUDE.md, jeweils per Test erzwungen)
| Regel | Inhalt | Test |
|---|---|---|
| R18 | Icon neben dem Text, nie darüber, nie größer als bestellt | IconLayoutTest |
| R19 | Gespeichert in UTC, angezeigt in app.display_timezone via ->local() |
DisplayTimezoneTest |
| R20 | Bearbeiten mit Feldern passiert im Modal, nie in der Zeile | EditInModalTest |
2. Was teuer gelernt wurde
2.1 Das Muster hinter den meisten Rückmeldungen
Ich habe eine Regel gelesen, sie geglaubt und das gerenderte Ergebnis nicht gemessen. Jede Runde Kritik („verwendest du die vorlage überhaupt", „das habe ich nun 3 mal geschrieben") hatte dieselbe Ursache. Konkrete Ausprägungen:
- CSS-Kaskade dreimal. Gleiche Spezifität + Reihenfolge entscheidet; ein undefiniertes
var()rechnet zuunset(beibackground-colortransparent);.open pschlägt.tpl. - Namenskollisionen mit Tailwind-Utilities. Meine Klasse
.ringkollidierte mit Tailwindsring(box-shadow: 0 0 0 3px rgb(59 130 246/.5)) — daher der blaue Rahmen am Chart, den ich einmal fälschlich als Screenshot-Artefakt abtat. Ebenso.numgegen die Tabellenziffern-Utility. Umbenannt in.metric-ring/.count. - Eigene Sammeländerungen. Ein früherer Massenlauf von mir hatte 23 Seitentiteln
sm:text-3xl(40px) angehängt; die Vorlage hat 30px.
Konsequenz für die nächste Session: bei allem Optischen im Browser messen, nicht im Quelltext lesen. getComputedStyle gegen die Vorlage, beide Seiten.
2.2 Tests, die den Fehler mitrechnen
Die Zeitzonen-Tests prüften nichts, weil sie den erwarteten Wert mit demselben falschen Aufruf bildeten wie die Ansicht. Ein Test, der die Implementierung nachrechnet, ist keiner. Sie prüfen jetzt die Wanduhr und zusätzlich, dass die UTC-Zeit nicht erscheint.
2.3 Ein Wert ist eine Messung, kein Zustand
Der Update-Knopf war nur freigeschaltet, wenn die letzte Agent-Prüfung behind > 0 ergab. Die läuft alle fünf Minuten — nach einem Push behauptet die Konsole minutenlang, alles sei aktuell, und weigert sich zu handeln. Jetzt immer anbietbar, solange ein Agent lebt und kein Lauf aktiv ist.
Und: das Update startet die Container neu, auf denen das wire:poll läuft. Mitten im Lauf scheitert jede Anfrage, danach befragt altes JavaScript einen neuen Build — deshalb kam die Fertigmeldung nie an. Ein Alpine-Beobachter (updateWatcher in resources/js/app.js) fragt jetzt admin.update.state (schlichtes JSON, kein Livewire), behandelt eine gescheiterte Anfrage als den Neustart und lädt neu, sobald der Build wechselt.
2.4 Weitere Fallen
- Blade-Anonymkomponenten müssen unter
resources/views/components/liegen. - Tailwind kann keinen Deckkraft-Modifikator auf ein blankes
var()anwenden →color-mix(in srgb, …). #[Validate]an einer Livewire-Eigenschaft gilt klassenweit:$this->validate()in einer anderen Aktion zieht sie mit. Regeln gehören an die Aktion.- Carbon führt Makros in einer globalen Tabelle — zwei Registrierungen mit verschiedenem Rumpf, und die letzte gewinnt für alle Klassen.
Illuminate\Support\Carbonist mutabel: ohnecopy()schreibt bloßes Anzeigen das Modellattribut um.- Laravel gibt bei einer
null-Übersetzung den Schlüssel aus (traf die Fehlerseiten). php artisan view:clearim Container als falscher Benutzer hinterlässt Rechte, die zutouch(): Utime failedführen. Danachchown -R www-data:www-data storage/framework bootstrap/cache.
3. Offene Punkte
3.1 Zwingend als Nächstes
| # | Punkt | Umfang |
|---|---|---|
| 1 | Konsolen-Ansichten aufs Designsystem | 22 Blades unter resources/views/livewire/admin/. Sie laufen und sind funktionsfähig, tragen aber noch nicht durchgängig die Vorlagenmaße. |
| 2 | Support in der Konsole | Die Anfragen liegen in support_requests mit Kunde und Instanz daran, aber es gibt keine Ansicht, in der man sie beantwortet. Der Nutzer hat ausdrücklich gesagt, „wer stellt die Anfrage" gehöre in die Konsole und nicht zum Kunden. Fehlt: Warteschlange, Statuswechsel, Antwort + Mail. |
| 3 | Startseite ins Blade | Die freigegebene Vorlage liegt in docs/design/tpl-home.html; resources/views/landing.blade.php ist noch die alte Fassung. |
3.2 Vereinbart, noch nicht gebaut
- Integrationen/Secrets erweitern. Der Nutzer will alle Anbindungen dort: Hetzner DNS, Proxmox, Stripe, Monitoring, Backup-Speicher.
SecretVault::REGISTRYhat vier Einträge. - Mehrere Absender: no-reply, office, support, info, billing.
- Automatische E-Mail-Vorlagen: Bestellbestätigung, Störungsmeldung aus dem Dashboard heraus.
3.3 Rollout
mainist gepusht; der Live-Server (191.218.161.8) folgtmain, aktualisiert wird über Konsole → Einstellungen → Update anfordern (VPN-only), nicht per SSH.- Der Demo-Kunde ist auf live noch nicht angelegt. Nach dem Update:
Das Passwort wird einmalig ausgegeben. Entfernen:php artisan db:seed --class=DemoCustomerSeeder --forceUser::where('email','demo@clupilot.com')->first()?->delete(). - SSH zum Live-Server und
pip installsind in dieser Umgebung vom Berechtigungsfilter blockiert. Nicht umgehen — den Nutzer bitten oder ihn den Deploy auslösen lassen.
3.4 Zwei Dinge, die eine Entscheidung brauchen
- Badge gegen Button in der Kopfzeile. Der Nutzer hatte beanstandet, dass der Button größer ist als das Badge. In der freigegebenen Vorlage sind sie tatsächlich verschieden hoch (29px gegen 40px, mittig zueinander). Ich bin der Vorlage gefolgt, weil „genauso erstellen" die deutlichere Ansage war. Angleichen wäre eine bewusste Abweichung — seine Entscheidung.
- Bestehende Wartungsfenster sind verschoben gespeichert. Sie wurden über das alte Formular erfasst, das UTC hineinschrieb und UTC zurücklas. Die Anzeige ist ab R19 korrekt und zeigt ehrlich, was gespeichert wurde — wer damals 21:00 meinte, hat 23:00 in der Datenbank. Auf dev sichtbar:
Bewusst nicht automatisch umgerechnet: von außen ist nicht unterscheidbar, welches Fenster als Ortszeit gemeint war und welches absichtlich in UTC gesetzt wurde. Muss durchgesehen werden.test | gespeichert 28.07. 21:00 UTC | zeigt jetzt 28.07. 23:00
4. Verifikations-Workflow (unverändert gültig)
docker compose exec -T app php artisan test— muss vollständig grün sein.docker compose exec -T app npm run build.- Im Browser ansehen und messen, bei Optischem gegen die Vorlage (siehe 2.1). Konsolenfehler: 0.
- Codex-Review (R15) — Ablauf steht in der Nutzer-Memory
clupilot-r15-codex-review. Kurz:export PATH="$HOME/.local/bin:$PATH", danncodex exec …; im Hintergrund laufen lassen, der Aufruf überschreitet zwei Minuten. - Commit, Push auf
main.
5. Wichtige Dateien
| Zweck | Pfad |
|---|---|
| Regeln R18–R20 | CLAUDE.md |
| Farben/Maße | resources/css/portal-tokens.css |
| Dichte Konsole | resources/css/admin-tokens.css |
| Navigation (eine Quelle) | app/Support/Navigation.php |
| Zeitzone: Anzeige | Makro ->local() in app/Providers/AppServiceProvider.php |
| Zeitzone: Formularfelder | app/Support/LocalTime.php |
| Downgrade-Prüfung | app/Services/Billing/DowngradeCheck.php |
| Messreihen | app/Models/InstanceMetric.php, app/Provisioning/Jobs/CollectInstanceTraffic.php |
| Update-Kanal | app/Services/Deployment/UpdateChannel.php, deploy/update-agent.sh |
| Demo-Kunde | database/seeders/DemoCustomerSeeder.php |
| Vorlagen (freigegeben) | docs/design/tpl-{home,portal,console}.html + docs/design/README.md |
Die Vorlagen lagen ursprünglich im sitzungsgebundenen Scratchpad und sind jetzt im Repo.
docs/design/README.mdhält fest, wie man dagegen prüft (rendern und messen, nicht Quelltext lesen) und listet die Maße, die schon einmal danebenlagen.