# Handoff — CluPilot (Stand 2026-07-25)

**Für:** frische Claude-Code-Session
**Art:** Zustands-/Fortsetzungs-Handoff
**Zuerst lesen:** dieses Dokument komplett, dann `git log --oneline -15`.

---

## 0. TL;DR

CluPilot = **Managed-Nextcloud-Cloud-Business** (fertige, betreute Firmen-Cloud). Laravel-App auf einer lokalen Dev-VM. Gebaut & gepusht sind bereits: **öffentliche Landingpage**, **helles Kundenportal** (Login/2FA + Dashboard + 6 Reiter) und eine **dunkle Admin-/Operator-Konsole** (`/admin`, 6 Sektionen) — alles mit Chart.js, 48 Pest-Tests grün, jeder Commit Codex-(R15)-clean, überall 0 Konsolenfehler.

**Nächster großer Schritt:** die **Provisionierungs-Engine v1.0** (aus Bestellung → laufende Nextcloud-Instanz). Design-Spec liegt beim Nutzer (nicht im Repo). Superpowers-Plugin (TDD) ist installiert.

---

## 1. Umgebung / Fakten

- **VM:** Debian, User **`nexxo`** (uid/gid **1000**), IP **`10.10.90.185`**, nur Docker (kein PHP/Composer/Node auf dem Host → alles im Container, **R8**).
- **Projekt:** `/home/nexxo/clupilot` (eigenes Git-Repo).
- **Remote:** `https://git.bave.dev/boban/CluPilotCloud.git`. **Token liegt in `.env`** (`GIT_ACCESS_TOKEN`, gitignored). Push: Token nur zur Push-Zeit inline injizieren, danach `git config branch.<b>.remote origin` und `.git/config` auf Token prüfen. **Nie** `.env`/Token committen.
- **Aktueller Branch:** `feat/portal-design` (alles hier, gepusht). `main` existiert lokal ohne Commits — Merge/PR steht aus.
- **Docker sudo-frei** (nexxo in docker-Gruppe). `sudo` bräuchte Passwort → vermeiden.

## 2. Stack

Laravel **13.8**, Livewire **3** (klassenbasiert, kein Volt — **R2**), **Tailwind v3** (bewusst von v4 zurückgebaut, Nutzer-Entscheid), Vite, **Laravel Fortify** (Login + TOTP-2FA), **Laravel Reverb**, **MariaDB 11.4**, **Redis**, **Chart.js 4**, **phpseclib**, wire-elements/modal, **Pest 4** (Tests). Fonts: **IBM Plex** self-hosted via @fontsource (**R14**, kein CDN).

## 3. Docker / Befehle (alles im Container, R8)

Helper `clupilot` (global via `~/.local/bin`, + `cp-*` Aliases):
```bash
clupilot up | down | restart [svc] | rebuild | ps | logs [svc] | health
clupilot artisan … | composer … | npm … | tinker | migrate | shell
```
Roh, falls nötig (User 1000, HOME für Composer-/npm-Cache):
```bash
docker compose run --rm --no-deps -u 1000:1000 -e HOME=/tmp app <cmd>
docker compose exec -T -u 1000:1000 -e HOME=/tmp app php artisan …
```
Services: `app` (php-fpm+nginx+**vite** via supervisor — EIN Container), `reverb`, `queue`, `scheduler`, `mariadb`, `redis`. App auf **:80**, Vite HMR **:5173** (`VITE_HMR_HOST=10.10.90.185`).

## 4. Was gebaut ist

**Öffentlich**
- `/` → **Landingpage** (`resources/views/landing.blade.php`) — Marketing, self-contained CSS/JS in `@verbatim`, System-Fonts, **absichtlich außerhalb** des App-Token-Systems. Login-Link → `route('login')`.
- `/legal/{impressum,datenschutz,agb,status}` → **Platzhalter** (`legal.blade.php`). ⚠️ **Echter Rechtstext fehlt** (Nutzer muss liefern — gesetzl. Pflicht vor Launch).

**Kundenportal (hell)** — `layouts/portal.blade.php` (Auth) + `layouts/portal-app.blade.php` (App-Shell)
- Auth: `/login`, `/two-factor-challenge` (Fortify, `views=false`, Seiten als full-page Livewire — **R1/R2**).
- `/dashboard` + Reiter `/cloud /users /backups /invoices /support` — je full-page Livewire (`app/Livewire/*`), Chart.js, Tabellen, lokalisiert DE/EN.

