Write the handoff, and rescue the approved templates into the repo
The three templates the design was signed off against lived in a session-scoped scratchpad and would have disappeared with the session — the one artefact the whole conversion is measured against. They are in docs/design/ now, with the measurements that were got wrong at least once written down beside them, and a note that the way to check is to render and measure rather than to read the source. That distinction already settled one disagreement: the source said the metric grid used a 20px gap, rendered it is 14px. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>feat/mailboxes tested-20260727-1825-96dac23
parent
9b8d5dfd1e
commit
96dac236e8
|
|
@ -0,0 +1,37 @@
|
|||
# Freigegebene Vorlagen
|
||||
|
||||
Diese drei Dateien sind der **abgenommene** Entwurf für Startseite, Kundenportal
|
||||
und Konsole. Sie sind der Maßstab, gegen den umgesetzt wird — nicht eine
|
||||
Inspiration.
|
||||
|
||||
Sie lagen ursprünglich in einem sitzungsgebundenen Scratchpad und wären mit der
|
||||
Sitzung verschwunden.
|
||||
|
||||
## Wie man dagegen prüft
|
||||
|
||||
Nicht im Quelltext lesen — **rendern und messen**. Das hat schon einmal einen
|
||||
Streit entschieden: eine Quelltext-Prüfung behauptete 20px Rasterabstand,
|
||||
gerendert sind es 14px.
|
||||
|
||||
```bash
|
||||
cp docs/design/tpl-portal.html public/__tpl.html # danach wieder löschen
|
||||
```
|
||||
|
||||
Dann im Browser beide Seiten mit `getComputedStyle` abmessen und Spalte gegen
|
||||
Spalte vergleichen.
|
||||
|
||||
## Maße, die schon einmal danebenlagen
|
||||
|
||||
| | Vorlage |
|
||||
|---|---|
|
||||
| Rasterabstand | 14px |
|
||||
| Kartenpolsterung | 18px 20px |
|
||||
| Kartenradius | 16px |
|
||||
| Beschriftung | 11.5px, `.07em`, Mono |
|
||||
| Wert | 25px / 600 / `-.02em` / 9px oben |
|
||||
| Einheit | 13px / 450 |
|
||||
| Fußzeile | 12px / 6px oben |
|
||||
| h1 | 30px / 700 / `-.03em` / 1.12 — unter 900px 23px |
|
||||
| Badge | 12px / 500, 4px 11px, 7px Abstand → 29px hoch |
|
||||
| Button | 14px / 600, 0 18px, `min-height` 40px, Radius 10px |
|
||||
| Spalten | 4 → 2 ab 1100px → 1 ab 560px |
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,163 @@
|
|||
# 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:
|
||||
|
||||
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 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.md` hält fest, **wie** man dagegen prüft (rendern und messen, nicht Quelltext lesen) und listet die Maße, die schon einmal danebenlagen.
|
||||
Loading…
Reference in New Issue