CluPilotCloud/docs/handoffs/2026-07-27-portal-design-ha...

13 KiB
Raw Blame History

Handoff — CluPilot (Stand 2026-07-27)

Für: frische Claude-Code-Session Art: Zustands-/Fortsetzungs-Handoff Zuerst lesen: dieses Dokument komplett, dann CLAUDE.md (R18R20) und git log --oneline -15. Vorgänger: docs/handoffs/2026-07-25-clupilot-state-handoff.md (Umgebung, Stack, Befehle, R1R17 — 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:

  1. Ein Designsystem für alle drei Oberflächen, gebaut gegen eine vom Nutzer freigegebene Vorlage — und diesmal gemessen statt geglaubt.
  2. Erfundene Daten raus, echte Messreihen rein. Speicher, Datenvolumen und Verfügbarkeit werden jetzt tatsächlich erhoben; ein Demo-Kunde liegt als echte Datenbankzeilen vor.
  3. 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.css trä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 (Migrationen 2026_07_27_150000 + _160000): eine Zeile je Instanz und Tag, disk_used_bytes/disk_total_bytes nullable, dazu rx_bytes, tx_bytes, checks_total, checks_ok.
  • CollectInstanceTraffic liest den Plattenstand per Proxmox-guestExec (df), SyncMonitoringStatus zä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 null statt 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 aus DEMO_PASSWORD oder 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 (Migration 2026_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 auf 10.66.0.1:443 lauscht — dadurch blieb VPN_READY falsch und die App hielt DNS = 10.66.0.1 zurü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 zu unset (bei background-color transparent); .open p schlägt .tpl.
  • Namenskollisionen mit Tailwind-Utilities. Meine Klasse .ring kollidierte mit Tailwinds ring (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 .num gegen 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\Carbon ist mutabel: ohne copy() schreibt bloßes Anzeigen das Modellattribut um.
  • Laravel gibt bei einer null-Übersetzung den Schlüssel aus (traf die Fehlerseiten).
  • php artisan view:clear im Container als falscher Benutzer hinterlässt Rechte, die zu touch(): Utime failed führen. Danach chown -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::REGISTRY hat 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

  • main ist gepusht; der Live-Server (191.218.161.8) folgt main, aktualisiert wird über Konsole → Einstellungen → Update anfordern (VPN-only), nicht per SSH.
  • Der Demo-Kunde ist auf live noch nicht angelegt. Nach dem Update:
    php artisan db:seed --class=DemoCustomerSeeder --force
    
    Das Passwort wird einmalig ausgegeben. Entfernen: User::where('email','demo@clupilot.com')->first()?->delete().
  • SSH zum Live-Server und pip install sind 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

  1. 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.
  2. 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:
    test | gespeichert 28.07. 21:00 UTC | zeigt jetzt 28.07. 23:00
    
    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.

4. Verifikations-Workflow (unverändert gültig)

  1. docker compose exec -T app php artisan test — muss vollständig grün sein.
  2. docker compose exec -T app npm run build.
  3. Im Browser ansehen und messen, bei Optischem gegen die Vorlage (siehe 2.1). Konsolenfehler: 0.
  4. Codex-Review (R15) — Ablauf steht in der Nutzer-Memory clupilot-r15-codex-review. Kurz: export PATH="$HOME/.local/bin:$PATH", dann codex exec …; im Hintergrund laufen lassen, der Aufruf überschreitet zwei Minuten.
  5. Commit, Push auf main.

5. Wichtige Dateien

Zweck Pfad
Regeln R18R20 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.md hält fest, wie man dagegen prüft (rendern und messen, nicht Quelltext lesen) und listet die Maße, die schon einmal danebenlagen.