**Admin-Konsole (dunkel)** — `layouts/admin.blade.php` (`class="theme-admin"`), gate `is_admin`
- `/admin` + `/admin/{customers,instances,hosts,provisioning,revenue}` (`app/Livewire/Admin/*`). Middleware `['auth','admin']` (EnsureAdmin).

**Design-System** (Kern!)
- Tokens als CSS-Variablen: `resources/css/portal-tokens.css` (hell) + `resources/css/admin-tokens.css` (`.theme-admin` überschreibt alle Tokens dunkel). Tailwind mappt Tokens → Utilities (`tailwind.config.js`). **Nur Token-Utilities in Markup (R3), keine Hex.**
- Komponenten: `resources/views/components/ui/` — button, input, checkbox, alert, card, badge, stat-tile, otp-input, progress-stepper, nav-item, icon (Lucide, **keine Emoji im App-UI, R9**), **chart** (Alpine-Island).
- Chart-Island: PHP-Config-Array, Farben als `"token:accent"`/`"token:accent/0.12"` (in `resources/js/app.js` zur Laufzeit aus CSS-Vars aufgelöst, liest vom Element → theme-aware).

## 5. Verifikations-Workflow (jede Änderung)

1. **Tests:** `docker compose run --rm --no-deps -u 1000:1000 -e HOME=/tmp app ./vendor/bin/pest` (aktuell **48 grün**).
2. **R12 Browser (0 Konsolenfehler):** Prod-Assets bauen → `npm run build`, `supervisorctl stop vite` + `rm -f public/hot`, dann Puppeteer:
   ```bash
   docker run --rm --network host -v /pfad/probe.js:/home/pptruser/probe.js \
     -w /home/pptruser ghcr.io/puppeteer/puppeteer:latest node probe.js
   ```
   Probe: `page.on('console'/'pageerror'/'requestfailed')`, HTTP 200 + 0 Fehler pro Seite; Login-Flow via `#email`/`#password` + submit. **Danach `docker compose restart app`** (Vite/`public/hot` zurück). (Probe-Skripte liegen NICHT im Repo — bei Bedarf neu schreiben.)
