From c82ee91b9a8a5579e644ecb1a41c3d0b185fe5d53cda7f6873d5db08fcef933f Mon Sep 17 00:00:00 2001 From: Claude Dev Date: Mon, 9 Feb 2026 20:30:12 -0500 Subject: [PATCH] Enhance service detail page with type-specific sections and provisioning info Add service-type-specific detail cards (VPS, Dedicated, Game, Web Hosting), provisioning info accessor that filters sensitive credentials, and improved sidebar with service overview and quick actions. Co-Authored-By: Claude Sonnet 4.5 --- website/app/Models/Service.php | 51 ++ website/resources/ts/Pages/Services/Show.vue | 737 ++++++++++++++++++- website/resources/ts/types/index.ts | 1 + 3 files changed, 749 insertions(+), 40 deletions(-) diff --git a/website/app/Models/Service.php b/website/app/Models/Service.php index f9857fe..3ec0fdd 100644 --- a/website/app/Models/Service.php +++ b/website/app/Models/Service.php @@ -33,6 +33,24 @@ class Service extends Model 'auto_renew', ]; + /** + * The attributes that should be hidden for serialization. + * + * @var list + */ + protected $hidden = [ + 'credentials', + ]; + + /** + * The accessors to append to the model's array form. + * + * @var list + */ + protected $appends = [ + 'provisioning_info', + ]; + protected function casts(): array { return [ @@ -73,4 +91,37 @@ class Service extends Model { return $this->status === 'active'; } + + /** + * Get safe provisioning info excluding sensitive fields like passwords and keys. + * + * @return array|null + */ + public function getProvisioningInfoAttribute(): ?array + { + $credentials = $this->credentials; + + if (! is_array($credentials) || empty($credentials)) { + return null; + } + + $sensitiveKeys = [ + 'password', + 'secret', + 'token', + 'api_key', + 'api_secret', + 'private_key', + 'ssh_key', + 'ssh_password', + 'root_password', + 'admin_password', + 'database_password', + 'ftp_password', + ]; + + return collect($credentials) + ->reject(fn (mixed $value, string $key): bool => in_array(strtolower($key), $sensitiveKeys, true)) + ->toArray(); + } } diff --git a/website/resources/ts/Pages/Services/Show.vue b/website/resources/ts/Pages/Services/Show.vue index 5f8018d..eb140da 100644 --- a/website/resources/ts/Pages/Services/Show.vue +++ b/website/resources/ts/Pages/Services/Show.vue @@ -34,6 +34,10 @@ const controlPanelUrl = computed(() => { const isSuspended = computed(() => props.service.status === 'suspended') const isTerminated = computed(() => props.service.status === 'terminated') +const isVps = computed(() => props.service.service_type === 'vps') +const isDedicated = computed(() => props.service.service_type === 'dedicated') +const isGame = computed(() => props.service.service_type === 'game-server') +const isWebHosting = computed(() => props.service.service_type === 'web-hosting') const platformLabel = computed(() => { const labels: Record = { @@ -44,23 +48,79 @@ const platformLabel = computed(() => { } return labels[props.service.platform] ?? props.service.platform }) + +const serviceTypeLabel = computed(() => { + const labels: Record = { + vps: 'VPS', + dedicated: 'Dedicated Server', + 'game-server': 'Game Server', + 'web-hosting': 'Web Hosting', + } + return labels[props.service.service_type] ?? props.service.service_type +}) + +const serviceTypeIcon = computed(() => { + const icons: Record = { + vps: 'tabler-server', + dedicated: 'tabler-server-2', + 'game-server': 'tabler-device-gamepad-2', + 'web-hosting': 'tabler-world-www', + } + return icons[props.service.service_type] ?? 'tabler-server' +}) + +const provisioningEntries = computed>(() => { + const info = props.service.provisioning_info + if (!info || typeof info !== 'object') return [] + + return Object.entries(info).map(([key, value]) => ({ + key: key.replace(/_/g, ' '), + value: String(value), + })) +}) + +const connectionString = computed(() => { + const info = props.service.provisioning_info + if (!info) return null + + if (isGame.value) { + const port = info.port ?? info.game_port + if (props.service.ipv4_address && port) { + return `${props.service.ipv4_address}:${port}` + } + } + + return null +})