diff --git a/app/Livewire/Admin/Overview.php b/app/Livewire/Admin/Overview.php index 71183b3..0e78be4 100644 --- a/app/Livewire/Admin/Overview.php +++ b/app/Livewire/Admin/Overview.php @@ -42,17 +42,21 @@ class Overview extends Component ], ], 'revenueChart' => [ - 'type' => 'bar', + 'type' => 'line', 'data' => [ 'labels' => $months, 'datasets' => [[ 'label' => 'MRR', 'data' => [4100, 4460, 4900, 5300, 5600, 6050, 6300, 6700, 7050, 7300, 7620, 7842], - 'backgroundColor' => 'token:accent/0.85', 'borderRadius' => 3, + 'borderColor' => 'token:accent', + 'fill' => true, + 'tension' => 0.35, + 'pointRadius' => 0, + 'borderWidth' => 2, ]], ], 'options' => [ - 'scales' => ['x' => ['grid' => ['display' => false]], 'y' => ['grid' => ['color' => 'token:border']]], + 'scales' => ['x' => ['grid' => ['display' => false]], 'y' => ['beginAtZero' => true, 'grid' => ['color' => 'token:border']]], 'plugins' => ['legend' => ['display' => false]], ], ], diff --git a/app/Livewire/Backups.php b/app/Livewire/Backups.php index 4e4bc7c..68bf95b 100644 --- a/app/Livewire/Backups.php +++ b/app/Livewire/Backups.php @@ -32,14 +32,17 @@ class Backups extends Component 'rows' => $rows, 'lastTest' => $date('2026-07-01', 'LL'), 'sizeChart' => [ - 'type' => 'bar', + 'type' => 'line', 'data' => [ 'labels' => array_map(fn ($i) => $date(Carbon::parse('2026-07-24')->subDays(13 - $i)->toDateString(), 'D.'), range(0, 13)), 'datasets' => [[ 'label' => 'GB', 'data' => $sizes, - 'backgroundColor' => 'token:accent/0.85', - 'borderRadius' => 4, + 'borderColor' => 'token:accent', + 'fill' => true, + 'tension' => 0.35, + 'pointRadius' => 0, + 'borderWidth' => 2, ]], ], 'options' => [ diff --git a/app/Livewire/Billing.php b/app/Livewire/Billing.php index 0e91707..6be5376 100644 --- a/app/Livewire/Billing.php +++ b/app/Livewire/Billing.php @@ -91,6 +91,7 @@ class Billing extends Component 'current' => $plans[$currentKey] ?? [], 'instance' => $instance, 'plans' => $plans, + 'features' => (array) config('provisioning.plan_features'), 'upgrades' => $upgrades, 'storage' => (array) config('provisioning.storage_addon'), 'addons' => (array) config('provisioning.addons'), diff --git a/app/Livewire/Cloud.php b/app/Livewire/Cloud.php index a4976fc..159a29d 100644 --- a/app/Livewire/Cloud.php +++ b/app/Livewire/Cloud.php @@ -25,8 +25,8 @@ class Cloud extends Component 'php' => 'PHP 8.3 · MariaDB 11.4', 'storageUsed' => 235, 'storageQuota' => 500, - 'ram' => '8 GB', - 'vcpu' => '4 vCPU', + 'seats' => __('billing.seats_count', ['count' => 25]), + 'performance' => __('billing.perf.enhanced'), ], 'storageChart' => [ 'type' => 'line', diff --git a/app/Livewire/Invoices.php b/app/Livewire/Invoices.php index 0354bd6..38ae1bd 100644 --- a/app/Livewire/Invoices.php +++ b/app/Livewire/Invoices.php @@ -32,14 +32,17 @@ class Invoices extends Component 'nextCharge' => $date('2026-08-01'), 'nextAmount' => $eur(198), 'spendChart' => [ - 'type' => 'bar', + 'type' => 'line', 'data' => [ 'labels' => $months, 'datasets' => [[ 'label' => 'EUR', 'data' => [179, 179, 198, 198, 198], - 'backgroundColor' => 'token:accent/0.85', - 'borderRadius' => 4, + 'borderColor' => 'token:accent', + 'fill' => true, + 'tension' => 0.35, + 'pointRadius' => 0, + 'borderWidth' => 2, ]], ], 'options' => [ diff --git a/config/provisioning.php b/config/provisioning.php index 3bdb062..2382c97 100644 --- a/config/provisioning.php +++ b/config/provisioning.php @@ -47,11 +47,14 @@ return [ ], // Plan → resource sizing + monthly price + Nextcloud blueprint template VMID. + // quota_gb/disk_gb/ram_mb/cores drive infrastructure placement and are + // ADMIN-ONLY. Customers compare seats, storage, performance class and + // features — never raw vCPU/RAM (see docs/specs/2026-07-25-portal-d-*). 'plans' => [ - 'start' => ['quota_gb' => 100, 'disk_gb' => 120, 'ram_mb' => 4096, 'cores' => 2, 'price_cents' => 4900, 'template_vmid' => 9000], - 'team' => ['quota_gb' => 500, 'disk_gb' => 540, 'ram_mb' => 8192, 'cores' => 4, 'price_cents' => 17900, 'template_vmid' => 9000], - 'business' => ['quota_gb' => 1000, 'disk_gb' => 1050, 'ram_mb' => 16384, 'cores' => 8, 'price_cents' => 39900, 'template_vmid' => 9000], - 'enterprise' => ['quota_gb' => 2000, 'disk_gb' => 2100, 'ram_mb' => 32768, 'cores' => 16, 'price_cents' => 79900, 'template_vmid' => 9000], + 'start' => ['quota_gb' => 100, 'disk_gb' => 120, 'ram_mb' => 4096, 'cores' => 2, 'seats' => 5, 'performance' => 'standard', 'price_cents' => 4900, 'template_vmid' => 9000], + 'team' => ['quota_gb' => 500, 'disk_gb' => 540, 'ram_mb' => 8192, 'cores' => 4, 'seats' => 25, 'performance' => 'enhanced', 'price_cents' => 17900, 'template_vmid' => 9000], + 'business' => ['quota_gb' => 1000, 'disk_gb' => 1050, 'ram_mb' => 16384, 'cores' => 8, 'seats' => 100, 'performance' => 'high', 'price_cents' => 39900, 'template_vmid' => 9000], + 'enterprise' => ['quota_gb' => 2000, 'disk_gb' => 2100, 'ram_mb' => 32768, 'cores' => 16, 'seats' => 500, 'performance' => 'dedicated', 'price_cents' => 79900, 'template_vmid' => 9000], ], // Extra storage add-on (per unit) and the add-on catalogue (labels in lang/*/billing.php). @@ -62,6 +65,15 @@ return [ 'collabora_pro' => ['price_cents' => 1900], ], + // Customer-facing feature bullets per plan (labels in lang/*/billing.php → + // billing.feature.). Cumulative tiers: each plan lists its own bullets. + 'plan_features' => [ + 'start' => ['managed_updates', 'daily_backups', 'monitoring', 'subdomain'], + 'team' => ['managed_updates', 'daily_backups', 'monitoring', 'subdomain', 'office', 'branding', 'priority_support'], + 'business' => ['managed_updates', 'daily_backups', 'monitoring', 'custom_domain', 'office', 'branding', 'priority_support', 'extended_retention', 'audit_log'], + 'enterprise' => ['managed_updates', 'daily_backups', 'monitoring', 'custom_domain', 'office', 'branding', 'premium_sla', 'extended_retention', 'audit_log', 'onboarding'], + ], + 'dns' => [ 'provider' => 'hetzner', 'token' => env('HETZNER_DNS_TOKEN', ''), diff --git a/lang/de/billing.php b/lang/de/billing.php index 9a385de..8ecbe98 100644 --- a/lang/de/billing.php +++ b/lang/de/billing.php @@ -7,10 +7,33 @@ return [ 'per_month' => 'pro Monat', 'month_short' => 'Mon.', 'storage' => 'Speicher', - 'cores' => 'vCPU', - 'ram' => 'RAM', + 'seats' => 'Benutzer', + 'seats_count' => 'bis zu :count Benutzer', + 'performance' => 'Leistung', 'status' => 'Status', 'status_active' => 'Aktiv', + + 'perf' => [ + 'standard' => 'Standard', + 'enhanced' => 'Erweitert', + 'high' => 'Hoch', + 'dedicated' => 'Dediziert', + ], + + 'feature' => [ + 'managed_updates' => 'Verwaltete Updates', + 'daily_backups' => 'Tägliche Backups', + 'monitoring' => 'Monitoring rund um die Uhr', + 'subdomain' => 'Eigene CluPilot-Subdomain', + 'custom_domain' => 'Eigene Domain', + 'office' => 'Office-Integration (Collabora)', + 'branding' => 'Eigenes Branding', + 'priority_support' => 'Priority-Support', + 'premium_sla' => 'Premium-SLA', + 'extended_retention' => 'Längere Backup-Aufbewahrung', + 'audit_log' => 'Audit-Log', + 'onboarding' => 'Onboarding-Unterstützung', + ], 'pending_note' => ':count Kauf vorgemerkt — wird nach Zahlung aktiviert.', 'upgrade_title' => 'Upgrade', 'upgrade_cta' => 'Upgraden', diff --git a/lang/de/cloud.php b/lang/de/cloud.php index 0839f70..d48af69 100644 --- a/lang/de/cloud.php +++ b/lang/de/cloud.php @@ -13,7 +13,8 @@ return [ 'version' => 'Version', 'current' => 'aktuell', 'runtime' => 'Laufzeit', - 'resources' => 'Ressourcen', + 'seats' => 'Benutzer', + 'performance' => 'Leistung', 'storage' => 'Speicher', 'storage_growth' => 'Speicherverlauf', 'storage_growth_sub' => 'Belegter Speicher der letzten 12 Wochen.', diff --git a/lang/en/billing.php b/lang/en/billing.php index b18fd32..e698060 100644 --- a/lang/en/billing.php +++ b/lang/en/billing.php @@ -7,10 +7,33 @@ return [ 'per_month' => 'per month', 'month_short' => 'mo', 'storage' => 'Storage', - 'cores' => 'vCPU', - 'ram' => 'RAM', + 'seats' => 'Users', + 'seats_count' => 'up to :count users', + 'performance' => 'Performance', 'status' => 'Status', 'status_active' => 'Active', + + 'perf' => [ + 'standard' => 'Standard', + 'enhanced' => 'Enhanced', + 'high' => 'High', + 'dedicated' => 'Dedicated', + ], + + 'feature' => [ + 'managed_updates' => 'Managed updates', + 'daily_backups' => 'Daily backups', + 'monitoring' => 'Round-the-clock monitoring', + 'subdomain' => 'Own CluPilot subdomain', + 'custom_domain' => 'Custom domain', + 'office' => 'Office integration (Collabora)', + 'branding' => 'Custom branding', + 'priority_support' => 'Priority support', + 'premium_sla' => 'Premium SLA', + 'extended_retention' => 'Longer backup retention', + 'audit_log' => 'Audit log', + 'onboarding' => 'Onboarding assistance', + ], 'pending_note' => ':count purchase pending — activated after payment.', 'upgrade_title' => 'Upgrade', 'upgrade_cta' => 'Upgrade', diff --git a/lang/en/cloud.php b/lang/en/cloud.php index 89e1f2a..9122fc4 100644 --- a/lang/en/cloud.php +++ b/lang/en/cloud.php @@ -13,7 +13,8 @@ return [ 'version' => 'Version', 'current' => 'up to date', 'runtime' => 'Runtime', - 'resources' => 'Resources', + 'seats' => 'Users', + 'performance' => 'Performance', 'storage' => 'Storage', 'storage_growth' => 'Storage over time', 'storage_growth_sub' => 'Used storage over the last 12 weeks.', diff --git a/resources/js/app.js b/resources/js/app.js index 623a742..db585de 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -33,6 +33,45 @@ function resolveTokens(node, css) { return node; } +// Turn any resolved colour (#hex, rgb(), rgba()) into an rgba() with the given alpha. +function toRgba(color, alpha) { + if (typeof color !== 'string') return color; + if (color.startsWith('#')) { + let hex = color.slice(1); + if (hex.length === 3) hex = hex.split('').map((c) => c + c).join(''); + const r = parseInt(hex.slice(0, 2), 16); + const g = parseInt(hex.slice(2, 4), 16); + const b = parseInt(hex.slice(4, 6), 16); + return `rgba(${r}, ${g}, ${b}, ${alpha})`; + } + const m = color.match(/rgba?\(([^)]+)\)/); + if (m) { + const [r, g, b] = m[1].split(',').map((n) => n.trim()); + return `rgba(${r}, ${g}, ${b}, ${alpha})`; + } + return color; +} + +// Every filled line dataset gets a vertical gradient from its line colour to +// transparent — the "colour fades to nothing under the line" look (R3/token-driven). +function applyLineGradients(cfg) { + const isLine = (ds) => (ds.type || cfg.type) === 'line'; + for (const ds of cfg.data?.datasets ?? []) { + if (!isLine(ds) || !ds.fill) continue; + const base = Array.isArray(ds.borderColor) ? ds.borderColor[0] : ds.borderColor; + if (!base) continue; + ds.backgroundColor = (ctx) => { + const { chart } = ctx; + const area = chart.chartArea; + if (!area) return toRgba(base, 0.18); // first paint before layout + const g = chart.ctx.createLinearGradient(0, area.top, 0, area.bottom); + g.addColorStop(0, toRgba(base, 0.32)); + g.addColorStop(1, toRgba(base, 0)); + return g; + }; + } +} + document.addEventListener('alpine:init', () => { // ── Segmented OTP input ────────────────────────────────────────────── window.Alpine.data('otpInput', ({ length = 6 } = {}) => ({ @@ -82,6 +121,7 @@ document.addEventListener('alpine:init', () => { Chart.defaults.borderColor = css.getPropertyValue('--border').trim() || '#e4e7ec'; const cfg = resolveTokens(config, css); + applyLineGradients(cfg); cfg.options = { responsive: true, maintainAspectRatio: false, diff --git a/resources/views/livewire/billing.blade.php b/resources/views/livewire/billing.blade.php index 0407cfa..71466e2 100644 --- a/resources/views/livewire/billing.blade.php +++ b/resources/views/livewire/billing.blade.php @@ -22,8 +22,8 @@
@foreach ([ ['billing.storage', ($current['quota_gb'] ?? 0).' GB'], - ['billing.cores', $current['cores'] ?? '—'], - ['billing.ram', isset($current['ram_mb']) ? round($current['ram_mb'] / 1024).' GB' : '—'], + ['billing.seats', $current['seats'] ?? '—'], + ['billing.performance', __('billing.perf.'.($current['performance'] ?? 'standard'))], ['billing.status', __('billing.status_active')], ] as [$label, $value])
@@ -48,11 +48,22 @@
@foreach ($upgrades as $key) @php $p = $plans[$key]; @endphp -
-

{{ __('billing.plan.'.$key) }}

-

{{ $p['quota_gb'] }} GB · {{ $p['cores'] }} vCPU · {{ round($p['ram_mb'] / 1024) }} GB RAM

+
+
+

{{ __('billing.plan.'.$key) }}

+ {{ __('billing.perf.'.($p['performance'] ?? 'standard')) }} +
+

{{ $p['quota_gb'] }} GB · {{ __('billing.seats_count', ['count' => $p['seats'] ?? 0]) }}

+
    + @foreach (array_slice($features[$key] ?? [], 0, 4) as $f) +
  • + + {{ __('billing.feature.'.$f) }} +
  • + @endforeach +

{{ $eur($p['price_cents']) }} / {{ __('billing.month_short') }}

- + {{ __('billing.upgrade_cta') }}
@@ -70,21 +81,21 @@

{{ __('billing.storage_title') }}

{{ __('billing.storage_body', ['gb' => $storage['gb'], 'price' => $eur($storage['price_cents'])]) }}

- + {{ __('billing.storage_cta', ['gb' => $storage['gb']]) }}
{{-- Add-ons --}} @foreach ($addons as $key => $addon) -
+

{{ __('billing.addon.'.$key.'.name') }}

{{ __('billing.addon.'.$key.'.desc') }}

{{ $eur($addon['price_cents']) }} / {{ __('billing.month_short') }}

- + {{ __('billing.addon_cta') }}
diff --git a/resources/views/livewire/cloud.blade.php b/resources/views/livewire/cloud.blade.php index c11ecec..32c5aeb 100644 --- a/resources/views/livewire/cloud.blade.php +++ b/resources/views/livewire/cloud.blade.php @@ -20,10 +20,11 @@
@foreach ([ ['cloud.plan', $instance['plan']], + ['cloud.seats', $instance['seats']], + ['cloud.performance', $instance['performance']], ['cloud.location', $instance['location']], ['cloud.version', $instance['version'].' · '.__('cloud.current')], ['cloud.runtime', $instance['php']], - ['cloud.resources', $instance['vcpu'].' · '.$instance['ram']], ['cloud.storage', $storageLabel], ] as [$key, $val])