# Phase D — Service framing, redesign & CRUD completeness

**Status:** approved (brainstorming + Codex consultation 2026-07-25).
**Branch:** `feat/portal-design`. Rules R1–R17 apply.

Follow-up to the second UX review. Codex consultation stored decisions; user confirmed:
- Plans keep current **storage** (Start 100 GB / Team 500 GB / Business 1 TB / Enterprise 2 TB); **hide vCPU/RAM** from customers; add **seats** (5 / 25 / 100 / 500 — Enterprise is limited too) and a **performance class** (Standard/Enhanced/High/Dedicated) + feature list.
- **Cancellation:** effective at **end of billing term, no grace period, irreversible once set**; at term end the customer receives a **finished data export**, then deprovision. Account closure only when no active packages.
- Build **D1 then D2 straight through**; report each phase when green.

## D1 — Fixes & redesign (low-risk, mostly UI)

1. **Billing button bug.** `wire:loading.attr="disabled"` lacks `wire:target`, so any purchase spins/disables all buttons. Fix: scope `wire:target` per button + add `wire:key` on loop items.
2. **Service framing.** `config/provisioning.php` plans gain `seats` (5/25/100/500) and `performance` (standard/enhanced/high/dedicated). Customer views (Billing, Dashboard, Cloud) show storage · seats · performance · features — never vCPU/RAM. A "Technische Infos" block may show Nextcloud/PHP version, region, service URL, backup location. Admin views keep raw specs.
3. **Charts → gradient line.** Extend the chart island (`resources/js/app.js`) to build a canvas linear gradient (line colour → transparent) for `fill` line datasets. Convert the three time-series bar charts (Admin Overview MRR, Backups 14-day, Invoices spend) to gradient line. Admin Overview "Host-Auslastung" is categorical (per-host) — redesigned as capacity bars inside the host section, not a misleading line.
4. **Dashboard/MRR layout.** Chart must fill its card (min-height / aspect) so the box is not larger than the chart; intuitive ordering. `/frontend-design`.
5. **Host list scaling.** Replace the fixed cards with a dense, searchable/filterable (by datacenter + status) table/grid that scales to many hosts. `/frontend-design`.
6. **Host detail redesign.** Show health from `last_seen_at` (online/stale/offline), status, capacity breakdown (committed/available/reserve), RAM, CPU, hosted-instances list, version, node, mgmt/WG IP, current run + events. `/frontend-design`.
7. **Datacenter edit + delete (guarded).** Edit name/location (code immutable once referenced); delete only when 0 hosts, else block; activate/deactivate already exists.
8. **Admin CRUD audit fixes.** Host: edit `reserve_pct` + maintenance/drain toggle. Customer: edit profile + suspend. Instances/orders/runs stay guarded/append-only.

## D2 — Customer self-service (new pages)

9. **Settings** (`/settings`): edit company/billing profile (company name, contact, billing address, VAT ID, phone); **cancel package** (typed-confirm modal → schedules at term end, irreversible, status `cancellation_scheduled`, export produced at term end — mocked); **close account** (guarded: no active packages; sets `closed_at`; retains financial records).
10. **Branding** (in Settings): logo upload (PNG/WebP, MIME-validated, ≤2 MB), display name, primary + accent colour; stored at customer level, `NULL` → CluPilot defaults via a resolver; **snapshot resolved branding into the provisioning run context** at start so retries stay deterministic. Live preview.
11. **Users/seats** (`/users`): invite / edit role / revoke / resend against the plan seat limit; guard: cannot revoke the last owner. Seats are portal-managed records (invites mocked).

## Data model (additive migrations)

- `customers`: `billing_address` (text, null), `vat_id` (null), `phone` (null), `contact_name` (null), `brand_display_name` (null), `brand_logo_path` (null), `brand_primary_color` (null), `brand_accent_color` (null), `closed_at` (timestamp, null).
- `instances`: `cancel_requested_at` (null), `service_ends_at` (null); status gains `cancellation_scheduled`.
- `seats` table: `id`, `customer_id` fk, `email`, `name` (null), `role` (owner/admin/member/readonly), `status` (invited/active/revoked), `invited_at`, timestamps; unique(`customer_id`,`email`).
- Provisioning: `StartCustomerProvisioning` writes `context['branding']` = resolved snapshot.

## CRUD verdicts (Codex-guided)

| Entity | Verdict |
|---|---|
| Datacenters | create · edit · activate/deactivate · **delete only if unused** |
| Hosts | create · edit (reserve, drain/maintenance) · delete only if no instances/active runs |
| Customers | edit profile · suspend · close (guarded) — no routine hard-delete |
| Instances | cancel (scheduled) · deprovision via lifecycle — identity/history immutable |
| Orders/invoices | orders cancellable before fulfilment; **finalized invoices immutable** |
| Seats | invite · edit role · revoke · resend — cannot remove last owner |
| Provisioning runs | append-only; pending cancellable, failed retryable (new attempt) |
| Addons/subscriptions | add · schedule change · cancel — preserve price snapshots |

## Verification (per phase + final)
Pest green · DE+EN key parity (R16) · R12 Puppeteer 0 console errors on new/changed pages (production build) · R15 Codex loop `--base 80348d6` until clean · migrate/seed dev · push.