3. **R15 Codex-Review (Pflicht vor „fertig"):**
   ```bash
   export CLAUDE_PLUGIN_ROOT=/home/nexxo/.claude/remote/plugins/e3a3a3c04ad124ae
   node "$CLAUDE_PLUGIN_ROOT/scripts/codex-companion.mjs" review "--scope working-tree --background"
   ```
   Ergebnis pollen: neuestes `/tmp/codex-companion/*/jobs/review-*.log` (`awk '/Final output/{f=1}f'`). `/codex:review` ist `disable-model-invocation` → Skript direkt aufrufen. **Working-tree-Scope** (nicht branch/base). Loop bis „no actionable regressions". Findings waren durchweg echt (a11y, i18n, Portabilität, Security) — ernst nehmen.

**Dev-Logins** (beide PW `password`): `admin@clupilot.local` (Admin, sieht `/admin`+`/dashboard`), `kunde@clupilot.local` (Kunde, `/admin`=403). Nach DB-Reset: `clupilot artisan db:seed`.

## 6. Gotchas (teuer gelernt — beachten!)

- **`@js()` wird NICHT kompiliert in `@click`/Attributen von Blade-Komponenten** (`<x-ui.button>`) → landet literal, bricht Alpine. Lösung: `@js()` in `x-data` eines *plain* Elements, Buttons referenzieren die Variable.
- **Test-Isolation:** `env_file: .env` in docker-compose wurde **entfernt** — es injizierte Dev-DB-Config als echte Env-Vars und überschrieb phpunit-`force` → Tests liefen gegen die **Dev-MariaDB** und wischten sie. Laravel liest `.env` von der Platte; nur Vite-Vars sind explizit gesetzt. Tests nutzen jetzt sqlite `:memory:` (`phpunit.xml` mit `force="true"`).
- **CSRF in Tests:** `$this->post()` sendet kein Token → 419. In Auth-Tests Session-Token setzen: `withSession(['_token'=>'t'])->post(..., ['_token'=>'t', ...])`.
- **Großes rohes HTML/CSS/JS** in Blade → `@verbatim … @endverbatim` (sonst zerlegt Blade `@keyframes`/`${}`/`{{ }}`).
- **AA-Kontrast:** `#f97316` (accent) fällt für weißen/kleinen Text durch AA → Buttons/Links nutzen `accent-active`/`accent-text` (dunkleres Orange hell / helleres Orange dunkel). `--accent` nur Deko/Rahmen/Tints.
- **Locale:** Fixture-Daten immer locale-aware (`Carbon::isoFormat`, `Number::format/currency`), sonst mischt EN deutsche Formate — Codex flaggt das zuverlässig.
- **Vite reload:** `docker compose restart` liest `env_file`/Config **nicht** neu → `docker compose up -d app` (recreate) wenn Env/Compose geändert.

## 7. Offene Punkte

| Prio | Punkt |
|---|---|
| Hoch | **Echter Rechtstext** (Impressum/Datenschutz/AGB) — Nutzer liefert; ich erfinde keine Rechtsangaben |
| Mittel | Merge `feat/portal-design` → `main` (Gitea-PR) |
| Mittel | **Provisionierungs-Engine v1.0** (s. u.) |
| Niedrig | Multi-Domain-Split `www.` / `app.` / `admin.clupilot.com` (Nutzer legt Domains an) — aktuell pfad-basiert |
| Niedrig | v1.1-Auth: Passwort-Reset / „Passwort vergessen" (in Fortify bewusst deaktiviert) |
| Niedrig | Breiteres Komponenten-Kit (Modal-Nutzung, Tabs, Toast-Komponente, Skeleton, Tooltip …) falls gebraucht |

## 8. Nächster großer Block: Provisionierungs-Engine v1.0

**Ziel:** aus bezahlter Bestellung vollautomatisch eine laufende, gesicherte, überwachte Nextcloud-Instanz. Modell = **DB-State-Machine + Tick-Orchestrator** (kein Monolith-Job).

- **Design-Spec liegt beim Nutzer** (war als „Engine-v1.0-Handoff" vorhanden, ist NICHT im Repo). **Vor Baubeginn anfordern.**
- **TDD** (Superpowers `test-driven-development` ist ab dieser Session verfügbar). ProxmoxClient in Step-Tests mocken.
- Grob: Migrations/Models (`customers, orders, hosts, instances, provisioning_runs, provisioning_step_events, run_resources, …`) → Orchestrator-Kern (Step-Interface, StepResult advance/retry/fail, AdvanceRunJob, Per-Run-Lock, Timeout, Tick im `scheduler`) → **ProxmoxClient** (NEU, REST/UPID-Polling) → 15 idempotente Kunden-Pipeline-Steps → Stripe-Webhook (Idempotency-Key) → Livewire-Live-Fortschritt via Reverb.
- **Nicht anfassen/vermischen:** Alt-Domäne existiert hier nicht (frischer Build) — Engine ist additiv. Panel-Reverb/Queue/Scheduler sind da und wiederverwendbar.
- Die Admin-`/admin/provisioning`-Seite ist aktuell **Fixture-View** → später an echte `provisioning_runs`/`provisioning_step_events` binden (live via Reverb).

## 9. Regeln (Kurzfassung — verbindlich)

R1/R2 full-page **klassenbasierte Livewire** als Routen (kein Volt, kein Page-Controller) · R3 nur `@theme`/Token-Utilities, kein Hex · R4 keine Inline-Styles außer Progress-`width` · R5 destruktiv → wire-elements/modal · R6 Ordner-Map · R7 responsive 375/768/1280, Touch ≥44px · R8 alles im Container · R9 UI-Copy DE, **keine Emoji** (Status via Farbe/Dots/Pills) · R10 Design-System wiederverwenden · R11 URLs per **UUID** (nicht Integer-PK) · R12 **Browser-verifiziert HTTP 200 + 0 Konsolenfehler** · R13 Routen-Pfade/Namen **englisch** · R14 Fonts **self-hosted** · R15 **Codex-Review clean** vor „fertig" · R16 alles lokalisiert **DE+EN** (identische Keys) · R17 Blade-`@php`-Block-Regeln · **R18 Icon-Größe und Zeilenumbruch** (siehe `CLAUDE.md`).
> Volltext `rules.md`/`CLAUDE.md` hat der Nutzer (nicht im Repo). Bei Konflikt: STOP & fragen.

## 10. Session-Memory

Persistente Notizen unter `~/.claude/projects/-home-nexxo/memory/` (MEMORY.md + `clupilot-project-state`, `clupilot-portal-progress`, `clupilot-r15-codex-review`) — beim Start werden die relevanten automatisch eingespielt.
